Python & AI Tutorials Logo
Python 编程

31. 高级类特性

在第 30 章中,我们学习了如何使用实例属性和方法创建基础类。现在我们将探索更复杂的类特性,它们能让你对对象的行为进行更细粒度的控制。这些特性让你可以创建感觉像 Python 内置类型一样的类,对加法、比较、索引等操作拥有自然的语法。

31.1) 类变量 vs 实例变量

当我们在类中创建属性时,本质上有两个完全不同的存储位置:存储在类本身上,或存储在各个实例上。理解这种区别对于编写正确的面向对象代码至关重要。

31.1.1) 理解实例变量

实例变量(instance variables) 是属于某个特定对象的属性。每个实例都有这些变量各自独立的副本。我们在第 30 章中一直在使用实例变量——它们是在 __init__ 中通过 self 创建的属性:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner      # 实例变量
        self.balance = balance  # 实例变量
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.balance)  # Output: 1000
print(account2.balance)  # Output: 500

每个 BankAccount 实例都有自己的 ownerbalance。修改 account1.balance 不会影响 account2.balance——它们是完全独立的。

31.1.2) 理解类变量

类变量(class variables) 是属于类本身的属性,而不是属于某个特定实例。所有实例共享同一个类变量。我们在类体中直接定义类变量,放在任何方法之外:

python
class BankAccount:
    interest_rate = 0.02  # 类变量 - 所有实例共享
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    
    def apply_interest(self):
        self.balance += self.balance * BankAccount.interest_rate
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
print(BankAccount.interest_rate)  # Output: 0.02

注意,我们可以通过实例(account1.interest_rate)或通过类本身(BankAccount.interest_rate)来访问 interest_rate。它们都指向同一个变量。

这就是类变量强大的地方——当我们修改类变量时,所有实例都会看到这个变化:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
print(account1.interest_rate)  # Output: 0.02
print(account2.interest_rate)  # Output: 0.02
 
# 修改类变量
BankAccount.interest_rate = 0.03
 
print(account1.interest_rate)  # Output: 0.03
print(account2.interest_rate)  # Output: 0.03

两个实例会立刻看到新的利率,因为它们都在查看同一个类变量。

31.1.3) 遮蔽陷阱:当实例变量隐藏类变量时

这里有一个微妙但重要的行为:如果你通过实例为某个属性赋值,Python 会创建一个实例变量来遮蔽(隐藏)类变量:

python
class BankAccount:
    interest_rate = 0.02
    
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
 
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
 
# 创建一个遮蔽类变量的实例变量
account1.interest_rate = 0.05
 
print(account1.interest_rate)  # Output: 0.05 (instance variable)
print(account2.interest_rate)  # Output: 0.02 (class variable)
print(BankAccount.interest_rate)  # Output: 0.02 (class variable)

现在 account1 有了自己的 interest_rate 实例变量,它会隐藏类变量。类变量仍然存在,但 account1.interest_rate 会指向实例变量而不是类变量。这通常不是你想要的——如果你需要修改类变量,请通过类名来修改,而不是通过实例。

31.1.4) 类变量的实际用途

类变量适用于应在所有实例之间共享的数据:

python
class Student:
    school_name = "Python High School"  # 所有学生相同
    total_students = 0  # 跟踪学生总数
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        Student.total_students += 1  # 创建学生时递增
    
    def __str__(self):
        return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
 
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
 
print(student1)  # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}")  # Output: Total students: 3

注意我们在 __init__ 中使用 Student.total_students(而不是 self.total_students),这样可以清晰表明我们在修改类变量,而不是创建实例变量。

类变量

在类体中定义

所有实例共享

通过 ClassName.variable 访问

实例变量

init 中用 self 定义

每个实例独有

通过 instance.variable 访问

31.2) 使用 @property 管理属性

有时你希望控制别人访问或修改属性时会发生什么。例如,你可能想验证值必须为正数,或希望按需计算某个值而不是把它存起来。Python 的 @property 装饰器能让你编写“看起来像简单属性访问”的方法。

31.2.1) 问题:直接属性访问无法验证

当属性被直接访问时,你无法验证或转换这些值:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
 
# 没有什么能阻止我们设置物理上不可能的温度
temp.celsius = -500  # 低于绝对零度 (-273.15°C)!
print(temp.celsius)  # Output: -500
 
# 或者荒谬的超高值
temp.celsius = 1000000
print(temp.celsius)  # Output: 1000000

