Méthodes spéciales¶
Python est un langage avec une syntaxe de haut niveau, on le lit (presque)
comme on lit un livre. Derrière cette syntaxe se cachent des méthodes appelées
méthodes spéciales. On les reconnaît par la présence de soulignés qui
encadrent leur nom, c’est le cas de __new__()
et __init__()
.
Exemples :
## Syntaxe de haut niveau ## Méthode spéciale correspondante
# Listes
L = [1, 2, 3]
2 in L # L.__contains__(2)
len(L) # L.__len__()
# Opérations
objet1 + objet2 # objet1.__add__(objet2)
objet1 == objet2 # objet1.__eq__(objet2)
# Conversion en str ou affichage
print(objet) # objet.__str__()
str(objet) # objet.__str__()
# Appel d'une fonction
fonction() # fonction.__call__()
Référence
Création, initialisation et finalisation¶
-
Objet.
__new__
(cls[, *args, **kwargs])¶ Créateur de l’instance. C’est une méthode statique qui prend en premier paramètre obligatoire la classe de l’instance à créer.Cette méthode doit renvoyer une nouvelle instance (de
cls
) ; l'initialiseur est alors appelé avec comme argumentself
la nouvelle instance créée. Les autres paramètres sont passés à la suite.Sauf dans certains cas particuliers, on utilise plutôt
__init__()
pour initialiser un objet. Implémenter__new__()
est utile :lorsque l'on définit des métaclasses ;
lorsque l'on veut définir une sous-classe d'un type natif.
Cette méthode statique est un cas particulier qui ne nécessite pas de décorateur
@staticmethod
.
Exemple : On veut définir une sous-classe de str
avec un attribut
language
indiquant la langue de la chaîne de caractères. On surcharge
__new__()
pour définir un nouveau constructeur qui prendra en paramètres
la chaîne de caractères et le code de langue.
class LangString:
"""str subclass with language attribute."""
def __new__(cls, string, language):
instance = super().__new__(cls, string)
instance.language = language
return instance
mystr = LangString("Bonjour !", "fr")
print(mystr) # Bonjour !
print(mystr.language) # fr
-
Objet.
__init__
(self[, *args, **kwargs])¶ Initialiseur de l’instance. C’est ici qu’on initialise les attributs de l’instance.
Ces deux méthodes forment le constructeur de l’instance,
elles sont appelées lorsqu’on appelle la classe pour construire un
objet. Si on surcharge ces méthodes, il ne faut pas oublier d’appeler
les méthodes héritées grâce à super()
. Dans le cas où la classe
hérite uniquement d’object
, il n’y a pas besoin d’appeler
super().__init__()
car les instances d’object
n’ont aucun
attribut (donc il n’y a pas d’initialisation).
-
Objet.
__del__
(self)¶ Finaliseur de l’instance. Cette méthode est appelée lorsqu’un objet est sur le point d’être détruit, mais n’est pas responsable de sa destruction. Lorsqu’on hérite uniquement d'
object
, il n’est pas nécessaire d’appelersuper().__del__()
car elle ne fait rien. La syntaxedel variable
décrémente le nombre de références vers l'objet
correspondant. Si celui-ci atteint zéro, alorsobjet.__del__()
est appelée.
Représentation et chaîne de caractères d’un objet¶
Par défaut, évaluer une instance d'une classe personnalisée dans l'interpréteur interactif n'affiche pas quelque chose d'explicite :
>>> class MaClasse:
>>> ... pass
>>> ...
>>> MaClasse()
<__main__.MaClasse object at 0x7f6b90f7ba00>
On peut définir une méthode pour afficher une meilleure représentation.
-
Objet.
__repr__
(self)¶ Appelée par
repr()
, ou bien lorsqu’on évalue l’objet dans l’interpréteur interactif. Cette méthode calcule et renvoie une chaîne de caractères ressemblant à une expression permettant de recréer un objet semblable (avec les mêmes valeurs d'attributs). Si ce n’est pas possible, elle devrait renvoyer une description entre chevrons"<description>"
.
Exemple : En reprenant l'exemple précédent de LangString
, mystr
pourrait avoir comme représentation 'LangString("Bonjour !", "fr")'
.
Parfois, on veut quelque chose de plus joli destiné à un véritable
affichage (quand on appelle print()
). On peut vouloir aussi avoir la
capacité de convertir un objet en une chaîne de caractère avec
str()
. On doit alors définir une autre méthode spéciale :
-
Objet.
__str__
(self)¶ Appelée par
str()
,print()
etformat()
. Cette méthode renvoie une chaîne de caractère correspondant à la représentation informelle de l’objet. Si cette méthode n’est pas définie, alors__repr__()
est utilisée à la place.
class MaClasse:
def __init__(self, attr):
self.attribut = attr
def __repr__(self):
return f"MaClasse(attribut='{self.attribut}')"
def __str__(self):
return f"Instance de MaClasse ayant comme attribut {self.attribut}"
obj = MaClasse("Exemple")
print(repr(obj)) # MaClasse(attribut='Exemple')
print(obj) # Instance de MaClasse ayant comme attribut Exemple.
Accès et modification des attributs¶
On a vu précédemment les propriétés qui permettent une sorte
d’encapsulation des attributs. Lorsqu’on veut accéder à un attribut par
la syntaxe instance.attr
Python appelle en premier une méthode
spéciale.
-
Objet.
__getattribute__
(self, name)¶ Appelée en premier lorsque l’on veut accéder à un attribut par les syntaxes
objet.attr # objet.__getattribute__('attr') getattr(objet, 'attr') # objet.__getattribute__('attr')
Cette méthode doit renvoyer l’attribut
name
demandé s’il existe (ou calculer sa valeur) ou lever une exceptionAttributeError
sinon. Dans ce cas, la méthode__getattr__()
est appelée.Cette méthode est définie dans la classe
object
, le mécanisme d’accès par défaut aux attributs est le suivant :object.__getattribute__()
commence par recherchername
sous forme de descripteur dans le dictionnaire__dict__
de la classe de l’instance (et de ses classes parentes s’il ne trouve pas).type(objet).__dict__[name].__get__(objet, type(objet))
object.__getattribute__()
recherche ensuitename
sous forme de simple variable dans le dictionnaire__dict__
de l’instance, puis dans celui de sa classe si elle ne le trouve pas, ainsi que dans celui de chaque classe parente jusqu’à le trouver.objet.__dict__[name]
Si
object.__getattribute__()
n’a pas trouvéname
dans aucun__dict__
, elle lève une exceptionAttributeError
.
Surcharger cette méthode va donc modifier le mécanisme par défaut. Cependant, pour les attributs dont on ne veut pas modifier l’accès, il faut penser à appeler la méthode
__getattribute__()
d’object
ou de la classe parente. Pour les attributs dont on veut modifier le comportement d’accès, il ne faut pas utiliser la syntaxe classiqueobjet.attr
car cela va créer une récursivité infinie, il faut avoir recours àsuper().__getattribute__()
ou bien accéder directement aux descripteurs ou clé du dictionnaire.
-
Objet.
__getattr__
(self, name)¶ Appelée si
__getattribute__()
lève une exceptionAttributeError
. Par défaut, cette méthode fait la même chose, mais c’est ici que l’on peut calculer des attributs dynamiques: des attributs qui ne sont pas initialisés mais dont on veut pouvoir calculer la valeur.
-
Objet.
__setattr__
(self, name, value)¶ Appelée lorsque l’on assigne une valeur à un attribut:
objet.attr = value # objet.__setattr__('attr', value) setattr(objet, 'attr', value) # objet.__setattr__('attr', value)
Cette méthode est définie dans la classe
object
, l’ordre de recherche de l’attribut à modifier est le suivant:Si un descripteur est trouvé, il est utilisé pour modifier la valeur de
name
.type(objet).__dict__[name].__set__(objet, value)
Sinon une nouvelle clé est créée dans le
__dict__
de l’instance avec pour valeurvalue
.objet.__dict__[name] = value
Surcharger cette méthode va donc modifier le mécanisme par défaut. Cependant, pour les attributs dont on ne veut pas modifier le comportement de modification, il faut penser à appeler la méthode
__setattr__()
d’object
ou de la classe parente. Pour les attributs dont on veut surcharger le mécanisme de modification, il y a le même problème que pour__getattribute__()
, attention de ne pas créer de récursivité infinie.
-
Objet.
__delattr__
(self, name)¶ Appelée lorsque l’on veut détruire un attribut :
del objet.attr # objet.__delattr__('attr') delattr(objet, 'attr') # objet.__delattr__('attr')
Finalise l’attribut avant sa suppression s’il existe et lève une exception
AttributeError
sinon. Cette méthode doit appelersuper().__delattr__()
pour éviter une récursivité infinie lors de l’appel à la suppression de l’attribut.
Exemple : On peut créer une sous classe de dict
pour accéder aux
items par la notation pointée dictionaire.item
class DottedDict(dict):
def __getattr__(self, name):
return self[name]
def __setattr__(self, name, value):
if name in self:
self[name] = value
else:
super().__setattr__(name, value)
d = DottedDict({"a": 1, "b": 2})
print(d) # {'a': 1, 'b': 2}
print(d.a) # 1
d.a = 2
print(d) # {'a': 2, 'b': 2}
Surcharges d’opérateur¶
Les surcharges d’opérateur permettent de faire des opérations
arithmétiques avec des objets, c’est-à-dire d’indiquer à Python ce qu’il
faut faire lorsque l’on exécute objet1 + objet2
. Ces méthodes
prennent en arguments self
(l’objet 1) et l’objet 2.
Méthode |
Appel |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Les deux objets ne sont pas nécessairement du même type ! Cependant,
cette opération n’est pas symétrique : le code objet + 5
par exemple
exécute objet.__add__(5)
, alors que 5 + objet
exécute
int.__add__(5)
. Pour que l’opération soit symétrique, il faut aussi
définir ces fonctions avec le préfixe r
(par exemple
__radd__()
).
Duck typing¶
On appelle duck typing , en français typage canard , le fait de reconnaître un type d’objets grâce à leurs méthodes et attributs. Cela est utile lorsque l’on veut qu’une fonction puisse prendre en paramètre une certaine catégorie d’objets qui ne sont pas forcément du même type ; ils partageront cependant des caractéristiques communes qui leur permettront d’être traités par la fonction.
Par exemple, on peut récupérer la longueur d’une liste avec len()
:
>>> len([1, 2, 3])
3
Mais on peut récupérer la longueur de plein d’autres choses :
>>> len("abc")
3
>>> len((1, 2))
2
>>> len(range(5))
5
Tous ses objets sont de types différents : liste, chaîne de caractères,
tuple, range
; ils partagent cependant quelque chose :
>>> for obj in [[1, 2, 3], "abc", (1, 2), range(5)]:
... print(obj.__len__())
...
3
3
2
5
Ainsi, len()
accepte tout objet possédant une taille, c’est-à-dire
tout objet implémentant la méthode spéciale __len__()
. Les méthodes
spéciales permettent de cette manière d’implémenter une multitude de
comportements aux objets et de les rendre compatibles avec des API
Python : on peut très bien créer un objet sur lequel on peut itérer avec
une boucle for
en implémentant le protocole d’itération, mais aussi
le rendre appelable comme une fonction grâce à la méthode
__call__()
.
Les parties suivantes décrivent ces catégories d’objets définies selon les méthodes spéciales implémentées. On appelle parfois protocole l’ensemble des méthodes spéciales à implémenter pour une catégorie (exemples : protocole d’itérateur ou protocole de descripteur).