元类上的拦截运算符查找
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。
这是一个生成函数的工具(只是为了好玩),可以替代lambda
s。
例子:
>>> 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 -> x.__add__ -> x.__getattr__('__add__')
。这是一个有趣的想法,但似乎没有运营商是不可能的。
更像x + y -> x.__add__ -> cached_func = x.__getattr__('__add__')
。第二次直接x + y -> 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 个运算符(加上原位和反转形式),我不想手动编写它们并为它们中的每一个调用完全相同的函数(更改运算符)。 我现在的想法是只在调用运算符时创建func
或rfunc
。
懒惰地创建函数会给你什么?
那是因为这个类是一个函数生成器。它被设计为很少使用,并且生成的对象可以根据用户的需要多次调用。
在这种情况下,我会创建一个充满“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__ 没有?