Python双下划线详解:__name__与魔术方法/属性全面整理(含版本限制)
前言
Python 里到处都是双下划线:__name__、__init__、__all__、__getattr__……刚学的时候只知道 if __name__ == '__main__' 这么抄,不清楚背后的整套设计。这篇把双下划线家族(俗称 dunder,double underscore 的缩写)按用途全面整理一遍,每个都带小样例,有版本要求的标明版本。
先给个总原则(PEP 8 的原话):__xxx__ 这种前后双下划线的命名权属于 Python 解释器,自己写类不要发明这种名字,想表达内部属性用单下划线 _foo 或者双下划线前缀 __foo(后面讲它的改写机制)。
1、name:模块的身份牌
每个模块被导入时都有 __name__ 属性,值取决于它怎么被使用:
|
|
|
|
这就是 if __name__ == "__main__": 的意义:直接运行时执行入口逻辑,被导入时只提供定义不执行。
几个容易忽略的行为:
|
|
所以包内模块里写 if __name__ == "__main__" 的测试代码,用 python -m pkg.mymod 方式也能触发。另外 Flask/Django 的 debug 模式也是靠判断主模块来决定行为的。
2、常用的模块级双下划线属性
2.1 file:当前模块的文件路径
|
|
版本注意:Python 3.9 起 __file__ 保证是绝对路径;3.8 及以前是「执行 python 命令时的相对路径」,脚本里拼路径前必须先 abspath,老版本不处理的话换个工作目录就找不到文件。
2.2 doc:文档字符串
模块/类/函数的第一个字符串会被存进 __doc__,help() 就是读它:
|
|
2.3 all:from xxx import * 的白名单
|
|
|
|
它只是约定不是强制隔离,但对库作者来说,写 __all__ 是明确告诉使用者「这些才是公开 API」。
2.4 debug:解释器内置常量
|
|
assert 语句在 -O 模式下也会被整体剔除,生产脚本可以用 -O 提速(前提是 assert 里没有业务逻辑)。
2.5 其他常遇到的
| 属性 | 含义 |
|---|---|
__package__ |
模块所属包名 |
__spec__ |
模块的导入规格(importlib 元数据,3.4+) |
__module__ |
类或函数定义所在的模块名 |
__qualname__ |
限定名,含外层作用域路径(3.3+),嵌套类/函数里和 __name__ 有区别 |
__dict__ |
对象/模块的属性字典 |
__version__ |
惯例上的库版本号(非语言机制,纯约定) |
|
|
3、类的魔术方法:按用途分类
魔术方法(magic methods)不需要手动调用,解释器在特定语法下自动触发。按用途分组记最省力。
3.1 构造与销毁:new / init / del
|
|
__new__ 负责创建并返回实例(单例、不可变类型子类必须用它),__init__ 只负责初始化已创建的实例。__del__ 是对象被回收时的钩子,但回收时机依赖 GC,别在里面写关键逻辑(关文件请用 with)。
3.2 展示:repr / str
|
|
只写一个的话写 __repr__——str() 找不到 __str__ 会兜底用 __repr__,而 repr() 不会反向兜底。
3.3 比较与哈希:eq / hash
|
|
规则:__eq__ 定义后 __hash__ 会被置 None(实例变不可哈希),要放集合里就必须同时定义 __hash__,且「相等的对象哈希值必须相等」。懒人方案是 @functools.total_ordering——只写 __eq__ 和 __lt__,自动补全其余比较。
3.4 运算符:add 及其两兄弟
|
|
每个运算符都有三个版本:普通 __add__、反向 __radd__(左边不是本类型时先试它)、就地 __iadd__(不定义时 += 退回 add)。
3.5 让对象像容器:len / getitem / iter / contains
|
|
只要实现了 __getitem__,for 循环和切片就自动可用;实现 __iter__ + __next__(__next__ 没数据时 raise StopIteration)就是标准迭代器协议。
3.6 可调用对象:call
|
|
3.7 布尔与类型转换:bool / index
|
|
3.8 属性访问三件套:getattr / getattribute / setattr
|
|
记住分工:__getattribute__ 无条件拦截,__getattr__ 是兜底。动态代理、ORM 的字段懒加载都是 __getattr__ 的典型应用。
3.9 上下文管理器:enter / exit
|
|
异步版本 __aenter__ / __aexit__ 配合 async with 使用,Python 3.5+。
3.10 描述符协议:get / set(property 的底层)
|
|
property、ORM 字段、@classmethod 的底层全是描述符。__set_name__ 是 3.6+ 新增,省掉了以前手写属性名的样板。
3.11 子类定制钩子:init_subclass / class_getitem
|
|
__class_getitem__ 是 3.7+ 的,list[int]、dict[str, int] 这种泛型下标语法就靠它(__class_getitem__ = classmethod(GenericAlias) 的写法见 typing 源码)。
3.12 结构化模式匹配:match_args
|
|
Python 3.10+ 才有 match 语句,__match_args__ 指定按位置解构时的属性顺序。
3.13 序列化与内存:getstate / setstate / sizeof
|
|
版本注意:object.__getstate__ 的默认实现是 3.11+ 才加的,3.10 及以前 copyreg 对带 __slots__ 对象的处理要自己写。__sizeof__ 返回对象真实内存占用(sys.getsizeof 底层调它),一般不用自己实现。
4、slots:限制属性、省内存
|
|
两个用途:防止随手乱加属性;海量小对象(百万级)省内存效果明显——没有 __dict__ 了。代价:不能动态加属性、和某些多重继承组合冲突。dataclass 在 3.10+ 可以直接 @dataclass(slots=True) 自动生成。
5、名称改写(name mangling):__foo 双下划线前缀
前面说的 __xxx__(前后都有)是给解释器的;只有前导双下划线、无尾随的 __foo 是另一种机制——名称改写:
|
|
目的是避免子类无意覆盖父类属性(继承时 __password 改写成 _Account__password,子类的 __password 是 _Sub__password,互不干扰),而不是安全加密——真想访问随时能访问,它只是防误用。
单下划线 _foo 是纯约定(「内部使用,请勿直接碰」),from module import * 不会导入它;交互式解释器里 _ 表示上一个表达式的结果;i18n 场景 _ = gettext.gettext 也是惯用法。
6、速查总表
| 名称 | 用途 | 版本 |
|---|---|---|
__name__ / __main__ |
模块身份与入口判断 | 很早 |
__file__(绝对路径保证) |
模块路径 | 绝对路径 3.9+ |
__all__ |
import * 白名单 | 很早 |
__debug__ |
-O 优化开关 | 很早 |
__qualname__ |
限定名 | 3.3+ |
__new__ / __init__ |
实例创建/初始化 | 很早 |
__repr__ / __str__ |
展示 | 很早 |
__eq__ / __hash__ |
相等与哈希 | 很早 |
__add__ / __radd__ / __iadd__ |
运算符三兄弟 | 很早 |
__len__ / __getitem__ / __iter__ / __contains__ |
容器协议 | 很早 |
__call__ |
实例可调用 | 很早 |
__getattr__ / __getattribute__ / __setattr__ |
属性访问 | 很早 |
__enter__ / __exit__ |
with 上下文 | 很早 |
__aenter__ / __aexit__ |
async with | 3.5+ |
__imatmul__ 等矩阵运算 |
@ 运算符 | 3.5+ |
__init_subclass__ / __set_name__ |
子类钩子/描述符命名 | 3.6+ |
__class_getitem__ |
泛型下标 list[int] |
3.7+ |
__match_args__ |
match-case 位置匹配 | 3.10+ |
object.__getstate__ 默认实现 |
pickle | 3.11+ |
@dataclass(slots=True) |
dataclass 配 slots | 3.10+ |
总结
双下划线家族分三层记:模块层(__name__、__file__、__all__,管身份和导出)、类协议层(魔术方法,是各种语法糖的底层——== 是 __eq__、with 是 __enter__、for 是 __iter__,理解了这层,很多「语法」其实只是「协议」)、命名约定层(_foo 靠自觉、__foo 有改写、__foo__ 属于解释器)。写业务代码高频的就那几个(__init__/__repr__/__eq__/__enter__),写框架和库才会用到描述符和子类钩子这些高级货,按需查阅上面的速查表即可。
- 原文作者:Anttu
- 原文链接:https://anTtutu.github.io/post/2023-09-15-python-dunder-magic-methods/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。