属性访问与属性描述符

 

属性访问:

  Python的属性访问方式很直观,使用点属性运算符。

 

__getattribute__

一、__getattribute__应用:

  在新式类中,对对象属性的访问,都会调用特殊方法__getattribute__。

  __getattribute__允许我们在访问对象属性时自定义访问行为,但是使用它特别要小心无限递归的问题。

class Animal(object):
    run = True

class Dog(Animal):

    fly = False

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getattribute__(self, item):
        print('calling  __getattribute__')
        return super(Dog, self).__getattribute__(item)

    def sound(self):
        return "Wang Wang"


dog = Dog('David',3)
print(dog.name)
print(dog.fly)
print(dog.sound)
print(dog.run)

# 结果
calling  __getattribute__
David
calling  __getattribute__
False
calling  __getattribute__
<bound method Dog.sound of <__main__.Dog object at 0x0000026E69F087B8>>
calling  __getattribute__
True

总结:

  (1)重写__getattribute__方法中不能使用对象的点运算符访问属性,否则使用点运算符访问属性时,会再次调用__getattribute__。这样就会陷入无限递归。可以使用super()方法避免这个问题。

  (2)__getattribute__是实例对象查找属性或方法的入口。实例对象访问属性或方法时都需要调用到__getattribute__,之后才会根据一定的规则在各个__dict__中查找相应的属性值或方法对象,

  (3)若没有找到则会调用__getattr__

 

__getattr__

二、__getattr__应用:

  在进行属性查找时,如果在实例跟类上都查找失败的时候,就会走到__getattr__函数上,

  如果没有定义这个函数,就会抛出AttributeError异常。

  所以,这里大概我们可以认为__getattr__方法是属性查找的最后一个关卡。

class Dog(Animal):
    fly = False

    def __init__(self, age):
        self.age = age

    def __getattribute__(self, item):
        print('calling  __getattribute__')
        return super(Dog, self).__getattribute__(item)

    def __getattr__(self, item):
        print('calling __getattr__')
        if item == 'Tom':
            return True if self.age >= 2 else False
        else:
            raise AttributeError

>>> dog = Dog(3)
>>> print(dog.age)
calling  __getattribute__
3
>>> print(dog.Tom)
calling  __getattribute__
calling __getattr__
calling  __getattribute__
True
>>> print(dog.wang)
calling  __getattribute__
calling __getattr__
Traceback (most recent call last):
  File "s2.py", line 28, in <module>
    print(dog.wang)
  File "s2.py", line 23, in __getattr__
    raise AttributeError
AttributeError

 

__setattr__

三、__setattr__应用:

  __setattr__(self, name, value),

  __setattr__方法允许你自定义某个属性的赋值行为,不管这个属性存在与否,都可以对任意属性的任何变化都定义自己的规则。

  关于__setattr__有两点需要说明:

    第一,使用它时必须小心,不能写成类似self.name = “Tom”这样的形式,因为这样的赋值语句会调用__setattr__方法,这样会让其陷入无限递归;

    第二,必须区分 对象属性 和 类属性 这两个概念。

class Dog(Animal):
    fly = False

    def __init__(self, age):
        self.age = age

    def __setattr__(self, key, value):
        print('calling __setattr__')
        return super(Dog, self).__setattr__(key, value)

>>> dog = Dog(3)       # __init__方法中有赋值行为
calling __setattr__
>>> dog.age = 8
calling __setattr__
>>> dog.fly = True
calling __setattr__
>>> print(dog.__dict__)
{'fly': True, 'age': 8}
>>> print(Dog.__dict__)   # 类属性中,fly没有发生变化
{'__doc__': None, 'fly': False, '__init__': <function Dog.__init__ at 0x000001EE648D8C80>, '__module__': '__main__', '__setattr__': <function Dog.__setattr__ at 0x000001EE648D8D08>}

  实例对象的__setattr__方法可以定义属性的赋值行为,不管属性是否存在。

  当属性存在时,它会改变其值;当属性不存在时,它会添加一个对象属性信息到对象的__dict__中,然而这并不改变类的属性。

 

__delattr__

