sobrecarga metodos metodo __str__ python metaclass

metodos - metodo__str__ python



¿Usando el método__call__ de una metaclase en lugar de__nuevo__? (4)

Es una cuestión de fases del ciclo de vida y de lo que tiene acceso. __call__ recibe una llamada después de __new__ y pasa los parámetros de inicialización antes de que se pasen a __init__ , de modo que pueda manipularlos. Prueba este código y estudia su salida:

class Meta(type): def __new__(cls, name, bases, newattrs): print "new: %r %r %r %r" % (cls, name, bases, newattrs,) return super(Meta, cls).__new__(cls, name, bases, newattrs) def __call__(self, *args, **kw): print "call: %r %r %r" % (self, args, kw) return super(Meta, self).__call__(*args, **kw) class Foo: __metaclass__ = Meta def __init__(self, *args, **kw): print "init: %r %r %r" % (self, args, kw) f = Foo(''bar'') print "main: %r" % f

Al discutir las metaclases, los documentos establecen:

Por supuesto, también puede anular otros métodos de clase (o agregar nuevos métodos); por ejemplo, la definición de un método __call__() en la metaclase permite un comportamiento personalizado cuando se llama a la clase, por ejemplo, no siempre creando una nueva instancia.

Mis preguntas son: supongamos que quiero tener un comportamiento personalizado cuando se llama a la clase, por ejemplo, almacenar en caché en lugar de crear objetos nuevos. Puedo hacer esto anulando el método __new__ de la clase. ¿Cuándo querría definir una metaclase con __call__ en __call__ lugar? ¿Qué da este enfoque que no se puede lograr con __new__ ?


La respuesta directa a su pregunta es: cuando desea hacer algo más que personalizar la creación de una instancia, o cuando desea separar qué hace la clase de cómo se crea.

Vea mi respuesta a Crear un singleton en Python y la discusión asociada.

Hay varias ventajas.

  1. Te permite separar lo que hace la clase de los detalles de cómo se crea. La metaclase y la clase son responsables de una cosa.

  2. Puede escribir el código una vez en una metaclase y usarlo para personalizar el comportamiento de las llamadas de varias clases sin preocuparse por la herencia múltiple.

  3. Las subclases pueden anular el comportamiento en su método __new__ , pero __call__ en una metaclase ni siquiera tiene que llamar a __new__ en absoluto.

  4. Si hay trabajo de configuración, puede hacerlo en el método __new__ de la metaclase, y solo ocurre una vez, en lugar de cada vez que se llama a la clase.

Ciertamente, hay muchos casos en los que personalizar __new__ funciona igual de bien si no te preocupa el principio de responsabilidad única.

Pero hay otros casos de uso que tienen que ocurrir antes, cuando se crea la clase, en lugar de cuando se crea la instancia. Es cuando estos entran a jugar que una metaclase es necesaria. Consulte ¿Cuáles son sus casos de uso (concretos) para metaclases en Python? para un montón de grandes ejemplos.


Las diferencias sutiles se vuelven un poco más visibles cuando observa cuidadosamente el orden de ejecución de estos métodos.

class Meta_1(type): def __call__(cls, *a, **kw): print "entering Meta_1.__call__()" rv = super(Meta_1, cls).__call__(*a, **kw) print "exiting Meta_1.__call__()" return rv class Class_1(object): __metaclass__ = Meta_1 def __new__(cls, *a, **kw): print "entering Class_1.__new__()" rv = super(Class_1, cls).__new__(cls, *a, **kw) print "exiting Class_1.__new__()" return rv def __init__(self, *a, **kw): print "executing Class_1.__init__()" super(Class_1,self).__init__(*a, **kw)

Tenga en cuenta que el código anterior no hace nada más que registrar lo que estamos haciendo. Cada método difiere a su implementación principal, es decir, su valor predeterminado. Entonces, además del registro, es como si simplemente hubieras declarado las cosas de la siguiente manera:

class Meta_1(type): pass class Class_1(object): __metaclass__ = Meta_1

Y ahora vamos a crear una instancia de Class_1

c = Class_1() # entering Meta_1.__call__() # entering Class_1.__new__() # exiting Class_1.__new__() # executing Class_1.__init__() # exiting Meta_1.__call__()

Por lo tanto, si type es el padre de Meta_1 podemos imaginar una pseudo implementación del type.__call__() como tal:

class type: def __call__(cls, *args, **kwarg): # ... a few things could possibly be done to cls here... maybe... or maybe not... # then we call cls.__new__() to get a new object obj = cls.__new__(cls, *args, **kwargs) # ... a few things done to obj here... maybe... or not... # then we call obj.__init__() obj.__init__(*args, **kwargs) # ... maybe a few more things done to obj here # then we return obj return obj