没有验证的话,我们可能会不小心设置无效数据,导致程序后面出现 bug。我们可以使用像 get_celsius()set_celsius() 这样的方法,但这不是 Python 的惯用写法。Python 开发者期望直接访问属性,而不是像 Java 或 C++ 那样通过 getter/setter 方法。

31.2.2) 使用 @property 实现计算属性

@property 装饰器会把一个方法变成“getter”,并且像属性一样被访问:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        """按需将 celsius 转换为 fahrenheit"""
        return self.celsius * 9/5 + 32
 
temp = Temperature(25)
print(temp.celsius)  # Output: 25
print(temp.fahrenheit)  # Output: 77.0 (computed, not stored)

注意我们访问 temp.fahrenheit 时没有加括号——看起来像访问属性,但实际上是在调用方法。fahrenheit 的值在每次访问时都会计算,因此它始终与 celsius 保持同步:

python
temp = Temperature(0)
print(temp.fahrenheit)  # Output: 32.0
 
temp.celsius = 100
print(temp.fahrenheit)  # Output: 212.0 (automatically updated)

31.2.3) 使用 @property_name.setter 添加 Setter

为了允许设置 property,我们使用 @property_name.setter 装饰器添加一个 setter 方法:

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius
    
    @property
    def fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """在设置时将 fahrenheit 转换为 celsius"""
        self.celsius = (value - 32) * 5/9
 
temp = Temperature(0)
print(temp.celsius)  # Output: 0
print(temp.fahrenheit)  # Output: 32.0
 
# 使用 fahrenheit 设置温度
temp.fahrenheit = 212
print(temp.celsius)  # Output: 100.0
print(temp.fahrenheit)  # Output: 212.0

setter 方法会接收新值,并且可以在存储之前先验证或转换。

31.2.4) 使用属性进行验证

属性非常适合用于强制约束:

python
class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self._balance = balance  # 下划线表示“内部使用”
    
    @property
    def balance(self):
        """获取当前余额"""
        return self._balance
    
    @balance.setter
    def balance(self, value):
        """设置余额,但只允许非负"""
        if value < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = value
 
account = BankAccount("Alice", 1000)
print(account.balance)  # Output: 1000
 
account.balance = 1500  # 正常工作
print(account.balance)  # Output: 1500
 
# 这里会抛出错误
account.balance = -100
# Output: ValueError: Balance cannot be negative

注意命名约定:我们把实际值存放在 _balance(前导下划线)中,并通过 balance 属性对外暴露。下划线是 Python 的惯例,表示“这是内部实现细节”,尽管该属性在技术上仍然可访问。这个模式让我们可以通过 property 控制访问,同时将实际存储与对外接口分离。

31.2.5) 只读属性

如果你定义 property 时不提供 setter,它就会变成只读:

python
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    @property
    def area(self):
        """计算得出的只读属性"""
        return self.width * self.height
 
rect = Rectangle(5, 3)
print(rect.area)  # Output: 15
 
rect.width = 10
print(rect.area)  # Output: 30 (automatically updated)
 
# 尝试设置 area 会引发错误
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setter

这对那些应当计算而不是存储的派生值非常有用。

@property 装饰器

将方法变为 getter

像属性一样访问

可按需计算值

@property_name.setter

为属性添加 setter

存储前可验证

可转换值

31.3) 使用 @classmethod 的类方法

有时你需要的方法是对类本身起作用,而不是对实例起作用。类方法(class methods) 的第一个参数接收的是类(按惯例命名为 cls),而不是实例(self)。

31.3.1) 定义类方法

我们使用 @classmethod 装饰器创建类方法:

python
class Student:
    school_name = "Python High School"
    
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
    
    @classmethod
    def get_school_name(cls):
        """类方法 - 接收的是类,而不是实例"""
        return cls.school_name
 
# 在类本身上调用
print(Student.get_school_name())  # Output: Python High School
 
# 也可以在实例上调用(但 cls 仍然是类)
student = Student("Alice", 10)
print(student.get_school_name())  # Output: Python High School

cls 参数会自动接收类,就像普通方法里的 self 会自动接收实例一样。

31.3.2) 使用类方法作为替代构造器

类方法最常见的用途之一是创建替代构造器——以不同方式创建实例:

python
class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    
    @classmethod
    def from_string(cls, date_string):
        """从类似 '2024-12-27' 的字符串创建 Date"""
        year, month, day = date_string.split('-')
        return cls(int(year), int(month), int(day))
    
    @classmethod
    def today(cls):
        """创建代表今天的 Date(简化示例)"""
        # 在真实代码中,你会使用 datetime 模块
        return cls(2024, 12, 27)
    
    def __str__(self):
        return f"{self.year}-{self.month:02d}-{self.day:02d}"
 
