首页 / 知识库 / 0基础入门-阅读资料 / 0基础-python入门到精通

第 13 章 魔术方法(双下方法)全景

魔术方法(dunder methods)是 Python 的数据模型核心,让自定义类可以像内置类型一样使用各种运算符和语法。

13.1 对象表示:__repr____str____format__

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        """开发者看的表示,尽量可以 eval 回原对象"""
        return f"Point({self.x}, {self.y})"
    
    def __str__(self):
        """用户看的表示,更友好"""
        return f"({self.x}, {self.y})"
    
    def __format__(self, spec):
        """支持 format() 和 f-string 格式规范"""
        if spec == "r":
            return f"({self.x:.2f}, {self.y:.2f})"
        return str(self)

p = Point(3, 4)
print(repr(p))       # Point(3, 4) — 调用 __repr__
print(str(p))        # (3, 4) — 调用 __str__
print(p)             # (3, 4) — print 调用 __str__
print(f"{p}")        # (3, 4) — f-string 调用 __format__
print(f"{p:r}")      # (3.00, 4.00) — 带格式规范

# 在容器中,元素用 __repr__ 显示
print([p, Point(1, 2)])  # [Point(3, 4), Point(1, 2)]

规则

  • 总是实现 __repr__,它是最基本的表示
  • __str__ 没实现时会回退到 __repr__
  • __repr__ 应该尽量是无歧义的,最好可以 eval() 回原对象

13.2 比较运算:__eq____lt____hash__

from functools import total_ordering

@total_ordering   # 只需定义 __eq__ 和 __lt__,自动补全其他比较方法
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    def __eq__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented   # 让 Python 尝试反向比较
        return self.celsius == other.celsius
    
    def __lt__(self, other):
        if not isinstance(other, Temperature):
            return NotImplemented
        return self.celsius < other.celsius
    
    def __hash__(self):
        return hash(self.celsius)
    
    def __repr__(self):
        return f"Temperature({self.celsius})"

t1 = Temperature(20)
t2 = Temperature(30)
t3 = Temperature(20)

print(t1 == t3)    # True
print(t1 < t2)     # True
print(t1 >= t2)    # False — @total_ordering 自动实现
print(t2 > t1)     # True

# 可哈希 → 可作字典键和集合元素
temps = {t1: "舒适", t2: "炎热"}
print(Temperature(20) in temps)   # True

__eq____hash__ 的关系

  • 定义了 __eq__ 但没定义 __hash__,对象变成不可哈希(不能放入 set/dict)
  • 可变对象不应该实现 __hash__
  • __hash__ 必须保证:a == bhash(a) == hash(b)

13.3 算术运算:__add____radd____iadd__

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"
    
    # 正向运算: self + other
    def __add__(self, other):
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        if isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
        return NotImplemented
    
    # 反向运算: other + self(当 other 不知道如何 + Vector 时调用)
    def __radd__(self, other):
        return self.__add__(other)
    
    # 就地运算: self += other
    def __iadd__(self, other):
        if isinstance(other, Vector):
            self.x += other.x
            self.y += other.y
            return self
        return NotImplemented
    
    # 一元运算
    def __neg__(self):       # -v
        return Vector(-self.x, -self.y)
    
    def __abs__(self):       # abs(v)
        return (self.x ** 2 + self.y ** 2) ** 0.5
    
    # 标量乘法
    def __mul__(self, scalar):   # v * 3
        return Vector(self.x * scalar, self.y * scalar)
    
    def __rmul__(self, scalar):  # 3 * v
        return self.__mul__(scalar)

v1 = Vector(1, 2)
v2 = Vector(3, 4)

print(v1 + v2)       # Vector(4, 6)
print(v1 + 10)       # Vector(11, 12)
print(10 + v1)       # Vector(11, 12) — 触发 __radd__
print(v1 * 3)        # Vector(3, 6)
print(3 * v1)        # Vector(3, 6) — 触发 __rmul__
print(-v1)           # Vector(-1, -2)
print(abs(Vector(3, 4)))  # 5.0

完整的算术方法表:

运算正向反向就地
+__add____radd____iadd__
-__sub____rsub____isub__
*__mul____rmul____imul__
/__truediv____rtruediv____itruediv__
//__floordiv____rfloordiv____ifloordiv__
%__mod____rmod____imod__
**__pow____rpow____ipow__

13.4 容器协议:__len____getitem____setitem____contains__

class Deck:
    ranks = [str(n) for n in range(2, 11)] + list("JQKA")
    suits = "♠♥♦♣"
    
    def __init__(self):
        self._cards = [f"{r}{s}" for s in self.suits for r in self.ranks]
    
    def __len__(self):
        return len(self._cards)
    
    def __getitem__(self, index):
        return self._cards[index]
    
    def __setitem__(self, index, value):
        self._cards[index] = value
    
    def __delitem__(self, index):
        del self._cards[index]
    
    def __contains__(self, item):
        return item in self._cards
    
    def __iter__(self):
        return iter(self._cards)

deck = Deck()
print(len(deck))          # 52
print(deck[0])            # 2♠
print(deck[-1])           # A♣
print(deck[0:3])          # ['2♠', '3♠', '4♠'] — 切片也能用!
print("A♠" in deck)       # True

# __getitem__ 让对象可迭代(即使没有 __iter__)
for card in deck:
    pass

# 还能用 random
import random
print(random.choice(deck))  # 随机一张牌

13.5 可调用对象:__call__

实现 __call__ 让实例像函数一样被调用:

class Adder:
    def __init__(self, n):
        self.n = n
    
    def __call__(self, x):
        return x + self.n