四、__delattr__应用:

  __delattr__(self, name),

  __delattr__用于处理删除属性时的行为。和__setattr__方法一样要注意无限递归的问题,重写该方法时不要有类似del self.name的写法。

class Dog(Animal):
    fly = False

    def __init__(self, age):
        self.age = age

    def __delattr__(self, name):
        print ("calling __delattr__")
        super(Dog, self).__delattr__(name)

# 由于上面的例子中我们为dog设置了fly属性,现在删除它触发__delattr__方法
>>> del dog.fly
calling __delattr__
# 再次查看dog对象的__dict__,发现和fly属性相关的信息被删除
>>> print(dog.__dict__)
{'age': 8}

 

描述符

描述符(descriptor):

  描述符是实现了特定协议的类,这个协议包括__get__、__set__、__delete__方法。

  描述符是对多个属性运用相同存取逻辑的一种方式。

  描述符的作用是用来代理一个类的属性,需要注意的是描述符不能定义在类的构造函数中,只能定义为类的属性,它只属于类的,不属于实例,我们通过查看实例和类的字典即可知晓。

  只要类重写任何下面的一个方法,类就被看作是descriptor,当这些descriptor在另外一个类中作为属性被访问时, 就可以不去采用默认的查找属性的顺序。

  1、__get__(self, instance, owner) :获取属性时调用,返回设置的属性值,通常是set中的value,或者附加的其他组合值。

  2、__set__(self, instance, value) :设置属性时调用,返回None.

  3、__delete__(self, instance) :

  其中,instance是这个描述符属性所在的类的实例,而owner是描述符所在的类。

class RevealAccess(object):
    def __init__(self, initval=None, name='var'):
        self.val = initval
        self.name = name
    def __get__(self, obj, objtype):
        print 'Retrieving', self.name
        return self.val
    def __set__(self, obj, val):
        print 'Updating', self.name
        self.val = val
class MyClass(object):
    x = RevealAccess(10, 'var "x"')
    y = 5

  x是MyClass的类属性,作为RevealAccess的实例,它有get等方法,是一个描述符。

  RevealAccess类的实例是作为MyClass类属性x的值存在的。而且RevealAccess类定义了__get__、__set__方法,它是一个描述符对象。

  注意,描述符对象的__get__、__set__方法中使用了诸如self.val和self.val = val等语句,这些语句会调用__getattribute__、__setattr__等方法

>>> m = MyClass()
>>> m.__dict__
{}
>>> MyClass.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
            '__doc__': None,
            '__module__': '__main__',
            '__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
            'x': <__main__.RevealAccess at 0x5130080>,
            'y': 5})
>>> m.x
Retrieving var "x"
10
>>> m.x = 20
Updating var "x"
>>> m.x
Retrieving var "x"
20
>>> m.__dict__
{}
>>> MyClass.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
            '__doc__': None,
            '__module__': '__main__',
            '__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
            'x': <__main__.RevealAccess at 0x5130080>,
            'y': 5})

  (1)访问m.x。会先触发__getattribute__方法,由于x属性的值是一个描述符,会触发它的__get__方法。

  (2)设置m.x的值。对描述符进行赋值,会触发它的__set__方法,在__set__方法中还会触发__setattr__方法(self.val = val)

  (3)查看m和MyClass的__dict__,发现这与对描述符赋值之前一样。这一点与一般属性的赋值不同,可参考上述的__setattr__方法。之所以前后没有发生变化,是因为变化体现在描述符对象上,而不是实例对象m和类MyClass上。

 

两种种类的描述符:

  只要至少实现__get__、__set__、__delete__方法中的一个就可以认为是描述符;

  数据描述符(data descriptor)和非数据描述符(non-data descriptors)

  数据描述符:定义了 set 和 get方法的对象

  非数据描述符:只定义了 get 方法的对象。

 

属性访问规则

