news 2026/10/5 1:59:30

python-mastery Exercise 9.3 实战:用 `__all__` 控制包导出符号并拆分模块结构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
python-mastery Exercise 9.3 实战:用 `__all__` 控制包导出符号并拆分模块结构
  • 示例工程
  • 教程

【免费下载链接】python-mastery

Advanced Python Mastery (course by @dabeaz)

项目地址:https://gitcode.com/gh_mirrors/py/python-mastery
点击查看免费下载

本指南基于 Advanced Python Mastery(@dabeaz 课程)仓库中的 Exercises/ex9_3.md 练习展开,讲解如何把structly包从"多个子模块分散导入"重构为"单一顶层包统一导出":先通过__all__精确控制各子模块导出的符号,再在包级__init__.py聚合导出,最后把体积渐大的tableformat.py拆分成内部子包。读完本文,你将掌握__all__在包设计中的作用、from package import *的正确使用场景,以及"以目录替换同名 .py 模块"的模块拆分手法,并能在 Solutions/9_3 中看到完整的可运行实现。

背景:包的导入痛点

structly是课程前几节逐步构建的数据结构与格式化工具包,包含Structure(结构体基类)、CSV 读取函数和表格格式化器。当功能拆进多个子模块后,使用方stock.py的导入语句变得冗长:

from structly.structure import Structure from structly.reader import read_csv_as_instances from structly.tableformat import create_formatter, print_table

如果这个包是被当作一个整体来使用,把全部公共符号聚合到顶层包会更合理、也更易用。本练习分三步完成这一重构,每一步都在 Exercises/ex9_3.md 中有明确要求,Exercises/soln9_3.md 提供了标准答案,Solutions/9_3 目录下是完整落地代码。

(a) 用__all__控制子模块导出符号

第一步是为structly包中的每个子模块显式定义__all__变量,声明该模块"允许被import *带走的符号":

  • structure.py只导出Structure
  • reader.py导出全部read_csv_as_*()函数
  • tableformat.py导出create_formatter()与print_table()

__all__的实际定义方式

在 Solutions/9_3/structly/structure.py 中,__all__定义在文件最顶部(所有 import 之前),这是一种常见风格——读者一眼就能看到该模块的公共接口:

# structure.py __all__ = [ 'Structure' ] from .validate import Validator, validated from collections import ChainMap

Solutions/9_3/structly/reader.py 中,__all__列出的是两个高层读取入口(它们内部依赖convert_csv、csv_as_dicts、csv_as_instances等私有辅助函数,这些辅助函数因不在__all__中而不会被*导入):

# reader.py import csv import logging log = logging.getLogger(__name__) def convert_csv(lines, converter, *, headers=None): ... def csv_as_dicts(lines, types, *, headers=None): ... def csv_as_instances(lines, cls, *, headers=None): ... def read_csv_as_dicts(filename, types, *, headers=None): '''Read CSV data into a list of dictionaries with optional type conversion''' with open(filename) as file: return csv_as_dicts(file, types, headers=headers) def read_csv_as_instances(filename, cls, *, headers=None): '''Read CSV data into a list of instances''' with open(filename) as file: return csv_as_instances(file, cls, headers=headers)

需要说明的是:__all__并不限制from module import name形式的显式导入(Python 社区通常更推荐后者),它主要约束的是from module import *的符号范围。没有__all__时,import *会导入所有不以下划线开头的顶层名字;定义了__all__后,导入集合被严格限定。

在__init__.py中聚合子模块

给子模块加上__all__之后,在包初始化文件 Solutions/9_3/structly/init.py 中用import *把各子模块的公共符号提升到包顶层:

# structly/__init__.py from .structure import * from .reader import * from .tableformat import *

此时structly包本身已经像一个"统一逻辑模块",使用方可以只写一条导入语句:

