Python笔记之项目依赖管理与模块运行机制详解:从 requirements.txt 到 python -m 和包入口

在这里插入图片描述

code review!

文章目录

1.1 requirements.txt 是什么

在 Python 项目中,requirements.txt 是一个常见的依赖声明文件,用来记录项目需要安装的第三方 Python 包。

它的核心作用是:让不同环境安装一致的依赖,例如本地电脑、同事电脑、测试服务器、生产服务器等。

常见内容如下:

flask==3.0.2
requests>=2.31.0
numpy<=1.26.4
pandas

1.2 常见版本写法

flask==3.0.2

表示精确锁定版本,推荐在生产环境中使用。

requests>=2.31.0

表示安装大于或等于指定版本的包。

numpy<=1.26.4

表示安装小于或等于指定版本的包。

pandas

表示安装最新可用版本。

生产环境中不推荐不写版本号,因为未来包更新后可能导致项目行为变化。

1.3 生成 requirements.txt

在当前 Python 环境中导出已安装的包:

pip freeze > requirements.txt

更推荐写法:

python -m pip freeze > requirements.txt

需要注意,pip freeze 会导出当前环境中的所有已安装包。如果没有使用虚拟环境,它可能会把很多与当前项目无关的包也导出。

1.4 根据 requirements.txt 安装依赖

在新环境中安装依赖:

pip install -r requirements.txt

更推荐写法:

python -m pip install -r requirements.txt

pip 会读取 requirements.txt 中的每一行,并安装对应的包。

1.5 使用虚拟环境管理依赖

在正式项目中,强烈建议先创建并激活虚拟环境,再安装依赖。

创建虚拟环境:

python -m venv venv

Windows 激活虚拟环境:

venv\Scripts\activate

macOS / Linux 激活虚拟环境:

source venv/bin/activate

激活后再安装依赖:

python -m pip install flask requests

然后导出:

python -m pip freeze > requirements.txt

这样生成的 requirements.txt 会更干净,只包含当前虚拟环境里的依赖。

1.6 pip freeze 的局限

pip freeze 会把直接依赖和间接依赖全部导出。

例如你只安装了 flask,但 Flask 依赖的 WerkzeugJinja2click 等包也会被一起写进 requirements.txt

这有好处,也有缺点。

好处是环境可复现性强。

缺点是文件会变长,并且不容易看出哪些包是项目主动依赖的。

1.7 其他依赖管理工具

如果想只根据项目代码中的 import 语句生成依赖文件,可以了解 pipreqs

pipreqs . --force

如果项目更复杂,也可以使用现代依赖管理工具,例如 Poetry、Pipenv 等。

它们通常使用 pyproject.toml 或专门的锁文件来管理依赖,比传统 requirements.txt 更适合大型项目。

2. pip installpython -m pip install

2.1 二者的共同点

在很多简单场景下:

pip install requests

和:

python -m pip install requests

都可以安装第三方包。

但它们定位 Python 环境的方式不同。

2.2 pip install 的执行方式

当执行:

pip install requests

系统会在环境变量 PATH 中查找名为 pip 的可执行文件。

如果电脑上只有一个 Python,并且环境配置正确,通常没有问题。

但如果存在多个 Python 版本、多个虚拟环境,或者系统 Python 与项目 Python 并存,就可能调用到错误的 pip

例如电脑里同时有 Python 3.10 和 Python 3.12:

pip install requests

这个命令不一定会把 requests 安装到你实际运行项目的那个 Python 环境里。

2.3 python -m pip install 的执行方式

当执行:

python -m pip install requests

含义是:使用当前这个 python 解释器,运行它环境中的 pip 模块,并安装包。

也就是说,包会安装到当前 python 对应的环境中。

如果你明确使用 Python 3.12,可以写:

python3.12 -m pip install requests

这样可以更明确地把包安装到 Python 3.12 对应的环境中。

2.4 为什么推荐 python -m pip

推荐使用:

python -m pip install 包名

主要原因有三个。

第一,可以避免多个 Python 环境导致的安装错位。

第二,适合写进文档和自动化脚本,可读性和确定性更高。

第三,在某些系统上更新 pip 自身时更稳妥。

例如更新 pip

python -m pip install --upgrade pip

比直接执行下面命令更推荐:

pip install --upgrade pip

2.5 Linux/macOS 上避免滥用 sudo pip

不推荐这样安装包:

sudo pip install requests

这样可能会污染系统 Python 环境,影响系统工具或其他项目。

更安全的做法是使用虚拟环境:

python -m venv venv
source venv/bin/activate
python -m pip install requests

如果确实要安装到当前用户目录,可以使用:

python -m pip install --user requests

2.6 总结建议

命令 推荐程度 说明
pip install 包名 一般推荐 在已经明确激活虚拟环境时可以使用
python -m pip install 包名 更推荐 更能确保包安装到当前 Python 对应的环境中
python3.12 -m pip install 包名 多版本场景推荐 明确指定 Python 版本
sudo pip install 包名 不推荐 容易污染系统 Python 环境