属性访问的优先规则:

  属性访问的入口点是__getattribute__方法。

  查找 b.x 这样一个属性的过程。

  (1)搜索基类列表,(type(b).mro),直到找到该属性的第一个定义,并将该属性的值赋值给descr;

  (2)判断descr的类型。它的类型分为:数据描述符、非数据描述符、普通属性、未找到等类型。

    若descr为数据描述符,则调用desc.__get__(b,type(b)),并将结果返回,结束执行。否则进行下一步

    (3)若descr为非数据描述符、普通属性、未找到等类型,则查找实例b的实例属性,b.__dict__['x'],找到返回结果。否则进行下一步

      (4)如果在b.__dict__未找到相关属性,则重新回到descr值的判断上。

        【①】若descr为非数据描述符,则调用desc.__get__(b, type(b)),并将结果返回,结束执行;

        【②】若descr为普通属性,直接返回结果并结束执行;

        【③】若descr为空(未找到),则最终抛出 AttributeError 异常,结束查找。

 

延迟初始化

延迟初始化(lazy property)

Python对象的延迟初始化是指,当它第一次被创建时才进行初始化,或者保存第一次创建的结果,然后每次调用的时候直接返回结果。

延迟初始化主要是用于提高性能,避免浪费计算,并减少程序的内存需求。

class lazy(object):
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, cls):
        val = self.func(instance)
        setattr(instance, self.func.__name__, val)
        return val


class Circle(object):
    def __init__(self, radius):
        self.radius = radius

    @lazy
    def area(self):
        print('evalute')
        return 3.14 * self.radius ** 2

c = Circle(4)
print(c.radius)
print(c.area)
print(c.area)

  可以发现evalute只输出一次。

  在lazy类里面,因为定义了__get__()方法,所以它是一个描述符。当第一次执行c.area时,python解释器会先从 c._ditc__中查找,没有找到就会去Circle.__dict__中进行查找,这个时候因为area被定义为描述符,所以调用__get__方法。

  上面已经铺垫过def __get__(self, instance, cls)里面三个参数的代表什么,所以很明了val = self.func(instance) ,是执行了area方法,并返回结果,最后setattr完成了赋值操作。

  这样相当于设置c.__dict__['area'] = val

  当我们再次调用c.area时,直接从c.__dict__中进行查找,这时就会直接返回之前计算好的值了。

 

使用描述符类避免重复编写读值方法和设值方法:

class Quantity:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __set__(self, instance, value):
        if value > 0:
            instance.__dict__[self.storage_name] = value
        else:
            raise ValueError('Value must be > 0')


class LineItem:

    weight = Quantity('weight')
    price = Quantity('price')

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

 

解决重复输入属性的名称:

class Quantity:
    __counter = 0

    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix,index)
        cls.__counter += 1

    def __get__(self, instance, owner):
        return getattr(instance, self.storage_name)

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self.storage_name, value)
        else:
            raise ValueError('Value must be > 0')

class LineItem:
    weight = Quantity()
    price = Quantity()

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

  通过LineItem.weight从类中获取托管属性时,描述符的__get__方法接受到的instance参数值是None,因此抛出AttributeError异常。

修改上述代码:

def __get__(self, instance, owner):
    if instance is None:
        return self
    else:
        return getattr(instance, self.storage_name)

  Django模型的字段就是描述符。

  如果想自动把存储属性的名称设成与托管属性的名称类似,需要用到类装饰器或元类。

 

描述符的应用

描述符的应用:管理数据属性。

  AutoStorage基类负责自动存储属性;Validated类做验证;把职责委托给抽象方法validate;

  Quantity和NonBlank是validated的具体子类。

import abc

class AutoStorage:
    __counter = 0

    def __init__(self):

        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter

        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)

    def __set__(self, instance, value):
        setattr(instance, self.storage_name, value)

class Validated(abc.ABC, AutoStorage):

    def __set__(self, instance, value):
        value = self.validated(instance, value)
        super().__set__(instance, value)

    @abc.abstractmethod
    def validated(self,instance,value):
        """ return validate value or raise ValueError"""

class Quantity(Validated):
    """ a number greater than zero """

    def validated(self,instance,value):
        if value <= 0:
            raise ValueError('Value must be > 0')
        return value

class NonBlank(Validated):
    """ a string with at least one non-space character """

    def validated(self,instance,value):
        value = value.strip()
        if len(value) == 0:
            raise ValueError('value cannot be empty or blank')
        return value

  用户只需要知道,他们可以使用Quantity和NonBlank自动验证实例属性。

