本文并不是用来向你介绍运算符重载的基础知识,如果你想了解运算符重载的基础知识,本文可能并不适合你。本文的目的是从独特的角度介绍运算符重载,让你更深入地,更本质地了解什么是运算符重载。
0、什么是运算符重载
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
每个运算符都有自己特定的语法表达方式,例如加法运算符+要求其左右两边各有一个对象;括号运算符,要求其左边是一个可调用对象(函数对象),括号中需要一个任意长度的参数列表;等等。所谓运算符重载,说白了,就是让对象支持某个运算符的语法表达方式。
如果我们实现了一个类,这个类没有重载加法运算,那么这个类的对象就不支持加法运算表达式的语法。我们可以看一下如下的例子(如果你看不懂,你应该先了解一下运算符重载的知识再阅读本文):
a
1、对象运算符表达式的本质
正如你在第0节的例子所看到的,运算符表达式实现上,是一个成员函数。换句话说,运算符表达式其实是一个语法糖,如果你重载了某个运算符(实现其对应的成员函数),你就可以使用这个运算符的特定表达式(语法糖)来调用重载的函数。
在Python里,万物皆对象。那怕一个整数常量10,它也是一个整数类型的对象。当我们对10进行加法运算的时候,事实上就是调用了10的加法运算函数。如下的例子就很好地证实了我们本节的结论:
# 定义两个整数,事实上a和b都是整形对象
a = 10
b = 8
# 由于整型重载了加法运算符,即重载了函数__add__
# 在运行阶段, a + b 会被转成a.__add__(b)的形式运行
# 所以如下的两行代码是等价的
c = a + b
c = a.__add__(b)
# 你甚至可以直接调用数值的__add__函数
# 以下两行代码是等价的
# 思考题:(10)的括号不可去掉,为什么?
c = (10).__add__(8)
c = 10 + 8
2、运算符重载需要注意些什么
在正常的加法运算中,表达式会返回加法运算的结果,而加号+两边的对象并不会被修改,我们称这个为加法运算的惯例。每个运行符都有自己的惯例。但同时,你也可以不遵循这些惯例。
我们先来看一个例子:
class A:
def __init__(self, v):
self.v = v
def __add__(self, v):
self.v -= v
a = A(10)
# 以下的代码执行之后,可能并不会得到你想要的结果
# a.v的值为2,而c则为None
c = a + 8
在上面的例子中,a的值被修改了;运算符并没做加法运算,而是做了减法;同时,由于__add__并没有返回任何东西(没有返回东西,等同于返回None),所以c的值为None。我们看到, 这个例子就没有遵循加法运算符的惯例,但Python并没有阻止你这么做,正如第0节所介绍的,你可以在语法范围内对运算符进行重新定义,为所欲为。
虽然你可以这么做,但在实际开过程中,你应该尽量地遵循运算符的惯例。例如加法运算符最好是做“加法”逻辑的运算(如数值相加,字符串串连,集合合并等);运算符函数应该返回加法的结果;如果可能的话,尽量不要修改加号+左右两边的对象的值。
事实上,如果你要把加法运算的结果保存到a,即符号左边的变量,你可以用到如下的语法:
a += b
也就是说,你可以重载+=运算符,即实现__radd__函数来达到目的。
我们来看完整的例子:
class Yummy:
def __init__(self, v):
self.v = v
def __add__(self, rhs):
obj = Yummy(self.v)
obj.v += rhs.v
return obj
def __radd__(self, rhs):
# 修改自己
self.v += rhs.v
# 思考题:
# 如果不返回self,会有什么副作用?为什么?
return self
a = Yummy(10)
b = Yummy(8)
# 调用 __add__
c = a + b
a = c
# 调用 __radd__
a += c
在这个例子中,+(__add__)和+=(__radd__)的重载都遵循了惯例。
遵循惯例的好处是:在没有什么声明的情况情况下,代码的行为总是和它看起来的样子基本一致。
特别在大型工程中,特别是团队协作中,“惯例”显得尤为重要。如果一个代码的行为,总是如它所被期望的样子,我们的生活就会更美好,不是么!