上一篇文章主要涉及了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 __ 作为打印对象并显示定制信息的运算符重载方法十分常见。