# 普通构造器
date1 = Date(2024, 12, 27)
print(date1)  # Output: 2024-12-27
 
# 从字符串创建的替代构造器
date2 = Date.from_string("2024-12-27")
print(date2)  # Output: 2024-12-27
 
# 创建“今天”的替代构造器
date3 = Date.today()
print(date3)  # Output: 2024-12-27

注意 from_stringtoday 都返回 cls(...)——这会创建该类的新实例。使用 cls 而不是硬编码 Date 能让代码在子类场景下也能正确工作(我们会在第 32 章学习继承)。

31.3.3) 用类方法实现工厂模式

类方法适合用来创建带不同配置的实例:

python
class DatabaseConnection:
    def __init__(self, host, port, database, username):
        self.host = host
        self.port = port
        self.database = database
        self.username = username
    
    @classmethod
    def for_development(cls):
        """创建用于开发环境的连接配置"""
        return cls("localhost", 5432, "dev_db", "dev_user")
    
    @classmethod
    def for_production(cls):
        """创建用于生产环境的连接配置"""
        return cls("prod.example.com", 5432, "prod_db", "prod_user")
    
    def __str__(self):
        return f"Connection to {self.database} at {self.host}:{self.port}"
 
# 便捷地创建预配置连接
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
 
print(dev_conn)  # Output: Connection to dev_db at localhost:5432
print(prod_conn)  # Output: Connection to prod_db at prod.example.com:5432

31.3.4) 使用类方法统计实例数量

类方法可以与类变量配合,跟踪所有实例的相关信息:

python
class Product:
    total_products = 0
    
    def __init__(self, name, price):
        self.name = name
        self.price = price
        Product.total_products += 1
    
    @classmethod
    def get_total_products(cls):
        """返回创建的产品总数"""
        return cls.total_products
    
    @classmethod
    def reset_count(cls):
        """重置产品计数器"""
        cls.total_products = 0
 
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
 
print(Product.get_total_products())  # Output: 3
 
Product.reset_count()
print(Product.get_total_products())  # Output: 0

31.4) 使用 @staticmethod 的静态方法

静态方法(static methods) 不会接收实例(self)或类(cls)作为第一个参数。它们只是定义在类内部的普通函数,因为它们在逻辑上与该类相关。

31.4.1) 定义静态方法

我们使用 @staticmethod 装饰器创建静态方法:

python
class MathUtils:
    @staticmethod
    def is_even(number):
        """检查一个数字是否为偶数"""
        return number % 2 == 0
    
    @staticmethod
    def is_prime(number):
        """检查一个数字是否为质数(简化版)"""
        if number < 2:
            return False
        for i in range(2, int(number ** 0.5) + 1):
            if number % i == 0:
                return False
        return True
 
# 在类上调用静态方法
print(MathUtils.is_even(4))  # Output: True
print(MathUtils.is_even(7))  # Output: False
print(MathUtils.is_prime(17))  # Output: True
print(MathUtils.is_prime(18))  # Output: False
 
# 也可以在实例上调用(但它是同一个函数)
utils = MathUtils()
print(utils.is_even(10))  # Output: True

静态方法不需要访问实例或类数据——它们是自包含的工具函数。

31.4.2) 何时使用静态方法 vs 类方法 vs 实例方法

下面是选择方式:

python
class Temperature:
    # 类变量
    absolute_zero_celsius = -273.15
    
    def __init__(self, celsius):
        self.celsius = celsius
    
    # 实例方法 - 需要访问实例数据(self)
    def to_fahrenheit(self):
        return self.celsius * 9/5 + 32
    
    # 类方法 - 需要访问类数据(cls)
    @classmethod
    def get_absolute_zero(cls):
        return cls.absolute_zero_celsius
    
    # 静态方法 - 不需要实例或类数据
    @staticmethod
    def celsius_to_kelvin(celsius):
        return celsius + 273.15
    
    @staticmethod
    def fahrenheit_to_celsius(fahrenheit):
        return (fahrenheit - 32) * 5/9
 
temp = Temperature(25)
 
# 实例方法 - 使用实例数据
print(temp.to_fahrenheit())  # Output: 77.0
 
# 类方法 - 使用类数据
print(Temperature.get_absolute_zero())  # Output: -273.15
 
