python魔术方法属性篇

发布时间 2023-07-15 18:13:07作者: 天才九少

python魔术方法属性篇

本篇章主要讲与对象的属性有关的魔术方法

3,属性篇

  • __getattr__
  • __getattribute__
  • __setattr__
  • __delattr__
  • __dir__
  • __get__
  • __set__
  • __delete__
  • __slots__

 

__getattr__方法

每当我们写 形如这种o.test的代码的时候,我们实际上都是在尝试access这个对象的一个属性attribute,__getattr__就是当这个属性不存在的时候,你希望它做点什么,默认情况下,如果你在一个对象里尝试access一个不存在的属性的话,它会raise一个AttributeError。

我们可以看到如果我们没有定义这个__getattr__函数,它会raise这个:AttributeError:'A' object has no attribute 'test‘,

class A:
    pass
    # def __getattr__(self, name):
    #   print(f"getting {name}")
    #   raise AttributeError


o = A()
print(o.test)

定义了之后,我们可以看到这个print被执行了,然后后面我们还是raise了一个AttributeError,这里注意一下,你access的这attribute会以string的形式被传到这个__getattr__函数里,比如这个argument name里面传入的就是string test

class A:
    def __getattr__(self, name):
        print(f"getting {name}")
        raise AttributeError


o = A()
print(o.test)

 那假如说我们希望这个object,当有程序尝试去读取它一个不存在的属性的时候,它永远返回None,我们就可以在这个__getattr__里面返回一个None,这个时候我们运行程序就会发现o.test变成None了

class A:
    def __getattr__(self, name):
        print(f"getting {name}")
        raise None


o = A()
print(o.test)
# getting test
# None

这里我们注意一下,这个__getattr__函数,只有在你读取一个它不存在的属性的时候,才会被调用

class A:
    def __init__(self):
        self.exist = "abc"
    
    def __getattr__(self, name):
        print(f"getting {name}")
        raise None


o = A()
print(o.exist)
print(o.test)
# abc
# getting test
# None

 

__getattribute__方法

__getattrbute__这个方法是只要你尝试去读取它的属性,它都会被调用。同样的情况下,无论我们是打印o.exist还是o.test,它都调用了这个__getattrbute__函数,并且都打印出了None,那当然我们现在这个__getattrbute__函数没什么意义,就是你读取什么属性它都给你返回None

class A:
    def __init__(self):
        self.exist = "abc"
    
    def __getattribute__(self, name):
        print(f"getting {name}")
        raise None


o = A()
print(o.exist)
print(o.test)
# getting exist
# None
# getting test
# None

我们写一个稍微有一点点意义的例子,我们这里这个__getattrbute__函数加了一个逻辑,当access的这个名字是data的时候,我把这个         counter加一个,也就是说我要数一数我这个data属性被读了多少次,这里我们注意一下return super().__getattribute__(name),当你使用__getattribute__的时候,如果你想使用它的default behavior (默认行为),你一定要用super().__getattribute__,这里如果你不小心的话就会产生一个无限的递归

class A:
    def __init__(self):
        self.exist = "abc"
        self.counter = 0
    
    def __getattribute__(self, name):
        if name == "data":
            self.counter += 1
        return super().__getattribute__(name)

o = A()
print(o.data)
print(o.data)
print(o.counter)
# abc
# abc
# 2

比如有人可能会觉得我学过getattr这个函数,我默认返回一个getattr(self, name)不就可以了吗,这样的话就会产生一个无限的递归,因为getattr函数又会返回来调用这个self的__getattribute__函数,同时你也要意识到在这个函数里面,特别容易出现一些不易察觉的递归,比如说self.counter += 1 实际上是一个递归调用,它要读取 self.counter,而当你读取self.counter的时候就调用了这个__getattribute__函数,所以这里面是有一个递归的

class A:
    def __init__(self):
        self.exist = "abc"
        self.counter = 0
    
    def __getattribute__(self, name):
        if name == "data":
            self.counter += 1
        return getattr(self, name)

o = A()
print(o.data)
print(o.data)
print(o.counter)

