Python (y Python C API): __nuevo__ versus __init__

126

¿La pregunta que estoy por hacer parece ser un duplicado del uso que hace Python de __new__ y __init__? , pero independientemente, todavía no me queda claro cuál es la diferencia práctica entre __new__y __init__.

Antes de que te apresures a decirme eso __new__ es para crear objetos y __init__para inicializar objetos, déjame ser claro: lo entiendo. De hecho, esa distinción es bastante natural para mí, ya que tengo experiencia en C ++ donde tenemos una ubicación nueva , que separa de manera similar la asignación de objetos de la inicialización.

El tutorial de Python C API lo explica así:

El nuevo miembro es responsable de crear (en lugar de inicializar) objetos del tipo. Está expuesto en Python como __new__()método. ... Una razón para implementar un nuevo método es asegurar los valores iniciales de las variables de instancia .

Entonces, sí, entiendo lo que __new__hace, pero a pesar de esto, yo todavía no entiendo por qué es útil en Python. El ejemplo dado dice que __new__podría ser útil si desea "asegurar los valores iniciales de las variables de instancia". Bueno, ¿no es eso exactamente lo __init__que hará?

En el tutorial de la API de C, se muestra un ejemplo donde se crea un nuevo Tipo (llamado "Noddy"), y el Tipo __new__ se define función . El tipo Noddy contiene un miembro de cadena llamado first, y este miembro de cadena se inicializa en una cadena vacía de la siguiente manera:

static PyObject * Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    .....

    self->first = PyString_FromString("");
    if (self->first == NULL)
    {
       Py_DECREF(self);
       return NULL;
    }

    .....
}

Tenga en cuenta que sin el __new__método definido aquí, tendríamos que usar PyType_GenericNew, que simplemente inicializa todos los miembros de la variable de instancia a NULL. Entonces, el único beneficio del __new__método es que la variable de instancia comenzará como una cadena vacía, en lugar de NULL. Pero, ¿por qué es esto útil, ya que si nos preocupamos por asegurarnos de que nuestras variables de instancia se inicialicen con algún valor predeterminado, podríamos haberlo hecho en el __init__método?

Canal72
fuente

Respuestas:

137

La diferencia surge principalmente con los tipos mutables vs inmutables.

__new__acepta un tipo como primer argumento y (generalmente) devuelve una nueva instancia de ese tipo. Por lo tanto, es adecuado para su uso con tipos mutables e inmutables.

__init__acepta una instancia como primer argumento y modifica los atributos de esa instancia. Esto es inapropiado para un tipo inmutable, ya que permitiría que se modifiquen después de la creación llamando aobj.__init__(*args) .

Compare el comportamiento de tupley list:

>>> x = (1, 2)
>>> x
(1, 2)
>>> x.__init__([3, 4])
>>> x # tuple.__init__ does nothing
(1, 2)
>>> y = [1, 2]
>>> y
[1, 2]
>>> y.__init__([3, 4])
>>> y # list.__init__ reinitialises the object
[3, 4]

En cuanto a por qué están separados (aparte de razones históricas simples): los __new__métodos requieren un montón de repeticiones para acertar (la creación inicial del objeto, y luego recordar devolver el objeto al final). __init__los métodos, por el contrario, son muy simples, ya que solo establece los atributos que necesita establecer.

Además de que los __init__métodos son más fáciles de escribir, y la distinción mutable frente a inmutable mencionada anteriormente, la separación también se puede explotar para hacer que llamar a la clase padre __init__en las subclases sea opcional al configurar cualquier instancia invariante absolutamente necesaria __new__. Sin embargo, esta es generalmente una práctica dudosa: por lo general, es más claro llamar a los __init__métodos de la clase principal según sea necesario.

ncoghlan
fuente
1
el código al que se refiere como "boilerplate" __new__no es boilerplate, porque boilerplate nunca cambia. A veces necesita reemplazar ese código en particular con algo diferente.
Miles Rout
13
Crear, o adquirir de otro modo, la instancia (generalmente con una superllamada) y devolver la instancia son partes necesarias de cualquier __new__implementación, y la "referencia" a la que me refiero. Por el contrario, passes una implementación válida para __init__: no se requiere ningún comportamiento en absoluto.
ncoghlan
37

Probablemente hay otros usos para, __new__pero hay uno realmente obvio: no se puede subclasificar un tipo inmutable sin usar __new__. Entonces, por ejemplo, supongamos que desea crear una subclase de tupla que pueda contener solo valores integrales entre 0 y size.

class ModularTuple(tuple):
    def __new__(cls, tup, size=100):
        tup = (int(x) % size for x in tup)
        return super(ModularTuple, cls).__new__(cls, tup)

Simplemente no se puede hacer esto con __init__- si se trató de modificar selfen __init__, el intérprete se queja de que usted está tratando de modificar un objeto inmutable.

senderle
fuente
1
No entiendo por qué deberíamos usar super? Quiero decir, ¿por qué nuevo debería devolver una instancia de la superclase? Además, como lo expresó, ¿por qué deberíamos pasar cls explícitamente a new ? super (ModularTuple, cls) no devuelve un método enlazado?
Alcott
3
@Alcott, creo que estás malinterpretando el comportamiento de __new__. Pasamos clsexplícitamente a __new__porque, como puede leer aquí, __new__ siempre requiere un tipo como primer argumento. Luego devuelve una instancia de ese tipo. Entonces, no estamos devolviendo una instancia de la superclase, estamos devolviendo una instancia de cls. En este caso, es lo mismo que si hubiéramos dicho tuple.__new__(ModularTuple, tup).
senderle
35

