我们在前面几篇文章中,介绍了几种影响属性访问的方式,如__getattr__以及描述符等。现在我们具有了一干属性相关的概念,下面列举一下:
- 实例属性;
- 父类实例属性(包括父类的父类...);
- 类属性;
- 父类类属性;
- 数据描述符;
- 非数据描述符;
__getattr__;
我们用一个例子将上面这7项全部包含进去:
class DDesc:
def __get__(self, obj, type=None):
return 'I\'m data descriptor'
def __set__(self, obj, value):
obj.__dict__['a'] = 10
class NDDesc:
def __get__(self, obj, type=None):
return 'I\'m non-data descriptor'
class Attr:
a = 'I\'m attr class att'
def __init__(self):
self.a = 'I\'m attr att'
class F:
a = 'I\'m F class att'
def __init__(self):
self.a = 'I\'m F att'
class A(F):
a = DDesc()
# a = NDDesc()
# a = 'I\'m class att'
def __init__(self):
super().__init__()
self.a = 'I\'m inst att'
self.attr = Attr()
def __getattr__(self, name):
return getattr(self.attr, 'a')
a = A()
print(a.a)我们每次都将打印出来的代码注释掉,我们可以清楚地看到通过实例进行属性访问的优先级顺序:
# I'm data descriptor
# I'm inst att
# I'm F att
# I'm non-data descriptor
# I'm class att
# I'm F class att
# I'm attr att
# I'm attr class att
# AttributeError这其中有个问题,因为类属性和描述符都定义在类级,所以定义在后边的一项将覆盖前面一项,因而无法直接比较两者优先级。但非数据描述符一定位于父类类属性之前。
从上面我们可以看到,实例属性访问的顺序为:
数据描述符->实例__dict__(父类实例实际上被继承进子类了)->非数据描述符=普通类属性->父类类属性->__getattr__->AttributeError。
实际上,Python拥有一套内部属性访问机制,允许我们按照一定的顺序去寻找一个属性的位置或是修改、删除一个属性。这套机制由三个特殊方法控制,他们分别是__getattribute__,__setattr__和__delattr__。
__getattribute__会在访问大多数属性(不是全部,后面会说到)时被无条件调用,它接收一个参数作为属性名,并按照上述顺序查找该属性,找到则返回,否则抛出AttributeError异常。__getattribute__很像一个“钩子”,钩住了属性访问的语句。
class A:
ca = 10
def __init__(self):
self.a = 2
def __getattribute__(self, name):
print('Attribute access')
a = A()
print(a.a)
# Attribute access
# None
print(a.ca)
# Attribute access
# None另外一个问题在于,我们在定义类的时候,通常没有定义这个方法,那它是怎么起作用的呢?答案是调用了object基类(通过实例访问时)或type元类(通过类访问时)的__getattribute__方法。
class A:
ca = 10
def __init__(self):
self.a = 2
def __getattribute__(self, name):
print('Attribute access')
return object.__getattribute__(self, name)
#当然这里可以用super来替代,因为object是所有类的基类
#利用super可以调用继承链中的__getattribute__
# return super().__getattribute__(name)
a = A()
print(a.a)
# Attribute access
# 2
print(a.ca)
# Attribute access
# 10
print(A.ca)
# 10类与实例可以看到,通过类访问类属性时,实例的__getattribute__方法并没有被调用。如何定义类的__getattribute__方法?这需要用到元类的知识,我们放在后面介绍。
如果你还记得前面介绍的__getattr__,你会发现两者好像功能很像,都是接收一个属性名参数,返回实际的属性值。但是两者是完全不同的存在。我们在上面的例子中也能发现,__getattr__虽然定义了,但是只有当排在前面的几种属性都没有找到时,才会调用__getattr__。而这个搜索的功能是__getattribute__定义的,且是默认实现在object中的,它会被无条件调用。所以说,只有当默认的__getattribute__没有找到目标属性时,才会去调用用户定义的__getattr__来做最后的尝试。实际上,只要在__getattribute__中抛出AttributeError异常,解释器就会执行__getattr__:
class A:
def __getattribute__(self, name):
print('Finding in __getattribute__')
raise AttributeError('Not found')
def __getattr__(self, name):
print('Found in getattr')
return 0
a = A()
print(a.b)
# Finding in __getattribute__
# Found in getattr
# 0前面强调了,__getattribute__并不是对访问全部属性都会被自动调用。对于一些内建函数来说,Python有其他的属性访问方式。
以len()为例。在系列的前几期,我们介绍了许多内建函数(built-in functions),它们实现的机制是隐式调用Python的特殊方法协议。例如,调用len(a)实际上调用的是对象a的__len__特殊方法:
class A:
def __len__(self):
print('Call __len__ of Class A')
return 0
a = A()
print(len(a))
# Call __len__ of Class A
# 0
print(a.__len__()) #这俩个结果是一样的
# Call __len__ of Class A
# 0
print(A.__len__(a))
# Call __len__ of Class A
# 0现在我们给类A加上自定义的__getattribute__方法,看看会发生什么:
def __getattribute__(self, name):
print('Self-defined __getattribute__')
return object.__getattribute__(self, name)
A.__getattribute__ = __getattribute__
print(a.__len__())
# Self-defined __getattribute__
# Call __len__ of Class A
# 0
print(len(a))
# Call __len__ of Class A
# 0我们看到,前者调用了A中的__getattribute__方法,而后者则没有。这说明Python对于内建方法的调用会绕开__getattribute__。这样做的目的是为了解决一个叫做“元类混乱”(metaclass confusion)的问题。这些在元类中会介绍。
通常情况下,我们都不需要去碰触__getattribute__这个方法。Python为我们已经做好了一个高速的正确的版本(高速因为是利用C语言实现的)。如果确实需要自定义一些属性的查询方式,采用描述符或__getattr__。__getattribute__具有极强的破坏力,稍有不慎就会带来灾难性后果。
和描述符中的__get__很类似,__getattribute__也可能产生无限循环的问题。因为对当前类的任何的属性访问都会无条件先执行__getattribute__,所以在__getattribute__中如果写了任何对当前类的属性访问的语句就会出错(注意是任何,不管是点运算符还是getattr还是__dict__):
class A:
def __getattribute__(self, name):
return self.name
#return getattr(self, name)
#return self.__dict__[name]
a = A()
a.b = 1
print(a.b)
# RecursionError: maximum recursion depth exceeded while calling a Python object上面的三条return语句都会导致递归异常,原因说过了。所以在__getattribute__中必须避免对当前类的属性访问。但是可以访问父类或元类的属性:
class B:
def __getattribute__(self, name):
return 10
class A(B):
def __getattribute__(self, name):
return super().__getattribute__(name)
a = A()
print(a.b)
# 10或是直接访问object的方法,就像前面介绍的。需要指出的是,object中的__getattribute__是利用C语言实现的,因而具有极高的效率,任何对__getattribute__的Python重写都会极大地影响效率(因为每个属性访问都会经过__getattribute__)。
__getattribute__如果要改写,那么必须保证它正确抛出异常,否则会带来意想不到的结果:
class A:
def __getattribute__(self, name):
print('hi')
a = A()
if hasattr(a, 'b'):
print('a has attribute b')
# a has attribute b
print(a.b)
# None正常情况下,a里应该没有b属性,因为从头到尾也没有定义b,然而,hasattr(a, 'b')却返回了True的结果,因为__getattribute__没有返回值,也没有抛出异常。
有get自然也存在set和del的版本。不幸的是,set和del版本就是__setattr__和__delattr__而不是__setattribute__和__delattribute__。__setattr__在属性赋值时会无条件执行:
class A:
def __setattr__(self, name, value):
print('In __setattr__')
self.__dict__[name] = value
a = A()
a.b = 0
# In __setattr__关于这两个属性就不再多介绍了,它们和__getattribute__非常类似,只不过是用于赋值和析构。例如,__setattr__也会有递归异常问题,所以需要调用object的方法完成:
class A:
def __setattr__(self, name, value):
self.name = value
a = A()
a.b = 0
# RecursionError: maximum recursion depth exceeded while calling a Python object
#应改为__dict__或调用object.__setattr__
class A:
def __setattr__(self, name, value):
object.__setattr__(self, name, value)
a = A()
a.b = 0
print(a.b)
# 0