Descriptor

python在2.2版本中引入了descriptor功能,引入了一些新的概念,比如classmethod, staticmethod, super,Property等,这些新功能都是基于descriptor而实现的。

Descriptor代码示例

 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 RevealAccess:
    """创建一个Descriptor类,用来打印出访问它的操作信息"""
    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

#使用Descriptor
class MyClass(object):
    #生成一个Descriptor实例,赋值给类MyClass的x属性
    x = RevealAccess(10, 'var "x"')
    y = 5 #普通类属性

a = MyClass()  
print(a.x)
a.x = 1
print(a.y)
print(MyClass.x)

执行以上程序会输出如下结果:

1
2
3
4
5
6
Retrieving var "x"
10
Updating var "x"
5
Retrieving var "x"
1

Descriptor定义

descriptor可以说是一个绑定了特定访问方法的类属性,这些访问方法是重写了descriptor protocol中的三个方法,分别是get, __set__, del方法。如果三个中任一一个方法在对象中定义了,就说这个对象是一个descriptor对象,可以把这个对象赋值给其它属性。descriptor protocol可以看成是一个有三个方法的接口。

默认的查找属性的顺序 例如,对于操作a.x,会一个查找链从a.__dict[‘x’](实例的字典),再到type(a).__dict__[‘x’](类的字典),再到type(a)的父类的字典等等。代码如下:

1
2
3
4
5
6
7
8
class A:
    def __init__(self):
        self.x = 1
a = A()
print("dir(a)的值: ",dir(a))
print("dir(A)的值: ",dir(A))
print("type(a)的值: ",type(a))
print("dir(type(a))的值: ",dir(type(a))) #dir(type(a))与dir(A)结果是一样的

执行以上程序会输出如下结果:

1
2
3
4
dir(a)的值:  ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'x']
dir(A)的值:  ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
type(a)的值:  <class '__main__.A'>
dir(type(a))的值:  ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

可以看出类和实例的字典属性的值的内容是不一样的,因为实例中有绑定属性x的存在。type(a)返回就是实例a的类型,类A。 如果这个需要被查找的属性是一个定义了descriptor协议方法的对象,那么python就不会按照默认的查找方式,而是调用descriptor协议中定义的方法去做处理。

Descriptor Protocol(协议)

有下面这三个方法
object.get(self, instance, owner):return value
object.set(self, instance, value):return None
object.delete(self, instance): return None

只要对象重写任何上面的一个方法,对象就被看作是descriptor,就可以不去采用默认的查找属性的顺序。 如果一个对象同时定义了get,__set方法,被看作是data descriptor;只定义了get__,被称为non-data descriptor。注意 ,descriptor的实例是一定类属性(class attribute)。

Descriptor调用方法

可以直接使用descriptor实例进行方法调用,如d.__get__(obj)。 一般是在属性访问的时候自动被调用,例如obj.d是在obj实例的字典属性中查找d变量,如果d定义了get方法和set方法,是一个data descriptor,则根据上面提到的优先级,会自动去调用 d.__get__(obj)。

下面的代码展示了简单的用法:

 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
class Des:
    def __init__(self, init_value):
        self.value = init_value

    def __get__(self, instance, typ):
        print('call __get__', instance, typ)
        return self.value

    def __set__(self, instance, value):
        print('call __set__', instance, value)
        self.value = value

    def __delete__(self, instance):
        prin('call __delete__', instance)

class Widget:
    t = Des(1)

def main():
    w = Widget()
    print(type(w.t))
    w.t = 1
    print(w.t, Widget.t)
    del w.t

if __name__=='__main__':
    main()

运行结果如下:

1
2
3
4
5
6
('call __get__', <__main__.Widget object at 0x02868570>, <class '__main__.Widget'>)
<type 'int'>
('call __set__', <__main__.Widget object at 0x02868570>, 1)
('call __get__', <__main__.Widget object at 0x02868570>, <class '__main__.Widget'>)
1 ('call __get__', None, <class '__main__.Widget'>)
('call __delete__', <__main__.Widget object at 0x02868570>)

从输出结果可以看到,对于这个三个特殊函数,形参instance是descriptor实例所在的类的实例(w), 而形参owner就是这个类(widget) w.t等价于 Pro.get(t, w, Widget).而Widget.t等价于 Pro.get(t, None, Widget)

descriptor注意事项

需要注意的是, descriptor的实例一定是类的属性,因此使用的时候需要自行区分实例。比如下面这个例子,保证属性不超过一定的阈值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class MaxValDes(object):
    def __init__(self, inti_val, max_val):
        self.value = inti_val
        self.max_val = max_val

    def __get__(self, instance, typ):
        return self.value

    def __set__(self, instance, value):
        self.value= min(self.max_val, value)

class Widget(object):
    a = MaxValDes(0, 10)

if __name__ == '__main__':
    w0 = Widget()
    print 'inited w0', w0.a
    w0.a = 123
    print 'after set w0',w0.a
    w1 = Widget()
    print 'inited w1', w1.a

运行结果如下:

1
2
3
inited w0 0
after set w0 10
inited w1 10

可以看到,对w0.a的赋值符合预期,但是w1.a的值却不是0,而是同w0.a一样。这就是因为,a是类Widget的类属性, Widget的实例并没有’a’这个属性,可以通过dict查看。 那么要怎么修改才符合预期呢,看下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MaxValDes(object):
    def __init__(self, attr, max_val):
        self.attr = attr
        self.max_val = max_val
    def __get__(self, instance, typ):
        return instance.__dict__[self.attr]
    def __set__(self, instance, value):
        instance.__dict__[self.attr] = min(self.max_val, value)
