Définition de __new__ en Python
La méthode __new__ est une méthode spéciale (ou dunder method) en Python qui intervient avant la méthode __init__ lors de la création d'un objet. Tandis que __init__ se charge d'initialiser une instance déjà créée, __new__ est responsable de la création même de cette instance en mémoire. C'est donc la toute première étape du cycle de vie d'un objet Python.
Concrètement, lorsque vous écrivez MonObjet(), Python appelle d'abord MonObjet.__new__(MonObjet) pour créer l'instance, puis MonObjet.__init__(instance) pour l'initialiser. La plupart du temps, vous n'avez pas besoin de redéfinir __new__, car le comportement par défaut hérité de la class object suffit. Cependant, cette méthode devient indispensable dans certains cas avancés que nous allons explorer en détail.
Si vous souhaitez approfondir ce sujet et maîtriser l'ensemble de la programmation orientée objet en Python, nous vous recommandons de suivre notre formation complète sur Python qui couvre ce type de concepts avancés.
Comprendre le rôle de __new__ dans la création d'objets
Pour bien comprendre __new__, il est essentiel de distinguer les deux phases de l'instanciation d'un objet en Python :
| Étape | Méthode | Rôle | Type de méthode |
|---|---|---|---|
| 1. Création | __new__ | Crée et retourne une nouvelle instance | Méthode statique implicite |
| 2. Initialisation | __init__ | Configure l'instance déjà créée | Méthode d'instance |
La méthode __new__ reçoit la classe comme premier argument (conventionnellement nommé cls), contrairement à __init__ qui reçoit l'instance (nommée self). C'est logique : au moment où __new__ est appelée, l'instance n'existe pas encore.
__new__ est techniquement une méthode statique qui est traitée de manière spéciale par Python. Elle doit impérativement retourner une instance, sinon __init__ ne sera jamais appelée.
Signature de __new__
Voici la signature typique de __new__ :
class MaClasse:
def __new__(cls, *args, **kwargs):
instance = super().__new__(cls)
return instance
def __init__(self, *args, **kwargs):
# Initialisation de l'instance
passVous remarquerez que __new__ appelle super().__new__(cls) pour déléguer la création réelle de l'objet à la classe parente (généralement object). C'est cette instruction qui alloue la mémoire et crée l'instance brute.
Le flux complet d'instanciation
Voici un exemple qui illustre l'ordre d'appel des méthodes :
class Demonstration:
def __new__(cls, valeur):
print(f"1. __new__ est appelé avec cls={cls.__name__}")
instance = super().__new__(cls)
print(f"2. Instance créée : {instance}")
return instance
def __init__(self, valeur):
print(f"3. __init__ est appelé avec self={self}")
self.valeur = valeur
print(f"4. Initialisation terminée, valeur={self.valeur}")
obj = Demonstration(42)
# Affiche :
# 1. __new__ est appelé avec cls=Demonstration
# 2. Instance créée : <__main__.Demonstration object at 0x...>
# 3. __init__ est appelé avec self=<__main__.Demonstration object at 0x...>
# 4. Initialisation terminée, valeur=42Ce flux montre clairement que __new__ précède toujours __init__ et que l'instance retournée par __new__ est exactement celle qui est passée à __init__ en tant que self.
Cas d'utilisation pratiques de __new__
Bien que la redéfinition de __new__ soit un cas avancé, il existe plusieurs situations où cette méthode s'avère indispensable. Voici les principaux cas d'utilisation.
1. Le patron Singleton
Le Singleton est un patron de conception (design pattern) qui garantit qu'une class ne peut avoir qu'une seule instance. C'est l'un des cas d'utilisation les plus classiques de __new__ :
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, nom=None):
if nom is not None:
self.nom = nom
# Test du Singleton
a = Singleton("Premier")
b = Singleton("Second")
print(a is b) # True : c'est le même objet
print(a.nom) # "Second" (réinitialisé par le second appel)
print(id(a) == id(b)) # TrueAttention : même si __new__ retourne la même instance, __init__ est appelée à chaque fois. Cela peut provoquer des réinitialisations inattendues. Pour éviter cela, vous pouvez ajouter un drapeau pour n'initialiser qu'une seule fois.
Voici une version améliorée qui évite la réinitialisation :
class SingletonSafe:
_instance = None
_initialized = False
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, nom=None):
if not SingletonSafe._initialized:
self.nom = nom
SingletonSafe._initialized = True
a = SingletonSafe("Premier")
b = SingletonSafe("Second")
print(a.nom) # "Premier" (pas réinitialisé)
print(b.nom) # "Premier"
2. Sous-classer des types immuables
Les types immuables comme int, str, tuple ou frozenset ne peuvent pas être modifiés après leur création. Puisque __init__ intervient après la création, il est trop tard pour changer la valeur d'un objet immuable. Vous devez donc utiliser __new__ :
class PositiveInt(int):
"""Un entier qui est toujours positif (valeur absolue)."""
def __new__(cls, valeur):
# On transforme la valeur AVANT la création de l'objet
return super().__new__(cls, abs(valeur))
print(PositiveInt(-42)) # 42
print(PositiveInt(15)) # 15
print(type(PositiveInt(-7))) # Voici un autre exemple avec les chaînes de caractères :
class UpperStr(str):
"""Une chaîne de caractères toujours en majuscules."""
def __new__(cls, contenu):
return super().__new__(cls, contenu.upper())
texte = UpperStr("bonjour le monde")
print(texte) # "BONJOUR LE MONDE"
print(type(texte)) # Ce mécanisme est impossible avec __init__ seul, car les objets immuables sont déjà "figés" au moment où __init__ est appelée. C'est pourquoi __new__ est indispensable ici.
3. Système de registre et factory pattern
Vous pouvez utiliser __new__ pour implémenter un système de registre qui retourne automatiquement la bonne sous-classe en fonction des arguments :
class Animal:
_registry = {}
def __init_subclass__(cls, type_animal=None, **kwargs):
super().__init_subclass__(**kwargs)
if type_animal:
Animal._registry[type_animal] = cls
def __new__(cls, type_animal, nom):
if cls is Animal:
sous_classe = cls._registry.get(type_animal, cls)
return super().__new__(sous_classe)
return super().__new__(cls)
def __init__(self, type_animal, nom):
self.nom = nom
def parler(self):
return "..."
class Chat(Animal, type_animal="chat"):
def parler(self):
return f"{self.nom} fait : Miaou !"
class Chien(Animal, type_animal="chien"):
def parler(self):
return f"{self.nom} fait : Ouaf !"
# Utilisation
animal1 = Animal("chat", "Moustache")
animal2 = Animal("chien", "Rex")
print(type(animal1)) #
print(animal1.parler()) # Moustache fait : Miaou !
print(type(animal2)) #
print(animal2.parler()) # Rex fait : Ouaf !Ce patron est très puissant pour créer des architectures extensibles où de nouvelles sous-classes peuvent être ajoutées sans modifier le code existant.
4. Cache d'instances (Object Pool)
Vous pouvez utiliser __new__ pour réutiliser des instances existantes au lieu d'en créer de nouvelles, ce qui est utile pour optimiser la mémoire :
class Couleur:
_cache = {}
def __new__(cls, nom):
nom_normalise = nom.lower().strip()
if nom_normalise in cls._cache:
return cls._cache[nom_normalise]
instance = super().__new__(cls)
cls._cache[nom_normalise] = instance
return instance
def __init__(self, nom):
self.nom = nom.lower().strip()
rouge1 = Couleur("Rouge")
rouge2 = Couleur("rouge")
rouge3 = Couleur(" ROUGE ")
print(rouge1 is rouge2) # True
print(rouge1 is rouge3) # True
print(len(Couleur._cache)) # 1
__new__ et les métaclasses
La méthode __new__ joue également un rôle crucial dans les métaclasses. Une métaclasse est une class dont les instances sont elles-mêmes des classes. La méthode __new__ d'une métaclasse contrôle donc la création des classes, pas des instances :
class MetaValidation(type):
"""Métaclasse qui vérifie que chaque classe a une docstring."""
def __new__(mcs, name, bases, namespace):
if not namespace.get('__doc__'):
raise TypeError(
f"La classe '{name}' doit avoir une docstring."
)
return super().__new__(mcs, name, bases, namespace)
class MonService(metaclass=MetaValidation):
"""Service principal de l'application."""
def executer(self):
pass
# Ceci lèverait TypeError :
# class ServiceSansDoc(metaclass=MetaValidation):
# passDans ce contexte, __new__ reçoit quatre arguments : la métaclasse (mcs), le nom de la classe à créer, ses bases (classes parentes) et son espace de noms (dict contenant ses attributs et méthodes). C'est un outil puissant pour valider ou transformer des classes lors de leur définition.
Différences entre __new__ et __init__
Récapitulons les différences fondamentales entre ces deux méthodes pour éviter toute confusion :
| Caractéristique | __new__ | __init__ |
|---|---|---|
| Premier argument | cls (la classe) | self (l'instance) |
| Rôle | Créer l'instance | Initialiser l'instance |
| Valeur de retour | Doit retourner l'instance | Ne retourne rien (None) |
| Ordre d'appel | Appelée en premier | Appelée en second |
| Type de méthode | Méthode statique implicite | Méthode d'instance |
| Fréquence de redéfinition | Rarement | Très souvent |
| Utilisation principale | Types immuables, singletons, métaclasses | Configuration de l'objet |
Bonnes pratiques avec __new__
Voici les recommandations essentielles pour utiliser __new__ correctement dans vos projets Python :
- Ne redéfinissez
__new__que si nécessaire : dans la grande majorité des cas,__init__suffit. Utiliser__new__sans raison ajoute de la complexité inutile. - Appelez toujours
super().__new__(cls): oublier cet appel empêchera la création de l'instance et provoquera des erreurs difficiles à diagnostiquer. - Retournez toujours une instance : si
__new__ne retourne pas une instance de la classe,__init__ne sera pas appelée. - Transmettez les arguments correctement : assurez-vous que
*argset**kwargssont correctement propagés àsuper().__new__()quand c'est nécessaire. - Documentez votre utilisation : lorsque vous redéfinissez
__new__, ajoutez une docstring expliquant pourquoi cette redéfinition est nécessaire. - Pensez à la compatibilité avec l'héritage : si votre classe sera sous-classée, assurez-vous que votre implémentation de
__new__fonctionne correctement avec les classes filles.
Évitez de mettre de la logique d'initialisation dans __new__. Cette méthode est destinée à la création de l'instance, pas à sa configuration. Gardez la séparation des responsabilités entre __new__ (création) et __init__ (initialisation).
Voici un exemple de ce qu'il ne faut pas faire :
# ❌ Mauvaise pratique : logique d'initialisation dans __new__
class Mauvais:
def __new__(cls, nom, age):
instance = super().__new__(cls)
instance.nom = nom # ❌ Ceci devrait être dans __init__
instance.age = age # ❌ Ceci aussi
return instance
# ✅ Bonne pratique : séparation claire des responsabilités
class Bon:
def __init__(self, nom, age):
self.nom = nom
self.age = age
Cas particulier : __new__ qui retourne un objet d'un autre type
Un comportement intéressant et parfois déroutant : si __new__ retourne un objet qui n'est pas une instance de la classe, alors __init__ ne sera pas appelée :
class Surprise:
def __new__(cls):
print("__new__ appelé")
return "Je suis une chaîne, pas une instance de Surprise"
def __init__(self):
print("__init__ appelé")
obj = Surprise()
print(obj) # "Je suis une chaîne, pas une instance de Surprise"
print(type(obj)) #
# Note : __init__ n'est JAMAIS appelé iciCe comportement est utilisé dans certains cas avancés, mais il peut être source de confusion. Gardez-le à l'esprit lorsque vous déboguez des problèmes liés à l'instanciation.
Questions fréquentes
Quelle est la différence entre __new__ et __init__ en Python ?
__new__ est responsable de la création de l'instance en mémoire et reçoit la classe (cls) comme premier argument. __init__ est responsable de l'initialisation de l'instance déjà créée et reçoit l'instance (self) comme premier argument. __new__ est appelée avant __init__ et doit retourner une instance, tandis que __init__ ne retourne rien. Dans la pratique quotidienne, vous n'aurez besoin de redéfinir que __init__ dans la grande majorité des cas.
Quand faut-il utiliser __new__ plutôt que __init__ ?
Vous devez utiliser __new__ dans trois cas principaux : lorsque vous sous-classez un type immuable (comme int, str, tuple) et que vous devez modifier la valeur avant la création de l'objet ; lorsque vous implémentez le patron Singleton pour garantir une instance unique ; et lorsque vous travaillez avec des métaclasses pour contrôler la création des classes elles-mêmes. En dehors de ces cas, préférez toujours __init__.
Pourquoi __init__ n'est pas appelé après __new__ dans certains cas ?
Si __new__ retourne un objet qui n'est pas une instance de la classe (ou d'une de ses sous-classes), Python n'appelle pas __init__. C'est un mécanisme de sécurité : __init__ attend un objet du bon type en self. Si vous oubliez de retourner une instance (par exemple en oubliant le return), __new__ retournera implicitement None, et __init__ ne sera pas appelée non plus.
Comment apprendre à maîtriser __new__ et la POO avancée en Python ?
La maîtrise de __new__ et des concepts avancés de la programmation orientée objet en Python nécessite de la pratique et un apprentissage structuré. Nous vous recommandons de suivre notre formation dédiée à Python sur Believemy, qui couvre en profondeur les méthodes spéciales, les métaclasses, les patrons de conception et bien d'autres sujets essentiels pour devenir un développeur Python compétent.