Python 描述符

Python 描述符

什么是描述符(Descriptor)

定义了 __get__(),__set__(),__delete__()中任意一个描述符协议的对象成为描述符。简单来说,描述符就是可重用的属性。描述符具有诸多优点,诸如:保护属性不受修改、属性类型检查和自动更新某个依赖属性的值等。

    descr.__get__(self, obj, type=None) --> value

    descr.__set__(self, obj, value) --> None

    descr.__delete__(self, obj) --> None

__get__ 方法接受两个参数,一个是实例对象,另一个是实例的类型。__set__方法接受两个参数,一个是实例对象,另一个是数值。而 __delete__ 方法包含一个实例对象参数,并在实例对象被删除时调用。

__dict__ (每个对象均具备该属性)字典类型,存放本对象的属性,key(键)即为属性名,value(值)即为属性的值,形式为 {attr_key : attr_value}

Python 对象的属性控制默认是这样的,从对象的字典 __dict__ 中获取 get,设置 set ,删除 delete ,比如:对于实例 aa.x 的查找顺序为 a.__dict__['x'],然后是type(a).__dict__['x'].如果还是没找到就往上级(父类)中查找。具体顺序见 属性访问顺序

描述符会改变这种默认的控制行为,如果属性 x 是一个描述符,那么访问 a.x 时不再从字典 __dict__ 中读取,而是调用描述符的 __get__() 方法,对于设置和删除也是同样的原理。

property 属性

通过 property 将函数调用伪装成属性调用。

#!/usr/bin/env python

class Movie(object):
    def __init__(self, title, rating, runtime, budget, gross):
        self._budget = None
        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.gross = gross
        self.budget = budget

    @property
    def budget(self):
        return self._budget

    @budget.setter
    def budget(self, value):
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self._budget = value

    def profit(self):
        return self.gross - self.budget

m = Movie('Casablanca', 97, 102, 964000, 1300000)
print m.budget       # calls m.budget(), returns result
try:
    m.budget = -100  # calls budget.setter(-100), and raises ValueError
except ValueError:
    print "Woops. Not allowed"

# results

964000
Woops. Not allowed

当试图访问 budget 属性,Python 就会自动调用相应的 getter/setter 方法。比方说,当遇到 m.budget = value 这样的代码时就会自动调用 budget.setter

property 将自定义的代码同变量的访问/设定联系在了一起,同时使类保持一个简单的访问属性的接口。

但是当使用 property 对参数进行检查,需要对每个属性进行检查无法对代码进行复用。

描述符

#!/usr/bin/env python
from weakref import WeakKeyDictionary

class NonNegative(object):
    """A descriptor that forbids negative values"""
    def __init__(self, default):
        self.default = default
        self.data = WeakKeyDictionary()

    def __get__(self, instance, owner):
        # we get here when someone calls x.d, and d is a NonNegative instance
        # instance = x
        # owner = type(x)
        return self.data.get(instance, self.default)

    def __set__(self, instance, value):
        # we get here when someone calls x.d = val, and d is a NonNegative instance
        # instance = x
        # value = val
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self.data[instance] = value

class Movie(object):

    #always put descriptors at the class-level
    rating = NonNegative(0)
    runtime = NonNegative(0)
    budget = NonNegative(0)
    gross = NonNegative(0)

    def __init__(self, title, rating, runtime, budget, gross):
        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.budget = budget
        self.gross = gross

    def profit(self):
        return self.gross - self.budget

m = Movie('Casablanca', 97, 102, 964000, 1300000)
print m.budget  # calls Movie.budget.__get__(m, Movie)
m.rating = 100  # calls Movie.budget.__set__(m, 100)
try:
    m.rating = -1   # calls Movie.budget.__set__(m, -100)
except ValueError:
    print "Woops, negative value"

# results

964000
Woops, negative value

当函数调用 m.budget 时,会将其视为具有 __get__ 方法的描述符,然后调用 Moive.budget.__get__ 返回结果。

Reference