魔法方法(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__ 用于 setdict 的键。__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__ 的关系规则

  1. 如果定义 __eq__不定义 __hash__,则 __hash__ 被设为 None(对象变成不可哈希)
  2. 如果同时定义 __eq____hash__,必须确保:a == b 意味着 hash(a) == hash(b)
  3. 如果只定义 __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 内置操作无缝集成