基础知识
运算符重载:意味着你在自己定义的类方法中拦截内置的操作,当类的实例出现在内置操作中,Python自动调用你的方法,并返回相应自定义操作的结果。
运算符重载一般应用于模拟内置对象,使得自己定义的对象更像是底层对象。
常用运算符重载方法
方法 | 重载 | 调用 |
---|---|---|
__init__ | 构造函数 | 对象建立:X=Class(args) |
__del__ | 析构函数 | X对象回收,del [object] |
__add__ | 运算符+ | 如果没有定义__iadd__,在x+y,x+=y时调用 |
__or__ | 或运算符| | 如果没有定义__ior__,X|Y,X|=Y时调用 |
__repr__,__str__ | 打印,转换 | print(X),repr(X),str(X) |
__call__ | 函数调用 | X(*arg,**kargs) |
__getattr__ | 点号运算符 | X.undefined |
__setattr__ | 属性赋值语句 | X.[atrribute]=value |
__delattr__ | 属性删除 | del X.[atrribute] |
__getattribute__ | 属性获取 | X.[atrribute] |
__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(X),真值测试(在python2.6中为__nonzero__) |
__lt__,__gt__,__le__,__ge__,__eq__,__ne__ | 比较测试 | X < Y,X>Y,X<=Y,X>=Y,X==Y,X!=Y |
__radd__ | 右侧加法 | other+X |
__iadd__ | 增强加法 | X+=Y |
__iter__,__next__ | 迭代环境 | 在迭代环境中使用 |
__contains__ | 成员关系测试 | item in X |
__index__ | 整数值 | hex(X),bin(X),oct(X),O(X) |
__enter__,__exit__ | 环境管理器 | with obj as var: |
__get__,__set__,__delete__ | 描述符属性 | X.attr,X.attr=value,del X.attr |
__new__ | 创建 | 在__init__之前创建对象 |
所有重载方法的名称前后都由两个下划线组成以区分其它方法。一些自定义的类没有重载方法时,可能不支持相关运算符的操作,也可能是继承了Python内的内置类
索引和分片:__getitem__和__setitem__
__getitem__是拦截索引和分片相关的函数:
>>> class Indexer:
... def __init__(self):
... self.data=[1,2,3,4,5,6]
... def __getitem__(self,index):
... return self.data[index]
...
...
>>> lxm=Indexer()
>>> lxm[4]
5
>>> for i in range(5):
... print(lxm[i])
...
1
2
3
4
5
>>> lxm[1:4]
[2, 3, 4]
>>> lxm[2:4]
[3, 4]
>>>
而__setitem__方法是拦截索引赋值和分片赋值的方法:
>>> class Indexer:
... def __init__(self):
... self.data=[1,2,3,4,5,6]
... def __getitem__(self,index):
... return self.data[index]
... def __setitem__(self,index,value):
... print("setitem used")
... print(index)
... print(value)
... self.data[index] = [i*value for i in self.data[index] ]
...
>>> lxm=Indexer()
>>> lxm[3:5]
[4, 5]
>>> lxm[3:5]=4 #调用相应的setitem方法来修改self.data中的数据。
setitem used
slice(3, 5, None)
4
>>> lxm[3:5]
[16, 20]
>>>
在迭代环境for,成员关系测试in,列表解析,内置函数map,列表和元组赋值运算以及类型构造方法也会自动调用__getitem__方法。
尽管__getitem__方法可以实现迭代,但是在Python中,所有迭代环境都会最先尝试__iter__方法,再尝试__getitem__方法。
迭代器对象:__iter__和__next__
之前介绍过迭代器,用户可以通过自定义类中的方法来实现一个全新的迭代器。
>>> class iter:
... def __init__(self,start,end):
... self.start=start-1
... self.end=end
... def __iter__(self):
... print("call __iter__ function")
... return self
... def __next__(self):
... print("call __next__ function")
... if self.end == self.start:
... raise StopIteration
... self.start += 1
... return self.start**3
...
>>> lxm=iter(4,10)
>>> for i in lxm:
... print(i)
...
call __iter__ function #在for循环开始时,会调用iter方法
call __next__ function #每一次迭代都会调用对象的next方法
64
call __next__ function
125
call __next__ function
216
call __next__ function
343
call __next__ function
512
call __next__ function
729
call __next__ function
1000
call __next__ function #在next方法中,抛出了StopIteration异常
>>>
如果这时,继续对迭代器进行迭代:
>>> for i in lxm:
... print(i)
...
call __iter__ function #对象中的迭代器还是使用的上次迭代完成后的对象
call __next__ function
>>> lxm.start #属性并没有重新初始化
10
>>>
要达到可以使用多个迭代器的对象,那么就要重新编写__iter__方法。
>>> class iter:
... def __init__(self,start,end):
... self.start=start
... self.value=start-1
... self.end=end
... def __iter__(self):
... print("call __iter__ function")
... return iter(self.start,self.end) #每次迭代开始时,都会返回一个新的迭代器
... def __next__(self):
... print("call __next__ function")
... if self.end == self.value:
... raise StopIteration
... self.value += 1
... return self.value**3
...
>>> lxm=iter(4,6)
>>> for i in lxm:
... print(i)
...
call __iter__ function
call __next__ function
64
call __next__ function
125
call __next__ function
216
call __next__ function
>>> for i in lxm: #可以开始多次迭代
... print(i)
...
call __iter__ function
call __next__ function
64
call __next__ function
125
call __next__ function
216
call __next__ function
>>>
成员关系:__contain__,__iter__和__getitem__
成员关系判断中都都会使用in语句,实际上in语句也会间接的运用迭代协议,接着上面的例子看到,变量lxm中有64,125,216三个整数,那么对变量lxm进行成员关系判断时可以看到:
>>> 111 in lxm #间接的都调用了迭代环境中的函数。
call __iter__ function
call __next__ function
call __next__ function
call __next__ function
call __next__ function
False
>>> 64 in lxm
call __iter__ function
call __next__ function
True
>>>
有时候运算符的重载往往是多个层级的,类可以提供特定的方法来重载运算符,或者可以退而求其次的选项来选择更通用的运算符重载方法:
- Python2.6中的比较使用__lt__等这样特殊的比较方法(如果有的话)或者使用通用的__cmp__。
- 布尔测试会先找__bool__方法,如果没有就会找__len__方法
- 成员关系测试中,Python最先找的方法是__contains__方法,即__contains__方法最先拦击关系测试,如果没有该方法,才会进行__iter__方法,最后才是__getitem方法,可以看出__getitem__方法更为通用。
属性引用:__getattr__和__setattr__
__getattr__方法是拦截属性点号运算。更确切地说,当通过对未定义(不存在)属性名称和实例进行点号运算时,就会用属性名称作为字符串调用该方法。通常用作响应一些不存在的属性调用时,使用:
>>> class empty:
... def __getattr__(self,attrname):
... print(self,attrname)
... return "No this attrname"
...
>>> lxm=empty()
>>> lxm.aaa # 对不存在的属性执行点运算,会直接调用getattr
<__main__.empty object at 0x7f482be85128> aaa
'No this attrname'
>>> lxm.aaa=123 #定义了该属性后,调用就不会去调用getattr属性
>>> lxm.aaa
123
>>>
__setattr__会拦截所有属性的赋值语句,如果定义了这个方法,赋值语句self.attr=value,就会变成self.__setattr__(‘attr’,value):
注意到任何对该类的属性赋值都会调用_setattr_方法,所以在该方法内部对实例赋值时,就不能使用通常的赋值语句self.attr=value,否则会让函数产生循环递归,最后导致堆栈溢出,而要使用对属性字典做索引运算来赋值任何实例的属性。(self._dict_[‘name’]=X)
>>> class setattribute:
... def __setattr__(self,attr,value):
... print(attr,value)
... if attr == 'age' and value >100:
... self.__dict__['age']="Old man"
... else:
... self.__dict__[attr]=value
...
>>> lxm=setattribute()
>>> lxm.name='liximin' #使用赋值语句时,会调用setattr函数
name liximin
>>> lxm.name
'liximin'
>>> lxm.age=200
age 200
>>> lxm.age
'Old man'
>>>
其它属性管理工具
- __getattribute__方法可以拦截所有属性点运算的获取,而不仅仅是未定义的属性。使用时更要注意避免循环递归。
- Property内置函数允许我们把方法和特定类属性上的获取和设置操作关联起来。
- 描述符提供了一个协议,把一个类的__get__和__set__方法对特定类的属性访问联系起来。
这些方法与工具并不是我们开发python应用程序常用的,都是在开发python工具或者偏底层的程序工具时有广泛使用。
__repr__和__str__会返回字符串表达形式
这两个函数,在打印对象或者将对象转化成为字符串时,都会调用。而这两个方法的不同就在于,终端用户使用__str__方法而程序员在开发期间则使用底层的__repr__来显示。实际上,__str__只是覆盖了__repr__以得到用户友好的显示环境。
>>> class printclass:
... def __str__(self):
... print('str function called')
... return "Hello str function"
... def __repr__(self):
... print('repr function called')
... return "Hello Python repr"
...
>>> lxm=printclass()
>>> lxm
repr function called
Hello Python repr
>>> print(lxm) #调用str函数
str function called
Hello str function
>>> class printclass2:
... #def __str__(self):
... # print('str function called')
... # return "Hello str function"
... def __repr__(self):
... print('repr function called')
... return "Hello Python repr"
...
>>> lxm2=printclass2()
>>> lxm2 #交互模式下直接调用repr函数
repr function called
Hello Python repr
>>> print(lxm2) #print函数在没有定义str的情况下,会去调用repr函数
repr function called
Hello Python repr
>>>
需要注意的是,str和repr函数都必须返回字符串,如果返回其它类型的数据并不会自动转化,会产生错误。在实际应用中除了__init__函数,就是__str__函数用得最多了。
右侧加法和原处加法:__radd__和__iadd__
从技术上讲,如果真要实现加法,交换加数位置,结果不变,那么再实现__add__方法的同时也要实现右侧加法__radd__。当类实例在加号右侧会调用radd函数,而在加号左侧就会调用add方法。在原处加法运算符x+=1则由方法__iadd__拦截。
>>> class sumclass:
... def __init__(self,value):
... self.value=value
... def __add__(self,value):
... print('add function called',self.value,value)
... return self.value+value
... def __radd__(self,value):
... print('radd function called',self.value,value)
... return self.value+value
...
>>> lxm1=sumclass(12)
>>> lxm2=sumclass(44)
>>> lxm1+3
add function called 12 3
15
>>> lxm2+99
add function called 44 99
143
>>> 55+lxm1
radd function called 12 55
67
>>> lxm1+lxm2
add function called 12 <__main__.sumclass object at 0x7f482be85550> #首先调用add函数,add函数在做加法时
radd function called 44 12 #右侧是实例对象,那么就会调用radd函数,最后返回结果
56
>>>
Call表达式:__call__
当调用一个实例时,会使用__call__方法。(当创建一个实例时,会调用__init__方法),这让实例的使用方法从编码外观上来看就像调用了一个函数。
>>> class Callclass:
... def __call__(self): #这里的call函数比较简单,并没有传递任何参数,只是简单的类似于函数的调用。
... print('call function called')
...
>>> lxm=Callclass()
>>> lxm()
call function called
>>>
对象析构函数:__del__
当实例的内存空间被回收时,它会执行析构函数,由于以下原因,析构函数在Python编程中很少使用:
- Python在执行回收时由于是自动回收内存空间,所以对于回收空间来说,是不需要析构函数的。
- 无法准确的预测何时执行析构函数,当前对象可能在其他地方使用,而导致不会执行析构函数。
小结
本章主要介绍了类中运算符重载的一些方法,这些方法让类的编写有更高的灵活性。很多工具都会使用这些重载函数,理解这些内容,对工具的使用有重要意义。在定义的类自然的映射到运算符操作时,可以使用重载,来提高代码的可读性和灵活性,如果没有自然的逻辑映射关系,那么就用普通的类方法来进行处理。