元类上的拦截运算符查找

Posted

技术标签:

【中文标题】元类上的拦截运算符查找【英文标题】:Intercept operator lookup on metaclass 【发布时间】:2012-01-28 01:33:23 【问题描述】:

我有一个类需要对每个运算符产生一些魔力,例如__add____sub__ 等等。

我没有在类中创建每个函数,而是有一个元类,它定义了运算符模块中的每个运算符。

import operator
class MetaFuncBuilder(type):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        attr = '__01__'
        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

此外,生成的“函数”必须是绑定到所用实例的方法。

关于如何拦截此查找的任何想法?不知道是不是很清楚自己要做什么。


对于那些质疑我为什么需要这种事情的人,请查看完整代码here。 这是一个生成函数的工具(只是为了好玩),可以替代lambdas。

例子:

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

仅作记录,我不想知道做同样事情的另一种方法(我不会在类中声明每个运算符......这会很无聊,而且我的方法工作得很好: )。 我想知道如何拦截运算符的属性查找

【问题讨论】:

“我有一个类需要让每个操作符都变魔术” - 为什么?听起来你在吠叫一棵非常复杂的树...... @LennartRegebro 我正在使用某个对象上的运算符编写一个函数生成器。 f = FuncBuilder(); g = f ** 2 + 1; g(10) == 101。它不是很有用(很多函数调用),但使用起来有点有趣:D @LennartRegebro 我发布了完整的代码。 好的,所以您已经创建了一种制作 lambda 的方法。 :-) 只要你是为了好玩,没关系。 :-) 【参考方案1】:

一些黑魔法让你实现你的目标:

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 正确注意到的那样)。

注意:添加运算符方法的循环在整个程序中只执行一次,因此准备工作的开销在毫秒范围内是恒定的。

【讨论】:

好吧,看起来你的for 循环正在将所有运算符添加到类中(看看我的代码,我也这样做)。我想拥有它们:)。顺便说一句,我认为这已经是一种改进。 @JBernardo:再看一遍。它与您的代码完全不同。添加的不是创建的运算符函数,而只是 __getattr__ 调用的浅包装。这是必要的,因为正如您所说,您无法使用自定义 __getattr__ 函数拦截这些方法调用。由于循环在整个程序中只执行一次,并且运算符的数量是有限的,因此它需要毫秒范围内的恒定开销。基本上,这是一个允许您使用__getattr__ 像任何其他方法一样拦截运算符(这正是您所要求的)的技巧。 我了解您的代码(您也应该将这些 cmets 添加到答案中),但您要做的是:x + y -&gt; x.__add__ -&gt; x.__getattr__('__add__')。这是一个有趣的想法,但似乎没有运营商是不可能的。 更像x + y -&gt; x.__add__ -&gt; cached_func = x.__getattr__('__add__')。第二次直接x + y -&gt; cached_func。你是对的,因为根本没有__add__ 方法就不可能拦截加法(为什么应该这样?)。这应该是您可以找到的最接近问题的解决方案。 我接受你的回答,因为这是我能从我想要的东西中得到的最接近的答案,但我会再等一会儿再给予赏金。谢谢【参考方案2】:

当前的问题是 Python 在对象的类上查找 __xxx__ 方法,而不是在对象本身上查找 - 如果找不到,它不会回退到 __getattr____getattribute__

拦截此类调用的唯一方法是已经有一个方法。它可以是存根函数,如 Niklas Baumstark 的回答,也可以是成熟的替换函数;但是,无论哪种方式,必须已经存在某些东西,否则您将无法拦截此类调用。

如果您仔细阅读,您会注意到将 final 方法绑定到实例的要求是不可能的解决方案——您可以这样做,但 Python 永远不会调用它,因为 Python 正在查看__xxx__ 方法的实例类,而不是实例。 Niklas Baumstark 为每个实例创建唯一临时类的解决方案已尽可能满足您的要求。

【讨论】:

【参考方案3】:

看起来你把事情弄得太复杂了。您可以定义一个 mixin 类并从它继承。这比使用元类更简单,并且比使用__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__ 与常规方法查找相比速度较慢,而且由于方法存储一次(在 mixin 类上),它几乎没有节省任何东西。

【讨论】:

是的,至少有 10 个运算符(加上原位和反转形式),我不想手动编写它们并为它们中的每一个调用完全相同的函数(更改运算符)。 我现在的想法是在调用运算符时创建funcrfunc 懒惰地创建函数会给你什么? 那是因为这个类是一个函数生成器。它被设计为很少使用,并且生成的对象可以根据用户的需要多次调用。 在这种情况下,我会创建一个充满“make_adder”等函数的类。不要覆盖任何东西。然后为您需要的每个特定类从该基类子类化。【参考方案4】:

如果您想在没有元类的情况下实现您的目标,您可以将以下内容附加到您的代码中:

def get_magic_wrapper(name):
    
    def wrapper(self, *a, **kw):
        print('Wrapping')
        res = getattr(self._data, name)(*a, **kw)
        return res
    return wrapper

_magic_methods = ['__str__', '__len__', '__repr__']
for _mm in _magic_methods:
    setattr(ShowMeList, _mm, get_magic_wrapper(_mm))

它通过迭代地将这些属性添加到类中,将_magic_methods 中的方法重新路由到self._data 对象。检查它是否有效:

>>> l = ShowMeList(range(8))
>>> len(l)
Wrapping
8
>>> l
Wrapping 
[0, 1, 2, 3, 4, 5, 6, 7]
>>> print(l)
Wrapping
[0, 1, 2, 3, 4, 5, 6, 7]

【讨论】:

以上是关于元类上的拦截运算符查找的主要内容,如果未能解决你的问题,请参考以下文章

在具有智能指针的类上正确实现复制构造函数和等于运算符

为啥元类的 __call__ 方法在类上调用,而原生类的 __call__ 没有?

c++知识点总结--友元&运算符重载

学习日记0827异常处理 元类 自定义元类 自定义元类来实例化类 属性查找顺序

使用元类实现具有动态字段的描述符

C++基础语法梳理:友元类和友元函数以及using用法