El aviso de la orden de llamada anterior Meta_1.__call__() que Meta_1.__call__() (o en este caso el type.__call__() ) tiene la oportunidad de influir en si se Class_1.__new__() o no llamadas a Class_1.__new__() y Class_1.__init__() . En el transcurso de su ejecución, Meta_1.__call__() podría devolver un objeto que ninguno de los dos haya tocado. Tomemos, por ejemplo, este enfoque para el patrón de singleton:

class Meta_2(type): __Class_2_singleton__ = None def __call__(cls, *a, **kw): # if the singleton isn''t present, create and register it if not Meta_2.__Class_2_singleton__: print "entering Meta_2.__call__()" Meta_2.__Class_2_singleton__ = super(Meta_2, cls).__call__(*a, **kw) print "exiting Meta_2.__call__()" else: print ("Class_2 singleton returning from Meta_2.__call__(), " "super(Meta_2, cls).__call__() skipped") # return singleton instance return Meta_2.__Class_2_singleton__ class Class_2(object): __metaclass__ = Meta_2 def __new__(cls, *a, **kw): print "entering Class_2.__new__()" rv = super(Class_2, cls).__new__(cls, *a, **kw) print "exiting Class_2.__new__()" return rv def __init__(self, *a, **kw): print "executing Class_2.__init__()" super(Class_2, self).__init__(*a, **kw)

Observemos lo que sucede cuando se intenta crear un objeto de tipo Class_2

a = Class_2() # entering Meta_2.__call__() # entering Class_2.__new__() # exiting Class_2.__new__() # executing Class_2.__init__() # exiting Meta_2.__call__() b = Class_2() # Class_2 singleton returning from Meta_2.__call__(), super(Meta_2, cls).__call__() skipped c = Class_2() # Class_2 singleton returning from Meta_2.__call__(), super(Meta_2, cls).__call__() skipped print a is b is c True

Ahora observe esta implementación usando un método de clase '' __new__() para tratar de lograr lo mismo.

import random class Class_3(object): __Class_3_singleton__ = None def __new__(cls, *a, **kw): # if singleton not present create and save it if not Class_3.__Class_3_singleton__: print "entering Class_3.__new__()" Class_3.__Class_3_singleton__ = rv = super(Class_3, cls).__new__(cls, *a, **kw) rv.random1 = random.random() rv.random2 = random.random() print "exiting Class_3.__new__()" else: print ("Class_3 singleton returning from Class_3.__new__(), " "super(Class_3, cls).__new__() skipped") return Class_3.__Class_3_singleton__ def __init__(self, *a, **kw): print "executing Class_3.__init__()" print "random1 is still {random1}".format(random1=self.random1) # unfortunately if self.__init__() has some property altering actions # they will affect our singleton each time we try to create an instance self.random2 = random.random() print "random2 is now {random2}".format(random2=self.random2) super(Class_3, self).__init__(*a, **kw)

Observe que la implementación anterior, aunque registra con éxito un singleton en la clase, no impide que se __init__() , esto sucede implícitamente en el type.__call__() (siendo el type la metaclase predeterminada si no se especifica ninguno). Esto podría llevar a algunos efectos no deseados:

a = Class_3() # entering Class_3.__new__() # exiting Class_3.__new__() # executing Class_3.__init__() # random1 is still 0.282724600824 # random2 is now 0.739298365475 b = Class_3() # Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped # executing Class_3.__init__() # random1 is still 0.282724600824 # random2 is now 0.247361634396 c = Class_3() # Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped # executing Class_3.__init__() # random1 is still 0.282724600824 # random2 is now 0.436144427555 d = Class_3() # Class_3 singleton returning from Class_3.__new__(), super(Class_3, cls).__new__() skipped # executing Class_3.__init__() # random1 is still 0.282724600824 # random2 is now 0.167298405242 print a is b is c is d # True


Una diferencia es que al definir un método de metaclase __call__ , está exigiendo que se llame antes que cualquiera de los métodos de clase o subclases __new__ tenga la oportunidad de ser llamado.

class MetaFoo(type): def __call__(cls,*args,**kwargs): print(''MetaFoo: {c},{a},{k}''.format(c=cls,a=args,k=kwargs)) class Foo(object): __metaclass__=MetaFoo class SubFoo(Foo): def __new__(self,*args,**kwargs): # This never gets called print(''Foo.__new__: {a},{k}''.format(a=args,k=kwargs)) sub=SubFoo() foo=Foo() # MetaFoo: <class ''__main__.SubFoo''>, (),{} # MetaFoo: <class ''__main__.Foo''>, (),{}

Tenga en cuenta que SubFoo.__new__ nunca se llama. En cambio, si define Foo.__new__ sin una metaclase, permite que las subclases anulen Foo.__new__ .

Por supuesto, podrías definir MetaFoo.__call__ para llamar a cls.__new__ , pero eso depende de ti. Al negarse a hacerlo, puede evitar que las subclases tengan su método __new__ llamado.

No veo una ventaja convincente para usar una metaclase aquí. Y como "Simple es mejor que complejo", recomiendo usar __new__ .