首页 > 代码库 > python学习笔记-类的descriptor

python学习笔记-类的descriptor

descriptor应用背景

所谓描述器,是实现了描述符协议,即get, set, 和 delete方法的对象。
简单说,描述符就是可以重复使用的属性。
比如以下代码:

f = Foo()
b = f.bar
f.bar = c
del f.bar

在解释器执行上述代码时,当发现你试图访问属性(b = f.bar)、对属性赋值(f.bar = c)或者删除一个实例变量的属性(del f.bar)时,就会去调用自定义的方法。
为什么把对函数的调用伪装成对属性的访问?有什么好处?

从property说起

用property可以把函数调用伪装成对属性的访问。
举个例子,你的一个Movie类定义如下:

class Movie(object):
    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

开始在项目的其他地方使用这个类,但是之后你意识到:如果不小心给电影打了负分怎么办?你觉得这是错误的行为,希望Movie类可以阻止这个错误。 你首先想到的办法是将Movie类修改为这样:

class Movie(object):
    def __init__(self, title, rating, runtime, budget, gross):
        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.gross = gross
        if budget < 0:
            raise ValueError("Negative value not allowed: %s" % budget)
        self.budget = budget

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

但这行不通。因为其他部分的代码都是直接通过Movie.budget来赋值的——这个新修改的类只会在init方法中捕获错误的数据,但对于已经存在的类实例就无能为力了。如果有人试着运行m.budget = -100,那么谁也没法阻止。该怎么办?Python的property解决了这个问题。

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"

打印结果如下:
964000
Woops. Not allowed

用@property装饰器指定了一个getter方法,用@budget.setter装饰器指定了一个setter方法。当我们这么做时,每当有人试着访问budget属性,Python就会自动调用相应的getter/setter方法。比方说,当遇到m.budget = value这样的代码时就会自动调用budget.setter。

如果没有property,我们将不得不把所有的实例属性隐藏起来,提供大量显式的类似get_budget和set_budget方法。像这样编写类的话,使用起来就会不断的去调用这些getter/setter方法。更糟的是,如果我们不采用这种编码风格,直接对实例属性进行访问。那么稍后就没法以清晰的方式增加对非负数的条件检查——我们不得不重新创建set_budget方法,然后搜索整个工程中的源代码,将m.budget = value这样的代码替换为m.set_budget(value)。采用property的情况下,可以用object.value进行成员变量value值的获取,用object.value=http://www.mamicode.com/new_value对成员变量value进行重新赋值。
因此,property让我们将自定义的代码同变量的访问/设定联系在了一起,同时为你的类保持一个简单的访问属性的接口。

property的不足

对property来说,最大的缺点就是它们不能重复使用。举个例子,假设你想为rating,runtime和gross这些字段也添加非负检查。下面是修改过的新类:

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

        self.title = title
        self.rating = rating
        self.runtime = runtime
        self.gross = gross
        self.budget = budget

    #nice
    @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

    #ok    
    @property
    def rating(self):
        return self._rating

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

    #uhh...
    @property
    def runtime(self):
        return self._runtime

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

    #is this forever?
    @property
    def gross(self):
        return self._gross

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

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

可以看到代码增加了不少,但重复的逻辑也出现了不少。虽然property可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。所以,这时候出现了描述符!
描述符是property的升级版,允许你为重复的property逻辑编写单独的类来处理。下面的示例展示了描述符是如何工作的。

引入descriptor 描述符

我们知道装饰器需要用 @ 符号调用,迭代器通常在迭代过程,或者使用 next 方法调用。描述器则比较简单,访问对象属性的时候会调用。
先看下面例子:

from weakref import WeakKeyDictionary

class NonNegative(object):
    """A descriptor that forbids negative values"""
    def __init__(self, default):
        self.default = default
        self.data = http://www.mamicode.com/WeakKeyDictionary()"hljs-function">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 = http://www.mamicode.com/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)#这里所建立的4个描述符,可以视为普通的实例属性!
    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"

打印结果:
964000
Woops, negative value

NonNegative是一个描述符对象,因为它定义__get___set__delete_方法。
Movie类现在看起来非常清晰。我们在类的层面上创建了4个描述符,把它们当做普通的实例属性。显然,描述符在这里为我们做非负检查。

访问描述符

当解释器遇到print m.buget时,它就会把budget当作一个带有_get_ 方法的描述符,调用Movie.budget._get_方法并将方法的返回值打印出来,而不是直接传递m.budget来打印。这和访问一个property相似,Python自动调用一个方法,同时返回结果。

_get_接收2个参数:一个是点号左边的实例对象(在这里,就是m.budget中的m),另一个是这个实例的类型(Movie)。在一些Python文档中,Movie被称作描述符的所有者(owner)。如果我们需要访问Movie.budget,Python将会调用Movie.budget._get_(None, Movie)。可以看到,第一个参数要么是所有者的实例,要么是None。这些输入参数可能看起来很怪,但是这里它们告诉了你描述符属于哪个对象的一部分。当我们看到NonNegative类的实现时这一切就合情合理了。

对描述符赋值

当解释器看到m.rating = 100时,Python识别出rating是一个带有set方法的描述符,于是就调用Movie.rating._set_(m, 100)。和_get_一样,_set_的第一个参数是点号左边的类实例(m.rating = 100中的m)。第二个参数是所赋的值(100)。

删除描述符

为了说明的完整,这里提一下删除。如果你调用del m.budget,Python就会调用Movie.budget.delete(m)。

NonNegative类是如何工作的?

