2011-03-04 12 views
26

Quiero tener una instancia de clase registrada cuando se define la clase. Idealmente, el código siguiente sería el truco.Cómo registrar automáticamente una clase cuando está definida

registry = {} 

def register(cls): 
    registry[cls.__name__] = cls() #problem here 
    return cls 

@register 
class MyClass(Base): 
    def __init__(self): 
     super(MyClass, self).__init__() 

Por desgracia, este código genera el error NameError: global name 'MyClass' is not defined.

Lo que está sucediendo es en la línea #problem here Estoy tratando de crear una instancia de MyClass pero el decorador no ha regresado todavía, por lo que no existe.

¿De alguna manera esto está usando metaclases o algo así?

Respuesta

31

Sí, las metaclases pueden hacer esto. Un método meta class '__new__ devuelve la clase, por lo que solo debe registrar esa clase antes de devolverla.

class MetaClass(type): 
    def __new__(cls, clsname, bases, attrs): 
     newclass = super(MetaClass, cls).__new__(cls, clsname, bases, attrs) 
     register(newclass) # here is your register function 
     return newclass 

class MyClass(object): 
    __metaclass__ = MetaClass 

El ejemplo anterior funciona en Python 2.x. En Python 3.x, la definición de MyClass es ligeramente diferente (mientras MetaClass no se muestra porque no se ha modificado - excepto que super(MetaClass, cls) puede convertirse en super() si lo desea):

#Python 3.x 

class MyClass(metaclass=MetaClass): 
    pass 

[editar: fijo falta cls argumento para super().__new__()]

[editar: añadido Python ejemplo 3.x]

[editar: orden de args a super() corregido, y una mejor descripción de las diferencias 3.x]

+3

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

+0

me salvaste la vida, gracias por el fragmento simple –

+0

@dappawit: parece que Aplery no usa esta técnica ahora. Sin embargo, funcionó muy bien para mí! –

1

llamar a la clase base debe trabajar directamente (en lugar de usar super()):

def __init__(self): 
     Base.__init__(self) 
+0

He votado porque esto es en realidad (tipo de) una respuesta correcta, y no merece 3 downvotes. Sin embargo, no hay explicación, por lo que no habría votado esta respuesta si estuviera en 0. – Ben

+0

Ciertamente: Base .__ init __ (self) es la solución más simple. – RicLeal

11

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:

  1. 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__.
  2. 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.
  3. 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:

  1. 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.
  2. 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.
  3. 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.

Cuestiones relacionadas