# stock.py from structly import Structure class Stock(Structure): name = String() shares = PositiveInteger() price = PositiveFloat() @property def cost(self): return self.shares * self.price def sell(self, nshares: PositiveInteger): self.shares -= nshares if __name__ == '__main__': from structly import read_csv_as_instances, create_formatter, print_table portfolio = read_csv_as_instances('Data/portfolio.csv', Stock) formatter = create_formatter('text') print_table(portfolio, ['name','shares','price'], formatter)

注意stock.py中类体里直接使用的String、PositiveInteger、PositiveFloat来自structly.validate,它们经由structure.py的相对导入(from .validate import Validator, validated,而validate.py通过globals().update(...)动态生成了Integer/Float/String等类型)后进入模块命名空间,因此from structly import Structure一行即可让Stock类体正常解析这些名字。

(b) 在包级聚合__all__:一键导出全部符号

第二步是在 Solutions/9_3/structly/init.py 中定义一个聚合的__all__,把三个子模块的__all__拼接起来:

# structly/__init__.py from .structure import * from .reader import * from .tableformat import * __all__ = [ *structure.__all__, *reader.__all__, *tableformat.__all__ ]

这里用到了两个 Python 特性:

  1. 模块对象即名字:from .structure import *执行后,structure这个名字本身仍留在包命名空间中(import *只导入__all__列出的符号,不会移除模块名),所以可以直接引用structure.__all__、reader.__all__、tableformat.__all__;
  2. 列表解包星号表达式:[*a, *b, *c]是 Python 3.5+ 的列表拼接语法,等价于a + b + c。

这样做的收益是:包级__all__与各子模块的__all__保持一致,避免"顶层能导入、但不知道具体有哪些符号"的模糊状态。此时stock.py可进一步简化为:

# stock.py from structly import * class Stock(Structure): name = String() shares = PositiveInteger() price = PositiveFloat() @property def cost(self): return self.shares * self.price def sell(self, nshares: PositiveInteger): self.shares -= nshares if __name__ == '__main__': portfolio = read_csv_as_instances('Data/portfolio.csv', Stock) formatter = create_formatter('text') print_table(portfolio, ['name','shares','price'], formatter)

关于from module import *的取舍

练习文档明确指出:from module import *在 Python 社区中通常不受欢迎——尤其在你不清楚自己在做什么的时候,它可能悄悄遮蔽名字、污染命名空间、让代码难以追踪来源。但在某些场景下它是合理的,例如一个包定义了大量被共同使用的符号或常量,统一导出能显著降低使用方的样板代码。structly正是这种情形:Structure、校验器类型(String、PositiveInteger、PositiveFloat)、CSV 读取函数和表格格式化函数是成套使用的一组符号,聚合导出后stock.py的导入部分从三行缩成一行。

(c) 模块拆分:把tableformat.py升级为子包

第三步处理的是代码组织问题。tableformat.py目前在一个文件里同时容纳了:

  • TableFormatter抽象基类(配合abc.ABC与@abstractmethod强制子类实现headings()与row())
  • 具体格式化类:TextTableFormatter、CSVTableFormatter、HTMLTableFormatter
  • 可选增强 mixin:ColumnFormatMixin、UpperHeadersMixin
  • 工厂函数create_formatter()与打印函数print_table()

把这些具体类各归其位,可以拆成一个同名子包tableformat/。关键技巧是:新目录必须与被替换的模块(tableformat.py)同名,这样对外的导入路径(如from structly.tableformat import create_formatter)完全不变,调用方代码零改动。

拆分步骤

按 Exercises/ex9_3.md 的操作顺序:

1. 清理字节码缓存

% cd structly % rm -rf __pycache__

删除__pycache__是为了避免旧.pyc缓存干扰接下来的目录重构(注意:仓库是只读的,这里仅说明练习中的操作步骤)。

2. 建立同名子包目录并迁移原文件

bash % mkdir tableformat bash % mv tableformat.py tableformat/formatter.py

