Python——运算符重载(2)

44 篇文章 0 订阅
44 篇文章 21 订阅

继续介绍运算符重载的知识。

运算符重载(1)回顾这里

========================================================================

属性引用:__getattr__和__setattr__

__getattr__方法是拦截属性点号运算。确切地说,当通过对【未定义(即不存在)】的属性名称和实例进行点号运算时,就会用属性名称作为字符串调用这个方法。如果Python可通过其继承树搜索流程找到这个属性,该方法就不会被调用。因为有这种情况,所以__getattr__可以作为钩子来通过通用的方式响应属性请求。如下例:

>>> class empty:
	def __getattr__(self,attrname):
		if attrname == 'age':
			return 40
		else:
			raise AttributeError(attrname)

		
>>> X = empty()
>>> X.age
40
>>> X.name
Traceback (most recent call last):
  File "<pyshell#11>", line 1, in <module>
    X.name
  File "<pyshell#8>", line 6, in __getattr__
    raise AttributeError(attrname)
AttributeError: name
在这里,empty类和其实例X本身没有属性,所以对X.age的存取会转至__getattr__方法,self则赋值为实例(X),而attrname则赋值为未定义的属性名称字符串('age')。这个类传回一个实际值作为X.age点号表达式的结果(40),让age看起来像实际的属性。

而对于请求X.name属性,程序引发了异常。

与此相关的重载方法__setattr__会拦截所有属性的赋值语句。如果定义了这个方法,self.attr = value 会变成self.__setattr__('attr',value)。这一点技巧性很高,因为在__setattr__中对任何self属性做赋值,都会再调用__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')

		
>>> X = accesscontrol()
>>> X.age = 40
>>> X.age
40
>>> X.name = 'Gavin'
Traceback (most recent call last):
  File "<pyshell#28>", line 1, in <module>
    X.name = 'Gavin'
  File "<pyshell#24>", line 6, in __setattr__
    raise AttributeError(attr + ' not allowed')
AttributeError: name not allowed
------------------------------------------------------------------------------------ -----------------------------

模拟实例属性的私有化:第一部分

下例程序把上一个例子通用化了,让每个子类都有自己的私有变量名列表,这些变量名无法通过其实例进行赋值:

class PrivateExc(Exception):
    pass

class Privacy:
    def __setattr__(self,attrname,value):
        if attrname in self.privates:
            raise PrivateExc(attrname,self)
        else:
            self.__dict__[attrname] = value

class Test1(Privacy):
    privates = ['age']

class Test2(Privacy):
    privates = ['name','pay']
    def __init__(self):
        self.__dict__['name'] = 'Tom'

if __name__ == '__main__':
    x = Test1()
    y = Test2()

    x.name = 'Bob'
    y.name = 'Sue'      #这句话会报异常,因为name属性是Test2的私有变量

    y.age = 30
    x.age = 40  	#同理,这句话会报异常
实际上,这是Python实现【属性私有性】(也就是无法在类外对属性名进行修改)的首选方法。

不过,属性私有性的更完整的的解决方案将会在以后讲解,之后将会使用【类装饰器】来更加通用地拦截和验证属性。
========================================================================
__repr__和__str__会返回字符串表达形式

下例是已经见过的__init__构造函数和__add__重载方法:

>>> class adder:
	def __init__(self,value = 0):
		self.data = value
	def __add__(self,other):
		self.data += other

	
>>> x = adder()
>>> print(x)
<__main__.adder object at 0x0330DE10>
>>> x
<__main__.adder object at 0x0330DE10>
实例对象的默认显示既无用也不好看。但是,编写或继承字符串表示方法允许我们定制显示:

>>> class addrepr(adder):
	def __repr__(self):
		return 'addrepr(%s)'%self.data

	
>>> x = addrepr(2)
>>> x+1
>>> x
addrepr(3)
>>> print(x)
addrepr(3)
>>> str(x),repr(x)
('addrepr(3)', 'addrepr(3)')
那么,为什么要有两个显示方法呢?概括地讲,是为了进行用户友好地显示。具体来说:

1.打印操作会首先尝试__str__和str内置函数(print运行的内部等价形式),它通常应该返回一个用户友好的显示。
2.__repr__用于所有其他环境中:用于交互模式下提示回应以及repr函数。

总而言之,__repr__用于任何地方,除了当定义一个__str__的时候,使用print和str。然而要注意,如果没有定义__str__,打印还是使用__repr__,但反过来并不成立——例如,交互式相应模式,只是使用__repr__,并且根本不要尝试__str__:

>>> class addstr(adder):
	def __str__(self):
		return '[value:%s]'%self.data

	
>>> x = addstr(3)
>>> x+1
>>> x
<__main__.addstr object at 0x0330DE10>
>>> print(x)
[value:4]
>>> str(x),repr(x)
('[value:4]', '<__main__.addstr object at 0x0330DE10>')
正是由于这一点,如果想让所有环境都有统一的显示,__repr__是最佳选择。不过,通过分别定义这两个方法,就可以在不同的环境内支持不同显示。

但是要记得:__str__和__repr__都必须返回字符串,其他的类型会出错。
========================================================================

右侧加法和原处加法:__radd__和__iadd__

前面例子中出现的__add__方法不支持+运算符右侧使用实例对象。要实现这类表达式,而支持可互换的运算符,可以一并编写__radd__方法。只有当+右侧是类实例,而左边不是类实例的时候,Python才会调用__radd__。在其他情况下,则由左侧对象调用__add__方法。

>>> class Commuter:
	def __init__(self,val):
		self.val = val
	def __add__(self,other):
		print('add',self.val,other)
		return self.val + other
	def __radd__(self,other):
		print('radd',self.val,other)
		return other + self.val

	
