2011-12-26 5 views
8

Tengo una clase que necesita hacer algo de magia con cada operador, como __add__, __sub__ y así sucesivamente.Operación interceptar operador en la metaclase

En lugar de crear cada función en la clase, tengo una metaclase que define cada operador en el módulo de 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 genero el operador de reemplazo solo cuando sea necesario.

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

que 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 utilizado.

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


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

Ejemplo:

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

Sólo para que conste, yo no quiero saber otra forma de hacer lo mismo (no voy a declarar cada operador en la clase ... que habrá aburrido y el enfoque que tengo funciona bastante bien :). Quiero saber cómo interceptar la búsqueda de atributos desde un operador.

+0

"Tengo una clase que necesita hacer magia con cada operador" - ¿Por qué? Parece que está ladrando un árbol muy complicado ... –

+0

@LennartRegebro Estoy escribiendo un generador de funciones usando los operadores en algún objeto. 'f = FuncBuilder(); g = f ** 2 + 1; g (10) == 101'. No es algo muy útil (muchas llamadas a funciones), pero es algo divertido de usar: D – JBernardo

+0

@LennartRegebro Publiqué el código completo. – JBernardo

Respuesta

5

un poco de magia negro vamos a alcanzar su objetivo:

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! 

salida

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

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

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

+0

Bueno, parece que su bucle 'for' está agregando todos los operadores a la clase (mira mi código, yo también lo hago). Quiero ** no ** tenerlos :). Por cierto, creo que ya es una mejora. – JBernardo

+0

@JBernardo: mira de nuevo. Funciona completamente diferente a tu código. Lo que se agrega no son las funciones de operador creadas sino solo envoltorios poco profundos alrededor de una llamada '__getattr__'. Esto es necesario, porque, como dijiste, no puedes interceptar esas llamadas a métodos usando una función '__getattr__' personalizada. Como el ciclo solo se ejecuta _una vez en todo su programa_ y el número de operadores es finito, toma una sobrecarga constante en el rango de milisegundos. Básicamente, este es un truco que te permite usar '__getattr__' para interceptar operadores como cualquier otro método (que es exactamente lo que solicitaste). –

+0

Entiendo tu código (también debes agregar estos comentarios a la respuesta), pero lo que haces es: 'x + y -> x .__ add__ -> x .__ getattr __ ('__ add __')'. Es una idea interesante, pero parece que no tener operadores es imposible de alguna manera. – JBernardo

1

Parece que estás haciendo las cosas demasiado complicadas. Puede definir una clase mixin y heredar de ella. Esto es más simple que usar metaclases y se ejecutará más rápido que usando __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, heredará de OperatorMixin.

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

La generación de los métodos de operador cuando se necesitan no es una buena idea: __getattr__ es lento en comparación con las operaciones de búsqueda método regular, y dado que los métodos se almacenan una vez (en la clase mixin), se ahorra casi nada .

+0

Sí, hay al menos 10 operadores (más formas in situ y revertidas) y no quiero escribirlos a mano y llamar a la misma función (cambiar el operador) para cada uno de ellos. – JBernardo

+0

Mi idea ahora es * solo * crear 'func' o' rfunc' cuando se llama al operador. – JBernardo

+0

¿Qué le dará pereza la creación de las funciones? –

3

El tema en cuestión es que Python mira hacia arriba __xxx__ métodos en la clase del objeto, no en el objeto en sí mismo - y si no lo encuentra, que no se caiga de nuevo a __getattr__ ni __getattribute__.

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

Si está leyendo de cerca, habrá notado que su requisito para tener el método final 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 la instancia, no la instancia, para los métodos __xxx__. La solución de Niklas Baumstark de crear una clase de temperatura única para cada instancia es lo más cerca posible de ese requisito.

Cuestiones relacionadas