返回博客

31. 定制类:特殊方法详解

31. 定制类:特殊方法详解

第三十一篇:定制类——特殊方法详解

前言

Python 的强大之处在于:自定义类可以通过实现特殊方法(魔术方法)表现得像内置类型一样。通过 __str__、__len__、__iter__、__getitem__ 等方法,你的类可以支持 print()、len()、for 循环、索引访问等操作。本文将系统讲解这些核心特殊方法。

str 与 repr —— 对象的字符串表示

基本区别

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

p = Person("小明", 25)
print(p)       # <__main__.Person object at 0x...>  ← 不友好
print(str(p))  # 同上

实现 str 和 repr

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __str__(self):
        """print() 和 str() 调用,面向用户"""
        return f"Person(name={self.name}, age={self.age})"
    
    def __repr__(self):
        """交互式环境直接显示,面向开发者"""
        return f"Person('{self.name}', {self.age})"

p = Person("小明", 25)
print(p)       # Person(name=小明, age=25)
p              # Person('小明', 25)

repr 的备份策略

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
    
    # 如果只实现一个,实现 __repr__
    # 因为 __repr__ 会作为 __str__ 的后备
    def __repr__(self):
        return f"Product('{self.name}', {self.price})"

prod = Product("键盘", 299)
print(prod)   # Product('键盘', 299)  ← __repr__ 作为 __str__ 后备
print([prod]) # [Product('键盘', 299)] ← 列表中始终用 __repr__

len 与 bool —— 长度与真值

class Stack:
    def __init__(self):
        self._items = []
    
    def push(self, item):
        self._items.append(item)
    
    def pop(self):
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()
    
    def __len__(self):
        """支持 len(stack)"""
        return len(self._items)
    
    def __bool__(self):
        """支持 if stack: 判断是否为空"""
        return len(self._items) > 0

stack = Stack()
print(len(stack))   # 0
print(bool(stack))  # False

stack.push("a")
stack.push("b")
print(len(stack))   # 2
if stack:           # True
    print("栈不为空")

stack.pop()
stack.pop()
if not stack:       # True
    print("栈为空")

iter 与 next —— 让类可迭代

class NumberRange:
    """自定义数字范围类"""
    def __init__(self, start, end, step=1):
        self.current = start
        self.end = end
        self.step = step
    
    def __iter__(self):
        """返回迭代器对象(通常返回 self)"""
        return self
    
    def __next__(self):
        """返回下一个值,结束时抛出 StopIteration"""
        if self.current >= self.end:
            raise StopIteration
        value = self.current
        self.current += self.step
        return value

# 使用 for 循环遍历
for num in NumberRange(1, 10, 2):
    print(num, end=" ")
# 1 3 5 7 9

# 也可以手动迭代
rng = NumberRange(0, 5)
print(next(rng))  # 0
print(next(rng))  # 1
print(next(rng))  # 2

可重复迭代的类

class CardDeck:
    """一副扑克牌,可以反复洗牌、发牌"""
    suits = ["♠", "♥", "♦", "♣"]
    ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
    
    def __init__(self):
        self._cards = [(s, r) for s in self.suits for r in self.ranks]
        self._index = 0
    
    def __iter__(self):
        """每次迭代都从头开始"""
        self._index = 0
        return self
    
    def __next__(self):
        if self._index >= len(self._cards):
            raise StopIteration
        card = self._cards[self._index]
        self._index += 1
        return card
    
    def __len__(self):
        return len(self._cards)

deck = CardDeck()
print(len(deck))  # 52

# 第一次遍历
for suit, rank in deck:
    print(f"{suit}{rank}", end=" ")
print()

# 第二次遍历仍然可用
for suit, rank in deck:
    pass  # 52 张牌

getitem —— 索引与切片访问

class Sentence:
    """自定义句子类,支持索引访问"""
    def __init__(self, text):
        self.words = text.split()
    
    def __getitem__(self, index):
        """支持索引访问和切片"""
        if isinstance(index, slice):
            # 切片操作
            return Sentence(" ".join(self.words[index]))
        elif isinstance(index, int):
            # 索引访问
            return self.words[index]
        else:
            raise TypeError(f"索引类型不支持: {type(index)}")
    
    def __len__(self):
        return len(self.words)
    
    def __repr__(self):
        return f"Sentence('{ ' '.join(self.words) }')"

