Python中包含了许多内建的语言特性,它们使得代码简洁且易于理解。这些特性包括列表/集合/字典推导式,属性(property)、以及装饰器(decorator)。对于大部分特性来说,这些“中级”的语言特性有着完善的文档,并且易于学习。
但是这里有个例外,那就是描述符。至少对于我来说,描述符是Python语言核心中困扰我时间最长的一个特性。这里有几点原因如下:
1.有关描述符的官方文档相当难懂,而且没有包含优秀的示例告诉你为什么需要编写描述符(我得为Raymond Hettinger辩护一下,他写的其他主题的Python文章和视频对我的帮助还是非常大的)
2.编写描述符的语法显得有些怪异
3.自定义描述符可能是Python中用的最少的特性,因此你很难在开源项目中找到优秀的示例
但是一旦你理解了之后,描述符的确还是有它的应用价值的。这篇文章告诉你描述符可以用来做什么,以及为什么应该引起你的注意。
一句话概括:描述符就是可重用的属性
在这里我要告诉你:从根本上讲,描述符就是可以重复使用的属性。也就是说,描述符可以让你编写这样的代码:
f = Foo() 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可以让类从外部看起来接口整洁漂亮,但是却做不到内部同样整洁漂亮。
描述符登场(最终的大杀器)
这就是描述符所解决的问题。描述符是property的升级版,允许你为重复的property逻辑编写单独的类来处理。下面的示例展示了描述符是如何工作的(现在还不必担心NonNegative类的实现):
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" 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类是如何工作的了。每个NonNegative的实例都维护着一个字典,其中保存着所有者实例和对应数据的映射关系。当我们调用m.budget时,__get__方法会查找与m相关联的数据,并返回这个结果(如果这个值不存在,则会返回一个默认值)。__set__采用的方式相同,但是这里会包含额外的非负检查。我们使用WeakKeyDictionary来取代普通的字典以防止内存泄露——我们可不想仅仅因为它在描述符的字典中就让一个无用 的实例一直存活着。
使用描述符会有一点别扭。因为它们作用于类的层次上,每一个类实例都共享同一个描述符。这就意味着对不同的实例对象而言,描述符不得不手动地管理不同的状态,同时需要显式的将类实例作为第一个参数准确传递给__get__、__set__以及__delete__方法。
我希望这个例子解释清楚了描述符可以用来做什么——它们提供了一种方法将property的逻辑隔离到单独的类中来处理。如果你发现自己正在不同的property之间重复着相同的逻辑,那么本文也许会成为一个线索供你思考为何用描述符重构代码是值得一试的。
秘诀和陷阱
把描述符放在类的层次上(class level)
为了让描述符能够正常工作,它们必须定义在类的层次上。如果你不这么做,那么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
确保实例的数据只属于实例本身
你可能会像这样编写NonNegative描述符:
class BrokenNonNegative(object): def __init__(self, default): self.value = default 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 = value 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
注意不可哈希的描述符所有者
NonNegative类使用了一个字典来单独保存专属于实例的数据。这个一般来说是没问题的,除非你用到了不可哈希(unhashable)的对象:
class MoProblems(list): #you can't use lists as dictionary keys x = NonNegative(5) m = MoProblems() print m.x # womp womp --------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-8-dd73b177bd8d> in <module>() 3 4 m = MoProblems() ----> 5 print m.x # womp womp <ipython-input-3-6671804ce5d5> in __get__(self, instance, owner) 9 # instance = x 10 # owner = type(x) ---> 11 return self.data.get(instance, self.default) 12 13 def __set__(self, instance, value): TypeError: unhashable type: 'MoProblems'
class Descriptor(object): def __init__(self, label): self.label = label def __get__(self, instance, owner): print '__get__', instance, owner return instance.__dict__.get(self.label) def __set__(self, instance, value): print '__set__' instance.__dict__[self.label] = value class Foo(list): x = Descriptor('x') y = Descriptor('y') f = Foo() f.x = 5 print f.x __set__ __get__ [] <class '__main__.Foo'> 5
这种方法依赖于Python的方法解析顺序(即,MRO)。我们给Foo中的每个描述符加上一个标签名,名称和我们赋值给描述符的变量名相同,比如x = Descriptor(‘x')。之后,描述符将特定于实例的数据保存在f.__dict__['x']中。这个字典条目通常是当我们请求f.x时Python给出的返回值。然而,由于Foo.x 是一个描述符,Python不能正常的使用f.__dict__[‘x'],但是描述符可以安全的在这里存储数据。只是要记住,不要在别的地方也给这个描述符添加标签。
class Foo(object): x = Descriptor('y') f = Foo() f.x = 5 print f.x f.y = 4 #oh no! print f.x __set__ __get__ <__main__.Foo object at 0x10432c810> <class '__main__.Foo'> 5 __get__ <__main__.Foo object at 0x10432c810> <class '__main__.Foo'> 4
在元类中使用带标签的描述符
由于描述符的标签名和赋给它的变量名相同,所以有人使用元类来自动处理这个簿记(bookkeeping)任务。
class Descriptor(object): def __init__(self): #notice we aren't setting the label here self.label = None def __get__(self, instance, owner): print '__get__. Label = %s' % self.label return instance.__dict__.get(self.label, None) def __set__(self, instance, value): print '__set__' instance.__dict__[self.label] = value class DescriptorOwner(type): def __new__(cls, name, bases, attrs): # find all descriptors, auto-set their labels for n, v in attrs.items(): if isinstance(v, Descriptor): v.label = n return super(DescriptorOwner, cls).__new__(cls, name, bases, attrs) class Foo(object): __metaclass__ = DescriptorOwner x = Descriptor() f = Foo() f.x = 10 print f.x __set__ __get__. Label = x 10
尽管这样解决了描述符的标签和变量名不一致的问题,但是却引入了复杂的元类。虽然我很怀疑,但是你可以自行判断这么做是否值得。
访问描述符的方法
描述符仅仅是类,也许你想要为它们增加一些方法。举个例子,描述符是一个用来回调property的很好的手段。比如我们想要一个类的某个部分的状态发生变化时就立刻通知我们。下面的大部分代码是用来做这个的:
class CallbackProperty(object): """A property that will alert observers when upon updates""" def __init__(self, default=None): self.data = WeakKeyDictionary() self.default = default self.callbacks = WeakKeyDictionary() def __get__(self, instance, owner): return self.data.get(instance, self.default) def __set__(self, instance, value): for callback in self.callbacks.get(instance, []): # alert callback function of new value callback(value) self.data[instance] = value def add_callback(self, instance, callback): """Add a new function to call everytime the descriptor updates""" #but how do we get here?!?! if instance not in self.callbacks: self.callbacks[instance] = [] self.callbacks[instance].append(callback) class BankAccount(object): balance = CallbackProperty(0) def low_balance_warning(value): if value < 100: print "You are poor" ba = BankAccount() # will not work -- try it #ba.balance.add_callback(ba, low_balance_warning)
这是一个很有吸引力的模式——我们可以自定义回调函数用来响应一个类中的状态变化,而且完全无需修改这个类的代码。这样做可真是替人分忧解难呀。现在,我们所要做的就是调用ba.balance.add_callback(ba, low_balance_warning),以使得每次balance变化时low_balance_warning都会被调用。
但是我们是如何做到的呢?当我们试图访问它们时,描述符总是会调用__get__。就好像add_callback方法是无法触及的一样!其实关键在于利用了一种特殊的情况,即,当从类的层次访问时,__get__方法的第一个参数是None。
class CallbackProperty(object): """A property that will alert observers when upon updates""" def __init__(self, default=None): self.data = WeakKeyDictionary() self.default = default self.callbacks = WeakKeyDictionary() def __get__(self, instance, owner): if instance is None: return self return self.data.get(instance, self.default) def __set__(self, instance, value): for callback in self.callbacks.get(instance, []): # alert callback function of new value callback(value) self.data[instance] = value def add_callback(self, instance, callback): """Add a new function to call everytime the descriptor within instance updates""" if instance not in self.callbacks: self.callbacks[instance] = [] self.callbacks[instance].append(callback) class BankAccount(object): balance = CallbackProperty(0) def low_balance_warning(value): if value < 100: print "You are now poor" ba = BankAccount() BankAccount.balance.add_callback(ba, low_balance_warning) ba.balance = 5000 print "Balance is %s" % ba.balance ba.balance = 99 print "Balance is %s" % ba.balance Balance is 5000 You are now poor Balance is 99
结语
希望你现在对描述符是什么和它们的适用场景有了一个认识。前进吧骚年!
Python 中,通过使用描述符,可以让程序员在引用一个对象属性时自定义要完成的工作。 本质上看,描述符就是一个类,只不过它定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理全权委托给描述符类。 描述符是 Python 中复杂属性访问的基础,它在内部被用于实现 property、方法、类方法、静态方法和 super 类型。 描述符类基于以下 3 个特殊方法,换句话说,这 3 个方法组成
问题内容: 我试图了解什么是Python的描述符以及它们的用途。我了解它们的工作原理,但这是我的疑问。考虑以下代码: 为什么需要描述符类? 什么是和这里?(在中)。这些参数的目的是什么? 我将如何调用/使用此示例? 问题答案: 描述符是如何实现类型的。描述符仅实现,等等,然后被添加到其定义中的另一个类中(就像你上面对类所做的一样)。例如: 访问你为描述符分配的属性(celsius在上面的示例中)调
本文向大家介绍Python中的类与对象之描述符详解,包括了Python中的类与对象之描述符详解的使用技巧和注意事项,需要的朋友参考一下 描述符(Descriptors)是Python语言中一个深奥但却重要的一部分。它们广泛应用于Python语言的内核,熟练掌握描述符将会为Python程序员的工具箱添加一个额外的技巧。为了给接下来对描述符的讨论做一些铺垫,我将描述一些程序员可能会在日常编程活动中遇到
本文向大家介绍Python描述符descriptor使用原理解析,包括了Python描述符descriptor使用原理解析的使用技巧和注意事项,需要的朋友参考一下 描述符(descriptor)是实现了__get__、__set__、__del__方法的类,进一步可以细分为两类: 数据描述符:实现了__get__和__set__ 非数据描述符:没有实现__set__ 描述符在类的属性调用中起着很重
本文向大家介绍Python描述器descriptor详解,包括了Python描述器descriptor详解的使用技巧和注意事项,需要的朋友参考一下 前面说了descriptor,这个东西其实和Java的setter,getter有点像。但这个descriptor和上文中我们开始提到的函数方法这些东西有什么关系呢? 所有的函数都可以是descriptor,因为它有__get__方法。 注意,函数对
当我在Weblogic 10.3.6中部署WAR时,它会运行,但在Weblogic 10.3.0中,它会抛出以下内容: 无法加载描述符D:\bea103\user_projects\domain\SRVWinplex\Deployments\WinplexA模块WinplexA的oader2.java:768weblogic.servlet.internal.错误是escriptor.get描述符