>>> 
>>> x = Commuter(88)
>>> y = Commuter(99)
>>> x +1
add 88 1
89
>>> 1+y
radd 99 1
100
>>> x+y
add 88 <__main__.Commuter object at 0x0330DD50>
radd 99 88
187
当不同类的实例混合出现在表达式时,Python优先选择左侧的那个类。当我们把两个实例相加的时候,Python运行__add__,它反过来通过简化左边的运算数来触发__radd__。

在实际的应用中,类型可能需要在结果中传播,类型测试可能需要辨别它是否能够安全地转换并由此避免嵌套。例如,下面的代码如果没有isinstance测试,当两个实例相加并且__add__触发__radd__的时候,我们最终得到一个Commuter,其val是另一个Commuter:
>>> class Commuter:
	def __init__(self,val):
		self.val = val
	def __add__(self,other):
		if isinstance(other,Commuter):
			other = other.val
		return Commuter(self.val + other)
	def __radd__(self,other):
		return Commuter(other+self.val)
	def __str__(self):
		return '<Commuter:%s>'%self.val

	
>>> x = Commuter(88)
>>> y = Commuter(99)
>>> print(x+10)
<Commuter:98>
>>> print(10+y)
<Commuter:109>
>>> z = x+y
>>> z
<__main__.Commuter object at 0x037264D0>
>>> print(z)
<Commuter:187>
>>> print(z+z)
<Commuter:374>
------------------------------------------------------------------------------------ -----------------------------
原处加法

编写一个__iadd__方法可以实现+=原处扩展相加。当然,__add__也可以完成类似的功能,如果没有__iadd__的时候,Python会调用__add__:

>>> class Number:
	def __init__(self,val):
		self.val = val
	def __add__(self,other):
		self.val += other
		return self

	
>>> x = Number(5)
>>> x+=1
>>> x.val
6
>>> class Number:
	def __init__(self,val):
		self.val = val
	def __iadd__(self,other):
		self.val += other
		return self

	
>>> x = Number(5)
>>> x+=1
>>> x.val
6
每个二元运算都有类似的右侧和原处重载方法,它们以相同的方式工作,例如,__mul__,__rmul__,__imul__。
========================================================================

Call表达式:__call__

当调用实例时,使用__call__方法。如果定义了,Python就会为实例应用函数调用表达式运行__call__方法。这样可以让类实例的外观和用法类似于函数。

>>> class Callee:
	def __call__(self,*pargs,**kargs):
		print('Called:',pargs,kargs)

		
>>> C = Callee()
>>> C(1,2,3)
Called: (1, 2, 3) {}
>>> C(1,2,3,x=4,y=5)
Called: (1, 2, 3) {'x': 4, 'y': 5}
确切地说,之间介绍的【参数】传递方式,__call__方法都支持。

当需要为函数的API编写接口时,__call__就变得很有用:这可以编写遵循所需要的函数来调用接口对象,同时又能保留状态信息。事实上,__call__方法是除了__init__构造函数以及__str__和__repr__显示格式方法外,第三个最常用的运算符重载方法了。

这个方法最常用于Tkinter GUI工具箱中,可以把函数注册成事件处理器(也就是回调函数callback),但这个知识点在这里不做过多讲解。
========================================================================

比较:__lt__、__gt__和其他方法

类可以定义方法来捕获所有的6种比较运算符:<、>、<=、>=、==和!=。限制如下:

1.与前面讨论的__add__/__radd__对不同,比较方法没有右端形式。相反,当只有一个运算数支持比较的时候,使用其对应方法(例如,__lt__与__gt__互为对应)。

2.比较运算符没有隐式关系。例如,==并不意味着!=是假的,因此,__eq__和__ne__应该定义为确保两个运算符都正确地使用

注意:Python2.6中与此等效的__cmp__方法在Python3中已经移除!

看如下一个示例:

>>> class C:
	data = 'spam'
	def __gt__(self,other):
		return self.data>other
	def __lt__(self,other):
		return self.data<other

	
>>> X = C()
>>> print(X>'ham')
True
>>> print(X<'ham')
False
>>> print('ham'<X)
True
========================================================================

布尔测试:__bool__和__len__

在布尔环境中,Python首先尝试__bool__来获取一个直接的布尔值,然后,如果没有该方法,就尝试__len__类根据对象的长度确定一个真值。

>>> class Truth:
	def __bool__(self):
		return True

	
>>> X = Truth()
>>> if X:
	print('yes')

	
yes
>>> class Truth:
	def __bool__(self):
		return False

	
>>> X = Truth()
>>> bool(X)
False
如果没有这个方法,Python会退而求其次求其长度,因为一个非空对象看做是真:

>>> class Truth():
	def __len__(self):
		return 0

	
>>> X = Truth()
>>> if not X:
	print('no')

	
no
如果两个方法都有,Python会优先调用__bool__方法。

最后,如果没有定义真的方法,对象毫无疑义地看做为真:
>>> class Truth:
	pass

>>> X = Truth()
>>> bool(X)
True
========================================================================
对象析构函数:__del__

每当实例产生时,就会调用__init__构造函数。每当实例空间被回收时(在垃圾收集时),它的对立面__del__,也就是析构函数,就会自动执行:

>>> class Life:
	def __init__(self,name = 'unknown'):
		print('Hello',name)
		self.name = name
	def __del__(self):
		print('GoodBye',self.name)

		
>>> brian = Life('Brian')
Hello Brian
>>> brian = 'gavin'
GoodBye Brian
在这里,当Brian赋值为字符串时,我们就会失去Life实例的最后一个引用,因此会触发其析构函数。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值