第 7 章 · 模块、包与标准库

前六章都是单文件功夫,本章解决「代码长大之后」的组织问题:模块(文件)→ 包(目录)→ import 机制 → 标准库地图。这一章也是「Python 为什么生态无敌」的答案:机制简单 + 标准库(batteries included)太全。

7.1 模块:一个文件就是一个模块

# mymath.py
PI = 3.14159
def circle_area(r):
    return PI * r ** 2

def _helper(x):            # 下划线开头:约定「内部」,from-import 不带走
    return x * 2
# main.py
import mymath                     # 导入模块(整个文件)
mymath.circle_area(2)             # 用:模块名.成员

from mymath import circle_area    # 只导入成员
circle_area(2)

from mymath import circle_area as ca    # 起别名
import numpy as np                # 社区惯例别名(np、pd、plt)

四种 import 风格的取舍:import module(全带,前缀清晰,推荐默认)、from module import 成员(常用成员直接用)、as 别名(惯例缩写)、from module import *(污染命名空间,禁止)。模块只导入一次(之后用缓存,sys.modules 里查;这也是第 3 章「import 的副作用只跑一遍」的原因:模块顶层代码在首次 import 时执行)。

7.2 包:目录 + init.py

模块多了按目录分装,带 __init__.py 的目录就是包:

myproject/
├── main.py
└── utils/
    ├── __init__.py      # 可以是空文件,也可以做包的「门面」
    ├── text.py          # from utils.text import clean
    └── net.py
from utils.text import clean     # 包名.模块名.成员
from utils import net            # 或导入模块再 net.call()

__init__.py 的进阶用法:在里面写 from .text import clean,让使用者 from utils import clean 直接拿(把包的内部结构藏起来,对外只露接口——软件工程课第 4 章接口设计的 Python 版)。.py 文件内的 if __name__ == "__main__": 也在此讲透:每个模块有 __name__ 属性,被 import 时是模块名,直接运行时是 "__main__"——这个 if 是「既当库又当脚本」的双面开关,每个正式的 .py 都该有。

7.3 import 的查找机制与常见炸点

Python 找模块的顺序:内置 → sys.path 列表(当前目录排第一!)+ site-packages(第 1 章 venv 的安装地)。两个经典炸点全由此而来:

  • 同名遮蔽:自己的 random.py 会顶掉标准库 random(当前目录优先);命名避开标准库名(第 1 章的立牌重申)。
  • 循环导入:a.py import b,b.py import a,加载时一半为空;解法是「抽公共模块」或「函数内延迟 import」(软件工程课第 4 章解依赖环的 Python 版)。

包的发布第三方(pip install 装的那些)遵循同一机制,只是放在 site-packages;第 1 章的 venv 之所以能隔离,就是在 sys.path 里换了一套 site-packages。

7.4 类型注解:让动态语言长出静态护栏

def bmi(weight: float, height: float) -> float:
    return weight / height ** 2

def find(user_id: int, cache: dict[str, str] | None = None) -> str | None:
    ...

类型注解(3.5+)是提示而非强制(运行时不查,传错类型照样跑);配上 mypy 静态检查器(mypy main.py),在跑之前抓住类型错误(第 2 章动态类型的现代解药)。大项目标配,库的文档也靠注解自解释。语法要点:list[str]、dict[str, int](3.9+ 内置泛型),str | None 表示「或 None」(3.10+ 的联合类型写法,老代码用 Optional[str])。

7.5 标准库地图:batteries included

「Python 自带电池」——常用需求先查标准库再找三方包。按用途分区的速查地图(每个都值得 help() 一下):

需求模块代表 API
命令行参数argparse解析 --flag 选项(第 10 章实战)
数学/随机math, random, statisticsceil, choice, mean
时间datetimedatetime.now(), timedelta
文本/正则re, json, csvre.findall, json.load(第 10 章)
文件/路径pathlib, shutil, osglob, copy, walk(第 5 章)
数据结构加强collectionsCounter(一行数词频!), deque, defaultdict
迭代工具itertoolsproduct, groupby, combinations
调试logging, pprint日志(替代 print)、美化打印
系统/子进程os, subprocess跑外部命令
网络urllib最基础的 HTTP(第 10 章用 requests)

collections.Counter 单独点名(最常用彩蛋):

from collections import Counter
words = "to be or not to be".split()
Counter(words)             # {'to': 2, 'be': 2, 'or': 1, 'not': 1}
Counter(words).most_common(2)   # [('to', 2), ('be', 2)]

第 4 章手写的词频统计器,标准库一行。**「先搜标准库」**是 Pythonic 的核心习惯:你写的每个工具函数,九成标准库已有更好的版本。

常见错误

  • 循环导入(7.3 的炸点)。症状:「cannot import name X from partially initialized module」;重构依赖方向。
  • import 位置乱。PEP 8 规定:标准库 → 三方库 → 本地,三组之间空行;文件头一排乱 import 是坏味道(软件工程课第 7 章)。
  • 把 init.py 当普通代码堆。它该薄(只做导出门面),业务逻辑进子模块。
  • 类型注解写了但不跑 mypy。注解只是注释;CI 里挂 mypy(软件工程课第 10 章)才闭环。

练习

  1. 动手:把第 6 章的 Student 类拆成 models.py,把输入输出逻辑拆成 io_utils.py,主程序 main.py import 它们;验证 python main.py 和 python -c "from models import Student" 双模式(顺带理解 name 开关)。
  2. 动手:用 Counter 重写第 4 章的词频统计(对比行数);再用 defaultdict(list) 实现「按首字母分组单词」。
  3. 动手:给第 3 章的 bmi 函数加类型注解,故意传字符串调用,再装 mypy 跑一遍,对比「运行时才炸」和「静态就标红」。
  4. 思考:Python 的「模块顶层代码会被执行」(import 有副作用)灵活但危险(第三方包 import 时就联网/开文件)。对比 Java 的类加载(第 1 章 JVM)与 Python 的模块系统,讨论「import 的副作用」该不该被禁止,现实里的包(如 matplotlib 的 rcParams)为什么又离不开它。