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

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

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

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

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

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

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

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

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

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

Python
f = Foo() b = f.bar f.bar = c del f.bar
1
2
3
4

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

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

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

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

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

Python
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
1
2
3
4
5
6
7
8
9
10

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类改动为这样:

Python
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
1
2
3
4
5
6
7
8
9
10
11
12

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的使用方法。以下是一个演示样例:

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" 964000 Woops. Not allowed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

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方法,然后搜索整个project中的源码,将m.budget = value这种代码替换为m.set_budget(value)。太蛋疼了!!

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

property的不足

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

Python
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

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类的实现):

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" 964000 Woops, negative value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

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__方法。

Python
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
1
2
3
4
5
6
7
8
9

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仅仅会返回描写叙述符本身。真是魔法一般的存在啊。

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

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

Python
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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,这会产生一些令人痛苦的结果:

Python
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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实例单独保存一份数据。

Python
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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__。

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

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

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

Python
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'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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'

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

Python
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

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’]。可是描写叙述符能够安全的在这里存储数据。

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

Python
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

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

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

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

Python
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

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

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

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

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

訪问描写叙述符的方法

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

Python
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

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。

Python
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

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内置的装饰器有三个: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@propertydef x(self):"""I'm the 'x' property."""return self._x@x.setterdef x(self, value):self._x = value@x.deleterdef x(self):del self._xif __name__ == "__main__":c = C()print c.xc.x = 1print c.xdel c.xprint c.x

參考文献

  • Encapsulation With Descriptors by Luciano Ramalho
  • Python 3 Metaprogramming by David Beazley
  • Python’s class development toolkit by Raymond Hettinger

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

Python描写叙述符(descriptor)解密相关推荐

  1. 进程与进程描写叙述符(task_struct)

    一. 进程 进程(Process) 计算机中的程序关于某数据集合上的一次执行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.在早期面向进程设计的计算机结构中,进程是程序的基本执行实体: ...

  2. 技术图文:Python描述符 (descriptor) 详解

    背景 今天在B站上学习"零基础入门学习Python"这门课程的第46讲"魔法方法:描述符",这也是我们组织的 Python基础刻意练习活动 的学习任务,其中有这 ...

  3. python描述符(descriptor)、属性(property)、函数(类)装饰器(decorator )原理实例详解

    2019独角兽企业重金招聘Python工程师标准>>> 1.前言 Python的描述符是接触到Python核心编程中一个比较难以理解的内容,自己在学习的过程中也遇到过很多的疑惑,通过 ...

  4. python打印换行符_Python换行符以及如何在不使用换行符的情况下进行Python打印

    python打印换行符 Welcome! The new line character in Python is used to mark the end of a line and the begi ...

  5. 【算法拾遗(java描写叙述)】--- 选择排序(直接选择排序、堆排序)

    选择排序的基本思想 每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,知道所有记录排序完毕.主要有两种选择排序方法:直接选择排序(或称简单选择排序)和堆排序. 直接选择排序 ...

  6. 可以考的python方面的证书-python有证书的加密解密实现方法

    本文实例讲述了python有证书的加密解密实现方法.分享给大家供大家参考.具体实现方法如下: 最近在做python的加解密工作,同时加完密的串能在php上能解出来,网上也找了一些靠谱的资料,刚好也有时 ...

  7. Python字符串逐字符或逐词反转方法

    这篇文章主要介绍了Python字符串逐字符或逐词反转方法,本文对逐字符或逐词分别给出两种方法,需要的朋友可以参考下 目的 把字符串逐字符或逐词反转过来,这个蛮有意思的. 方法 先看逐字符反转吧,第一种 ...

  8. python函数修饰符@的使用

    python函数修饰符@的作用是为现有函数增加额外的功能,常用于插入日志.性能测试.事务处理等等. 创建函数修饰符的规则: (1)修饰符是一个函数 (2)修饰符取被修饰函数为参数 (3)修饰符返回一个 ...

  9. SAP UI5 初学者教程之十 - 什么是 SAP UI5 应用的描述符 Descriptor 试读版

    一套适合 SAP UI5 初学者循序渐进的学习教程 教程目录 SAP UI5 本地开发环境的搭建 SAP UI5 初学者教程之一:Hello World SAP UI5 初学者教程之二:SAP UI5 ...

  10. * 类描写叙述:字符串工具类 类名称:String_U

    /******************************************* 类描写叙述:字符串工具类 类名称:String_U* **************************** ...

最新文章

  1. html小球跳跃技术原理,弹跳的小球.html · web-project-songyu/原生js小例子 - Gitee.com...
  2. 高并发编程-线程通信_使用wait和notify进行线程间的通信
  3. Java安全编码之用户输入
  4. 【Selenium】之谷歌、IE、火狐浏览器各个版本的浏览器驱动下载地址
  5. 在计算机系统中 外存储器必须通过,大学计算机基础第4章作业.doc
  6. Java FileWriter示例
  7. Python numpy函数:all()和any()比较矩阵
  8. [POI2001] 和平委员会 Peaceful Commission——2-sat(dfs构造字典序最小解)
  9. ubuntu安装ROBOWARE
  10. ZOJ 3429 Cube Simulation (思维题)
  11. 学习笔记12--智能驾驶安全设计案例
  12. matlab篮球队需要五名队员,球队战绩影响因素分析.doc
  13. JAVA有没有moba游戏_你了解MOBA么?——MOBA游戏类型介绍(上)
  14. Linux基础、vim、find命令等
  15. TM1637芯片驱动数码管 – play with TM1637 seven segment display driver
  16. html5账号秘密,JavaScript有关的10个秘密和怪癖
  17. 【找工作】三大运营商、航十
  18. long 雪花算法_小飞
  19. Installed Build Tools revision 32.0.0 is corrupted. Remove and install again using the SDK Manager
  20. 妈妈和四川地震伤员住院的日子-感谢广医附属第一人民医院骨科的医生护士

热门文章

  1. MathType 6.9中的字距该怎样进行调
  2. 基于vue的黑马前端项目小兔鲜
  3. PTA 7-8 求月供
  4. 增值电信业务都有哪些种类?都用于什么业务呢?
  5. Mac 连接 Appstore.报未知错误
  6. CIT 594 Module 7 Programming AssignmentCSV Slicer
  7. 莫等闲 - 做一个优秀的人
  8. ACM题---三角型周长和
  9. 【数据结构笔记二】AVL-平衡树__2-3-4树__红黑树实现(b站波哥)
  10. 决策树(Desition Tree)