Python (et Python C API): __new__ contre __init__

126

La question que je m'apprête à poser semble être une copie de l'utilisation par Python de __new__ et __init__? , mais peu importe, je ne sais toujours pas exactement quelle est la différence pratique entre __new__et __init__.

Avant de vous précipiter pour me dire que __new__c'est pour créer des objets et __init__c'est pour initialiser des objets, laissez-moi être clair: je comprends cela. En fait, cette distinction est tout à fait naturelle pour moi, car j'ai de l'expérience en C ++ où nous avons un placement nouveau , qui sépare de la même manière l'allocation d'objets de l'initialisation.

Le didacticiel de l'API Python C l' explique comme suit:

Le nouveau membre est responsable de la création (par opposition à l'initialisation) des objets du type. Il est exposé en Python comme __new__()méthode. ... Une des raisons d'implémenter une nouvelle méthode est de garantir les valeurs initiales des variables d'instance .

Donc, oui - je comprends ce qui __new__fait, mais malgré cela, je ne comprends toujours pas pourquoi c'est utile en Python. L'exemple donné dit que cela __new__peut être utile si vous voulez "garantir les valeurs initiales des variables d'instance". Eh bien, n'est-ce pas exactement ce qui __init__va faire?

Dans le didacticiel de l'API C, un exemple est montré où un nouveau type (appelé "Noddy") est créé et la __new__fonction du type est définie. Le type Noddy contient un membre de chaîne appelé first, et ce membre de chaîne est initialisé à une chaîne vide comme ceci:

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

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

    .....
}

Notez que sans la __new__méthode définie ici, nous devrions utiliser PyType_GenericNew, qui initialise simplement tous les membres de la variable d'instance à NULL. Ainsi, le seul avantage de la __new__méthode est que la variable d'instance démarre comme une chaîne vide, par opposition à NULL. Mais pourquoi est-ce toujours utile, puisque si nous voulions nous assurer que nos variables d'instance sont initialisées à une valeur par défaut, nous aurions pu le faire dans la __init__méthode?

Channel72
la source

Réponses:

137

La différence se produit principalement entre les types mutables et immuables.

__new__accepte un type comme premier argument et renvoie (généralement) une nouvelle instance de ce type. Ainsi, il convient à une utilisation avec des types mutables et immuables.

__init__accepte une instance comme premier argument et modifie les attributs de cette instance. Cela n'est pas approprié pour un type immuable, car cela permettrait de les modifier après la création en appelant obj.__init__(*args).

Comparez le comportement de tupleet 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]

Quant à savoir pourquoi ils sont séparés (à part de simples raisons historiques): les __new__méthodes nécessitent un tas de passe-partout pour réussir (la création initiale de l'objet, puis se souvenir de renvoyer l'objet à la fin). __init__Les méthodes, en revanche, sont extrêmement simples, car vous ne définissez que les attributs que vous devez définir.

Outre le fait que les __init__méthodes sont plus faciles à écrire et la distinction mutable vs immuable notée ci-dessus, la séparation peut également être exploitée pour rendre __init__facultatif l'appel de la classe parente dans les sous-classes en configurant tous les invariants d'instance absolument nécessaires dans __new__. C'est généralement une pratique douteuse - il est généralement plus clair d'appeler simplement les __init__méthodes de la classe parent si nécessaire.

ncoghlan
la source
1
le code que vous appelez «passe-partout» __new__n'est pas passe-partout, car le passe-partout ne change jamais. Parfois, vous devez remplacer ce code particulier par quelque chose de différent.
Miles Rout
13
Créer, ou acquérir autrement, l'instance (généralement avec un superappel) et renvoyer l'instance sont des éléments nécessaires de toute __new__implémentation, et le «passe-partout» auquel je fais référence. En revanche, passest une implémentation valide pour __init__- il n'y a aucun comportement requis.
ncoghlan
37

Il existe probablement d'autres utilisations de, __new__mais il y en a une qui est vraiment évidente: vous ne pouvez pas sous-classer un type immuable sans utiliser __new__. Par exemple, supposons que vous vouliez créer une sous-classe de tuple qui ne peut contenir que des valeurs intégrales comprises entre 0 et 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)

Vous ne pouvez tout simplement pas faire cela avec __init__- si vous essayez de modifier selfdans __init__, l'interpréteur se plaindra que vous essayez de modifier un objet immuable.

expéditeur
la source
1
Je ne comprends pas pourquoi devrions-nous utiliser super? Je veux dire pourquoi new devrait-il retourner une instance de la superclasse? De plus, comme vous l'avez dit, pourquoi devrions-nous passer explicitement cls à new ? super (ModularTuple, cls) ne retourne pas une méthode liée?
Alcott
3
@Alcott, je pense que vous ne comprenez pas le comportement de __new__. Nous passons clsexplicitement à __new__car, comme vous pouvez le lire ici, nécessite __new__ toujours un type comme premier argument. Il renvoie ensuite une instance de ce type. Nous ne renvoyons donc pas une instance de la superclasse - nous renvoyons une instance de cls. Dans ce cas, c'est exactement la même chose que si on l'avait dit tuple.__new__(ModularTuple, tup).
senderle le
35

