实例属性管理__dict__

下面的测试代码中定义了三个实例属性,每个实例属性注册后都print()此时的__dict__,代码如下:

class AnotherFun:
    def __init__(self):
        self.name = "Liu"
        print(self.__dict__)
        self.age = 12
        print(self.__dict__)
        self.male = True
        print(self.__dict__)
another_fun = AnotherFun()

得到的结果显示出,每次实例属性赋值时,都会将属性名和对应值存储到__dict__字典中:

{'name': 'Liu'}
{'name': 'Liu', 'age': 12}
{'name': 'Liu', 'age': 12, 'male': True}

setattr__()与__dict

由于每次类实例进行属性赋值时都会调用__setattr__(),所以可以重载__setattr__()方法,来动态的观察每次实例属性赋值时__dict__()的变化。下面的Fun类重载了__setattr__()方法,并且将实例的属性和属性值作为__dict__的键-值对:

class Fun:
    def __init__(self):
        self.name = "Lu"
        self.age = 14
        self.male = True
        
    def __setattr__(self, key, value):
        print("*"*50)
        print("setting:{},  with:{}".format(key[], value))
        print("current __dict__ : {}".format(self.__dict__))
        # 属性注册
        self.__dict__[key] = value
fun = Fun()

通过在__setattr__()中将属性名作为key,并将属性值作为value,添加到了__dict__中,得到的结果如下:

**************************************************
setting:name,  with:Lu
current __dict__ : {}
**************************************************
setting:age,  with:14
current __dict__ : {'name': 'Lu'}
**************************************************
setting:male,  with:True
current __dict__ : {'name': 'Lu', 'age': 14}

可以看出,__init__()中三个属性赋值时,每次都会调用一次__setattr__()函数。

重载__setattr__()必须谨慎

由于__setattr__()负责在__dict__中对属性进行注册,所以自己在重载时必须进行属性注册过程,下面是__setattr__()不进行属性注册的例子:

class NotFun:
    def __init__(self):
        self.name = "Liu"
        self.age = 12
        self.male = True
    
    def __setattr__(self, key, value):
        pass
not_fun = NotFun()
print(not_fun.name)

由于__setattr__中并没有将属性注册到__dict__中,所以not_fun对象并没有name属性,因此最后的print(not_fun.name)会报出属性不存在的错误:

AttributeError                            
Traceback (most recent call last)
<ipython-input-21-6158d7aaef71> in <module>()
      8         pass
      9 not_fun = NotFun()
---> 10 print(not_fun.name)

AttributeError: 'NotFun' object has no attribute 'name'

所以,重载__setattr__时必须要考虑是否在__dict__中进行属性注册。

__getattr__(self, name)浅谈

当调用类中不存在的属性(变量或函数)时,如果有__getattr__,则会调用到__getattr__函数中。
例如:a = A(),a.foo(),当a中没有foo函数时,会自动调用a的__getattr__(self, foo)函数。
具体在实际中, __getattr__使得实现adapter wrapper模式非常容易,由于“组合优于继承”,__getattr__实现的adapter就是以组合的形式。
如果adapter需要修改adaptee的行为,
那么定义一个同名的属性就行了,其他的想直接“继承”的属性,通通交给__getattr__去自动调用adaptee的函数。
以下是一个具体的例子:

class adaptee(object):
    def foo(self):
        print ('foo in adaptee')
    def bar(self):
        print ('bar in adaptee')

class adapter(object):
    def __init__(self):
       self.adaptee = adaptee()

    def foo(self):
        print ('foo in adapter')

    def __getattr__(self, name):
       print(f"has no {name}")
       return getattr(self.adaptee, name)

if __name__ == '__main__':
    a = adapter()
    a.foo()
    a.bar()

运行结果

 foo in adapter 
 has no bar 
 bar in adaptee 
 [Finished in 0.2s]

__del__()和__delattr__()的区别

__delattr__函数何时被触发

这个问题比较简单,只要采用del语句删除类实例属性时__delattr__就会被触发,如下的测试代码:

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.layer = nn.Linear(1, 1)
    def __delattr__(self, name):
        print("del {} now".format(name))
net = Net()
del net.layer
# 此时输出:del layer now
print(net.layer)
# 仍然会输出:Linear(in_features=1, out_features=1, bias=True)

执行del net.layer时会输出“del layer now”,这就说明删除类实例属性时触动了__delattr_()方法。但是由于自己重载了__delattr__()却没有执行删除操作,所以net.layer并没有被删除(重载需谨慎)。

del语句作用于类实例和类实例属性是两种不同的情况,前者触发__del__(),而后者才触发__delattr__()
测试代码如下:

class Fun:
    def __init__(self):
        self.name = "Tom Ford"
    def __del__(self):
        print("calling __del__")
    def __delattr__(self, name):
        print("calling __delattr__")
        
fun = Fun()
del fun.name
del fun

输出如下:

calling __delattr__
calling __del__

