[python] Python魔法方法完全指南

Python的字符串、列表和数字之所以好用,一半在于能保存数据,一半在于可以用熟悉的方式操作这些类型,比如用print()显示内容,用len()获取长度,用+完成相加,用for逐项遍历。定义一个类时,自然也希望这个类的对象能用上这些写法,而不必为每种操作另起一个方法名。

这些写法背后都对应着Python预先约定好的一组特殊方法名,也就是魔法方法。基本结构很固定,方法名由前后各两个下划线包住,例如__len__和__add__,定义在类里,由Python在特定操作发生时自动调用。print()会去找__str__,len()会去找__len__,+会去找__add__。类里实现了哪个方法,对象就接上了哪种写法,没实现,对应操作就会报错。

这样做的好处是让对象更容易使用,也让代码更容易理解。类的作者可以把具体实现留在内部,只向外提供一致的行为。没有魔法方法时,只能给每个操作另起名字:

python 复制代码
class Shelf:
    def __init__(self, books):
        self.books = books

    def count(self):
        return len(self.books)

    def get(self, i):
        return self.books[i]

shelf = Shelf(['a', 'b'])
shelf.count()     # 2
shelf.get(0)      # 'a'

# 下面四行都会抛TypeError,注释掉后整段仍可运行
# len(shelf)
# shelf[0]
# [x for x in shelf]
# sorted(shelf)

有了魔法方法,对象就能直接用语言已有的写法:

python 复制代码
class Shelf:
    def __init__(self, books):
        self.books = books

    def __len__(self):
        return len(self.books)

    def __getitem__(self, i):
        return self.books[i]

    def __iter__(self):
        return iter(self.books)

shelf = Shelf(['a', 'b'])
print(len(shelf))          # 2
print(shelf[0])            # a
print([x for x in shelf])  # ['a', 'b']
print(sorted(shelf))       # ['a', 'b']

从上面例子可以看到,魔法方法让自定义类无缝融入Python生态,没有魔法方法,类行为笨拙,要记各种专用方法名,也无法与内置函数和语法协作。

Python提供的魔法方法很多,覆盖了创建与显示、容器、运算、上下文管理、属性访问、类型与保存等不同场景。下面结合这些场景,说明各自有哪些魔法方法,以及该怎么用。更详细的介绍见Python魔法方法全面详解和Python中魔法函数全面系统详解。

目录

1 相关概念

1.1、创建与显示

对象从创建到销毁的过程中,创建、显示、销毁各有对应的魔法方法:

  • __new__:真正把对象造出来。
  • __init__:对象造出来后,往里塞初始数据。
  • __str__:print时显示给人看的内容。
  • __repr__:调试时显示的对象样子。
  • __format__:f"{obj:...}"里冒号后面的格式由它管。
  • __bytes__:bytes(obj)时怎么变成字节串。
  • __del__:对象被回收前通知一下,但不一定马上发生,也不保证一定发生。

示例代码如下:

python 复制代码
class Demo:
    def __new__(cls, x):  # 造对象
        print("__new__:造对象"); return super().__new__(cls)

    def __init__(self, x):  # 塞初始数据
        print("__init__:塞初始数据"); self.x = x

    def __str__(self):  # print给人看
        return f"我是Demo,x={self.x}"

    def __repr__(self):  # 调试看
        return f"Demo(x={self.x!r})"

    def __format__(self, spec):  # 管f"{obj:...}"冒号后的格式
        return str(self) if spec == "" else f"x={self.x:{spec}}"

    def __bytes__(self):  # bytes(obj)
        return str(self.x).encode()

    def __del__(self):  # 回收前通知,不保证时机
        print("__del__:对象快没了")


d = Demo(42)
print(d)            # __str__:我是Demo,x=42
print(repr(d))      # __repr__:Demo(x=42)
print(f"{d:.2f}")   # __format__:x=42.00
print(bytes(d))     # __bytes__:b'42'
del d               # __del__:对象快没了