原tableformat.py更名为formatter.py移入新目录,继续扮演"基类与工厂逻辑所在"的角色。

3. 按职责拆分代码文件

  • formatter.py——TableFormatter基类、mixin 以及print_table/create_formatter函数
  • formats/text.py——TextTableFormatter
  • formats/csv.py——CSVTableFormatter
  • formats/html.py——HTMLTableFormatter

4. 补充__init__.py并保持导出符号不变

在tableformat/与tableformat/formats/各放一个__init__.py,tableformat/__init__.py必须导出与原tableformat.py相同的符号(print_table与create_formatter)。

完成后目录结构如下:

structly/ __init__.py validate.py reader.py structure.py tableformat/ __init__.py formatter.py formats/ __init__.py text.py csv.py html.py

拆分后的各文件实现

在 Solutions/9_3/structly/tableformat/formatter.py 中,formatter.py保留了基类、mixin、工厂与打印逻辑,并通过相对导入把三个具体类"引入并再导出":

# tableformat/formatter.py from abc import ABC, abstractmethod def print_table(records, fields, formatter): if not isinstance(formatter, TableFormatter): raise RuntimeError('Expected a TableFormatter') formatter.headings(fields) for r in records: rowdata = [getattr(r, fieldname) for fieldname in fields] formatter.row(rowdata) class TableFormatter(ABC): @abstractmethod def headings(self, headers): pass @abstractmethod def row(self, rowdata): pass from .formats.text import TextTableFormatter from .formats.csv import CSVTableFormatter from .formats.html import HTMLTableFormatter class ColumnFormatMixin: formats = [] def row(self, rowdata): rowdata = [ (fmt % item) for fmt, item in zip(self.formats, rowdata)] super().row(rowdata) class UpperHeadersMixin: def headings(self, headers): super().headings([h.upper() for h in headers]) def create_formatter(name, column_formats=None, upper_headers=False): if name == 'text': formatter_cls = TextTableFormatter elif name == 'csv': formatter_cls = CSVTableFormatter elif name == 'html': formatter_cls = HTMLTableFormatter else: raise RuntimeError('Unknown format %s' % name) if column_formats: class formatter_cls(ColumnFormatMixin, formatter_cls): formats = column_formats if upper_headers: class formatter_cls(UpperHeadersMixin, formatter_cls): pass return formatter_cls()

三个具体格式化类各归其位,都通过from ..formatter import TableFormatter(相对导入,..指tableformat包)继承基类:

Solutions/9_3/structly/tableformat/formats/text.py:

# text.py from ..formatter import TableFormatter class TextTableFormatter(TableFormatter): def headings(self, headers): print(' '.join('%10s' % h for h in headers)) print(('-'*10 + ' ')*len(headers)) def row(self, rowdata): print(' '.join('%10s' % d for d in rowdata))

Solutions/9_3/structly/tableformat/formats/csv.py:

# csv.py from ..formatter import TableFormatter class CSVTableFormatter(TableFormatter): def headings(self, headers): print(','.join(headers)) def row(self, rowdata): print(','.join(str(d) for d in rowdata))

Solutions/9_3/structly/tableformat/formats/html.py:

# html.py from ..formatter import TableFormatter class HTMLTableFormatter(TableFormatter): def headings(self, headers): print('<tr>', end=' ') for h in headers: print('<th>%s</th>' % h, end=' ') print('</tr>') def row(self, rowdata): print('<tr>', end=' ') for d in rowdata: print('<td>%s</td>' % d, end=' ') print('</tr>')

tableformat/__init__.py(Solutions/9_3/structly/tableformat/init.py)承担"对外契约不变"的职责,只提升两个公共函数并声明__all__:

# __init__.py from .formatter import print_table, create_formatter __all__ = [ 'print_table', 'create_formatter' ]