sentence = Sentence("Hello World Python is awesome")
print(sentence[0])     # Hello
print(sentence[-1])    # awesome
print(sentence[1:3])   # Sentence('World Python')
print(len(sentence))   # 5

# 有了 __getitem__,还能用 for 遍历!
for word in sentence:
    print(word, end=" ")
# Hello World Python is awesome

setitem 与 delitem

class Matrix:
    """简单的矩阵类"""
    def __init__(self, rows, cols):
        self._data = [[0] * cols for _ in range(rows)]
        self._rows = rows
        self._cols = cols
    
    def __getitem__(self, pos):
        row, col = pos
        return self._data[row][col]
    
    def __setitem__(self, pos, value):
        row, col = pos
        self._data[row][col] = value
    
    def __delitem__(self, pos):
        row, col = pos
        self._data[row][col] = 0  # 重置为默认值
    
    def __repr__(self):
        lines = []
        for row in self._data:
            lines.append("  " + " ".join(f"{v:3}" for v in row))
        return "Matrix(\n" + "\n".join(lines) + "\n)"

m = Matrix(3, 3)
m[0, 0] = 1
m[1, 1] = 2
m[2, 2] = 3
print(m[0, 0])  # 1
print(m)
# Matrix(
#     1   0   0
#     0   2   0
#     0   0   3
# )

getattr —— 动态属性访问

