4.7 特殊属性和方法

在任何类中,都有一些特殊的属性和方法,它们的特殊性从表观就能看出来,通常是用双画线“”开头和结尾。本书中把它们归类为特殊的属性和方法,之所以特殊,是因为它们跟你自己写的或者其他不是以“”开头和结尾的属性、方法有所差异。或许你从事一般开发的项目时,对这些属性和方法使用得不多,但是我认为也是有必要了解的,因为这是你“From Beginner to Master”过程中必须要迈出的一步。知道有这一步,或许会对你的项目有帮助。俗话说“艺不压身”,还是认真了解为好。

4.7.1 dict

要访问类或者实例的属性必须通过“object.attribute”的方式,这是我们已经熟知的了。在这个认知的基础上,请思考:类或者实例属性在Python中是怎么存储的?如何修改、增加、删除属性,以及我们能不能控制这些属性?下面就一一道来。

  1. >>> class A(object):
  2. ... pass
  3. ...
  4.  
  5. >>> a = A()
  6. >>> dir(a)
  7. ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
  8. >>> dir(A)
  9. ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

用dir()能够查看类的属性和方法,从上面的结果中可以看出,数量不少,因为我们写的那个类里面只有pass,所以在列出的结果中,都是以“__”开头和结尾的,这些都是所谓的特殊属性和方法。

从众多的内容中寻觅出dict,之所以选它,是因为dict保存了某些机密。

  1. >>> class Spring(object):
  2. ... season = "the spring of class"
  3. ...
  4.  
  5. >>> Spring.__dict__
  6. dict_proxy({'__dict__': <attribute '__dict__' of 'Spring' objects>,
  7. 'season': 'the spring of class',
  8. '__module__': '__main__',
  9. '__weakref__': <attribute '__weakref__' of 'Spring' objects>,
  10. '__doc__': None})

为了便于观察,将上面的显示结果进行了换行,每个键/值对一行。

从现实的结果中可以发现,有一个键“season”,它是这个类的属性;其值就是类属性的数据。

  1. >>> Spring.__dict__['season']
  2. 'the spring of class'
  3. >>> Spring.season
  4. 'the spring of class'

Spring.dict['season']意思是访问类属性,这是看到上述结果为字典类型而想到的;另外一个我们熟悉的方式就是通过点号,也一样能够实现同样的效果。

下面将这个类实例化,再看看它的实例属性:

  1. >>> s = Spring()
  2. >>> s.__dict__
  3. {}

实例属性的dict是空的。有点奇怪?不奇怪,接着看:

  1. >>> s.season
  2. 'the spring of class'

s.season应该是指向了类属性中的Spring.season,至此,我们其实还没有建立任何实例属性。下面就建立一个实例属性:

  1. >>> s.season = "the spring of instance"
  2. >>> s.__dict__
  3. {'season': 'the spring of instance'}

这样,实例属性里面就不空了。这时候建立的实例属性和上面的那个s.season重名,并且把原来的“遮盖”了。这句好是不是熟悉?因为在讲述“实例属性”和“类属性”的时候就提到了,现在读者肯定理解更深入了。

  1. >>> s.__dict__['season']
  2. 'the spring of instance'
  3. >>> s.season
  4. 'the spring of instance'

此时,那个类属性如何?我们看看:

  1. >>> Spring.__dict__['season']
  2. 'the spring of class'
  3. >>> Spring.__dict__
  4. dict_proxy({'__dict__': <attribute '__dict__' of 'Spring' objects>, 'season': 'the spring of class', '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Spring' objects>, '__doc__': None})
  5. >>> Spring.season
  6. 'the spring of class'

Spring的类属性没有受到实例属性的影响。

按照前面讲述的类属性和实例属性的操作,如果将实例属性(s.season)删除,会不会回到实例属性s.dict为空呢?

  1. >>> del s.season
  2. >>> s.__dict__
  3. {}
  4. >>> s.season
  5. 'the spring of class'

果然打回原型。

当然,你可以定义其他名称的实例属性,它一样被存储到dict里面:

  1. >>> s.lang = "python"
  2. >>> s.__dict__
  3. {'lang': 'python'}
  4. >>> s.__dict__['lang']
  5. 'python'