每个NonNegative的实例都维护着一个字典,其中保存着所有者实例和对应数据的映射关系。当我们调用m.budget时,_get_方法会查找与m相关联的数据,并返回这个结果(如果这个值不存在,则会返回一个默认值)。_set_采用的方式相同,但是这里会包含额外的非负检查。我们使用WeakKeyDictionary来取代普通的字典以防止内存泄露,因为这可以避免仅仅因为它在描述符的字典中就让一个无用?的实例一直存活着。

使用描述符会有一点别扭。因为它们作用于类的层次上,每一个类实例都共享同一个描述符。这就意味着对不同的实例对象而言,描述符不得不手动地管理?不同的状态,同时需要显式的将类实例作为第一个参数准确传递给_get__set_以及_delete_方法。

从这个例子可以指定描述符可以用来做什么——它们提供了一种方法将property的逻辑隔离到单独的类中来处理。如果你发现自己正在不同的property之间重复着相同的逻辑,那么也许你可以考虑下尝试下用描述符重构代码。

缺陷

为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么Python无法自动为你调用_get__set_方法。

class Broken(object):
    y = NonNegative(5)
    def __init__(self):
        self.x = NonNegative(0)  # NOT a good descriptor

b = Broken()
print "X is %s, Y is %s" % (b.x, b.y)

X is <__main__.NonNegative object at 0x10432c250>, Y is 5

可以看到,访问类层次上的描述符y可以自动调用_get_。但是访问实例层次上的描述符x只会返回描述符本身。
是使用描述符的时候要确保实例的数据只属于实例本身。
比如下面的代码:

class BrokenNonNegative(object):
    def __init__(self, default):
        self.value = http://www.mamicode.com/default"hljs-function">def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Negative value not allowed: %s" % value)
        self.value = http://www.mamicode.com/value"hljs-class">class Foo(object):
    bar = BrokenNonNegative(5) 

f = Foo()
try:
    f.bar = -1
except ValueError:
    print "Caught the invalid assignment"

Caught the invalid assignment

这么做看起来似乎能正常工作。但这里的问题就在于所有Foo的实例都共享相同的bar,这会产生一些令人痛苦的结果:

class Foo(object):
    bar = BrokenNonNegative(5) 

f = Foo()
g = Foo()

print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)
print "Setting f.bar to 10"
f.bar = 10
print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)  #ouch
f.bar is 5
g.bar is 5
Setting f.bar to 10
f.bar is 10
g.bar is 10

这就是为什么我们要在NonNegative中使用数据字典的原因。_get__set_的第一个参数告诉我们需要关心哪一个实例。NonNegative使用这个参数作为字典的key,为每一个Foo实例单独保存一份数据。

class Foo(object):
    bar = NonNegative(5)

f = Foo()
g = Foo()
print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)
print "Setting f.bar to 10"
f.bar = 10
print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)  #better
f.bar is 5
g.bar is 5
Setting f.bar to 10
f.bar is 10
g.bar is 5

这就是描述符最令人感到别扭的地方(坦白的说,我不理解为什么Python不让你在实例的层次上定义描述符,并且总是需要将实际的处理分发给_get__set_。这么做行不通一定是有原因的)

descriptor使用例子

源码文件testriyu.py

class Desc:
    def __get__(self, ins, cls):
        print(‘self in Desc: %s ‘ % self )
        print(self, ins, cls)#当前Desc的实例,ins值是拥有属性的对象,即拥有它的对象。
        #要注意的是,如果是直接用类访问descriptor(别嫌啰嗦,descriptor是个属性,直接用类访问descriptor就是直接用类访问类的属性),ins的值是None。
        #cls是ins的类型,如果直接通过类访问descriptor,ins是None,此时cls就是类本身。
class Test:
    x = Desc()
    def prt(self):
        print(‘self in Test: %s‘ % self)
t = Test()
t.prt()
t.x

self指的是当前类(即Desc)的实例。ins值是拥有属性的对象。描述符descriptor是对象的稍微有点特殊的属性,这里的ins就是拥有它的对象,要注意的是,如果是直接用类访问descriptor(注意,descriptor是个属性,直接用类访问descriptor就是直接用类访问类的属性),ins的值是None。cls是ins的类型,如果直接通过类访问descriptor,ins是None,此时cls就是类本身。

打印结果:
技术分享
在描述符类中,self指的是描述符类的实例,所以第一行的结果,没有疑问;第二行
为什么在Desc类中定义的self不是应该是调用它的实例t吗?怎么变成了Desc类的实例了呢?
这里调用的是t.x,也就是说是Test类的实例t的属性x,由于实例t中并没有定义属性x,所以找到了类属性x,而该属性是描述符属性,为Desc类的实例而已,所以此处并没有调用Test的任何方法。所以,出现了第二和第三行的打印内容。
其中第二行是由于t.x获取实例t的属性,就会调用get函数,先执行 print(‘self in Desc: %s ’ % self )语句,此时的self为实例,所以结果为
self in Desc: <testriyu.Desc object at 0x000000000337A320>
再执行print(self, ins, cls),此时的self是Desc的实例,ins是拥有x属性的对象,所以为testriyu.Test object,cls为ins的类型,
所以打印结果为
<testriyu.Desc object at 0x000000000337A320> <testriyu.Test object at 0x000000000337A2E8> <class ‘testriyu.Test’>
把t.x改为Test.x的运行结果如下:
技术分享

前两条结果和上面是一致的。
第三条结果不同。
<testriyu.Desc object at 0x000000000123ABE0> None <class ‘testriyu.Test’>
由于在很多时候描述符类中仍然需要知道调用该描述符的实例是谁,所以在描述符类中存在第二个参数ins,用来表示调用它的类实例,所以t.x时可以看到第三行中的运行结果中第二项为None,这是因为Test.x是直接通过类来进行调用。由于没有实例,所以返回None。
[未完,待补充!!!!!]

参考:
http://www.geekfan.net/7862/

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    python学习笔记-类的descriptor