再谈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。