zoukankan      html  css  js  c++  java
  • python中的property和描述符对象

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

    首先我们设想正在编写某个管理电影信息的类(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.budget
    1
    >>> s.budget=-999
    
    Traceback (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 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
    

    我们首先从内建库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 = value
    
    class Foo(object):
        bar = BrokenNonNegative(5) 
    

    但是这样实现有一个很严重的问题,这种实现下Foo类的所有实例的bar属性都是完全同步的:

    class Foo(object):
        bar = BrokenNonNegative(5) 
    
    f = Foo()
    g = Foo()
    
    print "f.bar is %s
    g.bar is %s" % (f.bar, g.bar)
    print "Setting f.bar to 10"
    f.bar = 10
    print "f.bar is %s
    g.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
    

    可见,在创建第二个实例之后,第二个实例的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.x
    
    Traceback (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 FilePython27libweakref.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] = value
    
    class Foo(list):
        x = Descriptor('x')
        y = Descriptor('y')
    
    f = Foo()
    f.x = 5
    print f.x
    
    __set__
    __get__ [] <class '__main__.Foo'>
    

    这种方法依赖于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提供的标准方法。

  • 相关阅读:
    2013第2周四晴
    2012第53周&2013第1周日
    2013周六雪转阴
    2013年第二周日生活整理
    php技术–php中感叹号!和双感叹号!!的用法(三元运算)
    laravel拓展validator验证
    laravel 5 自定义全局函数,怎么弄呢?
    Laravel 清空配置缓存
    网上很多laravel中cookie的使用方法。
    艾伟也谈项目管理,给敏捷团队中的架构师的10个建议 狼人:
  • 原文地址:https://www.cnblogs.com/Khannia/p/6195321.html
Copyright © 2011-2022 走看看