10 minutes
魔法方法与运算符重载
魔法方法(Magic Methods)是 Python 中以双下划线开头和结尾的特殊方法,它们让自定义对象能够与 Python 的内置操作无缝集成。通过实现魔法方法,你可以让自己的类支持 +、[]、len()、for...in 等语法,让代码更加自然和 Pythonic。
魔法方法概述
魔法方法也被称为 dunder 方法(double underscore 的缩写)。它们不需要手动调用——Python 在特定操作时会自动触发。
class Demo:
def __init__(self, value):
self.value = value
def __str__(self):
return f"Demo({self.value})"
def __len__(self):
return len(str(self.value))
d = Demo(42)
print(str(d)) # 触发 __str__
print(len(d)) # 触发 __len__
对象的生命周期管理
__new__:真正的构造函数
__new__ 在 __init__ 之前调用,负责创建并返回实例。它通常只在需要控制对象创建时才会被重写:
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: int):
# 注意:每次调用 Singleton() 都会执行 __init__
self.value = value
s1 = Singleton(1)
s2 = Singleton(2)
print(s1 is s2) # True(同一实例)
print(s1.value) # 2(被覆盖了)
print(s2.value) # 2
__init__:初始化方法
最常见,创建对象后自动调用:
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
print(f"User {name} created")
u = User("Alice", 30) # User Alice created
__del__:析构方法
对象被垃圾回收时调用(不保证立即调用):
class Resource:
def __init__(self, name: str):
self.name = name
print(f"Resource {name} acquired")
def __del__(self):
print(f"Resource {self.name} released")
r = Resource("database")
del r # 立即触发 __del__
print("after del") # 在 __del__ 之后执行
注意:不要依赖
__del__来释放重要资源。使用上下文管理器(with语句)更可靠。
字符串表示
__str__ vs __repr__ vs __format__
from datetime import datetime
class Book:
def __init__(self, title: str, author: str, price: float):
self.title = title
self.author = author
self.price = price
def __repr__(self) -> str:
"""开发者友好的表示,应该能重建对象"""
return f"Book(title={self.title!r}, author={self.author!r}, price={self.price})"
def __str__(self) -> str:
"""用户友好的表示"""
return f"《{self.title}》by {self.author}"
def __format__(self, format_spec: str) -> str:
"""自定义格式化"""
match format_spec:
case "short":
return self.title
case "detail":
return f"{self.title} - {self.author} (¥{self.price:.2f})"
case "price":
return f"¥{self.price:.2f}"
case _:
return str(self)
book = Book("Python从入门到精通", "张三", 79.00)
print(repr(book)) # Book(title='Python从入门到精通', author='张三', price=79.0)
print(str(book)) # 《Python从入门到精通》by 张三
print(book) # 《Python从入门到精通》by 张三
print(f"{book:short}") # Python从入门到精通
print(f"{book:detail}") # Python从入门到精通 - 张三 (¥79.00)
print(f"{book:price}") # ¥79.00
容器相关方法
__len__、__getitem__、__setitem__、__delitem__
class Playlist:
"""模拟一个播放列表容器"""
def __init__(self, name: str):
self.name = name
self._songs: list[str] = []
def add(self, song: str) -> None:
self._songs.append(song)
def __len__(self) -> int:
"""支持 len()"""
return len(self._songs)
def __getitem__(self, index) -> str:
"""支持 playlist[i]"""
return self._songs[index]
def __setitem__(self, index, song: str) -> None:
"""支持 playlist[i] = song"""
self._songs[index] = song
def __delitem__(self, index) -> None:
"""支持 del playlist[i]"""
del self._songs[index]
def __contains__(self, song: str) -> bool:
"""支持 'song' in playlist"""
return song in self._songs
def __iter__(self):
"""支持 for s in playlist"""
return iter(self._songs)
def __repr__(self) -> str:
return f"Playlist({self.name!r}, songs={self._songs!r})"
playlist = Playlist("My Favorites")
playlist.add("Bohemian Rhapsody")
playlist.add("Stairway to Heaven")
playlist.add("Hotel California")
print(len(playlist)) # 3
print(playlist[0]) # Bohemian Rhapsody
playlist[1] = "Imagine"
print("Imagine" in playlist) # True
del playlist[2]
for song in playlist:
print(f" - {song}")
# - Bohemian Rhapsody
# - Imagine
切片支持
如果希望 __getitem__ 支持切片,需要处理 slice 对象:
class Deck:
def __init__(self):
self._cards = list(range(52))
def __getitem__(self, index):
if isinstance(index, slice):
return self._cards[index]
return self._cards[index]
def __len__(self):
return len(self._cards)
deck = Deck()
print(deck[0:5]) # [0, 1, 2, 3, 4]
print(deck[-1]) # 51
迭代相关
__iter__ 与 __next__
class Countdown:
"""从 n 倒数到 1 的迭代器"""
def __init__(self, start: int):
self.start = start
self.current = start
def __iter__(self):
"""返回迭代器对象本身"""
self.current = self.start
return self
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for num in Countdown(5):
print(num, end=" ") # 5 4 3 2 1
可调用对象
__call__
让实例像函数一样调用:
class Multiplier:
def __init__(self, factor: float):
self.factor = factor
def __call__(self, x: float) -> float:
return x * self.factor
double = Multiplier(2)
triple = Multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# 验证是否可调用
print(callable(double)) # True
# 实用:带状态的累加器
class Accumulator:
def __init__(self):
self.total = 0
def __call__(self, *args):
self.total += sum(args)
return self.total
acc = Accumulator()
print(acc(1, 2, 3)) # 6
print(acc(10)) # 16
print(acc.total) # 16
上下文管理器协议
__enter__ 和 __exit__
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import time
elapsed = time.perf_counter() - self.start
print(f"Elapsed: {elapsed:.4f}s")
return False # 不抑制异常
with Timer():
sum(range(1_000_000))
# Elapsed: 0.0123s
对象比较与哈希
__eq__ 与 __hash__
__hash__ 用于 set 和 dict 的键。__eq__ 和 __hash__ 之间有重要关联:
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __eq__(self, other) -> bool:
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age
def __hash__(self) -> int:
return hash((self.name, self.age))
def __repr__(self) -> str:
return f"Person({self.name}, {self.age})"
p1 = Person("Alice", 30)
p2 = Person("Alice", 30)
p3 = Person("Bob", 25)
print(p1 == p2) # True
print(p1 is p2) # False
# 可作为字典键
d = {p1: "person1", p3: "person3"}
print(d[p2]) # person1(因为 p1 == p2)
# 可放入集合
s = {p1, p2, p3}
print(len(s)) # 2(p1 和 p2 被视为相同)
__eq__ 和 __hash__ 的关系规则
- 如果定义
__eq__但不定义__hash__,则__hash__被设为None(对象变成不可哈希) - 如果同时定义
__eq__和__hash__,必须确保:a == b意味着hash(a) == hash(b) - 如果只定义
__hash__但不定义__eq__,则继承默认的__eq__(基于身份)
class HashablePerson:
def __init__(self, name: str, ssn: str):
self.name = name
self.ssn = ssn
def __eq__(self, other):
if not isinstance(other, HashablePerson):
return NotImplemented
return self.ssn == other.ssn
def __hash__(self):
return hash(self.ssn)
def __repr__(self):
return f"HashablePerson({self.name})"
class UnhashablePerson:
def __init__(self, name: str):
self.name = name
def __eq__(self, other):
return self.name == other.name
# 没有定义 __hash__,所以对象不可哈希
# u = {UnhashablePerson("A")} # TypeError: unhashable type
比较运算符
__lt__、__le__、__gt__、__ge__
实现排序支持:
from functools import total_ordering
@total_ordering # 只需实现 __eq__ 和 __lt__,其余自动生成
class Student:
def __init__(self, name: str, score: int):
self.name = name
self.score = score
def __eq__(self, other) -> bool:
if not isinstance(other, Student):
return NotImplemented
return self.score == other.score
def __lt__(self, other) -> bool:
if not isinstance(other, Student):
return NotImplemented
return self.score < other.score
def __repr__(self) -> str:
return f"{self.name}({self.score})"
students = [
Student("Alice", 95),
Student("Bob", 78),
Student("Charlie", 88),
Student("David", 95),
]
print(sorted(students))
# [Bob(78), Charlie(88), Alice(95), David(95)]
print(max(students)) # Alice(95) 或 David(95)
print(min(students)) # Bob(78)
算术运算符
__add__、__sub__、__mul__、__truediv__
class Money:
"""支持算术运算的金额类"""
def __init__(self, amount: float, currency: str = "CNY"):
self.amount = amount
self.currency = currency
def __add__(self, other) -> "Money":
if isinstance(other, Money):
if other.currency != self.currency:
raise ValueError("币种不同不能相加")
return Money(self.amount + other.amount, self.currency)
if isinstance(other, (int, float)):
return Money(self.amount + other, self.currency)
return NotImplemented
def __radd__(self, other) -> "Money":
"""支持 int + Money"""
return self.__add__(other)
def __sub__(self, other) -> "Money":
if isinstance(other, Money):
if other.currency != self.currency:
raise ValueError("币种不同不能相减")
return Money(self.amount - other.amount, self.currency)
if isinstance(other, (int, float)):
return Money(self.amount - other, self.currency)
return NotImplemented
def __mul__(self, factor: float) -> "Money":
if isinstance(factor, (int, float)):
return Money(self.amount * factor, self.currency)
return NotImplemented
def __rmul__(self, factor: float) -> "Money":
return self.__mul__(factor)
def __truediv__(self, divisor: float) -> "Money":
if divisor == 0:
raise ZeroDivisionError("不能除以零")
if isinstance(divisor, (int, float)):
return Money(self.amount / divisor, self.currency)
return NotImplemented
def __neg__(self) -> "Money":
return Money(-self.amount, self.currency)
def __pos__(self) -> "Money":
return Money(+self.amount, self.currency)
def __repr__(self) -> str:
return f"¥{self.amount:.2f}"
m1 = Money(100)
m2 = Money(50)
print(m1 + m2) # ¥150.00
print(m1 - m2) # ¥50.00
print(m1 * 3) # ¥300.00
print(3 * m1) # ¥300.00(触发 __rmul__)
print(m1 + 30) # ¥130.00
print(m1 / 4) # ¥25.00
print(-m1) # ¥-100.00
反向运算符与就地运算符
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __add__(self, other) -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
return Vector(self.x + other.x, self.y + other.y)
def __iadd__(self, other) -> "Vector":
"""支持 +="""
if not isinstance(other, Vector):
return NotImplemented
self.x += other.x
self.y += other.y
return self
def __radd__(self, other) -> "Vector":
"""支持标量 + Vector(有歧义,仅作演示)"""
if isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
return NotImplemented
def __repr__(self) -> str:
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Vector(4, 6)
v1 += v2 # 就地操作
print(v1) # Vector(4, 6)
类型转换
class Fraction:
def __init__(self, numerator: int, denominator: int):
if denominator == 0:
raise ValueError("分母不能为 0")
self.numerator = numerator
self.denominator = denominator
def __int__(self) -> int:
"""int() 转换"""
return self.numerator // self.denominator
def __float__(self) -> float:
"""float() 转换"""
return self.numerator / self.denominator
def __bool__(self) -> bool:
"""bool() 转换,用于 if 判断"""
return self.numerator != 0
def __repr__(self) -> str:
return f"{self.numerator}/{self.denominator}"
f = Fraction(3, 2)
print(int(f)) # 1
print(float(f)) # 1.5
print(bool(f)) # True
print(bool(Fraction(0, 5))) # False
属性访问控制
__getattr__、__setattr__、__getattribute__
class ValidatedAttributes:
"""带属性校验的对象"""
def __init__(self):
self._data = {}
def __getattr__(self, name: str):
"""属性不存在时调用"""
if name.startswith("_"):
raise AttributeError(name)
if name in self._data:
return self._data[name]
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
def __setattr__(self, name: str, value) -> None:
"""任何属性赋值都会触发"""
if name.startswith("_"):
super().__setattr__(name, value)
else:
# 校验:字符串属性不能为空
if isinstance(value, str) and not value.strip():
raise ValueError("字符串属性不能为空")
self._data[name] = value
def __getattribute__(self, name: str):
"""任何属性访问都触发(谨慎重写)"""
if name.startswith("_"):
raise AttributeError("Cannot access private attributes")
return super().__getattribute__(name)
obj = ValidatedAttributes()
obj.name = "Alice" # 正常
print(obj.name) # Alice
# obj.name = "" # ValueError: 字符串属性不能为空
# print(obj._data) # AttributeError(__getattribute__ 拦截了)
综合实战:Vector 类
下面构建一个完整的 2D 向量类,综合运用各种魔法方法:
import math
class Vector2D:
"""完整的 2D 向量实现"""
def __init__(self, x: float = 0, y: float = 0):
self.x = x
self.y = y
# ---- 字符串表示 ----
def __repr__(self) -> str:
return f"Vector2D({self.x}, {self.y})"
def __str__(self) -> str:
return f"({self.x:.1f}, {self.y:.1f})"
def __format__(self, format_spec: str) -> str:
match format_spec:
case "polar":
r, theta = self.polar
return f"(r={r:.2f}, theta={math.degrees(theta):.1f}deg)"
case "short":
return f"({self.x:.1f}, {self.y:.1f})"
case _:
return str(self)
# ---- 容器方法 ----
def __len__(self) -> int:
return 2
def __getitem__(self, index: int) -> float:
if index == 0:
return self.x
if index == 1:
return self.y
raise IndexError(f"Vector2D index out of range: {index}")
def __setitem__(self, index: int, value: float) -> None:
if index == 0:
self.x = value
elif index == 1:
self.y = value
else:
raise IndexError(f"Vector2D index out of range: {index}")
# ---- 可迭代 ----
def __iter__(self):
return iter((self.x, self.y))
# ---- 可哈希 ----
def __hash__(self) -> int:
return hash((self.x, self.y))
def __eq__(self, other) -> bool:
if not isinstance(other, Vector2D):
return NotImplemented
return math.isclose(self.x, other.x) and math.isclose(self.y, other.y)
def __ne__(self, other) -> bool:
return not self.__eq__(other)
# ---- 一元运算符 ----
def __neg__(self) -> "Vector2D":
return Vector2D(-self.x, -self.y)
def __pos__(self) -> "Vector2D":
return Vector2D(+self.x, +self.y)
def __abs__(self) -> float:
"""|v|:向量模长"""
return math.sqrt(self.x ** 2 + self.y ** 2)
def __bool__(self) -> bool:
"""零向量为 False"""
return self.x != 0 or self.y != 0
# ---- 二元算术运算 ----
def __add__(self, other) -> "Vector2D":
if isinstance(other, Vector2D):
return Vector2D(self.x + other.x, self.y + other.y)
return NotImplemented
def __sub__(self, other) -> "Vector2D":
if isinstance(other, Vector2D):
return Vector2D(self.x - other.x, self.y - other.y)
return NotImplemented
def __mul__(self, other) -> "Vector2D | float":
if isinstance(other, (int, float)):
return Vector2D(self.x * other, self.y * other)
if isinstance(other, Vector2D):
# 点积(dot product)
return self.x * other.x + self.y * other.y
return NotImplemented
def __rmul__(self, other) -> "Vector2D":
return self.__mul__(other)
def __neg__(self) -> "Vector2D":
return Vector2D(-self.x, -self.y)
# ---- 比较运算(用于排序) ----
def __lt__(self, other) -> bool:
if not isinstance(other, Vector2D):
return NotImplemented
return abs(self) < abs(other)
# ---- 属性 ----
@property
def polar(self) -> tuple[float, float]:
"""极坐标 (r, theta)"""
r = abs(self)
theta = math.atan2(self.y, self.x)
return (r, theta)
@staticmethod
def from_polar(r: float, theta: float) -> "Vector2D":
"""从极坐标创建向量"""
return Vector2D(r * math.cos(theta), r * math.sin(theta))
# ---- 使用示例 ----
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
# 字符串表示
print(repr(v1)) # Vector2D(3, 4)
print(str(v1)) # (3.0, 4.0)
print(f"{v1:polar}") # (r=5.00, theta=53.1deg)
# 算术运算
print(v1 + v2) # (4.0, 6.0)
print(v1 * 3) # (9.0, 12.0)
print(v1 * v2) # 11.0(点积)
# 容器行为
print(v1[0]) # 3
x, y = v1 # 解包
print(x, y) # 3 4
# 哈希与比较
v3 = Vector2D(3, 4)
print(v1 == v3) # True
print(hash(v1) == hash(v3)) # True
s = {v1, v3} # 集合
print(len(s)) # 1
# 布尔运算
print(bool(v1)) # True
print(bool(Vector2D())) # False
# 极坐标
print(v1.polar) # (5.0, 0.927...)
# 零向量测试
zero = Vector2D()
print(bool(zero)) # False
常见陷阱
陷阱 1:忘记返回 NotImplemented
class BadMath:
def __init__(self, value):
self.value = value
def __add__(self, other):
if not isinstance(other, BadMath):
raise TypeError("Type mismatch") # 错误!
# return NotImplemented # 正确做法
class GoodMath:
def __init__(self, value):
self.value = value
def __add__(self, other):
if not isinstance(other, GoodMath):
return NotImplemented # Python 会给对方反过来的机会
return GoodMath(self.value + other.value)
陷阱 2:定义了 __eq__ 忘了 __hash__
class MutablePerson:
def __init__(self, name: str):
self.name = name
def __eq__(self, other):
return isinstance(other, MutablePerson) and self.name == other.name
# 没有 __hash__,对象不可哈希
p = MutablePerson("Alice")
# d = {p: 1} # TypeError: unhashable type: 'MutablePerson'
陷阱 3:__getattribute__ 中的无限递归
class InfiniteRecursion:
def __getattribute__(self, name):
# return self.__dict__[name] # 会递归!
return object.__getattribute__(self, name) # 正确
小结
魔法方法是 Python 赋予自定义对象"原生"行为的关键机制。本篇我们全面覆盖了:
- 对象生命周期:
__new__(创建)、__init__(初始化)、__del__(析构) - 字符串表示:
__repr__、__str__、__format__ - 容器行为:
__len__、__getitem__、__setitem__、__delitem__ - 迭代:
__iter__、__next__ - 可调用:
__call__ - 上下文管理器:
__enter__、__exit__ - 比较与哈希:
__eq__、__hash__、__lt__、__gt__等 - 算术运算:
__add__、__sub__、__mul__、__truediv__等 - 类型转换:
__int__、__float__、__bool__ - 属性访问控制:
__getattr__、__setattr__、__getattribute__
合理使用魔法方法能让你的类看起来像是 Python 内置类型一样自然。
Summary: 魔法方法让自定义类与 Python 内置操作无缝集成