
前言Python 里凡是名字前后各带两条下划线的东西例如__init__、__repr__、__dict__社区俗称「魔术方法」magic method或 dunderdouble underscore 的缩写。它们不是给程序员随便调用的而是语言本身在特定时刻会去调用的钩子写len(obj)时解释器实际调用的是obj.__len__()写obj other时调用的是obj.__add__(other)。围绕它们有两个典型误解。第一把「内置方法」和「内置函数」混为一谈——len()是内置函数__len__是你可以在自己类里实现的协议方法两者是调用者与被调用者的关系。第二以为__str__不定义就没法打印——实际上print(obj)会退而调用__repr__而__repr__由object提供了一个默认实现只是输出不好看而已。本文讲清属性attribute与方法method两套机制给出能直接运行的示例并说明哪些行为是文档保证的、哪些只是当前实现的细节。示例基于 Python 3.8。一、先分清「属性」与「方法」在 Python 的对象模型里一切都在字典里。实例属性存在__dict__方法本质上就是存在类字典里的函数对象通过描述符协议在访问时绑定出self。名字类型含义在哪里__dict__属性对象/类的属性字典实例与类都有__class__属性所属类实例__name__属性名字类、函数、模块__doc__属性文档字符串类、函数、模块__module__属性定义所在的模块名类、函数__qualname__属性限定名含外层类名类、函数3.3__bases__属性直接父类元组类__mro__属性方法解析顺序类__slots__属性声明式属性表省内存类可选# 适用于 Python 3.8class Point:二维点。dims 2def __init__(self, x, y):self.x xself.y yp Point(1, 2)print(p.__dict__) # {x: 1, y: 2}print(p.__class__) # class __main__.Pointprint(Point.__name__) # Pointprint(Point.__doc__) # 二维点。print(Point.__bases__) # (class object,)print(Point.dims, p.dims)# 2 2类属性实例也能读到注意p.__dict__里没有dims——它是类属性。找属性时先查实例字典再顺着__mro__查类字典。给实例赋值p.dims 3会新建一个实例属性遮蔽类属性不影响别的实例。二、对象的构造与表示方法何时被调用返回值约定__new__(cls, ...)创建实例最先执行返回实例__init__(self, ...)初始化实例必须返回None__repr__(self)repr()、交互式回显、调试str尽量可还原__str__(self)str()、print()str面向人__format__(self, spec)f-string 与format()str__hash__(self)hash()、放进set/dict键int__eq__(self, other)任意按约定给bool# 适用于 Python 3.8class Money:def __init__(self, amount, currencyCNY):self.amount amountself.currency currencydef __repr__(self):return fMoney({self.amount!r}, {self.currency!r})def __str__(self):return f{self.amount} {self.currency}m Money(12.5)print(repr(m)) # Money(12.5, CNY)print(str(m)) # 12.5 CNYprint(m) # 12.5 CNYprint 用的是 __str__print([m]) # [Money(12.5, CNY)]容器用的是 __repr__经验法则__repr__给开发者看越像构造表达式越好__str__给最终用户看。只定义__repr__时str()会自动回退到它。三、容器协议与迭代协议想让自定义对象支持len()、下标、for循环、in判断就实现对应协议。方法触发它的语法__len__len(obj)__getitem__obj[key]__setitem__obj[key] v__delitem__del obj[key]__contains__x in obj__iter__for x in obj、iter(obj)__next__next(obj)__call__obj(...)__enter__/__exit__with obj:# 适用于 Python 3.8class Countdown:def __init__(self, start):self.start startdef __iter__(self):n self.startwhile n 0:yield nn - 1def __len__(self):return self.startcd Countdown(3)print(len(cd)) # 3print(list(cd)) # [3, 2, 1]class Multiplier:def __init__(self, factor):self.factor factordef __call__(self, x):return x * self.factordouble Multiplier(2)print(double(21)) # 42print(callable(double)) # True只要定义了__call__实例就能像函数一样被调用callable()也会返回True。四、哪些行为是保证的__dict__的存在、__mro__的线性化顺序、属性查找顺序都是文档保证的。但下面这些属于实现细节实例属性在__dict__里的顺序CPython 中通常是插入顺序但不应依赖__slots__能省内存但它会去掉实例的__dict__一旦定义就只能给列出的名字赋值无法再动态添加属性定义了__eq__却没定义__hash__Python 会把该类的__hash__置为None实例立即变成不可哈希、不能放进set或当字典键。# 适用于 Python 3.8class Item:def __init__(self, sku):self.sku skudef __eq__(self, other):return isinstance(other, Item) and self.sku other.sku__hash__ None # 显式写出这个事实print(Item(A) Item(A)) # True# {Item(A)} # TypeError: unhashable type常见坑点坑 1__init__里写了return。❌class C:def __init__(self):return 1✅__init__必须返回None或不写 return否则实例化时抛TypeError: __init__() should return None。想控制返回值要改__new__。坑 2只写__str__就以为调试输出够用。❌ 容器、交互式回显用的都是__repr__print(list_of_obj)里全是默认的内存地址形式形如 object at 0x 加一串十六进制。✅ 至少实现__repr__再按需实现__str__。坑 3定义__eq__后忘记__hash__。❌ 对象放进set或当dict键时报unhashable type。✅ 要么一起实现__hash__要么明确该类设计为不可哈希。坑 4__eq__里没做类型检查。❌def __eq__(self, other):return self.sku other.sku # other 可能是任何类型✅ 先if not isinstance(other, Item): return NotImplemented让解释器有机会走对方的比较逻辑。坑 5以为self.__x是私有的。❌ 以为双下划线能阻止外部访问。✅ 双下划线只做名字改写name mangling在类C里写self.__x实际是self._C__x仍然能被访问。约定上受保护用单下划线_x表示但它同样只是约定。坑 6给类加__slots__后还想动态加属性。❌ 定义__slots__ (x,)之后写obj.y 1抛AttributeError。✅ 把y也列进__slots__或在__slots__里保留__dict__这样就不再省内存了。坑 7__len__返回了非整数。❌def __len__(self): return 3——len(obj)抛TypeError。✅ 返回int且非负bool(obj)也可以用__len__的结果为 0 即假所以别让__len__有副作用。坑 8在__repr__里访问会触发大量计算或 IO 的属性。❌__repr__里读数据库、打印大对象全量内容导致调试时更卡。✅__repr__保持轻量、无副作用只输出标识性的少量字段。总结类别代表方法 / 属性一句话提醒属性__dict__、__class__、__doc__、__mro__查找顺序实例字典 → 类 MRO构造__new__、__init____init__必须返回None表示__repr__、__str__、__format__至少实现__repr__比较与哈希__eq__、__hash__定义__eq__会连带清空__hash__容器与迭代__len__、__getitem__、__iter__、__contains__协议齐全才写得顺手调用与上下文__call__、__enter__、__exit__with语句靠这两个理解 dunder 的关键是转换视角它们不是要背的方法名而是语言在某个语法被使用时去找的钩子。想知道某个类实现了哪些钩子直接print(类名.__mro__)再help(类名)或者读object的文档不要凭印象猜哪个魔术方法存在、参数是不是self, other。本主题与 Python 2 的主要差别在于Python 2 里存在__unicode__、__nonzero__、__cmp__这些方法在 Python 3 中分别被__str__、__bool__、__eq__体系取代。Python 2.7 已于 2020 年 1 月 1 日停止维护新代码不要再用旧方法名。