上一篇文章主要涉及了Python类特的继承、拓展和定制以及抽象超类等概念。今天继续学习类机制中的另一重要特性——运算符重载。运算符重载可以截获并响应用在内置类型上的运算:加法、切片、打印和点号等,使类实例的行为更像内置类型。
运算符重载的概念
运算符重载是指在类方法中拦截内置操作————当类实例出现在内置操作中,Python会自动调用程序员自行设计的算法。运算符重载使类实例的行为更像内置类型。
- 运算符重载让类拦截常规的Python运算。
- 类可以重载所有的Python表达式运算符以及打印、函数调用、属性点号运算等内置运算。
- 重载是通过提供特殊名称的类方法来实现的。
其中实例的构造函数 (__ init __) 就是运算符重载的常用例子。部分常见的运算符重载方法如下:
方法 | 重载 | 调用 |
__ init __ | 构造函数 | 对象建立: X = class(args) |
__ del __ | 析构函数 | X对象回收 |
__ add __ | 运算符+ | 对象加法 |
__ repr __ , __ str __ | 打印、转换 | print(X)、repr(X)、str(X) |
__ call __ | 函数调用 | X(*args,**kargs) |
__ getattr __ | 点号运算 | X.undefined |
_s etattr _ | 属性赋值语句 | X.any = value |
__ getattribute __ | 属性获取 | X.any |
__ getitem __ | 索引运算 | X[key],X[i : j] |
__ setitem __ | 索引复制语句 | X[key] = value |
__ len __ | 长度 | len(X) |
__ bool__ | 布尔测试 | bool(X), 真值测试 |
__ iter __ , __ next __ | 迭代环境 | I = iter(X), next(X); for loops;其他 |
__ contains __ | 成员关系测试 | item in X(任何可迭代的) |
所有重载方法的名称前后都有两个下划线字符,以便把同类中定义的变量名区别开来。当类没有编写或者继承一个方法的时候,类不支持该运算。
运算符重载实例
索引与切片:__ getitem __ 和 __ setitem __
如果在类中定义或继承了索引运算,当实例出现X[i]这样的索引运算时,Python会自动调用该实例继承的__getitem__方法。
class UserList(object):
def \__init\__(self, _list):
self._list = _list
def \__getitem\__(self, key):
return self._list[key]
if \__name\__ == '\__main\__':
X = UserList([1, 2, 3, 4, 5, 6, 7])
Y = X[1]
Z = X[1:5]
print(Y, Z, sep='\n')
输出结果为:
2
[2, 3, 4, 5]
在Python中切片实际上是一个用** 切片对象**进行索引的语法糖。 切片边界被绑定到了一个 切片对象中,并传递给索引的列表实现,即当我们调用L[a:b]
时,实际上调用了L[slice(a, b)]
。
对于如上文中带有\__getitem\__
的类,当传入key时,它的数学假设为传入一个整数索引(index),即进行索引操作。然而对该实例进行切片操作时,该方法接受的实际上是一个切片对象,在新的索引表达式中直接传递给嵌套的列表索引(即本例中的self._list[key]
)。
为了更好地理解,我们可以修改一下类方法定义:
class UserList(object):
def \__init\__(self, li):
self.li = li
def \__getitem\__(self, key):
print('Key:', key)
return self.li[key]
if \__name\__ == '\__main\__':
userlist = UserList([1, 2, 3, 4, 5, 6, 7])
userslice = userlist[1:5]
print(userslice)
输出结果:
Key: slice(1, 5, None)
[2, 3, 4, 5]
从输出结果可以看到,当进行切片操作时,实际上传入了一个切片对象slice()
。
返回字符串表达形式:__ repr __ 和 __ str __
一个定义或继承了__repr__方法的类,继承自该类的实例打印或转换成字符串时_repr_(或者_str_)就会自动调用。用这种方法可以替对象定义更好地显示格式,而不是默认的实例显示。同样用一个例子介绍这种重载方法:
class returnstring:
def \__init\__(self, data):
self.data = data
def \__repr\__(self):
return '{name}: {data}'.format(name=self.\__class\__.\__name\__, data=self.data)
if \__name\__ == '\__main\__':
X = returnstring(123456)
print(X, type(repr(X)), sep='\n')
print(X, type(str(X)), sep=' ')
输出结果:
returnstring: 123456 <class 'str'>
returnstring: 123456 <class 'str'>
为什么会有__str__和__repr__两个显示方法?总体说为了进行用户友好界面显示:
- 打印操作会首先尝试__str__和str内置函数,通常可以返回一个用户友好的显示。
- __repr__用于其他所有环境中:交互模式下提示回应、repr函数以及如果类中没有定义_str_,该重载运算符可以(代替__str__方法)使实例可以使用内置的print和str函数。通常返回一个编码字符串,用以重新创建对象或者给开发者一个详细的显示。
- 如果没有定义_str_,打印还是使用_repr_,但反过来并不成立。然而在某些环境,如交互式响应模式下,仅使用_repr_。基于此,__ repr __是所有环境统一显示的最佳选择。
- __ repr __ 和 __ str __ 都必须返回字符串,如果必要的话,确保使用转换器(内置str函数)处理返回值。
在实际应用中,__ repr __ 和 __ str __ 作为打印对象并显示定制信息的运算符重载方法十分常见。