4.7 特殊属性和方法
在任何类中,都有一些特殊的属性和方法,它们的特殊性从表观就能看出来,通常是用双画线“”开头和结尾。本书中把它们归类为特殊的属性和方法,之所以特殊,是因为它们跟你自己写的或者其他不是以“”开头和结尾的属性、方法有所差异。或许你从事一般开发的项目时,对这些属性和方法使用得不多,但是我认为也是有必要了解的,因为这是你“From Beginner to Master”过程中必须要迈出的一步。知道有这一步,或许会对你的项目有帮助。俗话说“艺不压身”,还是认真了解为好。
4.7.1 dict
要访问类或者实例的属性必须通过“object.attribute”的方式,这是我们已经熟知的了。在这个认知的基础上,请思考:类或者实例属性在Python中是怎么存储的?如何修改、增加、删除属性,以及我们能不能控制这些属性?下面就一一道来。
- >>> class A(object):
- ... pass
- ...
- >>> a = A()
- >>> dir(a)
- ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
- >>> dir(A)
- ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
用dir()能够查看类的属性和方法,从上面的结果中可以看出,数量不少,因为我们写的那个类里面只有pass,所以在列出的结果中,都是以“__”开头和结尾的,这些都是所谓的特殊属性和方法。
从众多的内容中寻觅出dict,之所以选它,是因为dict保存了某些机密。
- >>> class Spring(object):
- ... season = "the spring of class"
- ...
- >>> Spring.__dict__
- dict_proxy({'__dict__': <attribute '__dict__' of 'Spring' objects>,
- 'season': 'the spring of class',
- '__module__': '__main__',
- '__weakref__': <attribute '__weakref__' of 'Spring' objects>,
- '__doc__': None})
为了便于观察,将上面的显示结果进行了换行,每个键/值对一行。
从现实的结果中可以发现,有一个键“season”,它是这个类的属性;其值就是类属性的数据。
- >>> Spring.__dict__['season']
- 'the spring of class'
- >>> Spring.season
- 'the spring of class'
Spring.dict['season']意思是访问类属性,这是看到上述结果为字典类型而想到的;另外一个我们熟悉的方式就是通过点号,也一样能够实现同样的效果。
下面将这个类实例化,再看看它的实例属性:
- >>> s = Spring()
- >>> s.__dict__
- {}
实例属性的dict是空的。有点奇怪?不奇怪,接着看:
- >>> s.season
- 'the spring of class'
s.season应该是指向了类属性中的Spring.season,至此,我们其实还没有建立任何实例属性。下面就建立一个实例属性:
- >>> s.season = "the spring of instance"
- >>> s.__dict__
- {'season': 'the spring of instance'}
这样,实例属性里面就不空了。这时候建立的实例属性和上面的那个s.season重名,并且把原来的“遮盖”了。这句好是不是熟悉?因为在讲述“实例属性”和“类属性”的时候就提到了,现在读者肯定理解更深入了。
- >>> s.__dict__['season']
- 'the spring of instance'
- >>> s.season
- 'the spring of instance'
此时,那个类属性如何?我们看看:
- >>> Spring.__dict__['season']
- 'the spring of class'
- >>> Spring.__dict__
- dict_proxy({'__dict__': <attribute '__dict__' of 'Spring' objects>, 'season': 'the spring of class', '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Spring' objects>, '__doc__': None})
- >>> Spring.season
- 'the spring of class'
Spring的类属性没有受到实例属性的影响。
按照前面讲述的类属性和实例属性的操作,如果将实例属性(s.season)删除,会不会回到实例属性s.dict为空呢?
- >>> del s.season
- >>> s.__dict__
- {}
- >>> s.season
- 'the spring of class'
果然打回原型。
当然,你可以定义其他名称的实例属性,它一样被存储到dict里面:
- >>> s.lang = "python"
- >>> s.__dict__
- {'lang': 'python'}
- >>> s.__dict__['lang']
- 'python'
诚然,这样做仅仅是更改了实例的dict内容,对Spring.dict无任何影响,也就是说通过Spring.lang或者Spring.dict['lang']是得不到上述结果的。
- >>> Spring.lang
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- AttributeError: type object 'Spring' has no attribute 'lang'
- >>> Spring.__dict__['lang']
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- KeyError: 'lang'
那么,如果这样操作,会怎样呢?
- >>> Spring.flower = "peach"
- >>> Spring.__dict__
- dict_proxy({'__module__': '__main__',
- 'flower': 'peach',
- 'season': 'the spring of class',
- '__dict__': <attribute '__dict__' of 'Spring' objects>, '__weakref__': <attribute '__weakref__' of 'Spring' objects>, '__doc__': None})
- >>> Spring.__dict__['flower']
- 'peach'
类的dict被更改了,类属性中增加了一个flower属性。但是,实例的dict中如何?
- >>> s.__dict__
- {'lang': 'python'}
没有被修改。然而,还能这样:
- >>> s.flower
- 'peach'
这个读者是否能解释?其实又回到了前面第一个出现s.season上面了。
通过上面的探讨,是不是基本理解了实例和类的dict,并且也看到了属性的变化特点。特别是,这些属性都是可以动态变化的,即你可以随时修改和增删。
属性如此,方法呢?下面就看看方法(类中的函数)。
- >>> class Spring(object):
- ... def tree(self, x):
- ... self.x = x
- ... return self.x
- ...
- >>> Spring.__dict__
- dict_proxy({'__dict__': <attribute '__dict__' of 'Spring' objects>,
- '__weakref__': <attribute '__weakref__' of 'Spring' objects>,
- '__module__': '__main__',
- 'tree': <function tree at 0xb748fdf4>,
- '__doc__': None})
- >>> Spring.__dict__['tree']
- <function tree at 0xb748fdf4>
结果跟前面讨论属性差不多,方法tree()也在dict里面。
- >>> t = Spring()
- >>> t.__dict__
- {}
又跟前面一样。虽然建立了实例,但是在实例的dict中没有方法。接下来执行:
- >>> t.tree("xiangzhangshu")
- 'xiangzhangshu'
还记得前面某章某节有一幅阐述“数据流转”的图吗,其中显示非常明确,当用上面的方式执行方法的时候,实例t与self建立了对应关系,两者是一个外一个内。在方法中self.x=x,将x的值给了self.x,也就是实例应该拥有这么一个属性。
- >>> t.__dict__
- {'x': 'xiangzhangshu'}
果然如此。这也印证了实例t和self的关系,即实例方法(t.tree('xiangzhangshu'))的第一个参数(self,但没有写出来)绑定实例t,透过self.x来设定值,给t.dict添加属性值。
换一个角度再看看:
- >>> class Spring(object):
- ... def tree(self, x):
- ... return x
- ...
这个方法中没有将x赋值给self的属性,而是直接return,结果是:
- >>> s = Spring()
- >>> s.tree("liushu")
- 'liushu'
- >>> s.__dict__
- {}
是不是理解更深入了?
现在需要对Python中的一个观点:“一切皆对象”再深入领悟。以上不管是类还是实例的属性和方法,都符合object.attribute格式,并且属性类似。
当你看到这里的时候,要么明白了类和实例的dict的特点,要么就糊涂了。糊涂也不要紧,再将上面的重复一遍,特别要自己敲一敲有关代码。
需要说明,我们对dict的探讨还留有一个尾巴——属性搜索路径。这个留在后面讲述。
不管是类还是实例,其属性都能随意增加,有时这不是一件好事情,或许在某些时候你不希望别人增加属性。有办法吗?当然有,请继续学习。
4.7.2 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挤出去了,返回来看看,没有slots,现在它进入了类的属性。
- >>> 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已经把实例属性牢牢地管控了起来,但更本质的是优化了内存。诚然,这种优化会在有大量的实例时显出效果。
4.7.3 getattr、setattr和其他类似方法
结合4.7.2节内容,看一个例子:
- >>> class A(object):
- ... pass
- ...
- >>> a = A()
- >>> a.x
- Traceback (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'”
也就是说,如果访问a.x,它不存在,那么就要转向到某个操作。我们把这种情况称之为“拦截”。在Python中,方法就具有这种“拦截”能力。
- setattr(self,name,value):如果要给name赋值,就调用这个方法。
- getattr(self,name):如果name被访问,同时它不存在,此方法被调用。
- getattribute(self,name):当name被访问时自动被调用(注意:这个仅能用于新式类),无论name是否存在,都要被调用。
- delattr(self,name):如果要删除name,这个方法就被调用。下面用例子说明。
- >>> class A(object):
- ... def __getattr__(self, name):
- ... print "You use getattr"
- ... def __setattr__(self, name, value):
- ... print "You use setattr"
- ... self.__dict__[name] = value
类A是新式类,除了两个方法,没有别的属性。
- >>> a = A()
- >>> a.x
- You use getattr
依然调用了不存在的属性a.x,按照开头的例子是要报错的。但是,由于在这里使用了getattr(self,name)方法,当发现x不存在于对象的dict中时,就调用了getattr“拦截成员”。
- >>> a.x = 7
- You use setattr
给对象的属性赋值时,调用了setattr(self,name,value)方法,这个方法中有一句self.dict[name]=value,通过这个语句,就将属性和数据保存到了对象的dict中,如果再调用这个属性:
- >>> a.x
- 7
x已经存在于对象的dict之中。
在上面的类中,当然可以使用getattribute(self,name),因为它是新式类,并且,只要访问属性就会调用它。例如:
- >>> class B(object):
- ... def __getattribute__(self, name):
- ... print "you are useing getattribute"
- ... return object.__getattribute__(self, name)
- ...
为了与前面的类区分,重新搞一个类,在类的方法etattribute()中使用return object.getattribute_(self,name)。
再来访问一个不存在的属性:
- >>> b = B()
- >>> b.y
- you are useing getattribute
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "<stdin>", line 4, in __getattribute__
- AttributeError: 'B' object has no attribute 'y'
- >>> b.two
- you are useing getattribute
- Traceback (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.y
- you are useing getattribute
- 8
当给其赋值后,意味着其已经在dict里面了,再调用,依然被拦截,但是由于其已经在dict内,所以会把结果返回。
特别注意,在这个方法中,没有使用return self.dict[name],因为如果用这样的方式就是访问self.dict,只要访问类的某个属性,就要调用getattribute,这样就会导致无限递归下去(死循环)。要避免之。
当你看到这里,是不是觉得上面的方法有点魔力呢?在理解前述内容的基础上,再认真阅读下面的代码,会体会到在实践中的应用。
- #!/usr/bin/env python
- # coding=utf-8
- """
- study __getattr__ and __setattr__
- """
- class Rectangle(object):
- """
- 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
- if __name__ == "__main__":
- r = Rectangle()
- r.width = 3
- r.length = 4
- print r.getSize()
- r.setSize( (30, 40) )
- print r.width
- print r.length
上面的代码来自《Beginning Python:From Novice to Professional,Second Edittion》(by Magnus Lie Hetland),根据本教程的需要,稍有修改。
- $ python 21301.py
- (3, 4)
- 30
- 40
这段代码已经可以正确运行了,但是,作为一个精益求精的程序员,总觉得那种调用方式还有可以改进的空间。比如,要给长宽赋值的时候,必须赋予一个元组,里面包含长和宽。这个能不能改进一下呢?
- #!/usr/bin/env python
- # coding=utf-8
- class Rectangle(object):
- 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,就好像调用一个属性一样。
虽然优化了上面的代码,但是还没有和本节讲述的特殊方法拉上关系,所以,还要继续改写。
- #!/usr/bin/env python
- # coding=utf-8
- class 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 AttributeError
- if __name__ == "__main__":
- r = NewRectangle()
- r.width = 3
- r.length = 4
- print r.size
- r.size = 30, 40
- print r.width
- print r.length
除了类的样式变化之外,调用样式没有变,结果是一样的。
4.7.4 获得属性顺序
通过实例获取其属性,如果在dict中有,就直接返回其结果;如果没有,会到类属性中找。比如:
- #!/usr/bin/env python
- # coding=utf-8
- class A(object):
- author = "qiwsir"
- def __getattr__(self, name):
- if name != "author":
- return "from starter to master."
- if __name__ == "__main__":
- a = A()
- print a.author
- print a.lang
运行程序:
- $ python 21302.py
- qiwsir
- from starter to master.
当a=A()后,并没有为实例建立任何属性,或者说实例的dict是空的(意思是说没有某些属性值)。但是如果要查看a.author,因为实例的属性中没有,所以就去类属性中找,发现果然有,于是返回其值qiwsir。但是,找a.lang时候,不仅实例属性中没有,类属性中也没有,于是就调用了getattr()方法。幸好在这个类中有这个方法,如果没有getattr()方法呢?如果没有定义这个方法,就会引发AttributeError。
这就是通过实例查找特性的顺序。