python - Interceptar búsqueda de operador en metaclass
python-3.x operators (3)
Algo de magia negra te permite alcanzar tu 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 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 recursividad involucrada 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 su programa, por lo que la preparación requiere una sobrecarga constante en el rango de milisegundos.
Tengo una clase que necesita hacer 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 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.
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 queda 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, revisen el código completo aquí . Es una herramienta para generar funciones ( solo por diversión ) que podría funcionar como reemplazo de lambda
.
Ejemplo:
>>> f = FuncBuilder()
>>> g = f ** 2
>>> g(10)
100
>>> g
<var [(''pow'', 2)]>
Solo para el registro, 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 .
El problema es que Python busca métodos __xxx__
en la clase del objeto, no en el objeto en sí, y si no se encuentra, no vuelve a __getattr__
ni a __getattribute__
.
La única forma 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 manera, sin embargo, debe haber algo que ya esté allí o no podrá interceptar tales llamadas.
Si está leyendo atentamente, 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 del 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.
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
Entonces, cada clase que desee tener estos operadores heredará de OperatorMixin.
class Expression(OperatorMixin):
... the regular methods for your class
No es una buena idea generar los métodos del operador cuando se necesitan: __getattr__
es lento en comparación con la búsqueda de métodos regulares, y dado que los métodos se almacenan una vez (en la clase mixin), no ahorra casi nada.