class LineItem:
    
    description = model.NonBlank()
    weight = model.Quantity()
    price = model.Quantity()
    
    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price
        
    def subtotal(self):
        return self.weight * self.price

 

覆盖型与非覆盖型描述符的对比:

### 辅助函数,仅用于显示 ###
def cls_name(obj_or_cls):
    cls = type(obj_or_cls)
    if cls is type:
        cls = obj_or_cls
    return cls.__name__.split('.')[-1]

def display(obj):
    cls = type(obj)
    if cls is type:
        return '<class {}>'.format(obj.__name__)
    elif cls in [type(None), int]:
        return repr(obj)
    else:
        return '<{} object>'.format(cls_name(obj))

def print_args(name, *args):
    pseudo_args = ', '.join(display(x) for x in args)
    print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))
辅助函数,仅用于显示
class Overriding:
    '''也称数据描述符或强制描述符'''

    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)

    def __set__(self, instance, value):
        print_args('set', self, instance, value)

class OverridingNoGet:
    '''没有``__get__``方法的覆盖型描述符'''

    def __set__(self, instance, value):
        print_args('set', self, instance, value)

class NonOverriding:
    '''也称非数据描述符或遮盖型描述符'''
    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)

class Managed:
    over = Overriding()
    over_no_get = OverridingNoGet()
    non_over = NonOverriding()

    def spam(self):
        print('-> Managed.spam({})'.format(display(self)))

 

一、覆盖型描述符:

  实现__set__方法的描述符属于 覆盖型描述符,虽然描述符是类属性,但是实现__set__方法的话,会覆盖对实例属性的赋值操作。

>>> obj = Managed()
>>> obj.over
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
>>> Managed.over
-> Overriding.__get__(<Overriding object>, None, <class Managed>)
>>> obj.over = 7
-> Overriding.__set__(<Overriding object>, <Managed object>, 7)
>>> obj.over
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
>>> obj.__dict__['over'] = 8
>>> vars(obj)
{'over': 8}
>>> obj.over
-> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

  (1)Managed.over触发描述符的__get__方法,第二个参数(instance)的值是None。

  (2)跳过描述符,直接通过obj.__dict__属性设值。

 

二、没有__get__方法的覆盖型描述符:

  通过实例读取描述符会返回描述符对象本身。因为没有处理读操作的__get__方法。

  如果直接通过实例的__dict__属性创建一个同名的实例属性,以后再设置那个属性时,仍会由__set__方法插手接管。

  但是读取那个属性的话,就会直接从实例中返回新赋予的值,而不会返回描述符对象。

  实例属性会遮盖描述符。

>>> obj.over_no_get
<__main__.OverridingNoGet object at 0x665bcc>
>>> Managed.over_no_get
<__main__.OverridingNoGet object at 0x665bcc>
>>> obj.over_no_get = 7
-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)
>>> obj.over_no_get
<__main__.OverridingNoGet object at 0x665bcc>
>>> obj.__dict__['over_no_get'] = 9
>>> obj.over_no_get
9
>>> obj.over_no_get = 7
-> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)
>>> obj.over_no_get

 

三、非覆盖型描述符:

  没有实现__set__方法的描述符是非覆盖型描述符。

  如果设置了同名的实例属性,描述符会被遮盖,致使描述符无法处理那个实例的那个属性。

  方法是以非覆盖型描述符实现的。

>>> obj = Managed()
>>> obj.non_over
-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)
>>> obj.non_over = 7
>>> obj.non_over
7
>>> Managed.non_over
-> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)
>>> del obj.non_over
>>> obj.non_over
-> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)

 

四、在类中覆盖描述符:

  不管描述符是不是覆盖型,为类属性赋值都能覆盖描述符。

  这是一种猴子补丁技术。

>>> obj = Managed()
>>> Managed.over = 1
>>> Managed.over_no_get = 2
>>> Managed.non_over = 3
>>> obj.over, obj.over_no_get, obj.non_over
(1, 2, 3)