这里还必须指出,del对象时删除的是“引用”,所以当同一个对象存在多个引用时,仅在删除最后一个引用才会触发__del__(),测试如下:

fun = Fun()
del fun_1
print("*"*20)
del fun_2
print("*"*20)
del fun

输出如下:


********************
********************
calling __del__

__setattr__和__delattr__和__getattr__的用法

class Foo:
    x = 1

    def __init__(self, y):
        self.y = y

    def __getattr__(self, item):
        print('----> from getattr:你找的属性不存在')

    def __setattr__(self, key, value):
        print('----> from setattr')
        # self.key = value  # 这就无限递归了
        # self.__dict__[key] = value  # 应该使用它

    def __delattr__(self, item):
        print('----> from delattr')
        # del self.item  # 无限递归了
        self.__dict__.pop(item)


f1 = Foo(10)

setattr

添加/修改属性会触发它的执行

print(f1.__dict__)  
# 因为你重写了__setattr__,凡是赋值操作都会触发它的运行,你啥都没写,就是根本没赋值,除非你直接操作属性字典,否则永远无法赋值
f1.z = 3
print(f1.__dict__)

delattr

删除属性的时候会触发

f1.__dict__['a'] = 3  # 我们可以直接修改属性字典,来完成添加/修改属性的操作
del f1.a
print(f1.__dict__)
----> from delattr
{}

getattr

只有在使用点调用属性且属性不存在的时候才会触发

f1.xxxxxx
----> from getattr:你找的属性不存在

getattribute

__getattribute__定义了你的属性被访问时的行为,相比较,__getattr__只有该属性不存在时才会起作用。
因此,在支持__getattribute__的Python版本,调用__getattr__前必定会调用 __getattribute__
__getattribute__同样要避免"无限递归"的错误。需要提醒的是,最好不要尝试去实现__getattribute__,因为很少见到这种做法,而且很容易出bug。
在进行属性访问控制定义的时候很可能会很容易引起“无限递归”。如下面代码:

#  错误用法 
def __setattr__(self, name, value): 
    self.name = value 
    # 每当属性被赋值的时候(如self.name = value), ``__setattr__()`` 会被调用,这样就造成了递归调用。 
    # 这意味这会调用 ``self.__setattr__('name', value)`` ,每次方法会调用自己。这样会造成程序崩溃。 
#  正确用法 
def __setattr__(self, name, value): 
    self.__dict__[name] = value  # 给类中的属性名分配值 
    # 定制特有属性

__getattribute__的用法,先看代码:

class Tree(object):
    def __init__(self,name):
        self.name = name
        self.cate = "plant"
    def __getattribute__(self,obj):
        print("哈哈")
        return object.__getattribute__(self,obj)
aa = Tree("大树")
print(aa.name)

执行结果是:

哈哈
大树

为什么会这个结果呢?

__getattribute__是属性访问拦截器,就是当这个类的属性被访问时,会自动调用类的__getattribute__方法。即在上面代码中,当我调用实例对象aa的name属性时,不会直接打印,而是把name的值作为实参传进__getattribute__方法中(参数obj是我随便定义的,可任意起名),经过一系列操作后,再把name的值返回。Python中只要定义了继承object的类,就默认存在属性拦截器,只不过是拦截后没有进行任何操作,而是直接返回。所以我们可以自己改写__getattribute__方法来实现相关功能,比如查看权限、打印log日志等。
如下代码,简单理解即可:

class Tree(object):
    def __init__(self,name):
        self.name = name
        self.cate = "plant"
    def __getattribute__(self,*args,**kwargs):
        if args[0] == "大树"
            print("log 大树")
            return "我爱大树"
        else:
            return object.__getattribute__(self,*args,**kwargs)
aa = Tree("大树")
print(aa.name)
print(aa.cate)

结果是:

log 大树
我爱大树
plant

另外,注意注意:
初学者用__getattribute__方法时,容易栽进这个坑,什么坑呢,直接看代码:

class Tree(object):
    def __init__(self,name):
        self.name = name
        self.cate = "plant"
    def __getattribute__(self,obj):
        if obj.endswith("e"):
            return object.__getattribute__(self,obj)
        else:
            return self.call_wind()
    def call_wind(self):
        return "树大招风"
aa = Tree("大树")
print(aa.name)#因为name是以e结尾,所以返回的还是name,所以打印出"大树"
print(aa.wind)#这个代码中因为wind不是以e结尾,#所以返回self.call_wind的结果,打印的是"树大招风"

上面的解释正确吗?
先说结果,关于print(aa.name)的解释是正确的,但关于print(aa.wind)的解释不对,为什么呢?我们来分析一下,执行aa.wind时,先调用__getattribute__方法,经过判断后,它返回的是self.call_wind(),即self.call_wind的执行结果,但当去调用aa这个对象的call_wind属性时,前提是又要去调用__getattribute__方法,反反复复,没完没了,形成了递归调用且没有退出机制,最终程序就挂了!