当遇到明明安装了包,但运行代码仍然报错:

ModuleNotFoundError

可以优先检查是否安装到了错误的 Python 环境中,并尝试使用:

python -m pip install 包名

3. python -m 的含义和用法

3.1 python -m 是什么

python -m 的作用是:把一个模块或包当作脚本运行。

基本语法是:

python -m 模块名

或者:

python -m 包名

这里的 -m 可以理解为 module,表示后面跟的是模块名或包名,而不是普通的文件路径。

3.2 python -m 如何查找模块

执行:

python -m pip

时,Python 会在当前解释器的模块搜索路径中查找 pip 模块。

这个搜索路径通常包括:

  1. 当前工作目录
  2. Python 标准库
  3. 当前环境中安装的第三方包
  4. 其他 sys.path 中的路径

找到对应模块后,Python 会以脚本方式运行它。

3.3 常见使用场景

3.3.1 使用当前 Python 环境安装包

python -m pip install requests

这表示使用当前 Python 解释器对应的 pip 安装 requests

3.3.2 创建虚拟环境

python -m venv venv

这表示运行 Python 标准库中的 venv 模块,在当前目录创建一个名为 venv 的虚拟环境。

3.3.3 启动临时 HTTP 文件服务器

python -m http.server 8000

这会在当前目录启动一个简单的 HTTP 服务。

访问地址通常是:

http://localhost:8000

3.3.4 运行代码格式化工具

如果安装了 black,可以这样运行:

python -m black myscript.py

3.4 只输入 python -m 会怎样

如果只执行:

python -m

而不提供模块名,Python 会报错,因为 -m 后面必须跟模块或包名。

正确格式是:

python -m 模块名

例如:

python -m pip --version

4. Python 脚本入口:if __name__ == '__main__'

4.1 它的作用

if __name__ == '__main__': 是 Python 中常见的程序入口判断。

它的作用是:让一个 Python 文件既可以被直接运行,也可以被其他文件导入,并且在导入时不会执行某些只应该在直接运行时执行的代码。

4.2 __name__ 是什么

每个 Python 文件在运行时,解释器都会自动提供一个变量:

__name__

它的值取决于当前文件的运行方式。

如果文件被直接运行:

python calculator.py

那么该文件中的:

__name__

值是:

'__main__'

如果文件被其他文件导入:

import calculator

那么 calculator.py 中的:

__name__

值通常是:

'calculator'

也就是模块名。

4.3 不使用入口判断的问题

假设有一个文件 calculator.py

def add(a, b):
    return a + b

print("正在测试 add 函数...")
print(add(2, 3))

直接运行它:

python calculator.py

输出测试信息是正常的。

但是如果另一个文件 main.py 导入它:

import calculator

print(calculator.add(5, 5))

运行:

python main.py

会发现 calculator.py 中的测试代码也被执行了。

原因是:import calculator 会执行 calculator.py 中的顶层代码。

4.4 使用入口判断解决问题

可以改成:

def add(a, b):
    return a + b

if __name__ == '__main__':
    print("正在测试 add 函数...")
    print(add(2, 3))

这样:

直接运行 calculator.py 时:

python calculator.py

__name__ 等于 '__main__',测试代码会执行。

被其他文件导入时:

import calculator

__name__ 等于 'calculator',测试代码不会执行。

4.5 推荐写法:封装 main() 函数

更推荐把直接运行时要执行的逻辑放进 main() 函数中:

def my_awesome_function():
    print("核心逻辑")

def main():
    print("程序开始运行...")
    my_awesome_function()

if __name__ == '__main__':
    main()

这种结构更清晰,也方便后续维护、测试和复用。

5. Python 包中的 __init__.py__main__.py

5.1 二者的核心区别

__init__.py__main__.py 都是 Python 包中具有特殊含义的文件,但作用不同。

简单来说:

__init__.py  负责包的初始化
__main__.py 负责包被直接运行时的入口

5.2 __init__.py 的作用

__init__.py 通常放在包目录下。

例如:

my_game/
├── __init__.py
├── core.py
└── utils.py

它的主要作用包括:

  1. 标识这个目录是一个 Python 包
  2. 在包第一次被导入时执行初始化代码
  3. 简化外部导入路径
  4. 控制 from package import * 的导出内容

在现代 Python 中,即使没有 __init__.py,某些目录也可以作为命名空间包被导入。

但在普通项目中,仍然建议为常规包添加 __init__.py,这样结构更清晰,也更兼容。

示例:

# my_game/__init__.py

__version__ = "1.0.0"

from .core import start_game

这样外部可以写:

from my_game import start_game

而不必写:

from my_game.core import start_game

5.3 __main__.py 的作用

__main__.py 用来让一个包可以被直接运行。

如果包结构如下:

my_game/
├── __init__.py
├── __main__.py
├── core.py
└── utils.py

当执行:

python -m my_game

Python 会查找并执行:

my_game/__main__.py

因此,__main__.py 通常放启动逻辑,例如命令行入口、服务启动入口等。

示例:

# my_game/__main__.py

from .core import start_game

def main():
    print("游戏正在通过命令行启动...")
    start_game()

if __name__ == '__main__':
    main()

对应的核心逻辑文件:

# my_game/core.py

def start_game():
    print("游戏开始")

5.4 __init__.py__main__.py 的触发时机

文件 作用 触发时机
__init__.py 初始化包 当包被 import 时执行
__main__.py 包的运行入口 当执行 python -m 包名 时执行

示例:

import my_game

会触发:

my_game/__init__.py

执行:

python -m my_game

会触发:

my_game/__main__.py

需要注意的是,执行 python -m my_game 时,Python 通常也会先加载包,因此 __init__.py 中的代码也可能先执行,然后再执行 __main__.py

所以不建议在 __init__.py 中放复杂逻辑或有副作用的代码。

6. 相对导入:from .core import start_game

6.1 .core 中的点是什么意思

在:

from .core import start_game

中,.core 表示相对导入。

这里的点 . 表示当前包。

所以:

from .core import start_game

意思是:从当前包中的 core.py 文件里导入 start_game

如果项目结构是:

my_game/
├── __init__.py
├── __main__.py
└── core.py

那么在 my_game/__main__.py 中:

from .core import start_game

表示从:

my_game/core.py

导入:

start_game

6.2 为什么不直接写 from core import start_game

不建议在包内部写:

from core import start_game

因为这是绝对导入。

Python 会在模块搜索路径中查找名为 core 的模块,而不一定是当前包里的 core.py

如果环境中刚好存在其他名为 core 的模块,就可能导入错误。

使用相对导入:

from .core import start_game

可以明确表示:从当前包内部导入。

6.3 一个点和两个点的区别

一个点:

from .core import start_game

表示当前包。

两个点:

from ..common import helper

表示上一级包。

三个点:

from ...utils import tool

表示上上一级包。

相对导入通常用于包内部模块之间的引用。

7. 如何运行包:python -m my_game

7.1 正确启动方式

如果项目结构如下:

project/
└── my_game/
    ├── __init__.py
    ├── __main__.py
    ├── core.py
    └── utils.py

应该在 project 目录下执行:

python -m my_game

注意:不是进入 my_game 目录里面执行,而是在它的上一级目录执行。

7.2 python -m my_game 背后的过程

执行:

python -m my_game

时,大致过程如下:

  1. Python 在当前目录和 sys.path 中查找名为 my_game 的包
  2. 找到 my_game 目录
  3. 识别它是一个包
  4. 加载包
  5. 查找并执行 my_game/__main__.py

因此,如果想让一个包支持:

python -m 包名

包中需要有:

__main__.py

7.3 不推荐直接运行 __main__.py

不推荐这样运行:

python my_game/__main__.py

也不推荐进入包目录后运行:

python __main__.py

原因是:这样 Python 会把 __main__.py 当成一个普通脚本执行,而不是作为 my_game 包的一部分执行。

此时,类似下面的相对导入可能会失败:

from .core import start_game

常见错误是:

ImportError: attempted relative import with no known parent package

因为相对导入依赖包上下文,而直接运行单个文件时,Python 不知道它属于哪个父包。

7.4 推荐项目结构

一个较规范的包结构可以写成:

project/
└── my_game/
    ├── __init__.py
    ├── __main__.py
    ├── core.py
    └── utils.py

core.py

def start_game():
    print("游戏开始")

__main__.py

from .core import start_game

def main():
    print("游戏正在通过命令行启动...")
    start_game()

if __name__ == '__main__':
    main()

project 目录下运行:

python -m my_game

输出:

游戏正在通过命令行启动...
游戏开始

8. 整体关系总结

8.1 依赖安装层面

requirements.txt 负责记录项目依赖。

推荐用虚拟环境隔离项目依赖:

python -m venv venv

安装依赖推荐使用:

python -m pip install 包名

导出依赖:

python -m pip freeze > requirements.txt

安装依赖文件:

python -m pip install -r requirements.txt

8.2 命令执行层面

python -m 表示把模块或包作为脚本运行。

例如:

python -m pip install requests
python -m venv venv
python -m http.server 8000
python -m my_game

8.3 单文件脚本层面

如果一个文件既要能直接运行,又要能被导入复用,推荐写:

def main():
    pass

if __name__ == '__main__':
    main()

8.4 包结构层面

__init__.py 负责包初始化。

__main__.py 负责包作为程序运行时的入口。

相对导入:

from .core import start_game

表示从当前包内部的 core.py 导入 start_game

运行整个包:

python -m my_game

而不是:

python my_game/__main__.py

这样可以保证包上下文正确,相对导入也能正常工作。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