返回介绍

建议59:理解描述符机制

发布于 2024-01-30 22:19:09 字数 5037 浏览 0 评论 0 收藏 0

除了在不同的局部变量、全局变量中查找名字,还有一个相似的场景不可不察,那就是查找对象的属性。在Python中,一切皆是对象,所以类也是对象,类的实例也是对象。

>>> class MyClass(object):
...  class_attr = 1
... 
>>> MyClass.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>, '
  __module__': '__main__', '__weakref__': <attribute '__weakref__'
  of 'MyClass' objects>, '__doc__': None, ' class _attr': 1})

每一个类都有一个__dict__属性,其中包含的是它的所有属性,又称为类属性。留意类属性的最后一个元素,可以看到我们代码中定义的属性在其中的体现。

>>> my_instance = MyClass()
>>> my_instance.__dict__
{}

除了与类相关的类属性之外,每一个实例也有相应的属性表(__dict__),称为实例属性。当我们通过实例访问一个属性时,它首先会尝试在实例属性中查找,如果找不到,则会到类属性中查找。

>>> my_instance. class _attr
1

可以看到实例my_instance可以访问类属性class_attr。但与读操作有所不同,如果通过实例增加一个属性,只能改变此实例的属性,对类属性而言,并没有丝毫变化。这从下面的代码中可以得到印证。

>>> my_instance.inst_attr = 'china'
>>> my_instance.__dict__
{'inst_attr': 'china'}
>>> MyClass.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>, '
  __module__': '__main__', '__weakref__': <attribute '__weakref__'
  of 'MyClass' objects>, '__doc__': None, ' class _attr': 1})

那么,能不能给类增加一个属性呢?答案是,能,也不能。说能,是因为每一个class也是一个对象,动态地增减对象的属性与方法正是Python这种动态语言的特性,自然是支持的。

>>> MyClass.class_attr2 = 100
>>> my_instance.class_attr2
100

说不能,是因为在Python中,内置类型和用户定义的类型是有分别的,内置类型并不能够随意地为它增加属性或方法。

>>> str.new_attr = 1
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'str'
>>> setattr(str, 'new_attr', 1)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'str'

至此,我们应当理解了,当我们通过“.”操作符访问一个属性时,如果访问的是实例属性,与直接通过__dict__属性获取相应的元素是一样的;而如果访问的是类属性,则并不相同:“.”操作符封装了对两种不同属性进行查找的细节。

>>> my_instance.__dict__['inst_attr']
'china'
>>> my_instance.__dict__['class_attr2']
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
KeyError: 'class_attr2'

不过,这里要讲的并不止于此,“.”操作符封装了对实例属性和类属性查找的细节,只讲了一半事实,还有一部分隐而未谈,那就是描述符机制。

>>> MyClass.__dict__['inst_attr']
'china'
>>> MyClass.inst_attr
'china'

我们已经知道访问类属性时,通过__dict__访问和使用“.”操作符访问是一样的,但如果是方法,却又不是如此了。

>>> class MyClass(object):
...  def my_method(self):
...      print 'my_method'
... 
>>> MyClass.__dict__['my_method']
<function my_method at 0x102773aa0>
>>> MyClass.my_method
<unbound method MyClass.my_method>

甚至它们的类型都不一样!

>>> type(MyClass.my_method)
<type 'instancemethod'>
>>> type(MyClass.__dict__['my_method'])
<type 'function'>

这其中作怪的就是描述符了。当通过“.”操作符访问时,Python的名字查找并不是之前说的先在实例属性中查找,然后再在类属性中查找那么简单,实际上,根据通过实例访问属性和根据类访问属性的不同,有以下两种情况:

一种是通过实例访问,比如代码obj.x,如果x是一个描述符,那么__getattribute__()会返回type(obj).__dict__['x'].__get__(obj,type(obj))结果,即:type(obj)获取obj的类型;type(obj).__dict__['x']返回的是一个描述符,这里有一个试探和判断的过程;最后调用这个描述符的__get__()方法。

另一种是通过类访问的情况,比如代码cls.x,则会被__getattribute__()转换为cls.__dict__['x'].__get__(None,cls)。

至此,就能够明白MyClass.__dict__['my_method']返回的是function而不是instancemethod了,原因是没有调用它的__get__()方法。是否如此呢?怎么验证一下?我们可以尝试手动调用__get__()。

>>> t = f.__get__(None, MyClass)
>>> t
<unbound method MyClass.my_method>
>>> type(t)
<type 'instancemethod'>

看,果然是这样!这是因为描述符协议是一个Duck Typing的协议,而每一个函数都有__get__方法,也就是说其他每一个函数都是描述符。

描述符机制有什么作用呢?其实它的作用编写一般程序的话还真用不上,但对于编写程序库的读者来说,就非常有用了。比如大家熟悉的已绑定方法和未绑定方法,它是怎么来的呢?

>>> MyClass.my_method
<unbound method MyClass.my_method>
>>> a = MyClass()
>>> a.my_method
<bound method MyClass.my_method of <__main__.MyClass object at 0x10277a490>>

上面例子输出的不同,其实来自于对描述符的__get__()的调用参数的不同,当以obj.x的形式访问时,调用参数是__get__(obj,type(obj));而以cls.x的形式访问时,调用参数是__get__(None,type(obj)),这可以通过未绑定方法的im_self属性为None得到印证。

>>> print MyClass.my_method.im_self
None
>>> a.my_method.im_self
<__main__.MyClass object at 0x10277a490>

除此之外,所有对属性、方法进行修饰的方案往往都用到了描述符,比如classmethod、staticmethod和property等。在这里,给出property的参考实现作为本节的结束,更深入的应用可以进一步参考Python源码中的其他用法。

class Property(object):
   "Emulate PyProperty_Type() in Objects/descrobject.c"
   def __init__(self, fget=None, fset=None, fdel=None, doc=None):
     self.fget = fget
     self.fset = fset
     self.fdel = fdel
     self.__doc__ = doc
   def __get__(self, obj, objtype=None):
     if obj is None:
       return self     
     if self.fget is None:
       raise AttributeError, "unreadable attribute"
     return self.fget(obj)
   def __set__(self, obj, value):
     if self.fset is None:
       raise AttributeError, "can't set attribute"
     self.fset(obj, value)
   def __delete__(self, obj):
     if self.fdel is None:
       raise AttributeError, "can't delete attribute"
     self.fdel(obj)

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文