首页/Python 与数据实践/模块与包的组织
Python 与数据实践 · 第 15 章

模块与包的组织

写到这个阶段,你在一个 .py 文件里已经能堆出几百行像样的程序了。但麻烦也跟着来:文件越长,找一个函数越要翻半天屏;下一个项目想复用上次写的那几个工具,只能整段复制过去。Python 给出的答案很朴素——把代码拆进多个…

约 15 分钟6,042 字17 个代码示例

写到这个阶段,你在一个 .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__ 一共干三件事:

  1. 圈定 from pricing import * 的导入范围——只导入清单里的名字
  2. 相当于一份公开 API 声明,告诉使用者"清单里的算稳定接口,放心用"
  3. 给不少工具当依据(文档生成器、静态检查器都会读它)

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
  1. 整理门面:把内部各模块挑出来的成员汇拢到包级别,使用者一句 from shop_kit import with_tax 就能拿到,不必关心它藏在哪个子模块里
  2. 做包级初始化:读取配置、建立数据库连接、校验运行环境……这类"整个包开工前的准备动作"都可以放在这里

顺带一提:__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 列表。 从左到右逐个目录翻,找到第一个匹配就停。这份列表大致由四部分按序拼成:

  1. 当前脚本所在目录
  2. 环境变量 PYTHONPATH 里配置的额外路径
  3. 标准库所在目录
  4. 第三方包的安装目录(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 没定义出来,于是炸了。

破局有两条路:

  1. 把圈拆成线:双方都需要的代码抽出来,放进第三个模块,依赖关系恢复成单向
  2. 推迟导入:把 import 挪进函数体,等函数真正被调用时,两个模块早已各自就绪
# kitchen.py —— 改用延迟导入
def wash_veg() -> str:
    from chef import cook  # 调用时才导入,此时 chef 已经加载完毕

    return "菜已洗净"

8. 作业

习题一:费曼复述

合上讲义,用自己的话把下面四件事讲给完全没碰过 Python 的人听,讲到对方点头为止:

  1. 包、模块、成员三者是什么关系
  2. __all__ 管的是什么
  3. __init__.py 什么时候执行、能拿来做什么
  4. 一条 import 语句背后,Python 按什么顺序找模块

习题二:自学 __slots__

查一查 Python 里的 __slots__:它解决什么问题、怎么用、有什么代价。把结论整理成三五行笔记。


下一章:16 · 标准库巡礼