基础知识
“运算符重载”只是意味着在某个类的方法中拦截内置的操作——当类的实例出现内置操作中时,python会自动调用你的方法,并且你的方法的返回值会作为相应操作的结果。
- 运算符重载让类拦截常规的python操作。
- 类可重载所有的python表达式运算符。
- 类也可重载打印、函数调用、属性访问等内置运算。
- 重载使类实例的行为更接近内置类型。
- 重载是通过在一个类中提供特殊名称的方法来实现的。
一、构造函数和加减表达式:__init__和__add__和__sub__
class Number:
def __init__(self, start):
self.data = start
def __add__(self, other):
return Number(self.data + other)
def __sub__(self, other):
return Number(self.data - other)
if __name__ == "__main__":
X = Number(5)
print(X.data)
Y = X + 1
print(Y.data)
Z = X - 1
print(Z.data)
Numbr生成的调用现在会传入一个参数,这是传给__init__构造函数内的参数value的,并在构造函数中被赋值给self.data。此外,Number对象现在可以出现在 + 表达式和 - 表达式中。对于 +、-,Python把左侧的实例对象传给__add__中的self参数,而把右侧的对象传给other。__add__的返回值则成为 + 和 - 表达式的结果。
注意:从技术的角度讲,在一个实例被创建的过程中,首先触发的是__new__方法,这一方法将创建并返回一个新的实例对象,并传入__init__函数以供初始化。
二、索引和分片:__getitem__和__setitem__
- 如果一个类定义(或继承)了__getitem__的话,该方法就会在实例进行索引运算和分片运算的时候被调用。
class Indexer:
data = [5, 6, 7, 8, 9]
def __getitem__(self, index):
print("getitem:", index)
return self.data[index]
if __name__ == "__main__":
X = Indexer()
print("索引:")
X[0]
X[1]
X[-1]
print("分片:")
print(X[2:4])
print(X[1:])
print(X[:-1])
print(X[::2])
索引:
getitem: 0
getitem: 1
getitem: -1
分片:
getitem: slice(2, 4, None)
[7, 8]
getitem: slice(1, None, None)
[6, 7, 8, 9]
getitem: slice(None, -1, None)
[5, 6, 7, 8]
getitem: slice(None, None, 2)
[5, 7, 9]
当实例X出现在X[i]这样的索引运算中时,Python会调用这个实例继承的__getitem__方法,把X作为第一位参数传入,并且将方括号内的索引值传给第二个参数。
当实例X出现在X[i,j]这样的分片运算中时,Python会调用这个实例继承的__getitem__方法,把X作为第一位参数传入,第二个参数接收一个分片对象,如:slice(2, 4, None),它在一个新的索引表达式中直接传递给内嵌的列表索引。
>>> L = [5,6,7,8,9]
>>> L[2:4]
[7, 8]
>>> L[slice(2,4)]
[7, 8]
上面的代码可以看出,分片语法只不过就是分片对象来索引的语法糖。
- 如果使用了__setitem__索引赋值方法的话,它能拦截索引赋值和分片赋值。
class IndexSetter:
data = [1, 2, 3, 4, 5, 6]
def __setitem__(self, key, value):
print("setitem:", key, value)
self.data[key] = value
if __name__ == "__main__":
X = IndexSetter()
print("索引赋值:")
X[0] = "a"
X[1] = "b"
print(X.data)
print("分片赋值:")
X[2:4] = ["c", "d"]
print(X.data)
索引赋值:
setitem: 0 a
setitem: 1 b
['a', 'b', 3, 4, 5, 6]
分片赋值:
setitem: slice(2, 4, None) ['c', 'd']
['a', 'b', 'c', 'd', 5, 6]
三、可迭代对象:__iter__和__next__
在python中,迭代是迭代上下文先将一个可迭代对象传入内置函数iter,并尝试调用__iter__方法来实现的,而这种方法应该返回一个迭代器对象。如果提供了这种方法,在使用迭代工具for时,Python就会重复调用这个迭代器对象的__next__方法来产生元素,直到引发StopIteation异常。对于手动迭代来说,可以使用一个next内置函数:next(x)与I.next()相同。我们可以自定义一个对象来模拟这个情景。
例如:用一个类来定义用户自定义可迭代对象,从按需生成所需要的值,而不是一次生成所有的值。
class Squares:
def __init__(self, start, stop):
self.value = start - 1
self.stop = stop
def __iter__(self):
print("__iter__")
return self
def __next__(self):
print("__next__")
if self.value == self.stop:
raise StopIteration
self.value += 1
return self.value
if __name__ == "__main__":
s = Squares(1, 5)
x = iter(s)
print("-------")
print(next(x))
print(next(x))
print(next(x))
print(next(x))
print(next(x))
__iter__
-------
__next__
1
__next__
2
__next__
3
__next__
4
__next__
5
我们注意到一个类的__iter__被设计为只调用一次,而不是多次调用。由于当前的Squares类的iter通常返回只带有一份迭代状态复制的self,因此这是一个一次性的迭代;一旦你迭代过这个类中的一个实例,它就变空。对该实例再次调用__iter__则还是会返回self,以及所剩下的最终状态,通常许需要为每一次新的迭代创建一个新的可迭代对象。
四、属性访问:__gettattr__和__setattr__
在python中,类在需要的时候也可以拦截基本的属性访问(也称为点号运算)。具体来说,对于一个类中创建的object,点号运算表达式object.attribute也可以用你的代码。来实现,用于属性引用、赋值操作或删除操作。
- 属性引用
__getattr__方法用来拦截属性引用。每当你用一个未定义的(不存在的)属性名称字符串对一个实例对象做点号运算时,它就会被调用。如果python能通过其继承搜索过程找到这个属性,那么该方法就不会被调用。
例如:下面这个类会捕获属性引用,为其中一个属性动态的计算值,并为其他不被支持的属性引发错误。
class Empty:
def __getattr__(self, attrname):
if attrname == "age":
return 40
else:
raise AttributeError(attrname)
if __name__ == "__main__":
x = Empty()
print(x.age)
print(x.name)
% getattrfunc.py
40
AttributeError: name
在这里,empty类和实例x本身并没有属性,所以对x.age属性的访问会被路由到__getattr__方法。其中self被赋值为实例(x),而attrname则被赋值为未定义的属性名称字符串('age')。这个类传回一个实际值作为x.age点号表达式的结果(40),这让age看上去就像真实的属性。对于类不知道该如何处理的属性,__getattr__会引发内置的AttributeErrot异常。
- 属性赋值和删除
__setattr__会拦截所有的属性赋值。self.attr=value会变成self.__setattr__('attr',value)。不过在使用的时候需要一点技巧,因为在__setattr__中队任何self属性做赋值,都将再次掉用__setattr__,这就潜在导致了无穷递归循环。在类中所有赋值都会被路由到__setattr__。可以把实例属性的赋值编写成对属性字典键的赋值来避免循环。也就是说,是使用self.__dict__['name'] = x,而不是self.name = x。
class Accesscontrol:
def __setattr__(self, attr, value):
if attr == "age":
self.__dict__[attr] = value
else:
raise AttributeError(attr + "not allowed")
if __name__ == "__main__":
x = Accesscontrol()
x.age = 40
print(x.age)
x.name = 'Bob'
五、调用表达式:__call__
带有一个__call__的类和实例能够支持与常规函数和方法完全相同的参数语法和语义。换一种说法,__call__方法支持所有的函数中参数传递模式——传递给实例的所有信息都会传递给该方法,包括通常隐含的self实例参数。
class Callee:
def __call__(self, *pargs, **kwargs):
print("Called:", pargs, kwargs)
if __name__ == "__main__":
c = Callee()
c(1, 2, 3)
c(1, 2, 3, x=4, y=5)
% python callfunc.py
Called: (1, 2, 3) {}
Called: (1, 2, 3) {'x': 4, 'y': 5}
一些知识点:当一个类实例化时(创建一个对象)执行__call__方法,__call__方法的返回值就是实例化的对象。__call__内部调用(1)__new__方法,创建一个对象;(2)__init__方法,初始化对象。
__new__至少要有一个参数cls,代表要实例化的类,此参数在实例化时由Python解释器自动提供
__new__必须要有返回值,返回实例化出来的实例,这点在自己实现__new__时要特别注意,可以return父类__new__出来的实例,或者直接是object的__new__出来的实例
__init__有一个参数self,就是这个__new__返回的实例,__init__在__new__的基础上可以完成一些其它初始化的动作,__init__不需要返回值
六、对象析构函数:__del__
每当实例产生时调用__init__构造函数(注意__new__先于__运行并创建出对象)。反过来,当实例空间被收回时(也称作“垃圾收集”),它的对立面__del__,也就是析构函数方法,就会自动执行。
class Life:
def __init__(self, name='unknown'):
print("Hello" + name)
self.name = name
def live(self):
print(self.name)
def __del__(self):
print("Goodbly", self.name)
if __name__ == "__main__":
brian = Life('Brian')
brian.live()
brian = "loretta"
% python lifefunc.py
HelloBrian
Brian
Goodbly Brian
在这里,当brian被赋值为一个字符串的时,我们会失去Life实实例的最后一个引用并因此触发其析构函数。
网友评论