Python描写叙述符(descriptor)解密

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python描写叙述符(descriptor)解密相关的知识,希望对你有一定的参考价值。

Python中包括了很多内建的语言特性,它们使得代码简洁且易于理解。这些特性包括列表/集合/字典推导式,属性(property)、以及装饰器(decorator)。对于大部分特性来说,这些“中级”的语言特性有着完好的文档。而且易于学习。

可是这里有个例外,那就是描写叙述符。

至少对于我来说。描写叙述符是Python语言核心中困扰我时间最长的一个特性。

这里有几点原因例如以下:

  1. 有关描写叙述符的官方文档相当难懂,并且没有包括优秀的演示样例告诉你为什么须要编写描写叙述符(我得为Raymond Hettinger辩护一下。他写的其它主题的Python文章和视频对我的帮助还是非常大的)
  2. 编写描写叙述符的语法显得有些怪异
  3. 自己定义描写叙述符可能是Python中用的最少的特性,因此你非常难在开源项目中找到优秀的演示样例

可是一旦你理解了之后,描写叙述符的确还是有它的应用价值的。

这篇文章告诉你描写叙述符能够用来做什么,以及为什么应该引起你的注意。

一句话概括:描写叙述符就是可重用的属性

在这里我要告诉你:从根本上讲。描写叙述符就是能够反复使用的属性。

也就是说,描写叙述符能够让你编写这种代码:

而在解释器运行上述代码时,当发现你试图訪问属性(b = f.bar)、对属性赋值(f.bar = c)或者删除一个实例变量的属性(del f.bar)时,就会去调用自己定义的方法。

让我们先来解释一下为什么把对函数的调用伪装成对属性的訪问是大有优点的。

property——把函数调用伪装成对属性的訪问

想象一下你正在编写管理电影信息的代码。你最后写好的Movie类可能看上去是这种:

你開始在项目的其它地方使用这个类,可是之后你意识到:假设不小心给电影打了负分怎么办?你认为这是错误的行为,希望Movie类能够阻止这个错误。

你首先想到的办法是将Movie类改动为这样:

但这行不通。由于其它部分的代码都是直接通过Movie.budget来赋值的——这个新改动的类仅仅会在__init__方法中捕获错误的数据,但对于已经存在的类实例就无能为力了。

假设有人试着执行m.budget = -100,那么谁也没法阻止。作为一个Python程序猿同一时候也是电影迷,你该怎么办?

幸运的是。Python的property攻克了这个问题。

假设你从未见过property的使用方法。以下是一个演示样例:

我们用@property装饰器指定了一个getter方法,用@budget.setter装饰器指定了一个setter方法。

当我们这么做时,每当有人试着訪问budget属性。Python就会自己主动调用对应的getter/setter方法。例如说,当遇到m.budget = value这种代码时就会自己主动调用budget.setter。

花点时间来赞赏一下Python这么做是多么的优雅:假设没有property,我们将不得不把全部的实例属性隐藏起来。提供大量显式的类似get_budget和set_budget方法。像这样编写类的话。使用起来就会不断的去调用这些getter/setter方法。这看起来就像臃肿的Java代码一样。更糟的是。假设我们不採用这种编码风格,直接对实例属性进行訪问。那么稍后就没法以清晰的方式添加对非负数的条件检查——我们不得不又一次创建set_budget方法,然后搜索整个project中的源码,将m.budget = value这种代码替换为m.set_budget(value)。太蛋疼了!!

因此,property让我们将自己定义的代码同变量的訪问/设定联系在了一起,同一时候为你的类保持一个简单的訪问属性的接口。干得美丽!

property的不足

对property来说。最大的缺点就是它们不能反复使用。举个样例。如果你想为rating,runtime和gross这些字段也加入非负检查。以下是改动过的新类:

能够看到代码添加了不少。但反复的逻辑也出现了不少。尽管property能够让类从外部看起来接口整洁美丽。可是却做不到内部相同整洁美丽。

描写叙述符登场(终于的大杀器)

这就是描写叙述符所解决的问题。

描写叙述符是property的升级版。同意你为反复的property逻辑编写单独的类来处理。

以下的演示样例展示了描写叙述符是怎样工作的(如今还不必操心NonNegative类的实现):

这里引入了一些新的语法,我们一条条的来看:

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类是怎样工作的了。每一个NonNegative的实例都维护着一个字典,当中保存着全部者实例和相应数据的映射关系。

当我们调用m.budget时,__get__方法会查找与m相关联的数据,并返回这个结果(假设这个值不存在,则会返回一个默认值)。__set__採用的方式同样。可是这里会包括额外的非负检查。

我们使用WeakKeyDictionary来代替普通的字典以防止内存泄露——我们可不想只由于它在描写叙述符的字典中就让一个无用?的实例一直存活着。

使用描写叙述符会有一点别扭。由于它们作用于类的层次上,每个类实例都共享同一个描写叙述符。

这就意味着对不同的实例对象而言,描写叙述符不得不手动地管理?不同的状态,同一时候须要显式的将类实例作为第一个參数准确传递给__get__、__set__以及__delete__方法。

我希望这个样例解释清楚了描写叙述符能够用来做什么——它们提供了一种方法将property的逻辑隔离到单独的类中来处理。假设你发现自己正在不同的property之间反复着同样的逻辑。那么本文或许会成为一个线索供你思考为何用描写叙述符重构代码是值得一试的。

