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:
- Primero verificamos si
__init__
está encls.__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 elobject.__init__
por lo que desea evitar intentarlo si la clase está usando un__init__
predeterminado. - Buscamos el método
__init__
yfunc_globals
diccionariofunc_globals
, que es dondefunc_globals
las búsquedas globales (por ejemplo, para encontrar la clase a la que se hace referencia en unasuper
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 elcls.__name__
insertado en él tan pronto como seregister
, por lo que 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 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:
- Puedo imaginar una clase extraña que no tiene un método
__init__
, pero hereda uno que llamaself.someMethod
, ysomeMethod
está anulado en la clase que se define y hace unasuper
llamada. Probablemente improbable. - 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. Sinfunc_globals
atributofunc_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. - 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__
]