La différence entre __new__ et __init__ en Python


Les méthodes __new__ et __init__ n’ont rien de spécial. Ce sont des méthodes ordinaires. Mais parce qu’elles sont nommées ainsi, Python les détecte et les appelle automatiquement a un moment précis.

Ce moment, c’est ce qui différencie __init__ de __new__.

__init__ pour initialiser

__init__ est la méthode qui va être appelée automatiquement après qu’un objet ai été crée. Ce n’est pas un contructeur du tout, c’est un initialiseur.

Si vous faîtes ça:

>>> class Premiere(object):
...         
...         def __init__(self, prix):
...                 print "%s euros" % prix
...         
>>> c = Premiere(10000)
10000 euros

A la ligne c = Premiere(10000), Python va créer une instance de la classe Première(). Il va ensuite immédiatement et automatiquement appeler __init__ en lui passant cette instance en premier argument et les paramètres passés par l’appel Premiere(paramètres). Donc, quand __init__ est appelé, l’objet instancié existe déjà.

On va utiliser __init__ pour initialiser l’objet, c’est à dire pour lui donner son état de départ: changer les attributs, configurer l’objet par rapports aux arguments, etc.

Dans tous les autres langages, on utiliserait le constructeur pour faire ce boulot. Pas en Python.

L’avantage de __init__, c’est qu’il est très facile à manipuler. Il n’y a pas de magie dangereuse dans __init__: on a l’objet tout neuf, et les arguments passés à l’instancitation, on peut donc manipuler l’objet sans se soucier du reste. Ici on attache deux attributs à l’instance self:

 >>> class Premiere(object):
...         discount = False
...         def __init__(self, prix):
...                 self.prix = prix
...                 if self.prix < 5000:
...                     self.discount = True
...         
>>> c = Premiere(10000)
>>> c.discount
False

Comme en Python les attributs sont dynamiques, on peut attacher un argument même si l’instance ne le déclare pas, et il est créé automatiquement.

En résumé: __init__ est appelé automatiquement APRES la création de l’objet, et on met dedans le code d’initialisation de l’objet (généralement une modification des attributs pour leur donner leur état de départ).

__new__ pour créer

__new__ est le vrai constructeur. Pour cette raison, elle doit retourner un objet.

>>> class Premiere(object):
...
...     def __new__(cls, prix):
...        print "%s euros" % prix
...        return super(Premiere, cls).__new__(cls)
...
>>> c = Premiere(10000)
10000 euros

__new__ est appelée AVANT la création de l’objet, car c’est son boulot de créer l’instance et de la retourner. Comme on ne sait pas retourner une instance nous même (enfin si, mais pas dans cet article :-)), on appelle super() pour utiliser la méthode __new__ de object et créer une instance pour cette classe.

L’objet créé sera ensuite passé à __init__ automatiquement par Python.

On utilise rarement __new__. Les deux cas principaux sont:

  • si on hérite d’un type immutable (str, int, tuple, etc), __new__ est le seul endroit où on puisse initialiser l’objet.
  • dans le cas des métaclasses.

En résumé: __new__ est le vrai constructeur, il est appelé pour créer l’objet, et l’objet ainsi instancié est passé à __init__. Vous n’avez presque aucune raison de vous en servir, c’est vraiment pour les cas particuliers.

Voici l’ordre d’éxécution:

>>> class Premiere(object):
...         def __new__(cls, prix):
...                 print "__new__"
...                 return super(Premiere, cls).__new__(cls)
...         def __init__(self, *args):
...                 print "__init__"
 
>>> c = Premiere(10000)
__new__
__init__

Exemple d’utilisation de __new__

Généralement on sait très bien utiliser __init__, mais __new__ est moins évident.

L’usage le plus fréquent de __new__ quand on hérite d’objets immutables. Par exemple, si vous voulez faire un objet Temperature qui hérite de float et qui accepte une unité en plus, ceci ne va pas marcher:

class Temperature(float):
 
    def __init__(self, value, unit):
 
        super(Temperature, self).__init__(value)
        self.unit = unit
 
    def __str__(self):
        return "%s%s" % (self.value, self.unit)
 
