如何理解 Python 的 Descriptor
发布网友
发布时间:2022-04-22 16:07
我来回答
共2个回答
懂视网
时间:2022-04-13 09:07
Python 描述符是一种创建托管属性的方法。每当一个属性被查询时,一个动作就会发生。这个动作默认是get,set或者delete。不过,有时候某个应用可能会有
更多的需求,需要你设计一些更复杂的动作。最好的解决方案就是编写一个执行符合需求的动作的函数,然后指定它在属性被访问时运行。一个具有这种功能的对象
称为描述符。描述符是python方法,绑定方法,super,PRoperty,staticmethod和classmethod的实现基础。
1.描述符协议
描述符descriptor就是一个表示属性值的对象,通过实现一个或多个__get__,__set__,__delete__方法,可以将描述符与属性访问机制挂钩,还可以自定义这些操作。
__get__(self,instance,own):用于访问属性,返回属性的值。instance为使用描述符的实例对象,own为实例所属的类。当通过类访问属性时,instance为None。
__set__(self,instance,value):设定属性值。
__delete__(self,instance):删除属性值。
2.描述符如何实现
class Descriptor(object):
def __get__(self, instance, owner):
print 'getting:%s'%self._name
return self._name
def __set__(self, instance, name):
print 'setting:%s'%name
self._name = name
def __delete__(self, instance):
print 'deleting:%s'%self._name
del self._name
class Person(object):
name = Descriptor()
一个很简单的描述符对象就产生了,现在可以对一个Person对象进行属性name的读取,设置和删除:
>>> p=Person()
>>> p.name='john'
setting:john
>>> p.name
getting:john
'john'
>>> del p.name
deleting:john
注意:描述符只能在类级别上进行实例化,不能通过在__init__()和其他方法中创建描述符对象来为每个实例创建描述符。
具有描述符的类使用的属性名称比实例上存储的属性名称具有更高的优先级。为了能让描述符在实例上存储值,描述符必须挑选一个与它本身所用名称不同的名称。
如上例,Person类初始化__init__函数为实例设置属性就不能用name名称了。
data描述符与none-data描述符:
如果实现了__get__和__set__就是一个data描述符,如果只有__get__就是一个non-data描述符。不同的效果在于data描述符总是替代在一个实例中的属性实现,
而non-data描述符由于没有set,在通过实例对属性赋值时,例如上面的p.name = 'hello',不会再调用__set__方法,会直接把实例属性p.name设为'hello'。
当然如果仅仅在__set__中raise AttributeError,仍然得到的是一个non-data的描述符。
描述符调用机制:
当查询一个对象的属性a.attr时,如果python发现attr是个描述符对象,如何读取属性取决于对象a:
直接调用:最简单的调用是直接使用代码调用描述符的方法,attr.__get__(a)
实例绑定:如果a是个实例对象,调用方法:type(a).__dict__['attr'].__get__(a,type(a))
类绑定:如果A是个类对象,调用方法:A.__dict__['attr'].__get__(None,A)
super绑定:如果a是个super实例,那么super(B,obj).m()通过查询obj.__class__.__mro__找到B的基类A,然后执行A.__dict__['m'].__get__(obj,obj.__class__)
3.执行属性类型检查的描述符
class TypedProperty(object):
def __init__(self,name,attr_type,default=None):
self.name='_'+name
self.type=attr_type
self.default=default if default else attr_type()
def __get__(self,instance,own):
return getattr(instance,self.name,self.default)
def __set__(self,instance,value):
if not isinstance(value,self.type):
raise TypeError,'Must be %s'%self.type
setattr(instance,self.name,value)
def __delete__(self,instance):
raise AttributeError('Can not delete attribute')
class Foo(object):
name=TypedProperty('name',str)
num=TypedProperty('num',int,37)
上述描述符可以对属性的类型进行检查,如果name属性不设为str类型或者num不设为int类型,就会报错:
>>> f.name=21
TypeError: Must be <type 'str'>
而且禁止对属性进行删除操作:
>>> del f.name
AttributeError: Can not delete attribute
f.name 隐形的调用type(f).__dict__['name'].__get__(f,Foo),即Foo.name.__get__(f,Foo)。
上述描述符实际是存储在实例上的,name通过setattr(f,_name,value)存储在f._name上,num存储在f._num上,这也是加下划线的原因,
否则描述符名称name会和实例属性name发生冲突,描述符属性f.name会覆盖掉实例属性f.name。
热心网友
时间:2022-04-13 06:15
一般来说,一个描述器是一个有“绑定行为”的对象属性(object attribute),它的访问控制被描述器协议方法重写。这些方法是 __get__(), __set__(), 和 __delete__() 。有这些方法的对象叫做描述器。
默认对属性的访问控制是从对象的字典里面(__dict__)中获取(get), 设置(set)和删除(delete)它。举例来说, a.x 的查找顺序是, a.__dict__['x'] , 然后 type(a).__dict__['x'] , 然后找 type(a) 的父类(不包括元类(metaclass)).如果查找到的值是一个描述器, Python就会调用描述器的方法来重写默认的控制行为。这个重写发生在这个查找环节的哪里取决于定义了哪个描述器方法。注意, 只有在新式类中时描述器才会起作用。(新式类是继承自 type 或者 object 的类)
描述器是强大的,应用广泛的。描述器正是属性, 实例方法, 静态方法, 类方法和 super 的背后的实现机制。描述器在Python自身中广泛使用,以实现Python 2.2中引入的新式类。描述器简化了底层的C代码,并为Python的日常编程提供了一套灵活的新工具。
描述器协议
descr.__get__(self, obj, type=None) --> value
descr.__get__(self, obj, value) --> None
descr.__delete__(self, obj) --> None
一个对象如果是一个描述器,被当做对象属性(很重要)时重写默认的查找行为。
如果一个对象同时定义了__get__和__set__,它叫data descriptor。仅定义了__get__的描述器叫non-data descriptor。
data descriptor和non-data descriptor区别在于: 相对于实例的字典的优先级,如果实例字典有与描述器具同名的属性,如果描述器是data descriptor,优先使用data descriptor。如果是non-data descriptor,优先使用字典中的属性。
class B(object):
def __init__(self):
self.name = 'mink'
def __get__(self, obj, objtype=None):
return self.name
class A(object):
name = B()
a = A()
print a.__dict__ # print {}
print a.name # print mink
a.name = 'kk'
print a.__dict__ # print {'name': 'kk'}
print a.name # print kk
这里B是一个non-data descriptor所以当a.name = 'kk'的时候,a.__dict__里会有name属性, 接下来给它设置__set__
def __set__(self, obj, value):
self.name = value
... do something
a = A()
print a.__dict__ # print {}
print a.name # print mink
a.name = 'kk'
print a.__dict__ # print {}
print a.name # print kk
因为data descriptor访问属性优先级比实例的字典高,所以a.__dict__是空的。
描述器的调用
描述器可以直接这么调用: d.__get__(obj)
然而更常见的情况是描述器在属性访问时被自动调用。举例来说, obj.d 会在 obj 的字典中找 d ,如果 d 定义了 __get__ 方法,那么 d.__get__(obj) 会依据下面的优先规则被调用。
调用的细节取决于 obj 是一个类还是一个实例。另外,描述器只对于新式对象和新式类才起作用。继承于 object 的类叫做新式类。
对于对象来讲,方法 object.__getattribute__() 把 b.x 变成 type(b).__dict__['x'].__get__(b, type(b)) 。具体实现是依据这样的优先顺序:资料描述器优先于实例变量,实例变量优先于非资料描述器,__getattr__()方法(如果对象中包含的话)具有最低的优先级。完整的C语言实现可以在 Objects/object.c 中 PyObject_GenericGetAttr() 查看。
对于类来讲,方法 type.__getattribute__() 把 B.x 变成 B.__dict__['x'].__get__(None, B) 。用Python来描述就是:
def __getattribute__(self, key):
"Emulate type_getattro() in Objects/typeobject.c"
v = object.__getattribute__(self, key)
if hasattr(v, '__get__'):
return v.__get__(None, self)
return v
其中重要的几点:
描述器的调用是因为 __getattribute__()
重写 __getattribute__() 方法会阻止正常的描述器调用
__getattribute__() 只对新式类的实例可用
object.__getattribute__() 和 type.__getattribute__() 对 __get__() 的调用不一样
资料描述器总是比实例字典优先。
非资料描述器可能被实例字典重写。(非资料描述器不如实例字典优先)
super() 返回的对象同样有一个定制的 __getattribute__() 方法用来调用描述器。调用 super(B, obj).m() 时会先在 obj.__class__.__mro__ 中查找与B紧邻的基类A,然后返回 A.__dict__['m'].__get__(obj, A) 。如果不是描述器,原样返回 m 。如果实例字典中找不到 m ,会回溯继续调用 object.__getattribute__() 查找。(译者注:即在 __mro__ 中的下一个基类中查找)
注意:在Python 2.2中,如果 m 是一个描述器, super(B, obj).m() 只会调用方法 __get__() 。在Python 2.3中,非资料描述器(除非是个旧式类)也会被调用。 super_getattro() 的实现细节在: Objects/typeobject.c ,[del] 一个等价的Python实现在 Guido's Tutorial [/del] (译者注:原文此句已删除,保留供大家参考)。
以上展示了描述器的机理是在 object, type, 和 super 的 __getattribute__() 方法中实现的。由 object 派生出的类自动的继承这个机理,或者它们有个有类似机理的元类。同样,可以重写类的 __getattribute__() 方法来关闭这个类的描述器行为。
描述器例子
下面的代码中定义了一个资料描述器,每次 get 和 set 都会打印一条消息。重写 __getattribute__() 是另一个可以使所有属性拥有这个行为的方法。但是,描述器在监视特定属性的时候是很有用的。
class RevealAccess(object):
"""A data descriptor that sets and returns values
normally and prints a message logging their access.
"""
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print 'Retrieving', self.name
return self.val
def __set__(self, obj, val):
print 'Updating' , self.name
self.val = val
>>> class MyClass(object):
x = RevealAccess(10, 'var "x"')
y = 5
>>> m = MyClass()
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.y
5
这个协议非常简单,并且提供了令人激动的可能。一些用途实在是太普遍以致于它们被打包成独立的函数。像属性(property), 方法(bound和unbound method), 静态方法和类方法都是基于描述器协议的。