1.2、容器

对象除了能创建、显示,还可以像列表或字典一样被使用,靠的就是下面这些方法:

  • __len__:有多长,不能是负数。
  • __getitem__:按下标或键取值,切片时拿到slice。
  • __setitem__:按下标或键写入。
  • __delitem__:按下标或键删除。
  • __contains__:在不在里面。
  • __iter__:交出一个迭代器。
  • __next__:取下一项,取完报StopIteration。
  • __reversed__:反向遍历用的迭代器。
  • __missing__:字典缺键时给个默认值。
  • __length_hint__:给个数量估计,只对没有__len__的迭代器生效。

示例代码如下:

python 复制代码
class BoxIter:  # 独立迭代器,游标不落在Box实例上
    def __init__(self, keys):
        self.keys = list(keys)
        self.i = 0

    def __iter__(self):
        return self

    def __next__(self):  # 取下一项,取完报StopIteration
        if self.i >= len(self.keys):
            raise StopIteration
        self.i += 1
        return self.keys[self.i - 1]

class Box(dict):  # 像字典/列表一样用
    def __len__(self):  # 长度
        return dict.__len__(self)

    def __getitem__(self, key):  # 取值,切片时key是slice
        return dict.__getitem__(self, key)

    def __setitem__(self, key, value):  # 写入
        dict.__setitem__(self, key, value)

    def __delitem__(self, key):  # 删除
        dict.__delitem__(self, key)

    def __contains__(self, key):  # in
        return dict.__contains__(self, key)

    def __iter__(self):  # 交出迭代器
        return BoxIter(dict.keys(self))

    def __reversed__(self):  # 反向遍历用的迭代器
        return reversed(list(dict.keys(self)))

    def __missing__(self, key):  # 字典缺键时给个默认值
        return 0

    def __length_hint__(self):  # 数量估计,Box有__len__,实际走不到这里
        return dict.__len__(self)

b = Box({"a": 1, "b": 2})
print(len(b))             # __len__:2
print(b["a"])             # __getitem__:1
b["c"] = 3                # __setitem__
print(b["c"])             # 3
del b["c"]                # __delitem__
print("a" in b)           # __contains__:True
print(list(b))            # __iter__ + __next__:['a', 'b']
print(list(reversed(b)))  # __reversed__:['b', 'a']
print(b["x"])             # __missing__:0
print(b.__length_hint__())  # __length_hint__:2,直调才看得到

1.3、运算与比较

对象除了能创建、显示、当容器用,还能像数字一样参与运算和比较:

算术

常见的有正向、反向、原地修改三类,反向用__rxxx__,原地修改用__ixxx__。

  • __add__、__sub__、__mul__:加、减、乘。
  • __truediv__、__floordiv__、__mod__、__pow__:真除、整除、求余、乘方。
  • __divmod__:商和余数一起算。

比较与真假

  • __eq__、__ne__:相等、不相等。
  • __lt__、__le__、__gt__、__ge__:小于、小于等于、大于、大于等于。
  • __bool__:真假,只能返回bool。
  • __hash__:算哈希,相等的对象结果要一样。

单目

  • __neg__、__pos__、__abs__、__invert__:取负、取正、绝对值、按位取反。

转换

  • __int__、__float__:变整数、变浮点。
  • __index__:当整数用,下标与hex都认。
  • __round__、__floor__、__ceil__:四舍五入、向下取整、向上取整。

示例代码如下:

python 复制代码
class Money:  # 像数字一样参与运算
    def __init__(self, v):  # 塞初始数据
        self.v = v

    def __add__(self, o):  # a + b
        return Money(self.v + o.v)

    def __eq__(self, o):  # a == b
        if not isinstance(o, Money):
            return NotImplemented
        return self.v == o.v

    def __lt__(self, o):  # a < b
        if not isinstance(o, Money):
            return NotImplemented
        return self.v < o.v

    def __bool__(self):  # bool(a)
        return self.v != 0

    def __hash__(self):  # hash(a),v可变时哈希跟着变,真实代码应避免
        return hash(self.v)

    def __neg__(self):  # -a
        return Money(-self.v)

    def __abs__(self):  # abs(a)
        return Money(abs(self.v))

    def __int__(self):  # int(a)
        return int(self.v)

    def __float__(self):  # float(a)
        return float(self.v)

    def __round__(self, n=None):  # round(a)、round(a, n)
        return Money(round(self.v)) if n is None else Money(round(self.v, n))

    def __repr__(self):  # 调试看
        return f"Money({self.v!r})"

a, b = Money(10), Money(3)
print(a + b)                    # __add__:Money(13)
print(a == b, a < b)            # __eq__、__lt__:False False
print(bool(a), bool(Money(0)))  # __bool__:True False
print(hash(a) == hash(Money(10)))  # __hash__:True
print(-a, abs(Money(-5)))       # __neg__、__abs__:Money(-10) Money(5)
print(int(a), float(a))         # __int__、__float__:10 10.0
print(round(Money(3.14159), 2)) # __round__:Money(3.14)

1.4、调用与上下文

对象除了能运算、比较,还能像函数一样被调用,像文件一样被with管理,像异步对象一样被await和async for使用。

  • __call__:让实例能加括号调用
  • __enter__:进with时执行
  • __exit__:离开with时执行
  • __await__:给await用的迭代器
  • __aiter__:交出异步迭代器
  • __anext__:异步取下一项,取完报StopAsyncIteration
  • __aenter__:异步版的进入
  • __aexit__:异步版的退出

示例代码如下:

python 复制代码
import asyncio

class F:  # __call__
    def __call__(self, x): return x * 2

class C:  # __enter__ / __exit__
    def __enter__(self): print("进"); return self
    def __exit__(self, *exc): print("出")

class A:  # __await__
    def __await__(self): yield; return "完"

class AI:  # __aiter__ / __anext__
    def __init__(self, n): self.n, self.i = n, 0
    def __aiter__(self): return self
    async def __anext__(self):
        if self.i >= self.n: raise StopAsyncIteration
        self.i += 1; return self.i

class AC:  # __aenter__ / __aexit__
    async def __aenter__(self): print("异步进"); return self
    async def __aexit__(self, *exc): print("异步出")

print(F()(3))       # __call__:6
with C(): pass      # __enter__、__exit__:进 出

async def main():
    print(await A())                 # __await__:完
    print([x async for x in AI(3)])  # __aiter__、__anext__:[1, 2, 3]
    async with AC(): pass            # __aenter__、__aexit__:异步进 异步出

asyncio.run(main())

1.5、属性访问

对象除了能被调用、管理上下文,属性怎么读、写、删也可以由类接管:

  • __getattr__:查不到属性时兜底。
  • __getattribute__:属性一被读就先过这一关。
  • __setattr__:属性一被赋值就过这一关。
  • __delattr__:属性一被删除就过这一关。
  • __dir__:决定dir()列出什么。
  • __get__:描述符被读时。
  • __set__:描述符被写时。
  • __delete__:描述符被删时。
  • __set_name__:建类时得知字段叫什么。

示例代码如下:

python 复制代码
class Model:
    def show(self):  # 普通方法
        return "方法"

    def __getattribute__(self, name):  # 读属性先过这关
        print("读", name)
        # super()从父类找方法,Model默认父类是object
        return super().__getattribute__(name)

    def __getattr__(self, name):  # 查不到属性时兜底
        return f"默认{name}"

    def __setattr__(self, name, v):  # 赋值时过这关
        print("写", name)
        # 不能写self.xxx = v,会再进__setattr__死循环
        # 这里交给父类默认逻辑真正赋值
        super().__setattr__(name, v)

    def __delattr__(self, name):  # 删除时过这关
        print("删", name)
        super().__delattr__(name)

    def __dir__(self):  # 决定dir()列出什么
        return ["x", "show"]