tableformat/formats/__init__.py保持为空文件,仅用于把formats标记为包;各具体类不直接对外暴露,而是通过formatter.py内的导入间接可达——这正是封装的目的:用户只面向create_formatter('text'|'csv'|'html')工厂,无需关心类名与文件位置。

拆分的意义:保持对外接口稳定

拆分后,对使用者而言一切照旧:Solutions/9_3/stock.py 依然只写from structly import *就能拿到Structure、校验器、read_csv_as_instances、create_formatter与print_table,并用它们读取 Data/portfolio.csv 输出表格:

# stock.py from structly import * class Stock(Structure): name = String() shares = PositiveInteger() price = PositiveFloat() @property def cost(self): return self.shares * self.price def sell(self, nshares: PositiveInteger): self.shares -= nshares if __name__ == '__main__': portfolio = read_csv_as_instances('../../Data/portfolio.csv', Stock) formatter = create_formatter('text') print_table(portfolio, ['name','shares','price'], formatter)

(练习文档中路径写作'Data/portfolio.csv',实际解决方案目录下因stock.py位于Solutions/9_3/,相对仓库根目录为'../../Data/portfolio.csv',运行时可依实际位置调整。)

这一节揭示的模式是:包的对外 API 可以保持稳定,而内部实现可以从"单文件"平滑演进为"多文件子包"——只要目录名与模块同名、__init__.py导出与原模块相同的符号,调用方就完全无感。模块拆分还带来了可维护性收益:后续若新增格式(如练习 9.4 中的tsv.py),只需在tableformat/formats/下加一个文件并在formatter.py的工厂分支中登记,无需改动任何使用方代码。

总结

通过本练习可以提炼出三条可复用的包设计经验:

  1. __all__是包的"白名单"契约:在每个子模块顶部声明__all__,配合包级__init__.py的from .xxx import *与聚合__all__ = [*a.__all__, *b.__all__, ...],可以把多模块包包装成单一逻辑入口,同时让导出符号精确可控、可查。
  2. import *要用在"成套符号"场景:对于像structly这样符号高度内聚、常被整体使用的包,from structly import *是合理的;而面对大型、符号分散的模块时则应谨慎,优先显式导入。
  3. 同名目录替换模块是零成本重构:模块拆分的核心约束是"新目录与被替换模块同名 + 包__init__.py保持原导出符号",满足这两点即可在不惊动任何调用方的前提下自由重组内部文件结构。

整个练习链路的上下文可继续参阅 Exercises/index.md、前一节 Exercises/ex9_2.md(包引入)与后续 Exercises/ex9_4.md(在拆分后的formats/中继续扩展新格式),标准答案见 Exercises/soln9_3.md。

  • 示例工程
  • 教程

【免费下载链接】python-mastery

Advanced Python Mastery (course by @dabeaz)

项目地址:https://gitcode.com/gh_mirrors/py/python-mastery
点击查看免费下载
上一篇:OpCore Simplify:5分钟构建专业级Hackintosh EFI配置
下一篇:离线翻译革命:pot-app让你在任何环境下都能自由翻译

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 1:55:53

OpenCore Legacy Patcher 指南:5 步给老 Mac 装上新版 macOS

OpenCore Legacy Patcher 指南&#xff1a;5 步给老 Mac 装上新版 macOS 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 2013 款的 iMac 收不到系统更新&…

作者头像 李华
网站建设 2026/10/5 1:53:52

CarSim与Simulink联合仿真:Driver Model与5个Driver Sensors闭环控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 1:53:43

LAMMPS 命令分类详解:从 9 大功能域到完整输入脚本实战

科研科学计算高性能计算 【免费下载链接】lammps Public development project of the LAMMPS MD software package 项目地址&#xff1a; https://gitcode.com/gh_mirrors/la/lammps 点击查看 免费下载 导读 本文以 LAMMPS 官方命令分类索引页 Commands by category 为主体骨…

作者头像 李华