再谈python的属性拦截

__setattr__是python重载协议中的内容,用于在实例中设置属性。

实际使用方式:

class Old(object):
    def __init__(self):
        pass
    
    def __setattr__(self, key, value):
        print "__setattr__: ", key, value
        object.__setattr__(self, key, value)
    
    def __getattribute__(self, key):
        print "__getattribute__: ", key
        return object.__getattribute__(self, key)
    
    def __getattr__(self, key):
        print "run __getattr__ in undefinied param: ", key
        return object.__getattribute__(self, key)

o = Old()
o.ok = 123
print o.ok
print o.undef

运行结果:

__setattr__:  ok 123 

__getattribute__:  ok  
123

__getattribute__:  undef

run __getattr__ in undefinied param:  undef

Traceback (most recent call last):
  File "dict_object.py", line 36, in <module>
    print o.undef
  File "dict_object.py", line 31, in __getattr__
    return object.__getattribute__(self, key)
AttributeError: 'Old' object has no attribute 'undef'

o.undef 是一个为定义的实例属性,会被__getattr__捕捉到,但是在实例上没有这个属性,最后还是会报错。

但是如果我们现在的需求是这样,预先在实例的__init__函数上上设置一个变量,例如:self.data = {}

我们让接下来的__setattr__和__getattribute__或__getattr__的存取属性都在self.data的字典中进行,如果还是使用上面的代码,将会带来极大的麻烦。

因为我们首先要取得实例的data属性,即self.data,但是这个动作又必须经过__getattr__或__getattribute__操作,可能会这样写:

class Old(object):
    def __init__(self):
        self.data = {}
    
    def __setattr__(self, key, value):
        print "__setattr__: ", key, value
        data = object.__getattribute__(self, 'data')
        data[key] = value
    
    def __getattribute__(self, key):
        print "__getattribute__: ", key
        return object.__getattribute__(self, key)
    
    def __getattr__(self, key):
        print "run __getattr__ in undefinied param: ", key
        return object.__getattribute__(self, key)

o = Old()
o.ok = 123
print dir(o)

运行结果:

__setattr__:  data {}
Traceback (most recent call last):
  File "dict_object.py", line 35, in <module>
    o = Old()
  File "dict_object.py", line 19, in __init__
    self.data = {}
  File "dict_object.py", line 23, in __setattr__
    data = object.__getattribute__(self, 'data')
AttributeError: 'Old' object has no attribute 'data'

我们甚至连self.data都没有赋值成功! 为什么呢? 还是因为这一句:

data = object.__getattribute__(self, 'data')

因为__init__里面赋值data给self要经过__getattribute__的拦截,但是此时实例上还没有data属性,上面一行代码当然会报属性错误!

OK,我们再加上异常捕获怎么样? 尝试忽略这个异常,让我们试试:(不过在尝试之前,大家可能想到结果了)

class Old(object):
    def __init__(self):
        self.data = {}
    
    def __setattr__(self, key, value):
        print "__setattr__: ", key, value
        try:
            data = object.__getattribute__(self, 'data')
            data[key] = value
        except:
            pass
    
    def __getattribute__(self, key):
        print "__getattribute__: ", key
        return object.__getattribute__(self, key)
    
    def __getattr__(self, key):
        print "run __getattr__ in undefinied param: ", key
        return object.__getattribute__(self, key)

o = Old()
o.ok = 123
print dir(o)

运行结果:

__setattr__:  data {}
__setattr__:  ok 123
__getattribute__:  __dict__
__getattribute__:  __members__
run __getattr__ in undefinied param:  __members__
__getattribute__:  __methods__
run __getattr__ in undefinied param:  __methods__
__getattribute__:  __class__
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattr__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

OK,运行成功,代码没有报错。中间淡色的部分可以忽略,它是dir函数获取实例上属性的过程。 注意加粗的部分,实例上根本没有data这个属性!

所以这个方法行不通。

那咋办捏?

有一个办法,在《Python学习手册》第4版中,提到了用实例的__dict__命名空间来预先分配好实例的属性:

class Old(object):
    def __init__(self):
        self.__dict__['data'] = {}
    
    def __setattr__(self, key, value):
        print "__setattr__: ", key, value
        #data = object.__getattribute__(self, 'data')
        #data[key] = value
        # 或者直接这样:
        self.data[key] = value
    
    def __getattribute__(self, key):
        print "__getattribute__: ", key
        return object.__getattribute__(self, key)
    
    def __getattr__(self, key):
        print "run __getattr__ in undefinied param: ", key
        return object.__getattribute__(self, key)

o = Old()
o.ok = 123
print o.data

OK,这样没问题了。

如果我们既想简单模拟类似dict类的工作方式,又想实现以属性的方式访问,我们可以直接重载__getitem__和__setitem__和__delitem__方法,并使用上面的代码:

class Old(object):
    def __init__(self):
        self.__dict__['data'] = {}
    
    def __setattr__(self, key, value):
        self.data[key] = value
    
    def __getattr__(self, key):
        # 这里我们用了一个取巧的方法
        # 因为self.__dict__还是会被__getattribute__拦截到,导致无限递归
        # 所以不能用它来实现
        return self.data[key]
    
    def __setitem__(self, key, value):
        self.data[key] = value
    
    def __getitem__(self, key):
        try:
            return self.data[key]
        except IndexError:
            pass
    
    def __delitem__(self, key):
        del self.data[key]

o = Old()
o.first = 123
print o.first

o['second'] = 456
print o['second']

特别说明:

在上面的代码中,属性访问使用了__getattr__,虽然只有在实例未定义该属性时才会被__getattr__拦截。

但是因为我们的__setattr__已经把属性保存在了self.data里面,所以每次__getattr__都会被调用,算是一种投机取巧的方式。

最终的原因还是因为,self.__dict__也会被__getattribute__拦截,所以不能在__getattribute__中访问self.__dict。

原文地址:https://www.cnblogs.com/huazi/p/2802080.html