在Python中,有什么方法可以重写任意对象的双下划线(魔法)方法吗?

7 投票
2 回答
2806 浏览
提问于 2025-04-17 13:36

我想写一个包装类,这个类可以接收一个值,并且表现得和这个值一模一样,只是多了一个“原因”属性。我心里大概是这样的想法:

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()赋值还有点用的情况。

撰写回答