描述符是实现描述符协议方法的Python对象,当将其作为其他对象的属性进行访问时,该描述符使您能够创建具有特殊行为的对象。
通常,描述符是具有“绑定行为”的对象属性,其属性访问已被描述符协议中的方法所覆盖。这些方法是__get __(),__set __()和__delete __()。如果为对象定义了这些方法中的任何一种,则称其为描述符。属性访问的默认行为是从对象的字典中获取,设置或删除属性。例如,a.x具有一个查找链,查找链从a .__ dict __ ['x']开始,然后键入(a).__ dict __ ['x'],并继续遍历类型(a)的基类(不包括元类)。如果查找到的值是定义描述符方法之一的对象,则Python可能会覆盖默认行为并改为调用描述符方法。优先链在何处发生取决于定义了哪些描述符方法。描述符是功能强大的通用协议。它们是属性,方法,静态方法,类方法和super()背后的机制。在Python本身中使用它们来实现2.2版中引入的新样式类。
1
2
3
|
descr.__get__( self , obj, type = None ) - > value
descr.__set__( self , obj, value) - > None
descr.__delete__( self , obj) - > None
|
定义这些方法中的任何一个,对象被视为描述符,并且在被视为属性时可以覆盖默认行为。
如果对象定义了__set __()或__delete __(),则将其视为数据描述符。仅定义__get __()的描述符称为非数据描述符(它们通常用于方法,但也可以用于其他用途)。数据和非数据描述符在实例字典中替代计算方式方面有所不同。如果实例的字典中的属性名称与数据描述符的名称相同,则以数据描述符为准。如果实例的字典中具有与非数据描述符同名的属性,则该字典属性优先。我们来看一下例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class lazy( object ):
def __init__( self , func):
self .func = func
def __get__( self , instance, owner):
val = self .func(instance)
setattr (instance, self .func.__name__, val)
return val
class Circle( object ):
def __init__( self , radius):
self .radius = radius
@lazy
def area( self ):
print ( 'evalute' )
return 3.14 * self .radius * * 2
def __getattr__( self , item):
return 1
c = Circle( 4 )
print (c.area)
print (c.area)
|
输出结果是
1
2
3
|
evalute
50.24
50.24
|
我们定义了一个描述符的类 lazy,它只实现了__get__方法,是一个非数据的描述符,我们用它定义了类Circle中的area方法,所以area方法成为了一个描述符的对象,可以看到,在第一次调用c.area的时候,执行了area的方法,打印了"evalute",在第二次的时间就直接输出了结果,没有指向area的方法,这是为什么呢?
那么重点来了,可以看到在lazy定义的__get__方法中,执行了被描述对象的方法,也就是这里的area函数,获取到结果之后,给当前的instance设置了一个同名的属性,并且设值为结果,这样下次在调用的时间,因为这是一个非数据的描述符,看上面的黑体字,实例的字典中的属性名称与数据描述符的名称相同,则以数据描述符为准。所以会取你刚刚设置的属性的值,不会再去取描述符的值。我们再来看看数据描述符的一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
class lazy( object ):
def __init__( self , func):
self .func = func
def __get__( self , instance, owner):
val = self .func(instance)
setattr (instance, self .func.__name__, val)
return val
def __set__( self , instance, value):
pass
class Circle( object ):
def __init__( self , radius):
self .radius = radius
@lazy
def area( self ):
print ( 'evalute' )
return 3.14 * self .radius * * 2
def __getattr__( self , item):
return 1
c = Circle( 4 )
print (c.area)
print (c.area)
|
我们看一下输出的结果:
1
2
3
4
|
evalute
50.24
evalute
50.24
|
同样的定义,只是在描述符中添加了__set__方法,就会执行调用描述符定义的属性,和非描述符的调用方式天壤之别。这就是这个高级特性的特别之处。我们可以使用非数据描述符做惰性加载,只计算一次,下次直接取值,我在工作中也是这样干的。
知其然,知其所以然,我们来看一下是为什么:
根据官方的解释,描述符可以通过其方法名称直接调用。例如,d .__ get __(obj)。另外,更常见的是在属性访问时自动调用描述符。例如,obj.d在obj的字典中查找d。如果d定义了方法__get __(),则根据下面列出的优先级规则调用d .__ get __(obj)。调用的细节取决于obj是对象还是类。
对于对象,机制位于object .__ getattribute __()中,它将b.x转换为type(b).__ dict __ ['x'] .__ get __(b,type(b))。该实现通过优先级链进行工作,该优先级链赋予数据描述符优先于实例变量的优先级,实例变量优先于非数据描述符的优先级,并为__getattr __()分配最低优先级。完整的C实现可在Objects / object.c中的PyObject_GenericGetAttr()中找到。
对于类,机制的类型为.__ getattribute __(),它将B.x转换为B .__ dict __ ['x'] .__ get __(无,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 __()防止自动描述符调用
-
object .__ getattribute __()和type .__ getattribute __()对__get __()进行不同的调用。
-
数据描述符始终会覆盖实例字典。非数据描述符可以被实例字典覆盖。
来源:站长资讯平台
|