• 属性访问与属性描述符


    属性访问:

      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__属性不会破坏常规的属性访问规则。

  • 相关阅读:
    网页中防拷贝、屏蔽鼠标右键代码
    Enterprise Library Exception Handling Application Block Part 1
    vs debug 技巧
    Winforms:消除WebBrowser的GDI Objects泄露
    WinForm窗体之间交互的一些方法
    TableLayoutPanel用法
    Winforms:把长ToolTip显示为多行
    Winforms: 不能在Validating时弹出有模式的对话框
    Winforms: 复杂布局改变大小时绘制错误
    读取Excel默认工作表导出XML
  • 原文地址:https://www.cnblogs.com/5poi/p/11468733.html
Copyright © 2020-2023  润新知