m = Model()
m.x = 5            # __setattr__:写x
print(m.x)         # __getattribute__:读x,结果5
print(m.show())    # __getattribute__:读show,结果方法
print(m.y)         # __getattribute__读y失败,转__getattr__,默认y
del m.x            # __delattr__:删x
print(dir(m))      # __dir__:排序后为['show', 'x']

1.6、类型与保存

前五部分补完了对象的行为,剩下两件事,类型会一直增加,数据要离开内存。类本身就是对象,造类、认子类、复制与保存时,另有一批名字。

类与类型

  • __init_subclass__,有子类时通知父类。
  • 元类的__call__,管类怎么造实例。
  • __instancecheck__,改写isinstance的结果。
  • __subclasscheck__,改写issubclass的结果。
  • __class_getitem__,让类能写方括号。

复制与保存

  • __copy__,浅复制。
  • __deepcopy__,深复制,用memo记已经复制的。
  • __getstate__,导出要保存的状态。
  • __setstate__,把状态装回去。
  • __reduce__,给出重建说明。

互操作

  • __fspath__,变成路径字符串。
  • __sizeof__,单个对象占用的内存估算,sys.getsizeof会在此基础上再加对象头。

示例代码如下:

python 复制代码
import copy
import pickle
import os

class Meta(type):
    def __call__(cls, *a, **kw):  # 元类的__call__:管类怎么造实例
        print("__call__:造实例")
        return super().__call__(*a, **kw)

    def __instancecheck__(cls, obj):  # 改写isinstance的结果
        print("__instancecheck__:isinstance")
        return True

    def __subclasscheck__(cls, sub):  # 改写issubclass的结果
        print("__subclasscheck__:issubclass")
        return True

class Shape(metaclass=Meta):
    def __init_subclass__(cls, **kw):  # 有子类时通知父类
        print("__init_subclass__:新子类", cls.__name__)
        super().__init_subclass__(**kw)

    def __class_getitem__(cls, item):  # 让类能写方括号
        print("__class_getitem__:类[方括号]")
        return item

class Square(Shape):  # 定义时触发__init_subclass__
    def __init__(self, side):
        self.side = side

class Data:
    def __init__(self, x):
        self.x = x

    def __copy__(self):  # 浅复制
        print("__copy__:浅复制")
        return Data(self.x)

    def __deepcopy__(self, memo):  # 深复制,memo记已复制的
        print("__deepcopy__:深复制")
        return Data(copy.deepcopy(self.x, memo))

    def __getstate__(self):  # 导出要保存的状态
        print("__getstate__:导出状态")
        return {"x": self.x}

    def __setstate__(self, state):  # 把状态装回去
        print("__setstate__:装回状态")
        self.x = state["x"]

    def __reduce__(self):  # 给出重建说明
        print("__reduce__:给重建说明")
        return (Data, (self.x,))


class Path:
    def __fspath__(self):  # 变成路径字符串
        print("__fspath__:变路径字符串")
        return "/tmp"

    def __sizeof__(self):  # 单个对象占用的内存估算
        print("__sizeof__:算体积")
        return 128

s = Square(2)                    # 元类的__call__
isinstance(s, Shape)             # __instancecheck__
issubclass(Square, Shape)        # __subclasscheck__
Shape[int]                       # __class_getitem__

d = Data([1, 2])
copy.copy(d)                     # __copy__
copy.deepcopy(d)                 # __deepcopy__
d.__getstate__()                 # __getstate__
d.__setstate__({"x": 3})         # __setstate__
pickle.loads(pickle.dumps(d))    # __reduce__

os.fspath(Path())                # __fspath__
Path().__sizeof__()              # __sizeof__

1.7、双下划线名称辨析

带双下划线的名字不一定都是魔法方法。魔法方法只是其中一类,前面只列了常见的,同类还有不少。另外有四类双下划线名字长得一样,却不属于魔法方法,容易混淆。

特殊属性

