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