Python描述符(descriptor)

定义:
如果一个类,它含有__get__,__set__,__del__中的任何一个,那么都可以说这个类是一个描述符。
对于一个类的属性访问,默认行为是从类的字典中set,get,del属性,查找顺序是a.__dict__['x'], 然后type(a).__dict__['x']。那如果定义了描述符,__set__,__get__,__del__会覆盖这些属性,直接访问描述符中的这些方法。

class myDescriptor(object):

    def __get__(self,instance,cls):
        return (self,instance,cls)

    def __set__(self,instance,value):
        print '%r %r %r' % (self,instance,value)



class Article(object):
    art = myDescriptor()



a = Article()
a.art = 10
print a.art


输出:

<__main__.myDescriptor object at 0x0000000002F04748> <__main__.Article object at 0x0000000002F04780> 10
(<__main__.myDescriptor object at 0x0000000002F04748>, <__main__.Article object at 0x0000000002F04780>, <class '__main__.Article'>)

可以看出,a.art,a.art=10,并没有直接访问,a.__dict__['art'],而是直接调用了myDescriptor.__get__和__set__方法。

通过这种重载方式,可以定义许多特殊的行为。

作用1:

Python类中的方法其实就是描述符实现的。

类中的方法在命名空间中还是以普通函数的形式存在的,如:

   class Test(object):

    def t(self,a):
        print a



    te=Test()
    te.t(1)
    Test.t(te,1)

t方法其实等价于Test.__dict__['t']函数。
在看上面的例子,一个 def 定义的 Python 函数或一个 lambda 表达式,除了拥有__call__ 实现外,还拥有__get__ 实现。也就是说,所有 Python 函数都默认是一个描述符。这个描述符的特性在“类”这一对象上下文之外没有任何意义,但只要到了类中,就会和其他描述符所表现的一样,将 my_instance.my_method 重载为 MyClass.__dict__['my_method'].__get__(my_instance, MyClass) 。“绑定 self 参数” 这个过程正是 __get__ 的行为,也就是说te.t(1)和Test.t(te,1)是等价的。

也就是说,类中的方法是通过描述符实现的。

作用二:property

Property类本身就是一个描述符,property会返回新式类的一个property属性,它可以将方法的访问变成属性的直接访问,比较方便。

class Property(object):
"Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is 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)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

Property定义了描述的方法。

property类有setter,getter,deleter属性,如果没有setter,那么它就只有只读属性。

class Article(object):

    def __init__(self,a,b):
        self.a = a
        self.b = b
        self._c = None

    @property
    def c(self):
        return self._c

    @c.setter
    def c(self,value):
        if value < 0:
            raise ValueError('the value must be bigger than 0')
        else:
            self._c = value

a = Article(2,3)
a.x = 100
print a.x 
a.x = -100

输出:

100

Traceback (most recent call last):
  File "C:\Python27\ths.py", line 41, in <module>
    a.c = -100
  File "C:\Python27\ths.py", line 32, in c
    raise ValueError('the value must be bigger than 0')
ValueError: the value must be bigger than 0

上面代码中,访问c就直接像访问属性那样即可,也就是说你执行a.x其实执行了property的__get__方法。 且在为属性赋值的时候,如果小于0就会抛出一个异常。

作用三:自定义描述符

我们可以通过自定义描述符,从而实现一些特定的功能,比如延迟属性等等。

python对象的延迟初始化是指,当它第一次被创建时才进行初始化,或者保存第一次创建的结果,然后每次调用的时候直接返回该结果。

延迟初始化主要用于提高性能,避免浪费计算,并减少程序的内存需求。

下面的这个例子是werkzeug的一个描述器,它继承自父类property,并自定义了__set__和__get__方法。它就实现了延迟属性。

class _Missing(object):

    def __repr__(self):
        return 'no value'

    def __reduce__(self):
        return '_missing'

_missing = _Missing()

class cached_property(property):

    """A decorator that converts a function into a lazy property.  The
    function wrapped is called the first time to retrieve the result
    and then that calculated result is used the next time you access
    the value::

        class Foo(object):

            @cached_property
            def foo(self):
                # calculate something important here
                return 42

    The class has to have a `__dict__` in order for this property to
    work.
    """

    # implementation detail: A subclass of python's builtin property
    # decorator, we override __get__ to check for a cached value. If one
    # choses to invoke __get__ by hand the property will still work as
    # expected because the lookup logic is replicated in __get__ for
    # manual invocation.

    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, _missing)
        if value is _missing:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value



class Foo(object):

    @cached_property
    def foo(self):
        print 'start calcute'
        # calculate something important here
        return 42


f = Foo()
print f.foo  start calcute ,42
print f.foo    42

第一次调用方法时将值存储起来,当第二次调用的时候就直接返回存储起来的值。这个例子是非常典型的例子,即写一个property的子类,并重构__get__,__set__等方法。__name__,__module__等属性也可以重构。

另外的例子是Django中model模型的定义:

class Category(models.Model):

    name = models.CharField(max_length=150,unique=True,verbose_name=u'类名')
    alias = models.CharField(max_length=150,verbose_name=u'英文名称')
    status = models.IntegerField(default=0,choices=STATUS.items(),verbose_name=u'状态')
    #Automatically set the field to now when the object is first created.
    create_time = models.DateTimeField(auto_now_add=True)

属性name其实就是一个描述器,只有在执行SQL查询时才会初始化。

--------EOF---------
微信分享/微信扫码阅读