在Python中,有什么方法可以重写任意对象的双下划线(魔法)方法吗?
我想写一个包装类,这个类可以接收一个值,并且表现得和这个值一模一样,只是多了一个“原因”属性。我心里大概是这样的想法:
class ExplainedValue(object):
def __init__(self, value, reason):
self.value = value
self.reason = reason
def __getattribute__(self, name):
print '__getattribute__ with %s called' % (name,)
if name in ('__str__', '__repr__', 'reason', 'value'):
return object.__getattribute__(self, name)
value = object.__getattribute__(self, 'value')
return object.__getattribute__(value, name)
def __str__(self):
return "ExplainedValue(%s, %s)" % (
str(self.value),
self.reason)
__repr__ = __str__
不过,双下划线开头的函数似乎不能通过 __getattribute__
来捕获,比如说:
>>> numbers = ExplainedValue([1, 2, 3, 4], "it worked")
>>> numbers[0]
Traceback (most recent call last):
File "<pyshell#118>", line 1, in <module>
numbers[0]
TypeError: 'ExplainedValue' object does not support indexing
>>> list(numbers)
__getattribute__ with __class__ called
Traceback (most recent call last):
File "<pyshell#119>", line 1, in <module>
list(numbers)
TypeError: 'ExplainedValue' object is not iterable
我本以为上面的两个应该会这样做:
>>> numbers.value[0]
__getattribute__ with value called
1
>>> list(numbers.value)
__getattribute__ with value called
[1, 2, 3, 4]
为什么会这样呢?我该怎么才能让它发生呢?(虽然这在实际代码中可能不是个好主意,但我现在对这个技术问题很感兴趣。)
2 个回答
2
为了后人留个记录,这是我想到的:
class BaseExplainedValue(object):
def __init__(self, value, reason):
self.value = value
self.reason = reason
def __getattribute__(self, name):
if name in ('value', 'reason'):
return object.__getattribute__(self, name)
value = object.__getattribute__(self, 'value')
return object.__getattribute__(value, name)
def __str__(self):
return "<'%s' explained by '%s'>" % (
str(self.value),
str(self.reason))
def __unicode__(self):
return u"<'%s' explained by '%s'>" % (
unicode(self.value),
unicode(self.reason))
def __repr__(self):
return "ExplainedValue(%s, %s)" % (
repr(self.value),
repr(self.reason))
force_special_methods = set(
"__%s__" % name for name in (
'lt le eq ne gt ge cmp rcmp nonzero call len getitem setitem delitem iter reversed contains getslice setslice delslice' + \
'add sub mul floordiv mod divmod pow lshift rshift and xor or div truediv' + \
'radd rsub rmul rdiv rtruediv rfloordiv rmod rdivmod rpow rlshift rrshift rand rxor ror' + \
'iadd isub imul idiv itruediv ifloordiv imod ipow ilshift irshift iand ixor ior' + \
'neg pos abs invert complex int long float oct hex index coerce' + \
'enter exit').split(),
)
def make_special_method_wrapper(method_name):
def wrapper(self, *args, **kwargs):
return getattr(self, method_name)(*args, **kwargs)
wrapper.__name__ = method_name
return wrapper
def EXP(obj, reason="no reason provided"):
if isinstance(obj, BaseExplainedValue):
return obj
class ThisExplainedValue(BaseExplainedValue):
pass
#special-case the 'special' (underscore) methods we want
obj_class = obj.__class__
for method_name in dir(obj_class):
if not (method_name.startswith("__") and method_name.endswith("__")): continue
method = getattr(obj_class, method_name)
if method_name in force_special_methods:
setattr(ThisExplainedValue, method_name, make_special_method_wrapper(method_name))
ThisExplainedValue.__name__ = "%sExplainedValue" % (obj_class.__name__,)
return ThisExplainedValue(obj, reason)
用法:
>>> success = EXP(True, "it went ok")
>>> if success:
print 'we did it!'
we did it!
>>> success = EXP(False, "Server was on fire")
>>> if not success:
print "We failed: %s" % (EXP(success).reason,)
We failed: Server was on fire
这些解释过的值可以和它们所包装的值互换使用:
>>> numbers = EXP([1, 2, 3, 4, 5], "method worked ok")
>>> numbers
ExplainedValue([1, 2, 3, 4, 5], 'method worked ok')
>>> numbers[3]
4
>>> del numbers[3]
>>> numbers
ExplainedValue([1, 2, 3, 5], 'method worked ok')
它甚至能欺骗 isinstance
(解释可以在这里找到):
>>> isinstance(EXP(False), bool)
True
>>> isinstance(EXP([]), list)
True
5
正如millimoose所说,隐式调用__foo__
时不会经过__getattribute__
。你能做的就是把合适的函数添加到你的包装类里面。
class Wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
for dunder in ('__add__', '__sub__', '__len__', ...):
locals()[dunder] = lambda self, __f=dunder, *args, **kwargs: getattr(self.wrapped, __f)(*args, **kwargs)
obj = [1,2,3]
w = Wrapper(obj)
print len(w)
类的主体就像其他代码块一样被执行(当然,def
除外);你可以在里面放循环或者其他任何你想放的东西。它们之所以有点特别,是因为在代码块结束时,整个本地作用域会被传递给type()
来创建这个类。
这可能是唯一一个对locals()
赋值还有点用的情况。