Python中字典类的动态运算符重载

3 投票
4 回答
2428 浏览
提问于 2025-04-15 21:55

我有一个类,它可以动态地重载一些基本的算术运算符,像这样...

import operator

class IshyNum:
    def __init__(self, n):
        self.num=n
        self.buildArith()

    def arithmetic(self, other, o):
        return o(self.num, other)

    def buildArith(self):
        map(lambda o: setattr(self, "__%s__"%o,lambda f: self.arithmetic(f, getattr(operator, o))), ["add", "sub", "mul", "div"])

if __name__=="__main__":
    number=IshyNum(5)
    print number+5
    print number/2
    print number*3
    print number-3

但是如果我把这个类改成从字典继承(class IshyNum(dict):),它就不管用了。我需要明确地定义一下def __add__(self, other)或者其他的东西,才能让它正常工作。为什么会这样呢?

4 个回答

2

对于新式类,Python在进行加法运算时,不会检查实例是否有__add__方法,而是直接检查类本身。问题在于,你把__add__方法(以及其他方法)绑定到了实例上,这样它就变成了一个绑定方法,而不是绑定到类上的未绑定方法。(其他特殊方法也是这样,你只能把它们附加到类上,而不能附加到实例上)。所以,你可能需要使用元类来实现这个功能(不过我觉得这样做挺麻烦的,因为直接写出这些方法会更容易理解)。无论如何,这里有一个使用元类的例子:

import operator

class OperatorMeta(type):
    def __new__(mcs, name, bases, attrs):
        for opname in ["add", "sub", "mul", "div"]:
            op = getattr(operator, opname)
            attrs["__%s__" % opname] = mcs._arithmetic_func_factory(op)
        return type.__new__(mcs, name, bases, attrs)

    @staticmethod
    def _arithmetic_func_factory(op):
        def func(self, other):
            return op(self.num, other)
        return func

class IshyNum(dict):
    __metaclass__ = OperatorMeta

    def __init__(self, n):
        dict.__init__(self)
        self.num=n

if __name__=="__main__":
    number=IshyNum(5)
    print number+5
    print number/2
    print number*3
    print number-3
3

一般来说,别在实例上设置 __ 方法——这些方法只支持在类上使用。(在这个例子中,问题在于它们在旧式类上能工作。别用旧式类)。

你可能想用元类,而不是你现在这样奇怪的做法。

这里有一个关于元类的教程: http://www.voidspace.org.uk/python/articles/metaclasses.shtml

4

答案在于Python有两种类型的类。

你提供的第一个代码片段使用的是一种老式的“旧风格”类(你可以通过它前面没有任何东西来判断,因为它没有继承其他类)。这种类的行为有点特别。特别是,你可以给一个实例添加一个特殊的方法:

class Foo:
   def __init__(self, num):
      self.num = num
      def _fn(other):
         return self.num + other.num
      self.__add__ = _fn

然后你会得到一个有效的响应:

>>> f = Foo(2)
>>> g = Foo(1)
>>> f + g
3

但是,继承dict意味着你正在创建一个新式类。新式类的操作符重载的行为是不同的:

class Foo (object):
   def __init__(self, num):
      self.num = num
      def _fn(other):
         return self.num + other.num
      self.__add__ = _fn
>>> f = Foo(2)
>>> g = Foo(1)
>>> f + g
Traceback ...
TypeError: unsupported operand type(s) for +: 'Foo' and 'Foo'

要让这在新式类中工作(这包括dict的子类或几乎任何其他类型),你必须确保这个特殊的方法是在类上定义的。你可以通过一个元类来做到这一点:

class _MetaFoo(type):
    def __init__(cls, name, bases, args):
        def _fn(self, other):
            return self.num + other.num
        cls.__add__ = _fn

class Foo(object):
    __metaclass__ = _MetaFoo
    def __init__(self, num):
        self.num = num

>>> f = Foo(2)
>>> g = Foo(1)
>>> f+g
3

而且,这种语义上的差异意味着在第一个例子中,我可以用一个参数定义我的本地加法方法(它使用的self是从定义它的周围环境中捕获的),但在新式类中,Python期望明确传入两个值,所以内部函数有两个参数。

正如之前的评论者提到的,最好尽量避免使用旧式类,尽量使用新式类(旧式类在Python 3及以上版本中已经被移除)。很遗憾,在这种情况下,旧式类恰好能为你工作,而新式类需要更多的代码。


编辑:

你也可以通过在上设置方法,而不是在实例上,来更接近你最初尝试的方式:

class Foo(object):
    def __init__(self, num):
        self.num = num
setattr(Foo, '__add__', (lambda self, other: self.num + other.num))
>>> f = Foo(2)
>>> g = Foo(1)
>>> f+g
3

我有时候会想到元类,而简单的解决方案可能更好 :)

撰写回答