# 静态方法 - 只是工具函数
print(Temperature.celsius_to_kelvin(25))  # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77))  # Output: 25.0

指南:

  • 当你需要访问实例属性(self)时使用 实例方法
  • 当你需要访问类属性或需要替代构造器(cls)时使用 类方法
  • 当你不需要访问实例或类数据,但函数在逻辑上与类相关时使用 静态方法

注意:静态方法也可以写成独立函数,但把它们放在类里可以把相关功能组织在一起,并避免污染全局命名空间。

方法类型第一个参数适用场景
实例方法self需要访问实例数据
类方法cls需要访问类数据或替代构造器
静态方法(无)与类相关的工具函数

31.4.3) 实用示例:验证工具

静态方法非常适合做验证与工具函数:

python
class User:
    def __init__(self, username, password):
        if not User.is_valid_username(username):
            raise ValueError("Invalid username")
        if not User.is_valid_password(password):
            raise ValueError("Invalid password")
        
        self.username = username
        self._password = password
    
    @staticmethod
    def is_valid_username(username):
        """检查 username 是否满足要求"""
        return len(username) >= 3 and username.isalnum()
        
    @staticmethod
    def is_valid_password(password):
        """检查 password 是否满足安全要求"""
        return len(password) >= 8 and any(c.isdigit() for c in password)
 
# 这些验证方法可以独立使用
print(User.is_valid_username("alice123"))  # Output: True
print(User.is_valid_username("ab"))  # Output: False
print(User.is_valid_password("pass1234"))  # Output: True
 
# 并且它们可以在类的任何方法中使用
try:
    user = User("ab", "short")
except ValueError as e:
    print(f"Error: {e}")  # Output: Error: Invalid username

31.5) 理解特殊方法(魔术方法)

Python 的 特殊方法(special methods)(也称为 魔术方法(magic methods)dunder methods,因为它们有双下划线)允许你自定义对象在 Python 内置操作中的行为。我们已经在第 30 章中使用过 __init____str____repr__。现在我们将探索更多方法。

31.5.1) 特殊方法的作用

当你使用某些语法或内置函数时,特殊方法会被 Python 自动调用:

python
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return f"Point({self.x}, {self.y})"
 
point = Point(3, 4)
 
# 当你调用 print() 时,Python 会调用 __str__()
print(point)  # Output: Point(3, 4)
# 这等价于:print(point.__str__())

特殊方法让你的类像内置类型一样工作。例如,你可以让对象:

  • 支持算术运算(+-*/
  • 可比较(<>==
  • len()in 和索引配合使用
  • 像容器或序列一样工作

31.5.2) 常见特殊方法类别

下面是特殊方法的主要类别:

字符串表示(对象如何显示):

  • __str__() - 用于 print()str()
  • __repr__() - 用于 REPL 和 repr()

比较(对象比较):

  • __eq__() - 对应 ==
  • __ne__() - 对应 !=
  • __lt__() - 对应 <
  • __le__() - 对应 <=
  • __gt__() - 对应 >
  • __ge__() - 对应 >=

算术(数学运算):

  • __add__() - 对应 +
  • __sub__() - 对应 -
  • __mul__() - 对应 *
  • __truediv__() - 对应 /

容器/序列(类集合行为):

  • __len__() - 对应 len()
  • __contains__() - 对应 in
  • __getitem__() - 用于索引 obj[key]
  • __setitem__() - 用于赋值 obj[key] = value

我们将在后续章节中详细探索这些内容。

31.6) 示例 1:集合接口(lencontains

我们来创建一个管理条目集合的类,并让它可以与 Python 内置的 len() 函数与 in 运算符配合使用。

31.6.1) 为 len() 实现 len

当你对对象使用 len() 时,会调用特殊方法 __len__()

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        """返回购物车中的条目数量"""
        return len(self.items)
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
 
# len() 会调用 __len__()
print(len(cart))  # Output: 3

如果没有 __len__(),调用 len(cart) 会抛出 TypeError。通过实现它,我们的 ShoppingCart 就能像内置集合一样工作。

31.6.2) 为 in 运算符实现 contains

当你使用 in 运算符时,会调用特殊方法 __contains__()

python
class ShoppingCart:
    def __init__(self):
        self.items = []
    
    def add_item(self, item):
        self.items.append(item)
    
    def __len__(self):
        return len(self.items)
    
    def __contains__(self, item):
        """检查某个条目是否在购物车中"""
        return item in self.items
 
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
 
