>>> from env_helper import info; info()
页面更新时间: 2023-12-27 10:42:20
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

6.6. 理解描述符机制

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

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

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

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

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

>>> my_instance.class_attr
1

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

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

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

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

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

>>> str.new_attr=1
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Cell In [8], line 1
----> 1 str.new_attr=1


TypeError: cannot set 'new_attr' attribute of immutable type 'str'

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

>>> my_instance.__dict__['inst_attr']
'china'
>>> my_instance.__dict__['class_attr2']
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

Cell In [10], line 1
----> 1 my_instance.__dict__['class_attr2']


KeyError: 'class_attr2'

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

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

KeyError                                  Traceback (most recent call last)

Cell In [11], line 1
----> 1 MyClass.__dict__['inst_attr']


KeyError: 'inst_attr'
>>> MyClass.inst_attr
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In [12], line 1
----> 1 MyClass.inst_attr


AttributeError: type object 'MyClass' has no attribute 'inst_attr'

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

>>> class MyClass(object):
>>>     def my_method(self):
>>>         print('my_method')
>>>
>>> MyClass.__dict__['my_method']
<function __main__.MyClass.my_method(self)>
>>> MyClass.my_method
<function __main__.MyClass.my_method(self)>

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

>>> type(MyClass.my_method)
function
>>> type(MyClass.__dict__['my_method'])
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
<function __main__.MyClass.my_method>
>>> a =MyClass()
>>> a.my_method
<bound method MyClass.my_method of <__main__.MyClass object at 0x7f80a86dce48>>

上面例子输出的不同,其实来自于对描述符的__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 Object/descrobject.c"
>>>     def __init__(self,fget=None,fset=None,fdel=None,doc=None):
>>>         self.fget = fget
>>>         self.fset = fset
>>>         self.fdel = fdel
>>>     def __get__(self,obj,objtype=None):
>>>         if obj is None:
>>>             return self
>>>         if self.fget in 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)