Python描述符(descriptor)解密

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

但是这里有个例外,那就是描述符。至少对于我来说,描述符是Python语言核心中困扰我时间最长的一个特性。这里有几点原因如下:

有关描述符的官方文档相当难懂,而且没有包含优秀的示例告诉你为什么需要编写描述符(我得为Raymond Hettinger辩护一下,他写的其他主题的Python文章和视频对我的帮助还是非常大的)

编写描述符的语法显得有些怪异

自定义描述符可能是Python中用的最少的特性,因此你很难在开源项目中找到优秀的示例

但是一旦你理解了之后,描述符的确还是有它的应用价值的。这篇文章告诉你描述符可以用来做什么,以及为什么应该引起你的注意。

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

在这里我要告诉你:从根本上讲,描述符就是可以重复使用的属性。也就是说,描述符可以让你编写这样的代码:

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

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

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

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程序员同时也是电影迷,你该怎么办?

幸运的是,Python的property解决了这个问题。如果你从未见过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。

花点时间来欣赏一下Python这么做是多么的优雅:如果没有property,我们将不得不把所有的实例属性隐藏起来,提供大量显式的类似get_budget和set_budget方法。像这样编写类的话,使用起来就会不断的去调用这些getter/setter方法,这看起来就像臃肿的Java代码一样。更糟的是,如果我们不采用这种编码风格,直接对实例属性进行访问。那么稍后就没法以清晰的方式增加对非负数的条件检查——我们不得不重新创建set_budget方法,然后搜索整个工程中的源代码,将m.budget = value这样的代码替换为m.set_budget(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可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。

Python 的详细介绍请点这里
Python 的下载地址请点这里

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/3dc6797b3f7b9e623fa19b1246a9fb50.html