__new__()puede devolver objetos de tipos distintos de la clase a la que está vinculado. __init__()solo inicializa una instancia existente de la clase.

>>> class C(object):
...   def __new__(cls):
...     return 5
...
>>> c = C()
>>> print type(c)
<type 'int'>
>>> print c
5
Ignacio Vazquez-Abrams
fuente
Esta es la explicación más ligera hasta ahora.
Tarik
No del todo cierto. Tengo __init__métodos que contienen un código similar self.__class__ = type(...). Eso hace que el objeto sea de una clase diferente a la que creías que estabas creando. Realmente no puedo cambiarlo a uno intcomo lo hiciste ... Recibo un error sobre los tipos de montón o algo así ... pero mi ejemplo de asignarlo a una clase creada dinámicamente funciona.
ArtOfWarfare
Yo también estoy confundido acerca de cuándo __init__()se llama. Por ejemplo, en la respuesta de lonetwin , se llama automáticamente Triangle.__init__()o Square.__init__()automáticamente según el tipo de __new__()retorno. Por lo que dices en tu respuesta (y he leído esto en otra parte), no parece que ninguno de ellos deba serlo, ya Shape.__new__() que no está devolviendo una instancia de cls(ni una de una subclase de ella).
Martineau
1
@martineau: Los __init__()métodos en la respuesta de lonetwin se llaman cuando los objetos individuales se instancian (es decir, cuando su __new__() método regresa), no cuando Shape.__new__()regresa.
Ignacio Vazquez-Abrams
Ahh, cierto, Shape.__init__()(si tuviera uno) no se llamaría. Ahora todo tiene más sentido ...:¬)
Martineau
13

No es una respuesta completa, pero quizás algo que ilustre la diferencia.

__new__siempre se llamará cuando se deba crear un objeto. Hay algunas situaciones en las __init__que no se llamará. Un ejemplo es cuando desengancha objetos de un archivo pickle, se asignarán ( __new__) pero no se inicializarán ( __init__).

Noufal Ibrahim
fuente
¿ Llamaría a init desde nuevo si quisiera asignar memoria y inicializar datos? Por ello, si la nueva no existe al crear una instancia de inicio es llamada?
redpix_
2
El trabajo del __new__método es crear (esto implica la asignación de memoria) una instancia de la clase y devolverla. La inicialización es un paso separado y es lo que generalmente es visible para el usuario. Haga una pregunta por separado si hay un problema específico que enfrenta.
Noufal Ibrahim
3

Solo quiero agregar una palabra sobre la intención (en oposición al comportamiento) de definir __new__versus__init__ .

Encontré esta pregunta (entre otras) cuando intentaba comprender la mejor manera de definir una fábrica de clase. Me di cuenta de que una de las formas en las que __new__es conceptualmente diferente __init__es el hecho de que el beneficio de __new__es exactamente lo que se dijo en la pregunta:

Por lo tanto, el único beneficio del método __new__ es que la variable de instancia comenzará como una cadena vacía, en lugar de NULL. Pero, ¿por qué es esto útil, ya que si nos preocupamos por asegurarnos de que nuestras variables de instancia se inicialicen con algún valor predeterminado, podríamos haberlo hecho en el método __init__?

Teniendo en cuenta el escenario establecido, nos preocupamos por los valores iniciales de las variables de instancia cuando la instancia es en realidad una clase en sí misma. Entonces, si estamos creando dinámicamente un objeto de clase en tiempo de ejecución y necesitamos definir / controlar algo especial sobre las instancias posteriores de esta clase que se está creando, definiríamos estas condiciones / propiedades en un__new__ método de una metaclase.

Estaba confundido sobre esto hasta que realmente pensé en la aplicación del concepto en lugar de solo el significado del mismo. Aquí hay un ejemplo que esperamos aclare la diferencia:

a = Shape(sides=3, base=2, height=12)
b = Shape(sides=4, length=2)
print(a.area())
print(b.area())

# I want `a` and `b` to be an instances of either of 'Square' or 'Triangle'
# depending on number of sides and also the `.area()` method to do the right
# thing. How do I do that without creating a Shape class with all the
# methods having a bunch of `if`s ? Here is one possibility

class Shape:
    def __new__(cls, sides, *args, **kwargs):
        if sides == 3:
            return Triangle(*args, **kwargs)
        else:
            return Square(*args, **kwargs)

class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return (self.base * self.height) / 2

class Square:
    def __init__(self, length):
        self.length = length

    def area(self):
        return self.length*self.length

Tenga en cuenta que este es solo un ejemplo demoníaco. Hay varias maneras de obtener una solución sin recurrir a un enfoque de fábrica de clase como el anterior e incluso si elegimos implementar la solución de esta manera, hay algunas pequeñas advertencias por razones de brevedad (por ejemplo, declarar la metaclase explícitamente). )

Si está creando una clase regular (también conocida como no metaclase), entonces __new__ no tiene sentido a menos que sea un caso especial como el escenario mutable versus inmutable en la respuesta de respuesta de ncoghlan (que es esencialmente un ejemplo más específico del concepto de definición los valores / propiedades iniciales de la clase / tipo que se crean a través de __new__para luego ser inicializados a través de __init__).

solitario
fuente