资讯动态

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

发布时间:2026/10/5 1:59:44 来源:尧图企业网站定制
示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载本指南基于 Advanced Python Masterydabeaz 课程仓库中的 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只导出Structurereader.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 ChainMapSolutions/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, *, headersNone): ... def csv_as_dicts(lines, types, *, headersNone): ... def csv_as_instances(lines, cls, *, headersNone): ... def read_csv_as_dicts(filename, types, *, headersNone): Read CSV data into a list of dictionaries with optional type conversion with open(filename) as file: return csv_as_dicts(file, types, headersheaders) def read_csv_as_instances(filename, cls, *, headersNone): Read CSV data into a list of instances with open(filename) as file: return csv_as_instances(file, cls, headersheaders)需要说明的是__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 特性模块对象即名字from .structure import *执行后structure这个名字本身仍留在包命名空间中import *只导入__all__列出的符号不会移除模块名所以可以直接引用structure.__all__、reader.__all__、tableformat.__all__列表解包星号表达式[*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可选增强 mixinColumnFormatMixin、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——TextTableFormatterformats/csv.py——CSVTableFormatterformats/html.py——HTMLTableFormatter4. 补充__init__.py并保持导出符号不变在tableformat/与tableformat/formats/各放一个__init__.pytableformat/__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_formatsNone, upper_headersFalse): 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__.pySolutions/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的工厂分支中登记无需改动任何使用方代码。总结通过本练习可以提炼出三条可复用的包设计经验__all__是包的白名单契约在每个子模块顶部声明__all__配合包级__init__.py的from .xxx import *与聚合__all__ [*a.__all__, *b.__all__, ...]可以把多模块包包装成单一逻辑入口同时让导出符号精确可控、可查。import *要用在成套符号场景对于像structly这样符号高度内聚、常被整体使用的包from structly import *是合理的而面对大型、符号分散的模块时则应谨慎优先显式导入。同名目录替换模块是零成本重构模块拆分的核心约束是新目录与被替换模块同名 包__init__.py保持原导出符号满足这两点即可在不惊动任何调用方的前提下自由重组内部文件结构。整个练习链路的上下文可继续参阅 Exercises/index.md、前一节 Exercises/ex9_2.md包引入与后续 Exercises/ex9_4.md在拆分后的formats/中继续扩展新格式标准答案见 Exercises/soln9_3.md。赞分享示例工程教程【免费下载链接】python-masteryAdvanced Python Mastery (course by dabeaz)项目地址https://gitcode.com/gh_mirrors/py/python-mastery点击查看免费下载相关推荐Python 包化实战将模块重构为 structly 包Advanced Python Mastery Exercise 9.2Python 包化实战将模块重构为 structly 包Advanced Python Mastery Exercise 9.2 导读 本篇文章对应 A示例工程教程Python 循环导入与动态模块导入实战基于 python-mastery 的子类注册表方案Exercise 9.4Python 循环导入与动态模块导入实战基于 python mastery 的子类注册表方案Exercise 9.4 在将单文件 tableformat.示例工程教程Python Mastery Exercise 5.5 实战用异常处理与 logging 模块驯服 reader.py 的脏数据Python Mastery Exercise 5.5 实战用异常处理与 logging 模块驯服 reader.py 的脏数据 本篇技术指南围绕 David示例工程教程上一篇OpCore Simplify5分钟构建专业级Hackintosh EFI配置下一篇离线翻译革命pot-app让你在任何环境下都能自由翻译创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