python学习笔记-类的descriptor
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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=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 = 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)#这里所建立的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 = 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
这就是描述符最令人感到别扭的地方(坦白的说,我不理解为什么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。
[未完,待补充!!!!!]
以上是关于python学习笔记-类的descriptor的主要内容,如果未能解决你的问题,请参考以下文章