El problema no es causado por la línea que has indicado, pero por el super
llamada en el método __init__
. El problema persiste si usas una metaclase como sugiere dappawit; La razón por la que el ejemplo de esa respuesta funciona es simplemente que dappawit ha simplificado su ejemplo al omitir la clase Base
y, por lo tanto, la llamada super
. En el siguiente ejemplo, ni ClassWithMeta
ni DecoratedClass
trabajo:
registry = {}
def register(cls):
registry[cls.__name__] = cls()
return cls
class MetaClass(type):
def __new__(cls, clsname, bases, attrs):
newclass = super(cls, MetaClass).__new__(cls, clsname, bases, attrs)
register(newclass) # here is your register function
return newclass
class Base(object):
pass
class ClassWithMeta(Base):
__metaclass__ = MetaClass
def __init__(self):
super(ClassWithMeta, self).__init__()
@register
class DecoratedClass(Base):
def __init__(self):
super(DecoratedClass, self).__init__()
El problema es el mismo en ambos casos; la función register
se llama (ya sea por la metaclase o directamente como un decorador) después de que se crea el objeto de clase , pero antes se ha vinculado a un nombre. Aquí es donde super
se vuelve complicado (en Python 2.x), ya que requiere que consulte la clase en la llamada super
, que solo puede hacer razonablemente al usar el nombre global y confiando en que se habrá vinculado a ese nombre cuando se invoca la llamada super
. En este caso, esa confianza está fuera de lugar.
Creo que una metaclase es la solución incorrecta aquí. Las metaclases son para formar una familia de clases que tienen algún comportamiento personalizado en común, exactamente como las clases son para hacer una familia de instancias que tienen algún comportamiento personalizado en común. Todo lo que estás haciendo es llamar a una función en una clase. No definiría una clase para llamar a una función en una cadena, tampoco debería definir una metaclase para llamar a una función en una clase.
Entonces, el problema es una incompatibilidad fundamental entre: (1) usar ganchos en el proceso de creación de clases para crear instancias de la clase, y (2) usar super
.
Una forma de resolver esto es no usar super
. super
resuelve un problema difícil, pero introduce otros (este es uno de ellos). Si está usando un complejo esquema de herencia múltiple, los problemas de super
son mejores que los problemas de no usar super
, y si hereda de clases de terceros que usan super
, entonces tiene que usar super
. Si ninguna de esas condiciones es verdadera, entonces simplemente reemplazar sus llamadas super
con llamadas de clase base directas puede ser una solución razonable.
Otra forma es no enganchar register
en la creación de clase. Agregar register(MyClass)
después de cada una de las definiciones de clase es bastante equivalente a agregar @register
antes que ellos o __metaclass__ = Registered
(o lo que sea que llame la metaclase) en ellos. Sin embargo, una línea en la parte inferior es mucho menos auto-documentada que una buena declaración en la parte superior de la clase, por lo que no parece genial, pero de nuevo, puede ser una solución razonable.
Finalmente, puede recurrir a los piratas informáticos que son desagradables, pero probablemente funcionen. El problema es que se está buscando un nombre en el alcance global de un módulo justo antes de que se ha vinculado allí. Por lo que podría engañar, de la siguiente manera:
def register(cls):
name = cls.__name__
force_bound = False
if '__init__' in cls.__dict__:
cls.__init__.func_globals[name] = cls
force_bound = True
try:
registry[name] = cls()
finally:
if force_bound:
del cls.__init__.func_globals[name]
return cls
Así es como funciona esto:
- primera Verificamos si
__init__
está en cls.__dict__
(a diferencia de si tiene un atributo __init__
, que siempre ser cierto). Si se hereda un método __init__
de otra clase, probablemente estemos bien (porque la superclase será ya vinculada a su nombre de la forma habitual), y la magia que estamos a punto de hacer no funciona en object.__init__
por lo que quiero evitar intentar eso si la clase está usando un predeterminado __init__
.
- Buscamos el método
__init__
y tomamos su diccionario func_globals
, que es donde irán las búsquedas globales (como para encontrar la clase mencionada en una llamada super
). Este es normalmente el diccionario global del módulo donde originalmente se definió el método __init__
. Tal diccionario es aproximadamente para tener el cls.__name__
insertado en él tan pronto como register
regrese, entonces solo lo insertamos nosotros mismos temprano.
- Finalmente creamos una instancia y la insertamos en el registro. Esto está en un bloque try/finally para asegurarnos de eliminar el enlace que creamos, ya sea que crear una instancia arroje una excepción o no. esto es muy poco probable que sea necesario (ya que el 99.999% del tiempo el nombre está a punto de rebote), pero es mejor mantener la magia extraña como esta aislada lo más posible para minimizar la posibilidad de que algún día otra magia extraña interactúe mal con eso.
Esta versión de register
va a funcionar ya sea invocada como un decorador o por la metaclase (que todavía creo que no es un buen uso de una metaclase).Hay algunos casos oscuros, donde se producirá un error sin embargo:
- puedo imaginar una clase extraña de que no lo hace tienen un método
__init__
pero hereda una que llama self.someMethod
, y someMethod
se redefine en la clase que se está definiendo y realiza una llamada super
. Probablemente poco probable.
- El método
__init__
podría haberse definido en otro módulo originalmente y luego usarse en la clase haciendo __init__ = externally_defined_function
en el bloque de clases. Sin embargo, el atributo func_globals
del otro módulo, lo que significa que nuestro enlace temporal anularía cualquier definición del nombre de esta clase en ese módulo (oops). De nuevo, poco probable.
- Probablemente otros casos extraños en los que no he pensado.
Puede intentar agregar más hacks para hacerlo un poco más robusto en estas situaciones, pero la naturaleza de Python es que este tipo de ataques es posible y que es imposible hacerlos a prueba de balas.
Por cierto, aquí hay un ejemplo del código del mundo real que hace exactamente esto (este no es mi código, pero he usado mucho la biblioteca). Mira las líneas 67-90 (a partir de mi escrito esto). https://github.com/ask/celery/blob/master/celery/task/base.py – dappawit
me salvaste la vida, gracias por el fragmento simple –
@dappawit: parece que Aplery no usa esta técnica ahora. Sin embargo, funcionó muy bien para mí! –