¿Cómo se hereda correctamente de una superclase que tiene un método __new__?

Supongamos que tenemos una clase ‘Parent’, que por alguna razón tiene __new__ definido y una clase ‘Child’ que la hereda. (En mi caso, estoy tratando de heredar de una clase de terceros que no puedo modificar)

 class Parent: def __new__(cls, arg): # ... something important is done here with arg 

Mi bash fue:

 class Child(Parent): def __init__(self, myArg, argForSuperclass): Parent.__new__(argForSuperclass) self.field = myArg 

Pero mientras

 p = Parent("argForSuperclass") 

funciona como se espera

 c = Child("myArg", "argForSuperclass") 

falla, porque ‘Child’ intenta llamar al método __new__ que hereda de ‘Parent’ en lugar de a su propio método __init__ .

¿Qué tengo que cambiar en ‘Niño’ para obtener el comportamiento esperado?

En primer lugar, no se considera una buena práctica anular __new__ exactamente para evitar estos problemas … Pero no es tu culpa, lo sé. Para tales casos, la mejor práctica para anular __new__ es hacer que acepte parámetros opcionales …

 class Parent(object): def __new__(cls, value, *args, **kwargs): print 'my value is', value return object.__new__(cls, *args, **kwargs) 

… para que los niños puedan recibir lo suyo:

 class Child(Parent): def __init__(self, for_parent, my_stuff): self.my_stuff = my_stuff 

Entonces, funcionaría:

 >>> c = Child(2, "Child name is Juju") my value is 2 >>> c.my_stuff 'Child name is Juju' 

Sin embargo, el autor de su clase padre no fue tan sensato y le dio este problema:

 class Parent(object): def __new__(cls, value): print 'my value is', value return object.__new__(cls) 

En este caso, simplemente anule __new__ en el hijo, haciéndole aceptar parámetros opcionales, y llame al __new__ los padres allí:

 class Child(Parent): def __new__(cls, value, *args, **kwargs): return Parent.__new__(cls, value) def __init__(self, for_parent, my_stuff): self.my_stuff = my_stuff 

El hijo no llama a Parent’s __new__ lugar de su propio __init__ , llama a su propio __new__ (heredado de Parent) antes de __init__ .

Debe comprender para qué sirven estos métodos y cuándo los llamará Python. Se llama a __new__ para crear un objeto de instancia, y luego se llama a __init__ para inicializar los atributos de esa instancia. Python pasará los mismos argumentos a ambos métodos: los argumentos pasados ​​a la clase para comenzar el proceso.

Entonces, lo que hace cuando hereda de una clase con __new__ es exactamente lo que hace cuando hereda cualquier otro método que tenga una firma diferente en el padre de la que quiere que tenga. __new__ anular __new__ para recibir los argumentos del niño y llamar a Parent.__new__ con los argumentos que espera. Luego reemplaza __init__ por separado (aunque con la misma lista de argumentos y la misma necesidad de llamar a __init__ de __init__ con su propia lista de argumentos esperada).

Sin embargo, hay dos dificultades potenciales que podrían interponerse en su camino, porque __new__ es para personalizar el proceso de obtención de una nueva instancia. No hay razón para que tenga que crear una nueva instancia (podría encontrar una que ya existe) y, de hecho, ni siquiera tiene que devolver una instancia de la clase. Esto puede llevar a los siguientes problemas:

  1. Si __new__ devuelve un objeto existente (por ejemplo, porque Parent espera poder almacenar en caché sus instancias), entonces es posible que se __init__ su __init__ sobre objetos ya existentes, lo que puede ser muy malo si se supone que sus objetos tienen un estado variable. Pero entonces, si el Padre espera poder hacer este tipo de cosas, probablemente esperará un montón de restricciones sobre la forma en que se usa, y la subclasificará de manera que se rompan arbitrariamente esas restricciones (por ejemplo, agregar un estado mutable a una clase que espera ser inmutable para que pueda almacenar en caché y reciclar sus instancias) es casi seguro que no va a funcionar, incluso si puede inicializar correctamente sus objetos. Si esto sucede, y no hay ninguna documentación que indique lo que debe hacer con la subclase Parent, entonces es probable que el tercero no tenga la intención de que usted haga una subclase de Parent, y las cosas van a ser difíciles. para ti. Sin embargo, lo mejor que puedes hacer es intentar mover toda tu inicialización a __new__ lugar de __init__ , por más feo que sea.

  2. Python solo llama al método __init__ si el método ‘ __new__ clase en realidad devuelve una instancia de la clase. Si Parent está utilizando su método __new__ como una especie de “función de fábrica” ​​para devolver objetos de otras clases, entonces es muy probable que las subclases fracasen, a menos que todo lo que tenga que hacer sea cambiar la forma en que funciona la “fábrica”.

Como lo entiendo, cuando llamas Child("myarg", "otherarg") , eso significa algo como esto:

 c = Child.__new__(Child, "myarg", "otherarg") if isinstance(c, Child): c.__init__("myarg", "otherarg") 

Tú podrías:

  1. Escriba un constructor alternativo, como Child.create_with_extra_arg("myarg", "otherarg") , que ejemplifica a Child("otherarg") antes de hacer cualquier otra cosa que necesite.

  2. Override Child.__new__ , algo como esto:

.

 def __new__(cls, myarg, argforsuperclass): c = Parent.__new__(cls, argforsuperclass) c.field = myarg return c 

No he probado eso. __new__ puede ser confuso rápidamente, por lo que es mejor evitarlo si es posible.