__new__()peut renvoyer des objets de types autres que la classe à laquelle il est lié. __init__()n'initialise qu'une instance existante de la classe.

>>> class C(object):
...   def __new__(cls):
...     return 5
...
>>> c = C()
>>> print type(c)
<type 'int'>
>>> print c
5
Ignacio Vazquez-Abrams
la source
C'est l'explication la plus simple à ce jour.
Tarik
Pas tout à fait vrai. J'ai des __init__méthodes qui contiennent du code qui ressemble à self.__class__ = type(...). Cela fait que l'objet est d'une classe différente de celle que vous pensiez créer. Je ne peux pas réellement le changer en un intcomme vous l'avez fait ... J'obtiens une erreur sur les types de tas ou quelque chose ... mais mon exemple de l'attribution à une classe créée dynamiquement fonctionne.
ArtOfWarfare
Moi aussi, je ne sais pas quand __init__()est appelé. Par exemple, dans la réponse de lonetwin , l'un Triangle.__init__()ou l' autre Square.__init__()est appelé automatiquement en fonction du type de __new__()retour. D'après ce que vous dites dans votre réponse (et j'ai lu cela ailleurs), il ne semble pas que l'un ou l'autre devrait l'être car il Shape.__new__() ne renvoie pas une instance de cls(ni une sous-classe de celle-ci).
martineau
1
@martineau: Les __init__()méthodes de la réponse de lonetwin sont appelées lorsque les objets individuels sont instanciés (c'est-à-dire lorsque leur __new__() méthode retourne), et non lors du Shape.__new__()retour.
Ignacio Vazquez-Abrams
Ahh, d'accord, Shape.__init__()(s'il en avait un) ne serait pas appelé. Maintenant, tout est plus logique ...:¬)
martineau
13

Pas une réponse complète mais peut-être quelque chose qui illustre la différence.

__new__sera toujours appelé lorsqu'un objet doit être créé. Il y a des situations où __init__ne sera pas appelé. Un exemple est que lorsque vous désélectionnez des objets d'un fichier pickle, ils seront alloués ( __new__) mais pas initialisés ( __init__).

Noufal Ibrahim
la source
Est-ce que j'appellerais init à partir de new si je voulais que la mémoire soit allouée et que les données soient initialisées? Pourquoi si new n'existe pas lors de la création de l'instance, init est appelé?
redpix_
2
Le travail de la __new__méthode est de créer (cela implique une allocation de mémoire) une instance de la classe et de la renvoyer. L'initialisation est une étape distincte et c'est ce qui est généralement visible par l'utilisateur. Veuillez poser une question distincte si vous rencontrez un problème spécifique.
Noufal Ibrahim
3

Je veux juste ajouter un mot sur l' intention (par opposition au comportement) de définir __new__versus __init__.

Je suis tombé sur cette question (entre autres) lorsque j'essayais de comprendre la meilleure façon de définir une fabrique de classes. J'ai réalisé que l'une des façons dont __new__est conceptuellement différente de __init__est le fait que l'avantage de __new__est exactement ce qui a été énoncé dans la question:

Donc, le seul avantage de la méthode __new__ est que la variable d'instance démarre comme une chaîne vide, par opposition à NULL. Mais pourquoi est-ce utile, puisque si nous voulions nous assurer que nos variables d'instance sont initialisées à une valeur par défaut, nous aurions pu simplement le faire dans la méthode __init__?

Compte tenu du scénario indiqué, nous nous soucions des valeurs initiales des variables d'instance lorsque l' instance est en réalité une classe elle-même. Donc, si nous créons dynamiquement un objet de classe au moment de l'exécution et que nous devons définir / contrôler quelque chose de spécial sur les instances suivantes de cette classe en cours de création, nous définirions ces conditions / propriétés dans une __new__méthode d'une métaclasse.

J'étais confus à ce sujet jusqu'à ce que je pense réellement à l'application du concept plutôt qu'à la signification de celui-ci. Voici un exemple qui, espérons-le, rendrait la différence claire:

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

Notez que ce n'est qu'un exemple de démonstration. Il y a plusieurs façons d'obtenir une solution sans recourir à une approche de fabrique de classes comme ci-dessus et même si nous choisissons d'implémenter la solution de cette manière, il y a quelques mises en garde pour des raisons de brièveté (par exemple, déclarer la métaclasse explicitement )

Si vous créez une classe régulière (alias une non-métaclasse), cela __new__n'a pas vraiment de sens à moins que ce ne soit un cas spécial comme le scénario mutable versus immuable dans la réponse de ncoghlan (qui est essentiellement un exemple plus spécifique du concept de définition les valeurs / propriétés initiales de la classe / du type en cours de création via __new__pour être ensuite initialisées via __init__).

lonetwin
la source