诚然,这样做仅仅是更改了实例的dict内容,对Spring.dict无任何影响,也就是说通过Spring.lang或者Spring.dict['lang']是得不到上述结果的。

  1. >>> Spring.lang
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. AttributeError: type object 'Spring' has no attribute 'lang'
  5. >>> Spring.__dict__['lang']
  6. Traceback (most recent call last):
  7. File "<stdin>", line 1, in <module>
  8. KeyError: 'lang'

那么,如果这样操作,会怎样呢?

  1. >>> Spring.flower = "peach"
  2. >>> Spring.__dict__
  3. dict_proxy({'__module__': '__main__',
  4. 'flower': 'peach',
  5. 'season': 'the spring of class',
  6. '__dict__': <attribute '__dict__' of 'Spring' objects>, '__weakref__': <attribute '__weakref__' of 'Spring' objects>, '__doc__': None})
  7. >>> Spring.__dict__['flower']
  8. 'peach'

类的dict被更改了,类属性中增加了一个flower属性。但是,实例的dict中如何?

  1. >>> s.__dict__
  2. {'lang': 'python'}

没有被修改。然而,还能这样:

  1. >>> s.flower
  2. 'peach'

这个读者是否能解释?其实又回到了前面第一个出现s.season上面了。

通过上面的探讨,是不是基本理解了实例和类的dict,并且也看到了属性的变化特点。特别是,这些属性都是可以动态变化的,即你可以随时修改和增删。

属性如此,方法呢?下面就看看方法(类中的函数)。

  1. >>> class Spring(object):
  2. ... def tree(self, x):
  3. ... self.x = x
  4. ... return self.x
  5. ...
  6. >>> Spring.__dict__
  7. dict_proxy({'__dict__': <attribute '__dict__' of 'Spring' objects>,
  8. '__weakref__': <attribute '__weakref__' of 'Spring' objects>,
  9. '__module__': '__main__',
  10. 'tree': <function tree at 0xb748fdf4>,
  11. '__doc__': None})
  12.  
  13. >>> Spring.__dict__['tree']
  14. <function tree at 0xb748fdf4>

结果跟前面讨论属性差不多,方法tree()也在dict里面。

  1. >>> t = Spring()
  2. >>> t.__dict__
  3. {}

又跟前面一样。虽然建立了实例,但是在实例的dict中没有方法。接下来执行:

  1. >>> t.tree("xiangzhangshu")
  2. 'xiangzhangshu'

还记得前面某章某节有一幅阐述“数据流转”的图吗,其中显示非常明确,当用上面的方式执行方法的时候,实例t与self建立了对应关系,两者是一个外一个内。在方法中self.x=x,将x的值给了self.x,也就是实例应该拥有这么一个属性。

  1. >>> t.__dict__
  2. {'x': 'xiangzhangshu'}

果然如此。这也印证了实例t和self的关系,即实例方法(t.tree('xiangzhangshu'))的第一个参数(self,但没有写出来)绑定实例t,透过self.x来设定值,给t.dict添加属性值。

换一个角度再看看:

  1. >>> class Spring(object):
  2. ... def tree(self, x):
  3. ... return x
  4. ...

这个方法中没有将x赋值给self的属性,而是直接return,结果是:

  1. >>> s = Spring()
  2. >>> s.tree("liushu")
  3. 'liushu'
  4. >>> s.__dict__
  5. {}

是不是理解更深入了?

现在需要对Python中的一个观点:“一切皆对象”再深入领悟。以上不管是类还是实例的属性和方法,都符合object.attribute格式,并且属性类似。

当你看到这里的时候,要么明白了类和实例的dict的特点,要么就糊涂了。糊涂也不要紧,再将上面的重复一遍,特别要自己敲一敲有关代码。

需要说明,我们对dict的探讨还留有一个尾巴——属性搜索路径。这个留在后面讲述。

不管是类还是实例,其属性都能随意增加,有时这不是一件好事情,或许在某些时候你不希望别人增加属性。有办法吗?当然有,请继续学习。

4.7.2 slots

