source pattern making python oop design-patterns decorator metaclass

python - making - design pattern decorator



Cómo registrar automáticamente una clase cuando está definida (5)

Desde Python 3.6 no necesitas metaclases para resolver esto.

En Python 3.6 se introdujo la personalización más simple de la creación de clase ( PEP 487 ).

Un gancho __init_subclass__ que inicializa todas las subclases de una clase determinada.

La propuesta incluye el siguiente ejemplo de registro de subclase

class PluginBase: subclasses = [] def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) cls.subclasses.append(cls)

En este ejemplo, PluginBase.subclasses contendrá una lista simple de todas las subclases en todo el árbol de herencia. Se debe tener en cuenta que esto también funciona bien como una clase mixin.

Quiero tener una instancia de clase registrada cuando se define la clase. Lo ideal sería que el siguiente código hiciera el truco.

registry = {} def register( cls ): registry[cls.__name__] = cls() #problem here return cls @register class MyClass( Base ): def __init__(self): super( MyClass, self ).__init__()

Desafortunadamente, 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, así que no existe.

¿Es de alguna manera el uso de metaclases o algo así?


El problema no se debe en realidad a la línea que ha indicado, sino a la super llamada en el método __init__ . El problema sigue siendo si utiliza una metaclase como lo 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 super llamada. En el siguiente ejemplo, ni ClassWithMeta ni DecoratedClass funcionan:

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 de register se llama (ya sea por la metaclase o directamente como decorador) después de que se crea el objeto de clase , pero antes de que se haya vinculado a un nombre. Aquí es donde el super pone gnarly (en Python 2.x), porque requiere que se refiera a la clase en la super llamada, que solo puede hacer razonablemente usando el nombre global y confiando en que se habrá vinculado a ese nombre en el momento en que se invoca la super llamada. En este caso, esa confianza está fuera de lugar.

Creo que una metaclase es la solución equivocada aquí. Las metaclases son para hacer una familia de clases que tienen un comportamiento personalizado en común, exactamente como las clases son para hacer una familia de casos que tienen un 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) el uso de ganchos en el proceso de creación de clases para crear instancias de la clase, y (2) el uso de 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 esquema complejo de herencia múltiple, los problemas de super son mejores que los problemas de no usar super , y si se hereda de clases de terceros que usan super entonces tiene que usar super . Si ninguna de estas condiciones es verdadera, entonces reemplazar sus super llamadas con llamadas directas de clase base puede ser una solución razonable.

Otra forma es no enganchar el register en la creación de la clase. Agregar register(MyClass) después de cada una de sus definiciones de clase es bastante equivalente a agregar @register antes de ellos o __metaclass__ = Registered (o como se llame a la metaclase) en ellos. Sin embargo, una línea en la parte inferior es mucho menos autodocumentada que una buena declaración en la parte superior de la clase, por lo que no se siente muy bien, pero nuevamente puede ser una solución razonable.

Finalmente, puedes recurrir a los hacks que son desagradables, pero que probablemente funcionen. El problema es que se está buscando un nombre en el alcance global de un módulo justo antes de que se haya enlazado allí. Entonces puedes hacer trampa, 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. Primero verificamos si __init__ está en cls.__dict__ (a diferencia de si tiene un atributo __init__ , que siempre será verdadero). Si se hereda un método __init__ de otra clase, probablemente estemos bien (porque la superclase ya estará vinculada a su nombre de la forma habitual), y la magia que estamos a punto de hacer no funciona en el object.__init__ por lo que desea evitar intentarlo si la clase está usando un __init__ predeterminado.
  2. Buscamos el método __init__ y func_globals diccionario func_globals , que es donde func_globals las búsquedas globales (por ejemplo, para encontrar la clase a la que se hace referencia en una super llamada). Este es normalmente el diccionario global del módulo donde se definió originalmente el método __init__ . Dicho diccionario está a punto de tener el cls.__name__ insertado en él tan pronto como se register , por lo que 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 que eliminamos el enlace que creamos ya sea que la creación de una instancia genere o no una excepción; es muy poco probable que sea necesario (ya que el 99,999% de las veces el nombre está a punto de recuperarse de todos modos), pero es mejor mantener esta magia extraña tan aislada como sea 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 funcionará si se invoca 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 fallará sin embargo:

  1. Puedo imaginar una clase extraña que no tiene un método __init__ , pero hereda uno que llama self.someMethod , y someMethod está anulado en la clase que se define y hace una super llamada. Probablemente improbable.
  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 clase. Sin func_globals atributo func_globals del otro módulo, lo que significa que nuestro enlace temporal eliminará cualquier definición del nombre de esta clase en ese módulo (oops). De nuevo, improbable.
  3. Probablemente otros casos extraños 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 hacks es posible y que es imposible hacerlos absolutamente a prueba de balas.


Las respuestas aquí no funcionaron para mí en python3 , porque __metaclass__ no funcionó.

Aquí está mi código que registra todas las subclases de una clase en el momento de su definición:

registered_models = set() class RegisteredModel(type): def __new__(cls, clsname, superclasses, attributedict): newclass = type.__new__(cls, clsname, superclasses, attributedict) # condition to prevent base class registration if superclasses: registered_models.add(newclass) return newclass class CustomDBModel(metaclass=RegisteredModel): pass class BlogpostModel(CustomDBModel): pass class CommentModel(CustomDBModel): pass # prints out {<class ''__main__.BlogpostModel''>, <class ''__main__.CommentModel''>} print(registered_models)


Llamar a la clase Base directamente debería funcionar (en lugar de usar super ()):

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


Sí, las metaclases pueden hacer esto. Un método de la meta clase '' __new__ devuelve la clase, así que solo registre 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 que MetaClass no se muestra porque no ha cambiado, excepto que super(MetaClass, cls) puede convertirse en super() si lo desea):

#Python 3.x class MyClass(metaclass=MetaClass): pass

A partir de Python 3.6 también hay un nuevo método __init_subclass__ (ver PEP 487 ) que se puede usar en lugar de una meta clase (gracias a @matusko por su respuesta a continuación):

class ParentClass: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) regsiter(cls) class MyClass(ParentClass): pass

[edit: se corrigió el argumento cls faltante a super().__new__() ]

[edit: ejemplo de Python 3.x agregado]

[edit: se corrigió el orden de args a super () y se mejoró la descripción de las diferencias 3.x]

[Editar: agregar ejemplo de Python 3.6 __init_subclass__ ]