# in 运算符会调用 __contains__()
print("Apple" in cart)  # Output: True
print("Orange" in cart)  # Output: False

现在我们的购物车支持自然的 Python 成员测试语法。

31.6.3) 构建更完整的集合类

我们来创建一个更贴近实际的集合类,用于跟踪学生成绩:

python
class GradeBook:
    def __init__(self):
        self.grades = {}  # student_name: list of grades
    
    def add_grade(self, student, grade):
        """为某个学生添加一个成绩"""
        if student not in self.grades:
            self.grades[student] = []
        self.grades[student].append(grade)
    
    def __len__(self):
        """返回学生数量"""
        return len(self.grades)
    
    def __contains__(self, student):
        """检查某个学生是否有任何成绩"""
        return student in self.grades
    
    def get_average(self, student):
        """获取某个学生的平均成绩"""
        if student not in self:
            return None
        grades = self.grades[student]
        return sum(grades) / len(grades)
    
    def __str__(self):
        return f"GradeBook with {len(self)} students"
 
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
 
print(gradebook)  # Output: GradeBook with 2 students
print(len(gradebook))  # Output: 2
 
print("Alice" in gradebook)  # Output: True
print("Carol" in gradebook)  # Output: False
 
print(f"Alice's average: {gradebook.get_average('Alice')}")  # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}")  # Output: Bob's average: 82.66666666666667

注意 get_average() 使用了 if student not in self——这会调用我们的 __contains__() 方法,让代码读起来更自然。

31.7) 示例 2:运算符重载(addeqlt

运算符重载(operator overloading) 指的是为自定义类定义 +==< 等运算符的行为。这会让你的对象能够自然地使用 Python 的语法。

31.7.1) 为加法实现 add

当你使用 + 运算符时,会调用特殊方法 __add__()

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """两个向量相加"""
        return Vector(self.x + other.x, self.y + other.y)
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
# + 运算符会调用 __add__()
v3 = v1 + v2
print(v3)  # Output: Vector(4, 6)

当 Python 看到 v1 + v2 时,会调用 v1.__add__(v2)。左操作数的 __add__() 方法会把右操作数作为参数接收。

31.7.2) 为相等实现 eq

当你使用 == 运算符时,会调用特殊方法 __eq__()

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    def __eq__(self, other):
        """检查两个向量是否相等"""
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
 
# == 运算符会调用 __eq__()
print(v1 == v2)  # Output: True
print(v1 == v3)  # Output: False

如果没有 __eq__(),Python 比较的是对象标识(它们是否是内存中的同一个对象),而不是它们的值。通过 __eq__(),我们定义了对于该类而言“相等”的含义。

31.7.3) 实现比较运算符

我们来为 Money 类实现比较运算符:

python
class Money:
    def __init__(self, amount):
        self.amount = amount
    
    def __eq__(self, other):
        """检查金额是否相等"""
        return self.amount == other.amount
    
    def __lt__(self, other):
        """检查该金额是否小于 other"""
        return self.amount < other.amount
    
    def __le__(self, other):
        """检查该金额是否小于或等于 other"""
        return self.amount <= other.amount
    
    def __gt__(self, other):
        """检查该金额是否大于 other"""
        return self.amount > other.amount
    
    def __ge__(self, other):
        """检查该金额是否大于或等于 other"""
        return self.amount >= other.amount
    
    def __str__(self):
        return f"${self.amount:.2f}"
 
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
 
print(price1 == price3)  # Output: True
print(price1 < price2)  # Output: True
print(price1 <= price3)  # Output: True
print(price2 > price1)  # Output: True
print(price2 >= price1)  # Output: True

31.7.4) 在运算符中处理类型不匹配

实现运算符时,你应该处理右操作数不是预期类型的情况:

python
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):
        """两个向量相加,或把标量加到两个分量上"""
        if isinstance(other, Vector):
            return Vector(self.x + other.x, self.y + other.y)
        elif isinstance(other, (int, float)):
            return Vector(self.x + other, self.y + other)
        else:
            return NotImplemented  # 让 Python 尝试 other.__radd__(self)
    
    def __eq__(self, other):
        if not isinstance(other, Vector):
            return False
        return self.x == other.x and self.y == other.y
    
    def __str__(self):
        return f"Vector({self.x}, {self.y})"
 
v1 = Vector(1, 2)
v2 = Vector(3, 4)
 
print(v1 + v2)  # Output: Vector(4, 6) (vector addition)
print(v1 + 5)  # Output: Vector(6, 7) (scalar addition)
 