add5 = Adder(5)
print(add5(10))     # 15
print(add5(20))     # 25

# 检查对象是否可调用
print(callable(add5))    # True
print(callable(42))      # False

实用场景:带状态的函数

class RateLimiter:
    def __init__(self, max_calls, period):
        self.max_calls = max_calls
        self.period = period
        self.calls = []
    
    def __call__(self, func):
        from functools import wraps
        import time
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()
            self.calls = [t for t in self.calls if now - t < self.period]
            if len(self.calls) >= self.max_calls:
                raise RuntimeError("调用频率超限")
            self.calls.append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimiter(max_calls=3, period=60)
def api_request():
    return "response"

13.6 上下文管理:__enter____exit__

class Timer:
    def __enter__(self):
        import time
        self.start = time.perf_counter()
        return self   # with ... as xxx 中的 xxx
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        import time
        self.elapsed = time.perf_counter() - self.start
        print(f"耗时: {self.elapsed:.4f}s")
        return False   # False: 不抑制异常; True: 抑制异常

with Timer() as t:
    sum(range(1_000_000))

print(f"实际耗时: {t.elapsed:.4f}s")
class ManagedFile:
    def __init__(self, filename, mode="r"):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
        # 如果想抑制某种异常
        if exc_type is FileNotFoundError:
            print(f"文件 {self.filename} 不存在")
            return True   # 抑制异常
        return False

with ManagedFile("test.txt", "w") as f:
    f.write("hello")

13.7 属性访问:__getattr____getattribute____setattr____delattr__

class DynamicAttrs:
    def __init__(self):
        self._data = {}
    
    def __getattr__(self, name):
        """访问不存在的属性时调用(最后的手段)"""
        if name in self._data:
            return self._data[name]
        raise AttributeError(f"没有属性 '{name}'")
    
    def __setattr__(self, name, value):
        """任何属性赋值都会调用"""
        if name.startswith("_"):
            super().__setattr__(name, value)  # 内部属性正常处理
        else:
            self._data[name] = value
    
    def __delattr__(self, name):
        """del obj.attr 时调用"""
        if name in self._data:
            del self._data[name]
        else:
            super().__delattr__(name)

obj = DynamicAttrs()
obj.name = "Alice"       # 触发 __setattr__ → 存入 _data
print(obj.name)          # 触发 __getattr__ → 从 _data 取
del obj.name             # 触发 __delattr__

__getattr__ vs __getattribute__

  • __getattribute__:每次属性访问都调用(包括存在的属性),慎用
  • __getattr__:只在正常查找失败后调用(更安全、更常用)
class Logged:
    def __getattribute__(self, name):
        print(f"访问属性: {name}")
        return super().__getattribute__(name)  # 必须调 super(),否则无限递归

obj = Logged()
obj.x = 10
obj.x   # 打印 "访问属性: x",返回 10

13.8 __slots__ 节省内存

默认情况下,Python 对象用 __dict__ 字典存储属性,灵活但占内存:

class RegularPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class SlottedPoint:
    __slots__ = ("x", "y")  # 用固定结构代替 __dict__
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

import sys
r = RegularPoint(1, 2)
s = SlottedPoint(1, 2)

print(sys.getsizeof(r) + sys.getsizeof(r.__dict__))  # ~200 字节
print(sys.getsizeof(s))                                # ~56 字节

# __slots__ 的限制
s.z = 3  # AttributeError! 不能添加未声明的属性

# RegularPoint 有 __dict__
print(r.__dict__)   # {'x': 1, 'y': 2}
# SlottedPoint 没有 __dict__
# print(s.__dict__)  # AttributeError

何时使用 __slots__:创建大量实例时(如数据点、数据库记录)。

13.9 __init_subclass____set_name__

__init_subclass__ — 在子类创建时触发

class Plugin:
    _registry = {}
    
    def __init_subclass__(cls, plugin_name=None, **kwargs):
        super().__init_subclass__(**kwargs)
        name = plugin_name or cls.__name__.lower()
        Plugin._registry[name] = cls
        print(f"注册插件: {name}")

class ImagePlugin(Plugin, plugin_name="image"):
    pass  # 注册插件: image

class VideoPlugin(Plugin, plugin_name="video"):
    pass  # 注册插件: video

class AudioPlugin(Plugin):
    pass  # 注册插件: audioplugin

print(Plugin._registry)
# {'image': <class 'ImagePlugin'>, 'video': <class 'VideoPlugin'>, 'audioplugin': <class 'AudioPlugin'>}

__set_name__ — 描述符获知自己的属性名

class Validator:
    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = f"_{name}"
    
    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name, None)
    
    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)
    
    def validate(self, value):
        pass

class PositiveNumber(Validator):
    def validate(self, value):
        if value < 0:
            raise ValueError(f"{self.public_name} 必须为正数,得到 {value}")

class Product:
    price = PositiveNumber()     # __set_name__ 自动获得 "price"
    quantity = PositiveNumber()  # __set_name__ 自动获得 "quantity"
    
    def __init__(self, name, price, quantity):
        self.name = name
        self.price = price
        self.quantity = quantity

p = Product("Widget", 9.99, 100)
# p.price = -1  # ValueError: price 必须为正数,得到 -1

本章小结:魔术方法是 Python 数据模型的精髓。通过实现不同的协议,自定义类可以完美融入 Python 的语法体系——支持运算符、迭代、上下文管理、属性访问等。记住 NotImplemented 的用法、__repr__ vs __str__ 的区别、__slots__ 的适用场景,这些是面试和实战中的高频知识点。