这些名字描述对象、类、函数或代码对象的数据与布局。语言不会在某个语法位置按这些名字自动调用对象,所以这些名字不构成特殊方法协议。

  • __dict__:对象或类的属性字典。
  • __slots__:声明实例允许的属性槽位。
  • __weakref__:让实例支持弱引用的槽位。
  • __match_args__:模式匹配时可按位置匹配的属性名列表。
  • __type_params__:记录泛型声明的类型参数。
  • __firstlineno__:记录类定义的起始行号,Python3.13起可用。

代码如下:

python 复制代码
class A:  # 定义普通类
    pass  # 类体留空

a = A()  # 创建实例
a.x = 1  # 给实例添加属性
print(a.__dict__)  # __dict__:查看实例属性字典

class S:  # 定义带__slots__的类
    __slots__ = ("x",)  # __slots__:只允许实例有x属性
print(S.__slots__)  # 查看槽位声明

import weakref  # 导入弱引用模块
class W:  # 定义普通类,默认支持弱引用
    pass  # 类体留空
print(W.__weakref__)  # __weakref__:查看弱引用槽位描述符
w = W()  # 先留住引用,对象才不会被立刻回收
print(weakref.ref(w))  # 创建弱引用对象

class P:  # 定义带模式匹配参数的类
    __match_args__ = ("x",)  # __match_args__:允许按位置匹配属性x
print(P.__match_args__)  # 查看模式匹配属性名

class Box[T]:  # __type_params__:定义泛型类,需要Python 3.12+
    pass  # 类体留空
print(Box.__type_params__)  # 查看类型参数

class C:  # __firstlineno__:定义类,需要Python 3.13+
    pass  # 类体留空
print(C.__firstlineno__)  # __firstlineno__:类定义的起始行号

模块元数据

这些是模块对象的资料或导入约定,不是给自定义实例实现的特殊方法。

  • __all__:控制from module import *导出哪些名字。
  • __name__:表示模块名。
  • __file__:表示模块文件路径。
  • __package__:表示模块所属包的上下文。

演示如下:

python 复制代码
__all__ = ["foo"]  # 控制from module import *只导出foo

def foo():  # 定义一个可导出函数
    pass  # 函数体留空

print(__all__)  # 查看导出名单
print(__name__)  # 查看模块名
print(__file__)  # 查看模块文件路径,脚本中运行才有
print(__package__)  # 查看包上下文

内置函数

这些是解释器提供的函数,可以被同名变量遮蔽,但不是供自定义实例实现的特殊方法。

  • __import__:执行导入。
  • __build_class__:class语句编译后的底层入口,一般不手工调用。

示例代码如下:

python 复制代码
m = __import__("math")  # 用内置函数导入math模块
print(m.sqrt(4))  # 输出2.0

普通方法

这些是可以调用的普通方法,不属于特殊方法协议,不需要手工实现成语法钩子。

  • __subclasses__:返回类的直接子类列表。

示例如下:

python 复制代码
class A:  # 定义父类
    pass  # 类体留空

class B(A):  # 定义子类
    pass  # 类体留空

print(A.__subclasses__())  # 查看A的直接子类列表

下划线的个数还有两套写法,与特殊方法无关。

  • _books:只提示内部使用,不限制访问。
  • __books:在类体里发生名称改写,用来避免子类重名,不构成保密。
  • class_:只是避开关键字。

代码如下:

python 复制代码
_books = ["a"]  # 单下划线开头,提示内部使用
print(_books)  # 仍可直接访问

class C:  # 定义类
    def f(self):  # 定义方法
        self.__books = 1  # 双下划线开头,在类体里会被改名为_C__books

c = C()  # 创建实例
c.f()  # 调用方法,触发名称改写
print(c._C__books)  # 用改写后的名字访问
# print(c.__books)  # 会报AttributeError,因为名字已被改写

class_ = "避开关键字"  # 单下划线结尾,避开class关键字
print(class_)  # 输出 避开关键字

2 参考