揭示了读写属性的一种不对等:

  ▲ 读类属性的操作可以由依附在托管类上定义有__get__方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有__set__方法的描述符处理。

  ▲ 若想控制设置类属性的操作,要把描述符依附在类的类上,即依附在元类上。

  ▲ 默认情况下,用户定义的类来说,元类是type,而我们不能为type添加属性,但可以自己创建元类。

 

方法是描述符

方法是描述符:

  在类中定义的函数属于 绑定方法(bound method),因为用户定义的函数都有__get__方法,所以依附到类上时,就相当于描述符。

>>> obj = Managed()
>>> obj.spam
<bound method Managed.spam of <descriptorkinds.Managed object at 0x74c80c>>
>>> Managed.spam
<function Managed.spam at 0x734734>
>>> obj.spam = 7
>>> obj.spam
7

  (1)obj.spam获取的是绑定方法对象 method。

  (2)但是Managed.spam获取的是函数 function。

class Foo:

    def __init__(self, name):
        self.name = name

    def run(self):
        print('run')
        return

>>> f = Foo('Tom')
>>> f
<__main__.Foo object at 0x000001227C077588>
>>> type(f.run),type(Foo.run)
(<class 'method'>, <class 'function'>)
>>> Foo.run.__get__(f)
<bound method Foo.run of <__main__.Foo object at 0x000001227C077588>>
>>> Foo.run.__get__(None,Foo)
<function Foo.run at 0x000001227C066D90>
>>> f.run
<bound method Foo.run of <__main__.Foo object at 0x000001227C077588>>
>>> f.run.__self__
<__main__.Foo object at 0x000001227C077588>
>>> f.run.__func__ is Foo.run
True

  (1)在类上调用方法相当于调用函数,Foo.run( Foo('Tom') )。

  (2)Foo.run 类型: function(函数), f.run 类型: method(方法)

  (3)函数都是非覆盖性描述符。

  (4)Foo.run.__get__(f);在函数上调用__get__方法时传入实例,得到的是绑定到那个实例上的方法。

  (5)Foo.run.__get__(None,f);在函数上调用__get__方法时,如果instance参数的值是None,那么得到的是函数本身。

  (6)f.run,表达式其实会调用Foo.run.__get__(f),返回对应的绑定方法。

  (7)f.run.__self__,绑定方法对象有个__self__属性,其值是调用这个方法的实例引用。

  (8)f.run.__func__,的__func__属性是依附在托管类上的那个原始函数的引用。

 

描述符用法

描述符用法建议:

(1)使用特性以保持简单:

  内置的property类创建的其实是覆盖型描述符,__set__方法和__get__方法都实现了,即便不定义设置方法也是如此。

(2)只读描述符必须有__set__方法:

  如果使用描述符类实现只读属性,要记住,__get__和__set__两个方法必须都定义,否则,实例的同名属性会遮盖描述符。

  只读属性的__set__方法只需抛出AttributeError异常。

(3)用于验证的描述符可以只有__set__方法:

  对仅用于验证的描述符来说,__set__方法应该检查value参数获得的值,如果有效,使用描述符实例的名称为键,直接在实例的__dict__属性中设置。

  这样,从实例中读取同名属性的速度很快,因为不用经过__get__方法处理。

(4)仅有__get__方法的描述符可以实现高效缓存:

  如果只编写了__get__方法,那么创建的是非覆盖型描述符。这种描述符可用于执行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。

  同名实例属性会遮盖描述符,因此后续访问会直接从实例的__dict__属性中获取值,而不会再触发描述符的__get__方法。

(5)非特殊的方法可以被实例属性遮盖:

  由于函数和方法只实现了__get__方法,他们不会处理同名实例属性的赋值操作。

  my_obj.the_method = 7这样简单赋值之后,后续通过该实例访问the_method得到的是数字7,但是不影响类或其他实例。

  然而,特殊方法不受这个问题的影响。

  repr(x)执行的其实是x.__class__.__repr__(x),因此x的__repr__属性对repr(x)方法调用没有影响。

  出于同样的原因,实例的__getattr__属性不会破坏常规的属性访问规则。

 

posted @ 2019-09-05 17:58  5_FireFly  阅读(422)  评论(0编辑  收藏  举报
web
counter