python描述器知多少

python描述器知多少

在改代码的时候碰到设计django model相关的,脑子里条件反射般的的想起model中各种数据类型的不就是借助描述器(descriptor)实现的么,但是却脑补不出具体的实现原理。趁着年底公司比较清静,可以好好的再研究下描述器。描述器有三个特殊方法:__get__(self, obj, type=None) –> value、__set__(self, obj, value) –> None、__delete__(self, obj) –> None。实现任一方法的对象都可以称作描述器对象。描述器对象又分为两种:实现了__get__和__set__的称为data descriptor;只实现了__get__的称为non-data descriptor。python中的属性、方法、静态方法和类方法装饰器都是基于描述器机制实现的,所以了解了描述器对了解python的某些底层实现是很有帮助的。那描述器到底干有什么神操作呢?

在讨论decriptor的作用之前,先熟悉下python对象在访问属性时的查找顺序:先调用__getattribute__特殊方法,也就是先查找对象的属性字典即a.__dict__[‘x’],然后是type(a).__dict__[‘x’] ,然后在baseclasses中查找。如果属性字典中不存在,则调用特殊方法__getattr__,前提是该对象重写了这个特殊方法。总结下就是先调用__getattribute__方法,然后调用__getattr__方法,都没有找到没有则触发AttributeError。

敲黑板了!如果先把一个描述器对象赋值给要查找的属性,那么这个时候再访问这个属性时,其查找顺序就会被改变了,根据descriptor的类型会进行不同的查找顺序。如果是data descriptor,则会优先调用描述器对象的__get__方法,即a.__dict__[‘x’].__get__(a, type(a)),没有查到则按对象属性的默认查找顺序继续执行;如果是non-data descriptor,则会先查找属性字典,如果没有查到则调用a.__dict__[‘x’].__get__(a, type(a)),经过这两步都没有查到则再调用__getattr__,也就是说__getattr__这个特殊方法的调用优先级是最低的。有几点需要说明下:

  • 从上边可以看到,无论是哪种描述器,在调用__get__时都得先调用a.__dict__[‘x’],也就是必须先调用__getattribute__方法,可见描述器的触发都是__getattribute__。如果重写了实例对象的__getattribute__特殊方法,则会影响描述器的自动调用,该特殊方法绝大多数情况下不要重写。
  • __get__方法中有两个参数:obj和type(obj),如果是通过obj.x访问属性,则obj和type(obj)都会被自动传入;如果是type(obj).x通过类访问属性,则obj为None。在实现__get__方法时需针对不同情况做相应处理。__set__和  __delete__是同样的道理。
  • 只要描述器对象定义了__set__和__delete__,在给属性赋值和删除时都会优先调用。
  • 一个描述器对象可以将其作为属性绑定给对象,也可以像普通类那样实例化,毕竟它只是一个类。
通过文字描述显得比较抽象,不容易理解,下边通过实际的例子来加深理解:

1、一个简单的描述器

class SimpleDes:
    def __init__(self):
        self._value = None

    def __set__(self, obj, value):
        self._value = value
    
    def __get__(self, obj, cls):
        return self._value

    def __delete__(self, obj):
        del self._value
        print('delete value')

class Simple:
    anything = SimpleDes()
    
s = Simple()
s.anything = 'Im anything!'
print(s.anything)
print("attribute dict of Simple object:", s.__dict__)
del s.anything

>>>output:
Im anything!
attribute dict of Simple object:{}
delete value

这个例子中的SimpleDes是一个data descriptor,实例属性的访问都要先经过描述器,而实例对象的属性并没有发生改变,所有的属性访问都是在操作描述器对象的私有属性。

2、实现了属性类型校验的描述器,同时会给实例绑定对应的属性。

class BaseDesc:
    def __init__(self, name):
        self.name = name
        self.value = None
        self._type = None

    def __get__(self, obj, cls):
        if obj is None:
            return self
        if self.name in obj.__dict__:
            return obj.__dict__[self.name]
        return self.value

    def __set__(self, obj, value):
        if not isinstance(value, self._type):
            raise TypeError("The type of '{}' is not {}".format(value, self._type))
        if obj:
            obj.__dict__[self.name] = value
        self.value = value

class Integer(BaseDesc):
    def __init__(self, name):
        super().__init__(name)
        self._type = int
        
class String(BaseDesc):
    def __init__(self, name):
        super().__init__(name)
        self._type = str

class Product:
    price = Integer("price")
    name = String("name")

p = Product()
p.price = 100
p.name = 'ipad'
print(p.__dict__)

>>>output:
{'price': 100, 'name': 'ipad'}

p.price = 'hello'
>>>output:
TypeError: The type of 'hello' is not <class 'int'>

3、实现一个lazyproperty装饰器,避免属性的重复计算

class lazyproperty:
    def __init__(self, func):
        self.func = func
    
    def __get__(self, obj, cls):
        if not obj:
            return self
        result = self.func(obj)
        setattr(obj, self.func.__name__, result)
        return result

class Circle:

    def __init__(self, radius=123):
        self.radius = radius
        self.hello = 444

    @lazyproperty
    def area(self):
        print('compute area')
        return self.radius * 2

c = Circle(333)
print(c.area)
print(c.area)

>>>output:
compute area
666
666

上边的例子中通过non-data descriptor实现了一个lazyproperty装饰器,首先访问Circle对象的属性字典,没有则调用描述器的__get__方法给Circle对象添加了计算后得出的属性值,下次访问c.area的时候则直接从属性字典中获取,无需再计算,从而可以提升性能。

4、利用描述器模拟python函数或方法的实现

import types

class FuncDes:
    '''模拟python函数或方法的实现'''
    __name__ = 'hello'

    def __init__(self, arg):
        self.arg = arg

    def __get__(self, obj, cls):
        if obj is None:
            return self
        #MethodType第一个参数必须为可调用对象
        return types.MethodType(self, obj)
    
    def __call__(self, obj=None, arg=None):
        if arg is None:
            raise SyntaxError
        if obj is None:
            print('I am a function')
        else:
            print("Im method '{}' of object '{}'".format(self.__name__, obj))
        print('my args:', arg)
        
class T:
    hello = FuncDes(arg=None)
    def __init__(self):
        self.a = 1
        self.b = 999
t = T()
print(t.hello)
t.hello('hello')

func = FuncDes(arg=None)
func(arg='world')

>>>output:
<bound method hello of <__main__.T object at 0x006FF490>>
Im method 'hello' of object '<__main__.T object at 0x006FF490>'
my args: hello
I am a function
my args: world

通过上边的例子能看出,描述器就类似于一个代理,所有属性访问都会经过描述器进行些自定义的特殊处理,进而来实现很多实用的功能,比如类型校验、延迟计算等。描述器的黑魔法还不止这些,常用的staticmethod和classmethod装饰器也是基于描述器实现的,具体的实现样例可参考Descriptor How To Guide。其实如果真正理解了其原理,至于实现,只不过是敲敲键盘的事情罢了。正所谓万变不离其宗,当把其中的原理搞明白了,才能触类旁通,举一反三,应用起来更加游刃有余。工欲善其事,必先利其器,共勉之!

Comments are closed.