slots能够限制属性的定义,但是这不是它存在的终极目标,它存在的终极目标应该是在编程中非常重要的一个方面:优化内存使用。在某些编程中,优化内存是非常重要的,万万不可忽视。

  1. >>> class Spring(object):
  2. ... __slots__ = ("tree", "flower")
  3. ...
  4. >>> dir(Spring)
  5. ['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'flower', 'tree']

仔细看看dir()的结果,还有dict属性吗?没有了。也就是说slotsdict挤出去了,返回来看看,没有slots,现在它进入了类的属性。

  1. >>> Spring.__slots__
  2. ('tree', 'flower')

从这里可以看出,类Spring有且仅有两个属性,并且返回的是一个元组对象。

  1. >>> t = Spring()
  2. >>> t.__slots__
  3. ('tree', 'flower')

实例化之后,实例的slots与类的完全一样,这跟前面的dict大不一样了。

  1. >>> Spring.tree = "liushu"

通过类,先赋予一个属性值。然后检验一下实例能否修改这个属性:

  1. >>> t.tree = "guangyulan"
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. AttributeError: 'Spring' object attribute 'tree' is read-only

看来,我们的意图不能达成,报错信息中显示tree这个属性是只读的,不能修改。

  1. >>> t.tree
  2. 'liushu'

因为前面已经通过类给这个属性赋值了,不能用实例属性来修改。只能:

  1. >>> Spring.tree = "guangyulan"
  2. >>> t.tree
  3. 'guangyulan'

用类属性修改。但是对于没有用类属性赋值的,可以通过实例属性:

  1. >>> t.flower = "haitanghua"
  2. >>> t.flower
  3. 'haitanghua'

此时:

  1. >>> Spring.flower
  2. <member 'flower' of 'Spring' objects>

实例属性的值并没有传回到类属性,你也可以理解为新建立了一个同名的实例属性。如果再给类属性赋值,那么就会这样了:

  1. >>> Spring.flower = "ziteng"
  2. >>> t.flower
  3. 'ziteng'

当然,此时再给t.flower重新赋值,就会报出跟前面一样的错误。

  1. >>> t.water = "green"
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. AttributeError: 'Spring' object has no attribute 'water'

这里试图给实例新增一个属性,也失败了。

看来slots已经把实例属性牢牢地管控了起来,但更本质的是优化了内存。诚然,这种优化会在有大量的实例时显出效果。

4.7.3 getattrsetattr和其他类似方法

结合4.7.2节内容,看一个例子:

  1. >>> class A(object):
  2. ... pass
  3. ...
  4. >>> a = A()
  5. >>> a.x
  6. Traceback (most recent call last):
  7. File "<stdin>", line 1, in <module>
  8. 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,这个方法就被调用。下面用例子说明。
  1. >>> class A(object):
  2. ... def __getattr__(self, name):
  3. ... print "You use getattr"
  4. ... def __setattr__(self, name, value):
  5. ... print "You use setattr"
  6. ... self.__dict__[name] = value

类A是新式类,除了两个方法,没有别的属性。

  1. >>> a = A()
  2. >>> a.x
  3. You use getattr

依然调用了不存在的属性a.x,按照开头的例子是要报错的。但是,由于在这里使用了getattr(self,name)方法,当发现x不存在于对象的dict中时,就调用了getattr“拦截成员”。

  1. >>> a.x = 7
  2. You use setattr

给对象的属性赋值时,调用了setattr(self,name,value)方法,这个方法中有一句self.dict[name]=value,通过这个语句,就将属性和数据保存到了对象的dict中,如果再调用这个属性:

  1. >>> a.x
  2. 7

x已经存在于对象的dict之中。

在上面的类中,当然可以使用getattribute(self,name),因为它是新式类,并且,只要访问属性就会调用它。例如:

  1. >>> class B(object):
  2. ... def __getattribute__(self, name):
  3. ... print "you are useing getattribute"
  4. ... return object.__getattribute__(self, name)
  5. ...

为了与前面的类区分,重新搞一个类,在类的方法etattribute()中使用return object.getattribute_(self,name)。

再来访问一个不存在的属性:

  1. >>> b = B()
  2. >>> b.y
  3. you are useing getattribute
  4. Traceback (most recent call last):
  5. File "<stdin>", line 1, in <module>
  6. File "<stdin>", line 4, in __getattribute__
  7. AttributeError: 'B' object has no attribute 'y'
  8. >>> b.two
  9. you are useing getattribute
  10. Traceback (most recent call last):
  11. File "<stdin>", line 1, in <module>
  12. File "<stdin>", line 4, in __getattribute__
  13. AttributeError: 'B' object has no attribute 'two'

访问不存在的成员,立刻被getattribute拦截了,虽然最后还是要报错的。

  1. >>> b.y = 8
  2. >>> b.y
  3. you are useing getattribute
  4. 8

当给其赋值后,意味着其已经在dict里面了,再调用,依然被拦截,但是由于其已经在dict内,所以会把结果返回。

特别注意,在这个方法中,没有使用return self.dict[name],因为如果用这样的方式就是访问self.dict,只要访问类的某个属性,就要调用getattribute,这样就会导致无限递归下去(死循环)。要避免之。

当你看到这里,是不是觉得上面的方法有点魔力呢?在理解前述内容的基础上,再认真阅读下面的代码,会体会到在实践中的应用。

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. """
  4. study __getattr__ and __setattr__
  5. """
  6.  
  7. class Rectangle(object):
  8. """
  9. the width and length of Rectangle
  10. """
  11. def __init__(self):
  12. self.width = 0
  13. self.length = 0
  14. def setSize(self, size):
  15. self.width, self.length = size
  16. def getSize(self):
  17. return self.width, self.length
  18.  
  19. if __name__ == "__main__":
  20. r = Rectangle()
  21. r.width = 3
  22. r.length = 4
  23. print r.getSize()
  24. r.setSize( (30, 40) )
  25. print r.width
  26. print r.length

上面的代码来自《Beginning Python:From Novice to Professional,Second Edittion》(by Magnus Lie Hetland),根据本教程的需要,稍有修改。

  1. $ python 21301.py
  2. (3, 4)
  3. 30
  4. 40

这段代码已经可以正确运行了,但是,作为一个精益求精的程序员,总觉得那种调用方式还有可以改进的空间。比如,要给长宽赋值的时候,必须赋予一个元组,里面包含长和宽。这个能不能改进一下呢?

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. class Rectangle(object):
  4. def __init__(self):
  5. self.width = 0
  6. self.length = 0
  7. def setSize(self, size):
  8. self.width, self.length = size
  9. def getSize(self):
  10. return self.width, self.length
  11.  
  12. size = property(getSize, setSize)
  13.  
  14. if __name__ == "__main__":
  15. r = Rectangle()
  16. r.width = 3
  17. r.length = 4
  18. print r.size
  19. r.size = 30, 40
  20. print r.width
  21. print r.length

以上代码中因为加了一句size=property(getSize,setSize),使得调用方法更优雅了。原来用r.getSize(),现在使用r.size,就好像调用一个属性一样。

虽然优化了上面的代码,但是还没有和本节讲述的特殊方法拉上关系,所以,还要继续改写。

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. class NewRectangle(object):
  5. def __init__(self):
  6. self.width = 0
  7. self.length = 0
  8. def __setattr__(self, name, value):
  9. if name == "size":
  10. self.width, self.length = value
  11. else:
  12. self.__dict__[name] = value
  13. def __getattr__(self, name):
  14. if name == "size":
  15. return self.width, self.length
  16. else:
  17. raise AttributeError
  18.  
  19. if __name__ == "__main__":
  20. r = NewRectangle()
  21. r.width = 3
  22. r.length = 4
  23. print r.size
  24. r.size = 30, 40
  25. print r.width
  26. print r.length

除了类的样式变化之外,调用样式没有变,结果是一样的。

4.7.4 获得属性顺序

通过实例获取其属性,如果在dict中有,就直接返回其结果;如果没有,会到类属性中找。比如:

  1. #!/usr/bin/env python
  2. # coding=utf-8
  3.  
  4. class A(object):
  5. author = "qiwsir"
  6. def __getattr__(self, name):
  7. if name != "author":
  8. return "from starter to master."
  9.  
  10. if __name__ == "__main__":
  11. a = A()
  12. print a.author
  13. print a.lang

运行程序:

  1. $ python 21302.py
  2. qiwsir
  3. from starter to master.

当a=A()后,并没有为实例建立任何属性,或者说实例的dict是空的(意思是说没有某些属性值)。但是如果要查看a.author,因为实例的属性中没有,所以就去类属性中找,发现果然有,于是返回其值qiwsir。但是,找a.lang时候,不仅实例属性中没有,类属性中也没有,于是就调用了getattr()方法。幸好在这个类中有这个方法,如果没有getattr()方法呢?如果没有定义这个方法,就会引发AttributeError。

这就是通过实例查找特性的顺序。