Cours-Python Chapitre 3
Cours-Python Chapitre 3
27 Mini-projets 349
27.1 Description des projets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
27.2 Accompagnement pas à pas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
27.3 Scripts de correction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 365
Module Matplotlib
Le module matplotlib 1 permet de générer des graphiques depuis Python. Il est l’outil complémentaire des modules
NumPy, scipy ou pandas (que l’on verra juste après) lorsqu’on veut faire de l’analyse de données.
Dans ce chapitre, nous vous montrerons quelques exemples d’utilisation du module matplotlib pour vous convaincre
de sa pertinence. Ces exemples seront exécutés dans un notebook Jupyter.
1 Les cellules de code apparaitront de cette manière
2 dans un notebook Jupyter, avec des numéros de lignes à gauche.
Remarque
On n’importe pas le module matplotlib directement, mais plutôt son sous-module pyplot. Par convention, et pour
l’utiliser plus rapidement, ce sous-module prendre l’alias plt.
226
21.3. Représentation en nuage de points Chapitre 21. Module Matplotlib
Dans un notebook Jupyter, vous devriez obtenir un graphique ressemblant à celui de la figure 21.1.
• Ligne 1. Tout d’abord, on importe le sous-module pyplot du module matplotlib et on lui donne l’alias plt pour
l’utiliser plus rapidement ensuite. Cet alias est standard, utilisez-le systématiquement.
• Lignes 3 et 4. On définit les variables temps et concentration comme des listes. Les deux listes doivent avoir
la même longueur (sept éléments dans le cas présent).
• Ligne 6. On crée une figure avec la fonction subplots() qui renvoie deux objets : une figure (fig) et un axe
(ax). L’axe est l’objet qui contient le graphique à proprement dit. On peut avoir plusieurs axes dans une même
figure.
• Ligne 7. La méthode .scatter() permet de représenter des points sous forme de nuage de points. Les deux
premiers arguments correspondent aux valeurs en abscisse et en ordonnée des points, fournis sous forme de listes.
Des arguments facultatifs sont ensuite précisés comme le symbole (marker) et la couleur (color).
• Lignes 8 et 9. Les méthodes .set_xlabel() et .set_ylabel() donnent une légende aux axes des abscisses et
des ordonnées.
• Ligne 10. La méthode .set_title() définit le titre du graphique.
• Ligne 11. L’instruction [Link]() affiche le graphique. Elle n’est pas nécessaire dans un notebook Jupyter, car
le graphique est affiché automatiquement, mais elle est indispensable dans un script Python.
Remarque
Le modèle présenté ici est purement fictif. Vous découvrirez dans le chapitre 22 Module Pandas comment réaliser une
régression linéaire pour modéliser des données expérimentales.
Représentons ce modèle avec les points expérimentaux et sauvegardons le graphique obtenu sous forme d’une image :
1 import numpy as np
2 import [Link] as plt
3
4 temps = [1, 2, 3, 4, 6, 7, 9]
5 concentration = [5.5, 7.2, 11.8, 13.6, 19.1, 21.7, 29.4]
6
7 fig, ax = [Link]()
8 [Link](temps, concentration, marker="o", color = "blue")
9 ax.set_xlabel("Temps (h)")
10 ax.set_ylabel("Concentration (mg/L)")
11 ax.set_title("Concentration de produit en fonction du temps")
12
13 x = [Link](min(temps), max(temps), 50)
14 y = 2 + 3 * x
15
16 [Link](x, y, color="green", ls="--")
17 [Link]()
18 [Link]("concentration_vs_temps_1.png", bbox_inches="tight", dpi=200)
points à représenter. On indique ensuite des arguments facultatifs comme le style de la ligne (ls pour line style)
et sa couleur (color).
• Ligne 17. La méthode .grid() affiche une grille.
• Ligne 18. Enfin, l’instruction [Link]() enregistre le graphique produit sous la forme d’une image au format
png. Des arguments par mot-clé définissent la manière de générer les marges autour du graphique (bbox_inches)
et la résolution de l’image (dpi).
Pour terminer, on peut améliorer un peu plus le graphique en ajoutant une légende et en modifiant l’étendue des axes
des abscisses et des ordonnées :
1 import numpy as np
2 import [Link] as plt
3
4 temps = [1, 2, 3, 4, 6, 7, 9]
5 concentration = [5.5, 7.2, 11.8, 13.6, 19.1, 21.7, 29.4]
6
7 fig, ax = [Link]()
8 [Link](temps, concentration, marker="o", color="blue", label="mesures")
9 ax.set_xlabel("Temps (h)")
10 ax.set_ylabel("Concentration (mg/L)")
11 ax.set_title("Concentration de produit en fonction du temps")
12
13 x = [Link](min(temps), max(temps), 50)
14 y = 2 + 3 * x
15
16 [Link](x, y, color="green", ls="--", label="modèle")
17
18 [Link]()
19 ax.set_xlim(0, 10)
20 ax.set_ylim(0, 35)
21
22 [Link](loc="upper left")
23 [Link]("concentration_vs_temps_2.png", bbox_inches="tight", dpi=200)
3. [Link]
4. [Link]
5. [Link]
6. [Link]
7. [Link]
8. [Link]
Module Pandas
Le module pandas 1 a été conçu pour l’analyse de données. Il est particulièrement puissant pour manipuler des données
structurées sous forme de tableau.
Vous aurez également besoin des modules matplotlib pour créer des graphiques et scipy pour réaliser une régression
linaire, que vous pouvez installer ainsi :
$ conda install -c conda-forge matplotlib scipy
Dans ce chapitre, nous vous montrerons quelques exemples d’utilisation du module pandas pour vous convaincre de
sa pertinence. Ces exemples seront exécutés dans un notebook Jupyter.
1 Les cellules de code apparaitront de cette manière
2 dans un notebook Jupyter, avec des numéros de lignes à gauche.
1. [Link]
2. [Link]
233
Chapitre 22. Module Pandas 22.3. Series
22.3 Series
Le premier type de données apporté par pandas est la Series, qui correspond à un vecteur à une dimension.
1 s = [Link]([10, 20, 30, 40], index = ['a', 'b', 'c', 'd'])
2 s
a 10
b 20
c 30
d 40
dtype: int64
10
Pour accéder au premier élément par son indice (ici 0), comme on le ferait avec une liste, on utilise la méthode
.iloc :
1 [Link][0]
10
Bien sûr, on peut extraire plusieurs éléments, par leurs indices ou leurs étiquettes :
1 s[["b", "d"]]
b 20
d 40
dtype: int64
et
1 [Link][[1, 3]]
b 20
d 40
dtype: int64
a 10
b 20
c 300
d 40
z 50
dtype: int64
22.3.3 Filtres
Enfin, on peut filtrer une partie de la Series :
1 s[s>30]
c 300
d 40
z 50
dtype: int64
Remarque
Cette écriture rappelle celle des masques booléens dans le chapitre 20 Module NumPy.
Enfin, on peut aussi combiner plusieurs critères de sélection avec les opérateurs logiques & (pour ET) et | (pour
OU) :
1 s[(s>20) & (s<100)]
d 40
z 50
dtype: int64
1 s[(s<15) | (s>150)]
a 10
c 300
dtype: int64
22.4 Dataframes
Un autre type d’objet particulièrement intéressant introduit par pandas sont les Dataframes. Ceux-ci correspondent
à des tableaux à deux dimensions avec des étiquettes pour nommer les lignes et les colonnes.
Remarque
Si vous êtes familier avec le langage de programmation et d’analyse statistique R, les Dataframes de pandas se
rapprochent de ceux trouvés dans R.
22.4.1 Création
Voici comment créer un Dataframe avec pandas à partir de données fournies comme liste de lignes :
1 import numpy as np
2 df = [Link](columns=["a", "b", "c", "d"],
3 index=["chat", "singe", "souris"],
4 data=[[Link](10, 14),
5 [Link](20, 24),
6 [Link](30, 34)])
7 df
a b c d
chat 10 11 12 13
singe 20 21 22 23
souris 30 31 32 33
a b c d
chat 10 11 12 13
singe 20 21 22 23
souris 30 31 32 33
• Lignes 1 à 4. Le dictionnaire data contient les données en colonnes. La clé associée à chaque colonne est le nom
de la colonne.
• Ligne 5. Le dataframe est créé avec la fonction [Link]() à laquelle on passe data en argument.
• Ligne 6. On peut définir les étiquettes des lignes de n’importe quel dataframe avec l’attribut [Link].
(3, 4)
La méthode .head(n) renvoie les n premières lignes du Dataframe (par défaut, n vaut 5) :
1 [Link](2)
Remarque
Les Dataframes utilisés ici comme exemples sont volontairement petits. Si vous êtes confrontés à des Dataframes
de grande taille, ceux-ci seront affichés partiellement dans un notebook Jupyter. Des ascenseurs en bas et à droite du
Dataframe permettront de naviguer dans les données.
22.4.3 Sélections
Les mécanismes de sélection fournis avec pandas sont très puissants. En voici un rapide aperçu :
chat 11
singe 21
souris 31
[Link]
Attention
On trouve parfois l’écriture [Link] pour sélectionner une colonne. C’est une très mauvaise pratique, car cette
écriture peut être confondue avec un attribut de l’objet df (par exemple .shape). Par ailleurs, elle ne fonctionne pas
pour des noms de colonnes qui contiennent des espaces ou des caractères spéciaux (ce qui n’est pas non plus une bonne
pratique).
Nous vous conseillons de toujours utiliser la notation df["nom_de_colonne"].
Pour sélectionner plusieurs colonnes, il faut fournir une liste de noms de colonnes :
1 df[["Lyon", "Pau"]]
Lyon Pau
chat 11 13
singe 21 23
souris 31 33
[Link]
Remarque
La sélection de plusieurs colonnes nécessite une liste entre les crochets, par exemple df[["Lyon", "Pau"]]. Si on
utilise un tuple du type df[("Lyon", "Pau")], Python renvoie une erreur KeyError: ('Lyon', 'Pau').
Pour sélectionner une ligne, il faut utiliser l’instruction .loc et l’étiquette de la ligne :
1 [Link]["singe"]
Paris 20
Lyon 21
Nantes 22
Pau 23
Name: singe, dtype: int64
Enfin, on peut aussi sélectionner des lignes avec l’instruction .iloc et l’indice de la ligne (la première ligne ayant
l’indice 0) :
1 [Link][1]
Paris 20
Lyon 21
Nantes 22
Pau 23
Name: singe, dtype: int64
1 [Link][[1, 0]]
On peut bien sûr combiner les deux types de sélection (en ligne et en colonne) :
1 [Link]["souris", "Pau"]
33
Nantes Lyon
singe 22 21
souris 32 31
Notez qu’à partir du moment où on souhaite effectuer une sélection sur des lignes, il faut utiliser .loc (ou .iloc si
on utilise les indices).
Sélectionnons maintenant toutes les lignes pour lesquelles les effectifs à Pau sont supérieurs à 15 :
1 df[ df["Pau"]>15 ]
singe 21
souris 31
Name: Lyon, dtype: int64
et | pour l’opérateur ou :
1 df[ (df["Pau"]>15) | (df["Lyon"]>25) ]
Lyon Paris
chat 10 3
singe 23 15
souris 17 20
et
Nantes Strasbourg
chat 3 5
souris 9 10
lapin 14 8
On souhaite combiner ces deux dataframes, c’est-à-dire connaître pour les quatre villes (Lyon, Paris, Nantes et
Strasbourg) le nombre d’animaux. On remarque d’ores et déjà qu’il y a des singes à Lyon et Paris, mais pas de lapin et
qu’il y a des lapins à Nantes et Strasbourg, mais pas de singe. Nous allons voir comment gérer cette situation.
Pandas propose pour cela la fonction concat() 3 , qui prend comme argument une liste de dataframes :
1 [Link]([df1, df2])
Ici, NaN indique des valeurs manquantes, cela signifie littéralement Not a Number. Mais le résultat obtenu n’est pas
celui que nous attendions, puisque les lignes de deux dataframes ont été recopiées.
L’argument supplémentaire axis=1 produit le résultat attendu :
1 [Link]([df1, df2], axis=1)
Par défaut, pandas va conserver le plus de lignes possible. Si on ne souhaite conserver que les lignes communes aux
deux dataframes, il faut ajouter l’argument join="inner" :
1 [Link]([df1, df2], axis=1, join="inner")
Un autre comportement par défaut de concat() est que cette fonction va combiner les dataframes en se basant sur
leurs index. Il est néanmoins possible de préciser, pour chaque dataframe, le nom de la colonne qui sera utilisée comme
référence avec l’argument join_axes.
3. [Link]
1 import numpy as np
2 import pandas as pd
3
4 nb_rows = 1000
5 df = [Link](
6 {
7 "a": [Link](100, 200, nb_rows),
8 "b": [Link](100, 200, nb_rows),
9 "c": [Link](100, 200, nb_rows),
10 }
11 )
(1000, 3)
1 [Link]()
a b c
0 105 156 122
1 116 135 138
2 125 190 113
3 196 175 179
4 129 184 153
On souhaite maintenant créer une nouvelle colonne (d) qui sera le résultat de la multiplication des colonnes a et b,
à laquelle on ajoute ensuite la colonne c.
Une première manière de faire est de procéder ligne par ligne. La méthode .iterrows() permet de parcourir les
lignes d’un Dataframe et renvoie un tuple contenant l’indice de la ligne (sous la forme d’un entier) et la ligne elle-même
(sous la forme d’une Series) :
1 for idx, row in [Link]():
2 [Link][idx, "d"] = (row["a"] * row["b"]) + row["c"]
Ici, l’instruction .at ajoute une cellule à la ligne d’indice idx et de colonne d. Cette instruction est plus efficace que
.loc pour ajouter une cellule à un Dataframe.
L’approche précédente produit le résultat attendu, mais elle n’est pas optimale, car très lente. Pour évaluer le temps
moyen pour réaliser ces opérations, on utilise la commande magique %%timeit abordée dans le chapitre 18 Jupyter et
ses notebooks :
1 %%timeit
2 for idx, row in [Link]():
3 [Link][idx, "d"] = (row["a"] * row["b"]) + row["c"]
qui renvoie :
52.4 ms ± 3.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
qui renvoie :
250 µs ± 36.1 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
Ici, la cellule de code s’exécute en moyenne en 250 µs, soit environ 200 fois (52400/250) plus rapidement qu’avec
.iterrows(). Tout comme avec les arrays du chapitre 20 Numpy, les opérations vectorielles avec les Dataframes sont
rapides et efficaces. Privilégiez toujours ce type d’approche avec les arrays de NumPy ou les Series et Dataframes de
pandas.
Remarque
Dans l’exemple précédent, l’utilisation de la commande magique %%timeit calcule le temps d’exécution moyen d’une
cellule. Python détermine automatiquement le nombre d’itérations à réaliser pour que le calcul se fasse dans un temps
raisonnable. Ainsi, pour la méthode .iterrows(), le calcul est réalisé 10 fois sur sept répétitions alors que pour les
opérations vectorielles, le calcul est effectué 1000 fois sur sept répétitions.
Remarque
Avant de nous lancer dans l’analyse de ce fichier, nous vous proposons cette petite devinette :
Qu’est-ce qu’une protéine dans une piscine ?
La réponse sera donnée à la fin de ce chapitre.
Le contenu est chargé sous la forme d’un Dataframe dans la variable df.
Le fichier contient 1 442 lignes de données plus une ligne d’en-tête. Cette dernière est automatiquement utilisée par
pandas pour nommer les différentes colonnes. Voici un aperçu des premières lignes :
1 [Link]()
La colonne d’entiers tout à gauche est un index automatiquement créé par pandas.
Nous pouvons demander à pandas d’utiliser une colonne particulière comme index. On utilise pour cela le paramètre
index_col de la fonction read_csv(). Ici, la colonne Entry s’y prête très bien, car cette colonne ne contient que des
identifiants uniques :
1 df = pd.read_csv("[Link]", index_col="Entry")
2 [Link]()
Remarque
La fonction .read_csv() permet également d’ouvrir un fichier au format TSV (voir l’annexe A Quelques formats de
données en biologie). Il faut pour cela préciser que le séparateur des colonnes de données est une tabulation (\t), avec
l’argument sep="\t".
Avant d’analyser un jeu de données, il est intéressant de l’explorer un peu. Par exemple, connaître ses dimensions :
1 [Link]
(1442, 5)
Notre jeu de données contient donc 1 442 lignes et 5 colonnes. En effet, la colonne Entry est maintenant utilisée
comme index et n’est donc plus prise en compte.
Il est aussi intéressant de savoir de quel type de données est constituée chaque colonne :
1 [Link]
Organism object
Length int64
Creation date object
Mass int64
PDB object
dtype: object
Les colonnes Length et Mass contiennent des valeurs numériques, en l’occurrence des entiers (int64). Le type
object est un type par défaut.
La méthode .info() permet d’aller un peu plus loin dans l’exploration du jeu de données en combinant les informa-
tions produites par les propriétés .shape et .dtypes :
1 [Link]()
<class '[Link]'>
Index: 1442 entries, A0A0B4J2F2 to Q5F361
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Organism 1442 non-null object
1 Length 1442 non-null int64
2 Creation date 1442 non-null object
3 Mass 1442 non-null int64
4 PDB 488 non-null object
dtypes: int64(2), object(3)
memory usage: 67.6+ KB
Avec l’argument memory_usage="deep", la méthode .info() permet de connaitre avec précision la quantité de
mémoire vive occupée par le Dataframe :
1 [Link](memory_usage="deep")
<class '[Link]'>
Index: 1442 entries, A0A0B4J2F2 to Q5F361
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Organism 1442 non-null object
1 Length 1442 non-null int64
2 Creation date 1442 non-null object
3 Mass 1442 non-null int64
4 PDB 488 non-null object
dtypes: int64(2), object(3)
memory usage: 351.0 KB
Organism 0
Length 0
Creation date 0
Mass 0
PDB 954
dtype: int64
Ici, la seule colonne qui contient des valeurs manquantes est la colonne PDB, qui contient 954 valeurs manquantes.
Cela signifie que pour 954 protéines, aucune structure 3D n’est disponible. Nous reviendrons plus tard sur cette colonne
PDB.
Mais le type de données de la colonne Creation date est maintenant une date (datetime64[ns]) :
1 [Link]
Organism object
Length int64
Creation date datetime64[ns]
Mass int64
PDB object
dtype: object
On apprend ainsi que la taille de la protéine (colonne Length) a une valeur moyenne de 756,14 acides aminés et que
la plus petite protéine est composée de 81 acides aminés et la plus grande de 2 986. Pratique !
Des statistiques sont également proposées pour la colonne Creation date. La protéine la plus récente a ainsi été
référencée le 13 septembre 2023.
La colonne Organism contient des chaînes de caractères, on peut rapidement déterminer le nombre de protéines pour
chaque organisme :
1 df["Organism"].value_counts()
Organism
Human 489
Mouse 489
Rat 253
Fruit fly 103
Chicken 75
Rabbit 25
Maize 8
Name: count, dtype: int64
On apprend ainsi que 489 protéines sont d’origine humaine (Human) et 8 proviennent du maïs (Maize).
Length Mass
Organism
Chicken 720.160000 81120.880000
Fruit fly 784.844660 88154.669903
Human 771.004090 86281.190184
Maize 666.875000 73635.000000
Mouse 768.092025 85942.274029
Rabbit 591.480000 66754.200000
Rat 722.379447 81081.822134
La méthode .groupby() rassemble d’abord les données suivant la colonne Organism. Puis on sélectionne les colonnes
Length et Mass. Enfin, la méthode .mean() calcule la moyenne pour chaque groupe.
Si on souhaite obtenir deux statistiques (par exemple les valeurs minimale et maximale) en une seule fois, il convient
alors d’utiliser la méthode .pivot_table(), méthode plus complexe, mais aussi beaucoup plus puissante :
1 df.pivot_table(
2 index="Organism",
3 values=["Length", "Mass"],
4 aggfunc=["min", "max"]
5 )
min max
Length Mass Length Mass
Organism
Chicken 303 34688 2311 260961
Fruit fly 294 33180 2554 287025
Human 253 28160 2986 340261
Maize 294 33834 996 105988
Mouse 244 27394 2964 337000
Rabbit 81 9405 1382 158347
Rat 274 31162 2959 336587
En réalisant une régression linéaire, on peut déterminer les paramètres de la droite qui passent le plus proche possible
des points du graphique. On utilise pour cela la fonction linregress() 6 du module [Link] :
1 from [Link] import linregress
2 model = linregress(df["Length"], df["Mass"])
3 model
LinregressResult(slope=110.63478918698122, intercept=1055.431834679228,
rvalue=0.9989676084416755, pvalue=0.0, stderr=0.13258187632073232,
intercept_stderr=113.66584551734655)
Ce modèle linéaire nous indique qu’un résidu a une masse d’environ 111 Dalton, ce qui est cohérent. On peut également
comparer ce modèle aux différentes protéines :
1 fig, ax = [Link]()
2 [Link](df["Length"], df["Mass"], label="données")
3 [Link](
4 df["Length"],
5 df["Length"]*[Link] + [Link],
6 ls=":",
7 label="modèle"
8 )
9 ax.set_xlabel("Taille (nombre d'acides aminés)")
10 ax.set_ylabel("Masse (Dalton)")
11 [Link]()
12 [Link]("[Link]")
min max
Creation date Creation date
Organism
Chicken 1986-07-21 2021-02-10
Fruit fly 1986-07-21 2023-09-13
Human 1986-07-21 2018-06-20
Maize 1990-08-01 2023-05-03
Mouse 1986-07-21 2017-03-15
Rabbit 1986-07-21 2010-03-02
Rat 1986-07-21 2023-09-13
Chez le poulet (Chicken), la première séquence a été référencée le 21 juillet 1986 et la dernière le 10 février 2021.
Une autre question est de savoir combien de kinases ont été référencées en fonction du temps.
La méthode .value_counts() peut être utilisée, mais elle ne renvoie que le nombre de protéines référencées dans
UniProt pour un jour donné. Par exemple, 40 structures ont été référencées le 28 novembre 2006 :
1 df["Creation date"].value_counts().head()
Creation date
1997-11-01 72
1996-10-01 58
2000-12-01 43
2000-05-30 41
2006-11-28 40
Name: count, dtype: int64
Si on souhaite une réponse plus globale, par exemple à l’échelle de l’année, la méthode .resample() calcule le
nombre de protéines référencées par an (en fournissant l’argument YE). En utilisant le method chaining présenté dans le
chapitre 11 Plus sur les chaînes de caractères, nous pouvons écrire toutes ces transformations en une seule instruction,
répartie sur plusieurs lignes pour plus de lisibilité (en utilisant des parenthèses) :
1 (df["Creation date"]
2 .value_counts()
3 .resample("YE")
4 .sum()
5 .head()
6 )
Creation date
1986-12-31 11
1987-12-31 12
1988-12-31 32
1989-12-31 29
1990-12-31 40
Freq: YE-DEC, Name: count, dtype: int64
Les dates apparaissent maintenant comme le dernier jour de l’année (31 décembre), mais désignent bien l’année
complète. Dans cet exemple, 11 kinases ont été référencées dans UniProt entre le 1er janvier et le 31 décembre 1986.
Pour connaître en quelle année le plus de kinases ont été référencées dans UniProt, il faut trier les valeurs obtenues
du plus grand au plus petit avec la méthode .sort_values(). Comme on ne veut connaître que les premières dates
(celles où il y a eu le plus de protéines référencées), on utilisera également la méthode .head() :
1 (df["Creation date"]
2 .value_counts()
3 .resample("YE")
4 .sum()
5 .sort_values(ascending=False)
6 .head()
7 )
Creation date
2006-12-31 167
2005-12-31 136
2004-12-31 118
2003-12-31 104
2007-12-31 88
Name: count, dtype: int64
En 2006, 167 kinases ont été référencées dans UniProt. La deuxième « meilleure » année est 2005 avec 136 protéines.
Toutes ces méthodes, enchaînées les unes à la suite des autres, peuvent vous sembler complexes, mais chacune d’elles
correspond à une étape du traitement des données. Bien sûr, on aurait pu créer des variables intermédiaires pour chaque
étape, mais cela aurait été plus lourd :
1 date1 = df["Creation date"].value_counts()
2 date2 = [Link]("YE")
3 date3 = [Link]()
4 date4 = date3.sort_values(ascending=False)
5 [Link]()
Remarque
Le method chaining 7 est une manière efficace et élégante de traiter des données avec pandas.
Enfin, pour obtenir un graphique de l’évolution du nombre de kinases référencées dans UniProt en fonction du temps,
7. [Link]
On observe un pic du nombre de kinases référencées dans UniProt sur la période 2003-2007.
• Ligne 2. La méthode isna() sélectionne les lignes qui contiennent des valeurs manquantes dans la colonne PDB,
puis l’opérateur ~ inverse cette sélection.
• Ligne 3. On limite l’affichage aux trois premières lignes.
On découvre que la colonne PDB contient des identifiants de structures 3D de protéines. Ces identifiants sont séparés
par des points-virgules, y compris pour la dernière valeur.
Nous souhaitons compter le nombre de structures 3D pour chaque protéine. Pour cela, nous allons d’abord créer une
fonction qui compte le nombre de points-virgules dans une chaîne de caractères :
1 def count_structures(row):
2 if [Link](row["PDB"]):
3 return 0
4 else:
5 return row["PDB"].count(";")
Dans la ligne 2, la méthode .isna() teste si la valeur est manquante et si ce n’est pas le cas, la fonction renvoie le
nombre de points-virgules dans la chaîne de caractères de la colonne PDB (ligne 5).
On applique ensuite la fonction count_structures() au Dataframe avec la méthode .apply(). On crée la nouvelle
colonne nb_structures en même temps :
1 df["nb_structures"] = [Link](count_structures, axis=1)
2 [Link]()
Les premières lignes ne sont pas très intéressantes, car elles ne contiennent pas de structures 3D. Mais on peut
chercher les kinases qui ont le plus de structures 3D :
1 (df
2 .sort_values(by="nb_structures", ascending=False)
3 .filter(["Organism", "nb_structures"])
4 .head()
5 )
La kinase P24941 possède 453 structures 3D référencées dans UniProt. Les cinq kinases qui ont le plus de structures
3D sont toutes d’origine humaine.
Remarque
La réponse à la devinette précédente est :
22.6 Exercices
Conseil
Pour ces exercices, utilisez des notebooks Jupyter.
L’objectif de cet exercice est de manipuler ce jeu de données avec pandas, de sélectionner des données et d’en calculer
quelques statistiques.
Conseil
Si vous n’êtes pas familier avec le format de fichier .tsv, nous vous conseillons de consulter l’annexe A Quelques
formats de données en biologie.
La programmation orientée objet (POO) est un concept de programmation très puissant qui permet de structurer ses
programmes d’une manière nouvelle. En POO, on définit un « objet » qui peut contenir des « attributs » ainsi que des «
méthodes » qui agissent sur lui-même. Par exemple, on définit un objet « citron » qui contient les attributs « saveur » et
« couleur », ainsi qu’une méthode « presser » permettant d’en extraire le jus. En Python, on utilise une « classe » pour
construire un objet. Dans notre exemple, la classe correspondrait au « moule » utilisé pour construire autant d’objets
citrons que nécessaire.
Définition
Une classe définit des objets, qui sont des instances (des représentants) de cette classe. Dans ce chapitre, on utilisera
les mots objet ou instance pour désigner la même chose. Les objets peuvent posséder des attributs (variables associées
aux objets) et des méthodes (qui sont des fonctions associées aux objets et qui peuvent agir sur ces derniers, ou encore
les utiliser).
Dans les chapitres précédents, nous avons déjà mentionné qu’en Python tout est objet. Une variable de type int est
en fait un objet de type int, donc construit à partir de la classe int. Même chose pour les float et string, mais aussi pour
les list, tuple, dict, etc. Voilà pourquoi nous avons rencontré de nombreuses notations et mots de vocabulaire associés à
la POO depuis le début de ce cours.
La POO permet de produire du code plus compact et plus facilement réutilisable. L’utilisation de classes évite
l’utilisation de variables globales en créant ce qu’on appelle un espace de noms, propre à chaque objet et permettant
d’y encapsuler des attributs et des méthodes. De plus, la POO amène de nouveaux concepts tels que le polymorphisme
(capacité à redéfinir le comportement des opérateurs), ou bien encore l’héritage (capacité à définir une classe à partir
d’une classe pré-existante et d’y ajouter de nouvelles fonctionnalités). Tous ces concepts seront définis dans ce chapitre.
Malgré tous ces avantages, la POO peut paraître difficile à aborder pour le débutant, spécialement dans la conception
des programmes. Elle nécessite donc la lecture de nombreux exemples, mais surtout beaucoup de pratique. Bien structurer
ses programmes en POO est un véritable art. Il existe même des langages qui formalisent la construction de programmes
orientés objets, par exemple le langage UML 1 .
Dans ce chapitre, nous vous donnerons tous les éléments pour démarrer la construction de vos premières classes.
Le chapitre 24 Avoir plus la classe avec les objets (en ligne) abordera des aspects plus poussés de la POO, comme le
polymorphisme, la composition, l’héritage, certains pièges à éviter, ainsi que des bonnes pratiques.
1. [Link]
254
23.1. Construction d’une classe Chapitre 23. Avoir la classe avec les objets
Après la lecture de ces deux chapitres sur la POO avec Python, vous verrez d’un autre œil de nombreux exemples
évoqués dans les chapitres précédents, et vous comprendrez sans doute de nombreuses subtilités qui avaient pu vous
paraître absconses.
Enfin, il est vivement recommandé de lire ces deux chapitres sur la POO avant d’aborder le chapitre 25 Fenêtres
graphiques et Tkinter (en ligne).
Ligne 1. La classe Citron est définie. Pas besoin de parenthèses comme avec les fonctions dans un cas simple comme
celui-là (nous verrons d’autres exemples plus loin où elles seront nécessaires).
Ligne 2. La classe ne contient rien, mais il faut mettre au moins une ligne, on met donc ici le mot-clé Python pass
qui ne fait rien (comme dans une fonction qui ne fait rien).
Lignes 4 et 5. Quand on tape le nom de notre classe Citron, Python nous indique que cette classe est connue.
Lignes 6 et 7. Lorsqu’on regarde le type de notre classe Citron, Python nous indique qu’il s’agit d’un type au même
titre que type(int). Nous avons donc créé un nouveau type !
Ligne 8. On crée une instance de la classe Citron, c’est-à-dire qu’on fabrique un représentant ou objet de la classe
Citron, que nous nommons citron1.
Lignes 9 et 10. Lorsqu’on tape le nom de l’instance citron1, l’interpréteur nous rappelle qu’il s’agit d’un objet de
type Citron, ainsi que son adresse en mémoire.
Il est également possible de vérifier qu’une instance est bien issue d’une classe donnée avec la fonction isinstance() :
1 >>> isinstance(citron1, Citron)
2 True
Lignes 1 et 2. L’objet possède de nombreuses méthodes ou attributs, qui commencent et qui se terminent par deux
caractères underscores. On se souvient que les underscores indiquent qu’il s’agit de méthodes ou attributs destinés au
fonctionnement interne de l’objet. Nous reviendrons sur certains d’entre-eux dans la suite.
Ligne 3. Ici on ajoute un attribut .couleur à l’instance citron1. Notez bien la syntaxe [Link] et
le point qui lie les deux.
Lignes 4 à 5. La fonction dir() nous montre que l’attribut .couleur a bien été ajouté à l’objet.
Lignes 6. La notation [Link] donne accès à l’attribut de l’objet.
L’attribut nommé .__dict__ est particulièrement intéressant. Il s’agit d’un dictionnaire qui listera les attributs créés
dynamiquement dans l’instance en cours :
1 >>> citron1 = Citron()
2 >>> citron1.__dict__
3 {}
4 >>> [Link] = "jaune"
5 >>> citron1.__dict__
6 {'couleur': 'jaune'}
L’ajout d’un attribut depuis l’extérieur de la classe (on parle aussi du côté « client ») avec une syntaxe instance.
nouvel_attribut = valeur, créera ce nouvel attribut uniquement pour cette instance :
1 citron1 = Citron()
2 [Link] = "jaune"
3 >>> citron1.__dict__
4 {'couleur': 'jaune'}
5 >>> citron2 = Citron()
6 >>> citron2.__dict__
7 {}
Si on crée une nouvelle instance de Citron, ici citron2, elle n’aura pas l’attribut
couleur à sa création.
Définition
Une variable ou attribut d’instance est une variable accrochée à une instance et qui lui est spécifique. Cet attribut
n’existe donc pas forcément pour toutes les instances d’une classe donnée et, d’une instance à l’autre, il ne prendra
pas forcément la même valeur. On peut retrouver tous les attributs d’instance d’une instance donnée avec une syntaxe
instance.__dict__.
L’instruction del fonctionne bien sûr pour détruire un objet (par exemple : del citron1), mais permet également
de détruire un attribut d’instance. Si on reprend notre exemple citron1 ci-dessus :
1 >>> citron1.__dict__
2 {'couleur': 'jaune'}
3 >>> del [Link]
4 >>> citron1.__dict__
5 {}
Dans la suite, on montrera du code à tester dans un script : n’hésitez pas, comme d’habitude, à le tester par
vous-même.
Définition
Une variable de classe ou attribut de classe est un attribut qui sera identique pour chaque instance. On verra plus
bas que de tels attributs suivent des règles différentes par rapport aux attributs d’instance.
À l’extérieur ou à l’intérieur d’une classe, un attribut de classe peut se retrouver avec une syntaxe NomClasse.
attribut :
1 print([Link])
Ce code affiche jaune. L’attribut de classe est aussi visible depuis n’importe quelle instance :
1 class Citron:
2 couleur = "jaune"
3
4
5 if __name__ == "__main__":
6 citron1 = Citron()
7 print([Link])
8 citron2 = Citron()
9 print([Link])
Attention
Même si on peut retrouver un attribut de classe avec la syntaxe [Link], un tel attribut ne peut pas
être modifié avec une instruction de cette forme :
1 [Link] = nouvelle_valeur
Définition
Une fonction définie au sein d’une classe est appelée méthode. Pour exécuter une méthode à l’extérieur de la classe,
la syntaxe générale est instance.méthode(). En général, on distingue attributs et méthodes (comme nous le ferons
systématiquement dans ce chapitre). Toutefois, il faut garder à l’esprit qu’une méthode est finalement un objet de type
fonction. Ainsi, elle peut être vue comme un attribut également, concept que vous croiserez peut-être en consultant de
la documentation externe.
Voici un exemple d’ajout d’une fonction, ou plus exactement d’une méthode, au sein d’une classe (attention à
l’indentation !) :
1 class Citron:
2 def coucou(self):
3 print("Coucou, je suis la mth .coucou() dans la classe Citron !")
4
5
6 if __name__ == "__main__":
7 citron1 = Citron()
8 [Link]()
Lignes 2 et 3. On définit une méthode nommée .coucou(), qui va afficher un petit message. Attention, cette
méthode prend obligatoirement un argument que nous avons nommé ici self. Nous verrons dans les deux prochaines
rubriques la signification de ce self. Si on a plusieurs méthodes dans une classe, on saute toujours une ligne entre elles
afin de faciliter la lecture (comme pour les fonctions).
Ligne 7 et 8. On crée l’instance citron1 de la classe Citron, puis on exécute la méthode .coucou() avec une
syntaxe instance.méthode().
Une méthode étant une fonction, elle peut bien sûr retourner une valeur :
1 class Citron:
2 def recup_saveur(self):
3 return "acide"
4
5
6 if __name__ == "__main__":
7 citron1 = Citron()
8 saveur_citron1 = citron1.recup_saveur()
9 print(saveur_citron1)
Vous l’aurez deviné, ce code affichera acide à l’écran. Comme pour les fonctions, une valeur retournée par une
méthode est récupérable dans une variable, ici saveur_citron1.
23.1.5 Le constructeur
Lors de l’instanciation d’un objet à partir d’une classe, il peut être intéressant de lancer des instructions, comme,
d’initialiser certaines variables. Pour cela, on ajoute une méthode spéciale nommée .__init__() : cette méthode s’appelle
le « constructeur » de la classe. Il s’agit d’une méthode spéciale dont le nom est entouré de doubles underscores : en
effet, elle sert au fonctionnement interne de notre classe et, sauf cas extrêmement rare, elle n’est pas supposée être lancée
comme une fonction classique par l’utilisateur de la classe. Ce constructeur est exécuté à chaque instanciation de notre
classe, et ne renvoie pas de valeur, il ne possède donc pas de return.
Remarque
Pour les débutants, vous pouvez sauter cette remarque. Certains auteurs préfèrent nommer .__init__() « instantia-
teur » ou « initialisateur », pour signifier qu’il existe une autre méthode appelée .__new__(), qui participe à la création
d’une instance. Vous n’avez bien sûr pas à retenir ces détails pour continuer la lecture de ce chapitre, retenez simplement
que nous avons décidé de nommer la méthode .__init__() « constructeur » dans cet ouvrage.
Pour bien comprendre comment cela fonctionne, nous allons suivre un exemple simple avec le site Python Tutor 2
(déjà utilisé dans les chapitres 10 et 13 sur les fonctions). N’hésitez pas à copier/coller ce code dans Python Tutor pour
le tester vous-même :
1 class Citron:
2 def __init__(self):
3 [Link] = "jaune"
4
5
6 if __name__ == "__main__":
7 citron1 = Citron()
8 print([Link])
Étape 1
Figure 23.1. Au départ, Python Tutor nous montre que la classe Citron a été mise en mémoire, elle contient pour
l’instant la méthode .__init__().
Étape 2
Figure 23.2. Nous créons ensuite l’instance citron1 à partir de la classe Citron. Notre classe Citron contenant
une méthode .__init__() (le constructeur), celle-ci est immédiatement exécutée au moment de l’instanciation. Cette
méthode prend un argument nommé self : cet argument est obligatoire. Il s’agit en fait d’une référence vers l’instance
2. [Link]
en cours (instance que nous appellerons citron1 dans le programme principal, mais cela serait vrai pour n’importe quel
autre nom d’instance). Python Tutor nous indique cela par une flèche pointant vers un espace nommé Citron instance.
La signification du self est expliquée en détail dans la rubrique suivante.
Étape 3
Figure 23.3. Un nouvel attribut est créé s’appelant [Link]. La chaîne de caractères couleur est ainsi «
accrochée » (grâce au caractère point) à l’instance en cours référencée par le self. Python Tutor nous montre cela par
une flèche qui pointe depuis le self vers la variable couleur (qui se trouve elle-même dans l’espace nommé Citron
instance). Si d’autres attributs étaient créés, ils seraient tous répertoriés dans cet espace Citron instance. Vous
l’aurez compris, l’attribut couleur est donc une variable d’instance (voir rubrique Ajout d’un attribut d’instance ci-
dessus). La méthode .__init__() étant intrinsèquement une fonction, Python Tutor nous rappelle qu’elle ne renvoie
rien (d’où le None dans la case Return value), une fois son exécution terminée. Et comme avec les fonctions classiques,
l’espace mémoire contenant les variables locales à cette méthode va être détruit une fois son exécution terminée.
Étape 4
Figure 23.4. De retour dans le programme principal, Python Tutor nous indique que citron1 est une instance de
la classe Citron par une flèche pointant vers l’espace Citron instance. Cette instance contient un attribut nommé
couleur auquel on accéde avec la syntaxe [Link] dans le print(). Notez que si l’instance s’était appelée
enorme_citron, on aurait utilisé enorme_citron.couleur pour accéder à l’attribut couleur.
Conseil
Dans la mesure du possible, nous vous conseillons de créer tous les attributs d’instance dont vous aurez besoin dans
le constructeur .__init__() plutôt que dans toute autre méthode. Ainsi, ils seront visibles dans toute la classe dès
l’instanciation.
1 class Citron:
2 def __init__(self, masse, couleur="jaune"):
3 [Link] = masse
4 [Link] = couleur
5
6
7 if __name__ == "__main__":
8 citron1 = Citron(100)
9 print("citron1:", citron1.__dict__)
10 citron2 = Citron(150, couleur="blanc")
11 print("citron2:", citron2.__dict__)
On a ici un argument positionnel (masse) et un autre par mot-clé (couleur). Le code donnera la sortie suivante :
1 citron1: {'masse': 100, 'couleur': 'jaune'}
2 citron2: {'masse': 150, 'couleur': 'blanc'}
Ligne 3. On crée l’attribut couleur que l’on accroche à l’instance avec self.
Ligne 4. Nous créons cette fois-ci une variable var sans l’accrocher à self.
Ligne 6. Nous créons une nouvelle méthode dans la classe Citron qui se nomme
.affiche_attributs(). Comme pour le constructeur, cette méthode prend comme premier argument une variable
obligatoire, que nous avons à nouveau nommée self. Il s’agit encore une fois d’une référence vers l’objet ou instance
créé(e).
Attention
On peut appeler cette référence comme on veut, toutefois nous vous conseillons vivement de l’appeler self, car c’est
une convention en Python. Ainsi, quelqu’un qui lira votre code comprendra immédiatement de quoi il s’agit.
Ligne 2. La méthode .affiche_attributs() montre que le self est bien une référence vers l’instance (ou objet)
citron1 (ou vers n’importe quelle autre instance : par exemple, si on crée citron2 = Citron(), le self sera une
référence vers citron2).
Ligne 3. La méthode .affiche_attributs() affiche l’attribut .couleur, qui avait été créé précédemment dans
le constructeur. Vous voyez ici l’intérêt principal de l’argument self passé en premier à chaque méthode d’une classe :
il « accroche » n’importe quel attribut qui sera visible partout dans la classe, y compris dans une méthode où il n’a pas
été défini.
Lignes 4 à 9. La création de la variable var dans la méthode .__init__() sans l’accrocher à l’objet self fait
qu’elle n’est plus accessible en dehors de .__init__(). C’est exactement comme pour les fonctions classiques, var
est finalement une variable locale au sein de la méthode .__init__() et n’est plus visible lorsque l’exécution de cette
dernière est terminée (voir les chapitres 10 et 13 sur les fonctions). Ainsi, Python renvoie une erreur, car var n’existe
pas lorsque .affiche_attributs() est en exécution.
En résumé, le self est nécessaire lorsqu’on a besoin d’accéder à différents attributs dans les différentes méthodes
d’une classe. Le self est également nécessaire pour appeler une méthode de la classe depuis une autre méthode :
1 class Citron:
2 def __init__(self, couleur="jaune"):
3 [Link] = couleur
4 self.affiche_message()
5
6 def affiche_message(self):
7 print("Le citron c'est trop bon !")
8
9
10 if __name__ == "__main__":
11 citron1 = Citron("jaune pâle")
Ligne 4. Nous appelons ici la méthode .affiche_message() depuis le constructeur. Pour appeler cette méthode
interne à la classe Citron, on doit utiliser une syntaxe self.méthode(). Le self sert donc pour accéder aux attributs,
mais aussi aux méthodes, ou plus généralement à tout ce qui est accroché à la classe.
Lignes 6 et 7. La méthode .affiche_message() est exécutée. On peut se poser la question « Pourquoi passer
l’argument self à cette méthode alors qu’on ne s’en sert pas dans celle-ci ? »
Attention
Même si on ne se sert d’aucun attribut dans une méthode, l’argument self (ou quel que soit son nom) est stricte-
ment obligatoire. En fait, la notation citron1.affiche_message() est équivalente à Citron.affiche_message
(citron1). Testez les deux pour voir ! Dans cette dernière instruction, on appelle la méthode accrochée à la classe
Citron et on lui passe explicitement l’instance citron1 en tant qu’argument. La notation citron1.affiche_message
() contient donc en filigrane un argument, à savoir la référence vers l’instance citron1 que l’on appelle self au sein
de la méthode.
Conseil
C’est la première notation citron1.affiche_attributs() (ou plus généralement instance.méthode()), plus
compacte, qui sera toujours utilisée.
Ligne 11. On crée l’instance citron1 en lui passant l’argument "jaune pâle". La variable d’instance couleur
prendra ainsi cette valeur au lieu de celle par défaut ("jaune"). À noter, l’instanciation affichera le message Le citron
c'est trop bon ! puisque la méthode .affiche_attributs() est appelée dans le constructeur .__init__().
Afin de bien comprendre les différentes étapes des codes de cette rubrique, nous vous conseillons de les retester de
votre côté dans Python Tutor.
23.2 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
Utilisez à nouveau l’instruction print() avec un objet de la classe Atome. Que constatez-vous par rapport au
précédent print() ?
3. [Link]
Dans le chapitre précédent, nous avons vu les bases sur comment créer une classe, les notions d’attributs d’instance
et de classe, le fonctionnent d’un constructeur et comment passer des arguments lors de l’instanciation. Nous avons vu
qu’une classe pouvait être vue comme un constructeur de conteneur (chaque conteneur construit est une instance), qu’on
pouvait y mettre tout un tas de variables ou objets (les attributs d’instance), mais également nous pouvions définir des
méthodes réalisant des actions pour modifier ce que contient l’objet.
Dans le présent chapitre, nous abordons de nouvelles notions qui augmentent la puissance des classes, à savoir le
polymorphisme, l’héritage et la composition. Nous verrons également les décorateurs property permettant le contrôle de
l’accès aux attributs. À la fin du chapitre, nous vous donnerons des bonnes pratiques pour construire vos classes. Mais
avant d’aborder ces sujets, nous revenons sur un concept important en Python, à savoir les espaces de noms.
Définition
Dans la documentation officielle 1 , un espace de noms est défini comme suit : « a namespace is a mapping from
names to objects ». Un espace de noms, c’est finalement une correspondance entre des noms et des objets. Un espace
de noms peut être vu aussi comme une capsule dans laquelle on trouve des noms d’objets. Par exemple, le programme
principal ou une fonction représentent chacun un espace de noms, un module aussi, et bien sûr une classe ou l’instance
d’une classe également.
Différents espaces de noms peuvent contenir des objets de même nom sans que cela ne pose de problème. Parce qu’ils
sont chacun dans un espace différent, ils peuvent cohabiter sans risque d’écrasement de l’un par l’autre. Par exemple, à
chaque fois que l’on appelle une fonction, un espace de noms est créé pour cette fonction. Python Tutor nous montre
cet espace sous la forme d’une zone dédiée (voir les chapitres 10 et 13 sur les fonctions). Si cette fonction appelle une
autre fonction, un nouvel espace est créé, bien distinct de la fonction appelante (ce nouvel espace peut donc contenir
1. [Link]
264
24.1. Espace de noms Chapitre 24. Avoir plus la classe avec les objets
un objet de même nom). En définitive, ce qui va compter, c’est de savoir quelles règles Python va utiliser pour chercher
dans les différents espaces de noms pour finalement accéder à un objet.
Nous allons dans cette rubrique refaire le point sur ce que l’on a appris dans cet ouvrage sur les espaces de noms en
Python, puis se pencher sur les spécificités de ce concept dans les classes.
En résumé, lorsqu’on lance une méthode d’un module, c’est l’espace de noms de celui-ci qui est utilisé. Bien sûr,
toutes les variables du programme principal / fonction / méthode appelant ce module sont conservées telles quelles, et
on les retrouve intactes lorsque l’exécution de la fonction du module est terminée. Un module a donc son propre espace
de noms qui est bien distinct de tout programme principal / fonction / méthode appelant un composant de ce module.
Enfin, les variables globales créées dans notre programme principal ne sont pas accessibles dans le module lorsque celui-ci
est en exécution.
On vient de voir qu’un module avait son propre espace de noms, mais qu’en est-il des classes ? En utilisant les exemples
vus depuis le début de ce chapitre, vous avez certainement la réponse. Une classe possède par définition son propre espace
de noms qui ne peut être en aucun cas confondu avec celui d’une fonction ou d’un programme principal. Reprenons un
exemple simple :
1 class Citron:
2 def __init__(self, saveur="acide", couleur="jaune"):
3 [Link] = saveur
4 [Link] = couleur
5 print("Dans __init__(), vous venez de créer un citron:",
6 self.affiche_attributs())
7
8 def affiche_attributs(self):
9 return f"{[Link]}, {[Link]}"
10
11
12 if __name__ == "__main__":
13 saveur = "sucrée"
14 couleur = "orange"
15 print(f"Dans le programme principal: {saveur}, {couleur}")
16 citron1 = Citron("très acide", "jaune foncé")
17 print("Dans citron1.affiche_attributs():", citron1.affiche_attributs())
18 print(f"Dans le programme principal: {saveur}, {couleur}")
Les deux variables globales saveur et couleur du programme principal ne peuvent pas être confondues avec les
variables d’instance portant le même nom. Au sein de la classe, on utilisera pour récupérer ces dernières [Link]
et [Link]. À l’extérieur, on utilisera [Link] et [Link]. Il n’y a donc aucun risque de
confusion possible avec les variables globales saveur et couleur, on accède à chaque variable de la classe avec un nom
distinct (qu’on soit à l’intérieur ou à l’extérieur de la classe).
Ceci est également vrai pour les méthodes. Si par exemple, on a une méthode avec un certain nom, et une fonction
du module principal avec le même nom, regardons ce qui se passe :
1 class Citron:
2 def __init__(self):
3 [Link] = "jaune"
4 self.affiche_coucou()
5 affiche_coucou()
6
7 def affiche_coucou(self):
8 print("Coucou interne !")
9
10
11 def affiche_coucou():
12 print("Coucou externe")
13
14
15 if __name__ == "__main__":
16 citron1 = Citron()
17 citron1.affiche_coucou()
18 affiche_coucou()
À nouveau, il n’y a pas de conflit possible pour l’utilisation d’une méthode ou d’une fonction avec le même nom. À
l’intérieur de la classe on utilise self.affiche_coucou() pour la méthode et affiche_coucou() pour la fonction.
À l’extérieur de la classe, on utilise instance.affiche_coucou() pour la méthode et affiche_coucou() pour la
fonction.
Dans cette rubrique, nous venons de voir une propriété des classes extrêmement puissante : une classe crée au-
tomatiquement son propre espace de noms. Cela permet d’encapsuler à l’intérieur tous les attributs et méthodes
dont on a besoin, sans avoir aucun risque de conflit de nom avec l’extérieur (variables locales, globales ou provenant de
modules). L’utilisation de classes évitera ainsi l’utilisation de variables globales qui, on l’a vu aux chapitres 10 et 13 sur
les fonctions, sont à proscrire absolument. Tout cela concourt à rendre le code plus lisible.
Dans le chapitre 25 Fenêtres graphiques et Tkinter (en ligne), vous verrez une démonstration de l’utilité de tout
encapsuler dans une classe afin d’éviter les variables globales.
Là encore, il existe certaines règles de priorités d’accès aux objets spécifiques à ce genre de cas, avec l’apparition d’un
nouveau mot-clé nommé nonlocal. Toutefois ces aspects vont au-delà du présent ouvrage. Pour plus d’informations sur
les fonctions imbriquées et la directive nonlocal, vous pouvez consulter la documentation officielle 4 .
D’autres subtilités concerneront la gestion des noms en cas de définition d’une nouvelle classe héritant d’une classe
4. [Link]
24.2 Polymorphisme
Nous allons voir maintenant des propriétés très importantes des classes en Python, le polymorphisme dans cette
rubrique et l’héritage dans la suivante. Ces deux concepts donnent un surplus de puissance à la POO par rapport à la
programmation classique.
24.2.1 Principe
Commençons par le polymorphisme. Dans la vie, celui-ci évoque la capacité à prendre plusieurs apparences, qu’en
est-il en programmation ?
Définition
En programmation, le polymorphisme est la capacité d’une fonction (ou méthode) à se comporter différemment en
fonction de l’objet qui lui est passé. Une fonction donnée peut donc avoir plusieurs définitions.
Prenons un exemple concret de polymorphisme : la fonction Python sorted() va trier par ordre ASCII si l’argument
est une chaîne de caractères, et elle va trier par ordre croissant lorsque l’argument est une liste d’entiers :
1 >>> sorted("citron")
2 ['c', 'i', 'n', 'o', 'r', 't']
3 >>> sorted([1, -67, 42, 0, 81])
4 [-67, 0, 1, 42, 81]
Le polymorphisme est intimement lié au concept de redéfinition des opérateurs que nous avons déjà croisé à plusieurs
reprises dans ce livre.
Définition
La redéfinition des opérateurs est la capacité à redéfinir le comportement d’un opérateur en fonction des opérandes
utilisées (on rappelle dans l’expression 1 + 1, + est l’opérateur d’addition et les deux 1 sont les opérandes).
Un exemple classique de redéfinition des opérateurs concerne l’opérateur +. Si les opérandes sont de type numérique,
il fait une addition, si elles sont des chaînes de caractère il fait une concaténation :
1 >>> 2 + 2
2 4
3 >>> "ti" + "ti"
4 'titi'
Nous verrons dans la rubrique suivante sur l’héritage qu’il est également possible de redéfinir des méthodes d’une
classe, c’est-à-dire leur donner une nouvelle définition.
Définition
Une méthode dunder (dunder method) est une méthode spéciale dont le nom est entouré de double underscores.
Par exemple, la méthode .__init__() est une méthode dunder. Ces méthodes sont, la plupart du temps, destinées
au fonctionnement interne de la classe. Nombre d’entre elles sont destinées à changer le comportement de fonctions ou
opérateurs internes à Python avec les instances d’une classe que l’on a créée. Le mot dunder signifie littéralement double
underscore. On parle aussi parfois de méthodes magiques.
Nous allons prendre un exemple concret. Imaginons que suite à la création d’une classe, nous souhaitions que Python
affiche un message personnalisé lors de l’utilisation de la fonction print() avec une instance de cette classe. La méthode
dunder qui permettra cela est nommée .__str__() : elle redéfinit le comportement d’une instance avec la fonction
print().
1 class CitronBasique:
2 def __init__(self, couleur="jaune", taille="standard"):
3 [Link] = "jaune"
4 [Link] = "standard"
5
6
7 class CitronCool:
8 def __init__(self, couleur="jaune", taille="standard"):
9 [Link] = couleur
10 [Link] = taille
11
12 def __str__(self):
13 return (f"Votre citron est de couleur {[Link]} "
14 f"et de taille {[Link]}")
15
16
17 if __name__ == "__main__":
18 citron1 = CitronBasique()
19 print(citron1)
20 citron2 = CitronCool("jaune foncée", "minuscule")
21 print(citron2)
Lignes 1 à 4. Création d’une classe CitronBasique dans laquelle il n’y a qu’un constructeur.
Lignes 7 à 14. Création d’une classe CitronCool où nous avons ajouté la nouvelle méthode .__str__(). Cette
dernière renvoie une chaîne de caractères contenant la description de l’instance.
Lignes 18 à 21. On crée une instance de chaque classe, et on utilise la fonction print() pour voir leur contenu.
L’exécution de ce code affichera la sortie suivante :
1 <__main__.CitronBasique object at 0x7ffe23e717b8>
2 Votre citron est de couleur jaune foncée et de taille minuscule 8-)
L’utilisation de la fonction print() sur l’instance citron1 construite à partir de la classe CitronBasique affiche
le message abscons que nous avons déjà croisé. Par contre, pour l’instance citron2 de la classe CitronCool, le texte
correspond à celui retourné par la méthode dunder .__str__(). Nous avons donc redéfini comment la fonction print()
se comportait avec une instance de la classe CitronCool. Notez que str(citron2) donnerait le même message que
print(citron2).
Ce mécanisme pourra être reproduit avec de très nombreux opérateurs et fonctions de bases de Python. En effet, il
existe une multitude de méthodes dunder, en voici quelques unes :
• .__repr__() : redéfinit le message obtenu lorsqu’on tape le nom de l’instance dans l’interpréteur ;
• .__add__() : redéfinit le comportement de l’opérateur + ;
• .__mul__() : redéfinit le comportement de l’opérateur * ;
• .__del__() : redéfinit le comportement de la fonction del.
Si on conçoit une classe produisant des objets séquentiels (comme des listes ou des tuples), il existe des méthodes
dunder telles que :
• .__len__() : redéfinit le comportement de la fonction len() ;
• .__getitem__() : redéfinit le comportement pour récupérer un élément ou des tranches sur un objet séquentiel.
Pour les tranches vous aurez également besoin de la fonction builtin slice() que nous ne développerons pas plus
en avant dans le cadre de ce cours.
Conseil
Nous vous conseillons la page de Trey Hunner 5 qui est bien complète sur comment mettre en place une méthode
.__getitem__() avec la fonction slice().
5. [Link]
Certaines méthodes dunder font des choses assez impressionnantes. Par exemple, la méthode .__call__() crée des
instances que l’on peut appeler comme des fonctions ! Dans cet exemple, nous allons vous montrer que l’on peut ainsi
créer un moyen inattendu pour mettre à jour des attributs d’instance :
1 class Citronnier:
2 def __init__(self, nb_citrons, age):
3 self.nb_citrons, [Link] = nb_citrons, age
4
5 def __call__(self, nb_citrons, age):
6 self.nb_citrons, [Link] = nb_citrons, age
7
8 def __str__(self):
9 return (f"Ce citronnier a {[Link]} ans "
10 f"et {self.nb_citrons} citrons")
11
12
13 if __name__ == "__main__":
14 citronnier1 = Citronnier(10, 3)
15 print(citronnier1)
16 citronnier1(30, 4)
17 print(citronnier1)
À la ligne 16, on utilise une notation instance(arg1, arg2), ce qui va automatiquement appeler la méthode dunder
.__call__() qui mettra à jour les deux attributs d’instance nbcitrons et age (lignes 5 et 6). Ce code affichera la
sortie suivante :
Ce citronnier a 3 ans et 10 citrons
Ce citronnier a 4 ans et 30 citrons
24.3 Héritage
24.3.1 Prise en main
L’héritage peut évoquer la capacité qu’ont nos parents à nous transmettre certains traits physiques ou de caractère
(ne dit-on pas, j’ai hérité ceci ou cela de ma mère ou de mon père ?). Qu’en est-il en programmation ?
Définition
En programmation, l’héritage est la capacité d’une classe d’hériter des propriétés d’une classe pré-existante. On parle
de classe mère et de classe fille. En Python, l’héritage peut être multiple lorsqu’une classe fille hérite de plusieurs classes
mères.
En Python, lorsque l’on veut créer une classe héritant d’une autre classe, on ajoutera après le nom de la classe fille
le nom de la ou des classe(s) mère(s) entre parenthèses :
6. [Link]
7. [Link]
1 class Mere1:
2 # contenu de la classe mère 1
3
4
5 class Mere2:
6 # contenu de la classe mère 2
7
8
9 class Fille1(Mere1):
10 # contenu de la classe fille 1
11
12
13 class Fille2(Mere1, Mere2):
14 # contenu de la classe fille 2
Dans cet exemple, la classe Fille1 hérite de la classe Mere1 et la classe Fille2 hérite des deux classes Mere1 et
Mere2. Dans le cas de la classe Fille2, on parle d’héritage multiple. Voyons maintenant un exemple concret :
1 class Mere:
2 def bonjour(self):
3 return "Vous avez le bonjour de la classe mère !"
4
5
6 class Fille(Mere):
7 def salut(self):
8 return "Un salut de la classe fille !"
9
10
11 if __name__ == "__main__":
12 fille = Fille()
13 print([Link]())
14 print([Link]())
Nous commençons à entrevoir la puissance de l’héritage. Si on possède une classe avec de nombreuses méthodes et
que l’on souhaite en ajouter de nouvelles, il suffit de créer une classe fille héritant d’une classe mère.
En revenant à notre exemple, une instance de la classe Fille sera automatiquement une instance de la classe Mere.
Regardons dans l’interpréteur :
1 >>> fille = Fille()
2 >>> isinstance(fille, Fille)
3 True
4 >>> isinstance(fille, Mere)
5 True
Si une méthode de la classe fille possède le même nom que celle de la classe mère, c’est la première qui prend la
priorité. Dans ce cas, on dit que la méthode est redéfinie (en anglais on parle de method overriding), tout comme on
parlait de redéfinition des opérateurs un peu plus haut. C’est le même mécanisme, car la redéfinition des opérateurs
revient finalement à redéfinir une méthode dunder (comme par exemple la méthode .__add__() pour l’opérateur +).
Voyons un exemple :
1 class Mere:
2 def bonjour(self):
3 return "Vous avez le bonjour de la classe mère !"
4
5
6 class Fille2(Mere):
7 def bonjour(self):
8 return "Vous avez le bonjour de la classe fille !"
9
10
11 if __name__ == "__main__":
12 fille = Fille2()
13 print([Link]())
Ce code va afficher Vous avez le bonjour de la classe fille !. La méthode .bonjour() de la classe fille
a donc pris la priorité sur celle de la classe mère. Ce comportement provient de la gestion des espaces de noms par
Python, il est traité en détail dans la rubrique suivante.
Remarque
À ce point, nous pouvons faire une note de sémantique importante. Python utilise le mécanisme de redéfinition de
méthode (method overriding), c’est-à-dire qu’on redéfinit une méthode héritée d’une classe mère. Il ne faut pas confondre
cela avec la surcharge de fonction (function overloading) qui désigne le fait d’avoir plusieurs définitions d’une fonction
selon le nombres d’arguments et/ou leur type (la surcharge n’est pas supportée par Python contrairement à d’autres
langages orientés objet).
On voit tout de suite que la classe Tk hérite de deux autres classes Misc et Wm. Ensuite, le help indique l’ordre de
résolution des méthodes : d’abord la classe Tk elle-même, ensuite ses deux mères Misc puis Wm, et enfin une dernière
Remarque
En Python, il existe une classe interne nommée object qui est en quelque sorte la classe ancêtre de tous les objets.
Toutes les classes héritent de object.
L’aide nous montre que Citron a hérité de [Link] bien que nous ne l’ayons pas déclaré explicitement.
Cela se fait donc de manière implicite.
Remarque
Le module builtins possède toutes les fonctions internes à Python. Il est donc pratique pour avoir une liste de toutes ces
fonctions internes en un coup d’œil. Regardons cela avec les deux instructions import builtins puis dir(builtins) :
Au début, on y trouve les exceptions commençant par une lettre majuscule (voir le chapitre 26 Remarques complé-
mentaires (en ligne) pour la définition d’une exception), puis les fonctions Python de base tout en minuscule. On retrouve
par exemple list ou str, mais il y a aussi object. Toutefois ces fonctions étant chargées de base dans l’interpréteur,
l’importation de builtins n’est pas obligatoire : par exemple list revient au même que [Link], ou object
revient au même que [Link].
Ainsi, même si on crée une classe Citron vide (contenant seulement une commande pass), elle possède déjà tout
une panoplie de méthodes héritées de la classe object. Regardez l’exemple suivant :
La quasi-totalité des attributs / méthodes de base de la classe Citron sont donc hérités de la classe object. Par
exemple, lorsqu’on instancie un objet Citron c = Citron(), Python utilisera la méthode .__init__() héritée de la
classe object (puisque nous ne l’avons pas définie dans la classe Citron).
Nous allons maintenant prendre un exemple un peu plus conséquent pour illustrer la puissance de l’héritage en
programmation. D’abord quelques mots à propos de la conception. Imaginons que nous souhaitions créer plusieurs classes
correspondant à nos fruits favoris, par exemple le citron (comme par hasard !), l’orange, le kaki, etc. Chaque fruit a ses
propres particularités, mais il y a aussi de nombreux points communs. Nous pourrions donc concevoir une classe Fruit
permettant, par exemple, d’instancier un fruit et ajouter des méthodes d’affichage commune à n’importe quel fruit, et
ajouter (ou toute autre méthode) pouvant être utilisée pour n’importe quel fruit. Nous pourrions alors créer des classes
comme Citron, Orange, etc., héritant de la classe Fruit et ainsi nous économiser des lignes de code identiques à
ajouter pour chaque fruit. Regardons l’exemple suivant que nous avons garni de print() pour bien comprendre ce qui
se passe :
1 class Fruit:
2 def __init__(self, taille=None, masse=None, saveur=None, forme=None):
3 print("(2) Je suis dans le constructeur de la classe Fruit")
4 [Link] = taille
5 [Link] = masse
6 [Link] = saveur
7 [Link] = forme
8 print("Je viens de créer [Link], [Link], [Link] "
9 "et [Link]")
10
11 def affiche_conseil(self, type_fruit, conseil):
12 print("(2) Je suis dans la méthode .affiche_conseil() de la "
13 "classe Fruit\n")
14 return (f"Instance {type_fruit}\n"
15 f"taille: {[Link]}, masse: {[Link]}\n"
16 f"saveur: {[Link]}, forme: {[Link]}\n"
17 f"conseil: {conseil}\n")
18
19
20 class Citron(Fruit):
21 def __init__(self, taille=None, masse=None, saveur=None, forme=None):
22 print("(1) Je rentre dans le constructeur de Citron, et je vais "
23 "appeler\n"
24 "le constructeur de la classe mère Fruit !")
25 Fruit.__init__(self, taille, masse, saveur, forme)
26 print("(3) J'ai fini dans le constructeur de Citron, "
27 "les attributs sont :\n"
28 f"[Link]: {[Link]}, [Link]: {[Link]}\n"
29 f"[Link]: {[Link]}, [Link]: {[Link]}\n")
30
31 def __str__(self):
32 print("(1) Je rentre dans la méthode .__str__() de la classe "
33 "Citron")
34 print("Je vais lancer la méthode .affiche_conseil() héritée "
35 "de la classe Fruit")
36 return self.affiche_conseil("Citron", "Bon en tarte :-p !")
37
38
39 if __name__ == "__main__":
40 # On crée un citron.
41 citron1 = Citron(taille="petite", saveur="acide",
42 forme="ellipsoïde", masse=50)
43 print(citron1)
Lignes 1 à 9. On crée la classe Fruit avec son constructeur qui initialisera tous les attributs d’instance décrivant le
fruit.
Lignes 11 à 17. Création d’une méthode .affiche_conseil() qui retourne une chaîne contenant le type de fruit,
les attributs d’instance du fruit, et un conseil de consommation.
Lignes 20 à 29. Création de la classe Citron qui hérite de la classe Fruit. Le constructeur de Citron prend les
mêmes arguments que ceux du constructeur de Fruit. La ligne 24 est une étape importante que nous n’avons encore
jamais vue : l’instruction Fruit.__init__() est un appel au constructeur de la classe mère (cf. explications plus bas).
Notez bien que le premier argument passé au constructeur de la classe mère sera systématiquement l’instance en cours
self. Le print() en lignes 26-29 illustre qu’après l’appel du constructeur de la classe mère tous les attributs d’instance
([Link], [Link], etc.) ont bel et bien été créés.
Lignes 31 à 36. On définit la méthode .__str__() qui va modifier le comportement de notre classe avec print().
Celle-ci fait également appel à une méthode hértitée de la classe mère nommée .affiche_conseil(). Comme on a l’a
héritée, elle est directement accessible avec un self.méthode() (et de l’extérieur ce serait instance.méthode()).
Lignes 39 à 43. Dans le programme principal, on instancie un objet Citron, puis on utilise print() sur l’instance.
Instance Citron
taille: petite, masse: 50
saveur: acide, forme: ellipsoïde
conseil: Bon en tarte :-p !
Prenez bien le temps de suivre ce code pas à pas pour bien en comprendre toutes les étapes.
Vous pourrez vous poser la question « Pourquoi utilise-t-on en ligne 24 la syntaxe Fruit.__init__() ? ». Cette
syntaxe est souvent utilisée lorsqu’une classe hérite d’une autre classe pour faire appel au constructeur de la classe mère.
La raison est que nous souhaitons appeler une méthode de la classe mère qui a le même nom qu’une méthode de la
classe fille. Dans ce cas, si on utilisait self.__init__(), cela correspondrait à la fonction de notre classe fille Citron.
En mettant systématiquement une syntaxe
ClasseMere.__init__() on indique sans ambiguïté qu’on appelle le constructeur de la classe mère, en mettant explici-
tement son nom. Ce mécanisme est assez souvent utilisé dans le module Tkinter (voir le chapitre 25 Fenêtres graphiques
et Tkinter (en ligne)) pour la construction d’interfaces graphiques, nous en verrons de nombreux exemples.
Remarque
Si vous utilisez des ressources externes, il se peut que vous rencontriez une syntaxe super().__init__(). La
fonction Python interne super() appelle automatiquement la classe mère sans que vous ayez à donner son nom. Même
si cela peut paraître pratique, nous vous conseillons d’utiliser dans un premier temps la syntaxe
ClasseMere.__init__() qui est selon nous plus lisible (on voit explicitement le nom de la classe employée, même s’il
y a plusieurs classes mères).
Ce mécanisme n’est pas obligatoirement utilisé, mais il est très utile lorsqu’une classe fille a besoin d’initialiser des
attributs définis dans la classe mère. On le croise donc souvent car :
• Cela donne la garantie que toutes les variables de la classe mère sont bien initialisées. On réduit ainsi les risques
de dysfonctionnement des méthodes héritées de la classe mère.
• Finalement, autant ré-utiliser les « moulinettes » de la classe mère, c’est justement à ça que sert l’héritage ! Au
final, on écrit moins de lignes de code.
Conseil
Pour les deux raisons citées ci-dessus, nous vous conseillons de systématiquement utiliser le constructeur de la classe
mère lors de l’instanciation.
Vous avez à présent bien compris le fonctionnement du mécanisme de l’héritage. Dans notre exemple, nous pourrions
créer de nouveaux fruits avec un minimum d’effort. Ceux-ci pourraient hériter de la classe mère Fruit à nouveau, et
nous n’aurions pas à réécrire les mêmes méthodes pour chaque fruit, simplement à les appeler. Par exemple :
1 class Kaki(Fruit):
2 def __init__(self, taille=None, masse=None, saveur=None, forme=None):
3 Fruit.__init__(self, taille, masse, saveur, forme)
4
5 def __str__(self):
6 return Fruit.affiche_conseil(self, "Kaki",
7 "Bon à manger cru, miam !")
8
9
10 class Orange(Fruit):
11 def __init__(self, taille=None, masse=None, saveur=None, forme=None):
12 Fruit.__init__(self, taille, masse, saveur, forme)
13
14 def __str__(self):
15 return Fruit.affiche_conseil(self, "Orange", "Trop bon en jus !")
Cet exemple illuste la puissance de l’héritage et du polymorphisme et la facilité avec laquelle on les utilise en Python.
Pour chaque fruit, on utilise la méthode .affiche_conseil() définie dans la classe mère sans avoir à la réécrire. Bien
sûr cet exemple reste simpliste et n’est qu’une « mise en bouche ». Vous verrez des exemples concrets de la puissance de
l’héritage dans le chapitre 25 Fenêtres graphiques et Tkinter (en ligne) ainsi que dans les exercices du présent chapitre.
Avec le module Tkinter, chaque objet graphique (bouton, zone de texte, etc.) est en fait une classe. On peut ainsi créer
de nouvelles classes héritant des classes Tkinter afin de personnaliser chaque objet graphique.
24.4 Composition
Définition
La composition désigne le fait qu’une classe peut contenir des instances provenant d’autres classes. On parle parfois
de classe Composite contenant des instances d’une classe Component (qu’on pourrait traduire par élément).
Pour vous illustrer cela, nous allons prendre un exemple sur notre fruit préféré, le citron. Un citron (classe Composite)
contient de la pulpe (classe Component). Voilà comment nous pourrions l’implémenter :
1 class Pulpe:
2 def __init__(self, quantite_jus):
3 self.quantite_jus = quantite_jus # En cL.
4
5 def __str__(self):
6 return f"Cette pulpe contient {self.quantite_jus} cL de jus"
7
8
9 class Citron:
10 def __init__(self, pulpe=None):
11 [Link] = pulpe
12
13 def presse_citron(self):
14 if [Link]:
15 print(f"Le pressage de la pulpe délivre "
16 f"{[Link].quantite_jus} cL de jus")
17 [Link] = None
18 else:
19 print("Il n'y a plus rien à presser dans votre citron !")
20
21 def __str__(self):
22 if [Link]:
23 return f"Votre citron contient {[Link].quantite_jus} cL de jus"
24 else:
25 return "Ce citron ne contient pas de pulpe"
26
27
28 if __name__ == "__main__":
29 pulpe = Pulpe(10)
30 print(pulpe)
31 citron1 = Citron()
32 print(citron1)
33 print()
34 citron2 = Citron(pulpe)
35 print([Link])
36 print(citron2)
37 print()
38 citron2.presse_citron()
39 citron2.presse_citron()
40 print(citron2)
Lignes 1 à 6. On crée une classe Pulpe qui prend en argument à l’instanciation une quantité de jus (en cL) qu’elle
peut délivrer si on la presse.
Lignes 9 à 25. On crée une classe Citron qui prend un objet Pulpe à l’instanciation. Si aucun objet est passé, on
affecte None. Cette classe contient une méthode .presse_citron() qui pressera la pulpe pour délivrer le jus de citron.
Une fois le pressage effectué, il n’y aura plus de jus à délivrer.
La sortie sera la suivante :
Cette pulpe contient 10 cL de jus
Ce citron ne contient pas de pulpe
Dans cet exemple, la classe Citron a utilisé une instance de la classe Pulpe pour fonctionner. Un avantage de la
composition est qu’on pourrait réutiliser cette classe Pulpe dans une classe Orange ou Pamplemousse. Par ailleurs, si
on change des détails dans la classe Pulpe, cela affectera peu la classe Citron à partir du moment où on garde l’attribut
.quantite_jus.
De manière générale, la composition est considérée comme plus flexible que l’héritage car les classes Composite et
Component sont peu couplées. Le changement de l’une d’entre elle aura peu d’effet sur l’autre. Au contraire, pour
l’héritage le changement d’une classe mère peut avoir des répercussions importantes pour les classes filles. Toutefois,
dans certains cas l’héritage peut s’avérer plus naturel. Nous vous parlions en introduction du chapitre 23 Avoir la classe
avec les objets de l’art pour concevoir des classes interagissant harmonieusement entre elles. Et bien nous y sommes !
Si on a deux classes A et B, la relation entre elles dans l’héritage sera de type B is a A (avec B qui hérite de A). Dans
la composition, ce sera plutôt A has a B. Cela peut vous servir de piste dans la conception des relations entre vos classes.
A-t-il plus de sens d’y avoir une relation is a ou bien has a ? Dans le premier cas vous irez plutôt vers l’héritage, alors
que dans le deuxième plutôt vers la composition. C’est ici que le langage UML 8 peut être pratique pour avoir une vision
d’ensemble sur comment les classes interagissent entre elles.
Bien sûr, il faudra vous entraîner sur des cas concrets pour acquérir l’expérience qui vous mènera aux bons choix.
A la fin de ce chapitre, nous vous présentons un exercice pour vous entraîner dans un premier temps à la composition.
Dans le chapitre 25 Fenêtres graphiques et Tkinter (en ligne), vous aurez des illustrations et des exercices sur l’héritage
qui est très utilisé en Tkinter.
8. [Link]
9. [Link]
10. [Link]
11. [Link]
12. [Link]
13. [Link]
1 class Citron:
2 forme = "ellipsoïde" # attribut de classe
3 saveur = "acide" # attribut de classe
4
5 def __init__(self, couleur="jaune", taille="standard", masse=0):
6 [Link] = couleur # attribut d'instance
7 [Link] = taille # attribut d'instance
8 [Link] = masse # attribut d'instance (masse en gramme)
9
10 def augmente_masse(self, valeur):
11 [Link] += valeur
12
13
14 if __name__ == "__main__":
15 citron1 = Citron()
16 print("Attributs de classe :", [Link], [Link])
17 print("Attributs d'instance :", [Link], [Link],
18 [Link])
19 citron1.augmente_masse(100)
20 print("Attributs d'instance :", [Link], [Link],
21 [Link])
Lignes 2 et 3. Nous créons deux variables de classe qui seront communes à toutes les instances (disons qu’un citron
sera toujours ellipsoïde et acide !).
Lignes 6 à 8. Nous créons trois variables d’instance qui seront spécifiques à chaque instance (disons que la taille, la
couleur et la masse d’un citron peuvent varier !), avec des valeurs par défaut.
Lignes 10 et 11. On crée une nouvelle méthode .augmente_masse() qui augmente l’attribut d’instance .masse.
Ligne 14 à 21. Dans le programme principal, on instancie la classe Citron sans passer d’argument (les valeurs par
défaut "jaune", "standard" et 0 seront donc prises), puis on imprime les attributs.
La figure 24.1 montre l’état des variables après avoir exécuté ce code grâce au site Python Tutor 14 .
Figure 24.1 – Illustration de la signification des attributs de classe et d’instance avec Python Tutor.
Python Tutor montre bien la différence entre les variables de classe forme et saveur qui apparaissent directement
dans les attributs de la classe Citron lors de sa définition et les trois variables d’instance couleur, taille et masse
14. [Link]
qui sont liées à l’instance citron1. Pour autant, on voit dans la dernière instruction print() qu’on peut accéder de la
même manière aux variables de classe ou d’instance, lorsqu’on est à l’extérieur, avec une syntaxe [Link].
Au sein des méthodes, on accède également de la même manière aux attributs de classe ou d’instance, avec une
syntaxe [Link] :
1 class Citron:
2 saveur = "acide" # attribut de classe
3
4 def __init__(self, couleur="jaune"):
5 [Link] = couleur # attribut d'instance
6
7 def affiche_attributs(self):
8 print(f"attribut de classe: {[Link]}")
9 print(f"attribut d'instance: {[Link]}")
10
11
12 if __name__ == "__main__":
13 citron1 = Citron()
14 citron1.affiche_attributs()
En résumé, qu’on ait des attributs de classe ou d’instance, on peut accéder à eux de l’extérieur par instance.
attribut et de l’intérieur par [Link].
Qu’en est-il de la manière de modifier ces deux types d’attributs ? Les attributs d’instance peuvent se modifier sans
problème de l’extérieur avec une syntaxe instance.attribut_d_instance = nouvelle_valeur et de l’intérieur
avec une syntaxe self.attribut_d_instance = nouvelle_valeur. Ce n’est pas du tout le cas avec les attributs
de classe.
Attention
Les attributs de classe ne peuvent pas être modifiés ni à l’extérieur d’une classe via une syntaxe instance.
attribut_de_classe = nouvelle_valeur, ni à l’intérieur d’une classe via une syntaxe self.attribut_de_classe
= nouvelle_valeur. Puisqu’ils sont destinés à être identiques pour toutes les instances, cela est logique de ne pas
pouvoir les modifier via une instance. Les attributs de classe Python ressemblent en quelque sorte aux attributs statiques
du C++.
À la ligne 7, on pourrait penser qu’on modifie l’attribut de classe saveur avec une syntaxe instance.attribut_de_classe
= nouvelle_valeur. Que se passe-t-il exactement ? La figure 24.3 nous montre l’état des variables grâce au site
Python Tutor. Celui-ci indique que la ligne 7 a en fait créé un nouvel attribut d’instance [Link] (contenant la
valeur sucrée) qui est bien distinct de l’attribut de classe auquel on accédait avant par le même nom ! Tout ceci est dû
à la manière dont Python gère les espaces de noms (voir rubrique Espaces de noms). Dans ce cas, l’attribut d’instance
est prioritaire sur l’attribut de classe.
À la ligne 9, on détruit finalement l’attribut d’instance [Link] qui contenait la valeur sucrée. Python
Figure 24.2 – Illustration avec Python Tutor de la non destruction d’un attribut de classe (étape 1).
Tutor nous montre que [Link] n’existe pas dans l’espace Citron instance qui est vide ; ainsi, Python
utilisera l’attribut de classe .saveur qui contient toujours la valeur acide (cf. figure 24.3).
Figure 24.3 – Illustration avec Python Tutor de la non destruction d’un attribut de classe (étape 2).
La ligne 11 va tenter de détruire l’attribut de classe .saveur. Toutefois, Python interdit cela, ainsi l’erreur suivante
sera générée :
En fait, la seule manière de modifier un attribut de classe est d’utiliser une syntaxe
NomClasse.attribut_de_classe = nouvelle_valeur,
dans l’exemple ci-dessus cela aurait été [Link] = "sucrée". De même, pour sa destruction, il faudra utiliser
la même syntaxe : del [Link].
Conseil
Même si on peut modifier un attribut de classe, nous vous déconseillons de le faire. Une utilité des attributs de classe
est par exemple de définir des constantes (mathématique ou autre), donc cela n’a pas de sens de vouloir les modifier ! Il
est également déconseillé de créer des attributs de classe avec des objets modifiables comme des listes et des dictionnaires,
cela peut avoir des effets désastreux non désirés. Nous verrons plus bas un exemple concret d’attribut de classe qui est
très utile, à savoir le concept d’objet de type property.
Si vous souhaitez avoir des attributs modifiables dans votre classe, créez plutôt des attributs d’instance dans le
.__init__().
24.6.1 Le problème
On a vu jusqu’à maintenant que Python était très permissif concernant le changement de valeur de n’importe quel
attribut depuis l’extérieur. On a vu aussi qu’il était même possible de créer de nouveaux attributs depuis l’extérieur ! Dans
d’autres langages orientés objet ceci n’est pas considéré comme une bonne pratique. Il est plutôt recommandé de définir
une interface, c’est-à-dire tout un jeu de méthodes accédant ou modifiant les attributs. Ainsi, le concepteur de la classe
a la garantie que celle-ci est utilisée correctement du « côté client ».
Remarque
Cette stratégie d’utiliser uniquement l’interface de la classe pour accéder aux attributs provient des langages orientés
objet comme Java et C++. Les méthodes accédant ou modifiant les attributs s’appellent aussi des getters et setters (en
français on dit accesseurs et mutateurs). Un des avantages est qu’il est ainsi possible de vérifier l’intégrité des données
grâce à ces méthodes : si par exemple on souhaitait avoir un entier seulement, ou bien une valeur bornée, on peut
facilement ajouter des tests dans le setter et renvoyer une erreur à l’utilisateur de la classe s’il n’a pas envoyé le bon type
(ou la bonne valeur dans l’intervalle imposé).
1 class Citron:
2 def __init__(self, couleur="jaune", masse=0):
3 [Link] = couleur
4 [Link] = masse # masse en g
5
6 def get_couleur(self):
7 return [Link]
8
9 def set_couleur(self, value):
10 [Link] = value
11
12 def get_masse(self):
13 return [Link]
14
15 def set_masse(self, value):
16 if value < 0:
17 raise ValueError("Z'avez déjà vu une masse négative ?")
18 [Link] = value
19
20
21 if __name__ == "__main__":
22 # définition de citron1
23 citron1 = Citron()
24 print(citron1.get_couleur(), citron1.get_masse())
25 # on change les attributs de citron1 avec les setters
26 citron1.set_couleur("jaune foncé")
27 citron1.set_masse(100)
28 print(citron1.get_couleur(), citron1.get_masse())
Lignes 6 à 10. On définit deux méthodes getters pour accéder à chaque attribut.
Lignes 12 à 18. On définit deux méthodes setters pour modifier chaque attribut. Notez qu’en ligne 16 nous testons
si la masse est négative, si tel est le cas nous générons une erreur avec le mot-clé raise (voir le chapitre 26 Remarques
complémentaires (en ligne)). Ceci représente un des avantages des setters : contrôler la validité des attributs (on pourrait
aussi vérifier qu’il s’agit d’un entier, etc.).
Lignes 22 à 28. Après instanciation, on affiche la valeur des attributs avec les deux fonctions getters, puis on les
modifie avec les setters et on les affiche à nouveau.
jaune 0
jaune foncé 100
1 jaune 0
2 Traceback (most recent call last):
3 File "getter_setter.py", line 27, in <module>
4 citron1.set_masse(-100)
5 File "getter_setter.py", line 17, in set_masse
6 raise ValueError("Z'avez déjà vu une masse négative ???")
7 ValueError: Z'avez déjà vu une masse négative ???
La fonction interne raise nous a permis de générer une erreur, car l’utilisateur de la classe (c’est-à-dire nous dans le
programme principal) n’a pas rentré une valeur correcte.
On comprend bien l’utilité d’une stratégie avec des getters et setters dans cet exemple. Toutefois, en Python, on
peut très bien accéder et modifier les attributs même si on a des getters et des setters dans la classe. Imaginons la même
classe Citron que ci-dessus, mais on utilise le programme principal suivant (notez que nous avons simplement ajouté les
lignes 9 à 12 ci-dessous) :
1 if __name__ == "__main__":
2 # définition de citron1
3 citron1 = Citron()
4 print(citron1.get_couleur(), citron1.get_masse())
5 # on change les attributs de citron1 avec les setters
6 citron1.set_couleur("jaune foncé")
7 citron1.set_masse(100)
8 print(citron1.get_couleur(), citron1.get_masse())
9 # on les rechange sans les setters
10 [Link] = "pourpre profond"
11 [Link] = -15
12 print(citron1.get_couleur(), citron1.get_masse())
Malgré la présence des getters et des setters, nous avons réussi à accéder et à modifier la valeur des attributs. De
plus, nous avons pu mettre une valeur aberrante (masse négative) sans que cela ne génère une erreur !
Vous vous posez sans doute la question : mais dans ce cas, quel est l’intérêt de mettre des getters et des setters en
Python ? La réponse est très simple : cette stratégie n’est pas une manière « pythonique » d’opérer (voir le chapitre 16
Bonnes pratiques en programmation Python pour la définition de « pythonique »). En Python, la lisibilité est la priorité.
Souvenez-vous du Zen de Python « Readability counts » (voir le chapitre 16).
De manière générale, une syntaxe avec des getters et setters du côté client surcharge la lecture. Imaginons que l’on
ait une instance nommée obj et que l’on souhaite faire la somme de ses trois attributs x, y et z :
1 # pythonique
2 obj.x + obj.y + obj.z
3
4 # non pythonique
5 obj.get_x() + obj.get_y() + obj.get_z()
La méthode pythonique est plus « douce » à lire, on parle aussi de syntactic sugar ou littéralement en français « sucre
syntaxique ». De plus, à l’intérieur de la classe, il faut définir un getter et un setter pour chaque attribut, ce qui multiple
les lignes de code.
Très bien. Donc en Python, on n’utilise pas comme dans les autres langages orientés objet les getters et les setters ?
Mais, tout de même, cela avait l’air une bonne idée de pouvoir contrôler comment un utilisateur de la classe interagit
avec certains attributs (par exemple, rentre-t-il une bonne valeur ?). N’existe-t-il pas un moyen de faire ça en Python ? La
réponse est : bien sûr il existe un moyen pythonique, la classe property. Nous allons voir cette nouvelle classe dans la
prochaine rubrique et nous vous dirons comment opérer systématiquement pour accéder, modifier, voire détruire, chaque
attribut d’instance de votre classe.
Les arguments passés à property() sont systématiquement des méthodes dites callback, c’est-à-dire des noms de
méthodes que l’on a définies précédemment dans notre classe, mais on ne précise ni argument, ni parenthèse, ni self
(voir le chapitre 25 Fenêtres graphiques et Tkinter (en ligne)). Avec cette ligne de code, attribut est un objet de type
property qui fonctionne de la manière suivante à l’extérieur de la classe :
• L’instruction [Link] appellera la méthode .accesseur().
• L’instruction [Link] = valeur appellera la méthode
.mutateur().
• L’instruction del [Link] appellera la méthode
.destructeur().
L’objet attribut est de type property, et la vraie valeur de l’attribut est stockée par Python dans une variable
d’instance qui s’appellera par exemple _attribut (même nom, mais commençant par un underscore unique, envoyant
un message à l’utilisateur qu’il s’agit d’une variable associée au comportement interne de la classe).
Comment cela fonctionne-t-il concrètement dans un code ? Regardons cet exemple (nous avons mis des print() un
peu partout pour bien comprendre ce qui se passe) :
1 class Citron:
2 def __init__(self, masse=0):
3 print("(2) J'arrive dans le .__init__()")
4 [Link] = masse
5
6 def get_masse(self):
7 print("Coucou je suis dans le get")
8 return self._masse
9
10 def set_masse(self, valeur):
11 print("Coucou je suis dans le set")
12 if valeur < 0:
13 raise ValueError("Un citron ne peut pas avoir"
14 " de masse négative !")
15 self._masse = valeur
16
17 masse = property(fget=get_masse, fset=set_masse)
18
19
20 if __name__ == "__main__":
21 print("(1) Je suis dans le programme principal, "
22 "je vais instancier un Citron")
23 citron = Citron(masse=100)
24 print("(3) Je reviens dans le programme principal")
25 print(f"La masse de notre citron est {[Link]} g")
26 # On mange le citron.
27 [Link] = 25
28 print(f"La masse de notre citron est {[Link]} g")
29 print(citron.__dict__)
Pour une fois, nous allons commenter les lignes dans le désordre :
Ligne 17. Il s’agit de la commande clé pour mettre en place le système : masse devient ici un objet de type property
(si on regarde son contenu avec une syntaxe NomClasse.attribut_property, donc ici [Link], Python nous
renverra quelque chose de ce style : <property object at 0x7fd3615aeef8>). Qu’est-ce que cela signifie ? Et bien la
prochaine fois qu’on voudra accéder au contenu de cet attribut .masse, Python appellera la méthode .get_masse(), et
quand on voudra le modifier, Python appellera la méthode .set_masse() (ceci sera valable de l’intérieur ou de l’extérieur
de la classe). Comme il n’y a pas de méthode destructeur (passée avec l’argument fdel), on ne pourra pas détruire cet
attribut : un del [Link] conduirait à une erreur de ce type : AttributeError: can't delete attribute.
Ligne 4. Si vous avez bien suivi, cette commande [Link] = masse dans le constructeur va appeler automa-
tiquement la méthode .set_masse(). Attention, dans cette commande, la variable masse à droite du signe = est une
variable locale passée en argument. Par contre, [Link] sera l’objet de type property. Si vous avez bien lu la rubrique
Différence entre les attributs de classe et d’instance, l’objet masse créé en ligne 16 est un attribut de classe, on peut
donc y accéder avec une syntaxe [Link] au sein d’une méthode.
Conseil
Notez bien l’utilisation de [Link] dans le constructeur (en ligne 4) plutôt que self._masse. Comme self.
masse appelle la méthode .set_masse(), cela permet de contrôler si la valeur est correcte dès l’instanciation. C’est
donc une pratique que nous vous recommandons. Si on avait utilisé self._masse, il n’y aurait pas eu d’appel à la
fonction mutateur et on aurait pu mettre n’importe quoi, y compris une valeur aberrante, lors de l’instanciation.
Attention
Dans les méthodes accesseur et mutateur, il ne faut surtout pas utiliser [Link] à la place de self._masse.
Pourquoi ? Par exemple, dans l’accesseur, si on met [Link] cela signifie que l’on souhaite accéder à la valeur
de l’attribut (comme dans le constructeur !). Ainsi, Python rappellera l’accesseur et retombera sur [Link], ce qui
rappellera l’accesseur et ainsi de suite : vous l’aurez compris, cela partira dans une récursion infinie et mènera à une erreur
du type RecursionError: maximum recursion depth exceeded. Cela serait vrai aussi si vous aviez une fonction
destructeur, il faudrait utiliser self._masse.
Cette exécution montre qu’à chaque appel de [Link] ou [Link] on va utiliser les méthodes accesseur
ou mutateur. La dernière commande qui affiche le contenu de citron.__dict__ montre que la vraie valeur de l’attribut
est stockée dans la variable d’instance ._masse (instance._masse de l’extérieur et self._masse de l’intérieur).
Nous venons de voir les objets property pour contrôler l’accès, la mutation et la supression d’attributs. Toutefois la
syntaxe est relativement lourde. Afin de la simplifier, une manière plus pythonique (sucre syntaxique) est d’utiliser un
décorateur. La syntaxe pour décorer une fonction est la suivante :
1 @decorateur
2 def fonction():
3 [...]
La ligne 1 précise que fonction() va être modifiée par une autre fonction nommée decorateur(). Le symbole @
en ligne 1 attend un nom de fonction qui sera la fonction décoratrice. Pour plus de détails sur comment les décorateurs
fonctionnent, vous pouvez consulter le chapitre 26 Remarques complémentaires où une rubrique leur est consacrée. Ici,
nous avons juste à savoir qu’un décorateur est une fonction qui modifie le comportement d’une autre fonction.
En reprenant l’exemple vu dans la rubrique précédente, voici comment on peut l’écrire avec des décorateurs :
1 class Citron:
2 def __init__(self, masse=0):
3 print(f"(2) J'arrive dans le .__init__(), je vais mettre la masse = {masse}")
4 [Link] = masse
5
6 @property
7 def masse(self):
8 print("Coucou je suis dans le getter")
9 return self._masse
10
11 @[Link]
12 def masse(self, valeur):
13 print("Coucou je suis dans le setter")
14 if valeur < 0:
15 raise ValueError("Un citron ne peut pas avoir"
16 " de masse négative !")
17 self._masse = valeur
18
19 @[Link]
20 def masse(self):
21 print("Coucou, je suis dans le deleter")
22 del self._masse
On voit que la syntaxe est plus lisible que celle de la rubrique précédente. Examinons les différences. La première
chose est que les méthodes getter (ligne 7), setter (ligne 11) et deleter (ligne 19) s’appellent toutes .masse(), masse
étant le nom de notre objet property. Comme dans la syntaxe de la rubrique précédente, la masse réelle se trouve dans
un attribut nommée ._masse pour ne pas confondre avec notre objet property. Afin de comprendre ce qu’il se passe,
nous vous avons concocté le programme principal suivant avec des print() un peu partout :
1 if __name__ == "__main__":
2 print("(1) Je suis dans le programme principal et "
3 "je vais instancier un Citron")
4 print()
5 citron = Citron(masse=100)
6 print()
7 print("(3) Je reviens dans le programme principal, je vais afficher "
8 "la masse du citron")
9 print(f"La masse de notre citron est {[Link]} g")
10 print()
11 # On mange une partie du citron.
12 print("(4)Je suis dans le prog principal "
13 "et je vais changer la masse du citron")
14 [Link] = 25
15 print()
16 print(f"(5) Je suis dans le prog principal, je vais afficher "
17 "la masse du citron")
18 print(f"La nouvelle masse de notre citron est {[Link]} g")
19 print(f"L'attribut citron.__dict__ m'indique bien le nom réel "
20 f"de l'attribut contenant la masse :")
21 print(citron.__dict__)
22 print()
23 # On mange la fin du citron.
24 print(f"(6) Je suis dans le prog principal, "
25 f"je détruis l'attribut .masse")
26 del [Link]
27 print(f"Ainsi, citron.__dict__ est maintenant vide :")
28 print(citron.__dict__)
Examinez bien les phrases Coucou je suis dans [...] et essayez de comprendre pourquoi elles apparaissent. Bien
que nos trois méthodes soient définies comme def masse(), vous pouvez constater qu’elles sont appelées lorsque on
invoque [Link], [Link] = 25 ou del [Link] (à l’intérieur de la classe, ce serait [Link],
[Link] = 25 ou del [Link]). Autrement dit, on n’utilise jamais la syntaxe .masse(). Ceci est justement
dû au fait que .masse est un objet de type property.
Conseil
Lorsque vous souhaitez créer des objets property , nous vous conseillons la syntaxe pythonique @property,@nom_attribut
.setter et @nom_attribut.deleter plutôt que celle de la rubrique précédente avec la ligne masse = property(
fget=get_masse, fset=set_masse, fdel=del_masse). Cette syntaxe améliore grandement la lisibilité.
Une méthode décorée avec @property peut être utile seule sans avoir le setter et/ou le deleter correspondant(s).
On rencontre cela lorsqu’on souhaite créer un « d’attribut dynamique » plutôt qu’avoir un appel de méthode explicite.
Regardons un exemple :
1 class ADN:
2 def __init__(self):
3 [Link] = []
4
5 def __repr__(self):
6 return f"La séquence de mon brin d'ADN est {[Link]}"
7
8 def ajoute_base(self, nom_base):
9 [Link](nom_base)
10
11 @property
12 def len(self):
13 return len([Link])
Lorsqu’on utilise l’attribut brin_adn.len, ceci invoque finalement l’appel de l’objet property len qui, in fine, est
une méthode. Ainsi, la valeur renvoyée sera calculée à chaque fois, bien que dans la syntaxe on n’a pas une notation
.methode(), mais plutôt .attribut. Voilà pourquoi nous avons parlé d’attribut dynamique. Cela permet d’alléger la
syntaxe quand il n’y a pas spécifiquement d’arguments à passer à la méthode qui se trouve derrière cet attribut.
Conseil
Si on souhaite contrôler ce que fait le client de la classe pour certains attributs « délicats » ou « stratégiques », on peut
utiliser la classe property. Toutefois, nous vous conseillons de ne l’utiliser que lorsque cela se révèle vraiment nécessaire,
donc avec parcimonie. Le but étant de ne pas surcharger le code inutilement. Cela va dans le sens des recommandations
des développeurs de Python (comme décrit dans la PEP8).
Attention
En Python, il n’existe pas d’attributs privés comme dans d’autres langages orientés objet. L’utilisateur a accès à tous
les attributs quels qu’ils soient, même s’ils contiennent un ou plusieurs caractère(s) underscore(s) (voir ci-dessous) !
Définition
En Python les attributs non publics sont des attributs dont le nom commence par un ou deux caractère(s) underscore.
Par exemple, _attribut, ou __attribut.
La présence des underscores dans les noms d’attributs est un signe clair que le client ne doit pas y toucher. Toutefois,
cela n’est qu’une convention, et comme dit ci-dessus le client peut tout de même modifier ces attributs.
Par exemple, reprenons la classe Citron de la rubrique précédente dont l’attribut .masse est contrôlé avec un objet
property :
Malgré l’objet property, nous avons pu modifier l’attribut non public ._masse directement !
Il existe également des attributs dont le nom commence par deux caractères underscores. Nous n’avons encore jamais
croisé ce genre d’attribut. Ces derniers mettent en place le name mangling.
Définition
Le name mangling 15 , ou encore substantypage ou déformation de nom en français, correspond à un mécanisme de
changement du nom d’un attribut selon si on est à l’intérieur ou à l’extérieur d’une classe.
Regardons un exemple :
15. [Link]
1 class Citron:
2 def __init__(self):
3 self.__mass = 100
4
5 def get_mass(self):
6 return self.__mass
7
8
9 if __name__ == "__main__":
10 citron1 = Citron()
11 print(citron1.get_mass())
12 print(citron1.__mass)
La ligne 12 du code a donc conduit à une erreur : Python prétend ne pas connaître l’attribut .__mass. On pourrait
croire que cela constitue un mécanisme de protection des attributs. En fait il n’en est rien, car on va voir que l’attribut
est toujours accessible et modifiable. Si on modifiait le programme principal comme suit :
1 if __name__ == "__main__":
2 citron1 = Citron()
3 print(citron1.__dict__)
Ce code affiche 100 puis 200. La ligne 12 a permis d’accéder à l’attribut .__mass de la classe mère Fruit, et la
ligne 13 a permis d’accéder à l’attribut .__mass de la classe Citron.
Le name mangling n’est donc pas un mécanisme de « protection » d’un attribut, il n’a pas été conçu pour ça. Les
concepteurs de Python le disent clairement dans la PEP 8 : « Generally, double leading underscores should be used only
to avoid name conflicts with attributes in classes designed to be subclassed ».
Donc en Python, on peut tout détruire, même les attributs délicats contenant des underscores. Pourquoi Python
permet-il un tel paradoxe ? Selon le concepteur de Python, Guido van Rossum : « We’re all consenting adults here »,
nous sommes ici entre adultes, autrement dit nous savons ce que nous faisons !
Conseil
En résumé, n’essayez pas de mettre des barrières inutiles vers vos attributs. Cela va à l’encontre de la philosophie
Python. Soignez plutôt la documentation et faites confiance aux utilisateurs de votre classe !
Les classes peuvent bien sûr contenir des docstrings comme les fonctions et les modules. C’est d’ailleurs une pratique
vivement recommandée. Voici un exemple sur notre désormais familière classe Citron :
1 class Citron:
2 """Voici la classe Citron.
3
4 Il s'agit d'une classe assez impressionnante qui crée des objets
5 citrons.
6 Par défaut une instance de Citron contient l'attribut de classe
7 saveur.
8 """
9 saveur = "acide"
10
11 def __init__(self, couleur="jaune", taille="standard"):
12 """Constructeur de la classe Citron.
13
14 Ce constructeur prend deux arguments par mot-clé
15 couleur et taille."""
16 [Link] = couleur
17 [Link] = taille
18
19 def __str__(self):
20 """Redéfinit le comportement avec print()."""
21 return f"saveur: {saveur}, couleur: {couleur}, taille: {taille}"
22
23 def affiche_coucou(self):
24 """Méthode inutile qui affiche coucou."""
25 print("Coucou !")
Python formate automatiquement l’aide comme il le fait avec les modules (voir chapitre 15 Création de modules).
Comme nous l’avons dit dans le chapitre 16 Bonnes pratiques en programmation Python, n’oubliez pas que les docstrings
sont destinées aux utilisateurs de votre classe. Elles doivent donc contenir tout ce dont un utilisateur a besoin pour
comprendre ce que fait la classe et comment l’utiliser.
Notez que si on instancie la classe citron1 = Citron() et qu’on invoque l’aide sur l’instance help(citron1), on
obtient la même page d’aide. Comme pour les modules, si on invoque l’aide pour une méthode de la classe
help(citron1.affiche_coucou), on obtient l’aide pour cette méthode seulement.
Toutes les docstrings d’une classe sont en fait stockées dans un attribut spécial nommé instance.__doc__. Cet
attribut est une chaîne de caractères contenant la docstring générale de la classe. Ceci est également vrai pour les
modules, méthodes et fonctions. Si on reprend notre exemple ci-dessus :
1 >>> citron1 = Citron()
2 >>> print(citron1.__doc__)
3 Voici la classe Citron.
4
5 Il s'agit d'une classe assez impressionnante qui crée des objets
6 citrons.
7 Par défaut une instance de Citron contient l'attribut de classe
8 saveur.
9
10 >>> print(citron1.affiche_coucou.__doc__)
11 Méthode inutile qui affiche coucou.
L’attribut .__doc__ est automatiquement créé par Python au moment de la mise en mémoire de la classe (ou module,
méthode, fonction, etc.).
• Nous vous déconseillons de mettre comme paramètre par défaut une liste vide (ou tout autre objet séquentiel
modifiable) :
1 def __init__(self, liste=[]):
2 [Link] = liste
Si vous créez des instances sans passer d’argument lors de l’instanciation, toutes ces instances pointeront vers la
même liste. Cela peut avoir des effets désastreux.
• Ne mettez pas non plus une liste vide (ou tout autre objet séquentiel modifiable) comme attribut de classe.
1 class Citron:
2 liste = []
Ici chaque instance pourra modifier la liste, ce qui n’est pas souhaitable. Souvenez-vous, la modification des attributs
de classe doit se faire par une syntaxe [Link] = valeur (et non pas via les instances).
• Comme abordé dans la rubrique Différence entre les attributs de classe et d’instance, nous vous conseillons de ne
jamais modifier les attributs de classe. Vous pouvez néanmoins les utiliser comme constantes.
• Si vous avez besoin d’attributs modifiables, utilisez des attributs d’instance et initialisez-les dans la méthode .
__init__() (et nulle part ailleurs). Par exemple, si vous avez besoin d’une liste comme attribut, créez la plutôt
dans le constructeur :
1 class Citron:
2 def __init__(self):
3 [Link] = []
Ainsi, vous aurez des listes réellement indépendantes pour chaque instance.
24.7.5 Namedtuples
Imaginons que l’on souhaite stocker des éléments dans un conteneur, que l’on puisse retrouver ces éléments avec une
syntaxe [Link] et que ces éléments soient non modifiables. On a vu ci-dessus, les classes ne sont pas faites
pour cela, il n’est pas conseillé de les utiliser comme des conteneurs inertes, on les conçoit en général afin d’y créer aussi
des méthodes. Dans ce cas, les namedtuples 16 sont faits pour vous ! Ce type de conteneur est issu du module collections
que nous avions évoqué dans le chapitre 14 Conteneurs.
1 >>> import collections
2 >>> Citron = [Link]("Citron", "masse couleur saveur forme")
3 >>> Citron
4 <class '__main__.Citron'>
5 >>> citron = Citron(10, "jaune", "acide", "ellipsoide")
6 >>> citron
7 Citron(masse=10, couleur='jaune', saveur='acide', forme='ellipsoide')
8 >>> [Link]
9 10
10 >>> [Link]
11 'ellipsoide'
Lignes 2 à 4. La fonction namedtuple() renvoie une classe qui sert à créer de nouveaux objets citrons. Attention
cette classe est différente de celles que l’on a rencontrées jusqu’à maintenant, car elle hérite de la classe [Link]
(on peut le voir en faisant help(Citron)). En ligne 2, on passe en argument le nom de la classe souhaitée (i.e. Citron),
puis une chaîne de caractères avec des mots séparés par des espaces qui correspondront aux attributs (on pourrait aussi
passer une liste ["masse", "couleur", "saveur", "forme"]).
Ligne 5. On instancie un nouvel objet citron.
Lignes 6 à 11. On peut retrouver les différents attributs avec une syntaxe [Link].
Mais dans namedtuple, il y a tuple ! Ainsi, l’instance citron hérite de tous les attributs des tuples :
16. [Link]
1 >>> citron[0]
2 10
3 >>> citron[3]
4 'ellipsoide'
5 >>> [Link] = 100
6 Traceback (most recent call last):
7 File "<stdin>", line 1, in <module>
8 AttributeError: can't set attribute
9 >>> for elt in citron:
10 ... print(elt)
11 ...
12 10
13 jaune
14 acide
15 ellipsoide
Lignes 1 et 2. On crée un nouveau namedtuples avec la méthode ._replace(). Notez qu’il faut passer un (ou
plusieurs) argument(s) par mot-clé à cette méthode désignant les attributs à modifier.
Lignes 3 et 4. L’objet initial citron est intact puisqu’un namedtuples est non modifiable.
Lignes 5 à 7. En ré-affectant ce que renvoie la méthode ._replace() dans dans un objet de même nom citron,
on peut faire évoluer son contenu comme on a pu le faire avec les chaînes de caractères.
Enfin, il est possible de convertir un namedtuple en dictionnaire (ordonné) avec la méthode ._asdict() :
1 >>> citron._asdict()
2 OrderedDict([('masse', 10), ('couleur', 'jaune'), ('saveur', 'acide'), ('forme', 'ellipsoide')])
Quand utiliser les namedtuples ? Vous souvenez-vous de la différence entre les listes et les dictionnaires ? Ici, c’est
un peu la même chose entre les tuples et les namedtuples. Les namedtuples permettent de créer un code plus lisible en
remplaçant des numéros d’indice par des noms. Le fait qu’ils soient non modifiables peut aussi avoir un avantage par
rapport à l’intégrité des données. Si vous trouvez les namedtuples limités, sachez que vous pouvez créer votre propre
classe qui hérite d’un namedtuple afin de lui ajouter de nouvelles méthodes « maison ».
17. [Link]
1 >>> int
2 <class 'int'>
3 >>> list
4 <class 'list'>
5 >>> range
6 <class 'range'>
7 >>> property
8 <class 'property'>
Et bien, c’est parce-que ce sont bel et bien des classes ! Donc, lorsqu’on invoque par exemple liste1 = list(), on
crée finalement une instance de la classe list. Python ne met pas list en CamelCase car ce sont des classes natives
(built-in classes). En effet, les auteurs de Python ont décidé que les classes et fonctions natives sont en minuscules, et
les exceptions en CamelCase (voir ce lien 18 ).
Finalement, la création d’une instance à partir d’une classe ou l’appel d’une fonction possède la même syntaxe
mot_clé() :
1 >>> class Citron:
2 ... pass
3 ...
4 >>> Citron()
5 <__main__.Citron object at 0x7fb776308a10>
6 >>> def fct():
7 ... return "Coucou"
8 ...
9 >>> fct()
10 'Coucou'
On peut le voir aussi quand on invoque l’aide sur un de ces outils, par exemple help(int) :
Help on class int in module builtins:
class int(object)
| int([x]) -> integer
| int(x, base=10) -> integer
[...]
Par conséquent, d’un point de vue purement sémantique nous devrions parler de classe plutôt que de fonction pour
les instructions comme list(), range(), etc. Toutefois, nous avons décidé de garder le nom fonction pour ne pas
compliquer les premiers chapitres de ce cours.
24.9 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
On se propose de tester cela sur la molécule simple de benzene. Vous aurons besoin du fichier [Link] 19 pour
réaliser cet exercice.
Après les import nécessaires, le programme contiendra une constante donnant les masses des atomes sous forme de
dictionnaire : ATOM_MASSES = {"C": 12.0, "O": 16.0, "H": 1.0}.
Créer une classe Atom en vous inspirant des exercices du chapitre 23 Avoir la classe avec les objets. Cette classe devra
instancier des objets contenant les attributs d’instance suivants :
• nom d’atome (par exemple C1)
• type d’atome (une seule lettre, déduit du nom d’atome, par exemple C)
• coordonnée x
• coordonnée y
• coordonnée z
Le nom d’atome et coordonnées cartésiennes seront passés au constructeur.
Ajouter les méthodes calc_distance(), calc_com() (center of mass). Ajouter une méthode mute_atom(name
) qui change le nom de l’atome, où name est un nouveau nom d’atome (par exemple O1). Cette méthode changera
également l’attribut d’instance décrivant le type d’atome.
Créer une classe Molecule qui construit les attributs d’instance : - Nom de la molécule - Une liste d’atomes (vide à
l’instanciation) : list_atoms - Une liste indiquant la connectivité (la liste des atomes connectés, vide à l’instanciation) :
list_connectivity
Le constructeur prendra en argument seulement le nom de la molécule.
Créer une méthode add_atom(atom) qui vérifie si l’argument passé est bien une instance de la classe Atom, et qui
ajoute atom dans la liste d’atomes.
Créer une autre méthode build_mlc_from_pdb(filename) qui prend en argument un nom de fichier pdb. La
méthode lit le fichier pdb, et pour chaque atome lu, crée une instance de la classe Atom, et ajouter celle-ci à list_atoms
.
Ajouter une méthode calc_mass() qui calcule et renvoie masse de la molécule.
Créer une méthode calc_com() qui cette fois-ci calcule et renvoie le centre de masse de la molécule entière.
Ajouter la méthode calc_connectivity() qui calcule et renvoie une liste décrivant la connectivité entre les atomes.
Deux atomes sont considérés connectés s’il y a une liaison covalente entre eux, on peut pour cela calculer la distance
entre eux qui doit être inférieure à 1.6 Å. La liste de connectivité pourra être construite dans ce style : [("C1", "H1")
, ("C1", "C2"), ...].
Chaque paire d’atome doit apparaitre une seule fois (pas de [("C1", "H1"), [("H1", "C1"), ...].
Créer une méthode spéciale affichant les caractéristiques de la molécule lorsqu’on utilise print() avec une instance
de cette classe Molecule, par exemple print(benzene). Cette méthode pourra par exemple afficher avant d’avoir créé
la molécule :
Molecule benzene
No atom for the moment
No connectivity for the moment
Ou bien, lorsque la molécule est créée et la connectivité déterminée, elle s’affichera comme ceci :
Molecule benzene
atom C1, type C, mass = 12.0 amu, coor( -2.145, 0.973, -0.003)
atom H1, type H, mass = 1.0 amu, coor( -3.103, 0.460, -0.005)
[...]
Connectivity
C1 connected to H1
C1 connected to C2
[...]
Pour lancer le programme dans un premier temps, vous pourrez instancier une molécule benzene, puis y ajouter les
atomes :
1 if __name__ == "__main__":
2 benzene = Molecule("benzene")
3 print(benzene)
4 benzene.build_mlc_from_pdb("[Link]")
5 print(benzene)
19. [Link]
Dans un deuxième temps, le programme principal calculera la masse et le centre de masse de benzene et les affichera.
Muter ensuite l’atome H1 en O1 et recalculer la masse et le centre de masse et les afficher.
Pour aller plus loin, vous pouvez ajouter une méthode qui calcule et affiche un graphe de la molécule avec le module
networkx 20 . La page de tutorial 21 pourra vous être utile.
Par exemple :
20. [Link]
21. [Link]
Conseil
Dans ce chapitre, nous allons utiliser des classes, nous vous conseillons de bien relire les chapitres 23 Avoir la classe
avec les objets et 24 Avoir plus la classe avec les objets (en ligne). Par ailleurs, nous vous conseillons de relire également
la rubrique Arguments positionnels et arguments par mot-clé du chapitre 10 sur les fonctions.
Les arguments passés à la ligne de commande sont tout à fait classiques dans le monde de la bioinformatique. Toutefois,
il se peut que vous développiez un programme pour une communauté plus large, qui n’a pas forcément l’habitude d’utiliser
un shell et la ligne de commande. Une GUI permettra un usage plus large de votre programme, il est donc intéressant
de regarder comment s’y prendre. Dans notre exemple ci-dessus on pourrait par exemple développer une interface où
l’utilisateur choisirait le nom du fichier d’entrée par l’intermédiaire d’une boîte de dialogue, et de contrôler les options en
cliquant sur des boutons, ou des « listes de choix ». Une telle GUI pourrait ressembler à la figure 25.1.
Au delà de l’aspect convivial pour l’utilisateur, vous pourrez, avec une GUI, construire des fenêtres illustrant des
éléments que votre programme génère à la volée. Ainsi, vous « verrez » ce qui se passe de manière explicite et en direct !
Par exemple, si on réalise une simulation de particules, on a envie de voir un « film » des particules en mouvement, c’est-
à-dire comment ces particules bougent au fur et à mesure que les pas de simulation avancent. Une GUI vous permettra
une telle prouesse ! Enfin, sachez que certains logiciels scientifiques ont été développés avec la bibliothèque graphique Tk
(par exemple pymol, vmd, etc.). Qui sait, peut-être serez-vous le prochain développeur d’un outil incontournable ?
Il existe beaucoup de modules pour construire des applications graphiques. Par exemple : Tkinter 1 , wxpython 2 , PyQt 3 ,
1. [Link]
2. [Link]
3. [Link]
300
25.2. Quelques concepts liés à la programmation graphique Chapitre 25. Fenêtres graphiques et Tkinter
PyGObject 4 , etc. Nous présentons dans ce chapitre le module Tkinter qui est présent de base dans les distributions Python
(pas besoin a priori de faire d’installation de module externe). Tkinter permet de piloter la bibliothèque graphique Tk
(Tool Kit), Tkinter signifiant tk interface. On pourra noter que cette bibliothèque Tk peut être également pilotée par
d’autres langages (Tcl, perl, etc.).
Définition
Les widgets (window gadget) sont des objets graphiques permettant à l’utilisateur d’interagir avec votre programme
Python de manière conviviale. Par exemple, dans la fenêtre sur la figure 25.1, les boutons, les listes de choix, ou encore
la zone de texte sont des widgets.
L’utilisation d’une GUI va amener une nouvelle manière d’aborder le déroulement d’un programme, il s’agit de la
programmation dite « événementielle ». Jusqu’à maintenant vous avez programmé « linéairement », c’est-à-dire que
les instructions du programme principal s’enchaînaient les unes derrière les autres (avec bien sûr de possibles appels à
des fonctions). Avec une GUI, l’exécution est décidée par l’utilisateur en fonction de ses interactions avec les différents
widgets. Comme c’est l’utilisateur qui décide quand et où il clique dans l’interface, il va falloir mettre en place ce qu’on
appelle un « gestionnaire d’événements ».
Définition
Le gestionnaire d’événements est une sorte de « boucle infinie » qui est à l’affût de la moindre action de la part de
l’utilisateur. C’est lui qui effectuera une action lors de l’interaction de l’utilisateur avec chaque widget de la GUI. Ainsi,
l’exécution du programme sera réellement guidée par les actions de l’utilisateur.
La bibliothèque Tk que nous piloterons avec le module Python Tkinter propose tous les éléments cités ci-dessus (fe-
nêtre graphique, widgets, gestionnaire d’événements). Nous aurons cependant besoin d’une dernière notion : les fonctions
callback.
Définition
4. [Link]
Une fonction callback est une fonction passée en argument d’une autre fonction.
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
Jusqu’à maintenant nous avons toujours appelé les fonctions ou les méthodes de cette manière :
1 var = fct(arg1, arg2)
2
3 [Link](arg)
où les arguments étaient des objets « classiques » (par exemple une chaîne de caractères, un entier, un float, etc.).
Sachez qu’il est possible de passer en argument une fonction à une autre fonction ! Par exemple :
1 def fct_callback(arg):
2 print(f"J'aime bien les {arg} !")
3
4
5 def une_fct(ma_callback):
6 print("Je suis au début de une_fct(), "
7 "et je vais exécuter la fonction callback :")
8 ma_callback("fraises")
9 print("une_fct() se termine.")
10
11 if __name__ == "__main__":
12 une_fct(fct_callback)
Vous voyez que dans le programme principal, lors de l’appel de une_fct(), on lui passe comme argument une autre
fonction mais sans aucune parenthèses ni argument, c’est-à-dire fct_callback tout court. En d’autres termes, cela
est différent de
une_fct(fct_callback("scoubidous")).
Dans une telle construction, fct_callback("scoubidous") serait d’abord évaluée, puis ce serait la valeur ren-
voyée par cet appel qui serait passée à une_fct() (n’essayez pas sur notre exemple car cela mènerait à une erreur !).
Que se passe-t-il en filigrane lors de l’appel une_fct(fct_callback) ? Python passe une référence vers la fonction
fct_callback (en réalité il s’agit d’un pointeur, mais tout ceci est géré par Python et est transparent pour l’utilisateur).
Vous souvenez-vous ce qui se passait avec une liste passée en argument à une fonction (voir le chapitre 13 Plus sur les
fonctions) ? C’était la même chose, une référence était envoyée plutôt qu’une copie. Python Tutor 5 nous confirme cela
(cf. figure 25.2).
Lorsqu’on est dans une_fct() on pourra utiliser bien sûr des arguments lors de l’appel de notre fonction callback
si on le souhaite. Notez enfin que dans une_fct() la fonction callback reçue en argument peut avoir un nom différent
(comme pour tout type de variable).
À quoi cela sert-il ? À première vue cette construction peut sembler ardue et inutile. Toutefois, vous verrez que dans
le module Tkinter les fonctions callback sont incontournables. En effet, on utilise cette construction pour lancer une
fonction lors de l’interaction de l’utilisateur avec un widget : par exemple, lorsque l’utilisateur clique sur un bouton et
qu’on souhaite lancer une fonction particulière suite à ce clic. Notez enfin que nous les avons déjà croisées avec :
5. [Link]
• le tri de dictionnaire par valeur avec la syntaxe sorted(dico, key=[Link]) (voir le chapitre 8 Dictionnaires
et tuples) ;
• le tri par longueur de mots avec la syntaxe sorted(liste, key=len) (voir chapitre 12 Plus sur les listes) ;
• les objets property avec la syntaxe property(fget=get_masse, fset=set_masse) (voir le chapitre 24 Avoir
plus la classe avec les objets (en ligne)).
Ligne 2. On crée la fenêtre principale (vous la verrez apparaître !). Pour cela, on crée une instance de la classe [Link]
dans la variable racine. Tous les widgets que l’on créera ensuite seront des fils de cette fenêtre. On pourra d’ailleurs
noter que cette classe [Link] ne s’instancie en général qu’une seule fois par programme. Vous pouvez, par curiosité, lancer
une commande dir(racine) ou help(racine), vous verrez ainsi les très nombreuses méthodes et attributs associés à
un tel objet Tk.
Ligne 3. On crée un label, c’est-à-dire une zone dans la fenêtre principale où on écrit un texte. Pour cela, on a créé
une variable label qui est une instance de la classe [Link]. Cette variable label contient donc notre widget, nous la
réutiliserons plus tard (par exemple pour placer ce widget dans la fenêtre). Notez le premier argument positionnelracine
passé à la classe [Link], celui-ci indique la fenêtre parente où doit être dessinée le label. Cet argument doit toujours
être passé en premier et il est vivement conseillé de le préciser. Nous avons passé un autre argument avec le nom
text pour indiquer, comme vous l’avez deviné, le texte que nous souhaitons voir dans ce label. La classe [Link]
peut recevoir de nombreux autres arguments, en voici la liste exhaustive 6 . Dans les fonctions Tkinter qui construisent
6. [Link]
un widget, les arguments possibles pour la mise en forme de celui-ci sont nombreux, si bien qu’ils sont toujours des
arguments par mot-clé. Si on ne précise pas un de ces arguments lors de la création du widget, l’argument prendra
alors une valeur par défaut. Cette liste des arguments par mot-clé est tellement longue qu’en général on ne les précisera
pas tous. Heureusement, Python autorise l’utilisation des arguments par mot-clé dans un ordre quelconque. Comme nous
l’avons vu dans le chapitre 10 Fonctions, souvenez vous que leur utilisation dans le désordre implique qu’il faudra toujours
préciser leur nom : par exemple vous écrirez text="blabla" et non pas "blabla" tout court.
Ligne 4. De même, on crée un bouton « Quitter » qui provoquera la fermeture de la fenêtre et donc l’arrêt de
l’application si on clique dessus. À nouveau, on passe la fenêtre parente en premier argument, le texte à écrire dans le
bouton, puis la couleur de ce texte. Le dernier argument command=[Link] va indiquer la fonction / méthode
à exécuter lorsque l’utilisateur clique sur le bouton. On pourra noter que l’instance de la fenêtre mère [Link] (que nous
avons nommée racine) possède une méthode .destroy() qui va détruire le widget sur lequel elle s’applique. Comme
on tue la fenêtre principale (que l’on peut considérer comme un widget contenant d’autres widgets), tous les widgets
fils seront détruits et donc l’application s’arrêtera. Vous voyez par ailleurs que cette méthode [Link] est
passée à l’argument command= sans parenthèses ni arguments : il s’agit donc d’une fonction callback comme expliqué
ci-dessus. Dans tous les widgets Tkinter, on doit passer à l’argument command=... une fonction / méthode callback.
La liste exhaustive des arguments possibles de la classe [Link] se trouve ici 7 .
Lignes 6 et 7. Vous avez noté que lors de la création de ce label et de ce bouton, rien ne s’est passé dans la fenêtre.
C’est normal, ces deux widgets existent bien, mais il faut maintenant les placer à l’intérieur de la fenêtre. On appelle pour
ça la méthode .pack(), avec une notation objet [Link]() : à ce moment précis, vous verrez votre label apparaître
ainsi que la fenêtre qui se redimensionne automatiquement en s’adaptant à la grandeur de votre label. L’invocation de la
même méthode pour le bouton va faire apparaître celui-ci juste en dessous du label et redimensionner la fenêtre. Vous
l’aurez compris la méthode .pack() place les widgets les uns en dessous des autres et ajuste la taille de la fenêtre. On
verra plus bas que l’on peut passer des arguments à cette méthode pour placer les widgets différemment (en haut, à
droite, à gauche).
Au final, vous devez obtenir une fenêtre comme sur la figure 25.3.
Vous voyez maintenant la même fenêtre avec les mêmes fonctionnalités par rapport à la version dans l’interpréteur
(voir la figure 25.3). Nous commentons ici les différences (dans le désordre) :
Ligne 6. Le bouton a été créé en ligne 5, mais on voit qu’il est possible de préciser une option de rendu du widget
après cette création (ici on met le texte en rouge avec l’option "fg"). La notation ressemble à celle d’un dictionnaire
avec une syntaxe générale widget["option"] = valeur.
Ligne 9. L’instruction [Link]() va lancer le gestionnaire d’événements que nous avons évoqué ci-dessus.
C’est lui qui interceptera la moindre action de l’utilisateur, et qui lancera les portions de code associées à chacune de ses
actions. Bien sûr, comme nous développerons dans ce qui va suivre toutes nos applications Tkinter dans des scripts (et
non pas dans l’interpréteur), cette ligne sera systématiquement présente. Elle sera souvent à la fin du script, puisque, à
l’image de ce script, on écrit d’abord le code construisant l’interface, et on lance le gestionnaire d’événements une fois
l’interface complètement décrite, ce qui lancera au final l’application.
7. [Link]
Ligne 10. Cette ligne ne s’exécute qu’après l’arrêt de l’application (soit en cliquant sur le bouton « Quitter », soit en
cliquant sur la croix).
Ligne 5. Pour quitter l’application, on utilise ici la méthode .quit(). Celle-ci casse la .mainloop() et arrête ainsi
le gestionnaire d’événements. Cela mène à l’arrêt de l’application. Dans le premier exemple dans l’interpréteur, on avait
utilisé la méthode .destroy() sur la fenêtre principale. Comme son nom l’indique, celle-ci détruit la fenêtre principale
et mène aussi à l’arrêt de l’application. Cette méthode aurait donc également fonctionné ici. Par contre, la méthode
.quit() n’aurait pas fonctionné dans l’interpréteur car, comme on l’a vu, la boucle .mainloop() n’y est pas présente.
Comme nous écrirons systématiquement nos applications Tkinter dans des scripts, et que la boucle .mainloop() y est
obligatoire, vous pourrez utiliser au choix .quit() ou .destroy() pour quitter l’application.
Ligne 3. On crée notre application en tant que classe. Notez que cette classe porte un nom qui commence par
une majuscule (comme recommandé dans les bonnes pratiques de la PEP8 8 , voir le chapitre 16 Bonnes pratiques en
programmation Python). L’argument passé dans les parenthèses indique que notre classe Application hérite de la
classe [Link]. Par ce mécanisme, nous héritons ainsi de toutes les méthodes et attributs de cette classe mère, mais nous
pouvons en outre en ajouter de nouvelles/nouveaux (on parle aussi de « redéfinition » de la classe [Link]) !
Ligne 4. On crée un constructeur, c’est-à-dire une méthode qui sera exécutée lors de l’instanciation de notre classe
(à la ligne 16).
Ligne 5. On appelle ici le constructeur de la classe mère [Link].__init__(). Pourquoi fait-on cela ? On se souvient
dans la version linéaire de l’application, on avait utilisé une instanciation classique : racine = [Link](). Ici, l’effet de
l’appel du constructeur de la classe mère permet d’instancier la fenêtre Tk dans la variable self directement. C’est-à-dire
que la prochaine fois que l’on aura besoin de cette instance (lors de la création des widgets par exemple, cf. lignes 9 et
8. [Link]
10), on utilisera directement self plutôt que racine ou tout autre nom donné à l’instance. Comme vu dans le chapitre
23 Avoir la classe avec les objets, appeler le constructeur de la classe mère est une pratique classique lorsqu’une classe
hérite d’une autre classe.
Ligne 6. On appelle la méthode self.creer_widgets() de notre classe Application. Pour rappel, le self avant
le .creer_widgets() indique qu’il s’agit d’une méthode de notre classe (et non pas d’une fonction classique).
Ligne 8. La méthode .creer_widgets() va créer des widgets dans l’application.
Ligne 9. On crée un label en instanciant la classe [Link](). Notez que le premier argument passé est maintenant
self (au lieu de racine précédemment) indiquant la fenêtre dans laquelle sera construit ce widget.
Ligne 10. De même on crée un widget bouton en instanciant la classe [Link](). Là aussi, l’appel à la méthode
.quit() se fait par [Link] puisque la fenêtre est instanciée dans la variable self. Par ailleurs, on ne met ni
parenthèses ni arguments à [Link] car il s’agit d’une fonction callback (comme dans la rubrique précédente).
Lignes 11 et 12. On place les deux widgets dans la fenêtre avec la méthode .pack().
Ligne 15. Ici on autorise le lancement de notre application Tkinter en ligne de commande (python tk_application.
py), ou bien de réutiliser notre classe en important tk_application.py en tant que module (import tk_application
) (voir le chapitre 15 Création de modules).
Ligne 16. On instancie notre application.
Ligne 17. On donne un titre dans la fenêtre de notre application. Comme on utilise de petits widgets avec la méthode
pack(), il se peut que le titre ne soit pas visible lors du lancement de l’application. Toutefois, si on « étire » la fenêtre
à la souris, le titre le deviendra. On pourra noter que cette méthode .title() est héritée de la classe mère Tk.
Ligne 18. On lance le gestionnaire d’événements.
Au final, vous obtiendrez le même rendu que précédemment (cf. figure 25.3). Alors vous pourrez-vous poser la
question, « pourquoi ai-je besoin de toute cette structure alors que le code précédent semblait plus direct ? ». La réponse
est simple, lorsqu’un projet de GUI grossit, le code devient très vite illisible s’il n’est pas organisé en classe. De plus,
la non-utilisation de classe rend quasi-obligatoire l’utilisation de variables globales, ce qui on l’a vu, est à proscrire
définitivement ! Dans la suite du chapitre, nous verrons quelques exemples qui illustrent cela (cf. la rubrique suivante).
Ligne 4. On voit qu’il faut d’abord créer le widget canvas, comme d’habitude en lui passant l’instance de la fenêtre
principale en tant qu’argument positionnel, puis les options. Notons que nous lui passons comme options la hauteur et la
largeur du canvas. Même s’il s’agit d’arguments par mot-clé, donc optionnels, c’est une bonne pratique de les préciser.
En effet, les valeurs par défaut risqueraient de nous mener à dessiner hors de la zone visible (cela ne génère pas d’erreur
mais n’a guère d’intérêt).
Ligne 6 à 8. Nous dessinons maintenant des objets graphiques à l’intérieur du canevas avec les méthodes .create_oval
() (dessine une ellipse) et .create_line() (dessine une ligne). Les arguments positionnels sont les coordonnées de
9. [Link]
l’ellipse (les deux points englobant l’ellipse, cf. ce lien 10 pour la définition exacte) ou de la ligne. Ensuite, on passe comme
d’habitude des arguments par mot-clé (vous commencez à avoir l’habitude !) pour mettre en forme ces objets graphiques.
Le rendu de l’image est montré dans la figure 25.4 ainsi que le système de coordonnées associé au canvas. Comme
dans la plupart des bibliothèques graphiques, l’origine du repère du canvas (i.e. la coordonnée (0,0)) est en haut à gauche.
Les x vont de gauche à droite, et les y vont de haut en bas.
Figure 25.4 – Exemple 1 de canvas avec le système de coordonnées. Le système de coordonnées est montré en vert et
n’apparaît pas sur la vraie fenêtre Tkinter.
Attention
L’axe des y est inversé par rapport à ce que l’on représente en mathématique. Si on souhaite représenter une fonction
mathématique (ou tout autre objet dans un repère régi par un repère mathématique), il faudra faire un changement de
repère.
Voici un exemple un peu plus conséquent d’utilisation du widget canvas qui est inclus dans une classe. Il s’agit d’une
application dans laquelle il y a une zone de dessin, un bouton dessinant des cercles, un autre des lignes et un dernier
bouton qui quitte l’application (figure 25.5).
10. [Link]
1 import tkinter as tk
2 import random as rd
3
4 class AppliCanevas([Link]):
5 def __init__(self):
6 [Link].__init__(self)
7 [Link] = 500
8 self.creer_widgets()
9
10 def creer_widgets(self):
11 # création canevas
12 [Link] = [Link](self, bg="light gray", height=[Link],
13 width=[Link])
14 [Link](side=[Link])
15 # boutons
16 self.bouton_cercles = [Link](self, text="Cercle !",
17 command=self.dessine_cercles)
18 self.bouton_cercles.pack(side=[Link])
19 self.bouton_lignes = [Link](self, text="Lignes !",
20 command=self.dessine_lignes)
21 self.bouton_lignes.pack()
22 self.bouton_quitter = [Link](self, text="Quitter",
23 command=[Link])
24 self.bouton_quitter.pack(side=[Link])
25
26 def rd_col(self):
27 return [Link](("black", "red", "green", "blue", "yellow", "magenta",
28 "cyan", "white", "purple"))
29
30 def dessine_cercles(self):
31 for i in range(20):
32 x, y = [[Link](1, [Link]) for j in range(2)]
33 diameter = [Link](1, 50)
34 [Link].create_oval(x, y, x+diameter, y+diameter,
35 fill=self.rd_col())
308 36 Cours de Python / Université Paris Cité / UFR Sciences du Vivant
37 def dessine_lignes(self):
38 for i in range(20):
25.6. Le widget canvas Chapitre 25. Fenêtres graphiques et Tkinter
Lignes 4 à 6. Comme montré dans la rubrique Construire une application Tkinter avec une classe, notre classe
AppliCanevas hérite de la classe générale [Link] et la fenêtre Tk se retrouve dans la variable self.
Ligne 7. On crée un attribut de la classe [Link] qui contiendra la taille (hauteur et largeur) du canvas. On
rappelle que cet attribut sera visible dans l’ensemble de la classe puisqu’il est « accroché » à celle-ci par le self.
Ligne 8. On lance la méthode .creer_widgets() (qui est elle aussi « accrochée » à la classe par le self).
Lignes 12 à 14. On crée un widget canvas en instanciant la classe [Link]. On place ensuite le canvas dans la
fenêtre avec la méthode .pack() en lui précisant où le placer avec la variable Tkinter [Link].
Lignes 15 à 24. On crée des widgets boutons et on les place dans la fenêtre. À noter que chacun de ces widgets appelle
une méthode différente, dont deux que nous avons créées dans la classe (.dessine_cercle() et .dessine_lignes()).
Ligne 26 à 28. Cette méthode renvoie une couleur au hasard sous forme de chaîne de caractères.
Lignes 30 à 40. On définit deux méthodes qui vont dessiner des paquets de 20 cercles (cas spécial d’une ellipse) ou 20
lignes aléatoires. Lors de la création de ces cercles et lignes, on ne les récupère pas dans une variable car on ne souhaite
ni les réutiliser ni changer leurs propriétés par la suite. Vous pourrez noter ici l’avantage de programmer avec une classe,
le canvas est directement accessible dans n’importe quelle méthode de la classe avec [Link] (pas besoin de le passer
en argument ou de créer une variable globale).
Dans ce dernier exemple, nous allons illustrer la puissance du widget canvas en vous montrant que l’on peut animer
les objets se trouvant à l’intérieur. Nous allons également découvrir une technique intéressante, à savoir, comment «
intercepter » des clics de souris générés ou des touches pressées par l’utilisateur. L’application consiste en une « baballe
» qui se déplace dans la fenêtre et dont on contrôle les propriétés à la souris (cf. figure 25.6). Vous pouvez télécharger
le script ici 11 .
11. [Link]
Lignes 19 à 23. Les coordonnées de la baballe, ses pas de déplacement, et sa taille sont créés en tant qu’attributs de
notre classe. Ainsi ils seront visibles partout dans la classe.
Lignes 25 à 31. Le canvas est ensuite créé et placé dans la fenêtre, puis on définit notre fameuse baballe. À noter,
les coordonnées self.x et self.y de la baballe représentent en fait son côté « nord-ouest » (en haut à gauche, voir le
point (x0 , y0 ) dans la documentation officielle 12 ).
Lignes 33 à 35. Jusqu’à maintenant, nous avons utilisé des événements provenant de clics sur des boutons. Ici, on va
« intercepter » des événements générés par des clics de souris sur le canvas et les lier à une fonction / méthode (comme
nous l’avions fait pour les clics sur des boutons avec l’option command=...). La méthode pour faire cela est .bind(),
voilà pourquoi on parle de event binding en anglais. Cette méthode prend en argument le type d’événement à capturer
en tant que chaîne de caractères avec un format spécial : par exemple "<Button-1>" correspond à un clic gauche de la
souris (de même "<Button-2>" et "<Button-3>" correspondent aux clics central et droit respectivement). Le deuxième
argument de la méthode .bind() est une méthode / fonction callback à appeler lors de la survenue de l’événement
(comme pour les clics de bouton, vous vous souvenez ? On l’appelle donc sans parenthèses ni arguments). On notera
que tous ces événements sont liés à des clics sur le canvas, mais il est possible de capturer des événements de souris sur
d’autres types de widgets.
Ligne 36. De même, on peut « intercepter » un événement lié à l’appui sur une touche, ici la touche Esc.
Ligne 38. La méthode .move() est appelée, ainsi l’animation démarrera dès l’exécution du constructeur, donc peu
après l’instanciation de notre application (Ligne 86).
Lignes 40 à 58. On définit une méthode .move() qui va gérer le déplacement de la baballe avec des chocs élastiques
sur les parois (et faire en sorte qu’elle ne sorte pas du canvas).
Lignes 55 et 56. On utilise la méthode .coords() de la classe Canvas, qui « met à jour » les coordonnées de
n’importe quel objet dessiné dans le canvas (c’est-à-dire que cela déplace l’objet).
Ligne 58. Ici, on utilise une autre méthode spécifique des objets Tkinter. La méthode .after() rappelle une autre
méthode ou fonction (second argument) après un certain laps de temps (ici 50 ms, passé en premier argument). Ainsi la
méthode .move() se rappelle elle-même, un peu comme une fonction récursive. Toutefois, ce n’est pas une vraie fonction
récursive comme celle vue dans le chapitre 13 (exemple du calcul de factorielle), car Python ne conserve pas l’état de la
fonction lors de l’appel de .after(). C’est comme si on avait un return, tout l’espace mémoire alloué à la méthode
.move() est détruit lorsque Python rencontre la méthode .after(). On obtiendrait un résultat similaire avec la boucle
suivante :
1 import time
2
3 ...
4
5 while True:
6 move()
7 [Link](0.05) # attendre 50 ms
Le temps de 50 ms donne 20 images (ou clichés) par seconde. Si vous diminuez ce temps, vous aurez plus d’images
par secondes et donc un « film » plus fluide.
Ligne 60 à 66. On définit la méthode .boom() de notre classe qui on se souvient est appelée lors d’un événement clic
central sur le canvas. Vous noterez qu’outre le self, cette fonction prend un autre argument que nous avons nommé
ici mclick. Il s’agit d’un objet spécial géré par Tkinter qui va nous donner des informations sur l’événement généré par
l’utilisateur. Dans les lignes 62 et 63, cet objet mclick récupère les coordonnées où le clic a eu lieu grâce aux attributs
mclick.x et mclick.y. Ces coordonnées sont réaffectées à la baballe pour la faire repartir de l’endroit du clic. Nous
créons ensuite un petit texte dans le canevas et affectons des valeurs aléatoires aux variables de déplacement pour faire
repartir la baballe dans une direction aléatoire.
Lignes 68 à 78. On a ici deux méthodes .incr() et .decr() appelées lors d’un clic gauche ou droit. Deux choses
sont à noter : i) l’attribut [Link] est modifié dans les deux fonctions, mais le changement de diamètre de la boule ne
sera effectif dans le canvas que lors de la prochaine exécution de l’instruction [Link]() (dans la méthode
.move()) ; ii) de même que pour la méthode .boom(), ces deux méthodes prennent un argument après le self (lclick
ou rclick) récupérant ainsi des informations sur l’événement de l’utilisateur. Même si on ne s’en sert pas, cet argument
après le self est obligatoire car il est imposé par la méthode .bind().
Lignes 80 à 82. Cette méthode quitte l’application lorsque l’utilisateur fait un clic sur la touche Esc.
12. [Link]
Figure 25.6 – Exemple de canvas animé à deux instants de l’exécution (panneau de gauche : au moment où on effectue
un clic central ; panneau de droite : après avoir effectué plusieurs clics gauches).
Il existe de nombreux autres événements que l’on peut capturer et lier à des méthodes / fonctions callback. Vous
trouverez une liste complète ici 13 .
13. [Link]
14. [Link]
page Universal widget methods 15 vous donnera une vue d’ensemble des différentes méthodes associées à chaque widget.
Il existe également une extension de Tkinter nommée ttk, réimplémentant la plupart des widgets de base de Tkinter
et qui en propose de nouveaux (Combobox, Notebook, Progressbar, Separator, Sizegrip et Treeview). Typiquement, si
vous utilisez ttk, nous vous conseillons d’utiliser les widgets ttk en priorité, et pour ceux qui n’existent pas dans ttk, ceux
de Tkinter (comme Canvas qui n’existe que dans Tkinter). Vous pouvez importer le sous-module ttk de cette manière :
import [Link] as ttk.
Vous pourrez alors utiliser les classes de widget de ttk (par exemple [Link], etc.). Si vous souhaitez importer
ttk et Tkinter, il suffit d’utiliser ces deux lignes :
1 import tkinter as tk
2 import [Link] as ttk
Ainsi vous pourrez utiliser des widgets de Tkinter et de ttk en même temps.
Pour plus d’informations, vous pouvez consulter la documentation officielle de Python 16 , ainsi que la documentation
très complète du site du MNT 17 .
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
Lorsque vous souhaitez mettre un jour un widget avec une certaine valeur (par exemple le texte d’un label), vous ne
pouvez pas utiliser une variable Python ordinaire, il faudra utiliser une variable Tkinter dite de contrôle. Par exemple,
si on souhaitait afficher les coordonnées de notre baballe (cf. rubrique précédente) dans un label, et que cet affichage
se mette à jour au fur et à mesure des mouvements de la baballe, il faudrait utiliser des variables de contrôle. On peut
créer de telles variables avec les classes [Link] pour les chaînes de caractères, [Link] pour les floats, et
[Link] pour les entiers. Une fois créée, par exemple avec l’instruction var = [Link](), on peut modifier
la valeur d’une variable de contrôle avec la méthode [Link](nouvelle_valeur) : ceci mettra à jour tous les widgets
utilisant cette variable var. Il existe aussi la méthode [Link]() qui récupère la valeur actuelle contenue dans var.
Enfin, il faudra lors de la création du label utiliser l’option textvariable= avec votre variable de contrôle (par exemple
[Link](..., textvariable=var, ...)) pour que cela soit fonctionnel.
À nouveau, vous trouverez une documentation précise sur le site du MNT 18 .
Dans les exemples montrés dans ce chapitre, nous avons systématiquement utiliser la méthode .pack() pour placer
les widgets. Cette méthode très simple et directe « empaquette » les widgets les uns contre les autres et redimensionne
la fenêtre automatiquement. Avec l’option side= et les variables [Link], [Link], [Link] et [Link] on place
facilement les widgets les uns par rapport aux autres. Toutefois, la méthode .pack() peut parfois présenter des limites,
il existe alors deux autres alternatives.
La méthode .grid() permet, grâce à l’utilisation d’une grille, un placement mieux contrôlé des différents widgets. La
méthode .place() place enfin les widgets en utilisant les coordonnées de la fenêtre principale. Nous ne développerons
pas plus ces méthodes, mais voici de la documentation supplémentaire en accès libre :
• .pack() 19 ;
15. [Link]
16. [Link]
17. [Link]
18. [Link]
19. [Link]
• .grid() 20 , 21 ;
• .place() 22 .
Lignes 17 à 21. Commentons d’abord le programme principal : ici on crée la fenêtre principale dans l’instance racine
puis on instancie notre classe en passant racine en argument.
Lignes 4 et 5. Ici réside la principale différence par rapport à ce que nous vous avons montré dans ce chapitre : en
ligne 4 on passe l’argument racine à notre constructeur, puis en ligne 5 on passe ce même argument racine lors de
l’appel du constructeur de la classe [Link] (ce qui était inutile lorsqu’on héritait de la classe Tk).
Ligne 6. L’argument racine passé à la méthode .__init__() est finalement une variable locale. Comme il s’agit
de l’instance de notre fenêtre principale à passer à tous nos widgets, il faut qu’elle soit visible dans toute la classe. La
variable [Link] est ainsi créée afin d’être réutilisée dans d’autres méthodes.
Vous pourrez vous posez la question : « Pourquoi en ligne 4 l’argument par mot-clé racine=None prend la valeur
None par défaut ? ». Et bien, c’est parce que notre classe Application peut s’appeler sans passer d’instance de fenêtre
Tk. Voici un exemple avec les lignes qui changent seulement (tout le reste est identique au code précédent) :
1 [...]
2 class Application([Link]):
3 def __init__(self, racine=None):
4 [Link].__init__(self)
5 [Link] = racine
6 [...]
7 [...]
8 if __name__ == "__main__":
9 app = Application()
10 [Link]()
Dans un tel cas, l’argument racine prend la valeur par défaut None lorsque la méthode .__init__() de notre
classe est exécutée. L’appel au constructeur de la classe Frame en ligne 4 instancie automatiquement une fenêtre Tk (car
cela est strictement obligatoire). Dans la suite du programme, cette instance de la fenêtre principale sera [Link]
et il n’y aura pas de changement par rapport à la version précédente. Cette méthode reste toutefois peu intuitive car
cette instance de la fenêtre principale [Link] vaut finalement None !
20. [Link]
21. [Link]
22. [Link]
Hériter de la classe Frame ou de la classe Tk sont deux manières tout à fait valides pour créer des applications
Tkinter. Le choix de l’une ou de l’autre relève plus de préférences que l’on acquiert en pratiquant, voire de convictions
philosophiques sur la manière de programmer. Toutefois, nous pensons qu’hériter de la classe [Link] est une manière
plus générale et plus compacte : tout ce qui concerne le fenêtrage Tkinter se situera dans votre classe Application, et le
programme principal n’aura qu’à instancier l’application et à lancer le gestionnaire d’événements (les choses seront ainsi
mieux « partitionnées »). C’est donc la méthode que nous vous recommandons.
Si vous allez chercher de la documentation supplémentaire sur Tkinter, il se peut que vous tombiez sur ce style de
syntaxe lorsque vous créez votre classe contenant l’application graphique :
1 class MonApplication([Link]):
2 def __init__(self, *args, **kwargs):
3 [Link].__init__(self, *args, **kwargs)
4 # ici débute la construction de votre appli
5 [...]
6
7 # programme principal
8 if __name__ == "__main__":
9 [...]
10 app = MonApplication()
11 [...]
Les arguments *args et **kwargs récupérent facilement tous les arguments « positionnels » et « par mot-clé ». Pour
plus de détails sur comment *args et **kwargs fonctionnent, reportez-vous au chapitre 26 Remarques complémentaires
(en ligne).
Dans l’exemple ci-dessus, *args et **kwargs sont inutiles car lors de l’instanciation de notre application, on ne passe
aucun argument : app = MonApplication(). Toutefois, on pourrait être intéressé à récupérer des arguments passés
au constructeur, par exemple :
app = MonApplication(arg1, arg2, option1=val1, option2=val2)
Ainsi certains auteurs laissent toujours ces *args et **kwargs au cas où on en ait besoin dans le futur. Cela est
bien utile lorsqu’on distribue notre classe à la communauté et que l’on souhaite que les futurs utilisateurs puissent passer
des arguments Tkinter au constructeur de notre classe.
Toutefois, même si cela « ne coûte rien », nous vous recommandons de ne pas mettre ces *args et **kwargs si vous
n’en avez pas besoin, comme nous vous l’avons montré dans les exemples de ce chapitre. Rappelons nous de la PEP 20
(voir le chapitre 16 Bonnes Pratiques en programmation Python), les assertions « Simple is better than complex » ou «
Sparse is better than dense » nous suggèrent qu’il est inutile d’ajouter des choses dont on ne se sert pas.
Tkinter est parfois surprenant. Dans le code suivant, on pourrait penser que celui-ci n’est pas fonctionnel :
1 >>> import tkinter as tk
2 >>> bouton = [Link](text="Quitter")
3 >>> [Link]()
Pour autant, cela fonctionne et on voit un bouton apparaître ! En fait, Tkinter va automatiquement instancier la
fenêtre principale, si bien qu’il n’est pas obligatoire de passer cette instance en argument d’un widget. À ce moment, on
peut se demander où est passé cette instance. Heureusement, Tkinter garde toujours une filiation des widgets avec les
attributs .master et .children :
1 >>> racine = [Link]
2 >>> racine
3 <[Link] object .>
4 >>> [Link]
5 {'!button': <[Link] object .!button>}
6 >>> bouton["command"] = [Link]
Conseil
Même si cela est possible, nous vous conseillons de systématiquement préciser l’instance de la fenêtre principale lors
de la création de vos widgets.
Comme vu dans nos exemples ci-dessus, les fonctions callback ne prennent pas d’arguments ce qui peut se révéler
parfois limitant. Il existe toutefois une astuce qui utilise les fonctions lambda ; nous expliquons brièvement les fonctions
lambda dans le chapitre 26 Remarques complémentaires (en ligne). Toutefois, nous ne développons pas leur utilisation
avec Tkinter et les fonctions callback car cela dépasse le cadre de cet ouvrage. Pour de plus amples explications sur cette
question, vous pouvez consulter le site pythonprogramming 23 et le livre de Gérard Swinnen 24 .
Dans ce chapitre d’introduction, nous vous avons montré des GUI simples avec une seule page. Toutefois, si votre
projet se complexifie, il se peut que vous ayez besoin de créer plusieurs fenêtre différentes. Le livre de Gérard Swinnen 25
et le site pythonprogramming 26 sont des bonnes sources pour commencer et voir concrètement comment faire cela.
25.8 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell. Nous vous recommandons de concevoir une classe
pour chaque exercice.
25.8.2 Horloge
Sur la base de l’application précédente, faites une application qui affiche l’heure dans un label en se mettant à jour
sur l’heure de l’ordinateur une fois par seconde. Vous concevrez une méthode .mise_a_jour_heure() qui met à jour
l’heure dans le label et qui se rappelle elle-même toutes les secondes (n’oubliez pas la méthode .after(), cf. rubrique
Un canvas animé dans une classe ci-dessus). Pour cette mise à jour, vous pourrez utiliser la méthode .configure(), par
exemple : [Link](text=heure) où heure est une chaîne de caractères représentant l’heure actuelle.
35. [Link]
36. [Link]
Le rendu final attendu est montré dans la figure 25.8. On utilisera un canevas de 400x400 pixels. Il y a aura un
bouton « Quitter » et un bouton « Launch ! » qui calculera et affichera 10000 points supplémentaires dans le triangle de
Sierpinski.
Améliorer l’application précédente en proposant une liste de choix supplémentaire demandant à l’utilisateur de choisir
le nombre de sommets (de 3 à 10). Le programme calculera automatiquement la position des sommets. Pour prendre en
main le widget Listbox, voici un code minimal qui pourra vous aider. Celui-ci contient une Listbox et permet d’afficher
dans le terminal l’élément sélectionné. Nous vous conseillons de bien étudier le code ci-dessous et d’avoir résolu l’exercice
précédent avant de vous lancer !
1 import tkinter as tk
2
3 class MaListBox([Link]):
4 def __init__(self):
5 # Instanciation fenêtre Tk.
6 [Link].__init__(self)
7 [Link] = [Link](self, height=10, width=4)
8 [Link]()
9 # Ajout des items à la listbox (entiers).
10 for i in range(1, 10+1):
11 # Utilisation de ma méthode .insert(index, element)
12 # Ajout de l'entier i ([Link] signifie en dernier).
13 [Link]([Link], i)
14 # Selection du premier élément de listbox.
15 [Link].select_set(0)
16 # Liaison d'une méthode quand clic sur listbox.
17 [Link]("<<ListboxSelect>>", self.clic_listbox)
18
19 def clic_listbox(self, event):
20 # Récupération du widget à partir de l'objet event.
21 widget = [Link]
22 # Récupération du choix sélectionné dans la listbox (tuple).
23 # Par exemple renvoie `(5,)` si on a cliqué sur `5`.
24 selection = [Link]()
25 # Récupération du nombre sélectionné (déjà un entier).
26 choix_select = [Link](selection[0])
27 # Affichage.
28 print(f"Le choix sélectionné est {choix_select}, "
29 f"son type est {type(choix_select)}")
30
31
32
33 if __name__ == "__main__":
34 app = MaListBox()
35 [Link]("MaListBox")
36 [Link]()
Remarques complémentaires
Dans ce chapitre, nous présentons un certain nombre de points en vrac qui ne rentraient pas forcément dans les
autres chapitres ou qui étaient trop avancés au moment où les chapitres étaient abordés. Outre quelques points mineurs,
nous abordons les grandes différences entre Python 2 et Python 3, les anciennes méthodes de formatage des chaînes
de caractères, les fonctions lambda, les itérateurs, la gestion des exceptions, les passage d’arguments avancés dans les
fonctions et les décorateurs. Certains de ces points sont réellement avancés et nécessiteront d’avoir assimilé d’autres
notions avant de les aborder.
Par contre, en Python 3, print() est une fonction. Ainsi, si vous n’utilisez pas de parenthèse, Python vous renverra
une erreur :
1 >>> print 12
2 File "<stdin>", line 1
3 print 12
4 ^
5 SyntaxError: Missing parentheses in call to 'print'
320
26.1. Différences Python 2 et Python 3 Chapitre 26. Remarques complémentaires
1 >>> 3 / 4
2 0.75
Faites très attention à cet aspect si vous programmez encore en Python 2, c’est une source d’erreur récurrente.
Comme on a vu au chapitre 5 Boucles et comparaisons, ces objets sont itérables produisant successivement les nombres
0, puis 1 puis 2 sur notre exemple :
1 >>> for i in range(3):
2 ... print(i)
3 ...
4 0
5 1
6 2
La création de liste avec range() était pratique, mais très peu efficace en mémoire lorsque l’argument donné à
range() était un grand nombre.
D’ailleurs la fonction xrange() est disponible en Python 2 pour faire la même chose que la fonction range() en
Python 3. Attention, ne vous mélangez pas les pinceaux !
1 >>> range(3)
2 [0, 1, 2]
3 >>> xrange(3)
4 xrange(3)
Remarque
Pour générer une liste d’entiers avec la fonction range() en Python 3, vous avez vu dans le chapitre 4 Listes qu’il
suffisait de l’associer avec la fonction list(). Par exemple :
1 >>> list(range(10))
2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Conseil
Pour une comparaison exhaustive entre xrange() en Python 2 et range() en Python 3, vous pouvez lire ce très
bon article 1 tiré du blog de Trey Hunner.
1 >>> # Python3.
2 >>> zip(range(4), range(10, 14))
3 <zip object at 0x7f11423ffd80>
Vous pouvez lire la rubrique Itérables, itérateurs, générateurs et module itertools un peu plus bas dans ce chapitre
pour en savoir plus sur les itérateurs.
Pour éviter ce genre de désagrément, ajoutez la ligne suivante en tout début de votre script :
1 # coding: utf-8
Si vous utilisez un shebang (voir rubrique précédente), il faudra mettre la ligne # coding: utf-8 sur la deuxième
ligne (la position est importante 2 ) de votre script :
1 #! /usr/bin/env python
2 # coding: utf-8
Remarque
L’encodage utf-8 peut aussi être déclaré de cette manière :
1 # -*- coding: utf-8 -*-
La deuxième manière avec la méthode .format() est encore utilisée et reste tout à fait valide. Elle est clairement
plus puissante et évite un certain nombre de désagréments par rapport à l’opérateur %. Vous la croiserez sans doute
de temps en temps dans des programmes et ouvrages plus ou moins récents. Heureusement elle a un fonctionnement
relativement proche des f-strings, donc vous ne serez pas totalement perdus !
Enfin, nous indiquons à la fin de cette rubrique nos conseils sur quelle méthode utiliser.
26.2.1 L’opérateur %
On a vu avec les entiers que l’opérateur % ou modulo renvoyait le reste d’une division entière. Cet opérateur existe
aussi pour les chaînes de caractères mais il met en place l’écriture formatée. En voici un exemple :
1 >>> x = 32
2 >>> nom = "John"
3 >>> print("%s a %d ans" % (nom, x))
4 John a 32 ans
5 >>> nb_G = 4500
6 >>> nb_C = 2575
7 >>> prop_GC = (nb_G + nb_C)/14800
8 >>> print("On a %d G et %d C -> prop GC = %.2f" % (nb_G, nb_C, prop_GC))
9 On a 4500 G et 2575 C -> prop GC = 0.48
La syntaxe est légèrement différente. Le symbole % est d’abord appelé dans la chaîne de caractères (dans l’exemple
ci-dessus %d, %d et %.2f) pour :
• Désigner l’endroit où sera placée la variable dans la chaîne de caractères.
• Préciser le type de variable à formater, d pour un entier (i fonctionne également) ou f pour un float.
• Éventuellement pour indiquer le format voulu. Ici .2 signifie une précision de deux décimales.
Le signe % est rappelé une seconde fois (% (nb_G, nb_C, prop_GC)) pour indiquer les variables à formater.
• Dans la chaîne de caractères, les accolades vides {} précisent l’endroit où le contenu de la variable doit être inséré.
• Juste après la chaîne de caractères, l’instruction .format(nom, x) fournit la liste des variables à insérer, d’abord
la variable nom puis la variable x.
• On peut éventuellement préciser le formatage en mettant un caractère deux-points : puis par exemple ici .2f qui
signifie deux chiffres après la virgule.
• La méthode .format() agit sur la chaîne de caractères à laquelle elle est attachée par le point.
Tout ce que nous avons vu avec les f-strings sur la manière de formater l’affichage d’une variable (après les :
au sein des accolades) est identique avec la méthode .format(). Par exemple {:.2f}, {:0>6d}, {:.6e}, etc.,
fonctionneront de la même manière. La différence notable est qu’on ne met pas directement le nom de la variable au
sein des accolades. Comme pour l’opérateur %, c’est l’emplacement dans les arguments passés à la méthode .format()
qui dicte quelle variable doit être remplacée. Par exemple, dans "{} {} {}".format(bidule, machin, truc), les
premières accolades remplaceront la variable bidule, les deuxièmes la variable machin, les troisièmes la variable truc.
Le formatage avec la méthode .format() se rapproche de la syntaxe des f-strings (accolades, deux-points), mais
présente l’inconvénient – comme avec l’opérateur % – de devoir mettre la liste des variables tout à la fin, alourdissant
ainsi la syntaxe. En effet, dans l’exemple avec la proportion de GC, la ligne équivalente avec une f-string apparait tout
de même plus simple à lire :
1 >>> print(f"On a {nb_G} G et {nb_C} C -> prop GC = {prop_GC:.2f}")
2 On a 4500 G et 2575 C -> prop GC = 0.48
Conseil
Pour conclure, ces deux anciennes façons de formater une chaîne de caractères avec l’opérateur % ou la méthode
.format() vous sont présentées à titre d’information. La première avec l’opérateur % est clairement déconseillée. La
deuxième avec la méthode .format() est encore tout à fait valable. Si vous débutez Python, nous vous conseillons
fortement d’apprendre et d’utiliser les f-strings. C’est ce que vous rencontrerez dans la suite de ce cours. Si vous connaissez
déjà Python et que vous utilisez la méthode .format(), nous vous conseillons de passer aux f-strings. Depuis que nous
les avons découvertes, aucun retour n’est envisageable pour nous tant elles sont puissantes et plus claires à utiliser !
Définition
Une fonction lambda est une fonction qui s’écrit sur une ligne. En Python, il s’agit du moyen d’implémenter une
fonction anonyme 6 (en anglais anonymous function), c’est-à-dire, une fonction qui est la plupart du temps non reliée
à un nom (d’où le terme anonyme). Une fonction lambda s’utilise en général à la volée. On parle aussi d’expressions
lambda utilisées pour fabriquer des fonctions lambda.
• Ligne 1. On a ici une expression lambda typique définissant une fonction lambda. La syntaxe est (dans l’ordre) :
le mot-clé (statement) lambda, zero ou un ou plusieurs argument(s), deux-points, une expression utilisant ou pas
les arguments.
• Ligne 2. Python confirme qu’il s’agit d’une fonction.
• Lignes 3 à 6. Pour utiliser la fonction lambda, pour l’instant, on la met entre parenthèses et on utilise un autre
jeu de parenthèses pour l’appeler et éventuellement passer des arguments.
Attention
Une fonction lambda ne s’écrit que sur une ligne. Si vous essayez de l’écrire sur plusieurs lignes, Python lèvera une
exception SyntaxError: invalid syntax.
Comme pour les fonctions classiques, le nombre d’arguments est variable et doit être cohérent avec l’appel :
4. [Link]
5. [Link]
6. [Link]
Dans les deux cas l’appel est identique, mais la fonction lambda requière une syntaxe à une ligne lors de sa définition.
Même si on peut le faire, les dévelopeurs déconseillent toutefois d’assigner une fonction lambda à un nom dans
la PEP8 7 . Une des raisons est que si une erreur est générée, l’interpréteur ne renvoie pas le numéro de ligne dans la
Traceback :
1 >>> inverse = lambda: 1/0
2 >>> inverse()
3 Traceback (most recent call last):
4 File "<stdin>", line 1, in <module>
5 File "<stdin>", line 1, in <lambda>
6 ZeroDivisionError: division by zero
Ligne 5. L’indication de la ligne pour l’erreur dans la fonction lambda (line 1) correspond à celle de l’appel et non
pas de la définition.
Alors qu’avec une fonction classique :
>>> def inverse():
... return 1/0
...
>>> inverse()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in inverse
ZeroDivisionError: division by zero
Ligne 5. Cette fois-ci, la Traceback indique bien la bonne ligne (line 2) dans la fonction.
7. [Link]
Conseil
Pour cette raison, n’assignez pas une fonction lambda à un nom, mais utilisez la seulement à la volée (voir ci-dessous).
Une autre raison est que cela peut nuire à la lisibilité. Si une fonction lambda s’écrit en une ligne, c’est bien pour qu’on
puisse la lire quand elle est utilisée.
Lignes 2 et 4. La fonction lambda permet de lire clairement quelle opération on réalise plutôt que de se référer à
une fonction classique se trouvant à un autre endroit. Ainsi, cela améliore la lisibilité.
Cela vous rappelle peut-être ce qu’on a rencontré avec les objets NumPy et les opérations vectorielles :
1 >>> import numpy as np
2 >>> array1 = [Link](10)
3 >>> array1
4 array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
5 >>> array1 * 2
6 array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
7 >>>
8 >>> liste1 = list(range(10))
9 >>> liste1
10 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
11 >>> list(map(lambda x: x*2, liste1))
12 [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Ligne 5. Nativement, l’opération array1 * 2 se fait vectoriellement (élément par élément) avec un array NumPy.
Ligne 11. La fonction map() applique l’opération * 2 de la lambda sur tous les éléments de la liste. Ainsi, on obtient
le même effet que sur l’array NumPy.
Bien que cela s’avère pratique, nous verrons dans la rubrique suivante sur les itérateurs qu’il existe une syntaxe plus
Pythonique avec les listes de compréhensions et les expressions génératrices.
La deuxième grande utilité des fonctions lambda concerne leur utilisation pour faire des tris puissants. Dans le chapitre
14 Conteneurs, nous avions vu les tris de dictionnaires par valeurs :
1 >>> dico = {"a": 15, "b": 5, "c":20}
2 >>> sorted(dico, key=[Link])
3 ['b', 'a', 'c']
8. [Link]
Ligne 2. On passe à l’argument par mot-clé key la callback [Link] (cette méthode renvoie initialement les valeurs
d’un dictionnaire). Cela permet finalement de trier par ce que renvoie cette méthode, à savoir les valeurs.
Cet argument par mot-clé peut prendre d’autres callback, par exemple len. Dans l’exemple suivant, on prend 10 mots
au hasard dans le dictionnaire et on les trie par leur longueur :
1 >>> mots = ['étudier', 'pie-grièche', 'figurerait', 'retraitait', 'allégerais',
2 'distribuent', 'affilierait', 'ramassa', 'galettes', 'connu']
3 >>> sorted(mots, key=len)
4 ['connu', 'étudier', 'ramassa', 'galettes', 'figurerait', 'retraitait',
5 'allégerais', 'pie-grièche', 'distribuent', 'affilierait']
Bien sûr, on peut utiliser aussi une fonction lambda. Celle-ci va nous permettre de passer une fonction de tri à la
volée au moment de l’appel de la fonction sorted(). Par exemple, si on reprend le même exemple que le dictionnaire
mais sous forme d’une liste de tuples :
1 liste1 = [('a', 15), ('b', 5), ('c', 20)]
Comment trier en fonction du deuxième élément de chaque tuple ? Réponse, avec une fonction lambda bien sûr !
Regardez :
1 >>> sorted(liste1, key=lambda x: x[1])
2 [('b', 5), ('a', 15), ('c', 20)]
Autre exemple, on souhaite trier une liste d’entiers aléatoires non pas par leur valeur, mais par le résultat de la fonction
x**2 :
1 >>> liste1 = [-5, 2, 5, 8, 6, 3, -9, 4, -10, 2]
2 >>> sorted(liste1, key=lambda x: x**2)
3 [2, 2, 3, 4, -5, 5, 6, 8, -9, -10]
Pour comprendre comment le tri est opéré en ligne 3, voici la liste initiale et une autre liste avec les carrés :
1 >>> liste1
2 [-5, 2, 5, 8, 6, 3, -9, 4, -10, 2]
3 >>> [x**2 for x in liste1]
4 [25, 4, 25, 64, 36, 9, 81, 16, 100, 4]
Le tri de liste1 ci-dessus est bien effectué en fonction des valeurs montrées en ligne 4.
L’agument par mot-clé key existe dans d’autres fonctions ou méthodes. Bien sûr il existe dans la méthode .sort()
qui trie les listes sur place. Mais aussi, dans les fonctions natives min() et max(). Enfin, on le croise dans la fonction
groupby() du module itertools (voir rubrique suivante). Dans tous ces cas, on peut utiliser une fonction lambda pour
l’argument key.
Par exemple, dans le code suivant :
1 >>> liste = ['baccalauréat', 'abaissera', 'barricadé', 'zouave', 'tabac',
2 'typographie', 'dactylographes', 'éclipse']
3 >>> min(liste)
4 'abaissera'
5 >>> max(liste)
6 'éclipse'
7 >>> min(liste, key=lambda x: [Link]("a"))
8 'éclipse'
9 >>> max(liste, key=lambda x: [Link]("a"))
10 'baccalauréat'
Vous l’aurez sans doute compris, avec notre fonction lambda, nous avons trié en fonction du nombre de lettres a dans
chaque mot !
26.3.4 Conclusion
Nous avons vu que les fonctions lambda permettaient des définitions de fonction rapidement sur une ligne. Il faut
absolument éviter de les assigner à un nom. Elles ont toute leur utilité lorsqu’on les utilise avec map() pour appliquer
une opération à tous les éléments d’un conteneur, ou pour des tris puissants avec sorted().
Définition
Un itérateur est un objet Python qui permet d’itérer sur une suite de valeurs avec la fonction next() jusqu’à temps
qu’elles soient épuisées. Si on itère sur une partie des valeurs seulement, l’itérateur garde en mémoire là où il s’est arrêté.
Si on le resollicite avec un next() il repartira de l’élément suivant. Une règle est toutefois importante : les valeurs ne
peuvent être parcourues qu’une seule fois.
On peut générer un itérateur avec la fonction iter() à partir de n’importe quel conteneur :
1 >>> animaux = ["chien", "chat", "souris"]
2 >>> iterateur = iter(animaux)
3 >>> iterateur
4 <list_iterator object at 0x7f917e907a30>
Une fois l’itérateur généré, on peut accéder à l’élément suivant avec la fonction next() :
1 >>> next(iterateur)
2 'chien'
3 >>> next(iterateur)
4 'chat'
5 >>> next(iterateur)
6 'souris'
7 >>> next(iterateur)
8 Traceback (most recent call last):
9 File "<stdin>", line 1, in <module>
10 StopIteration
Quand il n’y a plus de valeurs sur lesquelles itérer, la fonction next() lève une exception StopIteration. En général,
on n’utilisera pas les itérateurs de cette manière, mais plutôt avec une boucle for ce qui évitera cette levée d’exception :
9. [Link]
10. [Link]
11. [Link]
12. [Link]
On peut transformer un objet de type itérateur en un objet de type séquentiel, par exemple en tuple :
1 >>> iterateur = iter(animaux)
2 >>> tuple(iterateur)
3 ('chien', 'chat', 'souris')
Le point important est qu’une fois toutes les valeurs parcourues, l’itérateur est épuisé et ne renvoie plus rien :
1 >>> iterateur = iter(animaux)
2 >>> tuple(iterateur)
3 ('chien', 'chat', 'souris')
4 >>> tuple(iterateur)
5 ()
Ainsi, on ne pourra parcourir l’ensemble des valeurs d’un itérateur qu’une fois.
À ce stade, on pourrait se dire que la construction d’un itérateur à partir d’une liste ci-dessus est inutile puisqu’on
peut itérer directement sur la liste avec une boucle for. Toutefois, lorsqu’on réalise une telle boucle, il y a un itérateur
qui est généré implicitement même si on ne s’en rend pas compte. Pour prouver cela, essayons la fonction next() avec
une liste :
1 >>> next(animaux)
2 Traceback (most recent call last):
3 File "<stdin>", line 1, in <module>
4 TypeError: 'list' object is not an iterator
Ceci n’est pas possible car une liste n’est pas un itérateur. Alors pourquoi peut-on itérer dessus avec une boucle for ?
Et bien, c’est parce que l’objet de type liste possède une méthode dunder spéciale nommée .__iter__(). Celle-ci génère
un itérateur à partir d’elle-même permettant d’itérer sur ses éléments. L’objet itérateur ainsi généré possèdera une autre
méthode dunder spéciale .__next__() permettant de passer à l’élément suivant lorsqu’on itère dessus.
Remarque
Pour rappel, les méthodes dunder des classes ont été définies dans la rubrique 24.2.2 Méthodes magiques ou dunder
methods du chapitre 24 Avoir plus la classe avec les objets.
Lorsque vous construirez votre propre objet itérable, il faudra écrire une classe contenant ces deux méthodes dunder
et l’objet sera de facto un itérateur et itérable. Pour vous donnez une première idée, voici une classe minimale créant un
objet itérateur sur les lettres de l’alphabet :
1 class Alphabet:
2 def __init__(self):
3 [Link] = 97 # ASCII code for a.
4
5 def __iter__(self):
6 return self
7
8 def __next__(self):
9 if [Link] > 122: # ASCII code for z.
10 raise StopIteration
11 letter = chr([Link])
12 [Link] += 1
13 return letter
• Ligne 3. Dans le constructeur, on crée un attribut d’instance [Link] qui gardera l’état de l’itérateur. On
l’initialise à 97 correspondant au code ASCII de la lettre a.
• Lignes 5 et 6. La méthode dunder .__iter__() est très simple à écrire. Elle renvoie self correspondant à
l’itérateur lui-même. Si cette méthode n’est pas présente, l’objet n’est pas itérable.
• Lignes 8 à 13. La méthode dunder .__next__() s’occupe de passer à l’élément suivant et de garder une mémoire
de là où l’itérateur est arrivé. Cela se passe en quatre étapes : i) levée d’une exception StopIteration si on est
arrivé au bout, ii) détermination de la lettre actuelle, iii) incrémenter le [Link] de 1 pour l’itération suivante
et iv) retourner la lettre actuelle.
Si on sauve cette classe dans un fichier [Link], voici comment on pourrait l’utiliser :
À nouveau, une fois l’itérateur épuisé, il ne renvoie plus rien. Bien sûr, cela représente un exemple très simple et la
plupart du temps on créera ses propres classes itérateurs en implémentant de nombreuses fonctionnalités et méthodes sup-
plémentaires. Pour créer un itérateur basique comme celui-ci sur l’alphabet, il est plus commode d’utiliser les générateurs
(voir rubrique Générateurs ci-dessous).
Dans les chapitres précédents, nous avons déjà croisé des itérateurs sans le savoir, car nous ne vous l’avons pas
toujours précisé explicitement ! Dans le chapitre 5 Boucles avec la fonction enumerate(), dans le chapitre 11 Plus sur
les chaînes de caractères avec la fonction map() et dans le chapitre 12 Plus sur les listes avec la fonction zip(). Ces
trois fonctions renvoient des itérateurs qui sont épuisés une fois utilisés :
13. [Link]
14. [Link]
15. [Link]
16. [Link]
Lorsque ces fonctions avaient été évoquées, nous n’avions pas vu ce problème d’épuisement car elles étaient utilisées
directement dans une boucle. Par exemple :
1 >>> for i, j in zip(range(5), range(5, 10)):
2 ... print(i, j)
3 ...
4 0 5
5 1 6
6 2 7
7 3 8
8 4 9
Ainsi, l’itérateur était généré à chaque fois qu’on lançait la boucle et n’était utilisé qu’une seule fois.
Une derniere fonction renvoyant un itérateur qui existe nativement dans les fonctions builtins de Python est reversed
(). Celle-ci prend en argument un objet de type séquence (liste, tuple, chaîne de caractère ou range) et renvoie un itérateur
parcourant la séquence en sens inverse :
1 >>> reversed(range(5))
2 <range_iterator object at 0x7f8b34227780>
3 >>> rev_iterateur = reversed(range(5))
4 >>> for i in rev_iterateur:
5 ... print(i)
6 ...
7 4
8 3
9 2
10 1
11 0
12 >>> list(rev_iterateur)
13 []
Pour finir, examinons les propriétés des itérateurs que nous avions vues pour les conteneurs. Un objet itérateur est
bien sûr iterable et ordonné, par contre il n’est pas indexable. Il ne supporte pas la fonction len(), supporte l’opérateur
in et il est hachable.
1 >>> animaux = ["chien", "chat", "souris"]
2 >>> iterateur = iter(animaux)
3 >>> len(iterateur)
4 Traceback (most recent call last):
5 File "<stdin>", line 1, in <module>
6 TypeError: object of type 'list_iterator' has no len()
7 >>> iterateur[1]
8 Traceback (most recent call last):
9 File "<stdin>", line 1, in <module>
10 TypeError: 'list_iterator' object is not subscriptable
11 >>> "chien" in iterateur
12 True
13 >>> hash(iterateur)
14 8741535406492
Attention
L’utilisation de l’opérateur in pour un test d’appartenance sur un itérateur épuise ce dernier (au même titre que
l’utilisation de l’itérateur dans une boucle où avec la fonction list()) :
1 >>> line = "9 11 25 92 49 98 62 72 63 74"
2 >>> obj_map = map(int, [Link]())
3 >>> 9 in obj_map
4 True
5 >>> 9 in obj_map
6 False
Ligne 3, on fait un premier test qui parcourt l’itérateur et renvoie True. Même si la valeur 9 était présente initialement,
le deuxième test, ligne 5, renvoie False car l’itérateur est épuisé.
La fonction product() prend (au moins) deux conteneurs en argument et génère toutes les combinaisons possibles
d’association :
1 >>> import itertools
2 >>> predateurs = ["lion", "requin", "tigre"]
3 >>> proies = ["souris", "oiseau", "gazelle"]
4 >>> for pred, proie in [Link](predateurs, proies):
5 ... print(pred, proie)
6 ...
7 lion souris
8 lion oiseau
9 lion gazelle
10 requin souris
11 requin oiseau
12 requin gazelle
13 tigre souris
14 tigre oiseau
15 tigre gazelle
On a ici toutes les combinaisons possibles entre les trois objets ma_liste passés en argument.
Avec deux conteneurs en argument, cette fonction product() revient à faire une double boucle sur les deux conte-
neurs. Elle est donc particulièrement adaptée pour parcourir toutes les éléments d’un tableau. Par exemple, la commande
suivante parcourera toutes les cases d’un échiquier :
17. [Link]
18. [Link]
19. [Link]
20. [Link]
Mais attention, la fonction product() est un itérateur. Donc quand elle est épuisée, on ne peut plus l’utiliser :
1 >>> list(parcours_echiquier)
2 []
Une utilisation particulièrement utile de product() en bioinformatique peut être de générer toutes les séquences
d’ADN possibles (mots) de deux lettres :
1 >>> bases = "atgc"
2 >>> list([Link](bases, bases))
3 [('a', 'a'), ('a', 't'), ('a', 'g'), ('a', 'c'), ('t', 'a'), ('t', 't'), ('t', 'g'),
4 ('t', 'c'), ('g', 'a'), ('g', 't'), ('g', 'g'), ('g', 'c'), ('c', 'a'), ('c', 't'),
5 ('c', 'g'), ('c', 'c')]
De même, [Link](bases, bases, bases) itérera sur tous les mots de trois lettres possibles. Ou en-
core, si on définit une chaîne de caractères contenant les vingt acides aminés comme suit aas = "acdefghiklmnpqrstvwy
", [Link](aas, aas) produira tous les dipeptides possibles.
La fonction groupby() permet de faire des regroupements puissants. Pour vous montrer son fonctionnement, nous
allons prendre un exemple. Nous partons d’une liste de mots que nous triions par longueur avec l’argument key auquel
on passe la callback len (voir chapitre 12 Plus sur les listes) :
1 >>> mots = ["bar", "babar", "bam", "ba", "bababar", "barre", "bla", "barbare"]
2 >>> [Link](key=len)
3 >>> mots
4 ['ba', 'bar', 'bam', 'bla', 'babar', 'barre', 'bababar', 'barbare']
• Lignes 1 et 3. Il est important de passer à l’argument key la même fonction callback que lors du tri initial.
• Lignes 4 à 7. En transformant cet itérateur en liste, on voit qu’il génère une liste de tuples. Le premier élément de
chaque tuple est un entier correspondant à une longueur de mot, le second élément est un itérateur. Que contient
ce dernier ?
1 >>> for longueur, iterateur in [Link](mots, key=len):
2 ... print(longueur, list(iterateur))
3 ...
4 2 ['ba']
5 3 ['bar', 'bam', 'bla']
6 5 ['babar', 'barre']
7 7 ['bababar', 'barbare']
Lignes 4 à 7. La conversion de cet itérateur en liste montre qu’il contient tous les mots de même longueur.
Comme vu dans une rubrique précédente, on peut passer une fonction lambda à l’argument key :
1 >>> [Link](key=lambda chaine: [Link]("a"))
2 >>> mots
3 ['ba', 'bar', 'bam', 'bla', 'barre', 'babar', 'barbare', 'bababar']
4 >>> [Link](mots, key=lambda chaine: [Link]("a"))
5 <[Link] object at 0x7f467a6d0ca0>
6 >>> list([Link](mots, key=lambda chaine: [Link]("a")))
7 [(1, <itertools._grouper object at 0x7f467a58c490>),
8 (2, <itertools._grouper object at 0x7f467a58c100>),
9 (3, <itertools._grouper object at 0x7f467a58c040>)]
10 >>> for nb_a, iterateur in [Link](mots, key=lambda chaine: [Link]("a")):
11 ... print(nb_a, list(iterateur))
12 ...
13 1 ['ba', 'bar', 'bam', 'bla', 'barre']
14 2 ['babar', 'barbare']
15 3 ['bababar']
Conseil
Avant de faire un regroupement avecgroupby(), pensez à trier la liste initiale avec .sort() ou sorted() en utilisant
la même fonction (ou fonction lambda) passée à l’argument key.
Remarque
Il existe aussi une méthode .groupby() qui procède à des regroupements sur les dataframes pandas. Son mode de
fonctionnement est assez différent par rapport à la fonction groupby() du module itertools. Vous pouvez consulter le
chapitre 22 Modules pandas pour en savoir un peu plus.
26.4.4 Générateurs
Définition
Un générateur est un type d’itérateur particulier. On peut créer un générateur très facilement avec le mot-clé yield
ou avec les expression génératrices (generator expressions en anglais) qui ont une syntaxe similaire à celle des listes de
compréhension.
La création d’un générateur avec le mot-clé yield consiste à créer une fonction utilisant ce mot-clé. À partir de ce
moment là, la fonction renvoie un générateur. Avant de voir un exemple, imaginons une fonction qui crée et renvoie une
liste :
1 >>> def cree_alphabet():
2 ... alphabet = []
3 ... for i in range(97, 123):
4 ... [Link](chr(i))
5 ... return alphabet
6 ...
7 >>> alphabet = cree_alphabet()
8 >>> alphabet
9 ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q',
10 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
Pour créer un générateur équivalent, il suffira de remplacer le .append() par un yield et d’enlever le return :
Comme pour tous les itérateurs, une fois tous les éléments parcourus le générateur est épuisé. Notez que le yield
n’est pas une fonction mais un mot-clé, on n’utilise donc pas de parenthèses. Ce mot-clé yield n’a de sens que dans
une fonction et ne s’utilise que pour créer des générateurs.
La technique avec une expression génératrice ressemble à la syntaxe des listes de compréhension (voir la rubrique
Listes de compréhension du chapitre 12 Plus sur les listes), mais on l’entoure de parenthèses à la place des crochets :
1 >>> [n**2 for n in range(10)] # Liste de compréhension.
2 [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
3 >>> (n**2 for n in range(10)) # Expression génératrice.
4 <generator object <genexpr> at 0x7f917feb39f0>
5 >>> gen = (n**2 for n in range(10))
6 >>> for n in gen:
7 ... print(n)
8 ...
9 0
10 [...]
11 81
Conseil
Comme vous le voyez, créer un générateur est extrêmement aisé avec le mot-clé yield ou les expressions génératrices
par rapport à l’écriture d’une classe itérateur (voir ci-dessus). Ainsi nous vous conseillons d’utiliser plutôt les générateurs
lorsque vous souhaitez créer des itérateurs simples.
21. [Link]
On peut lancer timeit directement à la ligne de commande Unix avec l’option -m suivie de l’instruction Python à
exécuter entre guillemets. Python va effectuer plusieurs fois l’instruction (ici 50 fois) et donnera une approximation au
plus juste du temps d’exécution de celle-ci. Le nombre d’exécutions de l’instruction dépendra du temps pris par celle-ci
et sera entièrement déterminé par Python.
En revenant à notre problématique, voici les résultats de notre somme de 1 à 100000 (testé sur un ordinateur portable
relativement récent avec la version Python 3.12 ) :
$ python -m timeit "sum(n**2 for n in range(100000))"
50 loops, best of 5: 3.76 msec per loop
$ python -m timeit "somme=0" "for n in range(100000): somme += n**2"
50 loops, best of 5: 3.59 msec per loop
$ python -m timeit "sum([n**2 for n in range(100000)])"
50 loops, best of 5: 4.89 msec per loop
• Ligne 1. On utilise un générateur et la fonction sum() pour calculer cette somme. Notez que lorsqu’un générateur
est utilisé dans une fonction, les parenthèses ne sont pas obligatoires. Cela simplifie la syntaxe par rapport à
sum((n**2 for n in range(nb))).
• Ligne 3. On utilise une boucle Python classique pour calculer cette somme. Notez que pour pouvoir utiliser timeit
sur une ligne, on est obligé de passer deux arguments entre guillemets (initialisation de la variable somme et boucle).
• Ligne 5. On utilise une liste de compréhension pour calculer cette somme.
La méthode avec les générateurs est à peu près équivalente à l’utilisation d’une boucle classique où on accumule la
somme, preuve que les deux méthodes sont bien optimisées. De manière spectaculaire, la liste de compréhension est bien
plus lente (presque 1 ms de plus). Ceci vient du fait qu’il faut créer la liste de tous les éléments en mémoire, ce qui est
contre-productif. Le générateur ou la boucle classique se contentent d’itérer et sont bien plus économes.
Dernier point, un test réalisé avec la version Python 3.13 sortie en octobre 2024 conduit aux mêmes observations.
Attention de ne pas transformer cet itérateur en liste ou tuple sous peine de saturer la mémoire de l’ordinateur et de
le faire planter !
Dans le même module les fonctions cycle() 26 et repeat() 27 sont également des itérateurs infinis.
De manière générale, l’utilisation d’itérateurs peut améliorer la lisibilité de vos programmes. Cet article 28 fait remarquer
que le simple fait de créer un itérateur et de le nommer donne un sens à ce qu’il contient. En reprenant notre exemple
sur la somme des carrés :
1 tous_les_carres = (n**2 for n in range(nb))
2 somme = sum(tous_les_carres)
On voit que ce que représente l’objet tous_les_carres n’existe tout simplement pas avec la boucle for ! Par
ailleurs, outre l’avantage de rapidité, l’utilisation de la fonction sum() rend la lecture très claire.
Dernier point, les itérateurs et notamment les générateurs, donnent un moyen de faire de la programmation fonction-
nelle 29 en Python. Sans rentrer dans les considérations théoriques, nous avons déjà vu l’idée générale lorsque nous avons
abordé le method chaining sur les chaînes de caractères ou sur les dataframes pandas. Initialement, la programmation
fonctionnelle en Python utilisait la fonction map() (ainsi que les fonctions filter() et reduce() non abordées ici).
Mais depuis l’arrivée des générateurs, on préfère ces derniers qui sont considérés plus Pythoniques. Regardons un exemple
où nous transformons une chaîne de caractères en entiers puis nous calculons la somme. D’abord avec un générateur :
1 >>> ligne = "9 11 25 92 49 98 62 72 63 74"
2 >>> sum(int(nb) for nb in [Link]())
3 555
4 >>>
26. [Link]
27. [Link]
28. [Link]
can_improve_your_code
29. [Link]
30. [Link]
Une chose à noter dans cet exemple est que lorsqu’on crée un générateur à partir d’un autre générateur, le générateur
initial n’est pas déclenché. Par exemple, en Ligne 4 pour inverses le générateur nombres n’est pas encore déclenché,
ou en Ligne 5 pour cos_inverses le générateur inverses n’est pas déclenché non plus. Tous les générateurs seront
déclenchés en chaine lorsqu’on exécutera la Ligne 6.
Conseil
En écrivant un générateur par ligne, le code est bien lisible. Evitez une syntaxe en une ligne qui s’avérera illisible :
([Link](nb) for nb in (nb**-1 for nb in (int(nb) for nb in [Link]())))
La fonction input() demande à l’utilisateur de saisir une chaîne de caractères. Cette chaîne de caractères est ensuite
transformée en nombre entier avec la fonction int().
Si l’utilisateur ne rentre pas un nombre, voici ce qui se passe :
1 >>> nb = int(input("Entrez un nombre entier : "))
2 Entrez un nombre entier : ATCG
3 Traceback (most recent call last):
4 File "<stdin>", line 1, in <module>
5 ValueError: invalid literal for int() with base 10: 'ATCG'
L’erreur provient de la fonction int() qui n’a pas pu convertir la chaîne de caractères "ATCG" en nombre entier, ce
qui est parfaitement normal. En termes plus techniques, on dira que « Python a levé une exception de type ValueError
». Eh oui il y a de nombreux types d’exceptions différents (voir plus bas) ! Le nom de l’exception apparaît toujours comme
le premier mot de la dernière ligne du message d’erreur. Si nous lancions ces lignes de code sous forme de script (du style
python [Link]), cet exemple conduirait à l’arrêt de l’exécution du programme.
Le jeu d’instructions try / except permet de tester l’exécution d’une commande et d’intervenir en cas de levée
d’exception.
31. [Link]
32. [Link]
1 >>> try:
2 ... nb = int(input("Entrez un nombre entier : "))
3 ... except:
4 ... print("Vous n'avez pas entré un nombre entier !")
5 ...
6 Entrez un nombre entier : ATCG
7 Vous n'avez pas entré un nombre entier !
Dans cet exemple, l’exception levée par la fonction int() (qui ne peut pas convertir "ATCG" en nombre entier) est
interceptée et déclenche l’affichage du message d’avertissement.
On peut ainsi redemander sans cesse un nombre entier à l’utilisateur, jusqu’à ce que celui-ci en rentre bien un.
1 >>> while True:
2 ... try:
3 ... nb = int(input("Entrez un nombre entier : "))
4 ... print("Le nombre est", nb)
5 ... break
6 ... except:
7 ... print("Vous n'avez pas entré un nombre entier !")
8 ... print("Essayez encore")
9 ...
10 Entrez un nombre entier : ATCG
11 Vous n'avez pas entré un nombre entier !
12 Essayez encore
13 Entrez un nombre entier : toto
14 Vous n'avez pas entré un nombre entier !
15 Essayez encore
16 Entrez un nombre entier : 3.2
17 Vous n'avez pas entré un nombre entier !
18 Essayez encore
19 Entrez un nombre entier : 55
20 Le nombre est 55
Notez que dans cet exemple, l’instruction while True est une boucle infinie car la condition True est toujours
vérifiée. L’arrêt de cette boucle est alors forcé par la commande break lorsque l’utilisateur a effectivement entré un
nombre entier.
La gestion des exceptions est très utile dès lors que des données extérieures entrent dans un programme Python, que
ce soit directement par l’utilisateur (avec la fonction input()) ou par des fichiers. Cela est fondamental si vous distribuez
votre code à la communauté : si les utilisateurs ne connaissent pas Python, un message comme Vous n'avez pas entré
un nombre entier ! reste plus clair que ValueError: invalid literal for int() with base 10: 'ATCG'.
Vous pouvez par exemple vérifier qu’un fichier a bien été ouvert.
1 >>> nom = "[Link]"
2 >>> try:
3 ... with open(nom, "r") as fichier:
4 ... for ligne in fichier:
5 ... print(ligne)
6 ... except:
7 ... print("Impossible d'ouvrir le fichier", nom)
Si une erreur est déclenchée, c’est sans doute que le fichier n’existe pas à l’emplacement indiqué sur le disque ou que
vous n’avez pas les droits pour le lire.
Il est également possible de spécifier le type d’erreur à gérer. Le premier exemple que nous avons étudié peut s’écrire :
1 >>> try:
2 ... nb = int(input("Entrez un nombre entier : "))
3 ... except ValueError:
4 ... print("Vous n'avez pas entré un nombre entier !")
5 ...
6 Entrez un nombre entier : ATCG
7 Vous n'avez pas entré un nombre entier !
Ici, on intercepte une exception de type ValueError, ce qui correspond bien à un problème de conversion avec
int().
Attention, si vous précisez le type d’exception comme ValueError, le except ValueError n’empêchera pas la
levée d’une autre exception.
1 >>> try:
2 ... nb = int(variable)
3 ... except ValueError:
4 ... print("Vous n'avez pas entré un nombre entier !")
5 ...
6 Traceback (most recent call last):
7 File "<stdin>", line 2, in <module>
8 NameError: name 'variable' is not defined. Did you mean: 'callable'?
Ici l’exception levée est de type NameError, car variable n’existe pas. Alors que si vous mettez except tout court,
cela intercepte n’importe quelle exception.
1 >>> try:
2 ... nb = int(variable)
3 ... except:
4 ... print("Vous n'avez pas entré un nombre entier !")
5 ...
6 Vous n'avez pas entré un nombre entier !
7 >>>
Vous voyez qu’ici cela pose un nouveau problème : le message d’erreur ne correspond pas à l’exception levée !
Conseil
• Nous vous conseillons vivement de toujours préciser le type d’exception dans vos except. Cela évite d’intercepter
une exception que vous n’aviez pas prévue. Il est possible d’intercepter plusieurs types d’exceptions en passant un
tuple à except, par exemple : except (Exception1, Exception2).
• Par ailleurs, ne mettez pas trop de lignes dans le bloc du try. Dans un tel cas, il peut être très pénible de trouver la
ligne qui a conduit à l’exécution du except. Pire encore, il se peut que des lignes que vous aviez prévues ne soient
pas exécutées ! Donc gardez des choses simples dans un premier temps, comme par exemple tester les conversions
de type ou vérifier qu’un fichier existe bien et que vous pouvez l’ouvrir.
Il existe de nombreux types d’exception comme RuntimeError, TypeError, NameError, IOError, etc. Vous pouvez
aller voir la liste complète 33 sur le site de Python. Nous avions déjà croisé des noms d’exception au chapitre 23 (Avoir
la classe avec les objets) en regardant ce que contient le module builtins.
Leur présence dans le module builtins signifie qu’elles font partie du langage lui même, au même titre que les
fonctions de base comme range(), list(), etc.
Avez-vous aussi remarqué que leur nom commence toujours par une majuscule et qu’il peut en contenir plusieurs à
la façon CamelCase ? Si vous avez bien lu le chapitre 16 Bonnes pratiques en programmation Python, avez-vous deviné
pourquoi ? Et bien, c’est parce que les exceptions sont des classes. C’est très intéressant car il est ainsi possible
d’utiliser l’héritage pour créer ses propres exceptions à partir d’exceptions pré-existantes. Nous ne développerons pas cet
aspect, mais en guise d’illustration, regardez ce que renvoit un help() de l’exception OverflowError.
33. [Link]
1 >>> help(OverflowError)
2 [...]
3 class OverflowError(ArithmeticError)
4 | Result too large to be represented.
5 |
6 | Method resolution order:
7 | OverflowError
8 | ArithmeticError
9 | Exception
10 | BaseException
11 | object
La ligne 2 lève une exception ValueError lorsque la variable valeur est négative. L’instruction raise est bien
pratique lorsque vous souhaitez stopper l’exécution d’un programme si une variable ne se trouve pas dans le bon intervalle
ou ne contient pas la bonne valeur. Vous avez sans doute compris maintenant pourquoi on parlait de « levée » d’exception…
Enfin, on peut aussi être très précis dans le message d’erreur. Observez la fonction download_page() qui, avec le
module urllib, télécharge un fichier sur internet.
1 import [Link]
2
3 def download_page(address):
4 error = ""
5 page = ""
6 try:
7 data = [Link](address)
8 page = [Link]()
9 except IOError as e:
10 if hasattr(e, 'reason'):
11 error = "Cannot reach web server: " + str([Link])
12 if hasattr(e, 'code'):
13 error = f"Server failed {[Link]:d}"
14 return page, error
15
16 data, error = download_page("[Link]
17
18 if error:
19 print(f"Erreur rencontrée : {error}")
20 else:
21 with open("[Link]", "w") as prot:
22 [Link]([Link]("utf-8"))
23 print("Protéine enregistrée")
La variable e est une instance de l’exception IOError. Certains de ses attributs sont testés avec la fonction hasattr()
pour ainsi affiner le message renvoyé (ici contenu dans la variable error).
Si tout se passe bien, la page est téléchargée est stockée dans la variable data, puis enregistrée sur le disque dur.
il va alors contenir :
1 #!/usr/bin/env python
2
3 print("Hello World !")
Remarque
La ligne #! /usr/bin/env python n’est pas considérée comme un commentaire par Python, ni comme une ins-
truction Python d’ailleurs . Cette ligne a une signification particulière pour le système d’exploitation Unix.
Pour exécuter le script, il suffit alors de taper son nom précédé des deux caractères ./ (afin de préciser au shell où se
trouve le script) :
$ ./[Link]
Hello World !
Définition
Le shebang 34 correspond aux caractères #! qui se trouvent au début de la première ligne du script test.
Le shebang est suivi du chemin complet du programme qui interprète le script ou du programme qui sait où se
trouve l’interpréteur Python. Dans l’exemple précédent, c’est le programme /usr/bin/env qui indique où se trouve
l’interpréteur Python.
L’utilisation de la syntaxe *args permet d’empaqueter tous les arguments positionnels dans un tuple unique args
récupéré au sein de la fonction. L’avantage est que nous pouvons passer autant d’arguments positionnels que l’on veut.
Toutefois, on s’aperçoit en ligne 10 que cette syntaxe ne fonctionne pas avec les arguments par mot-clé.
Il existe un équivalent avec les arguments par mot-clé :
34. [Link]
La syntaxe **kwargs permet d’empaqueter l’ensemble des arguments par mot-clé, quel que soit leur nombre, dans
un dictionnaire unique kwargs récupéré dans la fonction. Les clés et valeurs de celui-ci sont les noms des arguments et les
valeurs passées à la fonction. Toutefois, on s’aperçoit en ligne 9 que cette syntaxe ne fonctionne pas avec les arguments
positionnels.
Si on attend un mélange d’arguments positionnels et par mot-clé, on peut utiliser *args et **kwargs en même
temps :
1 >>> def fct(*args, **kwargs):
2 ... print(args)
3 ... print(kwargs)
4 ...
5 >>> fct()
6 ()
7 {}
8 >>> fct(1, 2)
9 (1, 2)
10 {}
11 >>> fct(z=1, y=2)
12 ()
13 {'y': 2, 'z': 1}
14 >>> fct(1, 2, 3, z=1, y=2)
15 (1, 2, 3)
16 {'y': 2, 'z': 1}
Conseil
Les noms *args et **kwargs sont des conventions en Python, ils rappellent les mots arguments et keyword argu-
ments. Bien qu’on puisse mettre ce que l’on veut, nous vous conseillons de respecter ces conventions pour faciliter la
lecture de votre code par d’autres personnes.
L’utilisation de la syntaxe *args et **kwargs est très classique dans le module Fenêtres graphiques et Tkinter
présenté dans le chapitre 25 (en ligne).
Il est possible d’utiliser ce mécanisme d’empaquetage / désempaquetage (packing / unpacking) dans l’autre sens :
Avec la syntaxe *t on désempaquette le tuple à la volée lors de l’appel à la fonction. Cela est aussi possible avec un
dictionnaire :
1 >>> def fct(x, y, z):
2 ... print(x, y, z)
3 ...
4 >>> dico = {'x': -1, 'y': -2, 'z': -3}
5 >>> fct(**dico)
6 -1 -2 -3
Au final, on récupère des tuples au lieu des listes initiales. Mais à ce stade, vous devriez être capable de les retransformer
en liste ;-).
26.8 Décorateurs
Dans le chapitre 24, nous avons rencontré la notion de décorateur pour déclarer des objets de type property. Cela
permettait de rendre des méthodes accessibles comme des attributs (décorateur @property), et plus généralement de
Définition
Un décorateur est une fonction qui modifie le comportement d’une autre fonction.
Ceci étant dit, comme cela fonctionne-t-il ? Commençons par une fonction simple qui affiche de la nourriture :
1 def imprime_victuaille():
2 print("tomate / mozza")
On souhaite améliorer cette fonction et transformer cette victuaille en sandwich, en affichant une tranche de pain avant
et après. La stratégie va être de créer une fonction spéciale, qu’on appelle décorateur, modifiant imprime_victuaille
().
1 def transforme_en_sandwich(fonction_a_decorer):
2 def emballage():
3 print("Pain")
4 fonction_a_decorer()
5 print("Pain")
6 return emballage
La fonction transforme_en_sandwich() est notre décorateur, elle prend en argument la fonction que l’on sou-
haite décorer sous forme de callback (donc sans les parenthèses). On voit qu’à l’intérieur, on définit une sous-fonction
emballage() qui va littéralement « emballer » (wrap) notre fonction à décorer, c’est-à-dire, effectuer une action avant
et après l’appel de la fonction à décorer. Enfin, le décorateur renvoie cette sous-fonction emballage sous forme de
callback. Pour que le décorateur soit actif, il faudra « transformer » la fonction à décorer avec notre fonction décoratrice :
1 imprime_victuaille = transforme_en_sandwich(imprime_victuaille)
Au final l’idée est d’appeler la fonction décoratrice plutôt que la fonction imprime_victuaille() elle-même.
Regardons ce que donne l’exécution de la fonction avant et après décoration :
Fonction non décorée:
tomate/ mozza
Fonction décorée:
Pain
tomate/ mozza
Pain
Le premier appel en ligne 13 exécute la fonction simple, alors que le second en ligne 17 exécute la fonction décorée.
Cette construction peut sembler ardue et difficile à comprendre. Heureusement, Python a une notation en « sucre
syntaxique » (syntactic sugar) qui en facilite la lecture. Celle-ci utilise le symbole @ :
1 def transforme_en_sandwich(fonction_a_decorer):
2 def emballage():
3 print("Pain")
4 fonction_a_decorer()
5 print("Pain")
6 return emballage
7
8 @transforme_en_sandwich
9 def imprime_victuaille():
10 print("tomate / mozza")
11
12 if __name__ == "__main__":
13 imprime_victuaille()
La ligne 8 transforme irrémédiablement la fonction imprime_victuaille() en fonction décorée. Cela parait déjà
un peu plus lisible. L’exécution donnera bien sûr :
Pain
tomate / mozza
Pain
Au final, la notation :
1 @decorator
2 def fct():
3 [...]
est équivalente à :
1 fct = decorator(fct)
Cela fonctionne avec n’importe quelle fonction prenant en argument une autre fonction.
Conseil
Nous vous conseillons bien sûr d’utiliser systématiquement la notation @decorator qui est plus lisible et intuitive.
Si tout cela vous semble ardu (on vous comprend…), vous devez vous dire « pourquoi utiliser une construction aussi
complexe ? ». Et bien, c’est tout simplement parce qu’un décorateur est ré-utilisable dans n’importe quelle fonction. Si
on reprend la même fonction décoratrice que ci-dessus :
1 @transforme_en_sandwich
2 def imprime_victuaille1():
3 print("tomate / mozza")
4
5 @transforme_en_sandwich
6 def imprime_victuaille2():
7 print("jambon / fromage")
8
9 if __name__ == "__main__":
10 imprime_victuaille1()
11 print()
12 imprime_victuaille2()
On a donc un décorateur permettant de transformer en sandwich n’importe quelle fonction imprimant une victuaille !
Ceci renverra :
Pain
tomate / mozza
Pain
Pain
jambon / fromage
Pain
Un exemple plus concret de décorateur pourrait être la mesure du temps d’exécution d’une fonction :
import time
def mesure_temps(fonction_a_decorer):
def emballage():
temps1 = [Link]()
fonction_a_decorer()
temps2 = [Link]()
print(f"Le temps d'éxécution de {fonction_a_decorer.__name__} est "
f"{temps2 - temps1} s")
return emballage
En ligne 8, l’attribut .__name__ renvoie le nom de la fonction sous forme de chaîne de caractères. Dans cet exemple,
le décorateur @mesure_temps mis devant n’importe quelle fonction affichera systématiquement le temps d’exécution de
celle-ci.
Pour finir, si on revient sur le décorateur @property vu dans le chapitre 24 Avoir plus la classe avec les objets, nous
avions vu également qu’il existait une fonction property(). Donc pour les décorateurs pré-existants que nous avons
abordés dans le chapitre 24, il existe des fonctions équivalentes. Comme dans notre exemple, la notation @decorateur
va finalement appeler la fonction décoratrice. Donc derrière une notation @quelquechose, il existe toujours une fonction
quelquechose() remplissant ce rôle de décorateur.
35. [Link]
Lignes 7. On calcule directement les valeurs en ordonnées avec la fonction cosinus du module NumPy. On constate
ici que NumPy redéfinit certaines fonctions ou constantes mathématiques de base, comme pi, cos() ou abs() (valeur
absolue, ou module d’un nombre complexe). Ces fonctions sont directement utilisables avec un objet array.
Ligne 9. On calcule la transformée de Fourier avec la fonction fft() qui renvoie un vecteur (objet array à une
dimension) de nombres complexes. Eh oui, le module NumPy gère aussi les nombres complexes !
Ligne 10. On extrait le module du résultat précédent avec la fonction abs().
Ligne 11. La variable x_ABSTFL représente l’abscisse du spectre (en radian−1 ).
Ligne 12. La variable ABSTF contient le spectre lui même. L’analyse de ce dernier nous donne un pic à 0,15 radian−1 ,
ce qui correspond bien à 2π (c’est plutôt bon signe de retrouver ce résultat).
Quittez Python. L’historique de toutes vos commandes est dans votre répertoire personnel, dans le fichier .history.
Relancez l’interpréteur Python.
1 >>> import readline
2 >>> readline.read_history_file()
Vous pouvez accéder aux commandes de la session précédente avec la flèche du haut de votre clavier. D’abord les
commandes readline.read_history_file() et import readline de la session actuelle, puis print(a), a = a +
11, a = 22…
Mini-projets
Dans ce chapitre, nous vous proposons quelques scénarios pour développer vos compétences en Python et mettre en
œuvre les concepts que vous avez rencontrés dans les chapitres précédents.
Conseil
Des explications sur le format FASTA et des exemples de code sont fournis dans l’annexe A Quelques formats de
données en biologie.
27.1.2 Genbank2fasta
Ce projet consiste à écrire un convertisseur de fichier, du format GenBank au format FASTA.
Pour cela, nous allons utiliser le fichier GenBank du chromosome I de la levure de boulanger Saccharomyces cerevisiae.
Vous pouvez télécharger ce fichier :
• soit via le lien sur le site du cours NC_001133.gbk 4 ;
1. [Link]
2. [Link]
3. [Link]
4. [Link]
349
Chapitre 27. Mini-projets 27.1. Description des projets
• soit directement sur la page de Saccharomyces cerevisiae S288c chromosome I, complete sequence 5 sur le site du
NCBI, puis en cliquant sur Send to, puis Complete Record, puis Choose Destination : File, puis Format : GenBank
(full) et enfin sur le bouton Create File.
Vous trouverez des explications sur les formats FASTA et GenBank ainsi que des exemples de code dans l’annexe A
Quelques formats de données en biologie.
Vous pouvez réaliser ce projet sans ou avec des expressions régulières (abordées dans le chapitre 17).
d2θ g
aθ (t) = 2
(t) = − ∗ sin(θ (t))
dt l
où θ représente l’angle entre la verticale et la tige du pendule, aθ l’accélération angulaire, g la gravité, et l la longueur
de la tige (note : pour la dérivation d’une telle équation vous pouvez consulter la page wikipedia 7 ou l’accompagnement
pas à pas, cf. la rubrique suivante).
Pour trouver la valeur de θ en fonction du temps, on pourra utiliser la méthode semi-implicite d’Euler 8 de résolution
d’équation différentielle. La formule ci-dessus donne l’accélération angulaire au temps t : aθ (t) = − gl × sin(θ (t)). À
partir de celle-ci, la méthode propose le calcul de la vitesse angulaire au pas suivant : vθ (t + δ t) = vθ (t) + aθ (t) × δ t
(où δ t représente le pas de temps entre deux étapes successives de la simulation). Enfin, cette vitesse vθ (t + δ t) donne
l’angle θ au pas suivant : θ (t + δ t) = θ (t) + vθ (t + δ t) × δ t. On prendra un pas de temps δ t = 0.05 s, une accélération
gravitationnelle g = 9.8 m.s−2 et une longueur de tige de l = 1 m.
Pour la visualisation, vous pourrez utiliser le widget canvas du module Tkinter (voir le chapitre 25 Fenêtres graphiques
et Tkinter (en ligne), rubrique Un canvas animé dans une classe). On cherche à obtenir un résultat comme montré dans
la figure 27.1.
5. [Link]
6. [Link]
7. [Link]
8. [Link]
Nous vous conseillons de procéder d’abord à la mise en place du simulateur physique (c’est-à-dire obtenir θ en fonction
du temps ou du pas de simulation). Faites par exemple un premier script Python qui produit un fichier à deux colonnes
(temps et valeur de θ ). Une fois que cela fonctionne bien, il vous faudra construire l’interface Tkinter et l’animer. Vous
pouvez ajouter un bouton pour démarrer / stopper le pendule et une règle pour modifier sa position initiale.
N’oubliez pas, il faudra mettre dans votre programme final une fonction qui convertit l’angle θ en coordonnées
cartésiennes x et y dans le plan du canvas. Faites également attention au système de coordonnées du canvas où les
ordonnées sont inversées par rapport à un repère mathématique. Pour ces deux aspects, reportez-vous à l’exercice
Polygone de Sierpinski du chapitre 25 Fenêtres graphiques et Tkinter (en ligne).
Toujours dans le script words_in_proteome.py, écrivez la fonction read_sequences() qui va lire le protéome
dans le fichier dont le nom est fourni en second argument du script. Cette fonction va renvoyer un dictionnaire dont les
clefs sont les identifiants des protéines (par exemple, O95139, O75438, Q8N4C6) et dont les valeurs associées sont les
séquences.
Dans le programme principal, affichez le nombre de séquences lues. À des fins de test, affichez également la séquence
associée à la protéine O95139.
9. [Link]
10. [Link]
11. [Link]
Écrivez maintenant la fonction search_words_in_proteome() qui prend en argument la liste de mots et le dic-
tionnaire contenant les séquences des protéines et qui va compter le nombre de séquences dans lesquelles un mot est
présent. Cette fonction renverra un dictionnaire dont les clefs sont les mots et les valeurs le nombre de séquences qui
contiennent ces mots. La fonction affichera également le message suivant pour les mots trouvés dans le protéome :
ACCESS found in 1 sequences
ACID found in 38 sequences
ACT found in 805 sequences
[...]
Pour terminer, écrivez maintenant la fonction find_most_frequent_word() qui prend en argument le dictionnaire
renvoyé par la précédente fonction search_words_in_proteome() et qui affiche le mot trouvé dans le plus de protéines,
ainsi que le nombre de séquences dans lesquelles il a été trouvé, sous la forme :
=> xxx found in yyy sequences
Jusqu’à présent, nous avions déterminé, pour chaque mot, le nombre de séquences dans lesquelles il apparaissait.
Nous pourrions aller plus loin et calculer aussi le nombre de fois que chaque mot apparaît dans les séquences.
Pour cela modifier la fonction search_words_in_proteome() de façon à compter le nombre d’occurrences d’un
mot dans les séquences. La méthode .count() vous sera utile.
Déterminez alors quel mot est le plus fréquent dans le protéome humain.
Créez un script [Link] et créez la fonction lit_fichier() qui prend en argument le nom du fichier et
qui renvoie le contenu du fichier sous forme d’une liste de lignes, chaque ligne étant elle-même une chaîne de caractères.
Testez cette fonction avec le fichier GenBank NC_001133.gbk et affichez le nombre de lignes lues.
Dans le même script, ajoutez la fonction extrait_organisme() qui prend en argument le contenu du fichier
précédemment obtenu avec la fonction lit_fichier() (sous la forme d’une liste de lignes) et qui renvoie le nom de
l’organisme. Pour récupérer la bonne ligne vous pourrez tester si les premiers caractères de la ligne contiennent le mot-clé
ORGANISM.
Testez cette fonction avec le fichier GenBank NC_001133.gbk et affichez le nom de l’organisme.
Dans le fichier GenBank, les gènes sens sont notés de cette manière :
gene 58..272
ou
gene <2480..>2707
ou
gene complement(<13363..>13743)
Les valeurs numériques séparées par .. indiquent la position du gène dans le génome (numéro de la première base,
numéro de la dernière base).
Remarque
Le symbole < indique un gène partiel sur l’extrémité 5’, c’est-à-dire que le codon START correspondant est incomplet.
Respectivement, le symbole > désigne un gène partiel sur l’extrémité 3’, c’est-à-dire que le codon STOP correspondant
est incomplet. Pour plus de détails, consultez la documentation du NCBI sur les délimitations des gènes 12 . Nous vous
proposons ici d’ignorer ces symboles > et <.
Repérez ces différents gènes dans le fichier NC_001133.gbk. Pour récupérer ces lignes de gènes il faut tester si la
ligne commence par
gene
(c’est-à-dire 5 espaces, suivi du mot gene, suivi de 12 espaces). Pour savoir s’il s’agit d’un gène sur le brin direct ou
complémentaire, il faut tester la présence du mot complement dans la ligne lue.
Ensuite si vous souhaitez récupérer la position de début et de fin de gène, nous vous conseillons d’utiliser la fonction
replace() et de ne garder que les chiffres et les . Par exemple
gene <2480..>2707
sera transformé en
2480..2707
Enfin, avec la méthode .split() vous pourrez facilement récupérer les deux entiers de début et de fin de gène.
Dans le même script [Link], ajoutez la fonction recherche_genes() qui prend en argument le contenu
du fichier (sous la forme d’une liste de lignes) et qui renvoie la liste des gènes.
Chaque gène sera lui-même une liste contenant le numéro de la première base, le numéro de la dernière base et une
chaîne de caractère "sens" pour un gène sens et "antisens" pour un gène antisens.
Testez cette fonction avec le fichier GenBank NC_001133.gbk et affichez le nombre de gènes trouvés, ainsi que le
nombre de gènes sens et antisens.
La taille du génome est indiqué sur la première ligne d’un fichier GenBank. Trouvez la taille du génome stocké dans
le fichier NC_001133.gbk.
Dans un fichier GenBank, la séquence du génome se trouve entre les lignes
ORIGIN
12. [Link]
et
//
Au début ce drapeau aura la valeur False. Ensuite, quand il se mettra à True, on pourra lire les lignes contenant la
séquence, puis quand il se remettra à False on arrêtera.
Une fois la séquence récupérée, il suffira d’éliminer les chiffres, retours chariots et autres espaces (Conseil : calculer
la longueur de la séquence et comparer la à celle indiquée dans le fichier gbk).
Toujours dans le même script [Link], ajoutez la fonction extrait_sequence() qui prend en argument
le contenu du fichier (sous la forme de liste de lignes) et qui renvoie la séquence nucléique du génome (dans une chaîne
de caractères). La séquence ne devra pas contenir d’espaces, ni de chiffres ni de retours chariots.
Testez cette fonction avec le fichier GenBank NC_001133.gbk et affichez le nombre de bases de la séquence extraite.
Vérifiez que vous n’avez pas fait d’erreur en comparant la taille de la séquence extraite avec celle que vous avez trouvée
dans le fichier GenBank.
Le numéro du gène sera un numéro consécutif depuis le premier gène jusqu’au dernier. Il n’y aura pas de différence
de numérotation entre les gènes sens et les gènes antisens.
Testez cette fonction avec le fichier GenBank NC_001133.gbk.
ou
gene <2480..>2707
ou
gene complement(<13363..>13743)
Les valeurs numériques séparées par .. indiquent la position du gène dans le génome (numéro de la première base,
numéro de la dernière base).
Remarque
Le symbole < indique un gène partiel sur l’extrémité 5’, c’est-à-dire que le codon START correspondant est incomplet.
Respectivement, le symbole > désigne un gène partiel sur l’extrémité 3’, c’est-à-dire que le codon STOP correspondant
est incomplet. Pour plus de détails, consultez la documentation du NCBI sur les délimitations des gènes 13 .
Repérez ces différents gènes dans le fichier NC_001133.gbk. Construisez deux expressions régulières pour extraire du
fichier GenBank les gènes sens et les gènes antisens.
Modifiez ces expressions régulières pour que les numéros de la première et de la dernière base puissent être facilement
extraits.
Dans le même script [Link], ajoutez la fonction recherche_genes() qui prend en argument le contenu
du fichier (sous la forme d’une liste de lignes) et qui renvoie la liste des gènes.
Chaque gène sera lui-même une liste contenant le numéro de la première base, le numéro de la dernière base et une
chaîne de caractère "sens" pour un gène sens et "antisens" pour un gène antisens.
Testez cette fonction avec le fichier GenBank NC_001133.gbk et affichez le nombre de gènes trouvés, ainsi que le
nombre de gènes sens et antisens.
et
//
• Prendre la séquence complémentaire. C’est-à-dire à remplacer la base a par la base t, t par a, c par g et g par c.
• Prendre l’inverse. C’est-à-dire à que la première base de la séquence complémentaire devient la dernière base et
réciproquement, la dernière base devient la première.
Pour vous faciliter le travail, ne travaillez que sur des séquences en minuscule.
Testez cette fonction avec les séquences atcg, AATTCCGG et gattaca.
Toujours dans le même script, ajoutez la fonction ecrit_fasta() qui prend en argument un nom de fichier (sous
forme de chaîne de caractères), un commentaire (sous forme de chaîne de caractères) et une séquence (sous forme de
chaîne de caractères) et qui écrit un fichier FASTA. La séquence sera à écrire sur des lignes ne dépassant pas 80 caractères.
Pour rappel, un fichier FASTA suit le format suivant :
>commentaire
sequence sur une ligne de 80 caractères maxi
suite de la séquence .......................
suite de la séquence .......................
...
Toujours dans le même script, ajoutez la fonction extrait_genes() qui prend en argument la liste des gènes, la
séquence nucléotidique complète (sous forme d’une chaîne de caractères) et le nom de l’organisme (sous forme d’une
chaîne de caractères) et qui pour chaque gène :
• extrait la séquence du gène dans la séquence complète ;
• prend la séquence complémentaire inverse (avec la fonction construit_comp_inverse() si le gène est antisens ;
• enregistre le gène dans un fichier au format FASTA (avec la fonction ecrit_fasta()) ;
• affiche à l’écran le numéro du gène et le nom du fichier fasta créé.
La première ligne des fichiers FASTA sera de la forme :
>nom-organisme|numéro-du-gène|début|fin|sens ou antisens
Le numéro du gène sera un numéro consécutif depuis le premier gène jusqu’au dernier. Il n’y aura pas de différence
de numérotation entre les gènes sens et les gènes antisens.
Testez cette fonction avec le fichier GenBank NC_001133.gbk.
Pour terminer, modifiez le script [Link] de façon à ce que le fichier GenBank à analyser (dans cet
exemple NC_001133.gbk), soit entré comme argument du script.
Vous afficherez un message d’erreur si :
• le script [Link] est utilisé sans argument,
• le fichier fourni en argument n’existe pas.
Pour vous aider, n’hésitez pas à jeter un œil aux descriptions des modules sys et pathlib dans le chapitre 9 sur les
modules.
Testez votre script ainsi finalisé.
F = ma
Cette loi est exprimée ici dans le système de coordonnées cartésiennes (le plan à 2 dimensions). La force F et
l’accélération a sont des vecteurs dont les composantes sont respectivement (Fx , Fy ) et (ax , ay ). La force F correspond
à la somme vectorielle de T et P. La tige du pendule étant rigide, le mouvement de la boule est restreint sur le cercle
de rayon égal à la longueur L de la tige (dessiné en pointillé). Ainsi, seule la composante tangentielle de l’accélération a
sera prise en compte dans ce mouvement. Comment la calculer ? La force de tension T étant orthogonale au mouvement
du pendule, celle-ci n’aura pas d’effet. De même, la composante orthogonale mgcosθ due au poids P n’aura pas d’effet
non plus. Au final, on ne prendra en compte que la composante tangentielle due au poids, c’est-à-dire mgsinθ (cf. figure
27.3). Au final, on peut écrire l’expression suivante en raisonnant sur les valeurs scalaires :
F = ma = −mgsinθ
14. [Link]
15. [Link]
16. [Link]
Le signe − dans cette formule est très important. Il indique que l’accélération s’oppose systématiquement à θ . Si le
pendule se balance vers la droite et que θ devient plus positif, l’accélération tendra toujours à faire revenir la boule dans
l’autre sens vers sa position d’équilibre à θ = 0. On peut faire un raisonnement équivalent lorsque le pendule se balance
vers la gauche et que θ devient plus négatif.
Si on exprime l’accélération en fonction de θ , on trouve ce résultat qui peut sembler peu intuitif au premier abord :
a = −gsinθ
s = θL
Pour bien comprendre cette formule, souvenez-vous de la formule bien connue du cercle l = 2π r (où l est la circon-
férence, et r le rayon) ! Elle relie la valeur de θ à la distance de l’arc entre la position actuelle de la boule et l’origine (à
θ = 0). On peut donc exprimer la vitesse du pendule en dérivant s par rapport au temps t :
ds dθ
v= =L
dt dt
On peut aussi exprimer l’accélération a en dérivant l’arc s deux fois par rapport à t :
d2s d2θ
a= 2
=L 2
dt dt
A nouveau, cette dernière formule exprime l’accélération de la boule lorsque le mouvement de celle-ci est restreint sur
le cercle pointillé. Si la tige n’était pas rigide, l’expression serait différente.
Si on remplace a dans la formule ci-dessus, on trouve :
d2θ
L = −gsinθ
dt 2
Soit en remaniant, on trouve l’équation différentielle en θ décrivant le mouvement du pendule :
d2θ g
+ sinθ = 0
dt 2 L
Dans la section suivante, nous allons voir comment résoudre numériquement cette équation différentielle.
Il existe de nombreuses méthodes numériques de résolution d’équations différentielles 17 . L’objet ici n’est pas de
faire un rappel sur toutes ces méthodes ni de les comparer, mais juste d’expliquer une de ces méthodes fonctionnant
efficacement pour simuler notre pendule.
Nous allons utiliser la méthode semi-implicite d’Euler 18 . Celle-ci est relativement intuitive à comprendre.
17. [Link]
18. [Link]
Commençons d’abord par calculer l’accélération angulaire aθ au temps t en utilisant l’équation différentielle précé-
demment établie :
d2θ g
aθ (t) = (t) = − sinθ (t)
dt 2 L
L’astuce sera de calculer ensuite la vitesse angulaire au pas suivant t + δ t grâce à la relation :
dθ
vθ (t + δ t) = (t + δ t) ≈ vθ (t) + aθ (t) × δ t
dt
Cette équation est ni plus ni moins qu’un remaniement de la définition de l’accélération, à savoir, la variation de
vitesse par rapport à un temps. Cette vitesse vθ (t + δ t) permettra au final de calculer θ au temps t + δ t (c’est-à-dire ce
que l’on cherche !) :
θ (t + δ t) ≈ θ (t) + vθ (t + δ t) × δ t
Dans une réalisation algorithmique, il suffira d’initialiser les variables de notre système puis de faire une boucle sur
un nombre de pas de simulation. A chaque pas, on calculera aθ (t), puis vθ (t + δ t) et enfin θ (t + δ t) à l’aide des formules
ci-dessus.
L’initialisation des variables pourra ressembler à cela :
L <- 1 # longueur tige en m
g <- 9.8 # accélération gravitationnelle en m/s^2
t <- 0 # temps initial en s
dt <- 0.05 # pas de temps en s
# conditions initiales
theta <- pi / 4 # angle initial en rad
dtheta <- 0 # vitesse angulaire initiale en rad/s
L’initialisation des valeurs de theta et dtheta est très importante, car elle détermine le comportement du pendule.
Nous avons choisi ici d’avoir une vitesse angulaire nulle et un angle de départ du pendule θ = π /4 rad = 45 deg. Le pas
dt est également très important, c’est lui qui déterminera l’erreur faite sur l’intégration de l’équation différentielle. Plus
ce pas est petit, plus on est précis, mais plus le calcul sera long. Ici, on choisit un pas dt de 0.05 s qui constitue un bon
compromis.
À ce stade, vous avez tous les éléments pour tester votre pendule. Essayez de réaliser un petit programme python
pendule_basic.py qui utilise les conditions initiales ci-dessus et simule le mouvement du pendule. À la fin de cette
rubrique, nous proposons une solution en langage algorithmique. Essayez dans un premier temps de le faire vous-même.
À chaque pas, le programme écrira le temps t et l’angle θ dans un fichier pendule_basic.dat. Dans les équations,
θ doit être exprimé en radian, mais nous vous conseillons de convertir cet angle en degré dans le fichier (plus facile à
comprendre pour un humain !). Une fois ce fichier généré, vous pourrez observer le graphe correspondant avec matplotlib
en utilisant le code suivant :
1 import [Link] as plt
2 import numpy as np
3
4 # La fonction [Link]() renvoie un array à 2 dim.
5 array_data = [Link]("pendule_basic.dat")
6 # col 0: t, col 1: theta
7 t = array_data[:,0]
8 theta = array_data[:,1]
9
10 # Figure.
11 fig, ax = [Link](figsize=(8, 8))
12 mini = min(theta) * 1.2
13 maxi = max(theta) * 1.2
14 ax.set_xlim(0, max(t))
15 ax.set_ylim(mini, maxi)
16 ax.set_xlabel("t (s)")
17 ax.set_ylabel("theta (deg)")
18 [Link](t, theta)
19 [Link]("pendule_basic.png")
Si vous observez une sinusoïde, bravo, vous venez de réaliser votre première simulation de pendule ! Vous avez
maintenant le « squelette » de votre « moteur » de simulation. N’hésitez pas à vous amuser avec d’autres conditions
initiales. Ensuite vous pourrez passer à la rubrique suivante.
Si vous avez bloqué dans l’écriture de la boucle, voici à quoi elle pourrait ressembler en langage algorithmique :
tant qu'on n'arrête pas le pendule:
# acc angulaire au tps t (en rad/s^2)
d2theta <- -(g/L) * sin(theta)
# v angulaire mise à jour de t -> t + dt
dtheta <- dtheta + d2theta * dt
# theta mis à jour de t -> t + dt
theta <- theta + dtheta * dt
# t mis à jour
t <- t + dt
# mettre à jour l'affichage
afficher_position_pendule(t, theta)
Nous allons maintenant construire l’application tkinter en vous guidant pas à pas. Il est bien sûr conseillé de relire le
chapitre 25 sur Fenêtres graphiques et Tkinter (en ligne) avant de vous lancer dans cette partie.
Comme expliqué largement dans les chapitres 23 Avoir la classe avec les objets et 24 Avoir plus la classe avec les
objets (en ligne), nous allons construire l’application avec une classe. Le programme principal sera donc très allégé et se
contentera d’instancier l’application, puis de lancer le gestionnaire d’événements :
1 if __name__ == "__main__":
2 """Programme principal (instancie la classe principale, donne un
3 titre et lance le gestionnaire d'événements)
4 """
5 app_pendule = AppliPendule()
6 app_pendule.title("Pendule")
7 app_pendule.mainloop()
Ensuite, nous commençons par écrire le constructeur de la classe. Dans ce constructeur, nous aurons une section
initialisant toutes les variables utilisées pour simuler le pendule (voir rubrique précédente), puis, une autre partie générant
les widgets et tous les éléments graphiques. Nous vous conseillons vivement de bien les séparer, et surtout de mettre
des commentaires pour pouvoir s’y retrouver. Voici un « squelette » pour vous aider :
1 class AppliPendule([Link]):
2 def __init__(self):
3 # Instanciation de la classe Tk.
4 [Link].__init__(self)
5 # Ici vous pouvez définir toutes les variables
6 # concernant la physique du pendule.
7 [Link] = [Link] / 4 # valeur intiale theta
8 [Link] = 0 # vitesse angulaire initiale
9 [...]
10 self.g = 9.8 # cst gravitationnelle en m/s^2
11 [...]
12 # Oci vous pouvez construire l'application graphique.
13 [Link] = [Link](self, bg='gray', height=400, width=400)
14 # Création d'un boutton demarrer, arreter, quitter.
15 # Pensez à placer les widgets avec .pack()
16 [...]
Conseil
Pour éviter un message d’erreur si toutes les méthodes n’existe pas encore, vous pouvez indiquer command=[Link]
pour chaque bouton (vous le changerez après).
Le pivot et la boule pourront être créés avec la méthode .create_oval(), la tige le sera avec la méthode .
create_line(). Pensez à créer des variables pour la tige et la boule lors de l’instanciation car celles-ci bougeront par
la suite.
Comment placer ces éléments dans le canvas ? Vous avez remarqué que lors de la création de ce dernier, nous avons
fixé une dimension de 400 × 400 pixels. Le pivot se trouve au centre, c’est-à-dire au point (200, 200). Pour la tige et
la boule, il sera nécessaire de connaître la position de la boule dans le repère du canvas. Or, pour l’instant, nous
définissons la position de la boule avec l’angle θ . Il va donc nous falloir convertir θ en coordonnées cartésiennes (x, y)
dans le repère mathématique défini dans la figure 27.3, puis dans le repère du canvas (xc , yc ) (cf. rubrique suivante).
Conversion de θ en coordonnées (x, y) Cette étape est relativement simple si on considère le pivot comme le centre du
repère. Avec les fonctions trigonométriques sin() et cos(), vous pourrez calculer la position de la boule (voir l’exercice
sur la spirale dans le chapitre 7 Fichiers). Faites attention toutefois aux deux aspects suivants :
• la trajectoire de la boule suit les coordonnées d’un cercle de rayon L (si on choisit L = 1 m, ce sera plus simple) ;
• nous sommes décalés par rapport au cercle trigonométrique classique ; si on considère L = 1 m :
— quand θ = 0, on a le point (0, −1) (pendule en bas) ;
— quand θ = +π /2 = 90 deg, on a (1, 0) (pendule à droite) ;
— quand θ = −π /2 = −90 deg, on a (−1, 0) (pendule à gauche) ;
— quand θ = ±π = ±180 deg, on a (0, 1) (pendule en haut).
La figure 27.3 montre graphiquement les valeurs de θ .
Si vous n’avez pas trouvé, voici la solution :
1 self.x = [Link]([Link]) * self.L
2 self.y = -[Link]([Link]) * self.L
Conversion des coordonnées (x, y) en (xc , yc ) Il nous faut maintenant convertir les coordonnées naturelles mathéma-
tiques du pendule (x, y) en coordonnées dans le canvas (xc , yc ). Plusieurs choses sont importantes pour cela :
• le centre du repère mathématique (0, 0) a la coordonnée (200, 200) dans le canvas ;
• il faut choisir un facteur de conversion : par exemple, si on choisit L = 1 m, on peut proposer le facteur 1 m →
100 pixels ;
• l’axe des ordonnées dans le canvas est inversé par rapport au repère mathématique.
Conseil
Dans votre classe, cela peut être une bonne idée d’écrire une méthode qui réalise cette conversion. Celle-ci pourrait
s’appeler par exemple map_realcoor2canvas().
Il reste maintenant à gérer les boutons permettant de démarrer / stopper le pendule. Pour cela il faudra créer trois
méthodes dans notre classe :
• La méthode .start() : met en mouvement le pendule ; si le pendule n’a jamais été en mouvement, il part de son
point de départ ; si le pendule avait déjà été en mouvement, celui-ci repart d’où on l’avait arrêté (avec la vitesse
qu’il avait à ce moment-là).
• La méthode .stop() : arrête le mouvement du pendule.
• La méthode .move() : gère le mouvement du pendule (génère les coordonnées du pendule au pas suivant).
Le bouton « Démarrer » appellera la méthode .start(), le bouton « Arrêter » appellera la méthode .stop() et
le bouton « Quitter » quittera l’application. Pour lier une action au clic d’un bouton, on se souvient qu’il faut donner à
l’argument par mot-clé command une callback (c’est-à-dire le nom d’une fonction ou méthode sans les parenthèses) :
• btn1 = [Link](self, text="Quitter", command=[Link])
• btn2 = [Link](self, text="Demarrer", command=[Link])
• btn3 = [Link](self, text="Arrêter", command=[Link])
Ici, [Link]() et [Link]() sont des méthodes que l’on doit créer, [Link]() pré-existe lorsque la fenêtre
tkinter est créée.
Nous vous proposons ici une stratégie inspirée du livre de Gérard Swinnen 19 . Créons d’abord un attribut d’instance
self.is_moving dans le constructeur. Celui-ci va nous servir de « drapeau » pour définir le mouvement du pendule.
Il contiendra un entier positif ou nul. Lorsque ce drapeau sera égal à 0, le pendule sera immobile. Lorsqu’il sera > 0, le
pendule sera en mouvement. Ainsi :
• la méthode .start() ajoutera 1 à self.is_moving. Si self.is_moving est égal à 1 alors la méthode self.
move() sera appelée ;
• la méthode .stop() mettra la valeur de self.is_moving à 0.
Puisque .start() ajoute 1 à self.is_moving, le premier clic sur le bouton « Démarrer » appellera la méthode
.move() car self.is_moving vaudra 1. Si l’utilisateur appuie une deuxième fois sur le bouton « Démarrer », self
.is_moving vaudra 2, mais n’appellera pas .move() une deuxième fois ; cela sera vrai pour tout clic ultérieur de
l’utilisateur sur ce bouton. Cette astuce évite des appels concurrents de la méthode .move().
Il nous reste maintenant à générer la méthode .move() qui meut le pendule. Pour cela vous pouvez vous inspirer de
la rubrique Un canvas animé dans une classe du chapitre 25 Fenêtres graphiques et Tkinter (en ligne).
Cette méthode va réaliser un pas de simulation de t à t + δ t. Il faudra ainsi réaliser dans l’ordre :
19. [Link]
• Calculer la nouvelle valeur de θ ([Link]) au pas t + δ t comme nous l’avons fait précédemment avec la
méthode semi-implicite d’Euler.
• Convertir la nouvelle valeur de θ ([Link]) en coordonnées cartésiennes dans le repère du pendule (self.x
et self.y).
• Convertir ces coordonnées cartésiennes dans le repère du Canvas (self.x_c et self.y_c).
• Mettre à jour le dessin de la baballe et de la tige avec la méthode [Link]().
• Incrémenter le pas de temps.
• Si le drapeau self.is_moving est supérieur à 0, la méthode [Link]() est rappelée après 20 millisecondes
(Conseil : la méthode .after() est votre amie).
Remarque
• Prenez le temps de chercher par vous-même avant de télécharger les scripts de correction.
• Nous proposons une correction. D’autres solutions sont possibles.
20. [Link]
21. [Link]
22. [Link]
23. [Link]
24. [Link]
A.1 FASTA
Le format FASTA est utilisé pour stocker une ou plusieurs séquences, d’ADN, d’ARN ou de protéines. Ces séquences
sont classiquement représentées sous la forme :
>en-tête
séquence avec un nombre maximum de caractères par ligne
séquence avec un nombre maximum de caractères par ligne
séquence avec un nombre maximum de caractères par ligne
séquence avec un nombre maximum de caractères par ligne
séquence avec un nombre max
La première ligne débute par le caractère > et contient une description de la séquence. On appelle souvent cette ligne
« ligne de description » ou « ligne de commentaire ».
Les lignes suivantes contiennent la séquence à proprement dite, mais avec un nombre maximum fixe de caractères
par ligne. Ce nombre maximum est généralement fixé à 60, 70 ou 80 caractères. Une séquence de plusieurs centaines de
bases ou de résidus est donc répartie sur plusieurs lignes.
Un fichier est dit multifasta lorsqu’il contient plusieurs séquences au format FASTA, les unes à la suite des autres.
Les fichiers contenant une ou plusieurs séquences au format FASTA portent la plupart du temps l’extension .fasta
mais on trouve également .seq, .fas, .fna ou .faa.
A.1.1 Exemples
La séquence protéique au format FASTA de l’insuline humaine 1 , extraite de la base de données UniProt, est :
>sp|P01308|INS_HUMAN Insulin OS=Homo sapiens OX=9606 GN=INS PE=1 SV=1
MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAED
LQVGQVELGGGPGAGSLQPLALEGSLQKRGIVEQCCTSICSLYQLENYCN
La première ligne contient la description de la séquence (Insulina), le type de base de données (ici, sp qui signifie Swiss-
Prot), son identifiant (P01308) et son nom (INS_HUMAN) dans cette base de données, ainsi que d’autres informations
(OS=Homo sapiens OX=9606 GN=INS PE=1 SV=1B).
Les lignes suivantes contiennent la séquence sur des lignes ne dépassant pas, ici, 60 caractères. La séquence de
l’insuline humaine est composée de 110 acides aminés, soit une ligne de 60 caractères et une seconde de 50 caractères.
1. [Link]
366
A.1. FASTA Annexe A. Quelques formats de données en biologie
Définition
UniProt 2 est une base de données de séquences de protéines. Ces séquences proviennent elles-mêmes de deux autres
bases de données : Swiss-Prot (où les séquences sont annotées manuellement) et TrEMBL (où les séquences sont annotées
automatiquement).
Voici maintenant la séquence nucléique (ARN), au format FASTA, de l’insuline humaine 3 , extraite de la base de
données GenBank 4 :
>BT006808.1 Homo sapiens insulin mRNA, complete cds
ATGGCCCTGTGGATGCGCCTCCTGCCCCTGCTGGCGCTGCTGGCCCTCTGGGGACCTGACCCAGCCGCAG
CCTTTGTGAACCAACACCTGTGCGGCTCACACCTGGTGGAAGCTCTCTACCTAGTGTGCGGGGAACGAGG
CTTCTTCTACACACCCAAGACCCGCCGGGAGGCAGAGGACCTGCAGGTGGGGCAGGTGGAGCTGGGCGGG
GGCCCTGGTGCAGGCAGCCTGCAGCCCTTGGCCCTGGAGGGGTCCCTGCAGAAGCGTGGCATTGTGGAAC
AATGCTGTACCAGCATCTGCTCCCTCTACCAGCTGGAGAACTACTGCAACTAG
On retrouve sur la première ligne la description de la séquence (Homo sapiens insulin mRNA), ainsi que son identifiant
(BT006808.1) dans la base de données GenBank.
Les lignes suivantes contiennent les 333 bases de la séquence, réparties sur cinq lignes de 70 caractères maximum. Il
est curieux de trouver la base T (thymine) dans une séquence d’ARN, qui ne devrait contenir normalement que les bases
A, U, G et C. Ici, la représentation d’une séquence d’ARN avec les bases de l’ADN est une convention.
Pour terminer, voici trois séquences protéiques, au format FASTA, qui correspondent à l’insuline humaine (Homo
sapiens), féline (Felis catus) et bovine (Bos taurus) :
>sp|P01308|INS_HUMAN Insulin OS=Homo sapiens OX=9606 GN=INS PE=1 SV=1
MALWMRLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAED
LQVGQVELGGGPGAGSLQPLALEGSLQKRGIVEQCCTSICSLYQLENYCN
>sp|P06306|INS_FELCA Insulin OS=Felis catus OX=9685 GN=INS PE=1 SV=2
MAPWTRLLPLLALLSLWIPAPTRAFVNQHLCGSHLVEALYLVCGERGFFYTPKARREAED
LQGKDAELGEAPGAGGLQPSALEAPLQKRGIVEQCCASVCSLYQLEHYCN
>sp|P01317|INS_BOVIN Insulin OS=Bos taurus OX=9913 GN=INS PE=1 SV=2
MALWTRLRPLLALLALWPPPPARAFVNQHLCGSHLVEALYLVCGERGFFYTPKARREVEG
PQVGALELAGGPGAGGLEGPPQKRGIVEQCCASVCSLYQLENYCN
Ces séquences proviennent de la base de données UniProt 5 . Chaque séquence est délimitée par la ligne d’en-tête qui
débute par >.
Pour chaque séquence lue dans le fichier FASTA, on affiche son identifiant et son nom, puis les 30 premiers résidus
de sa séquence :
2. [Link]
3. [Link]
4. [Link]
5. [Link]
sp|P06306|INS_FELCA
MAPWTRLLPLLALLSLWIPAPTRAFVNQHL
sp|P01317|INS_BOVIN
MALWTRLRPLLALLALWPPPPARAFVNQHL
sp|P01308|INS_HUMAN
MALWMRLLPLLALLALWGPDPAAAFVNQHL
Notez que les protéines sont stockées dans un dictionnaire (prot_dict) où les clefs sont les identifiants et les valeurs
les séquences.
On peut faire la même chose avec le module Biopython :
1 from Bio import SeqIO
2 with open("[Link]", "r") as fasta_file:
3 for record in [Link](fasta_file, "fasta"):
4 print([Link])
5 print(str([Link])[:30])
Cela produit le même résultat. L’utilisation de Biopython rend le code plus compacte car on utilise ici la fonction
[Link]() qui s’occupe de lire le fichier FASTA.
Remarque
L’attribut .id renvoie l’identifiant d’une séquence, c’est-à-dire la première partie de l’entête, sans le caractère >. Pour
obtenir l’entête complet (toujours sans le caractère >), il faut utiliser l’attribut .description.
A.2 GenBank
GenBank est une banque de séquences nucléiques. Le format de fichier associé contient l’information nécessaire pour
décrire un gène ou une portion d’un génome. Les fichiers GenBank portent le plus souvent l’extension .gbk.
Le format GenBank est décrit de manière très complète sur le site du NCBI 6 . En voici néanmoins les principaux
éléments, avec l’exemple du gène qui code pour la trypsine 7 chez l’Homme.
A.2.1 L’en-tête
• Ligne 1 (LOCUS) : le nom du locus (HUMTRPSGNA), la taille du gène (800 paires de bases), le type de molécule
(ARN messager).
• Ligne 3 (ACCESSION) : l’identifiant de la séquence (M22612).
• Ligne 4 (VERSION) : la version de la séquence (M22612.1). Le nombre qui est séparé de l’identifiant de la séquence
par un point est incrémenté pour chaque nouvelle version de la fiche GenBank. Ici, .1 indique que nous en sommes
à la première version.
• Ligne 6 (SOURCE) : la provenance de la séquence (souvent l’organisme d’origine).
• Ligne 7 (ORGANISME) : le nom scientifique de l’organisme, suivi de sa taxonomie (lignes 8 à 10).
6. [Link]
7. [Link]
[...]
FEATURES Location/Qualifiers
source 1..800
/organism="Homo sapiens"
/mol_type="mRNA"
/db_xref="taxon:9606"
/map="7q32-qter"
/tissue_type="pancreas"
gene 1..800
/gene="TRY1"
CDS 7..750
/gene="TRY1"
/codon_start=1
/product="trypsinogen"
/protein_id="AAA61231.1"
/db_xref="GDB:G00-119-620"
/translation="MNPLLILTFVAAALAAPFDDDDKIVGGYNCEENSVPYQVSLNSG
YHFCGGSLINEQWVVSAGHCYKSRIQVRLGEHNIEVLEGNEQFINAAKIIRHPQYDRK
TLNNDIMLIKLSSRAVINARVSTISLPTAPPATGTKCLISGWGNTASSGADYPDELQC
LDAPVLSQAKCEASYPGKITSNMFCVGFLEGGKDSCQGDSGGPVVCNGQLQGVVSWGD
GCAQKNKPGVYTKVYNYVKWIKNTIAANS"
sig_peptide 7..51
/gene="TRY1"
/note="G00-119-620"
[...]
• Ligne 9 (gene 1..800) : la délimitation du gène. Ici, de la base 1 à la base 800. Par ailleurs, la notation <x..y
indique que la séquence est partielle sur l’extrémité 5’. Réciproquement, x..y> indique que la séquence est partielle
sur l’extrémité 3’. Enfin, pour les séquences d’ADN, la notation complement(x..y) indique que le gène se trouve
de la base x à la base y, mais sur le brin complémentaire.
• Ligne 10 (/gene="TRY1") : le nom du gène.
• Ligne 11 (CDS 7..750) : la délimitation de la séquence codante.
• Ligne 14 (/product="trypsinogen") : le nom de la protéine produite.
• Lignes 17 à 20 (/translation="MNPLLIL...) : la séquence protéique issue de la traduction de la séquence
codante.
• Ligne 22 (sig_peptide 7..51) : la délimitation du peptide signal.
A.2.3 La séquence
[...]
ORIGIN
1 accaccatga atccactcct gatccttacc tttgtggcag ctgctcttgc tgcccccttt
61 gatgatgatg acaagatcgt tgggggctac aactgtgagg agaattctgt cccctaccag
121 gtgtccctga attctggcta ccacttctgt ggtggctccc tcatcaacga acagtgggtg
181 gtatcagcag gccactgcta caagtcccgc atccaggtga gactgggaga gcacaacatc
241 gaagtcctgg aggggaatga gcagttcatc aatgcagcca agatcatccg ccacccccaa
301 tacgacagga agactctgaa caatgacatc atgttaatca agctctcctc acgtgcagta
361 atcaacgccc gcgtgtccac catctctctg cccaccgccc ctccagccac tggcacgaag
421 tgcctcatct ctggctgggg caacactgcg agctctggcg ccgactaccc agacgagctg
481 cagtgcctgg atgctcctgt gctgagccag gctaagtgtg aagcctccta ccctggaaag
541 attaccagca acatgttctg tgtgggcttc cttgagggag gcaaggattc atgtcagggt
601 gattctggtg gccctgtggt ctgcaatgga cagctccaag gagttgtctc ctggggtgat
661 ggctgtgccc agaagaacaa gcctggagtc tacaccaagg tctacaacta cgtgaaatgg
721 attaagaaca ccatagctgc caatagctaa agcccccagt atctcttcag tctctatacc
781 aataaagtga ccctgttctc
//
La séquence est contenue entre les balises ORIGIN (ligne 2) et // (ligne 17).
Chaque ligne est composée d’une série d’espaces, puis du numéro du premier nucléotide de la ligne, puis d’au plus
6 blocs de 10 nucléotides. Chaque bloc est précédé d’un espace. Par exemple, ligne 10, le premier nucléotide de la ligne
(t) est le numéro 421 dans la séquence.
À partir de l’exemple précédent, voici comment lire un fichier GenBank avec Python et le module Biopython :
Pour la séquence lue dans le fichier GenBank, on affiche son identifiant, sa description et les 60 premiers résidus :
M22612.1
Human pancreatic trypsin 1 (TRY1) mRNA, complete cds.
ACCACCATGAATCCACTCCTGATCCTTACCTTTGTGGCAGCTGCTCTTGCTGCCCCCTTT
Il est également possible de lire un fichier GenBank sans le module Biopython. Une activité dédiée est proposée dans
le chapitre 27 Mini-projets (en ligne).
A.3 PDB
La Protein Data Bank 8 (PDB) est une banque de données qui contient les structures de biomacromolécules (protéines,
ADN, ARN, virus…). Historiquement, le format de fichier qui y est associé est le PDB, dont une documentation détaillée
est disponible sur le site éponyme 9 . Les extensions de fichier pour ce format de données sont .ent et surtout .pdb.
Un fichier PDB est constitué de deux parties principales : l’en-tête et les coordonnées.
• L’en-tête est lisible et utilisable par un être humain (comme par une machine).
• À l’inverse, les coordonnées sont surtout utilisables par un programme pour calculer certaines propriétés de la struc-
ture ou simplement la représenter sur l’écran d’un ordinateur. Bien sûr, un utilisateur expérimenté peut parfaitement
jeter un œil à cette seconde partie.
A.3.1 En-tête
Pour la trypsine bovine, l’en-tête compte 510 lignes. En voici quelques unes :
8. [Link]
9. [Link]
10. [Link]
A.3.2 Coordonnées
Avec la même protéine, la partie coordonnées représente plus de 1 700 lignes. En voici quelques unes correspondantes
au résidu leucine 99 :
11. [Link]
[...]
ATOM 601 N LEU A 99 10.007 19.687 17.536 1.00 12.25 N
ATOM 602 CA LEU A 99 9.599 18.429 18.188 1.00 12.25 C
ATOM 603 C LEU A 99 10.565 17.281 17.914 1.00 12.25 C
ATOM 604 O LEU A 99 10.256 16.101 18.215 1.00 12.25 O
ATOM 605 CB LEU A 99 8.149 18.040 17.853 1.00 12.25 C
ATOM 606 CG LEU A 99 7.125 19.029 18.438 1.00 18.18 C
ATOM 607 CD1 LEU A 99 5.695 18.554 18.168 1.00 18.18 C
ATOM 608 CD2 LEU A 99 7.323 19.236 19.952 1.00 18.18 C
[...]
Chaque ligne correspond à un atome et débute par ATOM ou HETATM. ATOM désigne un atome de la structure de la
biomolécule. HETATM est utilisé pour les atomes qui ne sont pas une biomolécule, comme les ions ou les molécules d’eau.
Toutes les lignes de coordonnées ont sensiblement le même format. Par exemple, pour la première ligne :
• ATOM (ou HETATM).
• 601 : le numéro de l’atome.
• N : le nom de l’atome. Ici, un atome d’azote du squelette peptidique. La structure complète du résidu leucine est
représentée figure A.1.
• LEU : le résidu dont fait partie l’atome. Ici, une leucine.
• A : le nom de la chaîne peptidique.
• 99 : le numéro du résidu dans la protéine.
• 10.007 : la coordonnée x de l’atome.
• 19.687 : la coordonnée y de l’atome.
• 17.536 : la coordonnée z de l’atome.
• 1.00 : le facteur d’occupation, c’est-à-dire la probabilité de trouver l’atome à cette position dans l’espace en
moyenne. Cette probabilité est inférieure à 1 lorsque, expérimentalement, on n’a pas pu déterminer avec une totale
certitude la position de l’atome. Par exemple, dans le cas d’un atome très mobile dans une structure, qui est
déterminé comme étant à deux positions possibles, chaque position aura alors la probabilité 0.50.
• 12.25 : le facteur de température, qui est proportionnel à la mobilité de l’atome dans l’espace. Les atomes situés
en périphérie d’une structure sont souvent plus mobiles que ceux situés au coeur de la structure.
• N : l’élément chimique de l’atome. Ici, l’azote.
Une documentation plus complète des différents champs qui constituent une ligne de coordonnées atomiques se trouve
sur le site de la PDB 12 .
Les résidus sont ensuite décrits les uns après les autres, atome par atome. Voici par exemple les premiers résidus de
la trypsine bovine :
[...]
ATOM 1 N ILE A 16 -8.155 9.648 20.365 1.00 10.68 N
ATOM 2 CA ILE A 16 -8.150 8.766 19.179 1.00 10.68 C
ATOM 3 C ILE A 16 -9.405 9.018 18.348 1.00 10.68 C
ATOM 4 O ILE A 16 -10.533 8.888 18.870 1.00 10.68 O
ATOM 5 CB ILE A 16 -8.091 7.261 19.602 1.00 10.68 C
ATOM 6 CG1 ILE A 16 -6.898 6.882 20.508 1.00 7.42 C
ATOM 7 CG2 ILE A 16 -8.178 6.281 18.408 1.00 7.42 C
ATOM 8 CD1 ILE A 16 -5.555 6.893 19.773 1.00 7.42 C
ATOM 9 N VAL A 17 -9.224 9.305 17.090 1.00 9.63 N
ATOM 10 CA VAL A 17 -10.351 9.448 16.157 1.00 9.63 C
ATOM 11 C VAL A 17 -10.500 8.184 15.315 1.00 9.63 C
ATOM 12 O VAL A 17 -9.496 7.688 14.748 1.00 9.63 O
ATOM 13 CB VAL A 17 -10.123 10.665 15.222 1.00 9.63 C
ATOM 14 CG1 VAL A 17 -11.319 10.915 14.278 1.00 11.95 C
ATOM 15 CG2 VAL A 17 -9.737 11.970 15.970 1.00 11.95 C
[...]
Vous remarquerez que le numéro du premier résidu est 16 et non pas 1. Cela s’explique par la technique expérimentale
utilisée qui n’a pas permis de déterminer la structure des 15 premiers résidus.
La structure de la trypsine bovine n’est constituée que d’une seule chaîne peptidique (notée A). Lorsqu’une structure
est composée de plusieurs chaînes, comme dans le cas de la structure du récepteur GABAB 1 et 2 chez la drosophile
12. [Link]
Figure A.1 – Structure tridimensionnelle d’un résidu leucine. Les noms des atomes sont indiqués en noir.
[...]
ATOM 762 HB1 ALA A 44 37.162 -2.955 2.220 1.00 0.00 H
ATOM 763 HB2 ALA A 44 38.306 -2.353 3.417 1.00 0.00 H
ATOM 764 HB3 ALA A 44 38.243 -1.621 1.814 1.00 0.00 H
TER 765 ALA A 44
ATOM 766 N GLY B 95 -18.564 3.009 13.772 1.00 0.00 N
ATOM 767 CA GLY B 95 -19.166 3.646 12.621 1.00 0.00 C
ATOM 768 C GLY B 95 -20.207 2.755 11.976 1.00 0.00 C
[...]
La première chaîne est notée A et la seconde B. La séparation entre les deux chaînes est marquée par la ligne :
Dans un fichier PDB, chaque structure porte un nom de chaîne différent (par exemple : A,B,C‘, etc.).
Enfin, lorsque la structure est déterminée par RMN, il est possible que plusieurs structures soient présentes dans le
même fichier PDB. Toutes ces structures, ou « modèles », sont des solutions possibles du jeu de contraintes mesurées
expérimentalement en RMN. Voici un exemple, toujours pour la structure du récepteur GABAB 1 et 2 chez la drosophile :
13. [Link]
[...]
MODEL 1
ATOM 1 N MET A 1 -27.283 -9.772 5.388 1.00 0.00 N
ATOM 2 CA MET A 1 -28.233 -8.680 5.682 1.00 0.00 C
[...]
ATOM 1499 HG2 GLU B 139 36.113 -5.242 2.536 1.00 0.00 H
ATOM 1500 HG3 GLU B 139 37.475 -4.132 2.428 1.00 0.00 H
TER 1501 GLU B 139
ENDMDL
MODEL 2
ATOM 1 N MET A 1 -29.736 -10.759 4.394 1.00 0.00 N
ATOM 2 CA MET A 1 -28.372 -10.225 4.603 1.00 0.00 C
[...]
ATOM 1499 HG2 GLU B 139 36.113 -5.242 2.536 1.00 0.00 H
ATOM 1500 HG3 GLU B 139 37.475 -4.132 2.428 1.00 0.00 H
TER 1501 GLU B 139
ENDMDL
MODEL 2
ATOM 1 N MET A 1 -29.736 -10.759 4.394 1.00 0.00 N
ATOM 2 CA MET A 1 -28.372 -10.225 4.603 1.00 0.00 C
[...]
et :
ENDMDL
où n est le numéro du modèle. Pour la structure du récepteur GABAB 1 et 2, il y a 20 modèles de décrits dans le
fichier PDB.
Remarque
Les fichiers PDB sont parfois (très) mal formatés. Si Biopython ne parvient pas à lire un tel fichier, remplacez alors
la ligne 2 par parser = PDBParser(PERMISSIVE=1). Soyez néanmoins très prudent quant aux résultats obtenus.
ce qui produit :
1 hydrolase (serine proteinase)
2 x-ray diffraction
1 model = structure[0]
2 chain = model["A"]
3 res1 = chain[16]
4 res2 = chain[17]
5 print([Link], res1["N"].coord)
6 print([Link], res2["CA"].coord)
ce qui produit :
1 ILE [ -8.15499973 9.64799976 20.36499977]
2 VAL [-10.35099983 9.44799995 16.15699959]
L’objet res1["N"].coord est un array de NumPy (voir le chapitre 20 Module NumPy). On peut alors obtenir
simplement les coordonnées x, y et z d’un atome :
1 print(res1["N"].coord[0], res1["N"].coord[1], res1["N"].coord[2])
ce qui produit :
1 -8.155 9.648 20.365
Remarque
Biopython utilise la hiérarchie suivante :
structure > model > chain > residue > atom
même lorsque la structure ne contient qu’un seul modèle. C’est d’ailleurs le cas ici, puisque la structure a été obtenue
par cristallographie aux rayons X.
Enfin, pour afficher les coordonnées des carbones α (notés CA) des 10 premiers résidus (à partir du résidu 16, car
c’est le premier résidu dont on connaît la structure) :
1 res_start = 16
2 model = structure[0]
3 chain = model["A"]
4 for i in range(10):
5 idx = res_start + i
6 print(chain[idx].resname, idx, chain[idx]["CA"].coord)
Il est aussi très intéressant (et formateur) d’écrire son propre parser de fichier PDB, c’est-à-dire un programme qui
lit un fichier PDB (sans le module Biopython). Dans ce cas, la figure A.2 vous aidera à déterminer comment extraire les
différentes informations d’une ligne de coordonnées ATOM ou HETATM.
Exemple : pour extraire le nom du résidu, il faut isoler le contenu des colonnes 18 à 20 du fichier PDB, ce qui
correspond aux index de 17 à 19 pour une chaîne de caractères en Python (soit la tranche de chaîne de caractères
[17:20], car la première borne est incluse et la seconde exclue).
Pour lire le fichier PDB de la trypsine bovine ([Link]) et extraire (encore) les coordonnées des carbones α des 10
premiers résidus, nous pouvons utiliser le code suivant :
ce qui donne :
ILE 16 -8.15 8.766 19.179
VAL 17 -10.351 9.448 16.157
GLY 18 -12.021 6.63 14.259
GLY 19 -10.902 3.899 16.684
TYR 20 -12.651 1.442 19.016
THR 21 -13.018 0.938 22.76
CYS 22 -10.02 -1.163 23.76
GLY 23 -11.683 -2.865 26.714
ALA 24 -10.648 -2.627 30.361
ASN 25 -6.97 -3.437 31.02
Remarque
Pour extraire des valeurs numériques, comme des numéros de résidus ou des coordonnées atomiques, il ne faudra pas
oublier de les convertir en entiers ou en floats.
A.4.1 XML
Le format XML est un format de fichier à balises qui permet de stocker quasiment n’importe quel type d’information
de façon structurée et hiérarchisée. L’acronyme XML signifie Extensible Markup Language qui pourrait se traduire en
français par « Langage de balisage extensible 14 ». Les balises dont il est question servent à délimiter du contenu :
<balise>contenu</balise>
La balise <balise> est une balise ouvrante. La balise </balise> est une balise fermante. Notez le caractère / qui
marque la différence entre une balise ouvrante et une balise fermante.
Il existe également des balises vides, qui sont à la fois ouvrantes et fermantes :
<balise />
Une balise peut avoir certaines propriétés, appelées attributs, qui sont définies, dans la balise ouvrante. Par exemple :
<balise propriété1=valeur1 propriété2=valeur2>contenu</balise>
Un attribut est un couple nom et valeur (par exemple propriété1 est un nom et valeur1 est la valeur associée).
Enfin, les balises peuvent être imbriquées les unes dans les autres :
14. [Link]
<protein>
<element>élément 1</element>
<element>élément 2</element>
<element>élément 3</element>
</protein>
Dans cet exemple, nous avons trois balises element qui sont contenues dans une balise protein.
Voici un autre exemple avec l’enzyme trypsine 15 humaine (code P07477 16 ), telle qu’on peut la trouver décrite dans
la base de données UniProt :
<?xml version='1.0' encoding='UTF-8'?>
<uniprot xmlns="[Link] xmlns:xsi=[...]>
<entry dataset="Swiss-Prot" created="1988-04-01" modified="2018-09-12" [...]>
<accession>P07477</accession>
<accession>A1A509</accession>
[...]
<gene>
<name type="primary">PRSS1</name>
<name type="synonym">TRP1</name>
<name type="synonym">TRY1</name>
</gene>
[...]
<sequence length="247" mass="26558" checksum="DD49A487B8062813" [...]>
MNPLLILTFVAAALAAPFDDDDKIVGGYNCEENSVPYQVSLNSGYHFCGGSLINEQWVVS
AGHCYKSRIQVRLGEHNIEVLEGNEQFINAAKIIRHPQYDRKTLNNDIMLIKLSSRAVIN
ARVSTISLPTAPPATGTKCLISGWGNTASSGADYPDELQCLDAPVLSQAKCEASYPGKIT
SNMFCVGFLEGGKDSCQGDSGGPVVCNGQLQGVVSWGDGCAQKNKPGVYTKVYNYVKWIK
NTIAANS
</sequence>
</entry>
[...]
</uniprot>
• Ligne 1. On utilise le sous-module etree du module lxml pour lire le fichier XML.
15. [Link]
16. [Link]
• Ligne 2. On utilise le module d’expressions régulières re pour supprimer tous les attributs de la balise uniprot
(ligne 7). Nous ne rentrerons pas dans les détails, mais ces attributs rendent plus complexe la lecture du fichier
XML.
• Ligne 9. La variable root contient le fichier XML prêt à être manipulé.
• Ligne 11. On recherche les noms des gènes (balises <name></name>) associés à la trypsine. Pour cela, on utilise
la méthode .xpath(), avec comme argument l’enchaînement des différentes balises qui conduisent aux noms des
gènes.
• Ligne 12. Pour chaque nom de gène, on va afficher son contenu ([Link]) et la valeur associée à l’attribut
type avec la méthode .get("type").
• Ligne 14. On stocke dans la variable sequence la balise associée à la séquence de la protéine. Comme root.
xpath("/uniprot/entry/sequence") renvoie un itérateur et qu’il n’y a qu’une seule balise séquence, on prend
ici le seul et unique élément [Link]("/uniprot/entry/sequence")[0].
• Ligne 15. On affiche le contenu de la séquence [Link], nettoyé d’éventuels retours chariots ou espaces
[Link]().
• Ligne 16. On affiche la taille de la séquence en récupérant la valeur de l’attribut length (toujours de la balise
<sequence></sequence>).
Le résultat obtenu est le suivant :
gene : PRSS1 (primary)
gene : TRP1 (synonym)
gene : TRY1 (synonym)
gene : TRYP1 (synonym)
sequence: MNPLLILTFVAAALAAPFDDDDKIVGGYNCEENSVPYQVSLNSGYHFCGGSLINEQWVVS
AGHCYKSRIQVRLGEHNIEVLEGNEQFINAAKIIRHPQYDRKTLNNDIMLIKLSSRAVIN
ARVSTISLPTAPPATGTKCLISGWGNTASSGADYPDELQCLDAPVLSQAKCEASYPGKIT
SNMFCVGFLEGGKDSCQGDSGGPVVCNGQLQGVVSWGDGCAQKNKPGVYTKVYNYVKWIK
NTIAANS
length: 247
L’acronyme CSV signifie « Comma-Separated values », qu’on peut traduire littéralement par « valeurs séparées par
des virgules ». De façon similaire, TSV signifie « Tabulation-Separated Values », soit des « valeurs séparées par des
tabulations ».
Ces deux formats sont utiles pour stocker des données structurées sous forme de tableau, comme vous pourriez l’avoir
dans un tableur.
À titre d’exemple, le tableau ci-dessous liste les structures associées à la transferrine, protéine présente dans le plasma
sanguin et impliquée dans la régulation du fer. Ces données proviennent de la Protein Data Bank (PDB). Pour chaque
protéine (PDB ID) est indiqué le nom de l’organisme associé (Source), la date à laquelle cette structure a été déposée
dans la PDB (Deposit Date), le nombre d’acides aminés de la protéine et sa masse moléculaire (MW ).
17. [Link]
Sur chaque ligne, les différentes valeurs sont séparées par une virgule. La première ligne contient le nom des colonnes
et est appelée ligne d’en-tête.
L’équivalent en TSV 18 est :
PDB ID Source Deposit Date Length MW
1A8E Homo sapiens 1998-03-24 329 36408.40
1A8F Homo sapiens 1998-03-25 329 36408.40
1AIV Gallus gallus 1997-04-28 686 75929.00
1AOV Anas platyrhynchos 1996-12-11 686 75731.80
[...]
Sur chaque ligne, les différentes valeurs sont séparées par une tabulation.
Attention
Le caractère tabulation est un caractère invisible « élastique », c’est-à-dire qu’il a une largeur variable suivant l’éditeur
de texte utilisé. Par exemple, dans la ligne d’en-tête, l’espace entre PDB ID et Source apparaît comme différent de
l’espace entre Deposit Date et Length alors qu’il y a pourtant une seule tabulation à chaque fois.
A.4.2.2 Lecture
En Python, le module csv de la bibliothèque standard est très pratique pour lire et écrire des fichiers au format CSV
et TSV. Nous vous conseillons de lire la documentation très complète sur ce module 19 .
Voici un exemple :
1 import csv
2
3 with open("transferrin_report.csv") as f_in:
4 f_reader = [Link](f_in)
5 for row in f_reader:
6 print(row["PDB ID"], row["Deposit Date"], row["Length"])
18. [Link]
19. [Link]
1 import csv
2
3 with open("transferrin_PDB_report.tsv") as f_in:
4 f_reader = [Link](f_in, delimiter="\t")
5 for row in f_reader:
6 print(row["PDB ID"], row["Deposit Date"], row["Length"])
A.4.2.3 Écriture
De façon très similaire, l’écriture d’un fichier TSV est réalisée avec le code suivant :
1 import csv
2
3 with open("[Link]", "w") as f_out:
4 fields = ["Name", "Quantity"]
5 f_writer = [Link](f_out, fieldnames=fields, delimiter="\t")
6 f_writer.writeheader()
7 f_writer.writerow({"Name": "girafe", "Quantity":5})
8 f_writer.writerow({"Name": "tigre", "Quantity":3})
9 f_writer.writerow({"Name": "singe", "Quantity":8})
Vous êtes désormais capables de lire et écrire des fichiers aux formats CSV et TSV. Les codes que nous vous avons
proposés ne sont que des exemples. À vous de poursuivre l’exploration du module csv.
Remarque
Le module pandas décrit dans le chapitre 22 Module Pandas est tout à fait capable de lire et écrire des fichiers CSV
et TSV. Nous vous conseillons de l’utiliser si vous analysez des données avec ces types de fichiers.
Installation de Python
Attention
La procédure d’installation ci-dessous a été testée avec la version Miniconda Latest - Conda 24.5.0 Python
3.12.4 released Jun 26, 2024.
Python est déjà présent sous Linux ou Mac OS X et s’installe très facilement sous Windows. Toutefois, nous décri-
vons dans cet ouvrage l’utilisation de modules supplémentaires qui sont très utiles en bioinformatique (NumPy, scipy,
matplotlib, pandas, Biopython), mais également les notebooks Jupyter.
On va donc utiliser un gestionnaire de paquets qui va installer ces modules supplémentaires. On souhaite également que
ce gestionnaire de paquets soit disponible pour Windows, Mac OS X et Linux. Fin 2018, il y a deux grandes alternatives :
1. Anaconda et Miniconda : Anaconda 1 est une distribution complète de Python qui contient un gestionnaire de
paquets très puissant nommé conda. Anaconda installe de très nombreux paquets et outils mais nécessite un espace
disque de plusieurs gigaoctets. Miniconda 2 est une version allégée d’Anaconda, donc plus rapide à installer et
occupant peu d’espace sur le disque dur. Le gestionnaire de paquet conda est aussi présent dans Miniconda.
2. Pip : pip 3 est le gestionnaire de paquets de Python et qui est systématiquement présent depuis la version 3.4.
1. [Link]
2. [Link]
3. [Link]
383
Annexe B. Installation de Python B.2. Installation de Python avec Miniconda
Depuis quelques années, Windows 10 (et 11) propose le WSL 4 (Windows Subsystem for Linux). Le WSL permet de
lancer un terminal Linux au sein de Windows et propose (quasiment) toutes les fonctionnalités disponibles sous un vrai
système Linux. Nous ne détaillons par comment l’installer, mais vous pouvez vous référer à la page d’installation sur le site
de Microsoft 5 . Si vous avez installé WSL sur votre ordinateur, nous vous recommandons de suivre la procédure ci-dessous
comme si vous étiez sous Linux (rubrique Installation de Python avec Miniconda pour Linux), plutôt que d’installer la
version Windows.
signifie l’invite d’un shell quel qu’il soit (PowerShell sous Windows, bash sous Mac OS X et Linux).
Comme demandé, appuyez sur la touche Entrée. Faites ensuite défiler la licence d’utilisation avec la touche Espace.
Tapez yes puis appuyez sur la touche Entrée pour valider :
Do you accept the license terms? [yes|no]
[no] >>> yes
Le programme d’installation vous propose ensuite d’installer Miniconda dans le répertoire miniconda3 dans votre
répertoire personnel. Par exemple, dans le répertoire /home/pierre/miniconda3 si votre nom d’utilisateur est pierre.
Validez cette proposition en appuyant sur la touche Entrée :
Miniconda3 will now be installed into this location:
/home/pierre/miniconda3
[/home/pierre/miniconda3] >>>
L’installation de Miniconda est terminée. L’espace utilisé par Miniconda sur votre disque dur est d’environ 450 Mo.
Ouvrez un nouveau shell. Vous devriez voir dans votre invite la chaîne (base) indiquant que l’environnement conda
de base est activé. À partir de maintenant, lorsque vous taperez la commande python, c’est le Python 3 de Miniconda
qui sera lancé :
$ python
Python 3.12.4 | packaged by Anaconda, Inc. | (main, Jun 18 2024, 15:12:24) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
Quittez Python en tapant la commande exit() puis appuyant sur la touche Entrée.
De retour dans le shell, testez si le gestionnaire de paquets conda est fonctionnel. Tapez la commande conda dans
le shell, vous devriez avoir la sortie suivante :
$ conda
usage: conda [-h] [-v] [--no-plugins] [-V] COMMAND ...
conda is a tool for managing and deploying applications, environments and packages.
options:
-h, --help Show this help message and exit.
[...]
Si c’est bien le cas, bravo, conda et bien installé et vous pouvez passez à la suite (rendez-vous à la rubrique Installation
des modules supplémentaires) !
Si vous souhaitez supprimer Miniconda, rien de plus simple, il suffit de suivre ces deux étapes :
Étape 1. Supprimer le répertoire de Miniconda. Par exemple pour l’utilisateur pierre :
$ rm -rf /home/pierre/miniconda3
Étape 2. Dans le fichier de configuration du shell Bash, supprimer les lignes comprises entre
# >>> conda initialize >>>
et
# <<< conda initialize <<<
puis suivez les mêmes instructions que dans la rubrique précédente (la seule petite subtilité est pour le chemin,
choisissez /User/votre_nom_utilisateur/miniconda3 sous Mac au lieu de /home/votre_nom_utilisateur/
miniconda3 sous Linux).
Attention
Nous partons du principe qu’aucune version d’Anaconda, Miniconda, ou encore de Python « classique » (obtenue sur
le site officiel de Python 7 ) n’est installée sur votre ordinateur. Si tel est le cas, nous vous recommandons vivement de la
désinstaller pour éviter des conflits de version.
7. [Link]
Il nous faut maintenant initialiser conda. Cette manipulation va permettre de le rendre visible dans n’importe quel
shell Powershell.
L’installateur a en principe ajouté des nouveaux raccourcis dans le Menu Démarrer contenant le mot Anaconda :
• Anaconda Powershell Prompt (Miniconda3) : pour lancer un shell Powershell (shell standard de Windows
équivalent du bash sous Linux) avec conda qui est activé correctement ;
• Anaconda Prompt (Miniconda3) : même chose mais avec le shell nommé cmd ; ce vieux shell est limité et nous
vous en déconseillons l’utilisation.
Nous allons maintenant initialiser conda « à la main ». Cliquez sur Anaconda Powershell Prompt (Miniconda3)
qui va lancer un Powershell avec conda activé, puis tapez la commande conda init :
Lorsque vous presserez la touche Entrée vous obtiendrez une sortie de ce style :
$ conda init
no change C:\Users\Pat\miniconda3\Scripts\[Link]
no change C:\Users\Pat\miniconda3\Scripts\[Link]
no change C:\Users\Pat\miniconda3\Scripts\[Link]
no change C:\Users\Pat\miniconda3\Scripts\[Link]
no change C:\Users\Pat\miniconda3\condabin\[Link]
no change C:\Users\Pat\miniconda3\Library\bin\[Link]
no change C:\Users\Pat\miniconda3\condabin\_conda_activate.bat
no change C:\Users\Pat\miniconda3\condabin\rename_tmp.bat
no change C:\Users\Pat\miniconda3\condabin\conda_auto_activate.bat
no change C:\Users\Pat\miniconda3\condabin\conda_hook.bat
no change C:\Users\Pat\miniconda3\Scripts\[Link]
no change C:\Users\Pat\miniconda3\condabin\[Link]
no change C:\Users\Pat\miniconda3\condabin\[Link]
modified C:\Users\Pat\miniconda3\Scripts\activate
modified C:\Users\Pat\miniconda3\Scripts\deactivate
modified C:\Users\Pat\miniconda3\etc\profile.d\[Link]
modified C:\Users\Pat\miniconda3\etc\fish\conf.d\[Link]
no change C:\Users\Pat\miniconda3\shell\condabin\Conda.psm1
modified C:\Users\Pat\miniconda3\shell\condabin\conda-hook.ps1
no change C:\Users\Pat\miniconda3\Lib\site-packages\xontrib\[Link]
modified C:\Users\Pat\miniconda3\etc\profile.d\[Link]
modified C:\Users\Pat\Documents\WindowsPowerShell\profile.ps1
modified HKEY_CURRENT_USER\Software\Microsoft\Command Processor\AutoRun
388 Cours
==> For changes to take effect, close and re-open decurrent
your Python /shell.
Université
<== Paris Cité / UFR Sciences du Vivant
$ conda init
no change C:\Users\Pat\Miniconda3\Scripts\[Link]
B.2. Installation de Python avec Miniconda Annexe B. Installation de Python
Cela signifie que vous êtes bien dans l’interpréteur Python. À partir de là vous pouvez taper exit() puis appuyer sur
Une fois revenu dans le shell, tapez la commande conda, vous devriez obtenir :
usage: [Link] [-h] [-v] [--no-plugins] [-V] COMMAND ...
conda is a tool for managing and deploying applications, environments and packages.
options:
-h, --help Show this help message and exit.
-v, --verbose Can be used multiple times. Once for detailed output, twice for INFO logging, thrice
for DEBUG
logging, four times for TRACE logging.
--no-plugins Disable all plugins that are not built into conda.
-V, --version Show the conda version number and exit.
[...]
Si c’est le cas, bravo, conda est bien installé et vous pouvez passez à la suite (rendez-vous à la rubrique Installation
des modules supplémentaires) !
Si vous souhaitez désinstaller Miniconda, rien de plus simple. Dans le menu Windows, tapez Anaconda puis Désins-
taller. Cela vous emmènera dans le panneau de configuration. Faites alors un clic droit sur Miniconda3 py312..., puis
cliquez sur Désinstaller. Cela devrait ouvrir la fenêtre suivante :
Cliquez sur Next. Vous aurez alors l’écran suivant :
Cliquez sur Uninstall, puis à l’écran suivant confirmez que vous souhaitez désintaller Miniconda :
Le désinstallateur se lancera alors (cela peut prendre quelques minutes) :
Une fois la désinstallation terminée, cliquez sur Next :
Puis enfin sur Finish :
À ce point, Miniconda est bien désinstallé. Il reste toutefois une dernière manipulation que l’installateur n’a pas
effectué : il faut détruire à la main le fichier
C:\Users\nom_utilisateur\Documents\WindowsPowerShell\profile.ps1
(bien sûr, remplacez nom_utilisateur par votre propre nom d’utilisateur). Si vous ne le faites pas, cela affichera
un message d’erreur à chaque fois que vous lancerez un Powershell.
Cette commande va lancer l’installation des modules externes NumPy, pandas, matplotlib, scipy, Biopython et Jupyter
lab. Ces modules vont être téléchargés depuis internet par conda, il faut bien sûr que votre connexion internent soit
fonctionnelle. Au début, conda va déterminer les versions des paquets à télécharger en fonction de la version de Python
ainsi que d’autres paramètres (cela prend une à deux minutes). Cela devrait donner la sortie suivante :
Channels:
- defaults
Platform: linux-64
Collecting package metadata ([Link]): done
Solving environment: done
## Package Plan ##
package | build
---------------------------|-----------------
anyio-4.2.0 | py312h06a4308_0 238 KB
argon2-cffi-21.3.0 | pyhd3eb1b0_0 15 KB
Cours de Python / Université Paris Cité
argon2-cffi-bindings-21.2.0| / UFR Sciences
py312h5eee18b_0 du Vivant33 KB 393
asttokens-2.0.5 | pyhd3eb1b0_0 20 KB
[...]
Annexe B. Installation de Python B.3. Utilisation de conda pour installer des modules complémentaires
Une fois que les versions des paquets ont été déterminées, conda vous demande confirmation avant de démarrer
le téléchargement. Tapez y puis appuyez sur la touche Entrée pour confirmer. S’en suit alors le téléchargement et
l’installation de tous les paquets (cela prendra quelques minutes) :
Une fois que tout cela est terminé, vous récupérez la main dans le shell :
[...]
Downloading and Extracting Packages:
mkl-2023.1.0 | 171.5 MB | #
###################################################################################################################
| 92%
Preparing transaction: done
Verifying transaction: done
Executing transaction: done
$
Si aucune erreur ne s’affiche et que vous récupérez la main dans l’interpréteur, bravo, ces modules sont bien installés.
Quittez l’interpréteur Python en tapant la commande exit() puis en appuyant sur la touche Entrée.
Vous êtes de nouveau dans le shell. Nous allons maintenant pouvoir tester Jupyter. Tapez dans le shell :
$ jupyter lab
Cette commande devrait ouvrir votre navigateur internet par défaut et lancer Jupyter :
Pour quitter Jupyter, allez dans le menu File puis sélectionnez Quit. Vous pourrez alors fermer l’onglet de Jupyter.
Pendant ces manipulations dans le navigateur, de nombreuses lignes ont été affichées dans l’interpréteur :
(base) PS C:\Users\Pat> jupyter lab
[I 18:26:05.544 LabApp] JupyterLab extension loaded from C:\Users\Pat\Miniconda3\lib\site-packages\
jupyterlab
[I 18:26:05.544 LabApp] JupyterLab application directory is C:\Users\Pat\Miniconda3\share\jupyter\lab
[...]
[I 18:27:20.645 LabApp] Interrupted...
[I 18:27:32.986 LabApp] Shutting down 0 kernels
(base) PS C:\Users\Pat>
Il s’agit d’un comportement normal. Quand Jupyter est actif, vous n’avez plus la main dans l’interpréteur et tous ces
messages s’affichent. Une fois que vous quittez Jupyter, vous devriez récupérer la main dans l’interpréteur. Si ce n’est
pas le cas, pressez deux fois la combinaison de touches Ctrl + C
Si tous ces tests ont bien fonctionné, bravo, vous avez installé correctement Python avec Miniconda ainsi que tous
les modules qui seront utilisés pour ce cours. Vous pouvez quitter le shell en tapant exit puis en appuyant sur la touche
Entrée et aller faire une pause !
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
Comme indiqué au début de ce chapitre, pip 8 est un gestionnaire de paquets pour Python et permet d’installer des
modules externes. Pip est également présent dans Miniconda, donc utilisable et parfaitement fonctionnel. Vous pouvez
vous poser la question « Pourquoi utiliser le gestionnaire de paquets pip si le gestionnaire de paquets conda est déjà
présent ? ». La réponse est simple, certains modules ne sont présents que sur les dépôts pip. Si vous souhaitez les installer
il faudra impérativement utiliser pip. Inversement, certains modules ne sont présents que dans les dépôts de conda.
Toutefois, pour les modules classiques (comme NumPy, scipy, etc), tout est gérable avec conda.
Sauf cas exceptionnel, nous vous conseillons l’utilisation de conda pour gérer l’installation de modules
supplémentaires.
Si vous souhaitez installer un paquet qui n’est pas présent sur un dépôt conda avec pip, assurez vous d’abord que
votre environnement conda est bien activé (avec conda activate ou conda activate nom_environnement). La
syntaxe est ensuite très simple :
$ pip install nom_du_paquet
Si votre environnement conda était bien activé lors de l’appel de cette commande, celle-ci aura installé votre paquet
dans l’environnement conda. Tout est donc bien encapsulé dans l’environnement conda, et l’ajout de tous ces paquets
ne risque pas d’interférer avec le Python du système d’exploitation, rendant ainsi les choses bien « propres ».
Il faudra entrer votre mot de passe utilisateur puis valider en appuyant sur la touche Entrée.
Pour lancer cet éditeur, tapez la commande gedit dans un shell ou cherchez gedit dans le lanceur d’applications.
Vous devriez obtenir une fenêtre similaire à celle-ci :
On configure ensuite gedit pour que l’appui sur la touche Tab corresponde à une indentation de 4 espaces, comme
recommandée par la PEP 8 (chapitre 15 Bonnes pratiques en programmation Python). Pour cela, cliquez sur l’icône en
forme de 3 petites barres horizontales en haut à droite de la fenêtre de gedit, puis sélectionnez Préférences. Dans la
nouvelle fenêtre qui s’ouvre, sélectionnez l’onglet Éditeur puis fixez la largeur des tabulations à 4 et cochez la case Insérer
des espaces au lieu des tabulations :
Si vous le souhaitez, vous pouvez également cochez la case Activer l’indentation automatique qui indentera automati-
quement votre code quand vous êtes dans un bloc d’instructions. Fermez la fenêtre de paramètres une fois la configuration
terminée.
8. [Link]
9. [Link]
sur Langage, puis à droite dans le carré Tabulations cochez la case Insérer des espaces en réglant sur 4 espaces
comme indiqué ci-dessous :
Ensuite, il est important de faire en sorte que Notepad++ affiche les numéros de ligne sur la gauche (très pratique
lorsque l’interpréteur nous indique qu’il y a une erreur, par exemple, à la ligne 47). Toujours dans la fenêtre Préférences,
dans la liste sur la gauche cliquez sur Zones d'édition, puis sur la droite cochez la case Afficher la numérotation
des lignes comme indiqué ici :
Sur les anciennes versions de Mac OS X (< 10.14), TextWrangler 10 était un éditeur de texte simple, intuitif et
efficace. Toutefois son développement a été arrêté car il fonctionnait en 32-bits. Il a été remplacé par BBedit 11 qui
possède de nombreuses fonctionnalités supplémentaires mais qui doit en principe être acheté. Toutefois, ce dernier est
utilisable gratuitement avec les mêmes fonctionnalités que TextWrangler, sans les nouvelles fonctionnalités étendues. Ne
possédant pas de Mac, nous nous contentons ici de vous donner quelques liens utiles :
• La page de téléchargement 12 ;
• La page vers de nombreuses ressources 13 utiles ;
• Le manuel d’utilisation 14 (avec toutes les instructions pour son installation au chapitre 2) ;
• Une page sur Stackoverflow 15 qui vous montre comment faire en sorte que l’appui sur la touche Tab affiche 4
espaces plutôt qu’une tabulation.
10. [Link]
11. [Link]
12. [Link]
13. [Link]
14. [Link]
15. [Link]
Figure B.23 – Lancement d’un terminal depuis un répertoire donné avec Nautilus).
De façon similaire sous Windows, il existe deux astuces très pratiques. Lorsqu’on utilise l’explorateur Windows et que
l’on est dans un répertoire donné :
Figure B.24 – Lancement d’un powershell depuis un répertoire donné (étape 1).
16. [Link]
17. [Link]
18. [Link]
Figure B.25 – Lancement d’un powershell depuis un répertoire donné (étape 2).
puis on appuie sur entrée et le PowerShell se lance en étant directement dans le bon répertoire !
Deuxième astuce
En pressant la touche Shift et en faisant un clic droit dans un endroit de l’explorateur qui ne contient pas de fichier
(attention, ne pas faire de clic droit sur un fichier !). Vous verrez alors s’afficher le menu contextuel suivant :
Cliquez sur Ouvrir la fenêtre PowerShell ici, à nouveau votre Powershell sera directement dans le bon répertoire !
Vérification
La figure suivante montre le PowerShell, ouvert de la première ou la deuxième façon, dans lequel nous avons lancé
la commande ls qui affiche le nom du répertoire courant (celui dans lequel on se trouve, dans notre exemple D:\PAT\
Python) ainsi que les fichiers s’y trouvant (ici il n’y a qu’un fichier : [Link]). Ensuite nous avons lancé l’exécution de
ce fichier [Link] en tapant python [Link].
À votre tour !
Pour tester si vous avez bien compris, ouvrez votre éditeur favori, tapez les lignes suivantes puis enregistrez ce fichier
avec le nom [Link] dans le répertoire de votre choix.
1 import tkinter as tk
2
3 racine = [Link]()
4 label = [Link](racine, text="J'adore Python !")
5 bouton = [Link](racine, text="Quitter", command=[Link])
6 bouton["fg"] = "red"
7 [Link]()
8 [Link]()
9 [Link]()
10 print("C'est fini !")
Comme nous vous l’avons montré ci-dessus, ouvrez un shell et déplacez-vous dans le répertoire où se trouve [Link].
Lancez le script avec l’interpréteur Python :
$ python [Link]
Si vous avez fait les choses correctement, cela devrait afficher une petite fenêtre avec un message « J’adore Python !
» et un bouton Quitter.
Figure B.26 – Lancement d’un powershell depuis un répertoire donné (étape 2bis).
21. [Link]
22. [Link]
Figure B.27 – Lancement d’un powershell depuis un répertoire donné (étape 3).
23. [Link]