如果说你想数一数这个object里面所有的属性被读取的次数,然后写了下面这么一个函数,你就会发现它又无限的递归了,因为你每一次加counter的时候,你都要读取一下counter,然后又要加counter,你就一直递归下去了,所以大家在使用这个__getattribute__方法的时候,一定要小心一点,看清里面有可能会产生的不显眼的递归,然后记住它的默认的behavior应该是super()之后再__getattribute__

class A:
    def __init__(self):
        self.exist = "abc"
        self.counter = 0
    
    def __getattribute__(self, name):
        self.counter += 1
        return super().__getattribute__(name)

o = A()
print(o.data)
print(o.data)
print(o.counter)

 

__setattr__方法

当我们尝试去写一个属性的时候,就要用到__setattr__,__setattr__有两个argument,一个是name,一个是value。

当我们做self.data = "abc"的时候,data就作为一个string被放到了name里面,然后“abc”就是这个value,同样的我们一般会使用

super().__setattr__来完成它的默认行为,当然这里你也可以用这个object.__setattr__,包括前面的get,也可以直接使用这个object的默认method,这里可以看到当我们写self.data和写self.counter的时候,这个__setattr__函数都被调用了,同时我们在打印o.data的时候也正确的打印出了“abc”这个string。

class A:
    def __init__(self):
        self.data = "abc"
        self.counter = 0

    def __setattr__(self, key, value):
        print(f"set {key}")
        super().__setattr__(key, value)


o = A()
print(o.data)
set data
set counter
abc

当然你说这个__setattr__包括__getattr__最后一定要用这个super吗?也不是哎,举一个简单的例子,我们看在这个class里面的__setattr__

跟__getattr__,我们不再使用super里面的默认的方法,而是在set的时候把这个name value pair给放到这个class级别的_attr variable里面,也就是在第二行这里我们定义的从属于class的这个dictionary,而在__getattr__的时候,我们直接从这个class的_attr dict里面找到这个value,这里注意,由于我们定义的是__getattr__,而不是__getattribute__,所以我们在尝试读取self.attr的时候并不会递归调用这个函数,因为self._attr是存在的,它可以在自己的class里面找到它,那这样做完的结果就是这个class的所有object,实际上,共享了他们所有自定义的attribute。

 我们让o1是A的object,o2也是A的object,这个时候我们把o1的data给写成xyz,然后打印o2的data,可看到o2的data就变成了xyz

class A:
    _attr = {}

    def __init__(self):
        self.data = "abc"

    def __setattr__(self, key, value):
        self._attr[key] = value

    def __getattr__(self, item):
        if item not in self._attr:
            raise AttributeError
        return self._attr[item]


o1 = A()
o2 = A()
o1.data = "xyz"
print(o2.data)
# xyz

 

__delattr__方法

delattr即delete attribute,这个__delattr__和我们之前提到的那个__del__,是不一样的,在一个object正常产生和消亡的过程中,这个__delattr__是不会被调用的,尽管我们在object上面定义了一个属性,但是这个object消失的时候,并不会调用这个__delattr__函数

class A:
    def __init__(self):
        self.data = "abc"

    def __delattr__(self, item):
        print(f"del {item}")


o = A()

 

这个__delattr__函数是在我们尝试删除一个object属性的时候才会被调用,而删除一个object属性,我们是使用del这个关键词,比如这里我们使用del o.data,这个__delattr__函数就会被调用,当然由于我们这个__delattr__函数里面,只写了一个print,所以在我们尝试del之后,这个o.data它还存在

class A:
    def __init__(self):
        self.data = "abc"

    def __delattr__(self, item):
        print(f"del {item}")


o = A()

del o.data

这里我们同样的需要用super()来调用它父类的__delattr__函数,然后最后print(o.data)就会出错

class A:
    def __init__(self):
        self.data = "abc"

    def __delattr__(self, item):
        print(f"del {item}")
        super().__delattr__(item)


o = A()
del o.data
print(o.data)
AttributeError: 'A' object has no attribute 'data'

 

__dir__方法

有关属性我们有一个挺常用的内置函数叫做dir,比如说我们在打印这个dir(o)的时候,它就会把o里面你能access到的一些属性给你列出来,那这里面当然有一些内置的variable和method,也有我们自己定义的这个attribute data

