拦截元类的操作符查找

8 投票
4 回答
1156 浏览
提问于 2025-04-17 09:02

我有一个类,需要对每个运算符进行一些特殊处理,比如 __add__(加法)、__sub__(减法)等等。

为了避免在类里一个个写这些函数,我使用了一个元类,它可以定义运算符模块里的所有运算符。

import operator
class MetaFuncBuilder(type):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        attr = '__{0}{1}__'
        for op in (x for x in dir(operator) if not x.startswith('__')):
            oper = getattr(operator, op)

            # ... I have my magic replacement functions here
            # `func` for `__operators__` and `__ioperators__`
            # and `rfunc` for `__roperators__`

            setattr(self, attr.format('', op), func)
            setattr(self, attr.format('r', op), rfunc)

这个方法运行得不错,但我觉得如果能在需要的时候再生成替代运算符会更好。

运算符的查找应该在元类里进行,因为 x + 1 实际上是通过 type(x).__add__(x, 1) 来实现的,而不是 x.__add__(x, 1),但这并不会被 __getattr____getattribute__ 方法捕获。

这个方法不行:

class Meta(type):
     def __getattr__(self, name):
          if name in ['__add__', '__sub__', '__mul__', ...]:
               func = lambda:... #generate magic function
               return func

而且,生成的“函数”必须是绑定到使用的实例上的方法。

有没有什么想法可以让我拦截这个查找过程?我不确定我想做的是否说得清楚。


对于那些质疑我为什么需要这样做的人,可以查看完整代码 这里。这是一个生成函数的工具(纯粹是为了好玩),可以作为 lambda 的替代品。

举个例子:

>>> f = FuncBuilder()
>>> g = f ** 2
>>> g(10)
100
>>> g
<var [('pow', 2)]>

顺便说一下,我并不想知道其他方法来做同样的事情(我不想在类里声明每一个运算符……那样太无聊了,而且我现在的方法也很好用 :)。我想知道如何拦截运算符的属性查找。

4 个回答

1

看起来你把事情搞得太复杂了。你可以定义一个混合类,然后从这个类继承。这比使用元类简单,而且运行速度比用 __getattr__ 快。

class OperatorMixin(object):
    def __add__(self, other):
        return func(self, other)
    def __radd__(self, other):
        return rfunc(self, other)
    ... other operators defined too

然后你想要这些操作符的每个类,都可以从 OperatorMixin 继承。

class Expression(OperatorMixin):
    ... the regular methods for your class

在需要的时候生成操作符方法其实不是个好主意:因为 __getattr__ 的速度比正常的方法查找慢,而且这些方法只存储一次(在混合类上),所以几乎没有节省什么。

3

这里的问题是,Python在查找__xxx__方法时,是在对象的类里面找,而不是在对象本身里找。如果在类里找不到这个方法,它不会去使用__getattr__或者__getattribute__来补救。

要想拦截这样的调用,唯一的方法就是在类里提前定义一个方法。这个方法可以是一个简单的占位符函数,就像Niklas Baumstark的回答里提到的,或者可以是一个完整的替代函数;无论怎样,必须有一个方法存在,否则你就无法拦截这些调用。

如果你仔细阅读,你会发现让最终的方法绑定到实例上并不是一个可行的解决方案——你可以这样做,但Python永远不会调用它,因为Python查找__xxx__方法时,是看实例的类,而不是实例本身。Niklas Baumstark提出的为每个实例创建一个独特的临时类,是最接近这个要求的解决方案。

7

有一些神奇的技巧可以帮助你实现目标:

operators = ["add", "mul"]

class OperatorHackiness(object):
  """
  Use this base class if you want your object
  to intercept __add__, __iadd__, __radd__, __mul__ etc.
  using __getattr__.
  __getattr__ will called at most _once_ during the
  lifetime of the object, as the result is cached!
  """

  def __init__(self):
    # create a instance-local base class which we can
    # manipulate to our needs
    self.__class__ = self.meta = type('tmp', (self.__class__,), {})


# add operator methods dynamically, because we are damn lazy.
# This loop is however only called once in the whole program
# (when the module is loaded)
def create_operator(name):
  def dynamic_operator(self, *args):
    # call getattr to allow interception
    # by user
    func = self.__getattr__(name)
    # save the result in the temporary
    # base class to avoid calling getattr twice
    setattr(self.meta, name, func)
    # use provided function to calculate result
    return func(self, *args)
  return dynamic_operator

for op in operators:
  for name in ["__%s__" % op, "__r%s__" % op, "__i%s__" % op]:
    setattr(OperatorHackiness, name, create_operator(name))


# Example user class
class Test(OperatorHackiness):
  def __init__(self, x):
    super(Test, self).__init__()
    self.x = x

  def __getattr__(self, attr):
    print "__getattr__(%s)" % attr
    if attr == "__add__":
      return lambda a, b: a.x + b.x
    elif attr == "__iadd__":
      def iadd(self, other):
        self.x += other.x
        return self
      return iadd
    elif attr == "__mul__":
      return lambda a, b: a.x * b.x
    else:
      raise AttributeError

## Some test code:

a = Test(3)
b = Test(4)

# let's test addition
print(a + b) # this first call to __add__ will trigger
            # a __getattr__ call
print(a + b) # this second call will not!

# same for multiplication
print(a * b)
print(a * b)

# inplace addition (getattr is also only called once)
a += b
a += b
print(a.x) # yay!

输出结果

__getattr__(__add__)
7
7
__getattr__(__mul__)
12
12
__getattr__(__iadd__)
11

现在,你可以通过继承我的 OperatorHackiness 基类,直接使用你的第二段代码示例。这样做还有一个额外的好处:__getattr__ 这个方法每个实例和每个操作符只会被调用一次,而且在缓存时没有额外的递归层级。因此,我们解决了方法调用比查找方法慢的问题(正如 Paul Hankin 正确指出的那样)。

注意:添加操作符方法的循环在整个程序中只执行一次,所以准备工作只会消耗几毫秒的时间。

撰写回答