Python中的property与描述符

来源:互联网 发布:matlab 读txt文件数据 编辑:程序博客网 时间:2024/05/18 14:42

在给出描述符的定义之前,我们首先介绍一下描述符的应用场景:

首先我们设想正在编写某个管理电影信息的类(class Movie), 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

我们可以看到,在 init 方法中,我们建立了大量的对象属性。这些属性有的从含以上仅支持字符串,有的则仅支持属于某一个特定取值范围的数值。

可是,在其他的用户或者程序使用我们的Movie类的时候,他们可能完全不去考虑这些规则。例如某个用户可以对某个实例的budget属性赋值-999,一旦出现了这种情况,我们可能希望Moive类的实例可以禁止相关操作并对用户做出提示“不要为这个属性赋上负值”。

那我们利用仅有的oop知识,完全可以这样设计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类中的bugdet属性的位置添加了一个条件判断。但是这样的改进并不能满足我们的需求。因为如下的代码这这种设计下仍然合法,但是我们需求恰恰是禁止这类使用方法:

>>> s=Movie(1,1,1,1,1)>>> s<__main__.Movie object at 0x0319C7B0>>>> s.budget=-999>>> 

其实分析上面的设计,不难看出,我们的改进只能确保对象在被创建时不能将budget设置为负值:

>>> s=Movie(1,1,1,-999,1)Traceback (most recent call last):  File "<pyshell#6>", line 1, in <module>    s=Movie(1,1,1,-999,1)  File "<pyshell#1>", line 8, in __init__    raise ValueError("Negative value not allowed: %s" % budget)ValueError: Negative value not allowed: -999>>> 

为了真正实现我们的需求,我们就要使用到属性(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

我们首先利用property修饰器修饰了budget方法,这相当于为Movie的budget属性建立了一个配套的getter方法,随后我们利用budget.setter修饰器修饰了另一个budget方法,作为我们的setter。这样,当用户或者程序访问某个实例的budget属性时,将会直接调用property修饰的budget,而当用户或者程序想要为budget赋值时,则会调用budget.setter方法。

>>> s=Movie(1,1,1,1,1)>>> s<__main__.Movie object at 0x032BE4D0>>>> s.budget1>>> s.budget=-999Traceback (most recent call last):  File "<pyshell#13>", line 1, in <module>    s.budget=-999  File "<pyshell#8>", line 18, in budget    raise ValueError("Negative value not allowed: %s" % value)ValueError: Negative value not allowed: -999>>> 

这样,我们就实现了利用用户自定义代码实现了对变量访问权限的操作。

但是,倘若我们想对Movie类的所有属性进行这样的改进呢?很遗憾,若仅仅使用property修饰器,我们只能手动地对每一个属性进行相关的修改。这样我们的描述符(descriptor)就派上用场了:

from weakref import WeakKeyDictionaryclass 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

我们首先从内建库weakref中调用WeakKeyDictionary,在这里可以仅仅将之视为一个字典。然后观察NonNegative类,它除了init方法之外仅仅具有get以及set方法。

例如,在https://docs.python.org/3/howto/descriptor.html中就这样提到:

描述符是是一种带有绑定行为的对象属性,但是它的对象属性的接口(访问对象值、为对象赋值)都已经被新的 get(), set(), 和delete()方法覆盖掉了(这三个方法属于描述符协议)。这样如果某个一对象定义了这三个方法或者其中的某几个,那么它就是一个描述符。

所以上面的NonNegative类就是一个描述符,这里我们先不讨论NonNegative的内部机理。直接看描述符是如何被应用的:

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

我们首先在 类层次(注意在这里必须是类层次,不能是实例层次),为Movie类的每一个属性创立一个对应的NonNegative对象,之后将他们直接作为Movie类的属性。这样就用十分简洁的方法为每一个属性构建了合理的访问权限控制。(可以自行尝试一下,现在每一个属性都具有上面budget的特性了)

下面我们额外讨论一下NonNegative对象的作用机理:
我们可以看到NonNegative的data属性是一个WeakKeyDictionary,我们不妨将它看作是一个字典。从NonNegative的set方法来看,每次在进行赋值时都会在data维护的字典内建立一个键值对。

你可能想这样设计NonNegative类,注意在BrokenNonNegative类中我们完全没有使用字典类型:

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 = valueclass Foo(object):    bar = BrokenNonNegative(5) 

但是这样实现有一个很严重的问题,这种实现下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 = 10print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar)  #ouchf.bar is 5g.bar is 5Setting f.bar to 10f.bar is 10g.bar is 10

可见,在创建第二个实例之后,第二个实例的bar值会覆盖掉第一个实例的bar值。但是,若仅仅将Foo类中的bar变量赋一个不可变对象(例如浮点数)。那么完全不会出现:”对某一个实例属性的修改会污染到其他实例乃至类的对应属性”这样及其严重的问题。

这里可能的原因是,由于BrokenNonNegative的实例是建立在类层次上的,并将其赋值给bar。当用户定义该类的一个实例时,实例中的bar变量仅仅只是类中创建的BrokenNonNegative(5) 的一个额外的引用,或者说从BrokenNonNegative类到对应的实例,python仅仅进行了一次bar的浅拷贝。所以,从某一个实例对其属性bar进行修改,就相当于在对类中的bar进行修改。这样就污染到了全局。

进一步深入NonNegative类的机理——可变对象使用NonNegative
我们这次从list类型直接继承:

>>> class MyMistake(list):        x=NonNegative(5)>>> >>> m=MyMistake()>>> m.xTraceback (most recent call last):  File "<pyshell#46>", line 1, in <module>    m.x  File "<pyshell#40>", line 11, in __get__    return self.data.get(instance, self.default)  File "D:\Program File\Python27\lib\weakref.py", line 358, in get    return self.data.get(ref(key),default)TypeError: unhashable type: 'MyMistake'>>>

随后尝试访问实例m的x属性,报错。这是因为list是unhashable类型,其子类型也具有这个性质。而在描述符的set方法中,我们要将实例直接作为字典的键,但是python要求字典的键hashable。

遗憾的是,解决此类问题大多数人采用了一种比较脆弱的方法:

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] = valueclass Foo(list):    x = Descriptor('x')    y = Descriptor('y')f = Foo()f.x = 5print f.x__set____get__ [] <class '__main__.Foo'>5

这种方法依赖于Python的方法解析顺序(即,MRO)。我们给Foo中的每个描述符加上一个标签名,名称和我们赋值给描述符的变量名相同,比如x = Descriptor(‘x’)。之后,描述符将特定于实例的数据保存在f.dict中。

这个字典条目通常是当我们请求f.x时Python给出的返回值。然而,由于Foo.x 是一个描述符,Python不能正常的使用f.dict[‘x’],但是描述符可以安全的在这里存储数据。只是要记住,不要在别的地方也给这个描述符添加标签。

之所以这里依赖了MRO,应该是说:我们在python中访问一个对象的属性时,常常直接输入obj.attr,这样的方法等同于obj.dict[‘attr’]。但是作为描述符对象x(做f.x这种访问操作),它已经有自己的访问方法(get方法)了,所以在访问x时会优先调用Descriptor类的方法,而不会优先调用python提供的标准方法。

0 0
原创粉丝点击