黑魔法
我们既蒙怜悯,受了这职分,就不丧胆,乃将那些暗昧可耻的事弃绝了,不行诡诈,不谬讲神的道理,只将真理表明出来,好在神面前把自己荐与各人的良心。(2 CORINTHIANS 4:1-2)
黑魔法
围绕类的话题,真实说也说不完,仅特殊方法,除了前面遇到过的__init__()
,__new__()
,__str__()
等之外,还有很多。虽然它们仅仅是在某些特殊情景中使用,但是,因为本教程是“From Beginner to Master”。当然,不是学习了类的更多特殊方法就能达到Master水平,但是这是通往Master的一步。
本节试图再介绍一些点“黑魔法”,既能窥探到Python的更高境界,也能感受到Master的未来能力。俗话说“艺不压身”,还是多学点好。
优化内存的__slots__
首先声明,__slots__
能够限制属性的定义,但是这不是它存在终极目标,它存在的终极目标更应该是一个在编程中非常重要的方面:优化内存使用。
>>> class Spring(object):... __slots__ = ("tree", "flower")... >>> dir(Spring)['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'flower', 'tree']
仔细看看dir()
的结果,还有__dict__
属性吗?没有了,的确没有了。也就是说__slots__
把__dict__
挤出去了,它进入了类的属性。
>>> Spring.__slots__('tree', 'flower')
这里可以看出,类Spring有且仅有两个属性。
>>> t = Spring()>>> t.__slots__('tree', 'flower')
实例化之后,实例的__slots__
与类的完全一样,这跟前面的__dict__
大不一样了。
>>> Spring.tree = "liushu"
通过类,先赋予一个属性值。然后,检验一下实例能否修改这个属性:
>>> t.tree = "guangyulan"Traceback (most recent call last): File "<stdin>", line 1, in <module>AttributeError: 'Spring' object attribute 'tree' is read-only
看来,我们的意图不能达成,报错信息中显示,tree
这个属性是只读的,不能修改了。
>>> t.tree'liushu'
因为前面已经通过类给这个属性赋值了。不能用实例属性来修改。只能:
>>> Spring.tree = "guangyulan">>> t.tree'guangyulan'
用类属性修改。但是对于没有用类属性赋值的,可以通过实例属性赋值。
>>> t.flower = "haitanghua">>> t.flower'haitanghua'
但此时:
>>> Spring.flower<member 'flower' of 'Spring' objects>
实例属性的值并没有传回到类属性,你也可以理解为新建立了一个同名的实例属性。如果再给类属性赋值,那么就会这样了:
>>> Spring.flower = "ziteng">>> t.flower'ziteng'
当然,此时在给t.flower
重新赋值,就会爆出跟前面一样的错误了。
>>> t.water = "green"Traceback (most recent call last): File "<stdin>", line 1, in <module>AttributeError: 'Spring' object has no attribute 'water'
这里试图给实例新增一个属性,也失败了。
看来__slots__
已经把实例属性牢牢地管控了起来,但更本质是的是优化了内存。诚然,这种优化会在大量的实例时候显出效果。
书接上回,不管是实例还是类,都用__dict__
来存储属性和方法,可以笼统地把属性和方法称为成员或者特性,一句话概括,就是__dict__
存储对象成员。但,有时候访问的对象成员没有存在其中,就是这样:
>>> class A(object):... pass... >>> a = A()>>> a.xTraceback (most recent call last): File "<stdin>", line 1, in <module>AttributeError: 'A' object has no attribute 'x'
x
不是实例的成员,用a.x
访问,就出错了,并且错误提示中报告了原因:“'A' object has no attribute 'x'”
在很多情况下,这种报错是足够的了。但是,在某种我现在还说不出的情况下,你或许不希望这样报错,或许希望能够有某种别的提示、操作等。也就是我们更希望能在成员不存在的时候有所作为,不是等着报错。
要处理类似的问题,就要用到本节中的知识了。
属性拦截
有时候,访问某个类或者实例属性,它不存在,就会异常。对于异常,总是要处理的。就好像“寻隐者不遇”,却被童子“遥指杏花村”,将你“拦截”了,不至于因为“不遇”而垂头丧气。
在Python中,有一些方法就具有这种“拦截”能力。
__setattr__(self, name,value)
:如果要给name赋值,就调用这个方法。__getattr__(self, name)
:如果name被访问,同时它不存在的时候,此方法被调用。__getattribute__(self, name)
:当name被访问时自动被调用(注意:这个仅能用于新式类),无论name是否存在,都要被调用。__delattr__(self, name)
:如果要删除name,这个方法就被调用。
用例子说明。
>>> class A(object): #Python 3: class A:... def __getattr__(self, name):... print "You use getattr" #Python 3: print("You use getattr"),下同,从略... def __setattr__(self, name, value):... print "You use setattr"... self.__dict__[name] = value...
类A
除了两个方法,没有别的了。
>>> a = A()>>> a.xYou use getattr
a.x
这个实例属性,本来是不存在的,但是,由于类中有了__getattr__(self, name)
方法,当发现属性x
不存在于对象的__dict__
中的时候,就调用了__getattr__
,即所谓“拦截成员”。
>>> a.x = 7You use setattr
给对象的属性赋值时候,调用了__setattr__(self, name, value)
方法,这个方法中有一句self.__dict__[name] = value
,通过这个语句,就将属性和数据保存到了对象的__dict__
中,如果再调用这个属性:
>>> a.x7
它已经存在于对象的__dict__
之中。
在上面的类中,当然可以使用__getattribute__(self, name)
,并且,只要访问属性就会调用它。例如:
>>> class B(object):... def __getattribute__(self, name):... print "you are useing getattribute"... return object.__getattribute__(self, name)
为了与前面的类区分,新命名一个类名字。需要提醒注意,在这里返回的内容用的是return object.__getattribute__(self, name)
,而没有使用return self.__dict__[name]
样式。因为如果用return self.__dict__[name]
这样的方式,就是访问self.__dict__
,只要访问这个属性,就要调用`getattribute``,这样就导致了无线递归下去(死循环)。要避免之。
>>> b = B()>>> b.yyou are useing getattributeTraceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __getattribute__AttributeError: 'B' object has no attribute 'y'>>> b.twoyou are useing getattributeTraceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in __getattribute__AttributeError: 'B' object has no attribute 'two'
访问不存在的成员,可以看到,已经被__getattribute__
拦截了,虽然最后还是要报错的。
>>> b.y = 8>>> b.yyou are useing getattribute8
当给其赋值后,意味着已经在__dict__
里面了,再调用,依然被拦截,但是由于已经在__dict__
内,会把结果返回。
当你看到这里,是不是觉得上面的方法有点魔力呢?不错,的确是“黑魔法”。但是,它有什么具体应用呢?看下面的例子,能给你带来启发。
#!/usr/bin/env python# coding=utf-8"""study __getattr__ and __setattr__"""class Rectangle(object): #Python 3: class Rectangle: """ the width and length of Rectangle """ def __init__(self): self.width = 0 self.length = 0 def setSize(self, size): self.width, self.length = size def getSize(self): return self.width, self.lengthif __name__ == "__main__": r = Rectangle() r.width = 3 r.length = 4 print r.getSize() #Python 3: print(r.getSize()) r.setSize( (30, 40) ) print r.width #Python 3: print(r.width) print r.length #Python 3: print(r.length)
上面代码来自《Beginning Python:From Novice to Professional,Second Edittion》(by Magnus Lie Hetland),根据本教程的需要,稍作修改。
$ python 21301.py (3, 4)3040
这段代码已经可以正确运行了。但是,作为一个精益求精的程序员。总觉得那种调用方式还有可以改进的空间。比如,要给长宽赋值的时候,必须赋予一个元组,里面包含长和宽。这个能不能改进一下呢?
#!/usr/bin/env python# coding=utf-8"""study __getattr__ and __setattr__"""class Rectangle(object): #Python 3: class Rectangle: """ the width and length of Rectangle """ def __init__(self): self.width = 0 self.length = 0 def setSize(self, size): self.width, self.length = size def getSize(self): return self.width, self.length size = property(getSize, setSize)if __name__ == "__main__": r = Rectangle() r.width = 3 r.length = 4 print r.size r.size = 30, 40 print r.width print r.length
以上代码的运行结果同上。但是,因为加了一句size = property(getSize, setSize)
,使得调用方法是不是更优雅了呢?原来用r.getSize()
,现在使用r.size
,就好像调用一个属性一样。难道你不觉得眼熟吗?在《多态和封装》中已经用到过property函数了,虽然写法略有差别,但是作用一样。
本来,这样就已经足够了。但是,因为本节中出来了特殊方法,所以,一定要用这些特殊方法从新演绎一下这段程序。虽然重新演绎的不一定比原来的好,主要目的是演示本节的特殊方法应用。
#!/usr/bin/env python# coding=utf-8class NewRectangle(object): def __init__(self): self.width = 0 self.length = 0 def __setattr__(self, name, value): if name == "size": self.width, self.length = value else: self.__dict__[name] = value def __getattr__(self, name): if name == "size": return self.width, self.length else: raise AttributeErrorif __name__ == "__main__": r = NewRectangle() r.width = 3 r.length = 4 print r.size #Python 3: print(r.size) r.size = 30, 40 print r.width #Python 3: print(r.width) print r.length #Python 3: print(r.length)
除了类的样式变化之外,调用样式没有变。结果是一样的。
如果要对于这种黑魔法有更深的理解,可以阅读:Python Attributes and Methods,读了这篇文章,对Python的对象属性和方法会有更深入的理解。
至此,是否注意到,我们使用了很多以双下划线开头和结尾的方法或者属性,比如__dict__
,__init__()
等。在Python中,用这种方法表示特殊的方法和属性,当然,这是一个惯例,之所以这样做,主要是确保这些特殊的名字不会跟你自己所定义的名称冲突,我们自己定义名称的时候,是绝少用双划线开头和结尾的。如果你需要重写这些方法,当然是可以的。