31. 高级类特性
在第 30 章中,我们学习了如何使用实例属性和方法创建基础类。现在我们将探索更复杂的类特性,它们能让你对对象的行为进行更细粒度的控制。这些特性让你可以创建感觉像 Python 内置类型一样的类,对加法、比较、索引等操作拥有自然的语法。
31.1) 类变量 vs 实例变量
当我们在类中创建属性时,本质上有两个完全不同的存储位置:存储在类本身上,或存储在各个实例上。理解这种区别对于编写正确的面向对象代码至关重要。
31.1.1) 理解实例变量
实例变量(instance variables) 是属于某个特定对象的属性。每个实例都有这些变量各自独立的副本。我们在第 30 章中一直在使用实例变量——它们是在 __init__ 中通过 self 创建的属性:
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 实例都有自己的 owner 和 balance。修改 account1.balance 不会影响 account2.balance——它们是完全独立的。
31.1.2) 理解类变量
类变量(class variables) 是属于类本身的属性,而不是属于某个特定实例。所有实例共享同一个类变量。我们在类体中直接定义类变量,放在任何方法之外:
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。它们都指向同一个变量。
这就是类变量强大的地方——当我们修改类变量时,所有实例都会看到这个变化:
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 会创建一个实例变量来遮蔽(隐藏)类变量:
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) 类变量的实际用途
类变量适用于应在所有实例之间共享的数据:
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),这样可以清晰表明我们在修改类变量,而不是创建实例变量。
31.2) 使用 @property 管理属性
有时你希望控制别人访问或修改属性时会发生什么。例如,你可能想验证值必须为正数,或希望按需计算某个值而不是把它存起来。Python 的 @property 装饰器能让你编写“看起来像简单属性访问”的方法。
31.2.1) 问题:直接属性访问无法验证
当属性被直接访问时,你无法验证或转换这些值:
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”,并且像属性一样被访问:
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 保持同步:
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 方法:
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.0setter 方法会接收新值,并且可以在存储之前先验证或转换。
31.2.4) 使用属性进行验证
属性非常适合用于强制约束:
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,它就会变成只读:
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这对那些应当计算而不是存储的派生值非常有用。
31.3) 使用 @classmethod 的类方法
有时你需要的方法是对类本身起作用,而不是对实例起作用。类方法(class methods) 的第一个参数接收的是类(按惯例命名为 cls),而不是实例(self)。
31.3.1) 定义类方法
我们使用 @classmethod 装饰器创建类方法:
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 Schoolcls 参数会自动接收类,就像普通方法里的 self 会自动接收实例一样。
31.3.2) 使用类方法作为替代构造器
类方法最常见的用途之一是创建替代构造器——以不同方式创建实例:
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_string 和 today 都返回 cls(...)——这会创建该类的新实例。使用 cls 而不是硬编码 Date 能让代码在子类场景下也能正确工作(我们会在第 32 章学习继承)。
31.3.3) 用类方法实现工厂模式
类方法适合用来创建带不同配置的实例:
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:543231.3.4) 使用类方法统计实例数量
类方法可以与类变量配合,跟踪所有实例的相关信息:
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: 031.4) 使用 @staticmethod 的静态方法
静态方法(static methods) 不会接收实例(self)或类(cls)作为第一个参数。它们只是定义在类内部的普通函数,因为它们在逻辑上与该类相关。
31.4.1) 定义静态方法
我们使用 @staticmethod 装饰器创建静态方法:
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 实例方法
下面是选择方式:
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) 实用示例:验证工具
静态方法非常适合做验证与工具函数:
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 username31.5) 理解特殊方法(魔术方法)
Python 的 特殊方法(special methods)(也称为 魔术方法(magic methods) 或 dunder methods,因为它们有双下划线)允许你自定义对象在 Python 内置操作中的行为。我们已经在第 30 章中使用过 __init__、__str__ 和 __repr__。现在我们将探索更多方法。
31.5.1) 特殊方法的作用
当你使用某些语法或内置函数时,特殊方法会被 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:集合接口(len、contains)
我们来创建一个管理条目集合的类,并让它可以与 Python 内置的 len() 函数与 in 运算符配合使用。
31.6.1) 为 len() 实现 len
当你对对象使用 len() 时,会调用特殊方法 __len__():
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__():
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) 构建更完整的集合类
我们来创建一个更贴近实际的集合类,用于跟踪学生成绩:
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:运算符重载(add、eq、lt)
运算符重载(operator overloading) 指的是为自定义类定义 +、==、< 等运算符的行为。这会让你的对象能够自然地使用 Python 的语法。
31.7.1) 为加法实现 add
当你使用 + 运算符时,会调用特殊方法 __add__():
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__():
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 类实现比较运算符:
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: True31.7.4) 在运算符中处理类型不匹配
实现运算符时,你应该处理右操作数不是预期类型的情况:
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 去尝试在右操作数上执行反射操作。这对于让运算符能在不同类型之间正确工作非常重要。
31.8) 示例 3:序列访问(getitem、setitem)
__getitem__() 和 __setitem__() 这两个特殊方法让你可以在自定义类上使用索引语法(obj[key])。这会让你的对象表现得像列表、字典或其他序列一样。
31.8.1) 为索引访问实现 getitem
当你使用方括号访问某个元素时,会调用 __getitem__() 方法:
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__() 方法也会处理切片:
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__() 方法:
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 B31.8.4) 使用 getitem 让对象可迭代
一个有趣的副作用是:如果你实现了从 0 开始的整数索引 __getitem__(),你的对象会自动变为可迭代:
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 CPython 会尝试通过调用 __getitem__(0)、__getitem__(1) 等进行迭代,直到得到 IndexError 为止。这是一种较旧的迭代协议——我们会在第 35 章学习现代的迭代器协议。
31.8.5) 使用字符串键实现类似字典的访问
__getitem__() 和 __setitem__() 可以使用任何类型的键,而不仅仅是整数:
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本章展示了如何创建能够与 Python 语法无缝集成的复杂类。通过实现类变量、属性、类方法、静态方法以及特殊方法,你可以让自定义类像内置类型一样工作。在第 32 章中,我们将探索继承与多态,它们让你能够构建相关类的层次结构,共享并扩展行为。