模块与包的组织
写到这个阶段,你在一个 .py 文件里已经能堆出几百行像样的程序了。但麻烦也跟着来:文件越长,找一个函数越要翻半天屏;下一个项目想复用上次写的那几个工具,只能整段复制过去。Python 给出的答案很朴素——把代码拆进多个…
写到这个阶段,你在一个 .py 文件里已经能堆出几百行像样的程序了。但麻烦也跟着来:文件越长,找一个函数越要翻半天屏;下一个项目想复用上次写的那几个工具,只能整段复制过去。Python 给出的答案很朴素——把代码拆进多个文件,需要时再取回来。拆出来的 .py 文件叫模块,收纳模块的目录叫包。这一章我们把拆分、导出、导入、查找这一整套机制讲清楚,它是后面所有工程化话题的地基。
1. 全景图:包、模块、成员是什么关系
先看一张结构图,本章的三个关键词一次认全:
project/ ← 项目根目录
├── app.py ← 入口脚本(它本身也是一个模块)
└── shop_kit/ ← 包:带 __init__.py 的目录
├── __init__.py ← 包的"身份证"
├── goods.py ← 模块:一个 .py 文件
│ ├── TAX_RATE ← 成员:模块顶层的变量
│ └── with_tax() ← 成员:模块顶层的函数
└── promo/ ← 子包:包里再套一层目录
├── __init__.py
└── banner.py ← 子包中的模块
给三个词各下一句定义:
- 成员:定义在模块顶层的变量、函数、类
- 模块:一个 .py 文件,文件名去掉后缀就是模块名
- 包:一个包含
__init__.py的目录,里面可以放模块,也可以继续放包(子包)
它们是层层收纳的关系:包装着模块,模块装着成员——跟"文件夹装文件、文件里写内容"是一回事。
2. 定义模块:写一个 .py 文件,就是在"供货"
新建一个 pricing.py,你就定义了一个名为 pricing 的模块。写在文件顶层的所有东西,都是这个模块对外供应的"货":
# pricing.py —— 文件叫 pricing.py,模块名就是 pricing
TAX_RATE: float = 0.13 # 供应一个变量
def with_tax(price: float, rate: float = TAX_RATE) -> float: # 供应一个函数
return round(price * (1 + rate), 2)
class Pricer: # 供应一个类
def member_price(self, price: float) -> float:
return round(price * 0.9, 2)
三条要点记牢:
- 模块名就是文件名(去掉
.py) - 顶层的变量、函数、类都是模块的成员,别的文件导入后就能用
- 定义在函数体、类体内部的名字不算成员,外部根本碰不到
2.1 单下划线:口头约定的"内部零件"
Python 没有强制私有这回事,但社区有个广泛遵守的默契:名字以下划线开头,意思是"这是内部实现,别从外面碰"。
# pricing.py
TAX_RATE: float = 0.13 # 公开成员
def with_tax(price: float) -> float: # 公开成员
return _round2(price * (1 + TAX_RATE))
def _round2(value: float) -> float: # "私有":只是约定,外部硬要访问也拦不住
return round(value, 2)
这个约定还有一个实际效果:from pricing import * 时,下划线开头的名字不会被带过来。
2.2 __all__:白纸黑字的供货清单
如果想让"星号导入到底带哪些东西"变得确定可控,就在模块里写一份 __all__:
# pricing.py
__all__: list[str] = ["TAX_RATE", "with_tax"]
TAX_RATE: float = 0.13
MEMBER_RATE: float = 0.9 # 不在清单上,import * 拿不到它
def with_tax(price: float) -> float:
return round(price * (1 + TAX_RATE), 2)
def bulk_price(price: float, count: int) -> float: # 同样不在清单上
return round(price * count * 0.95, 2)
__all__ 一共干三件事:
- 圈定
from pricing import *的导入范围——只导入清单里的名字 - 相当于一份公开 API 声明,告诉使用者"清单里的算稳定接口,放心用"
- 给不少工具当依据(文档生成器、静态检查器都会读它)
2.3 __name__:这段代码现在是不是"主角"
每个模块都自带一个属性 __name__,它的取值取决于这份代码是怎么被跑起来的:
- 直接运行(
python pricing.py)时,__name__是字符串"__main__" - 被别的文件导入时,
__name__是模块本名"pricing"
于是有了 Python 里最经典的固定搭配——把自测代码放进 if __name__ == "__main__"::
# pricing.py
__all__: list[str] = ["TAX_RATE", "with_tax"]
TAX_RATE: float = 0.13
def with_tax(price: float) -> float:
return round(price * (1 + TAX_RATE), 2)
# 只有直接运行本文件时才走这里;被导入时这段完全不会执行
if __name__ == "__main__":
print(with_tax(100)) # 113.0
print(with_tax(59.9)) # 67.69
这样一个文件既能当工具被别人导入,又能当脚本自己跑测试,两不耽误。
3. 导入模块:四种写法 + 两个底层真相
3.1 四种写法
import pricing # 整个模块搬进来,用时带前缀
print(pricing.TAX_RATE)
print(pricing.with_tax(100))
from pricing import TAX_RATE, with_tax # 只拿需要的成员,直接用名字
print(TAX_RATE)
print(with_tax(100))
from pricing import * # 拿 __all__ 清单里的成员(没写 __all__ 时拿所有非下划线成员)
print(TAX_RATE)
# bulk_price 不在 __all__ 里,这里用不了
import pricing as pr # 给模块起个别名
from pricing import with_tax as taxed # 给成员起个别名
print(pr.TAX_RATE)
print(taxed(100))
日常建议:优先 import 模块 或 from 模块 import 具体名字;import * 图省事但容易把命名空间搅浑,慎用。
3.2 真相一:import 是运行时才执行的普通语句
import 不像某些语言在编译期就把依赖定死——它是运行到那一行才发生的普通语句。所以把它写进 if 分支、写进函数体,都完全合法。推到极致,连"导入哪个模块"都可以拖到运行时再决定,这就是 importlib 登场的地方:
import importlib
# 模块名放在变量里
name = "statistics"
stats = importlib.import_module(name)
print(stats.mean([2, 4, 9])) # 5.0
# 模块名来自用户敲的键盘
choice = input("想加载哪个模块?")
mod = importlib.import_module(choice)
# 也可以用字符串指名道姓地导入某个子模块
parse = importlib.import_module("urllib.parse")
print(parse.quote("hello world")) # hello%20world
做插件系统、按配置动态加载组件时,这套玩法非常实用。
3.3 真相二:导入即执行,且只执行一次
某个模块第一次被导入时,Python 会从头到尾执行一遍这个 .py 文件;之后再导入(哪怕发生在别的文件里),直接吃缓存,文件不会重跑:
import pricing # 第一次:执行 pricing.py 的全部顶层代码
import pricing # 第二次:命中缓存,什么都不发生
import pricing # 第三次:同上
缓存登记在 sys.modules 里,可以现场查验:
import sys
print("pricing" in sys.modules) # True,刚才导入过,已经登记在册
print("sys" in sys.modules) # True,sys 自己也在
顺带解开一个常见疑惑:为什么模块里顶层的 print 有时会莫名其妙输出?因为导入本身就是执行——你 import 的那一刻,那个文件就跑了一遍。
4. 包:给模块挂上门牌号
模块一多,全平铺在一个目录里就乱了。自然的想法是建目录归类——在 Python 里,这个目录必须放一个 __init__.py,它才被认作一个"包":
project/
├── app.py
└── shop_kit/
├── __init__.py
├── goods.py
└── orders.py
从此模块的完整名字带上了路径:shop_kit.goods、shop_kit.orders。目录层级直接成为名字的一部分,天然避开重名冲突。
__init__.py 是干嘛的
这个文件在包第一次被导入时自动执行,相当于包的"开业剪彩"。它通常承担两类工作:
# shop_kit/__init__.py
__all__: list[str] = ["TAX_RATE", "with_tax", "order_no"]
from .goods import TAX_RATE, with_tax # 把子模块的成员提升到包这一层
from .orders import order_no
- 整理门面:把内部各模块挑出来的成员汇拢到包级别,使用者一句
from shop_kit import with_tax就能拿到,不必关心它藏在哪个子模块里 - 做包级初始化:读取配置、建立数据库连接、校验运行环境……这类"整个包开工前的准备动作"都可以放在这里
顺带一提:__init__.py 允许是空文件。很多时候它的存在本身就是意义——告诉 Python"这个目录是个包"。
5. 相对导入:包内部的简写引用
包里的模块经常要互相配合。如果每次都写全名,包一改名就得全局跟着改。相对导入解决这个问题:
project/
├── app.py
└── shop_kit/
├── __init__.py
├── goods.py
└── report.py
# shop_kit/report.py
from .goods import TAX_RATE # 一个点:从当前包(shop_kit)里找 goods
def tax_line(price: float) -> str:
return f"含税价:{round(price * (1 + TAX_RATE), 2)} 元"
点号的含义数层级就行:
.—— 当前包..—— 上一层的父包...—— 再往上的祖父包
一条铁律:相对导入只能出现在"被导入的包内模块"里。直接 python report.py 运行它是不行的——直接运行的脚本没有"当前包"的概念,Python 会直接报错。想用它,请从包外面导入,或者跑包外面的入口脚本 app.py。
6. import 的寻路规则:它去哪儿找模块
敲下 import xxx 之后,Python 按固定顺序翻两个地方:
第一站:内置模块。 这些模块直接编译在解释器里(sys、builtins 这类),查起来最快。名单可以现场打印:
import sys
print(sys.builtin_module_names) # 一长串,全是 C 实现的内置模块
第二站:sys.path 列表。 从左到右逐个目录翻,找到第一个匹配就停。这份列表大致由四部分按序拼成:
- 当前脚本所在目录
- 环境变量
PYTHONPATH里配置的额外路径 - 标准库所在目录
- 第三方包的安装目录(
site-packages)
import sys
for path in sys.path:
print(path) # 亲眼看看你的解释器都在哪些目录里翻东西
看懂这份清单,很多"明明装了却 import 不到"的怪事就有了答案:要么包被装进了另一个解释器的 site-packages,要么当前目录恰好有个同名文件把它半路截走了。
7. 循环导入:两个人互相等对方先开口
包内模块互相引用很自然,但引用成"一个圈"就出事了:
# chef.py
from kitchen import wash_veg
def cook() -> str:
return wash_veg() + ",下锅"
# kitchen.py —— 此刻 chef.py 还没执行完,cook 这个名字尚不存在
from chef import cook # ImportError!
def wash_veg() -> str:
return "菜已洗净"
死结是这样拧上的:导入 chef → chef 第一行转去导入 kitchen → kitchen 又回头要 chef 里的 cook——而 chef 此刻还是"半成品",cook 没定义出来,于是炸了。
破局有两条路:
- 把圈拆成线:双方都需要的代码抽出来,放进第三个模块,依赖关系恢复成单向
- 推迟导入:把
import挪进函数体,等函数真正被调用时,两个模块早已各自就绪
# kitchen.py —— 改用延迟导入
def wash_veg() -> str:
from chef import cook # 调用时才导入,此时 chef 已经加载完毕
return "菜已洗净"
8. 作业
习题一:费曼复述
合上讲义,用自己的话把下面四件事讲给完全没碰过 Python 的人听,讲到对方点头为止:
- 包、模块、成员三者是什么关系
__all__管的是什么__init__.py什么时候执行、能拿来做什么- 一条
import语句背后,Python 按什么顺序找模块
习题二:自学 __slots__
查一查 Python 里的 __slots__:它解决什么问题、怎么用、有什么代价。把结论整理成三五行笔记。
下一章:16 · 标准库巡礼