python 属性访问顺序 描述符
概述
了解和熟悉python中的属性访问顺序,有助于我们阅读源码,编写高质量代码,对python机制有个更深的理解。
在讲解属性访问顺序之前,我们先熟悉一下与之有关的知识,__getattribute__
,__getattr__
,描述符等。随后我们通过例子来讲解python的属性访问顺序到底是怎样的。
前置知识
- 方法
__getattribute__
- 方法
__getattr__
- 属性描述符
方法__getattribute__
当实例对象访问属性或者方法时都需要调用到__getattribute__
,之后才会在各个__dict__
中查找相应的属性或方法。注意在方法__getattribute__
内部,杜绝存在self.**,因为这样的话就又会调用到__getattribute__
,极可能会递归调用造成错误。
class Test(object):
def __init__(self,name):
self.name = name
def __getattribute__(self,key):
print("访问属性:%s" % key)
t = Test("test1")
t.name
输出结果:
访问属性:name
但是当有实例方法时,调用实例方法时会报错,如下
class Test(object):
def __init__(self,name):
self.name = name
def func(self):
pass
def __getattribute__(self,key):
print("访问属性:%s" % key)
t = Test("test1")
t.name
t.func()
输出结果:
访问属性:name
访问属性:func
Traceback (most recent call last):
File "c:/Users/DELL/Desktop/ssj/search/descrip.py", line 15, in <module>
t.func()
TypeError: 'NoneType' object is not callable
方法__getattr__
在当用户访问一个根本不存在的属性或者查找不到时,会调用__getattr__
。用于查找属性的最后一步。
class Test(object):
def __init__(self,name):
self.name = name
def __getattr__(self,key):
print("调用方法__getattr__: %s" % key)
t = Test("test1")
t.name
t.n
输出结果:
调用方法__getattr__: n
属性描述符
之所以会有描述符的存在,是因为__getattribute__
、__getattr__
、__setattr__
、__delattr__
等方法对属性的一般查找逻辑无法满足有效的对属性控制的需求,假如要实现me.age属性的类型设置,单去修改__setattr__
满足这个需求,那这个方法便有可能对其他的属性的设置造成影响。在类中设置属性的控制行为不能很好地解决问题,Python给出的方案是:__getattribute__
、__getattr__
、__setattr__
、__delattr__
等方法用来实现属性查找、设置、删除的一般逻辑,而对属性的控制行为就由描述符管理。当Test对象访问x时,会自动调用Desc的__get__
方法。
数据描述符:不只定义了__get__
方法,还定义了__set__
或者__delete__
。而非数据描述符,只定义了__get__
方法。
以下例子是数据描述符示例:
实例化Test后,调用对象me访问属性x,会自动调用类Desc的__get__
方法,关于__get__
方法的参数,self代表Desc实例,即x,obj代表Test的实例me,objtype代表Test这个类。
class Desc(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 Test(object):
x = Desc(10, 'var "x"')
me = Test(6)
me.x
输出结果:
Retrieving var "x"
以上例子中,访问me.x时,会先调用me的__getattribute__
()方法,再调用描述符对象的__get__
方法。
当实例对象的字典中,有与描述符同名的属性时。如下例子
class Desc(object):
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print ('Retrieving', self.name)
return 4
def __set__(self, obj, val):
print ('Updating', self.name)
self.val = val
class Test(object):
x = Desc(10, 'var "x"')
def __init__(self,c):
self.x = c
me = Test(6)
me.x
print(me.x)
输出结果:
Updating var "x"
Retrieving var "x"
Retrieving var "x"
4
以上例子中,实例化Test时,会先调用描述符 __set_
()方法,访问me.x时,会调用描述符 __get_
()方法,最后得出me.x的值是4,由此可见,数据描述符存在时,会覆盖掉实例属性,也就是数据描述法的优先级高于实例属性。
那么非数据描述符呢,与实例属性的优先级比,如何:
class Desc(object):
def __init__(self, initval=None, name='var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print ('Retrieving', self.name)
return 4
class Test(object):
x = Desc(10, 'var "x"')
def __init__(self,c):
self.x = c
self.y = Desc(10, 'var "y"')
me = Test(6)
me.x
print(me.x)
输出结果:
6
以上例子中,描述符__set_
()方法和 __get_
()都没有调用,说明非数据描述符的优先级低于实例属性的。
属性访问顺序
__dict__
自定义属性都会有一个字典__dict__
,包含了所有的实例属性,不包含实例方法
class Test(object):
x = 1
def __init__(self,c):
self.y = c
def func(self):
pass
@staticmethod
def static():
pass
@classmethod
def cla(cls):
pass
me = Test(6)
print("实例的__dict__属性",me.__dict__)
print("类的__dict__属性",Test.__dict__)
输出结果:
实例的__dict__属性 {'y': 6}
类的__dict__属性 {'static': <staticmethod object at 0x0000014701B280F0>, '__init__': <function Test.__init__ at 0x0000014701B21950>, 'cla': <classmethod object at 0x0000014701B28128>, 'func': <function Test.func at 0x0000014701B219D8>, 'x': 1, '__doc__': None, '__dict__': <attribute '__dict__' of 'Test' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Test' objects>}
实例的__dict__
包含了实例属性y,但是不会有实例方法func和类属性x。类的__dict__
包含了其余的东西(类属性,实例方法,静态方法,类方法等)。
发生继承时:
class Test(object):
x = 1
def __init__(self,c):
self.y = c
def func(self):
pass
@staticmethod
def static():
pass
@classmethod
def cla(cls):
pass
class Test1(Test):
z=2
def __init__(self,c):
self.a = c
def func1(self):
pass
@staticmethod
def static1():
pass
@classmethod
def cla1(cls):
pass
me = Test1(6)
print("子类实例的__dict__属性",me.__dict__)
print("子类的__dict__属性",Test1.__dict__)
输出结果:
子类实例的__dict__属性 {'a': 6}
子类的__dict__属性 {'cla1': <classmethod object at 0x000002198524D4A8>, 'static1': <staticmethod object at 0x000002198524D470>, '__module__': '__main__', 'z': 2, '__init__': <function Test1.__init__ at 0x0000021985241BF8>, 'func1': <function Test1.func1 at 0x0000021985241C80>, '__doc__': None}
可见,每个实例对象和类都有自己的__dict__
,互不影响。
属性查找顺序
其实,正常情况下,属性查找都是以一定的规则从__dict__
中查找的。
如果只有类属性x,没有实例属性x,当访问x的时候,会是如何呢?我们来看下面例子:
class Test(object):
x = 1
def __init__(self,c):
pass
me = Test(6)
me.x
print(me.x)
输出结果:
1
以上例子中,表明当没有实例属性时即差找不到实例属性,会查找类属性。
注意:当类中定义了__slots__
属性时,对象就不会有__dict__
属性了,这时访问属性时,是通过类似描述符的方式查找属性的。
一般情况下,python的属性访问机制是:实例属性,类属性,父类属性,object属性;也就是先查找实例对象的__dict__
属性,没有的话再查找类__dict__
属性,再没有的话查找父类的__dict__
属性,最后是基类object属性。当定义了数据描述符时时,会覆盖实例对象的__dict__
属性;非数据描述符访问属性时,先查找对象的__dict__
属性,没有的话再调用描述符的__get__
方法。
总结一下属性查找的顺序:
-
__getattribute__
(), 无条件调用 - 数据描述符(优先于实例属性)
- 实例对象的字典(与描述符属性同名时,会被覆盖哦)
- 非数据描述符(只有
__get__
()方法)或者类属性 -
__getattr__
() 方法(属性不存在时调用)