
很多人学Python的时候都有过这么一段迷茫期基础语法学完了函数会写了列表推导式也玩得挺溜但一看别人的开源项目满眼的类和模块结构整整齐齐再看看自己手底下几百行的脚本变量满天飞哪里要用就拼哪个函数改一次要翻半天。这篇东西就是写给刚好卡在这个阶段的你——核心就三件事单例模式、类的书写规范、包模块的制作与导入。这三件事表面上各讲各的其实都是同一个目标让你从“能写脚本”进化到“能写项目”。一个是用设计模式解决对象创建的问题一个是用规范和技巧让类像样一个是用包和模块把代码变成可复用的资产。学完这篇你至少能把手里的乱码脚本整理成一套结构清晰、可以给别人用也能自己维护的小项目。以下内容基于我平时带新人、做代码评审时最常碰到的真实场景来写的不是教科书搬法尽量说人话。1. 为什么这三个问题值得放在一起学从脚本到项目的必经之路1.1 三个痛点其实是同一件事的三张面孔先说说我为什么把这三件事放到一篇文章里讲。带过很多新人我发现一个普遍规律大家写Python学到一定程度瓶颈不在于语法不会而在于组织代码的能力。什么意思就是语法书上的例子都是解救单一问题的比如定义个类表示一只猫调用个方法让它叫但真实项目里没有人关心猫叫不叫大家关心的是用户登录、数据库连接、配置加载、日志记录这些东西怎么优雅地组织在一起。单例模式、类的书写规范、包和模块这三者看起来是三个知识点实际上对应的是同一个问题的三个层面单例模式解决的是运行时的对象管理。当你的程序里只需要一个全局配置对象、一个数据库连接对象时怎么保证不会重复创建、彼此干扰。类的书写规范解决的是代码层面的可读性。类不是把几个函数包在一个class里就完事了属性怎么命名、方法怎么划分、哪些该暴露哪些该私有这些写得好不好直接决定半年后你自己还看不看得懂。包和模块解决的是文件层面的组织能力。代码写多了总得分文件分文件就得考虑导入关系、命名空间、循环依赖这整套规则搞清楚了你才能接手稍微大一点的项目。这三样东西放在一起学最大的价值在于它们共同指向了“如何把一个几十行的小脚本扩展成一个几百几千行还能维护的项目”。这是所有想往进阶走的人必须迈过去的一道坎。1.2 一个贯穿全文的案例数据库连接管理器为了让这篇内容不是零散的知识陈列我先立一个贯穿全篇的小案例——DBManager一个数据库连接管理器。需求很简单我们正在写一个内部后台系统多个模块用户模块、订单模块、报表模块都需要连接同一个 MySQL 数据库。如果每个模块都自己去创建连接对象那么数据库的连接数会被快速打满配置变了还得每个模块都改一遍。这个案例看起来小但串起我们的三大主题正合适用单例模式保证所有模块共享同一个连接配置对象用类的书写规范把它写成一个结构合理、健壮易读的类最后把它放进一个包里让不同模块都能干净地导入使用。后面每一章都会围绕这个案例往下展开从徒手写个能用的版本到一步步改进成可以进生产环境的版本。这样你学完的不只是知识点而是一条完整的项目改造链路。2. 单例模式让关键对象“只存在一次”2.1 单例模式到底解决了什么问题先看一个最简单的反面案例这是很多新手项目里真实发生过的事# config.py class Config: def __init__(self): self.debug True self.db_host 127.0.0.1 self.db_port 3306 # 模块A config_a Config() # 模块B config_b Config()看起来没什么问题两个模块各拿各的配置对象互不干扰。但如果你用的是Config()意味着你在程序里任何地方创建它都会得到一份新的、独立的配置副本。假设某个模块运行到一半根据登录态修改了config.debug False另一个模块里的config完全不知情两边数据就对不上了。另外更现实的问题出在数据库连接上。如果你的数据库连接对象被到处创建一个后台系统同时跑着十几个模块每个模块又可能被多线程调用连接数就会被迅速耗尽数据库直接拒绝服务。单例模式的核心思想就一句话一个类在整个程序生命周期里只允许存在一个实例并且提供一个全局访问点。这不是Python的专利Java、C、Go里都有类似的套路但在Python里有好几种玩法而且门槛不同效果差异也很大。2.2 新手最容易想到的实现__new__方法先看最容易搜到的一种写法用__new__和类属性来实现class Singleton: _instance None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance super().__new__(cls) return cls._instance def __init__(self, value): self.value value a Singleton(1) b Singleton(2) print(a.value) # 2 print(b.value) # 2 print(a is b) # True这个写法的思路是重写__new__第一次创建时把实例存到类属性_instance里之后每次调用都直接返回同一个实例。这样确实做到了“同一个实例”但有个隐蔽的坑——__init__每次都会被执行。也就是说Singleton(2)创建 b 时虽然 a 和 b 是同一个实例但value却被改成了 2。这在某些场景下可能正是你要的效果比如每次调用都刷新配置但在另一些场景下会让人摸不着头脑——你以为创建了多个独立对象结果它们共享状态还被后者覆盖。什么时候用__new__这种写法合适坦白讲除了演示原理和极少数特殊场景我基本不推荐。因为它治标不治本__init__重复执行这个坑需要额外加_initialized标记去堵代码越写越拧巴。2.3 Python版的正解模块天然就是单例这里要分享一个Python老鸟都知道、但新手很少注意到的现象Python的模块本身就是单例。一个模块文件只要被导入过一次它里面的代码就只会被执行一次后续所有import拿到的都是同一个模块对象模块级的变量也是同一份。这就是实现单例最干净的方式# db_manager.py class DBManager: def __init__(self): self.host 127.0.0.1 self.port 3306 self.connected False def connect(self): if not self.connected: # 实际连接数据库的代码这里简化 print(fConnecting to {self.host}:{self.port}) self.connected True return self db_manager DBManager()别的模块需要用到DBManager时只需要from db_manager import db_manager不管你的程序里有多少个模块import db_manager拿到的都是同一个db_manager实例因为它是在模块加载时创建一次的。这就是Python所谓的“模块级单例”。这个方法的好处非常明显不需要任何设计模式的花活单纯靠Python的导入机制就实现了单例。坏处也有——对不熟悉这个套路的读者来说代码里没有一个显眼的Singleton字样如果不是注释写得清楚可能不明白为什么db_manager是全局唯一的。所以团队协作时良好的注释和文档这时候就显得格外重要。贴一个我在真实项目里用到的改进版把连接参数和连接状态都封装起来# db_manager.py class DBManager: def __init__(self): self.host 127.0.0.1 self.port 3306 self.user admin self.password secret self._connection None def connect(self): if self._connection is None: # 伪代码真实环境这里是 mysql.connector.connect(...) self._connection real-connection-object return self db_manager DBManager()2.4 进阶玩法用装饰器实现单例如果你想保留显式的Singleton语义但又不想用__new__那套偏底层的写法装饰器是个不错的折中方案def singleton(cls): _instances {} def getinstance(*args, **kwargs): if cls not in _instances: _instances[cls] cls(*args, **kwargs) return _instances[cls] return getinstance singleton class DBManager: def __init__(self): self.host 127.0.0.1 self.port 3306 db1 DBManager() db2 DBManager() print(db1 is db2) # True这个方案的优点是代码很集中一个装饰器就管住了所有被标记的类缺点是装饰器返回的是一个函数如果你依赖isinstance(db1, DBManager)这类类型判断会得到False因为db1的真实类型是getinstance函数的返回值。这个问题可以用给getinstance设置__wrapped__属性或者改用functools.wraps来缓解但整体来说会感觉有一点“跳”。2.5 真正的硬核方案用元类实现单例如果你去翻一些比较重型Python框架的源码会发现它们更喜欢用元类的方式实现单例。原因很简单元类接管了类的创建过程是在“类定义时”就规定好“只能有一个实例”的规则对调用方完全透明。class SingletonMeta(type): _instances {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] super().__call__(*args, **kwargs) return cls._instances[cls] class DBManager(metaclassSingletonMeta): def __init__(self): self.host 127.0.0.1 self.port 3306 db1 DBManager() db2 DBManager() print(db1 is db2) # True简单解释下原理正常情况下DBManager()实际上是在调用type.__call__元类SingletonMeta重写了__call__在调用前先检查字典里有没有存过这个类的实例。这个方案技术上最正宗也最能避免装饰器方案的类型问题。但说实话在日常应用开发里我建议优先用“模块天然单例”。元类虽好对团队里新手同事来说阅读成本高大家看到metaclassSingletonMeta多少会懵一下。单例模式的本质是为了控制共享状态Python的导入机制已经帮你完成了最难的“只执行一次”部分何必自己造一个更复杂的轮子。2.6 经验分享什么时候不该用单例很多新手学完单例模式之后容易走火入魔看到什么都想套单例。这里分享几个“单例怎么用是错误”的真实案例。第一个是滥用单例存临时数据。有人把用户每次请求的业务数据往单例的属性里塞理由是“这样全局都能访问”结果在高并发下数据互相覆盖莫名其妙地出现串号问题。单例适合存配置、连接池、日志器这种无状态或弱状态的共享对象不适合存与具体请求/线程绑定的数据。后者应该用上下文对象或请求级别的传递。第二个是测试场景坑。单例的全局唯一性意味着它天然难测试——测试用例之间会共享状态一个用例改了连接配置后面的用例全被带偏。解决办法是给单例类提供_reset()之类的重置方法或者在测试里重新加载模块importlib.reload这些都是后话但你要知道单例不是银弹。第三个是违反直觉的问题。写一个数值计算类也硬套单例结果所有调用方拿到的都是同一份内部状态完全违背了“每个计算独立”的直觉。记住单例只适用于那种“全程序就应该只有一个”的东西而不是“我只想要一个”。3. 类的书写规范把类写得像正规军的标准3.1 命名与结构一眼就能看懂的类说完单例模式马上进入第二个主题类的书写规范。这个东西听起来很空洞好像没什么好讲的但在实际工作中代码评审里大量时间都花在“纠正类的命名与结构”上。先说命名的硬性规则与约定这些来自 PEP 8虽然不是法律但行业里默认都遵守类名用驼峰式CamelCase比如DBManager、UserProfile、OrderService首字母大写多个单词间不用下划线。方法名和函数名用全小写下划线snake_case比如connect_db、get_user_by_id。私有属性和方法用单下划线开头比如self._connection这表示“请当作内部使用不要从外部访问”是个约定而不是强制。特殊方法魔法方法用双下划线包裹比如__init__、__repr__这类方法是Python解释器识别用的不要自己发明新的双下划线方法。再说结构。一个规整的类应该是“属性集中在__init__核心行为用方法表达且每个方法只做一件事”。我见过最典型的反面教材是一个类有一千行构造函数里塞了十几个参数每个方法又长又杂读起来像是在看一本没有目录的小说。类的核心价值是把相关状态和行为封装在一起而不是把所有代码硬塞进一个class里。3.2 属性保护与property别让外部随意改内部状态很多从Java转过来的人会有个习惯在Python里也写一堆getter和setterclass DBManager: def __init__(self): self._host 127.0.0.1 def get_host(self): return self._host def set_host(self, value): self._host value这种写法在Python里通常被认为是不符合Python风格的。Python社区的做法是直接对外暴露属性如果需要附加逻辑再用property装饰器把它包装起来class DBManager: def __init__(self): self._host 127.0.0.1 self._port 3306 property def host(self): return self._host host.setter def host(self, value): if not isinstance(value, str): raise TypeError(host must be a string) self._host value这样用起来既保持了db.host这样自然直观的访问方式又能在赋值时做校验。从外部看用户不需要知道_host的存在更不需要去调用什么get_host()方法代码读起来更像是在描述业务而不是在操作类内部机制。再补充一个很实用的技巧如果你想让属性只读只写property不写xxx.setter就可以了。class DBManager: def __init__(self): self._connection_id 0 property def connection_id(self): return self._connection_id后面如果有人试图db.connection_id 5Python会直接抛AttributeError这比你自己写一堆防御性检查整洁得多。3.3 魔法方法让对象自己会说话类书写规范里最容易让人忽略的是魔法方法。新手写类往往只写__init__其他的能不加就不加结果调试的时候到处print(obj.xxx)打印出来的全是__main__.DBManager object at 0x10a8bbf50看半天不知道是什么。最值得为每个业务类实现的两个魔法方法是__repr__和__str__。简单区分下__str__是给用户看的调用print(obj)或str(obj)时生效__repr__是给开发者和调试器看的在交互式环境里直接敲对象名回车时会用到。两者可以同时定义也可以只定义__repr__因为当你没有定义__str__时Python会使用__repr__作为兜底。class DBManager: def __init__(self, host127.0.0.1, port3306, debugFalse): self.host host self.port port self.debug debug def __repr__(self): return fDBManager(host{self.host!r}, port{self.port!r}, debug{self.debug!r}) def __str__(self): return f数据库连接管理器 ({self.host}:{self.port})注意我在__repr__里用了{self.host!r}这个!r表示“用repr()来格式化”这样当host是字符串时打印出来的会带引号看起来更明确。建议初学者把!r当常规操作记住。另外还有一个很实用的魔法方法__eq__。两个对象什么时候相等默认规则是内存地址相同才相等但业务里通常更关心内容是否相等class DBManager: def __init__(self, host, port): self.host host self.port port def __eq__(self, other): if not isinstance(other, DBManager): return NotImplemented return self.host other.host and self.port other.port有了__eq__两个不同实例只要配置相同db1 db2就是True。做测试断言时很有用不用一个一个字段比了。3.4 实例方法、类方法、静态方法的使用边界初学Python时每个类下面写的都是普通实例方法这是最自然的。但写的类多了以后就会发现有些方法不需要访问实例属性有些方法甚至需要直接作用在类本身。Python里提供了三种方法经常有人分不清方法类型装饰器第一个参数能访问类属性能访问实例属性典型使用场景实例方法无self可以可以绝大多数普通操作类方法classmethodcls可以不可以工厂方法、访问类级配置静态方法staticmethod无特殊不可以不可以与类相关的纯工具函数用DBManager来举例说明三类方法的划分class DBManager: _default_port 3306 def __init__(self, host, portNone): self.host host self.port port or self._default_port classmethod def from_unix_socket(cls, socket_path): # 工厂方法用Unix socket方式创建实例 return cls(localhost, port0, unix_socketsocket_path) staticmethod def is_valid_port(port): # 与实例无关的校验逻辑 return 0 port 65535这里的from_unix_socket就是一个典型的类方法——它本质上是“另一种创建实例的方式”所以用cls来调用构造函数这样不管子类怎么继承创建出来的还是对应子类的实例。is_valid_port则是纯工具函数跟具体实例无关放静态方法里清晰明了。很多新手分不清classmethod和staticmethod记住一句话就行如果方法内部要用到类本身比如调用cls()创建实例、访问类变量用类方法如果只是碰巧跟这个类相关、但完全不需要类或实例的数据用静态方法。两者都能通过ClassName.method_name()调用但语义和灵活性差别很大。3.5 面向对象设计的落地版组合优于继承“类的书写规范”不只包括格式还涉及设计层面。很多人一学面向对象就爱用继承仿佛不继承一下就不算面向对象。我见过的坑有人用继承表达“用户”和“管理员”的关系时硬是搞出UserBase - AdminUser - SuperAdminUser - SystemAdminUser这种四层继承链改一处逻辑要顺藤摸瓜改一个多月。Python社区更推崇的一个原则是组合优于继承。什么意思举个例子DBManager如果想记录日志新手会写class LoggingDBManager(DBManager)然后在里面覆写每个方法加上日志调用。但更Python的做法是让DBManager持有一个Logger对象class DBManager: def __init__(self, host, port, loggerNone): self.host host self.port port self.logger logger # 组合持有Logger对象 def connect(self): if self.logger: self.logger.info(fConnecting to {self.host}:{self.port}) # ...这样DBManager根本不需要知道logger到底是怎么实现的只要它有个info方法就行。以后你想换日志库或者换成测试用的伪logger直接传入新对象即可DBManager的代码一行都不用改。这就是组合带来的灵活性和低耦合。另一个设计落地的点是“保持接口简单”。一个类对外暴露的方法越少越好。每次添加一个公开方法前先问自己这个方法的调用方真的有吗还是只是“万一以后用得上”在Python里所有方法默认都是公开的所以公开一个方法意味着你承诺了其他人可以依赖它的存在多一个公开方法就多一层保障成本。3.6 一个规范化的完整类示例把上面这些技巧汇总一下写一个规范版本的DBManager给你直接抄作业from typing import Optional class DBManager: 管理数据库连接的单例类。 使用方式 from db_manager import db_manager db_manager.connect() _default_port 3306 def __init__(self, host: str 127.0.0.1, port: int 3306, user: str root, password: str ) - None: self._host host self._port port if port else self._default_port self._user user self._password password self._connection None self.connected False classmethod def from_config(cls, config_path: str) - DBManager: 从配置文件创建实例类方法的典型案例 # 这里简化为直接返回 return cls(host192.168.1.10, port3306) property def host(self) - str: return self._host host.setter def host(self, value: str) - None: if not isinstance(value, str) or not value.strip(): raise ValueError(host must be a non-empty string) self._host value property def port(self) - int: return self._port def connect(self) - None: 建立数据库连接 if self.connected and self._connection is not None: return # 这里写真实连接逻辑比如 mysql.connector.connect(...) self._connection fake-connection self.connected True def close(self) - None: 关闭数据库连接 self._connection None self.connected False def __repr__(self) - str: return fDBManager(host{self._host!r}, port{self._port!r}) def __str__(self) - str: return f数据库连接管理器 ({self._host}:{self._port}) # 模块级单例模块只加载一次所以这个实例全局唯一 db_manager DBManager()这个类有清晰的文档字符串docstring、类型标注、属性受保护、提供__repr__/__str__、包含类方法和静态方法的使用示范、最后用模块级变量实现单例。把一个简单需求写成这样就已经具备生产级代码的基本素养了。4. 包模块的制作与导入从零手写一个可复用包4.1 import的本质与搜索路径先从最基本的问题讲起。import到底做了什么简单说就是找到文件、执行文件、把执行结果中的名字绑定到当前命名空间。那么Python去哪里找文件呢就是sys.path一个路径列表。Python按照这个列表的顺序依次查找直到找到匹配的文件。sys.path通常包含当前脚本所在的目录或者交互式运行时的当前工作目录标准库目录第三方包所在的site-packages目录如果你设置了PYTHONPATH环境变量里面定义的路径也会被加进去这就解释了一个新手常遇到的问题为什么我在顶层文件夹里写了包子目录里导入就报ModuleNotFoundError因为运行时的当前目录不同sys.path里的相对位置就不同。排查这类问题时可以直接在代码里打印sys.path看看或者临时把需要导入的路径加进sys.pathimport sys sys.path.append(/path/to/your/project)但这只是临时解决办法正经项目还是推荐用标准的包管理方式配置项目结构。4.2 绝对导入与相对导入理解Python的导入家族当项目文件多了以后导入语句怎么写就成了大学问。Python支持两种导入方式绝对导入是从项目的根目录出发写路径比如from myproject.database.db_manager import db_manager这种方式最清晰推荐在大型项目里统一使用。前提是你需要把项目的根目录设为Python搜索路径的一部分通常通过在根目录运行脚本或者安装为可编辑包后面会讲来实现。相对导入用点号来表示当前位置比如在user_module.py里导入同级的另一个模块from .db_manager import db_manager那个前置点表示“当前包的相对位置”两个点表示“上一层包”三个点表示“再上一层”。相对导入的好处是移动整个包时不怎么需要改导入路径坏处是只能用在包内部不能用在直接作为脚本运行的文件里。这也是新人经常报的一个错误attempted relative import with no known parent package翻译成人话就是——你运行的顶楼文件本身就是没有父包的没法用相对导入。我的经验是包与包之间的导入用绝对导入明确、直观包内部的兄弟模块之间的导入可以用相对导入简洁、利于移动。但如果你图省心全用绝对导入也完全没问题只要你把项目根目录搞对。4.3 包的目录结构与__init__.py在讲“制作包”之前必须先建立正确的目录概念。在Python里任何一个包含__init__.py文件的文件夹都会被视作一个包严格来说是“常规包”。__init__.py的作用是告诉解析器“这个目录是个包”并可以在这个文件里初始化包级别的变量、控制对外导出的内容。一个典型的项目结构长这样myproject/ ├── __init__.py ├── database/ │ ├── __init__.py │ └── db_manager.py ├── modules/ │ ├── __init__.py │ ├── user_module.py │ └── order_module.py ├── tests/ │ ├── __init__.py │ └── test_db_manager.py └── main.py这里的database/和modules/都是子包它们各自的__init__.py可以做如下事情# myproject/database/__init__.py from .db_manager import db_manager # 让用户可以 from myproject.database import db_manager __all__ [db_manager]注意__all__这个变量它定义了from myproject.database import *的时候会导入哪些名字。虽然不是强制写但建议写上它与“隐藏私有对象”配合使用可以让你的包对外接口非常明确。4.4if __name__ __main__:的守护原理每个Python初学者都背过这行代码但真正理解它的价值往往要到项目变大以后。在Python里每个模块都有一个内置变量__name__。当这个模块被直接运行的时候__name__的值是__main__而当这个模块被别的模块导入的时候__name__的值是模块自己的名字比如db_manager。所以这行代码的意思是只有当这个文件是被直接运行时才执行下面的代码。被导入时不执行。这个机制有什么用它的价值体现在“既能作为库被导入又能作为脚本直接运行”。拿DBManager来举例# db_manager.py class DBManager: ... db_manager DBManager() if __name__ __main__: # 只有直接运行 python db_manager.py 时才执行 db_manager.connect() print(db_manager)另一种常见用法是当单独调试一个模块时if __name__ __main__: db DBManager() db.connect() print(db.connected)这样你写测试性代码不会污染生产环境的导入过程用起来非常舒服。开发阶段可以靠这句做临时调试项目正式跑起来时又不会被这些调试代码干扰。4.5 一个完整的包落地示例把前面的内容整合到一起我带你手写一个名叫simple_db的完整包。先建目录结构simple_db/ ├── __init__.py ├── db_manager.py └── config.pydb_manager.py里放我们的类定义和单例实例# simple_db/db_manager.py from .config import DEFAULT_CONFIG class DBManager: def __init__(self, **config): self.host config.get(host, DEFAULT_CONFIG[host]) self.port config.get(port, DEFAULT_CONFIG[port]) self.user config.get(user, DEFAULT_CONFIG[user]) self.connected False def connect(self): self.connected True def __repr__(self): return fDBManager({self.host}:{self.port}) db_manager DBManager()config.py里集中管理配置项# simple_db/config.py DEFAULT_CONFIG { host: 127.0.0.1, port: 3306, user: root, }__init__.py里统一对外导出# simple_db/__init__.py from .db_manager import DBManager, db_manager from .config import DEFAULT_CONFIG __all__ [DBManager, db_manager, DEFAULT_CONFIG]然后在项目主文件里这么用# main.py import sys sys.path.append(/path/to/simple_db的上级目录) from simple_db import db_manager db_manager.connect() print(db_manager)这里又回到了sys.path的问题。在正式项目里我们不希望每个使用者都手动追加路径所以现在标准的做法是把你的包安装进环境。这就牵扯到下一节内容。4.6 让包可以被installpyproject.toml 与依赖管理在Python 3.8之后社区逐渐统一用pyproject.toml来声明包的信息。这个文件放在包的项目根目录不是包目录本身描述“这个项目叫什么、版本多少、需要哪些依赖、包里的哪些文件要一起发布”等信息。继续拿simple_db举例项目根目录创建pyproject.toml[build-system] requires [setuptools61.0] build-backend setuptools.build_meta [project] name simple-db version 0.1.0 description A simple database manager for demo requires-python 3.8 [project] dependencies []然后在这个文件所在的目录执行pip install -e .-e是--editable的缩写意思是“以可编辑模式安装”。这个模式非常贴心它不会把你的代码拷贝到site-packages里而是创建一个链接指向源码目录。这样你修改源码后所有导入这个包的地方立刻生效不需要重新安装一遍。开发阶段强烈推荐用可编辑模式。安装完之后你就可以在任何目录里直接from simple_db import db_manager了因为pip已经帮你在sys.path里配置好了访问路径。4.7 发布到PyPI让别人pip install你的包更进一步如果你写的包不仅自己用还想分享给朋友或者发布到公网可以走一遍PyPI发布流程。这里简要说明步骤注册PyPI账号去 pypi.org 注册账号。安装发布工具pip install build twine构建发布包python -m build上传twine upload dist/*这样别人就能直接pip install simple-db了。对于大多数初学者来说发布到PyPI可能不是现阶段刚需但理解这条路径很重要——它能让你的代码从“自己电脑上能跑”变成“所有人都能安装”这是从学习者走向开源贡献者的一条完整闭环。至于包版本号怎么规划、文档怎么写、许可证选哪个这些属于进阶范畴看到这里你已经有能力自己查文档了这里不展开。5. 常见错误与排查技巧实录5.1 循环导入包一大了必踩的坑循环导入是包化改造过程中最经典、最难排查的错误没有之一。先说场景a.py里写了from b import func_b然后b.py里又写了from a import func_a。Python加载a.py时往下走遇到from b import func_b于是先加载b.py加载b.py时又遇到from a import func_a但此时a.py还没执行完模块缓存里只有个半成品的afunc_a根本还不存在于是抛出ImportError: cannot import name func_a from partially initialized module a。很多人的第一反应是“那个名字明明在a.py里写了怎么导入不了”其实不是你的名字有问题是加载顺序导致的半初始化问题。排查思路有这么几个把from a import func_a改成熟人做法import a然后在函数内部使用a.func_a()。因为函数体执行时a模块已经加载完成就不会再报错了。把公共代码抽到第三个模块让两个模块都依赖第三个模块而不是互相依赖。把导入语句放进函数内部延迟到调用时再导入也可以绕过加载时序问题。循环导入的本质是设计问题说明模块之间的依赖关系应该被打破了。最干净的解法是抽出公共部分而不是用“延迟导入”糊弄过去。但有时只是临时更新代码用函数内导入救急也是合理的。5.2 相对导入出包范围报错 “attempted relative import with no known parent package”这个问题前面提过但值得反复强调因为几乎每个新手都会踩。你写了一个包mypkg里面有个子模块tools.py在这个子模块里写from .config import CONFIG然后你直接运行python tools.pyPython会立刻报错。原因很简单相对导入的意义是“相对于当前包”但当你直接运行tools.py时Python把它当作顶层脚本没有任何包上下文“当前包”根本不存在。正确做法有两种从项目根目录运行入口脚本让相对导入位于包结构内部。在tools.py里改用绝对导入或者sys.path处理。我的建议是你在写src布局的包时永远不要直接运行子模块而是通过项目入口或测试来运行。这样既符合包的使用方式也避免了这类错误。5.3 修改类属性但实例表现不一致单例模式的隐藏问题再回到单例模式分享一个我在实际项目里踩过的坑。有一次我定义了一个类类属性DEBUG True然后在代码里通过实例修改了obj.DEBUG False之后新创建的实例DEBUG还是True。当时排查了很久才发现通过实例给类属性赋值并不会修改类属性而是给这个实例创建了一个同名的实例属性从此这个实例访问到的DEBUG是自己的实例属性而其他实例和类本身访问的还是原来的类属性。这不仅是单例模式会遇到的问题任何类属性都会这样。想修改类级配置应该用ClassName.DEBUG False而不是obj.DEBUG False。如果类属性应该被所有实例共享更安全的做法是用_x私有属性加property进行管理对外只暴露读写接口从源头上避免这种误用。5.4 覆写__str__不生效你是不是覆盖了__repr__而没覆盖__str__还有一个特别容易混淆的迷惑点。有人在类里写了__repr__然后用print(obj)发现输出还是那个丑陋的object at 0x...。原因在于print()调用的是__str__如果你没有定义__str__Python会尝试用__repr__做兜底——注意是“Python会尝试用__repr__兜底”这个行为发生在所有继承链的某个位置。如果你继承的基类已经实现了__str__而你只覆写了__repr__那么基类的__str__优先级更高你的__repr__根本不会被print()用到。反过来也一样。所以要改print(obj)的输出就直接覆写__str__两条都覆写最保险不要指望一个方法通吃。5.5 模块越来越大的重构信号很多人的包写着写着一个.py文件从100行长到500行、1000行还在往里塞类。这里分享几个“该拆分了”的信号每个都是我亲眼见过的真实项目一个模块里塞了好几个不相关的类。比如db_manager.py里既有DBManager又有UserManager还有OrderManager彼此之间几乎没有关联。一个文件里存在大量只能被某一个类使用的“工具函数”。这些函数该跟着类走要么放类内部要么拆到独立的utils.py。职责分离不够。比如一个类既管数据库连接又管日志记录又管发送邮件改一行业务需求要动三个层级的代码。判断标准很朴素的一个模块应该只有一个主要职责。你说不出来这个模块的核心职责是什么或者一句话要说很多个“和”那大概率该拆了。拆的时候先按业务功能分文件再按依赖关系分层次最后再来调整导入关系。切忌一开始就边拆边改容易拆出循环导入。5.6 面向实际的上手指南一页纸的类与包自查清单每次我做完代码评审最后都会给同事发一份自查清单。放在这里你写代码时也可以直接对照[ ] 类名是否是驼峰式方法名是否是蛇形[ ]__init__是否足够简洁属性是否都是“对象应有的状态”而不是临时变量[ ] 内部状态是否用_开头保护对外暴露的属性是否用了property[ ] 是否实现了__repr__建议和__str__需要print时[ ] 与类相关但不依赖实例的辅助逻辑是否用classmethod/staticmethod表达清楚[ ] 包目录下是否都有__init__.py对外导出的名字是否通过__all__控制[ ]pip install -e .是否能成功安装安装后能否在任意目录执行from 你的包 import 核心对象[ ] 同一个包内部是否有循环导入的隐患[ ] 是否有某个.py文件长到超过300行且包含多个不相关的类这份清单不是教条而是我多年来评审代码时总结出的最小检查集合。每一条都对应着真实项目里出现过的问题。你自己写代码时每过一条整体质量都在往正规军靠拢。说句实在话把单例模式、类规范、包与模块放在一起学最大的收获不是记住了三个名词而是建立了一种“组织者的视角”——写代码之前先想好对象怎么管理、文件怎么规划、依赖怎么梳理。这种视角一旦建立你再看别人的开源项目就不再是看天书了而是能顺着包结构看出作者的思考轨迹。我个人这几年带新人的经验里凡是能把这些基础串起来的人后续看框架源码、写独立工具包速度都会快上好几倍。希望这篇整理能帮你把这条路上最关键的一段走扎实。