JEPA4Japan · 教程

模块、包与虚拟环境

1,803字 5分钟阅读 #Python

将代码拆分到多个模块中、控制导入,并隔离项目依赖。

课程进度 课程大纲 已发布 24/24 课

把代码放进带标签的抽屉

一个很长的 Python 文件就像一个巨大的玩具箱:所有东西都在里面,但什么都不容易找到。Python 让我们可以为每项工作准备一个带标签的抽屉。

  1. 模块 一个可导入的 .py 文件
  2. 导入包 一个包含模块的文件夹
  3. 发行包 pip 可以安装的东西
  4. 虚拟环境 某个项目专属的 Python 空间
文件用来组织代码;环境用来组织解释器和已安装的发行包。

这些名称彼此相关,但不能互换使用。import 加载的是模块或导入包。python -m pip install ... 安装的是一个发行包。一个发行包可以提供多个导入包,而且它的安装名称不一定与导入名称相同。

导入时不要弄乱名称

import statistics 会绑定一个模块对象。点号说明了每个工具来自哪里:

import statistics

minutes = [20, 30, 40]
print(statistics.mean(minutes))
print(statistics.median(minutes))

输出:

30
30

这个模块前缀是一个命名空间。它让你自己的名称 mean 可以与 statistics.mean 同时存在。你也可以只导入需要的内容:

from statistics import mean as average

print(average([10, 20, 30]))

只在别名能让含义更清楚时使用它。避免使用 from statistics import *:通配符会隐藏究竟导入了哪些名称,以及它们替换了哪些原有名称。

在一个 Python 进程中,第一次成功导入模块时,会从上到下运行它的顶层代码。之后的普通导入会复用缓存中的模块。因此,顶层代码大多应该用来定义函数、类和常量,而不是请求输入或启动程序。

  1. 首次导入 运行顶层定义
  2. 记住 缓存模块对象
  3. 主程序保护 只在有意运行时执行应用工作
导入会让定义变得可用;主程序保护会防止只属于脚本的工作意外运行。
def main():
    print("Study report started.")


if __name__ == "__main__":
    main()

当它作为入口文件运行时,__name__ 是 "__main__",所以会显示消息。当它被导入时,__name__ 是模块的导入名称,因此 main() 会变得可用,但不会运行。

给包设置一个统一入口

常规包是一个包含 __init__.py 和相关模块的目录:

project/
├── study_report/
│   ├── __init__.py
│   ├── calculations.py
│   └── __main__.py
└── tests.py

__init__.py 用来标记这个常规包,并在第一次导入包时运行。它可以是空的,也可以重新导出一个精简的公共接口。在包内部,一个开头的点表示“从这个包中导入”:

from .calculations import summarize

__all__ = ["summarize"]

__all__ 说明了预期提供的接口;它并不是保护隐私或安全的屏障。避免循环导入:如果 A 导入 B,而 B 又导入 A,请把共享工作移到更底层的模块中,或者通过更清晰的边界传递值。

  1. 导入 __init__.py 打开 API 入口
  2. 相对导入 点号会保留包的上下文
  3. python -m 运行 __main__.py
包有一个导入入口,还可以有一个可选的运行按钮。

请在包含这个包的目录中运行它:

python -m study_report

不要运行 python study_report/__main__.py;这样会把包内文件当作独立脚本处理,并可能破坏相对导入。

构建一个可以运行的学习报告包

创建上面的目录树。把以下内容放入 study_report/calculations.py:

def summarize(sessions):
    totals = {}
    for topic, minutes in sessions:
        totals[topic] = totals.get(topic, 0) + minutes
    return sorted(totals.items())

把以下内容放入 study_report/__init__.py:

from .calculations import summarize

__all__ = ["summarize"]

把以下内容放入 study_report/__main__.py:

from . import summarize


def main():
    sessions = [("Python", 45), ("Git", 40), ("Python", 30)]
    print("Study report")
    for topic, minutes in summarize(sessions):
        print(f"- {topic}: {minutes} minutes")


if __name__ == "__main__":
    main()

最后,把以下内容放入包旁边的 tests.py:

from study_report import summarize

assert summarize([]) == []
assert summarize([("Python", 20), ("Python", 30)]) == [("Python", 50)]
print("Tests passed.")

先运行 python tests.py,再运行 python -m study_report。预期输出:

Tests passed.
Study report
- Git: 40 minutes
- Python: 75 minutes

测试中的导入不会打印报告。这个包对外提供一个稳定的操作,而 __main__.py 负责用户可见的应用行为。

检查来源,并给每个项目一个独立空间

名为 statistics.py、json.py 或 pathlib.py 的本地文件可能会遮蔽真正的库。检查 Python 实际加载了什么:

import statistics

print(statistics.__name__)
print(statistics.__file__)

第二行的结果因计算机而异;请确认它指向预期的库,而不是你的项目。应该修正文件名或目录结构,不要到处修改 sys.path。

在项目根目录中创建一个隔离环境:

python -m venv .venv
source .venv/bin/activate
python -c "import sys; print(sys.prefix != sys.base_prefix)"

Windows PowerShell 使用 .venv\Scripts\Activate.ps1。检查结果应该打印 True。使用 python -m pip install distribution-name 安装需要的发行包,然后使用 deactivate 退出环境。把 .venv/ 添加到 .gitignore,并记录创建环境所需的命令;应当重新创建环境,而不是移动或提交它。venv 隔离的是已安装的发行包,而不是你的数据文件或导入设计。

三个小任务与收尾检查

  1. 安全导入。 把一个温度转换函数移到 conversions.py;把显示内容放在 weather_report.py 的主程序保护之后。确认导入它时不会打印任何内容。
  2. 新建包。 创建一个 reading_log 包,其中包含一个公共汇总函数、一个 __init__.py 和一个 __main__.py。使用 python -m reading_log 运行它。
  3. 环境证明。 创建 .venv,激活它,确认前缀检查结果为 True,停用环境,并确认 Git 会忽略 .venv/。

容易踩到的坑:通配符导入会隐藏名称,顶层工作会产生导入副作用,直接执行可能丢失包的上下文,而本地文件名可能导致加载错误的模块。

  • 我可以区分模块、导入包、发行包和 venv。
  • 我可以解释命名空间和首次导入时的执行行为。
  • 我可以使用主程序保护和精简的 __init__.py 接口。
  • 我可以使用 python -m 运行并测试包。
  • 我可以检查已导入模块的来源。
  • 我可以创建、验证、退出并重新创建 .venv。
  • 我完成了三个小任务。

接下来,你将使用 pathlib、with、CSV 和 JSON,为这些组织良好的代码提供安全的本地存储。