>>> 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)