class Widget(object):
    a = MaxValDes('a', 10)
    b = MaxValDes('b', 12)
    def __init__(self):
        self.a = 0
        self.b = 1
if __name__ == '__main__':
    w0 = Widget()
    print 'inited w0', w0.a, w0.b
    w0.a = 123
    w0.b = 123
    print 'after set w0',w0.a, w0.b
    w1 = Widget()
    print 'inited w1', w1.a, w1.b

运行结果如下:

1
2
3
inited w0 0 1
after set w0 10 12
inited w0 0 1

可以看到,运行结果比较符合预期,w0、w1两个实例互不干扰。上面的代码中有两点需要注意:
第一:第7、10行都是通过instance.dict来取值、赋值,而不是调用getattr、setattr,否则会递归调用,死循环。
第二:现在类和类的实例都拥有‘a’属性,不过w0.a调用的是类属性‘a’.

基于Descriptor实现的功能

descriptor协议是简单而又强大的,一些新特性就是利用descriptor功能封装成一个独立的函数调用,如:
Property
绑定和非绑定方法
静态方法
类方法
super

property

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class C:
    def getX(self):
        print('get x')
        return self.__x

    def setX(self, value):
        print('set x', value)
        self.__x = value

    def delX(self):
        print('del x')
        del self.__x

    x = property(getX, setX, delX, "This is 'x' property.")

c = C()
c.x = 1  # set x 1
c.x      # get x
del c.x  # del x

运行结果如下:

1
2
3
set x 1
get x
del x

调用proprety()是一种创建data descriptor的一种简洁的方式,函数结构如下:
property(fget=None, fset=None, fdel=None, doc=None) #返回的是property对象,可以赋值给某属性,propety方法有四个参数,只要对没有进行赋值的参数进行访问就会报错。
c 是 C 的一个实例, x是C中定义的一个property属性:
当你引用c.x时, python调用 fget 方法取值给你. 当你为c.x赋值: x.attrib=value 时, python调用 fset方法, 并且value值做为fset方法的参数, 当你执行del c.x 时, python调用fdel方法, 当你传过去的名为 doc 的参数即为该属性的文档字符串.

又如,我们定义一个只读property属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Rect(object):
    def __init__(self, width, heigth):
        self.width = width
        self.heigth = heigth

    def getArea(self):
        return self.width * self.heigth
    area = property(getArea, doc='area of the rectangle')

a = Rect(2,3)
print(a.area)  # 6
a.area = 3
del a.area

只需要传入fget参数就可以,运行如下:

1
2
3
4
5
6
Traceback (most recent call last):
  File "main.py", line 12, in <module>
    a.area = 3
AttributeError: can't set attribute

属性area为只读,任何重新绑定和删除的操作都会报错。这是因为我们只定义了fget方法。

properties所做的事情与那些特殊方法getattr, __setattr__, \_delattr__ 等是极其相似的,不过同样的工作它干起来更简单更快捷。

关于property()方法的几点声明:
1.property()函数的四个参数,应该为methods(带self参数的那种),而不是function。
2.当你使用类去访问属性的时候,property设置的函数是不会被调用的。只有用实例去访问才会调用。

函数和方法,绑定与非绑定

Python的面向对象特性是基于函数的,函数的实现是需要使用到non-data descriptor的功能。
在类定义中,方法是由def或者lambda声明的。和一般函数不同的是,方法的第一个参数是self对象。
为了支持方法的调用,在访问方法属性的时候,functions使用相应的get方法。这就意味着所有的函数都是non-data descriptor,用于根据类或者对象的调用来返回unbound或者bound的方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
>>>class D(object):
      def f(self,x):
        retunr x
>>>d = D()
>>>D.__dict__['f']
<function f at 0x02164A30>
>>>D.f
<unbound method D.f>
>>>d.f
<bound method D.f of <__main__.D object at 0x021B1070>>
>>>D.f(d,3)
3
>>>d.f(3)
3
>>>D.f(3)
Traceback
TypeError

可以看到方法在字典中存放的类型其实是函数对象,bound和unbound方法是两个不同的类型。内部的实现其实是一个同一个对象,不同的是这个对象的im_self属性是否被赋值,或者是设为None。

super

在支持多继承的语言中,讨论谁是父类,感觉意义不大,尤其是像之类mro的菱形问题,父类是谁就更说不清了。需要强调的是super不会返回父类,它返回的是代理对象。代理对象就是利用委托(delegation)使用别的对象的方法来实现功能的对象。 super返回的是一个定制了getattribute方法的对象,是一个代理对象,它可以访问MRO中的方法。形式如下:
super(cls, instance-or-subclass).method(*args, **kw) 可以转化为:
right-method-in-the-MRO-applied-to(instance-or-subclass, *args, **kw) 需要注意的是,第二个参数instnce-or-subclass可以是第一个参数的实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
>>>class B(object):
      def __repr__(self):
        return '<instance of %s>' % self.__class__.__name__
>>>class C(B):
      pass
>>>class D(C):
      pass
>>>d = D()
>>>print super(C,d).__repr__
<bound methond D.__repr__ of <instance of D>>
>>>print super(C,D).__repr__
<unbound methond D.__repr>
>>>print super(C,d).__repr__()
 <instance of D>
>>>print super(C,D).__repr__()
Traceback
TypeError

如果返回了非绑定的方法,调用的时候需要加上第一个self参数。 通过descriptor的实现,可以说super也是一个non-data descriptor类。也就是实现了get(self, obj, objtyp=None)的类。假设descr是C类的一个descriptor,C.descr实现上调用的是descr.__get__(None, C);如果是实例来调用,c.descr调用的是descr.__get__(c, type©)。


转载请注明本网址。