print Temperature(10, '°C')
 
Traceback (most recent call last):
  File "<ipython-input-1-65b676255e09>", line 11, in <module>
    Temperature(10, '°C')
TypeError: float() takes at most 1 argument (2 given)

La raison est que du fait de la nature immutable de float, il est initialisé dans __new__, et il n’attend aucune valeur de plus dans __new__, mais on lui passe malgré tout (via Temperature(10, '°C')).

En revanche, ceci va marcher:

class Temperature(float):
 
    def __new__(cls, value, unit):
 
        instance = super(Temperature, cls).__new__(cls, value)
        instance.unit = unit
        return instance
 
    def __str__(self):
        return "%s%s" % (super(Temperature, self).__str__(), self.unit)
 
print Temperature(10, '°C')
10.0°C

Comme on override __new__, on lui donne la possibilité d’accepter une argument de plus.

Un autre exemple serait de vouloir créer une chaîne de caractères qui est toujours en majuscule (ce qui est bien moins utile que l’exemple précédent):

class CapsLockString(str):
 
    def __init__(self, value):
 
        print value # et maintenant je fais quoi ?
 
print CapsLockString('test')
test
test

Ça ne plantera pas, mais il n’y a rien que nous puissions faire car str est immutable. On ne peut tout simplement pas faire quoique ce soit avec value. Avec __new__, on peut faire quelque chose sur la chaîne intermédiaire:

class CapsLockString(str):
 
    def __new__(cls, value):
 
        return super(CapsLockString, cls).__new__(cls, value.upper())
 
print CapsLockString('test')
TEST

Deux chaînes sont en fait créées, une normale, puis une en majuscule retournée par upper() qui va servir de valeur à notre objet (en fait il y en a même 3 dans l’implémentation CPython, c’est pour ça que les notations littérales sont plus rapides que l’usage des classes pour créer des built-in).

__new__ permet donc essentiellement de créer de jolis API. On l’utilise par ailleurs dans les metaclasses, mais ce sera pour un autre article.

Un troisième usage de __new__, assez rare (mais en même temps utiliser __new__ est déjà rare), c’est le pattern factory. Les javaistes le connaissent bien, c’est un motif de conception qui permet de gérer la création d’objets qui peuvent eux même créer des objets, qui créer des objets qui… Bref.

Car en fait __new__ peut retourner n’importe quoi. Il peut retourner toujours la même instance pour faire un singleton par exemple. On peut même carrément renvoyer un truc qui n’a rien n’a voir, par exemple une fonction :

class FonctionFactory(object):
 
    def __new__(self, value, repeat):
 
        def repeater(string=value):
 
            return string * repeat
 
        return repeater
 
 
>>> function = FonctionFactory('hello', 2) # création de la fonction
>>> print function()
hellohello
>>> print function('bonjour')
bonjourbonjour

Ici on retourne carrément une fonction, et pas du tout une instance de FonctionFactory() comme prévu. On pourrait faire ceci de manière plus simple avec de la programmation fonctionnelle, mais __new__ permet de bénéficier de tout l’outillage de la POO.