class A:
    def __init__(self):
        self.data = "abc"


o = A()
print(dir(o))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'data']

那我们可以在一个class里面,通过定义这个__dir__这个魔术方法来改变dir这个内置函数的结果,python规定这个dir方法必须要返回一个sequence,那就常见的sequence就是一个list,我们如果在这个方法里面返回一个空list的话,我们在打印dir(o)就是空的了

class A:
    def __init__(self):
        self.data = "abc"

    def __dir__(self):
        return []


o = A()
print(dir(o))  # []

我们可以写一个稍微有意义一点的方法,比如我们拿到它正常的dir,然后排除这个list里面所有下划线开头的attribute,这样我们打印出来就只剩下一个data了

class A:
    def __init__(self):
        self.data = "abc"

    def __dir__(self):
        lst = super().__dir__()
        return [el for el in lst if
                not el.startswith("_")]


o = A()
print(dir(o))
# ['data']

 

__get__方法

接下来我们看几个与descriptor(描述器)有关的magic method。

首先class D我们定义了一个descriptor,然后在class A里面,这个attribute x是一个D的object,我们一般会说x 是一个描述器descriptor,

我们建立了一个A的object o,然后打印o.x,我们注意一下这个descriptor里面定义的__get__函数,这个__get__函数,会在我们尝试读取o.x的时候被调用,而__get__函数有两个argument,注意一下这里很容易搞混,self是这个descriptor object本身,也就是x,然后这个的obj有时候写成instance,对应的是这个o,是这个class A的object,也就是在我们读取o.x的时候,o会被作为这个object传进来,这个owner是o的class,我们运行一下可以看到,打印出来的obj是一个A的object,也就是o,而这个owner是这个class A,同时我们在这个__get__ method里面返回了0,所有o.x就是0

class D:
    def __get__(self, obj, owner=None):
        print(obj, owner)
        return 0


class A:
    x = D()


o = A()
print(o.x)
<__main__.A object at 0x0000020BC3E84850> <class '__main__.A'>
0

 

__set__方法

在我们尝试写一个descriptor object的时候,就会调用到这个__set__函数,我们把这个descriptor类稍微改了一下,我们让每一个这个类的object有一个val,然后写的时候呢写这val,读的时候也读这个val,这样我们实际上就是在模仿一个真实的attribute行为,我们在建立一个object之后,先打印o.x,然后把o.x写成1,再打印o.x,这里的结果就是0 1

class D:

    def __init__(self):
        self.val = 0

    def __get__(self, obj, owner=None):
        return self.val

    def __set__(self, obj, value):
        self.val = value


class A:
    x = D()


o = A()
print(o.x)
o.x = 1
print(o.x)
# 0
# 1

当然这里注意一下,descriptor这个东西是class level的,所以如果我们有另一个A的object o2,它的x也会被这个o.x写到,也就是通过这个descriptor我们实现了一个刚才我们用__getattr__跟__setattr__实现的功能

class D:

    def __init__(self):
        self.val = 0

    def __get__(self, obj, owner=None):
        return self.val

    def __set__(self, obj, value):
        self.val = value


class A:
    x = D()


o = A()
o2 = A()
print(o.x)
o.x = 1
print(o.x)
print(o2.x)

 

__delete__方法

当我们使用这个del o.x时候,会调用这个__delete__函数

class D:
    def __init__(self):
        self.val = 0

    def __get__(self, obj, owner=None):
        return self.val

    def __set__(self, obj, value):
        self.val = value

    def __delete__(self, obj):
        print("delete")


class A:
    x = D()


o = A()
del o.x
# delete

 

__slots__方法

__slots__它不是一个魔术方法,它压根就不是一个方法,但是它也算一个special names,它是有特殊的含义的,简单的说,它就是规定了这个class的object里面可以有哪些自定义的attribute,它是一个白名单机制,比如下面我们只允许让这个A的object里面,有a跟b这两个attribute,当我们在尝试做o.x = 1的时候,它就会报错,那如果我们做o.a = 1它就没事

class A:
    __slots__ = ['a', 'b']


o = A()
o.a = 1
o.x = 1
AttributeError: 'A' object has no attribute 'x'