简介
Python 中,一切皆对象。
当我们访问某个对象属性时,在不同的情况下,Python 对属性的访问机制有所不同。
在介绍 Python 属性访问机制前,先来了解一些前置知识。
前置知识
- Python 中与属性访问相关的一些魔法方法:
__getattr__(self, name)
:当默认属性访问抛出 AttributeError 异常(可能是__getattribute__
无法找到对应实例的属性而抛出 AttributeError 异常,或者实例本身无法在继承树上找到对应的实例,再或者是描述符对象的__get__
对该属性抛出 AttributeError异常导致)时被调用。该方法应当返回一个值或者抛出一个 AttributeError 异常。
注:如果按正常的属性搜索机制找到了属性,__getattr__
不会被调用。__getattribute__(self, name)
:该方法会被无条件调用, 无论属性存不存在。如果同时定义了__getattr__
方法,除非__getattribute__
显示调用它或者抛出一个 AttributeError 异常,否则不会被调用。该方法应当返回一个值或者抛出一个 AttributeError 异常。为了避免潜在的无限递归调用,方法的实现应当永远返回基类的同名方法调用,比如object.__getattribute__(self, name)
。__getitem__(self, key)
:对于self[key]
的调用实现。对于序列对象,参数key
必须为整形或切片类型对象。如果key
类型不恰当,可以触发 TypeError 异常;如果索引越界,可以触发 IndexError 异常;对于映射类型,如果key
缺失,可以触发 [KeyError] 异常。
注:__getitem__(self, key)
和__getattribute__(self, name)
一致,都是会无条件被调用。它们之间的区别只在于:__getattribute__(self, name)
被对象的.
操作触发;__getitem__(self, key)
被对象的[]
操作触发。
-
描述符对象:一般来说,描述符对象指的是具备“绑定行为”的对象属性,其属性访问机制被
__get__()
,__set__()
,__delete__()
所覆写。简单来讲,如果一个对象定义了该三种方法中的任意一种或多种,则将该对象称为描述符对象。
默认的属性访问行为是对对象的__dict__
属性字典进行获取,设置或删除操作。比如,a.x
会首先从a.__dict__['x']
进行查找,找不到就继续往查找链上级type(a).__dict__['x']
进行查找,在继承链(除了metaclass
)上如此往复。
然而,如果查找的属性是一个描述符对象,那么 Python 就会使用描述符方法替代默认的属性搜索机制。
对于定义了__set__
或__delete__
的描述符,称之为 数据描述符(data descriptor),否则,则为 非数据描述符(non-data descriptor)。通常,数据描述符一般都定义了__get__
和__set__
方法,非数据描述符一般只定义了__get__
方法。数据描述符会覆盖掉对象实例属性字典__dict__
,非数据描述符属性会被对象实例对应属性覆盖。
在 Python 中,静态方法staticmethod()
和 类方法classmethod()
以非数据描述符(non-data descriptor)的方式进行实现,因此,对象实例可以覆盖重定义这些方法。
property()
方法以数据描述符(data descriptor)进行实现,因此,对象实例无法重写property
行为。
属性访问机制
-
默认访问机制:实例属性字典 -> 类属性字典 -> 父类属性字典 ->...-> object 属性字典,即
obj.__dict__['x'] -> class.__dict__['x'] -> super().__dict__['x'] ->...-> object.__dict__['x']
class A(object):
a = 1
b = 10
def __init__(self):
self.a = 2
if __name__ == '__main__':
obj = A()
print(obj.a) # 2
print(obj.b) # 10
-
当定义了
__getattr__
:
class A(object):
a = 1
b = 10
def __init__(self):
self.a = 2
def __getattr__(self, item):
return item
if __name__ == '__main__':
obj = A()
print(obj.a) # 2
print(obj.b) # 10
print(obj.c) # c
从代码运行结果可以看出:对于设置了__getattr__
的对象,只有当搜索不到属性时,才会调用__getattr__
方法。
-
当定义了
__getattribute__
:
class A(object):
a = 1
b = 10
def __init__(self):
self.a = 2
def __getattribute__(self, item):
return item
if __name__ == '__main__':
obj = A()
print(obj.a) # a
print(obj.b) # b
print(obj.c) # c
可以看到,如果__getattribute__
直接返回一个结果,则对象所有的属性访问直接返回的是__getattribute__
的结果。如果__getattribute__
抛出异常,则无法获取属性值,程序直接退出。即 __getattribute__
会阻断属性默认访问机制,除非使用默认实现super().__getattribute__(item)
。
注:为了同时支持self['x']
这种操作,可以直接让__getattribute__
代理结果即可:
def __getitem__(self, item):
return type(self).__getattribute__(self, item)
-
当同时定义了
__getattr__
和__getattribute__
:
class A(object):
a = 1
b = 10
def __init__(self):
self.a = 2
def __getattribute__(self, item):
print('call __getattribute__')
if item == 'a':
return item
raise AttributeError
def __getattr__(self, item):
print('call __getattr__')
return item
if __name__ == '__main__':
obj = A()
print(obj.a)
print(obj.b)
print(obj.c)
结果如下:
call __getattribute__
a
call __getattribute__
call __getattr__
b
call __getattribute__
call __getattr__
c
可以看到,当同时存在__getattribute__
和__getattr__
时,会先调用__getattribute__
,如果返回一个值,则调用结束。否则,会继续走__getattr__
方法调用。如果__getattr__
抛出异常,程序直接退出。
- 访问数据描述符成员:
class DataDescriptor(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return '<DataDescriptor: {}>'.format(self.name)
def __get__(self, instance, owner):
print('call __get__')
print('instance', instance)
print('owner', owner)
return '1111111111'
def __set__(self, instance, value):
print('call __set__')
print('instance', instance)
self.value = value
class A(object):
data_desc = DataDescriptor('class A') # class attribute
if __name__ == '__main__':
obj = A()
obj.data_desc = DataDescriptor('main') # instance attribute
print('object __dict__:', obj.__dict__)
print(obj.data_desc)
结果如下:
call __set__
instance <__main__.A object at 0x0127E8D0>
object __dict__: {}
call __get__
instance <__main__.A object at 0x0127E8D0>
owner <class '__main__.A'>
1111111111
可以看到,当 数据描述符作为类属性 时,在对象上使用该属性(a.x
,x
为 data descriptor)时,赋值时会调用数据描述符的__set__
方法,取值时会调用数据描述符的__get__
方法。对象的属性字典obj.__dict__
不起作用。
下面看下在 类对象(A.data_desc)的使用:
class DataDescriptor(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return '<DataDescriptor: {}>'.format(self.name)
def __get__(self, instance, owner):
print('call __get__')
print('instance', instance)
print('owner', owner)
return '1111111111'
def __set__(self, instance, value):
print('call __set__')
print('instance', instance)
self.value = value
class A(object):
data_desc = DataDescriptor('class A') # class attribute
if __name__ == '__main__':
obj = A # class,not instance
obj.data_desc = DataDescriptor('main') # class attribute
print('object __dict__:', obj.__dict__)
print(obj.data_desc)
结果如下:
object __dict__: {'__module__': '__main__', 'data_desc': <DataDescriptor: main>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
call __get__
instance None
owner <class '__main__.A'>
1111111111
可以看到,当在 类对象(A.data_desc)上使用数据描述符属性时,由于该属性本身就是类对象所有,因此设置属性时,不会调用数据描述符的__set__
方法,而是直接设置到类的__dict__
中;而取值流程与对象的流程一致,只是数据描述符__get__
的参数instance
变为None
。
- 访问非数据描述符成员:
class DataDescriptor(object):
def __init__(self, name):
self.name = name
def __repr__(self):
return '<DataDescriptor: {}>'.format(self.name)
def __get__(self, instance, owner):
print('call __get__')
print('instance', instance)
print('owner', owner)
return '1111111111'
class A(object):
data_desc = DataDescriptor('class A') # class attribute
if __name__ == '__main__':
obj = A()
print('object __dict__:', obj.__dict__)
print(obj.data_desc)
结果如下:
object __dict__: {}
call __get__
instance <__main__.A object at 0x0034AF30>
owner <class '__main__.A'>
1111111111
更改main
函数为如下:
if __name__ == '__main__':
obj = A()
obj.data_desc = DataDescriptor('main') # instance attribute
print('object __dict__:', obj.__dict__)
print(obj.data_desc)
结果为:
object __dict__: {'data_desc': <DataDescriptor: main>}
<DataDescriptor: main>
可以看到,对象访问非数据描述符时,先调用对象的__dict__
,没有才调用描述符的__get__
方法。
下面看下在 类对象(A.data_desc) 上使用非数据描述符属性:
if __name__ == '__main__':
obj = A
# obj.data_desc = DataDescriptor('main') # class attribute
print('object __dict__:', obj.__dict__)
print(obj.data_desc)
结果如下:
object __dict__: {'__module__': '__main__', 'data_desc': <DataDescriptor: class A>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
call __get__
instance None
owner <class '__main__.A'>
1111111111
把代码中的注释解除,结果一致。因此,对于非数据描述符,类对象使用时直接调用其__get__
方法。
总结
默认属性访问机制:实例属性字典 -> 类属性字典 -> 父类属性字典 -> ... -> object 属性字典
__getattribute__
一旦定义,就会被调用,且会阻断默认属性访问机制(除非使用默认实现super().__getattribute__(item)
)。同理,__getitem__
当定义了
__getattr__
时,只有在属性无法找到时,才会进行调用。有以下几种可能会导致__getattr__
被调用:
1.__getattribute__
抛出 AttributeError 异常;
2. 默认属性访问机制无法找到所需属性值;
3. 描述符对象的__get__
方法抛出 AttributeError 异常;当 描述符作为实例属性 时,直接返回
__dict__['x']
。当 描述符作为类属性 时,存在如下几种情形:
1. 当属性为 数据描述符(data descriptor) 时,在对象实例上调用,会调用数据描述符的__set__
和__get__
方法,即数据描述符会覆盖对象的__dict__
;而在类对象上调用时,不会调用__set__
方法,而__get__
方法仍会被调用,只是参数instance
变为None
;
2. 当属性为 非数据描述符(non-data descriptor) 时,在对象实例上调用,会先调用对象的__dict__
,没有才调用非数据描述符的__get__
方法,即对象的__dict__
会覆盖非数据描述符;而在类对象上调用时,会直接调用非数据描述符的__get__
方法。
总结:对于描述符属性,__get__
方法一定会被调用,除了对象实例调用非数据描述符属性,且对象实例本身已有该同名属性;
对于描述符属性,__set__
方法一定不会被调用,除了对象实例调用数据描述符。描述符相关方法的调用是写在
__getattribute__
内部默认实现上的。