class Config:
    """配置类,支持动态属性访问"""
    def __init__(self, **kwargs):
        self._data = kwargs
    
    def __getattr__(self, name):
        """只在属性不存在时被调用"""
        if name.startswith("_"):
            raise AttributeError(f"'{type(self).__name__}' 对象没有属性 '{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 __repr__(self):
        return f"Config({self._data})"

config = Config(host="localhost", port=8080, debug=True)
print(config.host)   # localhost
print(config.port)   # 8080
print(config.debug)  # True
print(config)        # Config({'host': 'localhost', 'port': 8080, 'debug': True})

# 设置新属性
config.timeout = 30
print(config.timeout)  # 30

getattr 的链式调用

class Chain:
    """链式调用构建 URL 路径"""
    def __init__(self, path=""):
        self._path = path
    
    def __getattr__(self, name):
        if name.startswith("_"):
            raise AttributeError(name)
        new_path = f"{self._path}/{name}" if self._path else name
        return Chain(new_path)
    
    def __call__(self, *args):
        """支持 Chain().users(123) 这种调用"""
        if args:
            new_path = f"{self._path}/{'/'.join(str(a) for a in args)}"
            return Chain(new_path)
        return self
    
    def __str__(self):
        return f"/{self._path}"

# 构建 API 路径
api = Chain()
print(api.users)           # /users
print(api.users.posts)     # /users/posts
print(api.users(123))      # /users/123
print(api.users(123).posts(456))  # /users/123/posts/456

call —— 让实例可调用

class Multiplier:
    """乘法器,实例本身可以像函数一样被调用"""
    def __init__(self, factor):
        self.factor = factor
    
    def __call__(self, value):
        return value * self.factor
    
    def __repr__(self):
        return f"Multiplier({self.factor})"

double = Multiplier(2)
triple = Multiplier(3)

print(double(5))    # 10
print(triple(5))    # 15
print(callable(double))  # True ← 实例是可调用对象

实战:带状态的计数器

class Counter:
    """可调用计数器"""
    def __init__(self):
        self.count = 0
    
    def __call__(self):
        self.count += 1
        return self.count

counter = Counter()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3
print(counter.count)  # 3

eq 与 hash —— 相等性判断与哈希

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __eq__(self, other):
        """判断两个点是否相等"""
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y
    
    def __hash__(self):
        """让 Point 可用作字典键或集合元素"""
        return hash((self.x, self.y))
    
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

print(p1 == p2)  # True
print(p1 == p3)  # False

# 可用作字典键
locations = {Point(1, 2): "北京", Point(3, 4): "上海"}
print(locations[p2])  # 北京

# 可用作集合元素
point_set = {p1, p2, p3}
print(len(point_set))  # 2 ← p1 和 p2 相同

enter 与 exit —— 上下文管理器

class FileManager:
    """文件管理器,支持 with 语句"""
    def __init__(self, filename, mode="r"):
        self.filename = filename
        self.mode = mode
        self.file = None
    
    def __enter__(self):
        """进入 with 块时调用"""
        self.file = open(self.filename, self.mode)
        return self.file
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """离开 with 块时调用(即使发生异常)"""
        if self.file:
            self.file.close()
        # 返回 False(或 None)表示不抑制异常
        return False

# 使用方式
# with FileManager("test.txt", "w") as f:
#     f.write("Hello World")

class Timer:
    """计时器上下文管理器"""
    import time
    
    def __enter__(self):
        self.start = self.time.perf_counter()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end = self.time.perf_counter()
        elapsed = self.end - self.start
        print(f"耗时: {elapsed:.4f} 秒")
        return False

with Timer():
    sum(range(1_000_000))
# 耗时: 0.0312 秒

contains —— 成员判断

class RangeChecker:
    """范围检查类"""
    def __init__(self, low, high):
        self.low = low
        self.high = high
    
    def __contains__(self, item):
        """支持 in 运算符"""
        return self.low <= item <= self.high

checker = RangeChecker(1, 100)
print(50 in checker)    # True
print(150 in checker)   # False
print(0 not in checker) # True

综合实战:自定义链表

class LinkedList:
    """单链表,展示多个特殊方法组合"""
    class _Node:
        def __init__(self, value):
            self.value = value
            self.next = None
    
    def __init__(self, values=None):
        self._head = None
        self._size = 0
        if values:
            for v in values:
                self.append(v)
    
    def append(self, value):
        node = self._Node(value)
        if self._head is None:
            self._head = node
        else:
            current = self._head
            while current.next:
                current = current.next
            current.next = node
        self._size += 1
    
    def __len__(self):
        return self._size
    
    def __iter__(self):
        current = self._head
        while current:
            yield current.value
            current = current.next
    
    def __getitem__(self, index):
        if index < 0:
            index += self._size
        if not (0 <= index < self._size):
            raise IndexError("索引越界")
        current = self._head
        for _ in range(index):
            current = current.next
        return current.value
    
    def __contains__(self, value):
        for v in self:
            if v == value:
                return True
        return False
    
    def __repr__(self):
        items = list(self)
        return f"LinkedList({items})"

ll = LinkedList([1, 2, 3, 4, 5])
print(len(ll))      # 5
print(ll[0])        # 1
print(ll[-1])       # 5
print(3 in ll)      # True
print(ll)           # LinkedList([1, 2, 3, 4, 5])

for v in ll:
    print(v, end=" ")
# 1 2 3 4 5

常用特殊方法速查表

方法 触发方式 用途
__str__ print(obj) / str(obj) 用户友好的字符串
__repr__ 交互式直接显示 开发者友好的字符串
__len__ len(obj) 返回长度
__bool__ if obj: / bool(obj) 真值判断
__iter__ for x in obj 返回迭代器
__next__ next(obj) 返回下一个值
__getitem__ obj[key] 索引/切片访问
__setitem__ obj[key] = val 索引赋值
__delitem__ del obj[key] 索引删除
__getattr__ obj.attr(不存在时) 动态属性
__call__ obj(args) 实例调用
__eq__ obj1 == obj2 相等判断
__hash__ hash(obj) 哈希值
__contains__ x in obj 成员判断
__enter__/__exit__ with obj: 上下文管理

总结

本文系统讲解了 Python 定制类的核心特殊方法:

  1. str 与 repr — 控制对象的字符串表示
  2. len 与 bool — 支持 len() 和真值判断
  3. iter 与 next — 让自定义类可迭代
  4. getitem — 支持索引和切片访问
  5. getattr — 动态属性访问与链式调用
  6. call — 让实例像函数一样可调用
  7. eq 与 hash — 相等性判断与哈希支持
  8. enter 与 exit — 上下文管理器协议
  9. contains — 自定义 in 运算符行为
  10. 综合实战 — 通过组合特殊方法实现类内置类型行为

下一篇文章将讲解枚举类与元类。


本文是 Python 教程系列的第三十一篇。