Interceptar la búsqueda del operador en metaclase

Tengo una clase que necesita hacer algo de magia con cada operador, como __add__ , __sub__ y así sucesivamente.

En lugar de crear cada función en la clase, tengo una metaclase que define a cada operador en el módulo del operador.

 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) 

El enfoque funciona bien, pero creo que sería mejor si generara el operador de reemplazo solo cuando sea necesario.

La búsqueda de operadores debe estar en la metaclase porque x + 1 se hace como type(x).__add__(x,1) lugar de x.__add__(x,1) , pero no se ve atrapado por los métodos __getattr__ ni __getattribute__ .

Eso no funciona:

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

Además, la “función” resultante debe ser un método vinculado a la instancia utilizada.

¿Alguna idea sobre cómo puedo interceptar esta búsqueda? No sé si está claro lo que quiero hacer.


Para aquellos que preguntan por qué necesito este tipo de cosas, consulte el código completo aquí . Esa es una herramienta para generar funciones ( solo por diversión ) que podrían funcionar como reemplazo de los lambda s.

Ejemplo:

 >>> f = FuncBuilder() >>> g = f ** 2 >>> g(10) 100 >>> g  

Solo para que quede constancia, no quiero saber otra manera de hacer lo mismo (no declararé a todos los operadores de la clase … eso será aburrido y el enfoque que tengo funciona bastante bien :). Quiero saber cómo interceptar la búsqueda de atributos de un operador .

Un poco de magia negra te permite alcanzar tu objective:

 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: ax + bx elif attr == "__iadd__": def iadd(self, other): self.x += other.x return self return iadd elif attr == "__mul__": return lambda a, b: ax * bx 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 ax # yay! 

Salida

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

Ahora puede usar su segundo ejemplo de código literalmente heredando de mi clase base OperatorHackiness . Incluso obtiene un beneficio adicional: __getattr__ solo se llamará una vez por instancia y operador y no hay una capa adicional de recursión involucrada para el almacenamiento en caché. Por la presente, evitamos el problema de que las llamadas a los métodos sean lentas en comparación con la búsqueda de métodos (como Paul Hankin observó correctamente).

NOTA : El bucle para agregar los métodos del operador solo se ejecuta una vez en todo el progtwig, por lo que la preparación toma una sobrecarga constante en el rango de milisegundos.

El problema que nos ocupa es que Python busca métodos __xxx__ en la clase del objeto, no en el objeto en sí, y si no se encuentra, no __getattr__ a __getattr__ ni a __getattribute__ .

La única manera de interceptar tales llamadas es tener un método ya allí. Puede ser una función de código auxiliar, como en la respuesta de Niklas Baumstark, o puede ser la función de reemplazo de pleno derecho; De cualquier manera, sin embargo, debe haber algo ya allí o no podrá interceptar tales llamadas.

Si está leyendo con atención, habrá notado que su requisito para que el método final esté vinculado a la instancia no es una solución posible; puede hacerlo, pero Python nunca lo llamará, ya que Python está mirando la clase de instancia, no la instancia, para __xxx__ métodos. La solución de Niklas Baumstark de crear una clase temporal única para cada instancia es lo más cercano a ese requisito.

Parece que estás haciendo las cosas demasiado complicadas. Puede definir una clase de mezcla y heredarla. Esto es más simple que usar metaclases y se ejecutará más rápido que usar __getattr__ .

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

Luego, cada clase que desee tener estos operadores, hereda de OperatorMixin.

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

Generar los métodos del operador cuando son necesarios no es una buena idea: __getattr__ es lento en comparación con la búsqueda de métodos normal, y como los métodos se almacenan una vez (en la clase mixin), no se guarda casi nada.