21 thoughts on “La différence entre __new__ et __init__ en Python

  • amirouche

    Des use cases pour le __new__ des type builtins aurait fait de cette article un article incontournable. Tanpis intéressant malgré tout.

  • Sam Post author

    Tu as raison, je pourrais rajouter ça. Par contre, juste le demander sans chichi aurait fait de ce commentaire un commentaire plus agréable à lire. Tanpis intéressant malgré tout.

  • Sam Post author

    J’ai mis quelques exemples en plus du coup. Dis moi si ça mérite des ajouts.

  • Amirouche

    Super, j’ai appris plein de truc, mes idées sont plus clairs. J’avais déjà vu un exemple de ChaineOrdonnable qui héritais de str et ça faisait que la comparaison était faites sur la deuxieme valeur passé en argument, par exemple ça donnait:

    »»» ChaineOrdonnable('Sam', 1) &gt; 
    ChaineOrdonnable('Amirouche', 0)

    Chez moi quand je fait super(str, cls).__new__(cls, value) il me dit

    TypeError: basestring.__new__(foo) is not safe, use str.__new__()

    Merci à toi.

  • Krypted

    Très bon article! Merci.
    Si quelqu’un a des exemples de __new__ utilisé en production ce serait pas mal.

    Donc d’après ce que je comprends, on utilise __new__quand on veut utiliser des méthodes d’une classe immutable c’est ça ?

    Un petit article sur les immutables pourrait peut être intéresser d’autres personnes aussi :)

    (Ah et hors sujet total: http://www.linfo.re/Un-python-voyage-sur-l-aile-d-un-avion
    Héhé ça embouche un coin aux fans de PHP ça hein! C’est pas un éléphant qui ferait ça!)

  • Sam Post author

    @Kontre: j’avais vu ça. Je pense qu’ils font ça pour des raisons de perf.

    @Amirouche: si j’ai pas le code, je peux pas debug :-)

  • stoof

    Pourquoi faire compliqué quand on peux faire simple. Très bon post.

  • jpcw

    s/immutable/immuable/g
    ça sonne quand même beaucoup mieux, et ça n’entache plus la qualité de l’article.

  • Sam Post author

    @Krypted: peewee a un exemple d’utilisation de __new__ en prod. Il faut le lire lentement par contre :-)

    @jpcw: Oui mais dans les docs on lit immutable partout, donc je garde volontairement le terme pour qu’un débutant puisse faire facilement le lien. Il faut faire des concessions.

  • Gilles Lenfant

    On pourra aussi noter que new est la seule classmethod implicite de Python (il n’est pas nécessaire de précéder la méthode new du décorateur @classmethod)

  • ast

    Dans cette classe:

    class Premiere(object):

    ... def new(cls, prix):

    ... print "%s euros" % prix

    ... return super(Premiere, cls).new(cls)

    je m’étonne qu’il faille passer cls en argument de new

    Quand on utilise super() pour aller chercher le init d’une classe parente, on utilise la syntaxe:

    super(MyClass, self).init()

    il n’y a pas le self passé en argument du init(). il est passé automatiquement.

    Alors pourquoi cls est passé en argument de new ?

  • Sam Post author

    La réponse est assez surprenante:

    >>> class Foo:
    ...     pass
    
    >>> Foo.__new__
     # note l'id
    >>> type(Foo.__new__(Foo)) # note l'effet
    
    
    >>> object.__new__ 
     # meme id !
    >>> type(object.__new__(Foo)) # meme effet !
    
    
    

    __new__ est n’est pas une méthode de Foo mais de object, qui est attachée à Foo. C’est aussi une méthode statique. Toute classe custo en Python hérite de object, mais comme new est appelée très souvent, plutot que de faire un look up au parent à chaque fois ce qui est couteux, Python utilise cette astuce pour avoir la meme methode partout.

  • ast

    Une petite coquille dans la classe Temperature:

    def init(self, value, unit):

    …super(Temperature, cls).init(value)

    remplacer cls par self

    ça ne change pas le message d’erreur car l’erreur se produit avant, à l’appel de new

  • ast

    Ah new est une méthode statique. Dans ce cas Gilles Lenfant s’est trompé dans son commentaire, il dit que c’est une classmethod

  • Daxley

    Mais du coup en héritant float on peut aditionner des objets température avec un + ?

    Du style une pseudo surcharge d’opérateur comme en C++

    Et si on peut, peut on aussi renvoyer une exception dans le cas où l’unité ne serait pas la même ?

  • Sam Post author

    Yep:

    class Temperature(int):
     
        def __new__(cls, val, unit):
            return super().__new__(cls, val)
     
        def __init__(self, val, unit):
            super().__init__()
            self.unit = unit
     
        def __str__(self):  
            return '%g %s' % (self, self.unit)
     
        def __add__(self, other):
            if self.unit != other.unit:
                raise ValueError("Choux et carottes, connard !")
            return Temperature(super().__add__(other), self.unit)
     
     
    t1 = Temperature(1, "C")
    t2 = Temperature(2, "C")
    t3 = Temperature(2, "K")
     
    print(t1 + t2)
    print(t2 + t3)

    Mais bon autant utiliser une lib existante type https://pypi.python.org/pypi/units/

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.