秘诀和陷阱

把描写叙述符放在类的层次上(class level)

为了让描写叙述符可以正常工作。它们必须定义在类的层次上。

假设你不这么做,那么Python无法自己主动为你调用__get__和__set__方法。

能够看到,訪问类层次上的描写叙述符y能够自己主动调用__get__。

可是訪问实例层次上的描写叙述符x仅仅会返回描写叙述符本身。真是魔法一般的存在啊。

确保实例的数据仅仅属于实例本身 

你可能会像这样编写NonNegative描写叙述符:

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

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

这就是描写叙述符最令人感到别扭的地方(坦白的说。我不理解为什么Python不让你在实例的层次上定义描写叙述符。而且总是须要将实际的处理分发给__get__和__set__。

这么做行不通一定是有原因的)

注意不可哈希的描写叙述符全部者

NonNegative类使用了一个字典来单独保存专属于实例的数据。这个一般来说是没问题的,除非你用到了不可哈希(unhashable)的对象:

由于MoProblems的实例(list的子类)是不可哈希的,因此它们不能为MoProblems.x用做数据字典的key。有一些方法能够规避这个问题,可是都不完美。最好的方法可能就是给你的描写叙述符加标签了。

这样的方法依赖于Python的方法解析顺序(即,MRO)。

我们给Foo中的每一个描写叙述符加上一个标签名。名称和我们赋值给描写叙述符的变量名同样,比方x = Descriptor(‘x’)。

之后。描写叙述符将特定于实例的数据保存在f.__dict__[‘x‘]中。这个字典条目一般是当我们请求f.x时Python给出的返回值。然而,因为Foo.x 是一个描写叙述符,Python不能正常的使用f.__dict__[‘x’]。可是描写叙述符能够安全的在这里存储数据。

仅仅是要记住,不要在别的地方也给这个描写叙述符加入标签。

我不喜欢这种方式,由于这种代码非常脆弱也有非常多微妙之处。但这种方法的确非常普遍。能够用在不可哈希的全部者类上。David Beazley在他的中用到了这种方法。

在元类中使用带标签的描写叙述符

因为描写叙述符的标签名和赋给它的变量名同样。所以有人使用元类来自己主动处理这个簿记(bookkeeping)任务。

我不会去解释有关元类的细节——參考文献中David Beazley已经在他的文章中解释的非常清楚了。 须要指出的是元类自己主动的为描写叙述符加入标签。而且和赋给描写叙述符的变量名字相匹配。

虽然这样攻克了描写叙述符的标签和变量名不一致的问题。可是却引入了复杂的元类。

虽然我非常怀疑,可是你能够自行推断这么做是否值得。

訪问描写叙述符的方法

描写叙述符不过类。或许你想要为它们添加一些方法。举个样例,描写叙述符是一个用来回调property的非常好的手段。比方我们想要一个类的某个部分的状态发生变化时就立马通知我们。以下的大部分代码是用来做这个的:

这是一个非常有吸引力的模式——我们能够自己定义回调函数用来响应一个类中的状态变化,并且全然无需改动这个类的代码。这样做可真是替人分忧解难呀。如今,我们所要做的就是调用ba.balance.add_callback(ba, low_balance_warning)。以使得每次balance变化时low_balance_warning都会被调用。

可是我们是怎样做到的呢?当我们试图訪问它们时,描写叙述符总是会调用__get__。就好像add_callback方法是无法触及的一样!事实上关键在于利用了一种特殊的情况,即,当从类的层次訪问时,__get__方法的第一个參数是None。

结语

希望你如今对描写叙述符是什么和它们的适用场景有了一个认识。前进吧骚年!

Python 内置装饰器

Python内置的装饰器有三个:staticmethod、classmethod和property,起作用分别为:把类中定义的实例方法变成静态方法、类方法和属性方法。

@staticmethod

使用staticmethod装饰类的方法后,能够使用c.f()或者c().f()去调用。不须要传入self。

@classmethod

须要传入类对象,能够使用c.f()或者c().f()去调用。并将该class对象(不是class的实例对象)隐式地当作第一个參数传入。

staticmethod和classmethod的差别

staticmethod,classmethod相当于全局方法,一般用在抽象类或父类中。

一般与详细的类无关。类方法须要额外的类变量cls,当有子类继承时,调用类方法传入的类变量cls是子类,而不是父类。类方法和静态方法都能够通过类对象和类的实例对象訪问定义方式,传入的參数,调用方式都不同样。

@property

把函数方法变成属性,下面是经典样例:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        """I‘m the ‘x‘ property."""
        return self._x

    @x.setter
    def x(self, value):
        self._x = value

    @x.deleter
    def x(self):
        del self._x


if __name__ == "__main__":
    c = C()
    print c.x
    c.x = 1
    print c.x
    del c.x
    print c.x

參考文献



原文链接: Chris Beaumont 翻译: 极客范 -慕容老匹夫


以上是关于Python描写叙述符(descriptor)解密的主要内容,如果未能解决你的问题,请参考以下文章

Python描述符(descriptor)解密(转)

Linux 文件描写叙述符设置为非堵塞的方法

openssl之BIO系列之12---文件描写叙述符(fd)类型BIO

unix环境高级编程——文件i/o

每天进步一点点——Linux中的文件描写叙述符与打开文件之间的关系

多路I/O转接之select模型