魔法
只有魔法才能打败魔法啊。Python 这么强大,必须有点魔法来控制一下。
在编写自己的类的方法的时候,有些属性或方法的名字是不能随便用的,因为 Python 已经预定义了一些有特殊含义的的名字,它们被称为魔法方法(Magic Methods)和魔法属性。在 Python 中,魔法方法和属性也被称为特殊方法和属性或者双下划线方法和属性。它们以双下划线为前后缀,例如,之前介绍过的 __init__
、 __new__
、 __call__
方法,__name_
、 __doc__
属性等。
魔法方法和属性使得我们可以自定义对象的内部行为,这样就可以实现运算符重载(比如重新定义加减乘除的行为)、属性访问等高级功能。
构造、销毁、打印
我们通过一个简单的例子来了解这一些基本的魔法方法。假设我们要创建一个简单的 Point 类,表示二维平面上的一个点:
class Point:
# 初始化一个新创建的对象
def __init__(self, x=0, y=0):
self.x = x
self.y = y
print(f"创建点 ({self.x}, {self.y})")
# 析构方法,当对象被销毁时调用
def __del__(self):
print(f"点 ({self.x}, {self.y}) 被销毁")
# 返回一个“正式”的表示,通常可以用它来重新创建这个对象
def __repr__(self):
return f"点 ({self.x}, {self.y})"
# 返回一个“非正式”的表示,用于打印或日志
def __str__(self):
return f"({self.x}, {self.y})"
# 测试一下:
p = Point(1, 2) # 输出: 创建点 (1, 2)
print(p) # 输出: (1, 2)
print(repr(p)) # 输出: 点 (1, 2)
del p # 输出: 点 (1, 2) 被销毁
在上面的程序中:
- 当一个新对象被创建后,
__init__
方法就会立刻执行,用于初始化对象的状态。在这里,我们初始化了 x 和 y 两个属性。 - 当对象被销毁(例如,当它不再被引用时)时,
__del__
方法会被调用。我们的例子中只是打印了一个简单的消息,但在实际的应用中,它可能会被用于释放资源,如关闭文件、断开网络连接等。 - repr() 函数会调用
__repr__
方法。它返回一个字符串,表示Python表达式,重新创建这个对象时可以用这个表达式。在我们的例子中,repr(point) 将返回像 Point(1, 2) 这样的字符串。在打印自身的程序一节,有一个关于这个函数的比较有趣的应用。 - 当我们打印一个对象或将其转化为字符串时,
__str__
方法会被调用。在我们的例子中,str(point)将返回(1, 2)。
如果有多个变量同时指向一个对象,那么要等到所有指向这个对象的变量都被删除后,才会真正调用 __del__
销毁对象,比如:
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
print(f"创建点 ({self.x}, {self.y})")
def __del__(self):
print(f"点 ({self.x}, {self.y}) 被销毁")
p = Point(1, 2) # 构造函数被调用
print("=====分割线======")
q = p # 新变量指向原有对象,构造函数不会被调用
del p # 还有其它变量指向这个对象,析构函数不会被调用
print("=====分割线======")
del q # 所有变量均被删除,调用析构函数销毁对象
# 输出:
# 创建点 (1, 2)
# =====分割线======
# =====分割线======
# 点 (1, 2) 被销毁
运算符
算数运算符
算术魔法方法用于重新定义对象的算术运算符行为。最常用的方法包括:
__add__(self, other)
: 定义加法行为。当程序中,使用 + 符号计算加法时,调用的就是对象的这个方法。__sub__(self, other)
: 定义减法行为。__mul__(self, other)
: 定义乘法行为。__truediv__(self, other)
: 定义实数除法行为(Python 3 中的 /)。__floordiv__(self, other)
: 定义整数除法行为(Python 3 中的 //)。__mod__(self, other)
: 定义模除法(取余)行为。__pow__(self, power[, modulo])
: 定义乘方行为。
Python 内置有一个 Fraction 类,它用于表示数学上的分数。我们下面编写一个简化版的 Fraction 类,用它来演示算术魔法方法的实现与使用。Fraction 类有两个属性分别表示分子和分母。它的实现方法如下:
from math import gcd
class Fraction:
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ValueError("分母不能为 0!")
common = gcd(numerator, denominator)
self.numerator = numerator // common
self.denominator = denominator // common
def __add__(self, other):
new_numerator = self.numerator * other.denominator + other.numerator * self.denominator
new_denominator = self.denominator * other.denominator
return Fraction(new_numerator, new_denominator)
def __sub__(self, other):
new_numerator = self.numerator * other.denominator - other.numerator * self.denominator
new_denominator = self.denominator * other.denominator
return Fraction(new_numerator, new_denominator)
def __mul__(self, other):
new_numerator = self.numerator * other.numerator
new_denominator = self.denominator * other.denominator
return Fraction(new_numerator, new_denominator)
def __truediv__(self, other):
new_numerator = self.numerator * other.denominator
new_denominator = self.denominator * other.numerator
return Fraction(new_numerator, new_denominator)
def __repr__(self):
return f"{self.numerator}/{self.denominator}"
# 测试代码
f1 = Fraction(3, 4)
f2 = Fraction(5, 6)
print(f"{f1} + {f2} = {f1 + f2}") # 输出: 3/4 + 5/6 = 19/12
print(f"{f1} - {f2} = {f1 - f2}") # 输出: 3/4 - 5/6 = -1/12
print(f"{f1} * {f2} = {f1 * f2}") # 输出: 3/4 * 5/6 = 5/8
print(f"{f1} / {f2} = {f1 / f2}") # 输出: 3/4 / 5/6 = 9/10
需要注意的是,这几个常用的运算符都是二元运算符,是两个对象之间的运算。在运算时,程序调用的是第一个对象的对应方法。以加法为例,在运算 f1 + f2
时,它会调用对象 f1 的 __add__(self, other)
方法。并且传递给这个方法的参数中,self 是 f1,other 是 f2。
运算符两端的对象可以不是同类型的数据,只要第一个对象的 __add__
方法支持就行。比如,我们可以把这个 Fraction 类中的 __add__
方法改造一下,让它能够与一个整数相加:
from math import gcd
class Fraction:
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ValueError("分母不能为 0!")
common = gcd(numerator, denominator)
self.numerator = numerator // common
self.denominator = denominator // common
def __add__(self, other):
if isinstance(other, Fraction):
new_numerator = self.numerator * other.denominator + other.numerator * self.denominator
new_denominator = self.denominator * other.denominator
elif isinstance(other, int):
new_numerator = self.numerator + other * self.denominator
new_denominator = self.denominator
else:
raise TypeError("加法运算仅支持 Fraction 或整数类型")
return Fraction(new_numerator, new_denominator)
def __repr__(self):
return f"{self.numerator}/{self.denominator}"
# 测试代码
frac1 = Fraction(1, 2)
frac2 = Fraction(3, 4)
result1 = frac1 + frac2 # Fraction 类实例相加
result2 = frac1 + 3 # Fraction 类实例与整数相加
print(result1) # 输出: 5/4
print(result2) # 输出: 7/2
在上面的程序中,__add__
方法中检查了 other 参数的数据类型,如果它是另一个分数,那么使用分数的算法;如果它是一个整数,则采用整数的算法。因此,我们可以计算 frac1 + 3
,分数与整数相加。但是如果试图计算 3 + frac1
就会出错,因为 int 对象的 __add__
方法并没有实现对 Fraction 对象的支持。
比较运算符
顾名思义,比较魔法方法用于重新定义象之间的比较行为,常见的比较魔法方法包括:
__eq__(self, other)
: 定义等于的行为,使用 ==。__ne__(self, other)
: 定义不等于的行为,使用 !=。__lt__(self, other)
: 定义小于的行为,使用 <。__le__(self, other)
: 定义小于或等于的行为,使用 <=。__gt__(self, other)
: 定义大于的行为,使用 >。__ge__(self, other)
: 定义大于或等于的行为,使用 >=。
我们可以继续使用简化的 Fraction 类,比较魔法方法,比较两个分数之间的大小。
from math import gcd
class Fraction:
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ValueError("分母不能为 0!")
common = gcd(numerator, denominator)
self.numerator = numerator // common
self.denominator = denominator // common
def __eq__(self, other):
return self.numerator == other.numerator and self.denominator == other.denominator
def __lt__(self, other):
# 两个分数a/b 和 c/d的比较,转化为a*d < c*b
return self.numerator * other.denominator < other.numerator * self.denominator
def __le__(self, other):
return self.numerator * other.denominator <= other.numerator * self.denominator
def __gt__(self, other):
return self.numerator * other.denominator > other.numerator * self.denominator
def __ge__(self, other):
return self.numerator * other.denominator >= other.numerator * self.denominator
def __repr__(self):
return f"{self.numerator}/{self.denominator}"
# 测试
f1 = Fraction(1, 2) # 1/2
f2 = Fraction(3, 4) # 3/4
print(f1 == f2) # False
print(f1 < f2) # True
print(f1 <= f2) # True
print(f1 > f2) # False
print(f1 >= f2) # False
类型转换
类型转换魔法方法用于对象的数据类型转换, Python 内置类型转换函数会调用它们。常见的方法包括:
__int__(self)
: 使用 int(obj) 时调用。__float__(self)
: 使用 float(obj) 时调用。__bool__(self)
: 使用 bool(obj) 时调用。
我们仍然使用简化的 Fraction 类,用它演示类型转换方法,例如转换为整数、浮点数和布尔值。
class Fraction:
def __init__(self, numerator, denominator):
if denominator == 0:
raise ValueError("分母不能为 0!")
self.numerator = numerator
self.denominator = denominator
def __int__(self):
# 转换为整数,实际上是分子除以分母的整数结果
return self.numerator // self.denominator
def __float__(self):
# 转换为浮点数
return self.numerator / self.denominator
def __bool__(self):
# 如果分数不为 0,那么为 True,否则为 False
return self.numerator != 0
def __str__(self):
return f"{self.numerator}/{self.denominator}"
# 测试
f = Fraction(3, 4)
print(int(f)) # 0, 因为 3//4 = 0
print(float(f)) # 0.75, 因为 3/4 = 0.75
print(bool(f)) # True, 因为 3/4 = not 0
f_zero = Fraction(0, 1)
print(bool(f_zero)) # False, 因为分子是 0
数据结构
容器魔法方法用于定义自定义那种类似于 Python 容器(例如列表、字典)的对象。以下是一些常见的容器魔法方法:
__len__(self)
: 返回容器中的元素数。对应于内置函数 len()。__getitem__(self, key)
: 用于访问容器中的元素。对应于 obj[key] 的行为。__setitem__(self, key, value)
: 为容器中的某个元素分配值。对应于 obj[key] = value 的行为。__delitem__(self, key)
: 删除容器中的某个元素。对应于 del obj[key] 的行为。_contains__(self, item)
: 用于检查容器是否包含某个元素。对应于 item in obj 的行为。__iter__(self)
: 返回容器的迭代器。对应于 iter(obj) 的行为。
假设我们要创建一个简单的排序序列类,该类的行为类似 Python 自带的列表的基本功能,独特之处在于它内部的数据总是按大小排序。为了简单起见,我们在其内部用普通列表来保存数据,虽然这样效率不高。
class SortedList:
def __init__(self, initial_data=[]):
self.data = sorted(initial_data)
def __len__(self):
return len(self.data)
def __getitem__(self, index):
return self.data[index]
def __setitem__(self, index, value):
self.data[index] = value
self.data.sort()
def __delitem__(self, index):
del self.data[index]
def __contains__(self, value):
return value in self.data
def append(self, value):
self.data.append(value)
self.data.sort()
def __iter__(self):
return iter(self.data)
def __repr__(self):
return repr(self.data)
# 测试
lst = SortedList([3, 1, 2])
print(lst) # [1, 2, 3]
lst.append(0)
print(lst) # [0, 1, 2, 3]
lst[1] = 5 # 把第一个元素的值改为 5,之后,数据会重新排序
print(lst) # [0, 2, 3, 5]
del lst[2]
print(lst) # [0, 2, 5]
属性访问
属性访问魔法方法用于自定义属性的访问、设置、删除等行为。以下是一些常用的属性访问魔法方法:
__getattr__(self, name)
: 当尝试访问一个不存在的属性时调用此方法。__setattr__(self, name, value)
: 设置一个属性的值时调用此方法。__delattr__(self, name)
: 当尝试删除一个属性时调用此方法。__getattribute__(self, name)
: 当尝试访问任何属性时都会调用此方法。
假设我们要创建一个类,该类在设置任何属性时都会存储历史记录,并且我们想要确保某些属性名称不能被设置。
class HistoricalAttributes:
def __init__(self):
# 使用字典来存储属性历史记录
self._history = {}
self._forbidden_attributes = ["forbidden", "history"]
def __setattr__(self, name, value):
# 检查 _forbidden_attributes 是否已定义
if hasattr(self, '_forbidden_attributes'):
# 禁止设置某些属性
if name in self._forbidden_attributes:
raise AttributeError(f"'{name}' 是一个只读属性。")
# 将属性值添加到历史记录中
if name not in self._history:
self._history[name] = []
self._history[name].append(value)
# 使用超类的__setattr__方法来实际设置属性
super().__setattr__(name, value)
def history_of(self, name):
# 返回属性的历史记录
return self._history.get(name, [])
# 测试
obj = HistoricalAttributes()
obj.x = 10
obj.x = 20
obj.y = 5
print(obj.history_of('x')) # 输出:[10, 20]
print(obj.history_of('y')) # 输出:[5]
# obj.forbidden = 99 # 抛出 AttributeError: 'forbidden' 是一个只读属性。
上下文管理
当使用with语句时,上下文管理器可以保证资源,如文件、网络连接或数据库连接,被正确地获取和释放,无论在上下文中是否出现了异常。两个与此相关的方法分别是:
__enter__(self)
: 当执行with语句时被调用。这个方法的返回值被 with 语句的目标(或as子句中的变量)所使用。常用于初始化和返回需要管理的资源。__exit__(self, exc_type, exc_value, traceback)
: 当 with 块的代码执行完毕(或在执行过程中抛出异常时)被调用。如果在 with 块中没有发生异常,exc_type、 exc_value 和 traceback 将会是 None。如果这个方法返回 True,表示压制 with 块中抛出的异常,否则异常会继续传播。
假设, 我们需要编写一个计时器类,这个计时器会在 with 块的代码执行时开始计时,并在代码执行完成后停止计时,然后打印出执行时间。
import time
class Timer:
def __enter__(self):
self.start = time.time()
return self # 这里的 self 会被 with 语句的 as 子句所使用
def __exit__(self, exc_type, exc_value, traceback):
self.end = time.time()
print(f"耗时: {self.end - self.start:.2f} 秒")
return False # 如果发生异常,不要压制它
# 使用示例
with Timer() as t:
print(t)
for _ in range(1000000):
pass
# 输出类似于:耗时: 0.13 秒
常用属性
__dict__
这是一个字典,包含了对象的所有属性。当创建一个对象时,Python 通常会为其创建一个字典来保存所有属性,这样可以在运行时动态地向对象添加新的属性。
class MyClass:
def __init__(self, x, y):
self.x = x
self.y = y
obj = MyClass(1, 2)
print(obj.__dict__) # 输出:{'x': 1, 'y': 2}
obj.value = 3 # 为对象添加一个新属性
print(obj.__dict__) # 输出:{'x': 1, 'y': 2, 'value': 3}
__slots__
使用字典保存对象的属性虽然灵活,但字典有额外的内存开销。如果我们已经知道对象只需要固定的几个属性,那么使用 __slots__
可以避免这种字典开销,从而更有效地使用内存。定义 __slots__
的方法是在类中创建一个名为 __slots__
的属性,并将期望的属性名作为字符串保存在一个元组或列表中。
class Fraction:
__slots__ = ('numerator', 'denominator')
def __init__(self, numerator, denominator=1):
if denominator == 0:
raise ValueError("分母不能为 0!")
common = gcd(numerator, denominator)
self.numerator = numerator // common
self.denominator = denominator // common
# 测试
f = Fraction(1, 2) # 1/2
# 下面的代码会报错,因为 __slots__ 限制了只能有 'numerator' 和 'denominator' 这两个属性
# f.value = 3
在上面的例子中,我们只能为 Fraction 的实例设置 numerator 和 denominator 这两个属性。尝试设置其他属性会抛出一个 AttributeError。
__doc__
这个属性返回类的文档。
class MyClass:
"""This is a docstring for MyClass."""
pass
print(MyClass.__doc__) # 输出:This is a docstring for MyClass.
__name__
对于类,这返回类的名称。对于模块,返回模块的名称。
print(MyClass.__name__) # 输出:MyClass
__module__
这个属性保存了定义这个类或函数的模块名。
print(MyClass.__module__) # 通常输出:__main__
__bases__
这个属性是一个包含所有基类的元组。
class Parent:
pass
class Child(Parent):
pass
print(Child.__bases__) # 输出:(<class '__main__.Parent'>,)
__class__
它返回对象所属的类。
obj = MyClass()
print(obj.__class__) # 输出:<class '__main__.MyClass'>