python - ¿Por qué no agregar dinámicamente un método `__call__` a una instancia de trabajo?
instance (2)
Tanto en Python 2 como en Python 3, el código:
class Foo(object):
pass
f = Foo()
f.__call__ = lambda *args : args
f(1, 2, 3)
devuelve como error El
Foo object is not callable
.
¿Por qué sucede eso?
PD: con clases de estilo antiguo funciona como se esperaba.
PPS: Este comportamiento es intencionado (ver respuesta aceptada).
Como
__call__
, es posible definir una
__call__
a nivel de clase que solo reenvía a otro miembro y establece este miembro "normal" en una implementación de
__call__
por instancia.
En las nuevas clases de estilo (por defecto
3.x
) y heredadas del objeto en
2.x
los métodos de interceptación de atributos
__getattr__
y
__getattribute__
ya no se
__getattribute__
para operaciones integradas en
métodos de instancias sobrecargadas de dunder y, en cambio, la búsqueda comienza en la clase.
La razón detrás de esto implica una tecnicidad introducida por la presencia de MetaClasses.
Como las clases son instancias de metaclases y porque las metaclases pueden definir ciertas operaciones que actúan sobre las clases, omitir la clase (que en este caso puede considerarse una
instance
) tiene sentido;
debe invocar el método definido en la metaclase que se define para procesar clases (
cls
como primer argumento).
Si se utilizó la búsqueda de instancias, la búsqueda usaría el método de clase que se define para las instancias de esa clase (
self
).
Otra (razón en disputa ) implica la optimización : dado que las operaciones integradas en instancias generalmente se invocan con mucha frecuencia, omitir la búsqueda de instancias por completo e ir directamente a la clase nos ahorra algo de tiempo. [ Fuente Lutz, Learning Python, 5ª edición ]
El
área principal
donde esto podría ser un inconveniente es cuando se crean objetos proxy que sobrecargan
__getattr__
y
__getattribute__
con la intención de reenviar las llamadas al objeto interno incrustado.
Dado que la invocación integrada omitirá la instancia por completo, no quedarán atrapados y, en consecuencia, no se reenviarán al objeto incrustado.
Una solución fácil, pero tediosa, para esto es sobrecargar realmente todos los dunders que desea interceptar en el objeto proxy. Luego, en estos dunders sobrecargados, puede delegar la llamada al objeto interno según sea necesario.
La solución más fácil que se me ocurre es establecer el atributo en la clase
Foo
con
setattr
:
setattr(Foo, ''__call__'', lambda *args: print(args))
f(1, 2, 3)
(<__main__.Foo object at 0x7f40640faa90>, 1, 2, 3)
Los métodos de doble subrayado siempre se buscan en la clase, nunca en la instancia. Consulte Búsqueda de métodos especiales para clases de estilo nuevo :
Para las clases de estilo nuevo, las invocaciones implícitas de métodos especiales solo funcionan correctamente si se definen en el tipo de un objeto, no en el diccionario de instancias del objeto.
Esto se debe a que el tipo puede necesitar admitir la misma operación (en cuyo caso, se busca el método especial en el metatipo).
Por ejemplo, las clases son invocables (así es como se produce una instancia), pero si Python buscó el método
__call__
en el objeto real
, nunca podría hacerlo en las clases que implementan
__call__
para sus instancias.
ClassObject()
se convertiría en
ClassObject.__call__()
lo que fallaría porque el parámetro
self
no se pasa al método
self
.
Entonces, en su lugar, se
type(ClassObject).__call__(ClassObject)
, y llamar a
instance()
traduce como
type(instance).__call__(instance)
.
Para evitar esto, puede agregar un método
__call__
a la clase que verifica si hay un atributo
__call__
en la clase y, si existe, lo llama.