一篇文章让你彻底明白__getattr__、__getattribute__、__getitem__的用法与执行原理



  • __getattr__

    在Python中,当我们试图访问一个不存在的属性的时候,会报出一个AttributeError。但是如何才能避免这一点呢?于是__getattr__便闪亮登场了

    当访问一个不存在的属性的时候,会触发__getattr__方法

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
    
        def __getattr__(self, item):
            return ">>>" + item
    
    
    a = A()
    print(a.name)  # satori
    print(a.这个属性不存在)  # >>>这个属性不存在
    """
    可以看到当我们访问存在的属性的时候,是可以直接访问的
    如果视图访问一个不存在的属性,那么会触发__getattr__方法
    里面的参数item就是我们访问的不存在的属性名
    """
    

    因此有了__getattr__,我们便可以进行更安全的属性访问了

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
    
        def __getattr__(self, item):
            try:
                return self.__dict__[item]
                # 从属性字典里面返回,至于这里可不可以通过self.item返回呢?答案显然是不行的
                # 因为self.item这种方式表示的就是获取self里面的item属性,虽然我们后面传的是name
                # 但是.item就表示获取名称为item的属性,相当于self.__dict__["item"]
                # 因此如果是self.item,发现不存在item属性,于是触发__getattr__,然后又执行self.item
                # 于是又触发__getattr__,于是会引发无限递归,从而最终栈溢出。
            except KeyError:
                return f"不存在{item}属性"
    
    
    a = A()
    print(a.name)  # satori
    print(a.age)  # 不存在age属性
    

    __getattribute__

    __getattribute__是做什么的呢?其实它和__getattr__比较类似,只不过__getattr__是在访问一个不存在的属性才会触发,而__getattribute__则是在访问任何属性的时候都会触发。

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
    
        def __getattr__(self, item):
            try:
                return self.__dict__[item]
            except KeyError:
                return f"不存在{item}属性"
    
        def __getattribute__(self, item):
            return item
    
    
    a = A()
    print(a.name)  # name
    print(a.不存在的属性)  # 不存在的属性
    """
    可以看到不管属性存不存在,获取的时候都会触发__getattribute__方法
    __getattribute__里面的item参数,也是我们传入的属性名。
    这个__getattribute__也叫做属性拦截器,就是不管什么属性都会触发该方法
    """
    

    如果我们不希望外部访问某些变量,除了使用私有变量之外(然而也是可以访问的),还可以使用__getattribute__,我们看到把所有属性都拦截下来了。但是问题又来了,既然所有属性都拦截下来了,那我怎么访问想访问的属性呢?

    答案是raise一个AttributeError,一旦当__getattribute__引发了AttributeError,那么便会触发__getattr__方法。可以把__getattr__看成是__getattribute__的小弟,什么事都要经过老大,如果老大抛异常了,那么小弟要来兜着。

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
            self.age = 16
    
        def __getattr__(self, item):
            return f"{item} is satori"
    
        def __getattribute__(self, item):
            if item == "age":
                return "女人的芳龄不可泄露"
            else:
                print("其他的属性就无所谓喽")
                raise AttributeError
    
    
    a = A()
    print(a.age)  # 女人的芳龄不可泄露
    """
    当访问age的时候,直接给拦截下来了,不让访问该属性
    """
    
    print(a.name)
    """
    其他的属性就无所谓喽
    name is satori
    """
    

    __getattribute__引发异常,交给__getattr__执行的时候,那么也会把item传递给__getattr__。不过可能会有人好奇,__getattr__里面不是self.__dict__[item]吗?怎么改了?这正是我想说的,这个__getattribute__真的一不小心就会引发无限递归。所以这里写self.__dict__[item]是不合适的,可如果通过属性字典都不行的话,那我们怎么获取属性?别急,后面还有一个解决办法,不过在此之前,我们来看一下,为什么写成self.__dict__[item]会引发无限递归。

    首先我们要明白两件事,尽管已经说过了,但是再重复一遍。

    • 如果__getattribute__方法raise了一个AttributeError,那么会执行__getattr__
    • 不管什么时候,只要是访问实例对象(self)的属性,也就是通过self.xxx访问实例对象属性的时候,不管属性是否存在,都会触发__getattribute__方法。

    那我们要怎么做呢?通过super的方式,让父类去调用

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
            self.age = 16
    
        def __getattr__(self, item):
            try:
                return self.__dict__[item]
            except KeyError:
                return f"不存在{item}属性"
    
        def __getattribute__(self, item):
            print(">>>item:", item)
            if item == "age":
                return "女人的芳龄不可泄露"
            return super().__getattribute__(item)
    
    
    a = A()
    print(a.age)
    """
    >>>item: age
    女人的芳龄不可泄露
    """
    # 访问age,直接返回,这是肯定的
    
    print(a.name)
    """
    >>>item: name
    satori
    """
    # 如果执行__getattribute__方法之后,还return 父类调用__getattribute__方法
    # 那么父类会直接返回给我们,如果这个属性存在的话。
    
    print(a.xxx)
    """
    >>>item: xxx
    >>>item: __dict__
    不存在xxx属性
    """
    # 现在我访问了一个不存在的属性xxx
    # 那么肯定调用__getattribute__,这是肯定的,先打印xxx
    # 然后调用父类的__getattribute__,然后父类去帮我们获取self的对应属性。
    # 但是找不到,那么会默认执行__getattr__。如果我们没有重写__getattr__,那么默认执行object的__getattr__,显然会报错
    # 但是我们自己定义了__getattr__,因此会执行我们自己的__getattr__
    # 于是执行self.__dict__[item],但是__dict__是self的一个属性,于是在这一步又会触发__getattribute__
    # 此时的item就是字符串形式的__dict__,然后打印
    # 由于"__dict__" != "age", 因此会再调用父类的__getattribute__,帮我们获取,显然self是有__dict__这个属性字典的,于是直接返回
    # 此时的self.__dict__就返回了属性字典,然后获取对应的属性,但是发现没有,于是引发KeyError
    # 捕获、打印不存在该属性
    

    因此可以看到,这个__getattribute__功能很强大,但是搞不好会走火入魔,最保险的方法就是最后一定要让父类去调用,不建议使用raise AttributeError的方式。而且至于这个方法本身,没事的话也不建议使用,除非你能很清晰地理解这个方法,并且很明白自己就需要这个方法,那么是可以使用的。如果你都不确定要不要用这个方法,那么十有八九是用不到的,就跟元类一样,需要你有清晰的认识和较深的理解。

    getattr

    还记得getattr吗?获取对象的某个属性,通过指定一个默认值,如果找不到,就返回默认值。那么这是如何实现的呢?

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
            self.age = 16
    
        def __getattr__(self, item):
            try:
                return self.__dict__[item]
            except KeyError:
                return f"不存在{item}属性"
    
    
    a = A()
    print(getattr(a, "name", None))  # satori
    print(getattr(a, "age", None))  # 16
    """
    获取已存在的属性,那么显然结果是正确的
    """
    
    print(getattr(a, "xxx", None))  # 不存在xxx属性
    """
    明明不存在xxx这个属性,但是却没有返回我们自己定义的值,这是为什么?
    其实从返回的结果也能看出来,getattr(obj, "attr"),本质上就是调用obj对应的类的__getattr__方法,获取其返回值
    
    因此可以理解,我们上面即便获取不到属性的时候,还是会返回一个正常的值。
    getattr(obj, "attr")本质上就是obj对应的类.__getattr__(obj, "attr")
    只不过使用getattr()这种方式会多帮我们做一层处理,就是找不到属性返回默认值。
    而我们定义了__getattr__,而且不管属性存不存在都是正常返回的,那么getattr肯定就获取不到默认值了。
    """
    

    那么怎么办呢?我们可以看看如果在__getattr__只返回属性的值,属性不存在就不管了,这样使用getattr是不是就能返回默认值了呢?我们来试一下

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
            self.age = 16
    
        def __getattr__(self, item):
            return self.__dict__[item]
    
    
    a = A()
    print(getattr(a, "xxx", None))
    """
    Traceback (most recent call last):
      File "C:/Users/satori/Desktop/satori/task.py", line 15, in <module>
        print(getattr(a, "xxx", None))
      File "C:/Users/satori/Desktop/satori/task.py", line 11, in __getattr__
        return self.__dict__[item]
    KeyError: 'xxx'
    """
    

    但是解释器报错了,这是为什么呢?我们注意到异常是KeyError。我们再来试试不定义__getattr__的情况

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
            self.age = 16
    
    a = A()
    print(getattr(a, "xxx", None))  # None
    """
    我们发现自己不定义__getattr__,而是走父类的__getattr__是可以在访问不存在的属性的时候,获取到默认值。
    可这是为什么?
    """
    # 如果我们直接访问
    print(a.xxx)
    """
    Traceback (most recent call last):
      File "C:/Users/satori/Desktop/satori/task.py", line 17, in <module>
        print(a.xxx)
    AttributeError: 'A' object has no attribute 'xxx'
    """
    

    我们注意到,当我们在不定义__getattr__的时候,会使用父类的__getattr__,此时getattr就没问题。但是我们来注意一下,首先我们自己定义的__getattr__在找不到属性的时候,抛出的是一个KeyError,但是父类的__getattr__抛出的确是一个AttributeError。这不是意味着我们如果抛出的也是一个AttributeError就可以了呢?

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "satori"
            self.age = 16
    
        def __getattr__(self, item):
            try:
                return self.__dict__[item]
            except KeyError:
                raise AttributeError
    
    a = A()
    print(getattr(a, "xxx", None))  # None
    print(getattr(a, "哈哈哈", 666))  # 666
    

    咦,居然成功了。所以我们需要raise一个AttributeError

    这里还想提一点,我们说getattr(obj, "attr")本质上就是调用obj对应的类.__getattr__(obj, "attr")。具有这样特点方法,还有很多,比如说len。

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __len__(self):
            return 123
    
    
    a = A()
    print(len(a))  # 123
    print(A.__len__(a))  # 123
    
    """
    但是可能有人有疑问,这等不等于a.__len__()呢?可以一半等于吧
    """
    a.__len__ = lambda : 234
    print(a.__len__())  # 234
    print(len(a))  # 123
    
    """
    可以看到,当我手动给a设置了__len__属性的时候,再使用len(a)走的还是类的方法
    """
    
    # 因此如果类里面没有__len__方法的话,即使实例有,那么使用len()这种方式也是会报错的
    # 会报出TypeError: object of type 'A' has no len()
    # 因为这种方法的本质上就是,类.__len__(实例),而不是实例.__len__()
    # 但是如果不给类设置__len__方法,那么a.__len__()是可以的,因为实例没有__len__,就会去执行类的__len__,并自动把本身作为第一个参数传进去
    # 所以本质走的还是类的方法,因此我们说一半等于
    

    hasattr

    提到getattr,就不提hasattr,这是判断一个对象里面是否有某个属性,其实hasattr就是在getattr的基础上实现的。我们知道getattr获取属性是可以指定默认值的,属性找不到就会获取默认值。但是如果不指定,在获取不到的属性时候同样会抛出异常(而不是返回None,这一点切记),什么异常呢?对,AttributeError。如果抛出了这个异常,那么说明没有该属性,返回False,否则返回True

    我们完全可以手动实现一个hasattr

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "mashiro"
    
    
    
    def hasattr_(obj, attr):
        try:
            getattr(obj, attr)
            return True
        except AttributeError:
            return False
    
    
    a = A()
    print(hasattr_(a, "name"))  # True
    print(hasattr_(a, "age"))  # False
    

    setattr(__setattr__)

    同理还有setattr,这个底层则是调用类的__setattr__方法。

    class A:
    
        def __init__(self):
            self.name = "mashiro"
    
    
    a = A()
    setattr(a, "age", 16)  # 传入对象、属性名、值
    print(a.age)  # 16
    

    我们同样可以改写

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "mashiro"
    
        def __setattr__(self, key, value):
            print(key, value)
            print("设置完之后,不要别访问啊。我只是打印了一下,没有设置到你的属性字典里面去,你访问不到的")
    
    
    a = A()
    setattr(a, "age", 16)
    """
    name mashiro
    设置完之后,不要别访问啊。我只是打印了一下,没有设置到你的属性字典里面去,你访问不到的
    age 16
    设置完之后,不要别访问啊。我只是打印了一下,没有设置到你的属性字典里面去,你访问不到的
    """
    # 我们可以看到,__setattr__里面的key和value就是属性和值
    # 但是我们看到__init__里面的self.name居然也走了__setattr__
    # 说明self.xxx走的是__getattr__,self.xxx = xxx走的是__setattr__
    print(hasattr(a, "name"))  # False
    print(hasattr(a, "age"))  # False
    # 可以看到都没有设置到属性字典里面去。
    
    
    # 那我们如何设置呢?
    # 肯定不能通过a.__setattr__来设置,因为这走的就是A的__setattr__
    # 首先__setattr__的本质就是给一个 obj 赋上 指定name 的 value
    # 既然不能用A的__setattr__,那我用其他人的不就可以了吗?
    int.__setattr__(a, "name", "satori")
    object.__setattr__(a, "age", 16)
    dict.__setattr__(a, "gender", "f")
    set.__setattr__(a, "place", "东方地灵殿")
    
    print(hasattr(a, "name"), a.name)
    print(hasattr(a, "age"), a.age)
    print(hasattr(a, "gender"), a.gender)
    print(hasattr(a, "place"), a.place)
    """
    True satori
    True 16
    True f
    True 东方地灵殿
    """
    

    delattr(__delattr__)

    这个就更简单了,就是删除对象的某个属性

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/23 9:27
    class A:
        def __init__(self):
            self.name = "xxx"
    
    
    a = A()
    delattr(a, "name")
    try:
        a.name
    except AttributeError as e:
        print(e)  # 'A' object has no attribute 'name'
    # 本质上调用了__delattr__
    

    __getitem__

    __getattr__,是支持我们通过 . 的方式来访一个不存在的属性。而__getitem__则是支持我们可以像字典和列表一样使用[]的形式访问。

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "mashiro"
    
        def __getitem__(self, item):
            print(">>>:" + item)
            return self.__dict__.get(item, "不存在哦")
    
    
    a = A()
    print(a["name"])
    """
    >>>:name
    mashiro
    """
    print(a["gogogo"])
    """
    >>>:gogogo
    不存在哦
    """
    # 可以看到这里的item也是我们传入的属性名
    

    可以看到__getitem__是支持我们使用[]这种方式获取属性的,会把[]里面内容传给item。但从获取属性这一点上来说,这和__getattr__貌似没啥区别啊,只不过一个是通过self.xxx、一个是通过self["xxx"]的方式。但是__getitem__除了支持我们使用[]访问属性之外,还有一个更强大的功能,就是它可以充当迭代器。总所周知,如果我们自己定义一个类,想能够被for循环,那么这个类要实现__iter____next__方法,如果没有实现这两个方法,而是实现了__getitem__方法也是可以的。对于一个类,如果对其进行for循环,首先会去找__iter____next__方法,如果没有,那么会退而求其次去找__getitem__方法。

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.name = "mashiro"
    
        def __getitem__(self, item):
            return item
    
    
    for i in A():
        if i > 10:
            break
        print(i)
    """
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    """
    # 可以看到,如果没有__iter__和__next__,但如果有__getitem__
    # 那么会去执行__getitem__,并且每一次迭代,都会自动将值传递给item
    # 我们虽然没有在任何地方指定item,但是对于迭代来说,会自动传递0 1 2 3 4......
    

    我们看一个栗子

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.l = ['a', 'b', 'c', 'd']
    
        def __getitem__(self, item):
            return self.l[item]
    
    
    for i in A():
        print(i)
    """
    a
    b
    c
    d
    """
    # item显然是从0开始,依次遍历获取self.l里面的元素
    # 一旦当索引越界,for循环迭代不到元素,那么便迭代终止
    
    
    # 除此之外,__getitem__还可以做很多事情
    a = A()
    l = [123]
    l += a
    print(l)  # [123, 'a', 'b', 'c', 'd']
    # 为什么会有这个结果,首先对于列表来说,+= 就相当于extend
    # 同样会依次打开,将里面的元素迭代出来,会自动捕获异常
    

    不仅如此,我们看看__getitem__更让人惊叹的特性。

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __init__(self):
            self.l = ['a', 'b', 'c', 'd']
    
        def __getitem__(self, item):
            print(">>>item =", item)
            return self.l[item]
    
    
    a = A()
    print(a[1: 3])
    """
    >>>item = slice(1, 3, None)
    ['b', 'c']
    """
    
    print(a[0:: 2])
    """
    >>>item = slice(0, None, 2)
    ['a', 'c']
    """
    # 还可以传入一个切片
    

    所以__getitem__除了支持我们使用[]来取值之外,还可以充当迭代器的作用,功能还是很强大的。当然这只是__iter____next__不存在的时候,如果定义了,肯定还是会先走__iter____next____getitem__是在找不到这两个方法时,解释器才会非常智能地退化去找__getitem__方法。

    __setitem__和__delitem__

    既然提到了__getitem__,就必须提__setitem____delitem__。从__setattr____delattr__应该也知道这个__setitem____delitem__是干什么的。

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class A:
    
        def __getitem__(self, item):
            return self.__dict__[item]
    
        def __setitem__(self, key, value):
            print(key, value)
    
    
    a = A()
    a["name"] = "satori"
    """
    name satori
    """
    
    # 通过字典的形式赋值的时候会触发这个方法
    

    我们就来使用__getitem__模拟字典,其实字典之所以能够使用[]这种方式取值,也是因为其内部实现了__getitem__setitem____delitem__这几个魔法函数

    # -*- coding:utf-8 -*-
    # @Author: WanMingZhu
    # @Date: 2019/10/22 10:31
    class Dict:
    
        def __init__(self, items):
            for k, v in items:
                self.__dict__[k] = v
    
        def __getitem__(self, item):
            return self.__dict__[item]
    
        def __setitem__(self, key, value):
            self.__dict__[key] = value
    
        def __delitem__(self, key):
            del self.__dict__[key]
    
        def __str__(self):
            return str(self.__dict__)
    
    
    d = Dict([("a", 1), ("b", 2)])
    print(d)  # {'a': 1, 'b': 2}
    
    d['c'] = 3
    print(d)  # {'a': 1, 'b': 2, 'c': 3}
    
    print(d['b'])  # 2
    
    del d['a'], d['b']
    print(d)  # {'c': 3}
    

    以上就是全部内容啦。

    おしまい

    来自:https://www.cnblogs.com/traditional/p/11724876.html


Log in to reply
 

最新帖子

推荐阅读