Python程序员python进阶

python 属性访问顺序 描述符

2019-08-18  本文已影响3人  落羽归尘

概述

        了解和熟悉python中的属性访问顺序,有助于我们阅读源码,编写高质量代码,对python机制有个更深的理解。
        在讲解属性访问顺序之前,我们先熟悉一下与之有关的知识,__getattribute____getattr__,描述符等。随后我们通过例子来讲解python的属性访问顺序到底是怎样的。

前置知识

方法__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__方法。

总结一下属性查找的顺序:
  1. __getattribute__(), 无条件调用
  2. 数据描述符(优先于实例属性)
  3. 实例对象的字典(与描述符属性同名时,会被覆盖哦)
  4. 非数据描述符(只有__get__()方法)或者类属性
  5. __getattr__() 方法(属性不存在时调用)

如有不当之处,欢迎交流指正!

上一篇下一篇

猜你喜欢

热点阅读