print(v1 == v2)  # Output: False
print(v1 == "not a vector")  # Output: False (no error)

返回 NotImplemented(一个特殊的内置常量)会告诉 Python 去尝试在右操作数上执行反射操作。这对于让运算符能在不同类型之间正确工作非常重要。

运算符重载

算术运算符

比较运算符

__add 对应 +

__sub 对应 -

__mul 对应 *

__truediv 对应 /

__eq 对应 ==

__lt 对应 <

__le 对应 <=

__gt 对应 >

__ge 对应 >=

31.8) 示例 3:序列访问(getitemsetitem

__getitem__()__setitem__() 这两个特殊方法让你可以在自定义类上使用索引语法(obj[key])。这会让你的对象表现得像列表、字典或其他序列一样。

31.8.1) 为索引访问实现 getitem

当你使用方括号访问某个元素时,会调用 __getitem__() 方法:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """按索引获取歌曲"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# 索引会调用 __getitem__()
print(playlist[0])  # Output: Song A
print(playlist[1])  # Output: Song B
print(playlist[-1])  # Output: Song C (negative indexing works!)

因为我们把访问委托给了 self.songs[index],所以列表的所有索引特性都会自动生效:正索引、负索引,以及对无效索引抛出 IndexError

31.8.2) 使用 getitem 支持切片

同一个 __getitem__() 方法也会处理切片:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        """按索引或切片获取歌曲"""
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
 
# 切片也会调用 __getitem__()
print(playlist[1:3])  # Output: ['Song B', 'Song C']
print(playlist[:2])  # Output: ['Song A', 'Song B']
print(playlist[::2])  # Output: ['Song A', 'Song C']

当你使用切片时,Python 会把一个 slice 对象传给 __getitem__()。通过委托给 self.songs[index],我们就自动支持了所有切片语法。

31.8.3) 为赋值实现 setitem

当你对某个索引赋值时,会调用 __setitem__() 方法:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __setitem__(self, index, value):
        """替换指定索引处的歌曲"""
        self.songs[index] = value
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
print(playlist[1])  # Output: Song B
 
# 赋值会调用 __setitem__()
playlist[1] = "New Song B"
print(playlist[1])  # Output: New Song B

31.8.4) 使用 getitem 让对象可迭代

一个有趣的副作用是:如果你实现了从 0 开始的整数索引 __getitem__(),你的对象会自动变为可迭代:

python
class Playlist:
    def __init__(self, name):
        self.name = name
        self.songs = []
    
    def add_song(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)
 
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
 
# for 循环会自动工作!
for song in playlist:
    print(song)
# Output:
# Song A
# Song B
# Song C

Python 会尝试通过调用 __getitem__(0)__getitem__(1) 等进行迭代,直到得到 IndexError 为止。这是一种较旧的迭代协议——我们会在第 35 章学习现代的迭代器协议。

31.8.5) 使用字符串键实现类似字典的访问

__getitem__()__setitem__() 可以使用任何类型的键,而不仅仅是整数:

python
class ScoreBoard:
    def __init__(self):
        self.scores = {}
    
    def __getitem__(self, player_name):
        """获取某个玩家的分数"""
        return self.scores.get(player_name, 0)
    
    def __setitem__(self, player_name, score):
        """设置某个玩家的分数"""
        self.scores[player_name] = score
    
    def __contains__(self, player_name):
        return player_name in self.scores
    
    def __len__(self):
        return len(self.scores)
 
scoreboard = ScoreBoard()
 
# 使用字符串键设置分数
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
 
# 更新分数
scoreboard["Alice"] = 120
 
# 获取分数
print(scoreboard["Alice"])  # Output: 120
print(scoreboard["Bob"])    # Output: 85
print(scoreboard["Carol"])  # Output: 0
 
print("Alice" in scoreboard)  # Output: True
print(len(scoreboard))  # Output: 2

序列访问

getitem

setitem

用于 obj[key] 的调用

处理索引

处理切片

让对象可迭代

用于 obj[key] = value 的调用

支持赋值

可验证值


本章展示了如何创建能够与 Python 语法无缝集成的复杂类。通过实现类变量、属性、类方法、静态方法以及特殊方法,你可以让自定义类像内置类型一样工作。在第 32 章中,我们将探索继承与多态,它们让你能够构建相关类的层次结构,共享并扩展行为。

© 2025. Primesoft Co., Ltd.
support@primesoft.ai