5-5-运算符重载
1. 基础知识
运算符重载意味着在类方法中拦截内置的操作——当类的实例出现在内置操作中,Python 会自动调用你的方法,并且你的方法的返回值变成了相应操作的结果:
- 运算符重载让类拦截常规的 Python 运算。
- 类可重载所有 Python 表达式运算符。
- 类也可重载打印、函数调用、属性调号运算等内置运算。
- 重载使类实例的行为像内置类型。
- 重载是通过提供特殊名称的类方法来实现的。
1.1 构造函数和表达式:___init___
和 ___sub___
class Number:
def __init__(self, start):
self.data = start
def __sub__(self, other):
return Number(self.data - other)
X = Number(5) # Number.__init__(X, 5)
Y = X - 2 # Number.__sub__(X, 2)
Y.data
3
1.2 常见的运算符重载方法
方法 | 重载 | 调用 |
---|---|---|
__init__ | 构造函数 | 对象建立:X = Class(args) |
__del__ | 析构函数 | X 对象收回 |
__add__ | 运算符 + | 如果没有 __iadd__ ,则 X + Y, X += Y |
__or__ | 运算符 | (位OR) | 如果没有 __ior__ ,则 X | Y, X |= Y |
__repr__ ,__str__ | 打印、转换 | print(X)、repr(X)、str(X) |
__call__ | 函数调用 | X(*args, **kargs) |
__getattr__ | 点号运算 | X.undefined |
__setattr__ | 属性赋值语句 | X.any = value |
__delattr__ | 属性删除 | del X.any |
__getattribute__ | 属性获取 | X.any |
__getitem__ | 索引、分片、迭代 | X[key] , X[i:j] ,没 __iter__ 时的 for 循环和其他迭代器 |
__setitem__ | 索引和分片赋值语句 | X[key] = value , X[i:j] = sequence |
__delitem__ | 索引和分片删除 | del X[key] , del X[i:j] |
__len__ | 长度 | len(X),如果没有 __bool__ ,则真值测试 |
__bool__ | 布尔测试 | bool(X),真值测试 |
__lt__ , __gt__ , | 特定的比较 | X < Y, X > Y |
__le__ , __ge__ , | X <= Y, X >= Y | |
__eq__ , __ne__ | X == Y, X != Y | |
__radd__ | 右侧加法 | other + X |
__iadd__ | 原地增强加法 | X += Y |
__iter__ , __next__ | 迭代环境 | I=iter(X), next(I); for loops, 没有 __contains__ 的 in, 所有解析式, map(F,X), 其他 |
__contains__ | 成员关系测试 | item in X |
__index__ | 整数值 | hex(X), bin(X), oct(X), O[X] , O[X:] |
__enter__ , __exit__ | 环境管理器 | with obj as var: |
__get__ , __set__ , | 描述符属性 | X.attr, X.attr = value |
__delete__ | del X.attr | |
__new__ | 创建 | 在 __init__ 之前创建对象 |
2. 索引和分片:__getitem__
和 __setitem__
如果在类中定义了(或继承了)的话,则对于实例的索引运算,会自动调用 __getitem__
。
class Indexer:
def __getitem__(self, index):
return index ** 2
X = Indexer()
X[2]
4
for i in range(5):
print(X[i], end=' ')
0 1 4 9 16
2.1 拦截分片
对于分片表达式也调用 __getitem__
。
class Indexer:
data = [5, 6, 7, 8, 9]
def __getitem__(self, index):
print('getitem:', index)
return self.data[index]
X = Indexer()
X[0]
getitem: 0
5
X[1]
getitem: 1
6
X[2:4]
getitem: slice(2, 4, None)
[7, 8]
__setitem__
索引赋值方法类似地拦截索引和分片赋值——它为后者接收了一个分片对象。
3. 索引迭代:__getitem__
__getitem__
可以使 Python 中一种重载迭代的方式。如果定义了这个方法,for 循环每次循环时都会调用类的 __getitem__
,并持续搭配有更高的偏移值。
class stepper:
def __getitem__(self, i):
return self.data[i]
X = stepper()
X.data = 'Spam'
X[1]
'p'
for item in X:
print(item, end=' ')
S p a m
'p' in X
True
list(map(str.upper, X))
['S', 'P', 'A', 'M']
4. 迭代器对象:__iter__
和 __next__
Python 中所有的迭代环境都会先尝试 __iter__
方法,再尝试 __getitem__
。只有在对象不支持迭代协议的时候,才会尝试索引运算。
4.1 用户定义的迭代器
class Squares:
def __init__(self, start, stop):
self.value = start - 1
self.stop = stop
def __iter__(self):
return self
def __next__(self): # 每次迭代返回一个平方值
if self.value == self.stop:
raise StopIteration
self.value += 1
return self.value ** 2
for i in Squares(1, 5): # for 调用 __iter__
print(i, end=' ') # 每次迭代调用 __next__
1 4 9 16 25
和 __getitem__
不同,__iter__
只循环一次。每次新的循环,都得创建一个新的迭代器对象。
X = Squares(1, 5)
[n for n in X]
[1, 4, 9, 16, 25]
[n for n in X]
[]
4.2 有多个迭代器的对象
要达到多个迭代器的效果,__iter__
只需替迭代器定义新的状态对象,而不是返回 self。
在某些应用中,还可以将 __iter__
方法和 yield 生成器函数表达式结合在一起。因为生成器函数自动保存本地变量状态并且创建需要的迭代器方法。调用生成器函数时,自动创建 __iter__
方法,简单地返回自身。
class Squares:
def __init__(self, start, stop):
self.start = start
self.stop = stop
def __iter__(self):
for value in range(self.start, self.stop + 1):
yield value ** 2
for i in Squares(1, 5):
print(i, end=' ')
1 4 9 16 25
使用 __iter__
/yield 组合还支持多个活跃的迭代器:
S = Squares(1, 5)
I = iter(S)
next(I), next(I)
(1, 4)
J = iter(S)
next(J)
1
基于生成器的方法移除了编写额外迭代器类的需要。
4.3 成员关系:__contains__
、__iter__
和 __getitem__
运算符重载往往是多个层级的:类可以提供特定的方法,或者用作退而求其次选项的更通用的替代方案。
在迭代领域,类通常把 in 成员关系运算符实现为一个迭代,使用 __iter__
方法或 __getitem__
方法。要支持更加特定的成员关系,类可能编写一个 __contains__
方法——该方法优先于 __iter__
方法,__iter__
方法优先于 __getitem__
方法。__contains__
方法应该把成员关系定义为对一个映射应用键,以及用于序列的搜索。
class Iters:
def __init__(self, value):
self.data = value
def __getitem__(self, i): # 迭代的替代方案
print('get[%s]:' % i, end='') # 也可用于索引和分片
return self.data[i]
def __iter__(self): # 优先用于迭代
print('iter=> ', end='')
self.ix = 0
return self
def __next__(self):
print('next:', end='')
if self.ix == len(self.data): raise StopIteration
item = self.data[self.ix]
self.ix += 1
return item
def __contains__(self, x): # 优先用于 in
print('contains: ', end='')
return x in self.data
next = __next__
if __name__ == '__main__':
X = Iters([1, 2, 3, 4, 5])
print(3 in X) # 成员关系
for i in X:
print(i, end=' | ')
print()
print([i ** 2 for i in X]) # 其他迭代环境
print(list(map(bin, X)))
I = iter(X) # 手动迭代
while True:
try:
print(next(I), end=' @ ')
except StopIteration:
break
contains: True
iter=> next:1 | next:2 | next:3 | next:4 | next:5 | next:
iter=> next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=> next:next:next:next:next:next:['0b1', '0b10', '0b11', '0b100', '0b101']
iter=> next:1 @ next:2 @ next:3 @ next:4 @ next:5 @ next:
特定的 __contains__
拦截成员关系,通用的 __iter__
捕获其他的迭代环境以至 __next__
重复地被调用,而 __getitem__
不会被调用。
class Iters:
def __init__(self, value):
self.data = value
def __getitem__(self, i): # 迭代的替代方案
print('get[%s]:' % i, end='') # 也可用于索引和分片
return self.data[i]
def __iter__(self): # 优先用于迭代
print('iter=> ', end='')
self.ix = 0
return self
def __next__(self):
print('next:', end='')
if self.ix == len(self.data): raise StopIteration
item = self.data[self.ix]
self.ix += 1
return item
next = __next__
if __name__ == '__main__':
X = Iters([1, 2, 3, 4, 5])
print(3 in X) # 成员关系
for i in X:
print(i, end=' | ')
print()
print([i ** 2 for i in X]) # 其他迭代环境
print(list(map(bin, X)))
I = iter(X) # 手动迭代
while True:
try:
print(next(I), end=' @ ')
except StopIteration:
break
iter=> next:next:next:True
iter=> next:1 | next:2 | next:3 | next:4 | next:5 | next:
iter=> next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=> next:next:next:next:next:next:['0b1', '0b10', '0b11', '0b100', '0b101']
iter=> next:1 @ next:2 @ next:3 @ next:4 @ next:5 @ next:
注释掉 __contains__
方法后,成员关系路由到了通用的 __iter__
。
class Iters:
def __init__(self, value):
self.data = value
def __getitem__(self, i): # 迭代的替代方案
print('get[%s]:' % i, end='') # 也可用于索引和分片
return self.data[i]
def __next__(self):
print('next:', end='')
if self.ix == len(self.data): raise StopIteration
item = self.data[self.ix]
self.ix += 1
return item
next = __next__
if __name__ == '__main__':
X = Iters([1, 2, 3, 4, 5])
print(3 in X) # 成员关系
for i in X:
print(i, end=' | ')
print()
print([i ** 2 for i in X]) # 其他迭代环境
print(list(map(bin, X)))
I = iter(X) # 手动迭代
while True:
try:
print(next(I), end=' @ ')
except StopIteration:
break
get[0]:get[1]:get[2]:True
get[0]:1 | get[1]:2 | get[2]:3 | get[3]:4 | get[4]:5 | get[5]:
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:[1, 4, 9, 16, 25]
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:['0b1', '0b10', '0b11', '0b100', '0b101']
get[0]:1 @ get[1]:2 @ get[2]:3 @ get[3]:4 @ get[4]:5 @ get[5]:
如果 __contains__
和 __iter__
都注释掉,索引 __getitem__
替代方法会被调用,针对成员关系和其他迭代环境使用连续较高的索引。
5. 属性访问:__getattr__
和 __setattr__
5.1 属性引用
__getattr__
方法是拦截属性点号运算。当通过对未定义属性名称和实例进行点号运算时,就会用属性名称作为字符串调用这个方法。如果 Python 可通过其继承树搜索流程找到这个属性,该方法就不会被调用。__getattr__
可以作为 hook 来通过通用的方式响应属性请求:
class empty:
def __getattr__(self, attrname):
if attrname == 'age':
return 40
else:
raise AttributeError(attrname)
X = empty()
X.age
40
X.name
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-27-94c5350c32f8> in <module>()
----> 1 X.name
<ipython-input-26-518e835987e2> in __getattr__(self, attrname)
4 return 40
5 else:
----> 6 raise AttributeError(attrname)
7
8 X = empty()
AttributeError: name
empty 类和其实例 X 本身并没有属性,所以对 X.age 的存取会转至 __getattr__
方法,self 则赋值为实例(X),而 attrname 则赋值为未定义的属性名称字符串("age")。
5.2 属性赋值和删除
__setattr__
会拦截所有属性的赋值语句。如果定义或继承了这个方法,self.attr = value
会变成 self.__setattr__('attr', value)
。
在 __setattr__
中对任何 self 属性做赋值,都会再调用 __setattr__
,导致了无穷递归循环。如果想使用这个方法,要确定是通过对属性字典做索引运算来赋值任何实例属性。也就是说,使用 self.__dict__['name'] = x
,而不是 self.name = x
:
class Accesscontrol:
def __setattr__(self, attr, value):
if attr == 'age':
self.__dict__[attr] = value + 10
else:
raise AttributeError(attr + ' not allowed')
X = Accesscontrol()
X.age = 40
X.age
50
X.name = 'Bob'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-31-05b099f59059> in <module>()
----> 1 X.name = 'Bob'
<ipython-input-30-756addf50487> in __setattr__(self, attr, value)
4 self.__dict__[attr] = value + 10
5 else:
----> 6 raise AttributeError(attr + ' not allowed')
7
8 X = Accesscontrol()
AttributeError: name not allowed
另一个属性管理方法,__delattr__
,传递属性名字符串,所有属性删除(例如,del object.attr)时都会调用。像 __setattr__
一样,必须避免递归循环,通过使用 __dict__
或超类。
6. 模拟实例属性的私有化:第一部分
每个子类都有自己的私有变量名列表,这些变量名无法通过其实例进行赋值:
class PrivateExc(Exception): pass
class Privacy:
def __setattr__(self, attrname, value):
if attrname in self.privates:
raise PrivateExc(attrname, self)
else:
self.__dict__[attrname] = value
class Test1(Privacy):
privates = ['age']
class Test2(Privacy):
privates = ['name', 'pay']
def __init__(self):
self.__dict__['name'] = 'Tom'
if __name__ == '__main__':
x = Test1()
y = Test2()
x.name = 'Bob'
y.name = 'Sue'
---------------------------------------------------------------------------
PrivateExc Traceback (most recent call last)
<ipython-input-35-5241bd7fed48> in <module>()
----> 1 y.name = 'Sue'
<ipython-input-33-933031f557ee> in __setattr__(self, attrname, value)
4 def __setattr__(self, attrname, value):
5 if attrname in self.privates:
----> 6 raise PrivateExc(attrname, self)
7 else:
8 self.__dict__[attrname] = value
PrivateExc: ('name', <__main__.Test2 object at 0x000002739844FDD8>)
print(x.name)
Bob
y.age = 30
x.age = 40
---------------------------------------------------------------------------
PrivateExc Traceback (most recent call last)
<ipython-input-38-f7dd3a84c319> in <module>()
----> 1 x.age = 40
<ipython-input-33-933031f557ee> in __setattr__(self, attrname, value)
4 def __setattr__(self, attrname, value):
5 if attrname in self.privates:
----> 6 raise PrivateExc(attrname, self)
7 else:
8 self.__dict__[attrname] = value
PrivateExc: ('age', <__main__.Test1 object at 0x0000027398470D68>)
print(y.age)
30
这是 Python 中实现属性私有性的首选方法。这只是一部分的解决方案。更完整的方案是让子类也能够设置私有属性,并且使用 __getattr__
和包装(有时也称为代理)来检测对私有属性的读取。
7. 字符串表示:__repr__
和 __str__
当定义时,类的实例打印或转换成字符串时 __repr__
或 __str__
就会自动调用。这些方法可替对象定义更好的显式格式。
# 默认显示
class adder:
def __init__(self, value=0):
self.data = value
def __add__(self, other):
self.data += other
x = adder()
print(x)
<__main__.adder object at 0x000002739847A9B0>
# 定制显示
class addrepr(adder):
def __repr__(self):
return 'addrepr(%s)' % self.data
x = addrepr(2)
print(x) # 运行 __repr__
addrepr(2)
x # 运行 __repr__
addrepr(2)
- 打印操作会首先尝试
__str__
和 str 内置函数(print 运行的内部等价形式)。它通常应该返回一个用户友好的显示。 __repr__
用于所有其他的环境中:用于交互模式下提示回应以及 repr 函数,如果没有使用__str__
,会使用 print 和 str。它通常应该返回一个编码字符串,可以用来创建对象,或者给开发者一个详细的显示。
__repr__
可以定义在任何地方使用单一显示格式,并且可以将 __str__
编辑为只支持 print 和str,或者为它们提供一个替代的显示。
只要 print 和 str 显示对该工具足够,一般的工具也可能更倾向于使用 __str__
,让其他类可以选择添加一个替代的 __repr__
用于其他上下文中。相反,对 __repr__
进行编辑的通用工具仍然允许客户端为 print 和 str 添加带有 __str__
的替代显示。
如果没有定义 __str__
,打印还是使用 __repr__
,反过来并不成立——其他环境,只是使用 __repr__
,根本不要尝试 __str__
:
class addstr(adder):
def __str__(self):
return '[Value: %s]' % self.data
x = addstr(3)
x + 1
x # 运行 __repr__
<__main__.addstr at 0x2739847ac50>
print(x) # 运行 __str__
[Value: 4]
str(x), repr(x)
('[Value: 4]', '<__main__.addstr object at 0x000002739847AC50>')
如果想让所有环境都有统一的显示,__repr__
是最佳选择。通过分别定义这两个方法,就可在不同环境内支持不同显示。例如,终端用户显示使用 __str__
,程序员在开发期间则使用底层的 __repr__
来显示:
class addboth(adder):
def __str__(self):
return '[Value: %s]' % self.data
def __repr__(self):
return 'addboth(%s)' % self.data
x = addboth(4)
x
addboth(4)
print(x)
[Value: 4]
显示用法注意事项
__str__
和 __repr__
都必须返回字符串。
根据一个容器的字符串转换逻辑,__str__
的用户友好的显示可能只有当对象出现在一个打印操作顶层的时候才应用,嵌套到较大对象中的对象可能用其 __repr__
或默认方法打印:
class Printer:
def __init__(self, val):
self.val = val
def __str__(self):
return str(self.val)
objs = [Printer(2), Printer(3)]
for x in objs: print(x) # 打印实例时使用 __str__
2
3
print(objs) # 当实例在列表中时没有使用
[<__main__.Printer object at 0x0000027398483668>, <__main__.Printer object at 0x00000273984835F8>]
objs
[<__main__.Printer at 0x27398483668>, <__main__.Printer at 0x273984835f8>]
8. 右侧加法和原处加法:__radd__
和 __iadd__
8.1 右侧加法
__add__
方法并不支持 + 运算符右侧使用实例对象:
class Adder:
def __init__(self, value=0):
self.data = value
def __add__(self, other):
return self.data + other
x = Adder(5)
x + 2
7
2 + x
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-4f4e32a7ea51> in <module>()
----> 1 2 + x
TypeError: unsupported operand type(s) for +: 'int' and 'Adder'
要支持可互换的运算符,可以一并编写 __radd__
方法。只有当 + 右侧的对象是类实例,而左边对象不是类实例时,Python 才会调用 __radd__
:
class Commuter:
def __init__(self, value):
self.val = value
def __add__(self, other):
print('add', self.val, other)
return self.val + other
def __radd__(self, other):
print('radd', self.val, other)
return other + self.val
x = Commuter(88)
y = Commuter(99)
x + 1
add 88 1
89
1 + y
radd 99 1
100
x + y
add 88 <__main__.Commuter object at 0x000002A49EB504E0>
radd 99 88
187
类型测试可能需要辨别它是否能够安全地转换并由此避免嵌套,例如可以使用 isinstance 测试。
8.2 原处加法
class Number:
def __init__(self, val):
self.val = val
def __iadd__(self, other): # 显式定义 __iadd__: x += y
self.val += other # 通常返回 self
return self
x = Number(5)
x += 1
x += 1
x.val
7
9. Call 表达式:__call__
当调用实例时,使用 __call__
方法:
class Callee:
def __call__(self, *pargs, **kargs):
print('Called:', pargs, kargs)
C = Callee()
C(1, 2, 3)
Called: (1, 2, 3) {}
C(1, 2, 3, x=4, y=5)
Called: (1, 2, 3) {'x': 4, 'y': 5}
传递给实例的任何内容都会传递给该方法,包括通常隐式的实例参数。
拦截调用表达式允许类实例模拟类似函数的外观,但是,也在调用中保持了状态信息以供使用:
class Prod:
def __init__(self, value):
self.value = value
def __call__(self, other):
return self.value * other
x = Prod(2)
x(3)
6
x(4)
8
10. 比较:__lt__
、__gt__
和其他方法
- 与
__add__
/__radd__
对不同,比较方法没有右端形式。 - 比较运算符没有隐式关系。例如,== 并不意味着 != 是假的。
class C:
data = 'spam'
def __gt__(self, other):
return self.data > other
def __lt__(self, other):
return self.data < other
X = C()
print(X > 'ham')
print(X < 'ham')
True
False
11. 布尔测试:__bool__
和 __len__
在布尔环境中,Python 首先尝试 __bool__
来获取一个直接的布尔值,如果没有该方法,就尝试 __len__
类根据对象的长度确定一个真值。通常,首先使用对象状态或其他信息来生成一个布尔结果:
class Truth:
def __bool__(self): return True
X = Truth()
if X: print('yes!')
yes!
class Truth:
def __bool__(self): return False
X = Truth()
bool()
False
如果没有 __bool__
方法,Python 退而求其次地求长度,如果两个方法都有,Python 喜欢 __bool__
胜过 __len__
:
class Truth:
def __bool__(self): return True
def __len__(self): return 0
X = Truth()
if X: print('yes!')
yes!
如果没有定义真的方法,对象毫无疑义地看作为真:
class Truth:
pass
X = Truth()
bool(X)
True
12. 对象析构函数:__del__
每当实例产生时,就会调用 __init__
构造函数。每当实例空间被收回时,就会自动执行 __del__
析构函数:
class Life:
def __init__(self, name='unknown'):
print('Hello ' + name)
self.name = name
def live(self):
print(self.name)
def __del__(self):
print('Goodbye ' + self.name)
brian = Life('Brian')
Hello Brian
brian.live()
Brian
brian = 'loretta'
Goodbye Brian
失去实例的最后一个引用时,会触发其析构函数。在 Python 中,析构函数不像其他 OOP 语言那么常用。其一因为 Python 在实例收回时,会自动收回该实例所拥有的所有空间,对于空间管理来说,是不需要析构函数的;其二是无法轻易地预测实例何时收回,通常最好是在有意调用的方法中编写代码去终止活动。在某种情况下,系统表中可能还在引用该对象,使析构函数无法执行。