cours python 1
cours python 1
Introduction
iMac-de-pierre:Downloads$ python
Python 3.12.2 | packaged by Anaconda, Inc. | (main, Feb 27 2024, 12:57:28) [Clang 14.0.6 ]
on darwin Type "help", "copyright", "credits" or "license" for more information.
>>>
ou pour Linux :
pierre@jeera:~$ python
Python 3.12.2 | packaged by conda-forge | (main, Feb 16 2024, 20:50:58) [GCC
12.3.0] on linux Type "help", "copyright", "credits" or "license" for more
information.
>>>
Les blocs
• PS C:\Users\pierre> pour Windows,
• iMac-de-pierre:Downloads$ pour Mac OS X,
• pierre@jeera:~$ pour Linux.
représentent l’invite de commande de votre shell. Il se peut que vous ayez aussi le mot (base) qui indique
que vous avez un environnement conda activé. Par la suite, cette invite de commande sera représentée
simplement par le caractère
$, que vous soyez sous Windows, Mac OS X ou Linux.
Le triple chevron >>> est l’invite de commande (prompt en anglais) de l’interpréteur Python. Ici,
Python attend une commande que vous devez saisir au clavier. Tapez par exemple l’instruction :
print("Hello world!")
puis, validez cette commande en appuyant sur la touche Entrée.
Python a exécuté la commande directement et a affiché le texte Hello world!. Il attend ensuite
une nouvelle instruction en affichant l’invite de l’interpréteur Python (>>>). En résumé, voici ce qui a
dû apparaître sur votre écran :
1.7. Premier programme Chapitre 1. Introduction
1 >>> 1+1
2 2
3 >>> 6*3
4 18
À ce stade, vous pouvez entrer une autre commande ou bien quitter l’interpréteur Python, soit en tapant
la commande exit() puis en validant en appuyant sur la touche Entrée, soit en pressant simultanément
les touches Ctrl et D sous Linux et Mac OS X ou Ctrl et Z puis Entrée sous Windows.
En résumant, l’interpréteur fonctionne sur le modèle :
Remarque
L’extension de fichier standard des scripts Python est .py.
Pour exécuter votre script, ouvrez un shell et entrez la commande : python [Link]
Vous devriez obtenir un résultat similaire à ceci :
$ python
[Link] Hello
world!
Si c’est bien le cas, bravo ! Vous avez exécuté votre premier programme Python.
1.8 Commentaires
Dans un script, tout ce qui suit le caractère # est ignoré par Python jusqu’à la fin de la ligne et est
considéré comme un commentaire.
Les commentaires doivent expliquer votre code dans un langage humain. L’utilisation des commentaires
est rediscutée dans le chapitre 16 Bonnes pratiques en programmation Python.
Voici un exemple :
Remarque
On appelle souvent à tort le caractère # « dièse ». On devrait plutôt parler de « croisillon 12 ».
Pour chaque base de la séquence ATCCGACTG, nous souhaitons effectuer deux actions : d’abord
afficher la base puis compter une base de plus. Pour indiquer cela, on décalera vers la droite ces deux
instructions par rapport à la ligne précédente (pour chaque base [...]). Ce décalage est appelé
indentation et l’ensemble des lignes indentées constitue un bloc d’instructions.
Une fois qu’on aura réalisé ces deux actions sur chaque base, on pourra passer à la suite, c’est-à-dire
afficher la taille de la séquence. Pour bien préciser que cet affichage se fait à la fin, donc une fois
l’affichage puis le comptage de chaque base terminés, la ligne correspondante n’est pas indentée (c’est-
à-dire qu’elle n’est pas décalée vers la droite).
1.10. Autres ressources Chapitre 1. Introduction
Pratiquement, l’indentation en Python doit être homogène (soit des espaces, soit des tabulations, mais pas
un mélange des deux). Une indentation avec 4 espaces est le style d’indentation recommandé (voir le
chapitre 16 Bonnes pratiques en programmation Python).
Si tout cela semble un peu complexe, ne vous inquiétez pas. Vous allez comprendre tous ces détails
chapitre après chapitre.
1.10 Autres ressources
Pour compléter votre apprentissage de Python, n’hésitez pas à consulter d’autres ressources
complémentaires à cet ouvrage. D’autres auteurs abordent l’apprentissage de Python d’une autre
manière. Nous vous conseillons les ressources suivantes en langue française :
• Le livre Apprendre à programmer avec Python 3 de Gérard Swinnen. Cet ouvrage est téléchargeable
gratuitement sur le site de Gérard Swinnen. Les éditions Eyrolles proposent également la version
papier de cet ouvrage.
• Le livre Apprendre à programmer en Python avec PyZo et Jupyter Notebook de Bob Cordeau et
Laurent Pointal, publié aux éditions Dunod. Une partie de cet ouvrage est téléchargeable
gratuitement sur le site de Laurent Pointal.
• Le livre Apprenez à programmer en Python de Vincent Legoff que vous trouverez sur le site [Link]
pour terminer, une ressource incontournable en langue anglaise :
• Le site [Link] . Il contient énormément d’informations et de liens sur Python. La
page d’index des modules est particulièrement utile (et traduite en français).
CHAPITRE 2
Variables
Définition
Une variable est une zone de la mémoire de l’ordinateur dans laquelle une valeur est stockée. Aux yeux
du program- meur, cette variable est définie par un nom, alors que pour l’ordinateur, il s’agit en fait
d’une adresse, c’est-à-dire d’une zone particulière de la mémoire.
En Python, la déclaration d’une variable et son initialisation (c’est-à-dire la première valeur que
l’on va stocker dedans) se font en même temps. Pour vous en convaincre, testez les instructions suivantes
après avoir lancé l’interpréteur :
1 >>> x = 2
2 >>> x
3 2
Ligne 1. Dans cet exemple, nous avons déclaré, puis initialisé la variable x avec la valeur 2. Notez bien
qu’en réalité, il s’est passé plusieurs choses :
• Python a « deviné » que la variable était un entier. On dit que Python est un langage au typage dynamique.
• Python a alloué (réservé) l’espace en mémoire pour y accueillir un entier. Chaque type de
variable prend plus ou moins d’espace en mémoire. Python a aussi fait en sorte qu’on puisse
retrouver la variable sous le nom x.
• Enfin, Python a assigné la valeur 2 à la variable x.
Dans d’autres langages (en C par exemple), il faut coder ces différentes étapes une par une. Python étant
un langage dit de haut niveau, la simple instruction x = 2 a suffi à réaliser les trois étapes en une fois !
Lignes 2 et 3. L’interpréteur nous a permis de connaître le contenu de la variable juste en tapant son
nom. Retenez ceci, car c’est une spécificité de l’interpréteur Python, très pratique pour chasser
(debugger) les erreurs dans un programme. En revanche, la ligne d’un script Python qui contient
seulement le nom d’une variable (sans aucune autre indication) n’affichera pas la valeur de la variable
à l’écran lors de l’exécution (pour autant, cette instruction reste valide et ne générera pas d’erreur).
Depuis la version 3.10, l’interpréteur Python a amélioré ses messages d’erreur. Il est ainsi capable
de suggérer des noms de variables existants lorsqu’on fait une faute de frappe :
1 >>> voyelles = "aeiouy"
2 >>> voyelle
3 Traceback (most recent call last):
4 File "<stdin>", line 1, in <module>
5 NameError: name 'voyelle' is not defined. Did you mean: 'voyelles'?
Si le mot qu’on tape n’est pas très éloigné, cela fonctionne également lorsqu’on se trompe à
différents endroits du mot !
Définition
Le symbole = est appelé opérateur d’affectation. Il permet d’assigner une valeur à une variable en
Python. Cet opérateur s’utilise toujours de la droite vers la gauche. Par exemple, dans l’instruction x
= 2 ci-dessus, Python attribue
la valeur située à droite (ici, 2) à la variable située à gauche (ici, x). D’autres langages de programmation
comme R
utilisent les symboles <- pour rendre l’affectation d’une variable plus explicite, par exemple x <- 2.
1 >>> x = 2
2 >>> y = x
3 >>> y
4 2
5 >>> x = 5 - 2
6 >>> x
7 3
Ligne 2. Ici on a un nom de variable à gauche et à droite de l’opérateur =. Dans ce cas, on garde la
règle d’aller toujours de la droite vers la gauche. C’est donc le contenu de la variable y qui est affecté à
la variable x.
Ligne 5. Comme on le verra plus bas, si on a à droite de l’opérateur = une expression, ici la soustraction 4
- 2, celle-ci est d’abord évaluée et c’est le résultat de cette opération qui sera affecté à la variable x. On
pourra noter également que la valeur de x précédente (2) a été écrasée.
Attention
L’opérateur d’affectation = écrase systématiquement la valeur de la variable située à sa gauche si celle-ci existe
déjà.
Définition
Le type d’une variable correspond à la nature de celle-ci. Les trois principaux types dont nous
aurons besoin dans un premier temps sont les entiers (integer ou int), les nombres décimaux que nous
appellerons floats et les chaînes de caractères (string ou str).
Bien sûr, il existe de nombreux autres types (par exemple, les booléens, les nombres complexes, etc.).
Si vous n’êtes pas effrayés, vous pouvez vous en rendre compte ici .
Dans l’exemple précédent, nous avons stocké un nombre entier (int) dans la variable x, mais il est tout
à fait possible de stocker des floats, des chaînes de caractères (string ou str) ou de nombreux autres types de
variables que nous verrons par la suite :
1 >>> y = 3.14
2 >>> y
3 3.14
4 >>> a = "bonjour"
5 >>> a
6 'bonjour'
7 >>> b = 'salut'
8 >>> b
9 'salut'
10 >>> c = """girafe"""
11 >>> c
12 'girafe'
13 >>> d = '''lion'''
14 >>> d
15 'lion'
Remarque
Python reconnaît certains types de variables automatiquement (entier, float). Par contre, pour une
chaîne de carac- tères, il faut l’entourer de guillemets (doubles, simples, voire trois guillemets successifs
doubles ou simples) afin d’indiquer
à Python le début et la fin de la chaîne de caractères.
Dans l’interpréteur, l’affichage direct du contenu d’une chaîne de caractères se fait avec des
guillemets simples, quel que soit le type de guillemets utilisé pour définir la chaîne de caractères.
En Python, comme dans la plupart des langages de programmation, c’est le point qui est utilisé
comme séparateur décimal. Ainsi, 3.14 est un nombre reconnu comme un float en Python alors que ce
n’est pas le cas de 3,14.
Il existe également des variables de type booléen. Un booléen est une variable qui ne prend que deux
valeurs : Vrai ou Faux. En python, on utilise pour cela les deux mots réservés True et False :
2.3 Nommage
Le nom des variables en Python peut être constitué de lettres minuscules (a à z), de lettres
majuscules (A à Z), de nombres (0 à 9) ou du caractère souligné (_). Vous ne pouvez pas utiliser
d’espace dans un nom de variable.
Par ailleurs, un nom de variable ne doit pas débuter par un chiffre et il n’est pas recommandé de le
faire débuter par le caractère _ (sauf cas très particuliers).
De plus, il faut absolument éviter d’utiliser un mot « réservé » par Python comme nom de variable (par
exemple :
print, range, for, from, etc.).
Dans la mesure du possible, il est conseillé de mettre des noms de variables explicites. Sauf dans de
rares cas que nous expliquerons plus tard dans le cours, évitez les noms de variables à une lettre.
Enfin, Python est sensible à la casse, ce qui signifie que les variables TesT, test et TEST sont différentes.
1 >>> print(80_94_7527_49)
2 8094752749
2.5 Opérations
2.5.1 Opérations sur les types numériques
Les quatre opérations arithmétiques de base se font de manière simple sur les types numériques (nombres entiers
et
floats) :
1 >>> x = 45
2 >>> x + 2
3 47
4 >>> x - 2
5 43
6 >>> x * 3
7 135
8 >>> y = 2.5
9 >>> x - y
10 42.5
11 >>> (x * 10) + y
12 452.5
Remarquez toutefois que si vous mélangez les types entiers et floats, le résultat est renvoyé comme
un float (car ce type est plus général). Par ailleurs, l’utilisation de parenthèses permet de gérer les
priorités.
L’opérateur / effectue une division. Contrairement aux opérateurs +, - et *, celui-ci renvoie systématiquement un
float :
1 >>> 3 / 4
2 0.75
3 >>> 2.5 / 2
4 1.25
5 >>> 6 / 3
6 2.0
7 >>> 10 / 2
8 5.0
L’opérateur puissance utilise les symboles ** :
1 >>> 2**3
2 8
3 >>> 2**4
4 16
Pour obtenir le quotient et le reste d’une division entière (voir ici 4 pour un petit rappel sur la
division entière), on utilise respectivement les symboles // et modulo % :
1 >>> 5 // 4
2 1
3 >>> 5 % 4
4 1
5 >>> 8 // 4
6 2
7 >>> 8 % 4
8 0
Les symboles +, -, *, /, **, // et % sont appelés opérateurs, car ils réalisent des opérations sur les
variables. Enfin, il existe des opérateurs « combinés » qui effectue une opération et une
affectation en une seule étape :
1 >>> i = 0
2 >>> i = i + 1
3 >>> i
4 1
5 >>> i += 1
6 >>> i
7 2
8 >>> i += 2
9 >>> i
10 4
L’opérateur += effectue une addition puis affecte le résultat à la même variable. Cette opération
s’appelle une « incrémentation ».
Les opérateurs -=, *= et /= se comportent de manière similaire pour la soustraction, la multiplication et la division.
Attention
Vous observez que les opérateurs + et * se comportent différemment s’il s’agit d’entiers ou de chaînes de
caractères. Ainsi, l’opération 2 + 2 est une addition alors que l’opération "2" + "2" est une concaténation.
On appelle ce compor- tement redéfinition des opérateurs. Nous serons amenés à revoir cette notion dans
le chapitre 24 Avoir plus la classe avec les objets (en ligne).
2.6. La fonction type() Chapitre 2. Variables
Notez que Python vous donne des informations dans son message d’erreur. Dans le second exemple,
il indique que vous devez utiliser une variable de type str, c’est-à-dire une chaîne de caractères et pas
un int, c’est-à-dire un entier.
1 >>> x = 2
2 >>> type(x)
3 <class 'int'>
4 >>> y = 2.0
5 >>> type(y)
6 <class 'float'>
7 >>> z = '2'
8 >>> type(z)
9 <class 'str'>
10 >>> type(True)
11 <class 'bool'>
Nous verrons plus tard ce que signifie le mot class.
Attention
Pour Python, la valeur 2 (nombre entier) est différente de 2.0 (float) et est aussi différente de '2'
(chaîne de caractères).
1 >>> i = 3
2 >>> str(i)
3 '3'
4 >>> i = '456'
5 >>> int(i)
6 456
7 >>> float(i)
8 456.0
9 >>> i = '3.1416'
10 >>> float(i)
11 3.1416
On verra au chapitre 7 Fichiers que ces conversions sont essentielles. En effet, lorsqu’on lit ou écrit des
nombres dans un fichier, ils sont considérés comme du texte, donc des chaînes de caractères.
Toute conversion d’une variable d’un type en un autre est appelé casting en anglais, il se peut que
vous croisiez ce terme si vous consultez d’autres ressources.
2.8 Note sur le vocabulaire et la syntaxe
Nous avons vu dans ce chapitre la notion de variable qui est commune à tous les langages de
programmation. Toutefois, Python est un langage dit « orienté objet », il se peut que dans la suite du
cours, nous employions le mot objet pour désigner une variable. Par exemple, « une variable de type
entier » sera pour nous équivalent à « un objet de type entier ». Nous verrons dans le chapitre 23 Avoir
la classe avec les objets (en ligne) ce que le mot « objet » signifie réellement (tout comme le mot « classe
»).
Par ailleurs, nous avons rencontré plusieurs fois des fonctions dans ce chapitre, notamment avec
type(), int(), float() et str(). Dans le chapitre 1 Introduction, nous avons également vu la fonction print().
On reconnaît qu’il s’agit d’une fonction, car son nom est suivi de parenthèses (par exemple, type()). En
Python, la syntaxe générale est fonction().
Ce qui se trouve entre les parenthèses d’une fonction est appelé argument et c’est ce que l’on « passe » à
la fonction. Dans l’instruction type(2), c’est l’entier 2 qui est l’argument passé à la fonction type(). Pour
l’instant, on retiendra qu’une fonction est une sorte de boîte à qui on passe un (ou plusieurs)
argument(s), qui effectue une action et qui peut renvoyer un résultat ou plus généralement un objet. Par
exemple, la fonction type() renvoie le type de la variable qu’on lui a passé en argument.
Si ces notions vous semblent obscures, ne vous inquiétez pas, au fur et à mesure que vous avancerez
dans le cours, tout deviendra limpide.
2.10 Exercices
Conseil
Pour ces exercices, utilisez l’interpréteur Python.
• (1 + 28) × 5
• (2 + 18)7
Affichage
1 >>> var = 3
2 >>> print(var)
3 3
24
3.2. Messages d’erreur Chapitre 3. Affichage
Il est également possible d’afficher le contenu de plusieurs variables (quel que soit leur type) en les
séparant par des virgules :
1 >>> x = 32
2 >>> nom = "John"
3 >>> print(nom, "a", x, "ans")
4 John a 32 ans
Python a écrit une phrase complète en remplaçant les variables x et nom par leur contenu. Vous
remarquerez que pour afficher plusieurs éléments de texte sur une seule ligne, nous avons utilisé le
séparateur « , » entre les différents éléments. Python a également ajouté un espace à chaque fois que l’on
utilisait le séparateur « , ». On peut modifier ce comportement en passant à la fonction print() l’argument
par mot-clé sep :
1 >>> x = 32
2 >>> nom = "John"
3 >>> print(nom, "a", x, "ans", sep="")
4 Johna32ans
5 >>> print(nom, "a", x, "ans", sep="-")
6 John-a-32-ans
7 >>> print(nom, "a", x, "ans", sep="_")
8 John_a_32_ans
Pour afficher deux chaînes de caractères l’une à côté de l’autre, sans espace, on peut soit les
concaténer, soit utiliser l’argument par mot-clé sep avec une chaîne de caractères vide :
1 print("chat"
2 print("souris")
3 print(1 / 0)
4 print(int("deux"))
Vous avez sans doute repéré l’oubli d’une parenthèse fermante en ligne 1. Lorsqu’on lance le script, on obtient :
$ python [Link]
File "[Link]", line 1
print("chat"
^
SyntaxError: '(' was never closed
Comment doit-on lire ce message d’erreur ? Et bien cela se fait toujours du bas vers le haut. Le
message s’appelle une Traceback et contient plusieurs types d’information :
• Tout en bas : On a le type d’erreur qui a été généré (on verra plus tard que cela s’appelle en réalité
une exception). Ici une erreur de syntaxe appelée SyntaxError. Puis sur la même ligne, un indice
supplémentaire (ici l’absence d’une parenthèse).
• Un peu plus haut : une description de l’erreur où on voit la parenthèse ouverte qui n’a jamais été fermée.
• Encore plus haut : le numéro de ligne dans le code où l’erreur a été détectée.
Avec cette Traceback il devient facile de corriger l’erreur.
Attention
Dans les vieilles versions de Python (< 3.10), la même erreur conduisait à une Traceback beaucoup moins claire :
$ python3.5 [Link]
File "[Link]", line
2
print("souris")
^
SyntaxError: invalid syntax
L’interpréteur vous indiquait une erreur de syntaxe en ligne 2 alors que l’oubli de parenthèse était
en ligne 1 ! Pour cette raison, utilisez dans la mesure du possible une version récente de Python (3.12
ou 3.13).
Si nous corrigeons la ligne 1 en mettant la parenthèse finale et que nous relançons le script, nous
aurons cette fois-ci une erreur due à une division par zéro :
$ python
[Link] chat
souris
Traceback (most recent call
last): File "[Link]", line 3, in
<module>
print(1 / 0)
~~^~~
ZeroDivisionError: division by zero
Si nous corrigeons cette erreur en ligne 3 en évitant la division par zéro (par exemple en mettant
print(1 / 1)), l’exécution donnera un autre message d’erreur dû à la ligne 4 où la transformation d’une
chaîne de caractères en entier n’est pas possible :
$ python
[Link] chat
souri
s1
Traceback (most recent call
last): File "[Link]", line 4, in
<module>
print(int("deux"))
^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'deux'
À nouveau dans ces deux derniers exemples de Traceback, vous voyez qu’on a la même construction. Tout
en bas, le type d’erreur, puis en remontant une description du problème et le numéro de ligne où
l’erreur a été détectée.
Conseil
Il est important de bien lire chaque message d’erreur généré par Python. En général, la clé du problème
est mentionnée dans ce message vous donnant des éléments pour le corriger.
Définition
L’écriture formatée est un mécanisme permettant d’afficher des variables avec un format précis, par
exemple justifiées à gauche ou à droite, ou encore avec un certain nombre de décimales pour les floats.
L’écriture formatée est incontournable lorsqu’on veut créer des fichiers organisés en « belles colonnes »
comme par exemple les fichiers PDB (pour en savoir plus sur ce format, reportez-vous à l’annexe A
Quelques formats de données en biologie).
Depuis la version 3.6, Python a introduit les f-strings pour mettre en place l’écriture formatée que
nous allons décrire en détail dans cette rubrique. Il existe d’autres manières pour formater des chaînes
de caractères qui étaient utilisées
avant la version 3.6, nous expliquons cela dans le chapitre 26 Remarques complémentaires (en ligne).
Toutefois, nous vous conseillons vivement l’utilisation des f-strings si vous débutez l’apprentissage de
Python. Il est inutile d’apprendre les anciennes manières.
Définition
f-string est le diminutif de formatted string literals. Mais encore ? Dans le chapitre précédent, nous
avons vu les chaînes de caractères ou encore strings qui étaient représentées par un texte entouré de
guillemets simples ou doubles.
Par exemple :
Nous expliquons plus en détail dans le chapitre 11 Plus sur les chaînes de caractères pourquoi on doit
mettre ce f et quel est le mécanisme sous-jacent.
3.3.2 Prise en main des f-strings
Les f-strings permettent une meilleure organisation de l’affichage des variables. Reprenons l’exemple ci -
dessus à propos de notre ami John :
1 >>> x = 32
2 >>> nom = "John"
3 >>> print(f"{nom} a {x} ans")
4 John a 32 ans
Il suffit de passer un nom de variable au sein de chaque couple d’accolades et Python les remplace par leur
contenu.
La syntaxe apparait plus lisible que l’équivalent vu précédemment :
Remarque
Une variable est utilisable plus d’une fois pour une f-string donnée :
Le résultat obtenu présente trop de décimales (seize dans le cas présent). Pour écrire le résultat plus
lisiblement, vous pouvez spécifier dans les accolades {} le format qui vous intéresse. Dans le cas présent,
vous voulez formater un float pour l’afficher avec deux puis trois décimales :
Les instructions étant longues dans cet exemple, nous avons coupé chaque chaîne de caractères sur deux
lignes. Il faut mettre à chaque fois le f pour préciser à Python qu’on utilise une f-string. Les ... indiquent
que l’interpréteur attend que l’on ferme la parenthèse du print entamé sur la ligne précédente. Nous
reverrons cette syntaxe dans le chapitre 11 Plus sur les chaînes de caractères.
Enfin, il est possible de préciser sur combien de caractères vous voulez qu’un résultat soit écrit et
comment se fait l’alignement (à gauche, à droite), ou si vous voulez centrer le texte. Dans la portion de
code suivante, le caractère ; sert de séparateur entre les instructions sur une même ligne :
1 >>> print(10) ; print(1000)
2 10
3 1000
4 >>> print(f"{10:>6d}") ; print(f"{1000:>6d}")
5 10
6 1000
7 >>> print(f"{10:<6d}") ; print(f"{1000:<6d}")
8 10
9 1000
10 >>> print(f"{10:^6d}") ; print(f"{1000:^6d}")
11 10
12 1000
13 >>> print(f"{10:*^6d}") ; print(f"{1000:*^6d}")
14 **10**
15 *1000*
16 >>> print(f"{10:0>6d}") ; print(f"{1000:0>6d}")
17 000010
18 001000
Notez que > spécifie un alignement à droite, < spécifie un alignement à gauche et ^ spécifie un
alignement centré. Il est également possible d’indiquer le caractère qui servira de remplissage lors des
alignements (l’espace est le caractère par défaut).
Ce formatage est également possible sur des chaînes de caractères avec la lettre s (comme string) :
1 >>> print(f"{perc_GC:7.3f}")
2 47.804
3 >>> print(f"{perc_GC:10.3f}")
4 47.804
L’instruction 7.3f signifie que l’on souhaite écrire un float avec 3 décimales et formaté sur 7
caractères (par défaut justifiés à droite). L’instruction 10.3f fait la même chose sur 10 caractères.
Remarquez que le séparateur décimal . compte pour un caractère. De même, si on avait un nombre
négatif, le signe - compterait aussi pour un caractère.
3.3.4 Autres détails sur les f-strings
Si on veut afficher des accolades littérales avec les f-strings, il faut les doubler pour échapper au formatage :
Python considère le résultat de l’instruction f"{perc_GC:10.3f}" comme une chaîne de caractères et la fonction
type() nous le confirme.
3.3.5 Expressions dans les f-strings
Une fonctionnalité extrêmement puissante des f-strings est de supporter des expressions Python au sein des accolades.
Ainsi, il est possible d’y mettre directement une opération ou encore un appel à une fonction :
1 >>> print(f"{1_000_000_000:e}")
2 1.000000e+09
3 >>> print(f"{0.000_000_001:e}")
4 1.000000e-09
Il est également possible de définir le nombre de chiffres après la virgule. Dans l’exemple ci-dessous,
on affiche un nombre avec aucun, 3 et 6 chiffres après la virgule :
3.5 Exercices
Conseil
Pour les exercices 2 à 6, utilisez l’interpréteur Python.
3.5.2 Poly-A
Générez une chaîne de caractères représentant un brin d’ADN poly-A (c’est-à-dire qui ne contient
que des bases A) de 20 bases de longueur, sans taper littéralement toutes les bases.
3.5.3 Poly-A et poly-GC
Sur le modèle de l’exercice précédent, générez en une ligne de code un brin d’ADN poly-A
(AAAA…) de 20 bases suivi d’un poly-GC régulier (GCGCGC…) de 40 bases.
3.5.4 Écriture formatée
En utilisant l’écriture formatée, affichez en une seule ligne les variables a, b et c dont les valeurs sont
respectivement la chaîne de caractères "salut", le nombre entier 102 et le float 10.318. La variable c sera
affichée avec deux décimales.
3.5.5 Écriture formatée 2
Dans un script [Link], calculez un pourcentage de GC avec l’instruction suivante :
perc_GC = ((4500 + 2575)/14800)*100
Ensuite, affichez le contenu de la variable perc_GC à l’écran avec 0, 1, 2 puis 3 décimales sous
forme arrondie en utilisant l’écriture formatée et les f-strings. On souhaite que le programme
affiche la sortie suivante :
Le pourcentage de GC est 48
% Le pourcentage de GC est
47.8 % Le pourcentage de
GC est 47.80 % Le
pourcentage de GC est
47.804 %
Listes
4.1 Définition
Définition
Une liste est une structure de données qui contient une collection d’objets Python. Il s’agit d’un
nouveau type par rapport aux entiers, float, booléens et chaînes de caractères que nous avons vus
jusqu’à maintenant. On parle aussi
d’objet séquentiel en ce sens qu’il contient une séquence d’autres objets.
Python autorise la construction de liste contenant des valeurs de types différents (par exemple
entier et chaîne de caractères), ce qui leur confère une grande flexibilité. Une liste est déclarée par une
série de valeurs (n’oubliez pas les guillemets, simples ou doubles, s’il s’agit de chaînes de caractères)
séparées par des virgules, et le tout encadré par des crochets. En voici quelques exemples :
4.2 Utilisation
Un des gros avantages d’une liste est que vous accédez à ses éléments par leur position. Ce numéro est appelé indice
(ou index) de la liste.
32
1 >>> animaux = ["girafe", "tigre", "singe", "souris"]
2 >>> animaux[0]
3 'girafe'
4 >>> animaux[1]
5 'tigre'
6 >>> animaux[3]
7 'souris'
Par conséquent, si on appelle l’élément d’indice 4 de notre liste, Python renverra un message d’erreur :
1 >>> animaux[4]
2 Traceback (innermost last):
3 File "<stdin>", line 1, in ?
4 IndexError: list index out of range
N’oubliez pas ceci ou vous risquez d’obtenir des bugs inattendus !
Remarque
La notion de méthode est introduite dans la rubrique Note sur le vocabulaire et la syntaxe à la fin de ce
chapitre.
1 >>> liste1 = []
2 >>> liste1
3 []
puis lui ajouter deux éléments, l’un après l’autre, d’abord avec la concaténation :
Conseil
Nous vous conseillons dans ce cas précis d’utiliser la méthode .append(), dont la syntaxe est plus élégante.
Nous reverrons en détail la méthode .append() dans le chapitre 12 Plus sur les listes.
Les indices négatifs reviennent à compter à partir de la fin. Leur principal avantage est que vous
pouvez accéder au dernier élément d’une liste à l’aide de l’indice -1 sans pour autant connaître la
longueur de cette liste. L’avant-dernier élément a lui l’indice -2, l’avant-avant dernier l’indice -3,
etc. :
1 >>> animaux[-4]
2 'girafe'
4.5 Tranches
Un autre avantage des listes est la possibilité de sélectionner une partie d’une liste en utilisant un
indiçage construit sur le modèle [m:n+1] pour récupérer tous les éléments, du émième au énième (de
l’élément m inclu à l’élément n+1 exclu). On dit alors qu’on récupère une tranche de la liste, par
exemple :
1 >>> animaux = ["girafe", "tigre", "singe", "souris"]
2 >>> animaux[0:2]
3 ['girafe', 'tigre']
4 >>> animaux[0:3]
5 ['girafe', 'tigre', 'singe']
6 >>> animaux[0:]
7 ['girafe', 'tigre', 'singe', 'souris']
8 >>> animaux[:]
9 ['girafe', 'tigre', 'singe', 'souris']
10 >>> animaux[1:]
11 ['tigre', 'singe', 'souris']
12 >>> animaux[1:-1]
13 ['tigre', 'singe']
Notez que lorsqu’aucun indice n’est indiqué à gauche ou à droite du symbole deux-points :, Python
prend par défaut tous les éléments depuis le début ou tous les éléments jusqu’à la fin respectivement.
On peut aussi préciser le pas en ajoutant un symbole deux-points supplémentaire et en indiquant le pas par un entier
:
1 >>> animaux = ["girafe", "tigre", "singe", "souris"]
2 >>> animaux[0:3:2]
3 ['girafe', 'singe']
4 >>> x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
5 >>> x
6 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
7 >>> x[::1]
8 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
9 >>> x[::2]
10 [0, 2, 4, 6, 8]
11 >>> x[::3]
12 [0, 3, 6, 9]
13 >>> x[1:6:3]
14 [1, 4]
Finalement, on se rend compte que l’accès au contenu d’une liste fonctionne sur le modèle liste[début:fin:pas].
1 >>> list(range(10))
2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
La commande list(range(10)) a généré une liste contenant tous les nombres entiers de 0 inclus à 10 exclu.
Nous verrons l’utilisation de la fonction range() toute seule dans le chapitre 5 Boucles et comparaisons.
Dans l’exemple ci-dessus, la fonction range() a pris un argument, mais elle peut également prendre
deux ou trois arguments, voyez plutôt :
1 >>> list(range(10,0))
2 []
Ici la liste est vide car Python a pris la valeur du pas par défaut qui est de 1. Ainsi, si on commence
à 10 et qu’on avance par pas de 1, on ne pourra jamais atteindre 0. Python génère ainsi une liste vide.
Pour éviter ça, il faudrait, par exemple, préciser un pas de -1 pour obtenir une liste d’entiers
décroissants :
1 >>> list(range(10,0,-1))
2 [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
4.8 Listes de listes
Pour finir, sachez qu’il est tout à fait possible de construire des listes de listes. Cette fonctionnalité
peut parfois être très pratique. Par exemple :
1 >>> savane[1]
2 ['tigre', 2]
Pour accéder à un élément de la sous-liste, on utilise un double indiçage :
1 >>> savane[1][0]
2 'tigre'
3 >>> savane[1][1]
4 2
On verra un peu plus loin qu’il existe en Python des dictionnaires qui sont également très
pratiques pour stocker de l’information structurée. On verra aussi qu’il existe un module nommé
NumPy qui permet de créer des listes ou des tableaux de nombres (vecteurs et matrices) et de les
manipuler.
1 >>> min(3, 4)
2 3
Attention toutefois à ne pas mélanger entiers et floats d’une part avec une liste d’autre part, car
cela renvoie une erreur :
1 >>> min(liste1, 3, 4)
2 Traceback (most recent call last):
3 File "<stdin>", line 1, in <module>
4 TypeError: '<' not supported between instances of 'int' and 'list'
Soit on passe plusieurs entiers et / ou floats en argument, soit on passe une liste unique.
Attention
Cette astuce ne fonctionne que pour des listes à une dimension (c’est-à-dire pour des listes qui ne
contiennent que des éléments de type simple comme des entiers, des floats, des chaînes de caractères et des
booléens), mais pas pour des listes de listes. Le chapitre 12 Plus sur les listes explique l’origine de ce
comportement et comment s’en sortir à tous les coups.
la méthode .append() est liée à liste1 qui est un objet de type liste. La méthode modifie l’objet
liste en lui ajoutant un élément.
Nous aurons de nombreuses occasions de revoir cette notation objet.méthode().
4.12 Exercices
Conseil
Pour ces exercices, utilisez l’interpréteur Python.
4.12.1 Prédire la sortie
Soit les trois lignes de code suivantes :
4.12.3 Saisons
Créez quatre listes hiver, printemps, ete et automne contenant les mois correspondants à ces
saisons. Créez ensuite une liste saisons contenant les listes hiver, printemps, ete et automne.
Prévoyez ce que renvoient les instructions suivantes, puis vérifiez-le dans l’interpréteur :
1. saisons[2]
2. saisons[1][0]
3. saisons[1:2]
4. saisons[:][1]. Comment expliquez-vous ce dernier résultat ?
Boucles et comparaisons
Remarque
Les notions de bloc d’instruction et d’indentations ont été introduites dans le chapitre 1 Introduction.
Dans l’exemple suivant, le corps de la boucle contient deux instructions (ligne 2 et ligne 3) car elles
sont indentées par rapport à la ligne débutant par for :
Remarque
Outre une meilleure lisibilité, les deux-points et l’indentation sont formellement requis en
Python. Même si on peut indenter comme on veut (plusieurs espaces ou plusieurs tabulations, mais
pas une combinaison des deux), les développeurs recommandent l’utilisation de quatre espaces. Vous
pouvez consulter à ce sujet le chapitre 16 Bonnes pratiques de programmation en Python.
Faites en sorte de configurer votre éditeur de texte favori de façon à écrire quatre espaces lorsque
vous tapez sur la touche Tab (tabulation).
Dans les exemples ci-dessus, nous avons exécuté une boucle en itérant directement sur une liste. Une
tranche d’une liste étant elle même une liste, on peut également itérer dessus :
1 >>> animaux = ["girafe", "tigre", "singe", "souris"]
2 >>> for animal in animaux[1:3]:
3 ... print(animal)
4 ...
5 tigre
6 singe
On a vu que les boucles for pouvaient utiliser une liste contenant des chaînes de caractères, mais elles
peuvent tout aussi bien utiliser des listes contenant des entiers (ou n’importe quel type de variable)
:
1 >>> for i in [1, 2, 3]:
2 ... print(i)
3 ...
4 1
5 2
6 3
Remarque
Dans le chapitre 18 Jupyter et ses notebooks, nous mesurerons le temps d’exécution de ces deux méthodes
pour vous montrer que l’itération sur les éléments est la méthode la plus rapide.
Toutefois, il se peut qu’au cours d’une boucle vous ayez besoin des indices, auquel cas vous
devrez itérer sur les indices :
Enfin, Python possède la fonction enumerate() qui vous permet d’itérer sur les indices et les éléments eux-mêmes :
5.2 Comparaisons
Avant de passer aux boucles while, abordons tout de suite les comparaisons. Celles-ci seront reprises
dans le chapitre 6 Tests.
Python est capable d’effectuer toute une série de comparaisons entre le contenu de deux variables, telles que :
== égal à
!= différent de
> strictement supérieur à
>= supérieur ou égal à
< strictement inférieur à
<= inférieur ou
1 i=0
2 while i < 10:
3 print("Le Python c'est cool !")
Ici, nous avons omis de mettre à jour la variable i dans le corps de la boucle. Par conséquent, la boucle
ne s’arrêtera jamais (sauf en pressant Ctrl-C) puisque la condition i < 10 sera toujours vraie.
La boucle while combinée à la fonction input() peut s’avérer commode lorsqu’on souhaite demander
à l’utilisateur une valeur numérique. Par exemple :
1 >>> i = 0
2 >>> while i < 10:
3 ... reponse = input("Entrez un entier supérieur à 10 : ")
4 ... i = int(reponse)
5 ...
6 Entrez un entier supérieur à 10 : 4
7 Entrez un entier supérieur à 10 : -3
8 Entrez un entier supérieur à 10 : 15
9 >>> i
10 15
La fonction input() prend en argument un message (sous la forme d’une chaîne de caractères), demande à
l’utilisateur d’entrer une valeur et renvoie celle-ci sous forme d’une chaîne de caractères, qu’il faut
ensuite convertir en entier (avec la fonction int() ligne 4). Si on reprend les trois éléments d’une boucle
while, on trouve l’initialisation de la variable d’itération en ligne 1, le test de sa valeur en ligne 2, et
sa mise à jour en ligne 4.
Conseil
Comment choisir entre la boucle while et la boucle for ? La boucle while s’utilisera généralement
lorsqu’on ne sait pas à l’avance le nombre d’itérations (comme dans le dernier exemple). Si on connait
à l’avance le nombre d’itérations, par exemple si on veut écrire 10 fois Le Python c'est cool, nous
vous conseillons la boucle for.
5.4 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
Conseil
Pensez à relire le début du chapitre 3 Affichage qui discute de la fonction print().
8
24
48
[...]
5.4.7 Triangle
Créez un script qui dessine un triangle comme celui-ci :
*
**
***
****
*****
******
*******
********
*********
**********
5.4.10 Pyramide
Créez un script [Link] qui dessine une pyramide comme celle-ci :
*
***
*****
*******
*********
***********
************
*
*************
**
*************
****
***************
****
Essayez de faire évoluer votre script pour dessiner la pyramide à partir d’un nombre arbitraire
de lignes N. Vous pourrez demander à l’utilisateur le nombre de lignes de la pyramide avec les instructions
suivantes qui utilisent la fonction input() :
ligne colonne
1 2
1 3
1 4
2 3
2 4
3 4
Pour une matrice 4x4, on a parcouru 6 cases
Testez votre script avec N=3, puis N=4 et enfin N=5.
Concevez une seconde version à partir du script précédent, où cette fois on n’affiche plus tous les
couples possibles, mais simplement la valeur de N et le nombre de cases parcourues. Affichez cela pour
des valeurs de N allant de 2 à 10.
Pouvez-vous trouver une formule générale reliant le nombre de cases parcourues à N?
5.4.13 Sauts de puce
On imagine une puce qui se déplace aléatoirement sur une ligne, en avant ou en arrière, par pas
de 1 ou -1. Par exemple, si elle est à l’emplacement 0, elle peut sauter à l’emplacement 1 ou -1 ; si elle est
à l’emplacement 2, elle peut sauter à l’emplacement 3 ou 1, etc.
Avec une boucle while, simulez le mouvement de cette puce de l’emplacement initial 0 à
l’emplacement final 5 (voir le schéma de la figure 5.3). Combien de sauts sont nécessaires pour réaliser
ce parcours ? Relancez plusieurs fois le programme. Trouvez-vous le même nombre de sauts à
chaque exécution ?
Conseil
Utilisez l’instruction [Link]([-1,1]) qui renvoie au hasard les valeurs -1 ou 1 avec la même probabilité.
FIGURE 5.3 – Sauts de puce.
Avant d’utiliser cette instruction, mettez au tout début de votre script la ligne
import random
Nous verrons la signification de cette syntaxe particulière dans le chapitre 9 Modules.
Tests
6.1 Définition
Les tests sont un élément essentiel à tout langage informatique si on veut lui donner un peu de
complexité car ils permettent à l’ordinateur de prendre des décisions. Pour cela, Python utilise
l’instruction if ainsi qu’une comparaison que nous avons abordée au chapitre précédent. Voici un
premier exemple :
1 >>> x = 2
2 >>> if x == 2:
3 ... print("Le test est vrai !")
4 ...
5 Le test est vrai !
et un second :
1 >>> x = "souris"
2 >>> if x == "tigre":
3 ... print("Le test est vrai !")
4 ...
Il y a plusieurs remarques à faire concernant ces deux exemples :
• Dans le premier exemple, l’instruction print("Le test est vrai !") est exécutée, car le test est vrai.
Dans le second exemple, le test est faux et rien n’est affiché.
• Les blocs d’instructions dans les tests doivent forcément être indentés comme pour les boucles
for et while. L’indentation indique la portée des instructions à exécuter si le test est vrai.
• Comme avec les boucles for et while, la ligne qui contient l’instruction if se termine par le caractère deux-points
« : ».
Parfois, il est pratique de tester si la condition est vraie ou si elle est fausse dans une même
instruction if. Plutôt que d’utiliser deux instructions if, on peut se servir des instructions if et else :
1 >>> x = 2
2 >>> if x == 2:
3 ... print("Le test est vrai !")
4 ... else:
5 ... print("Le test est faux !")
6 ...
7 Le test est vrai !
8 >>> x = 3
9 >>> if x == 2:
10 ... print("Le test est vrai !")
11 ... else:
12 ... print("Le test est faux !")
13 ...
14 Le test est faux !
On peut utiliser une série de tests dans la même instruction if, notamment pour tester plusieurs valeurs
d’une même variable.
Par exemple, on se propose de tirer au sort une base d’ADN puis d’afficher le nom de cette dernière.
Dans le code suivant, nous utilisons l’instruction [Link](liste) qui renvoie un élément choisi
au hasard dans une liste. L’instruction import random sera vue plus tard dans le chapitre 9 Modules,
admettez pour le moment qu’elle est nécessaire :
Dans cet exemple, Python teste la première condition puis, si et seulement si elle est fausse, teste la deuxième
et ainsi de suite… Le code correspondant à la première condition vérifiée est exécuté puis Python sort
du bloc d’instructions du if. Il est également possible d’ajouter une condition else supplémentaire qui est
exécutée si aucune des conditions du if et des elif n’est vraie.
De nouveau, faites bien attention à l’indentation ! Vous devez être très rigoureux sur ce point. Pour vous
en convaincre, exécutez ces deux exemples de code :
Code 1
1 nombres = [4, 5, 6]
2 for nb in nombres:
3 if nb == 5:
4 print("Le test est vrai")
5 print(f"car la variable nb vaut {nb}")
Résultat :
Le test est vrai
car la variable nb vaut 5
6.4. Tests multiples Chapitre 6. Tests
Code 2
1 nombres = [4, 5, 6]
2 for nb in nombres:
3 if nb == 5:
4 print("Le test est vrai")
5 print(f"car la variable nb vaut {nb}")
Résultat :
car la variable nb
vaut 4 Le test est vrai
car la variable nb
vaut 5 car la variable
nb vaut 6
Les deux codes pourtant très similaires produisent des résultats très différents. Si vous observez
avec attention l’indentation des instructions sur la ligne 5, vous remarquerez que dans le code 1,
l’instruction est indentée deux fois, ce qui signifie qu’elle appartient au bloc d’instructions du test
if. Dans le code 2, l’instruction de la ligne 5 n’est indentée qu’une seule fois, ce qui fait qu’elle
n’appartient plus au bloc d’instructions du test if, d’où l’affichage de car la variable nb vaut xx pour
toutes les valeurs de nb.
6.4 Tests multiples
Les tests multiples permettent de tester plusieurs conditions en même temps en utilisant des opérateurs
booléens. Les deux opérateurs les plus couramment utilisés sont OU et ET. Voici un petit rappel sur le
fonctionnement de l’opérateur OU :
Condition 1 Opérateur Condition 2 Résultat
Vrai OU Vrai Vrai
Vrai OU Faux Vrai
Faux OU Vrai Vrai
Faux OU Faux Faux
et de l’opérateur ET :
Condition 1 Opérateur Condition 2 Résultat
Vrai ET Vrai Vrai
Vrai ET Faux Faux
Faux ET Vrai Faux
Faux ET Faux Faux
En Python, on utilise le mot réservé and pour l’opérateur ET et le mot réservé or pour l’opérateur
OU. Respectez bien la casse des opérateurs and et or qui, en Python, s’écrivent en minuscule. En voici
un exemple d’utilisation :
1 >>> x = 2
2 >>> y = 2
3 >>> if x == 2 and y == 2:
4 ... print("le test est vrai")
5 ...
6 le test est vrai
Notez que le même résultat serait obtenu en utilisant deux instructions if imbriquées :
1 >>> x = 2
2 >>> y = 2
3 >>> if x == 2:
4 ... if y == 2:
5 ... print("le test est vrai")
6 ...
7 le test est vrai
Conseil
Nous vous conseillons la syntaxe avec un and qui est plus compacte. De manière générale, moins il y a de
niveau d’indentations mieux c’est pour la lisibilité.
Vous pouvez aussi tester directement l’effet de ces opérateurs à l’aide de True et False (attention à
respecter la casse) :
Nous voyons que le résultat de l’opération 3 - 2.7 n’est pas exactement 0.3 d’où le résultat False en ligne 2.
En fait, ce problème ne vient pas de Python, mais plutôt de la manière dont un ordinateur traite les
nombres flottants (comme un rapport de nombres binaires). Ainsi certaines valeurs de float ne
peuvent être qu’approchées. Une manière
de s’en rendre compte est d’utiliser l’écriture formatée en demandant l’affichage d’un grand nombre de décimales
:
1 >>> 0.3
2 0.3
3 >>> f"{0.3:.5f}"
4 '0.30000'
5 >>> f"{0.3:.60f}"
6 '0.299999999999999988897769753748434595763683319091796875000000'
7 >>> var = 3 - 2.7
8 >>> f"{var:.60f}"
9 '0.299999999999999822364316059974953532218933105468750000000000'
10 >>> abs(var - 0.3)
11 1.6653345369377348e-16
On observe que lorsqu’on tape 0.3, Python affiche une valeur arrondie. En réalité, le nombre réel
0.3 ne peut être qu’approché lorsqu’on le code en nombre flottant. Il est essentiel d’avoir cela en tête
lorsque l’on compare deux floats. Même si 0.3 et 3 - 2.7 ne donnent pas le même résultat, la différence est
toutefois infinétisimale, de l’ordre de 1e-16 soit la 16me décimale !
Pour ces raisons, il ne faut surtout pas utiliser l’opérateur == pour tester si un float est égal à une
certaine valeur, car cet opérateur correspond à une égalité stricte. La bonne pratique est de vérifier si un
float est compris dans un intervalle avec une certaine précision. Si on appelle cette précision delta, on
peut procéder ainsi :
Conseil
Sur les trois manières de procéder pour comparer un float à une valeur, nous vous conseillons
celle avec math. isclose() qui nous parait la plus lisible.
6.7 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
6.7.1 Jours de la semaine
Constituez une liste semaine contenant le nom des sept jours de la semaine.
En utilisant une boucle, écrivez chaque jour de la semaine ainsi que les messages suivants :
• Au travail s’il s’agit du lundi au jeudi ;
• Chouette c'est vendredi s’il s’agit du vendredi ;
• Repos ce week-end s’il s’agit du samedi ou du dimanche.
Ces messages ne sont que des suggestions, vous pouvez laisser libre cours à votre imagination.
6.7.2 Séquence complémentaire d’un brin d’ADN
La liste ci-dessous représente la séquence d’un brin d’ADN :
["A", "C", "G", "T", "T", "A", "G", "C", "T", "A", "A", "C", "G"]
Créez un script qui transforme cette séquence en sa séquence complémentaire.
Rappel : la séquence complémentaire s’obtient en remplaçant A par T, T par A, C par G et G par C.
6.7.3 Minimum d’une liste
La fonction min() de Python renvoie l’élément le plus petit d’une liste constituée de valeurs numériques ou
de chaînes de caractères. Sans utiliser cette fonction, créez un script qui détermine le plus petit élément de la
liste [8, 4, 6, 1, 5].
6.7.4 Fréquence des acides aminés
La liste ci-dessous représente une séquence d’acides aminés :
["R", "A", "W", "W", "A", "W", "A", "R", "W", "W", "R", "A", "G"]
Calculez la fréquence des acides aminés alanine (A), arginine (R), tryptophane (W) et glycine (G) dans cette
séquence.
6.7.5 Notes et mention d’un étudiant
Voici les notes d’un étudiant : 14, 9, 13, 15 et 12. Créez un script qui affiche la note maximum (utilisez la
fonction
max()), la note minimum (utilisez la fonction min()) et qui calcule la moyenne.
Affichez la valeur de la moyenne avec deux décimales. Affichez aussi la mention obtenue sachant que la mention
est
« passable » si la moyenne est entre 10 inclus et 12 exclus, « assez bien » entre 12 inclus et 14 exclus et «
bien » au-delà de 14.
6.7.6 Nombres pairs
Construisez une boucle qui parcourt les nombres de 0 à 20 et qui affiche les nombres pairs inférieurs
ou égaux à 10 d’une part, et les nombres impairs strictement supérieurs à 10 d’autre part.
Pour cet exercice, vous pourrez utiliser l’opérateur modulo % qui renvoie le reste de la division
entière entre deux nombres et dont voici quelques exemples d’utilisation :
1 >>> 4 % 3
2 1
3 >>> 5 % 3
4 2
5 >>> 4 % 2
6 0
7 >>> 6 % 2
8 0
Vous remarquerez qu’un nombre est pair lorsque le reste de sa division entière par 2 est nul.
6.7.7 Conjecture de Syracuse (exercice +++)
La conjecture de Syracuse 2 est une conjecture mathématique qui reste improuvée à ce jour et qui
est définie de la manière suivante.
Soit un entier positif n. Si n est pair, alors le diviser par 2. S’il est impair, alors le multiplier par 3
et lui ajouter 1. En répétant cette procédure, la suite de nombres atteint la valeur 1 puis se prolonge
indéfiniment par une suite de trois valeurs triviales appelée cycle trivial.
Jusqu’à présent, la conjecture de Syracuse, selon laquelle depuis n’importe quel entier positif la
suite de Syracuse atteint 1, n’a pas été mise en défaut.
Par exemple, les premiers éléments de la suite de Syracuse si on prend comme point de départ 10 sont
: 10, 5, 16, 8, 4, 2, 1…
Créez un script qui, partant d’un entier positif n (par exemple 10 ou 20), crée une liste des nombres
de la suite de Syracuse. Avec différents points de départ (c’est-à-dire avec différentes valeurs de n), la
conjecture de Syracuse est-elle toujours vérifiée ? Quels sont les nombres qui constituent le cycle
trivial ?
Conseil
1. Pour cet exercice, vous avez besoin de faire un nombre d’itérations inconnu pour que la suite de Syracuse
atteigne le chiffre 1 puis entame son cycle trivial. Vous pourrez tester votre algorithme avec un
nombre arbitraire d’itérations, typiquement 20 ou 100, suivant votre nombre n de départ.
2. Un nombre est pair lorsque le reste de sa division entière (opérateur modulo %) par 2 est nul.
6.7.8 Attribution de la structure secondaire des acides aminés d’une protéine (exercice
+++)
Dans une protéine, les différents acides aminés sont liés entre eux par une liaison peptidique. Les angles
phi et psi sont deux angles mesurés autour de cette liaison peptidique. Leurs valeurs sont utiles pour
définir la conformation spatiale (appelée « structure secondaire ») adoptée par les acides aminés.
Par exemple, les angles phi et psi d’une conformation en « hélice alpha » parfaite ont une valeur de -
57 degrés et -47 degrés respectivement. Bien sûr, il est très rare que l’on trouve ces valeurs parfaites dans
une protéine, et il est habituel de tolérer une déviation de± 30 degrés autour des valeurs idéales de
ces angles.
Vous trouverez ci-dessous une liste de listes contenant les valeurs des angles phi et psi de 15 acides aminés de
la
protéine 1TFE 3 :
Remarque
Pour en savoir plus sur le monde merveilleux des protéines, n’hésitez pas à consulter la page Wikipedia
sur la structure secondaire des protéines 4.
Pour vous guider, voici ce que donnerait le programme avec la conversation précédente :
Fichiers
giraf
e
tigre
singe
souri
s
Enregistrez ce fichier dans votre répertoire courant avec le nom [Link]. Puis, testez le code
suivant dans l’interpréteur Python :
58
7.1. Lecture dans un fichier Chapitre 7. Fichiers
• Ligne 2. Lorsqu’on affiche le contenu de la variable filin, on se rend compte que Python la considère
comme un objet de type fichier ouvert (ligne 3).
• Ligne 4. Nous utilisons à nouveau la syntaxe objet.méthode() (présentée dans le chapitre 3
Affichage). Ici la méthode .readlines() agit sur l’objet filin en déplaçant le curseur de lecture du
début à la fin du fichier, puis elle renvoie une liste contenant toutes les lignes du fichier (dans notre
analogie avec un livre, ceci correspondrait à lire toutes les lignes du livre).
• Ligne 6. Enfin, on applique la méthode .close() sur l’objet filin, ce qui, vous vous en doutez, ferme
le fichier (ceci reviendrait à fermer le livre). Vous remarquerez que la méthode .close() ne renvoie rien,
mais modifie l’état de l’objet filin en fichier fermé. Ainsi, si on essaie de lire à nouveau les lignes
du fichier, Python renvoie une erreur, car il ne peut pas lire un fichier fermé (lignes 7 à 10).
Vous voyez qu’en cinq lignes de code, vous avez lu, parcouru le fichier et affiché son contenu.
Remarque
• Chaque élément de la liste lignes est une chaîne de caractères. C’est en effet sous forme de chaînes de
caractères que Python lit le contenu d’un fichier.
• Chaque élément de la liste lignes se termine par le caractère \n. Ce caractère un peu particulier
correspond au « saut de ligne 1 » qui permet de passer d’une ligne à la suivante (en anglais line feed).
Ceci est codé par un caractère spécial que l’on représente par \n. Vous pourrez parfois rencontrer
également la notation octale \012. Dans la suite de cet ouvrage, nous emploierons aussi
l’expression « retour à la ligne » que nous trouvons plus intuitive.
• Par défaut, l’instruction print() affiche quelque chose puis revient à la ligne. Ce retour à la ligne
dû à print() se cumule alors avec celui de la fin de ligne (\n) de chaque ligne du fichier et donne
l’impression qu’une ligne est sautée à chaque fois.
Il existe en Python le mot-clé with qui permet d’ouvrir et de fermer un fichier de manière efficace. Si
pour une raison ou une autre l’ouverture ou la lecture du fichier conduit à une erreur, l’utilisation de
with garantit la bonne fermeture du fichier, ce qui n’est pas le cas dans le code précédent. Voici donc le
même exemple avec with :
1 >>> with open("[Link]", 'r') as filin:
2 ... lignes = [Link]()
3 ... for ligne in lignes:
4 ... print(ligne)
5 ...
6 girafe
7
8 tigre
9
10 singe
11
12 souris
13
14 >>>
Remarque
• L’instruction with introduit un bloc d’instructions qui doit être indenté. C’est à l’intérieur de
ce bloc que nous effectuons toutes les opérations sur le fichier.
• Une fois sorti du bloc d’instructions, Python fermera automatiquement le fichier. Vous n’avez
donc plus besoin d’utiliser la méthode .close().
L’objet filin est « itérable », ainsi la boucle for va demander à Python d’aller lire le fichier ligne par ligne.
Conseil
Privilégiez cette méthode par la suite.
Remarque
Les méthodes abordées précédemment permettent d’accéder au contenu d’un fichier, soit ligne par ligne
(méthode
.readline()), soit globalement en une seule chaîne de caractères (méthode .read()), soit globalement avec
les lignes différenciées sous forme d’une liste de chaînes de caractères (méthode .readlines()). Il est
également possible en Python de se rendre à un endroit particulier d’un fichier avec la méthode .seek() mais
qui sort du cadre de cet ouvrage.
7.4 Note sur les retours à la ligne sous Unix et sous Windows
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
On a vu plus haut que le caractère spécial \n correspondait à un retour à la ligne. C’est le standard
sous Unix (Mac OS X et Linux).
Toutefois, Windows utilise deux caractères spéciaux pour le retour à la ligne : \r correspondant à un
retour chariot (hérité des machines à écrire) et \n comme sous Unix.
Si vous avez commencé à programmer en Python 2, vous aurez peut-être remarqué que, selon les
versions, la lecture de fichier supprimait parfois les \r et d’autres fois les laissait. Heureusement, la
fonction open() dans Python 3 3 gère
7.5. Importance des conversions de types avec les fichiers Chapitre 7. Fichiers
tout ça automatiquement et renvoie uniquement des sauts de ligne sous forme d’un seul \n (même si
le fichier a été conçu sous Windows et qu’il contient initialement des \r).
7.7 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
xA = cos(θ) ×
r yA = sin(θ)
×r
Pour calculer les coordonnées cartésiennes qui décrivent la spirale, vous allez faire varier deux
variables en même temps :
• l’angle θ, qui va prendre des valeurs de 0 à 4π radians par pas de 0,1, ce qui correspond à deux tours complets ;
• le rayon du cercle r, qui va prendre comme valeur initiale 0,5 puis que vous allez incrémenter (c’est-à-
dire augmenter) par pas de 0,1.
Les fonctions trigonométriques sinus et cosinus sont disponibles dans le module math que vous
découvrirez plus en détails dans le chapitre 9 Modules. Pour les utiliser, vous ajouterez au début de
votre script l’instruction :
import math
La fonction sinus sera [Link]() et la fonction cosinus [Link](). Ces deux fonctions prennent comme
argument une valeur d’angle en radian. La constante mathématique π sera également accessible grâce
à ce module via [Link]. Par exemple :
1 >>> [Link](0)
2 0.0
3 >>> [Link]([Link]/2)
4 1.0
5 >>> [Link]([Link])
6 -1.0
Sauvegardez ensuite les coordonnées cartésiennes dans le fichier [Link] en respectant le format suivant :
• un couple de coordonnées (x A et yA) par ligne ;
• au moins un espace entre les deux coordonnées x A et yA ;
• les coordonnées affichées sur 10 caractères avec 5 chiffres après la
virgule. Les premières lignes de [Link] devrait ressembler à :
0.50000 0.00000
0.59700 0.05990
0.68605 0.13907
0.76427 0.23642
0.82895 0.35048
0.87758 0.47943
[...] [...]
Une fois que vous avez généré le fichier [Link], visualisez votre spirale avec le code suivant (que
vous pouvez recopier dans un autre script ou à la suite de votre script [Link]) :
Remarque
Le module matplotlib est utilisé ici pour la visualisation de la spirale. Son utilisation est détaillée dans le
chapitre 21
Module matplotlib.
Essayez de jouer sur les paramètres θ et r, et leur pas d’incrémentation, pour construire de nouvelles spirales.
CHAPITRE 8
Dictionnaires et tuples
Dans ce chapitre, nous allons voir deux nouveaux types d’objet qui s’avèrent extrêmement utiles : les
dictionnaires et les tuples. Comme les listes vues dans le chapitre 4, les dictionnaires et tuples contiennent
une collection d’autres objets. Toutefois, nous verrons que ces trois types sont régis par des règles
différentes pour accéder à leur contenu, ainsi que dans leur fonctionnement.
8.1 Dictionnaires
8.1.1 Définition et fonctionnement
Définition
Un dictionnaire contient une collection d’objets Python auxquels on accède à l’aide d’une clé de
correspondance plutôt qu’un indice. Ainsi, il ne s’agit pas d’objets séquentiels comme les listes, mais plutôt
d’objets dits de correspondance
(mapping objects en anglais) ou tableaux associatifs.
1 >>> animal1 = {}
2 >>> animal1["nom"] = "girafe"
3 >>> animal1["taille"] = 5.0
4 >>> animal1["poids"] = 1100
5 >>> animal1
6 {'nom': 'girafe', 'taille': 5.0, 'poids': 1100}
• Ligne 1. On définit un dictionnaire vide avec les accolades {} (tout comme on peut le faire pour les listes
avec
[]).
• Lignes 2 à 4. On remplit le dictionnaire avec plusieurs clés ("nom", "taille", "poids") auxquelles
on affecte des valeurs ("girafe", 5.0, 1100).
• Ligne 5. On affiche le contenu du dictionnaire. Les accolades nous montrent qu’il s’agit bien
d’un dictionnaire, et pour chaque élément séparé par une virgule on a une association du type
clé: valeur. Ici, les clés sont des chaînes de caractères (ce qui sera souvent le cas), et les valeurs
peuvent être n’importe quel objet Python.
Une fois le dictionnaire créé, on récupère la valeur associée à une clé donnée avec une syntaxe du type
dictionnaire ["clé"]. Par exemple :
66
1 >>> animal1["nom"]
2 'girafe'
3 >>> animal1["taille"]
4 5.0
On se souvient que pour accéder à l’élément d’une liste, il fallait utiliser un indice (par exemple,
liste[2]). Ici, l’utilisation d’une clé (qui est souvent une chaîne de caractères) rend les choses plus
explicites.
Vous pouvez mettre autant de couples clé / valeur que vous voulez dans un dictionnaire (tout comme
vous pouvez ajouter autant d’éléments que vous le souhaitez dans une liste).
Remarque
Jusqu’à la version 3.6 de Python, un dictionnaire était affiché sans ordre particulier. L’ordre
d’affichage des éléments n’était pas forcément le même que celui dans lequel il avait été rempli. De
même, lorsqu’on itérait dessus, l’ordre n’était pas garanti. Depuis Python 3.7 (inclus), ce comportement
a changé : un dictionnaire est toujours affiché dans le même ordre que celui utilisé pour le remplir. Et si
on itère sur un dictionnaire, cet ordre est aussi respecté. Ce détail provient de l’implémentation interne des
dictionnaires dans Python, mais cela nous concerne peu. Ce qui importe, c’est de se rappeler qu’on accède
aux éléments par leur clé, et non par leur position telle que le dictionnaire est affiché. Cet ordre n’a
pas d’importance, sauf dans de rares cas.
On peut aussi initialiser toutes les clés et les valeurs d’un dictionnaire en une seule opération :
1 >>> animal2["age"] = 15
2 >>> animal2
3 {'nom': 'singe', 'poids': 70, 'taille': 1.75, 'age': 15}
Après ce premier tour d’horizon, on perçoit l’avantage des dictionnaires : pouvoir retrouver des éléments
par des noms (clés) plutôt que par des indices.
Les humains retiennent mieux les noms que les chiffres. Ainsi, les dictionnaires se révèlent très prat iques
lorsque vous devez manipuler des structures complexes à décrire et que les listes présentent leurs limites.
L’usage des dictionnaires rend en général le code plus lisible. Par exemple, si nous souhaitions stocker
les coordonnées (x, y, z) d’un point dans l’espace, nous pourrions utiliser coors = [0, 1, 2] pour la
version liste et coors = {"x": 0, "y": 1, "z": 2} pour la version dictionnaire. Quelqu’un qui lit le code
comprendra tout de suite que coors["z"] contient la coordonnée z, ce sera moins intuitif avec coors[2].
Conseil
Nous verrons dans le chapitre 14 Conteneurs que plusieurs types d’objets sont utilisables en tant que clé de
dictionnaire.
Malgré cela, nous conseillons de n’utiliser que des chaînes de caractères lorsque vous débutez.
1 >>> [Link]()
2 dict_keys(['poids', 'nom', 'taille'])
3 >>> [Link]()
4 dict_values([70, 'singe', 1.75])
Les mentions dict_keys et dict_values indiquent que nous avons à faire à des objets un peu
particuliers. Ils ne sont pas indexables (on ne peut pas retrouver un élément par indice, par exemple
[Link]()[0] renverra une erreur). Si besoin, nous pouvons les transformer en liste avec la fonction list()
:
1 >>> [Link]()
2 dict_values(['singe', 70, 1.75])
3 >>> list([Link]())
4 ['singe', 70, 1.75]
Toutefois, on peut itérer dessus dans une boucle (on dit qu’ils sont itérables) :
On ne peut pas retrouver un élément par son indice dans un objet dict_items, toutefois on peut itérer dessus :
1 >>> [Link]()[2]
2 Traceback (most recent call last):
3 File "<stdin>", line 1, in <module>
4 TypeError: 'dict_items' object is not subscriptable
5 >>> for key, val in [Link]():
6 ... print(key, val)
7 ...
8 0t
9 1o
10 2t
11 3o
Notez la syntaxe particulière qui ressemble à la fonction enumerate() vue au chapitre 5 Boucles et
comparaisons. On itère à la fois sur key et sur val. Nous aurons l’explication de ce mécanisme dans la
rubrique sur les tuples ci-après.
8.1.6 Existence d’une clé ou d’une valeur
Pour vérifier si une clé existe dans un dictionnaire, on peut utiliser le test d’appartenance avec
l’opérateur in qui renvoie un booléen :
1 >>> animal2 = {'nom': 'singe', 'poids': 70, 'taille': 1.75}
2 >>> "poids" in animal2
3 True
4 >>> if "poids" in animal2:
5 ... print("La clé 'poids' existe pour animal2")
6 ...
7 La clé 'poids' existe pour animal2
8 >>> "age" in animal2
9 False
10 >>> if "age" in animal2:
11 ... print("La clé 'age' existe pour animal2")
12 ...
Dans le second test (lignes 10 à 12), le message n’est pas affiché car la clé age n’est pas présente dans le dictionnaire
animal2.
Si on souhaite tester si une valeur existe dans un dictionnaire, on peut utiliser l’opérateur in avec
l’objet renvoyé par la méthode .values() :
1 >>> [Link]("nom")
2 'singe'
3 >>> [Link]("age")
4 >>>
Ici, la valeur associée à la clé nom est singe, mais la clé age n’existe pas. On peut également indiquer à
.get() une valeur par défaut si la clé n’existe pas :
Définition
Les tuples (« n-uplets » en français) sont des objets séquentiels correspondant aux listes, mais ils
sont toutefois non modifiables. On dit aussi qu’ils sont immuables. Vous verrez ci-dessous que nous les
avons déjà croisés à plusieurs
reprises !
Pratiquement, on utilise les parenthèses au lieu des crochets pour les créer :
L’affectation et l’indiçage fonctionnent comme avec les listes. Mais si on essaie de modifier un des
éléments du tuple (en ligne 10), Python renvoie un message d’erreur car les tuples sont non modifiables.
Si vous voulez ajouter un élément (ou le modifier), vous devez créer un nouveau tuple :
Conseil
Cet exemple montre que les tuples sont peu adaptés lorsqu’on a besoin d’ajouter, retirer, modifier
des éléments. La création d’un nouveau tuple à chaque étape s’avère lourde et il n’y a aucune méthode
pour faire cela puisque les tuples
sont non modifiables. Pour ce genre de tâche, les listes sont clairement mieux adaptées.
Remarque
Pour créer un tuple d’un seul élément comme ci-dessus, utilisez une syntaxe avec une virgule
(element,), pour éviter une ambiguïté avec une simple expression. Par exemple, (2) équivaut à l’entier 2,
alors que l’expression (2,) est un tuple contenant l’élément 2.
Autre particularité des tuples, il est possible de les créer sans les parenthèses, dès lors que ceci ne pose pas
d’ambiguïté avec une autre expression :
Toutefois, afin d’éviter les confusions, nous vous conseillons d’utiliser systématiquement les parenthèses
lorsque vous débutez.
Les opérateurs + et * fonctionnent comme pour les listes (concaténation et duplication) :
1 >>> tuple([1,2,3])
2 (1, 2, 3)
3 >>> tuple("ATGCCGCGAT")
4 ('A', 'T', 'G', 'C', 'C', 'G', 'C', 'G', 'A', 'T')
5 >>> tuple(range(5))
6 (0, 1, 2, 3, 4)
Remarque
Comme la fonction list(), la fonction tuple() prend en argument un objet contenant d’autres
objets. Elle ne fonctionne pas avec les entiers, floats ou booléens. Par exemple, tuple(2) renvoie une
erreur. On en verra plus sur ces
questions dans le chapitre 14 Conteneurs.
1 >>> x, y, z = 1, 2, 3
2 >>> x
3 1
4 >>> y
5 2
6 >>> z
7 3
Attention, le nombre de variables et de valeurs doit être cohérents à gauche et à droite de l’opérateur = :
1 >>> x, y = 1, 2, 3
2 Traceback (most recent call last):
3 File "<stdin>", line 1, in <module>
4 ValueError: too many values to unpack (expected 2)
Il est aussi possible de faire des affectations multiples avec des listes, par exemple :
[x, y, z] = [1, 2, 3].
Toutefois, cette syntaxe est alourdie par la présence des crochets. On préfèrera donc la syntaxe avec
les tuples sans parenthèses.
Remarque
Nous avons appelé l’opération x, y, z = 1, 2, 3 affectation multiple pour signifier que l’on affectait
des valeurs à plusieurs variables en même temps.
Vous pourrez rencontrer aussi l’expression tuple unpacking que l’on pourrait traduire par « désempaquetage de
tuple
». De même, il existe le list unpacking.
Ce terme tuple unpacking provient du fait que l’on peut décomposer un tuple initial de n
éléments en autant de variables différentes en une seule instruction.
Par exemple, si on crée un tuple de trois éléments :
1 >>> tuple1 = (1, 2, 3)
2 >>> tuple1
3 (1, 2, 3)
1 >>> x, y, z = tuple1
2 >>> x
3 1
Cela serait possible également avec l’indiçage, mais il faudrait utiliser autant d’instruction que d’éléments :
1 >>> x = tuple1[0]
2 >>> y = tuple1[1]
3 >>> z = tuple1[2]
Conseil
La syntaxe x, y, z = tuple1 pour désempaqueter un tuple est plus élégante, plus lisible et plus
compacte. Elle sera donc à privilégier.
L’affectation multiple est un mécanisme très puissant et important en Python. Nous verrons qu’il
est particulièrement utile avec les fonctions dans les chapitres 10 Fonctions et 13 Plus sur les
fonctions.
On pourrait concevoir la même chose sur quatre ou cinq éléments, voire plus. La seule contrainte
est d’avoir une correspondance systématique entre le nombre de variables d’itération (par exemple trois
variables dans l’exemple ci-dessus avec x, y, z) et la longueur de chaque sous-tuple de la liste sur laquelle
on itère (dans l’exemple ci-dessus, chaque sous-tuple a trois éléments).
1 >>> 3 / 4
2 0.75
3 >>> 3 // 4
4 0
5 >>> 3 % 4
6 3
7 >>> divmod(3, 4)
8 (0, 3)
En utilisant l’affectation multiple, on peut ainsi récupérer à la volée le quotient et le reste en une seule ligne :
1 >>> 754 / 60
2 12.566666666666666
3 >>> divmod(754, 60)
4 (12, 34)
La division normale nous donne un float en minutes qui n’est pas très pratique, il faut encore convertir
0.566666666666666
minute en secondes et gérer les problèmes d’arrondi. La fonction divmod() renvoie le résultat
directement : 12 min et 34 s. On pourrait raisonner de manière similaire pour convertir des minutes
en heures, des heures en jours, etc.
8.3 Exercices
Conseil
Pour le premier exercice, utilisez l’interpréteur Python. Pour les suivants, créez des scripts puis exécutez-les
dans un
shell.
8.3.1 Prédire la sortie
Soit les deux lignes de code suivantes :
1 dico_notes = {
2 "math": 14, "programmation": 12,
3 "anglais": 16, "biologie": 10,
4 "sport": 19
5 }
Calculez la moyenne de ses notes de deux manières différentes. Calculez à nouveau la moyenne sans la note de biologie.
8.3.3 Composition en acides aminés
En utilisant un dictionnaire, déterminez le nombre d’occurrences de chaque acide aminé dans la séquence
AGWPSGGASAGLAILWGASAI
. Le dictionnaire ne doit contenir que les acides aminés présents dans la séquence.
Vous ne pouvez pas utiliser autant d’instructions if que d’acides aminés différents. Pensez au test d’appartenance.
8.3.4 Convertisseur de secondes
Un athlète court un marathon, malheureusement sa montre ne mesure son temps qu’en secondes.
Celle-ci affiche 11 905. Aidez-le à convertir son temps en heures, minutes et secondes avec la fonction
divmod().
8.3.5 Convertisseur de jours
L’âge de Camille et Céline en jours est respectivement de 8 331 jours et 8 660 jours. Quel est leur
âge en années, mois et jours, en supposant qu’une année compte 365 jours et qu’un mois compte 30 jours
? La fonction divmod() vous aidera à nouveau.
8.3.6 Propriétés des acides aminés
Les acides aminés peuvent être séparés en quatre grandes catégories : apolaires (a), polaires (p), chargés
positivement (+) et chargés négativement (-). Le dictionnaire suivant implémente cette classification
:
1 aa2prop = {'A': 'a', 'V': 'a', 'L': 'a', 'G': 'a', 'I': 'a', 'M': 'a',
2 'W': 'a', 'F': 'a', 'P': 'a',
3 'S': 'p', 'C': 'p', 'N': 'p', 'Q': 'p', 'T': 'p', 'Y': 'p',
4 'D': '-', 'E': '-',
5 'K': '+', 'R': '+', 'H': '+' }
On souhaite convertir la séquence en acide aminé du domaine transmembranaire d’une intégrine humaine
SNADVVYEKQMLYLYVLSGIG
en une série de signes indiquant la nature des acides aminés (a, p, + et -). Affichez tout d’abord la
séquence sur une ligne, puis la nature des acides aminés sur une seconde ligne.
La séquence contient une hélice transmembranaire, donc une succession de résidus apolaires, essayez de
la retrouver visuellement.
Pour cet exercice, nous vous conseillons d’itérer sur la chaîne de caractères contenant la séquence.
Nous reverrons cela dans le chapitre 11 Plus sur les chaînes de caractères.
8.3.7 Boucle sur plusieurs éléments simultanément
À partir de la liste de tuples suivante :
[("chien", 3), ("chat", 4), ("souris", 16)]
affichez chaque animal et son nombre en utilisant qu’une seule boucle for. Attention, pour cet
exercice, il est interdit d’utiliser l’indiçage des listes.
CHAPITRE 9
Modules
9.1 Définition
Les modules sont des programmes Python qui contiennent des fonctions que l’on est amené à souvent
réutiliser (on les appelle aussi bibliothèques, ou libraries en anglais). Ce sont des « boîtes à outils » qui
vous seront très utiles.
Les développeurs de Python ont mis au point de nombreux modules qui effectuent différentes
tâches. Pour cette raison, prenez toujours le réflexe de vérifier si une partie du code que vous souhaitez
écrire n’existe pas déjà sous forme de module.
La plupart de ces modules sont déjà installés dans les versions standards de Python. Vous pouvez
accéder à une documentation exhaustive 1 sur le site de Python. N’hésitez pas à explorer un peu ce
site, la quantité de modules disponibles est impressionnante (plus de 300 modules).
76
9.2. Importation de modules Chapitre 9. Modules
En résumé, l’utilisation de la syntaxe import module permet d’importer tout une série de fonctions organisées
par
« thèmes ». Par exemple, les fonctions gérant les nombres aléatoires avec random et les fonctions mathématiques
avec
math. Python possède de nombreux autres modules internes (c’est-à-dire présent de base lorsqu’on installe Python).
Remarque
Dans le chapitre 3 Affichage, nous avons introduit la syntaxe [Link]() avec truc étant un objet et .bidule()
une méthode. Nous vous avions expliqué qu’une méthode était une fonction un peu particulière :
• elle était liée à un objet par un point ;
• en général, elle agissait sur ou utilisait l’objet auquel elle était liée.
Par exemple, la méthode .append() vue dans le chapitre 4 Listes. Dans l’instruction [Link](3), la méthode
.append() ajoute l’entier 3 à l’objet liste1 auquel elle est liée.
Avec les modules, nous rencontrons une syntaxe similaire. Par exemple, dans l’instruction
[Link](), on pourrait penser que .cos() est aussi une méthode. En fait la documentation officielle de
Python 3 précise bien que dans ce cas
.cos() est une fonction. Dans cet ouvrage, nous utiliserons ainsi le mot fonction lorsqu’on évoquera des
fonctions issues de modules.
Si cela vous parait encore ardu, ne vous inquiétez pas : c’est à force de pratiquer et de lire que vous vous
approprierez le vocabulaire. La syntaxe [Link]() est là pour rappeler de quel module
provient la fonction en un coup d’œil.
1 import random
puis d’appeler explicitement les fonctions voulues, par exemple :
Il est également possible de définir un alias (un nom plus court) pour un module :
1 >>> import random as rand
2 >>> [Link](1, 10)
3 6
4 >>> [Link](1, 3)
5 2.643472616544236
Dans cet exemple, les fonctions du module random sont accessibles via l’alias
rand. Enfin, pour vider de la mémoire un module déjà chargé, on peut utiliser
l’instruction del :
NAME
random - Random variable generators.
MODULE REFERENCE
[Link]
DESCRIPTION
integers
uniform within
range sequences
pick random
element pick
random sample
Remarque
• Pour vous déplacer dans l’aide, utilisez les flèches du haut et du bas pour le parcourir ligne par ligne, ou les
touches
Page-up et Page-down pour faire défiler l’aide page par page.
• Pour quitter l’aide, appuyez sur la touche Q.
• Pour chercher du texte, tapez le caractère / puis le texte que vous cherchez, puis la touche Entrée.
Par exemple, pour chercher l’aide sur la fonction randint(), tapez /randint puis Entrée.
9.4. Quelques modules courants Chapitre 9. Modules
• Vous pouvez également obtenir de l’aide sur une fonction particulière d’un module avec :
help([Link])
La commande help() est en fait une commande plus générale, permettant d’avoir de l’aide sur
n’importe quel objet chargé en mémoire :
1 >>> t = [1, 2, 3]
2 >>> help(t)
3 Help on list object:
4
5 class list(object)
6 | list() -> new list
7 | list(sequence) -> new list initialized from sequence's items
8 |
9 | Methods defined here:
10 |
11 | add (...)
12 | x. add (y) <==> x+y
13 |
14 |
15 ...
Enfin, pour connaître d’un seul coup d’œil toutes les méthodes ou variables associées à un objet, utilisez la
fonction
dir() :
1 >>> x = [1, 2, 3, 4]
2 >>> [Link](x)
3 >>> x
4 [2, 3, 1, 4]
5 >>> [Link](x)
6 >>> x
7 [4, 2, 1, 3]
La fonction choices() (avec un s à la fin) réalise plusieurs tirages aléatoires (avec remise, c’est-à-dire
qu’on peut piocher plusieurs fois le même élément) dans une liste donnée. Le nombre de tirages est
précisé par le paramètre k :
Définition
En informatique, la génération de nombres aléatoires est un problème complexe. On utilise plutôt des
« générateurs de nombres pseudo-aléatoires 14 ». Pour cela, une graine aléatoire 15 doit être définie. Cette
graine est la plupart du temps un nombre entier qu’on passe au générateur : celui-ci va alors produire une
série donnée de nombres pseudo-aléatoires qui dépendent de cette graine. Si on change la graine, la
série de nombres change.
9.6. Module sys : passage d’arguments Chapitre 9. Modules
1 >>> [Link](42)
2 >>> [Link](0, 10)
3 1
4 >>> [Link](0, 10)
5 0
6 >>> [Link](0, 10)
7 4
Ici, la graine aléatoire est fixée à 42. Si on ne précise pas la graine, par défaut Python utilise la date (plus
précisément, il s’agit du nombre de secondes écoulées depuis une date fixe passée). Ainsi, à chaque fois qu’on
relance Python, la graine sera différente, car ce nombre de secondes sera différent.
Si vous exécutez ces mêmes lignes de code (depuis l’instruction [Link](42)), il se peut que
vous ayez des résultats différents selon la version de Python. Néanmoins, vous devriez systématiquement
obtenir les mêmes résultats si vous relancez plusieurs fois de suite ces instructions sur une même
machine.
Remarque
Quand on utilise des nombres aléatoires, il est fondamental de connaitre la distribution de
probablités utilisée par la fonction.
Par exemple, la fonction de base du module random est [Link](), elle renvoie un float
aléatoire entre 0 et 1 tiré dans une distribution uniforme. Si on tire beaucoup de nombres, on aura la
même probabilité d’obtenir tous les nombres possibles entre 0 et 1. La fonction [Link]() tire
aussi un entier dans une distribution uniforme. La fonction [Link]() tire quant à elle un float
aléatoire dans une distribution gaussienne.
1 import sys
2 print([Link])
Ensuite, dans un shell, exécutons le script [Link] suivi de plusieurs arguments :
1 import sys
2
3 if len([Link]) != 2:
4 [Link]("ERREUR : il faut exactement un argument.")
5
6 print(f"Argument vaut : {[Link][1]}")
Puis on l’exécute sans argument :
$ python [Link]
ERREUR : il faut exactement un argument.
avec un seul argument :
$ python [Link] 42
Argument vaut : 42
puis avec plusieurs :
Remarque
On vérifie dans cet exemple que le script possède deux arguments, car le nom du script lui-même
compte pour un argument (le tout premier).
L’intérêt de récupérer des arguments passés dans la ligne de commande à l’appel du script est de
pouvoir ensuite les utiliser dans le script.
Voici comme nouvel exemple le script compte_lignes.py, qui prend comme argument le nom
d’un fichier puis affiche le nombre de lignes qu’il contient :
1 import sys
2
3 if len([Link]) != 2:
4 [Link]("ERREUR : il faut exactement un argument.")
5
6 nom_fichier = [Link][1]
7 taille = 0
8 with open(nom_fichier, "r") as f_in:
9 taille = len(f_in.readlines())
10
11 print(f"{nom_fichier} contient {taille} lignes.")
Supposons que dans le même répertoire, nous ayons le fichier [Link] dont voici le contenu :
giraf
e
tigre
singe
souri
s
et le fichier [Link] qui contient :
poisso
n
abeill
e chat
$ python compte_lignes.py
ERREUR : il faut exactement un argument.
$ python compte_lignes.py
[Link] [Link]
contient 4 lignes.
$ python compte_lignes.py
[Link] [Link]
contient 3 lignes.
$ python compte_lignes.py [Link]
[Link] ERREUR : il faut exactement un
argument.
Notre script est donc capable de :
• vérifier si un argument lui est donné et si ce n’est pas le cas d’afficher un message d’erreur ;
• d’ouvrir le fichier dont le nom est fourni en argument, de compter puis d’afficher le nombre de lignes.
9.7. Module pathlib : gestion des fichiers et des répertoires Chapitre 9. Modules
Par contre, le script ne vérifie pas si le fichier fourni en argument existe bien :
$ python compte_lignes.py
[Link] Traceback (most
recent call last):
File "compte_lignes.py", line 8, in
<module> with open(nom_fichier,
"r") as f_in:
^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '[Link]'
La lecture de la partie suivante va nous permettre d’améliorer notre script compte_lignes.py.
1 >>> str(Path().cwd())
2 '/home/pierre'
Mais l’intérêt de récupérer un objet de type PosixPath est qu’on peut ensuite utiliser les méthodes .name et .parent
pour obtenir respectivement le nom du répertoire (sans son chemin complet) et le répertoire parent :
1 >>> Path().cwd()
2 PosixPath('/home/pierre')
3 >>> Path().cwd().name
4 'pierre'
5 >>> Path().cwd().parent
6 PosixPath('/home')
Enfin, la méthode .iterdir() liste le contenu du répertoire depuis lequel est lancé Python :
1 >>> list(Path().iterdir())
2 [PosixPath('[Link]'), PosixPath('tests'), PosixPath('[Link]')]
Tout comme la fonction range() (voir le chapitre 4 Listes), la méthode .iterdir() est un itérateur. La fonction
list() permet d’obtenir une liste.
Toutefois, il est possible d’itérer très facilement sur le contenu d’un répertoire et de savoir s’il
contient des fichiers ou des sous-répertoires :
1 >>> for nom in Path().iterdir():
2 ... if nom.is_file():
3 ... print(f"{nom} est un fichier")
4 ... else:
5 ... print(f"{nom} n'est pas un fichier")
6 ...
7 [Link] est un fichier
8 tests n'est pas un fichier
9 [Link] est un fichier
La méthode .is_file() renvoie True si l’objet est un fichier, et False si ce n’est pas le cas.
La méthode .iterdir() parcourt le contenu d’un répertoire, sans en explorer les éventuels sous-
répertoires. Si on souhaite parcourir récursivement un répertoire, on utilise la méthode .glob(). Prenons
l’arborescence suivante comme exemple :
[Link]
b
[Link]
tests
├── [Link]
├── [Link]
└── [Link]
Le répertoire courant contient les fichiers [Link] et [Link], ainsi que le répertoire tests. Ce
dernier contient lui-même les fichiers [Link], [Link] et [Link].
On souhaite maintenant lister tous les scripts Python (dont l’extension est .py) présents dans le
répertoire courant et dans ses sous-répertoires :
9.8 Exercices
Conseil
Pour les trois premiers exercices, utilisez l’interpréteur Python. Pour les exercices suivants, créez
des scripts puis exécutez-les dans un shell.
Conseil
• Jetez un oeil à la rubrique sur la comparaison de floats abordée dans le chapitre 6 Tests.
• Les constantes π et e sont obtenues par [Link] et math.e.
Conseil
Pour générer la séquence d’ADN, utilisez la fonction [Link]() abordée dans l’exercice précédent.
$ python compte_lignes.py
[Link] [Link]
contient 4 lignes.
$ python compte_lignes.py
[Link] [Link]
contient 3 lignes.
$ python compte_lignes.py
[Link] ERREUR :
[Link] n'existe pas.
9.8.11 Détermination du nombre pi par la méthode Monte Carlo (exercice +++)
Soit un cercle de rayon 1 (en trait plein sur la figure 9.1) inscrit dans un carré de côté 2 (en
trait pointillé). Avec R = 1, l’aire du carré vaut (2R)2 soit 4 et l’aire du disque délimité par le
cercle vaut πR2 soit π.
En choisissant N points aléatoires (à l’aide d’une distribution uniforme) à l’intérieur du carré, la
probabilité que ces points se trouvent aussi dans le cercle est :
p = aire du cercle = π
aire du carré 4
Soit n, le nombre de points effectivement dans le cercle, il vient alors
n π
p= = ,
N 4
d’où
n
π=4 × .
N
Déterminez une approximation de π par cette méthode. Pour cela, pour N itérations :
FIGURE 9.1 – Cercle de rayon 1 inscrit dans un carré de côté 2.
1. Choisissez aléatoirement les coordonnées x et y d’un point entre -1 et 1. Utilisez la fonction uniform() du module
random.
2. Calculez la distance entre le centre du cercle et ce point.
3. Déterminez si cette distance est inférieure au rayon du cercle, c’est-à-dire si le point est dans le cercle ou pas.
4. Si le point est effectivement dans le cercle, incrémentez le compteur n.
Finalement calculez le rapport entre n et N et proposez une estimation de π. Quelle valeur de π
obtenez-vous pour 100 itérations ? 1000 itérations ? 10 000 itérations ? Comparez les valeurs obtenues à la
valeur de π fournie par le module math.
On rappelle que la distance d entre deux points A et B de coordonnées respectives (xA, yA) et (xB,
yB) se calcule comme :
√
d = (xB − xA)2 + (yB − yA)2
Fonctions
1 >>> ma_liste.append(5)
1. Vous passez l’entier 5 en argument ;
2. la méthode append() ajoute l’entier 5 à l’objet ma_liste;
3. et elle ne renvoie rien.
Aux yeux du programmeur, au contraire, une fonction est une portion de code effectuant une suite
d’instructions bien particulière. Mais avant de vous présenter la syntaxe et la manière de construire une
fonction, revenons une dernière fois sur cette notion de « boîte noire » :
• Une fonction effectue une tâche. Pour cela, elle reçoit éventuellement des arguments et renvoie
éventuellement quelque chose. L’algorithme utilisé au sein de la fonction n’intéresse pas
directement l’utilisateur. Par exemple, il est inutile de savoir comment la fonction [Link]()
calcule un cosinus. On a juste besoin de savoir qu’il faut lui passer en argument un angle en radian,
et qu’elle renvoie le cosinus de cet angle. Ce qui se passe à l’intérieur de la fonction ne regarde
que le programmeur.
• Chaque fonction effectue en général une tâche unique et précise. Si cela se complique, il est plus judicieux
d’écrire plusieurs fonctions (qui peuvent éventuellement s’appeler les unes les autres). Cette modularité
améliore la qualité générale et la lisibilité du code. Vous verrez qu’en Python, les fonctions
présentent une grande flexibilité.
Pour finir sur les généralités, nous avons utilisé dans la Figure ci-dessus le terme programme
principal (main en anglais), pour désigner l’endroit depuis lequel on appelle une fonction (on verra
plus tard que l’on peut en fait appeler une fonction de n’importe où). Le programme principal désigne
le code qui est exécuté lorsqu’on lance le script Python, c’est-à-dire toute la suite d’instructions en
dehors des fonctions. En général, dans un script Python, on écrit d’abord les fonctions, puis le
programme principal. Nous aurons l’occasion de revenir sur cette notion de programme principal plus
tard dans ce chapitre, ainsi que dans le chapitre 13 Plus sur les fonctions.
10.2 Définition
Pour définir une fonction, Python utilise le mot-clé def. Si on souhaite que la fonction renvoie quelque
chose, il faut utiliser le mot-clé return. Par exemple :
Dans ce cas, la fonction hello() se contente d’afficher la chaîne de caractères "bonjour" à l’écran.
Elle ne prend aucun argument et ne renvoie rien. Par conséquent, cela n’a pas de sens de vouloir récupérer
dans une variable le résultat renvoyé par une telle fonction. Si on essaie tout de même, Python affecte la
valeur None qui signifie rien en anglais :
Renvoyer un tuple ou une liste de deux éléments (ou plus) est très pratique en conjonction avec
l’affectation multiple, par exemple :
1 def est_pair(x):
2 if x % 2 == 0:
3 return True
4 else:
5 return False
6
7 # Programme principal.
8 for chiffre in range(1, 5):
9 if est_pair(chiffre):
10 print(f"{chiffre} est pair")
Comme la fonction renvoie un booléen, on peut utiliser la notation if est_pair(chiffre): qui
équivaut à if est_pair(chiffre) == True:. Il est courant d’appeler une fonction qui renvoie un booléen
est_quelquechose() car on comprend que ça pose la question si c’est vrai ou faux. En anglais, on
trouvera la notation is_even(). Nous reverrons ces notions dans le chapitre 13 Plus sur les
fonctions.
Définition
Lorsqu’on définit une fonction def fct(x, y): les arguments x et y sont appelés arguments positionnels
(en anglais, positional arguments). Il est strictement obligatoire de les préciser lors de l’appel de la
fonction. De plus, il est nécessaire de respecter le même ordre lors de l’appel que dans la définition de
la fonction. Dans l’exemple ci-dessus, 2
correspondra à x et 3 correspondra à y. Finalement, tout dépendra de leur position, d’où leur qualification de
positionnel.
Mais il est aussi possible de passer un ou plusieurs argument(s) de manière facultative et de leur
attribuer une valeur par défaut :
1 >>> def fct(x=1):
2 ... return x
3 ...
4 >>> fct()
5 1
6 >>> fct(10)
7 10
Définition
Un argument défini avec une syntaxe def fct(arg=val): est appelé argument par mot-clé (en anglais,
keyword argument). Le passage d’un tel argument lors de l’appel de la fonction est facultatif. Ce type
d’argument ne doit pas être confondu avec les arguments positionnels présentés ci-dessus, dont la
syntaxe est def fct(arg):.
On observe que pour l’instant, les arguments par mot-clé sont pris dans l’ordre dans lesquels on
les passe lors de l’appel. Comment faire si l’on souhaitait préciser l’argument par mot-clé z et garder
les valeurs de x et y par défaut ? Simplement en précisant le nom de l’argument lors de l’appel :
1 >>> fct(z=10)
2 (0, 0, 10)
Python permet même de rentrer les arguments par mot-clé dans un ordre arbitraire :
Que se passe-t-il lorsque nous avons un mélange d’arguments positionnels et par mot-clé ? Et bien
les arguments positionnels doivent toujours être placés avant les arguments par mot-clé :
On peut toujours passer les arguments par mot-clé dans un ordre arbitraire à partir du moment
où on précise leur nom. Par contre, si les deux arguments positionnels a et b ne sont pas passés à la
fonction, Python renvoie une erreur.
1 >>> fct(z=0)
2 Traceback (most recent call last):
3 File "<stdin>", line 1, in <module>
4 TypeError: fct() missing 2 required positional arguments: 'a' and 'b'
Conseil
Préciser le nom des arguments par mot-clé lors de l’appel d’une fonction est une pratique que nous vous recommandons.
Cela les distingue clairement des arguments positionnels.
L’utilisation d’arguments par mot-clé est habituelle en Python. Elle permet de modifier le
comportement par défaut de nombreuses fonctions. Par exemple, si on souhaite que la fonction print()
n’affiche pas un retour à la ligne, on peut utiliser l’argument end :
Pour la suite des explications, nous allons utiliser l’excellent site Python Tutor 1 qui permet de
visualiser l’état des variables au fur et à mesure de l’exécution d’un code Python. Avant de poursuivre,
nous vous conseillons de prendre 5 minutes pour tester ce site.
Regardons maintenant ce qui se passe dans le code ci-dessus, étape par étape :
• Étape 1 : Python est prêt à lire la première ligne de code.
• Étape 2 : Python met en mémoire la fonction carre(). Notez qu’il ne l’exécute pas ! La fonction
est mise dans un espace de la mémoire nommé Global frame, il s’agit de l’espace du programme
principal. Dans cet espace seront stockées toutes les variables globales créées dans le programme.
Python est maintenant prêt à exécuter le programme principal.
• Étape 3 : Python lit et met en mémoire la variable var. Celle-ci étant créée dans le programme
principal, il s’agira d’une variable globale. Ainsi, elle sera également stockée dans le Global
frame.
• Étape 4 : La fonction carre() est appelée et on lui passe en argument l’entier var. La fonction
s’exécute et un nouveau cadre est créé dans lequel Python Tutor va indiquer toutes les variables
locales à la fonction. Notez bien que la variable passée en argument, qui s’appelle x dans la fonction,
est créée en tant que variable locale. On remarquera aussi que les variables globales situées dans
le Global frame sont toujours là.
• Étape 5 : Python est maintenant prêt à exécuter chaque ligne de code de la fonction.
• Étape 6 : La variable y est créée dans la fonction. Celle-ci est donc stockée en tant que variable locale à la fonction.
• Étape 7 : Python s’apprête à renvoyer la variable locale y au programme principal. Python Tutor
nous indique le contenu de la valeur renvoyée.
• Étape 8 : Python quitte la fonction et la valeur renvoyée par celle-ci est affectée à la variable
globale resultat. Notez bien que lorsque Python quitte la fonction, l’espace des variables alloué à
la fonction est détruit. Ainsi, toutes les variables créées dans la fonction n’existent plus. On
comprend pourquoi elles portent le nom de locales puisqu’elles n’existent que lorsque la fonction
est exécutée.
D’ici là, essayez de vous entraîner au maximum avec les fonctions. C’est un concept ardu, mais il est
impératif de le maîtriser.
Enfin, comme vous avez pu le constater, Python Tutor nous a grandement aidé à comprendre ce
qui se passait. N’hésitez pas à l’utiliser sur des exemples ponctuels, ce site vous aidera à visualiser ce qui
se passe lorsqu’un code ne fait pas ce que vous attendez.
L’acronyme DRY 2 signifie Don’t Repeat Yourself. Les fonctions permettent de satisfaire ce
principe en évitant la duplication de code. En effet, plus un code est dupliqué plusieurs fois dans un
programme, plus il sera source d’erreurs, notamment lorsqu’il faudra le faire évoluer.
Considérons par exemple le code suivant qui convertit plusieurs températures des degrés Fahrenheit en degrés Celsius
:
1 >>> temp_in_fahrenheit = 60
2 >>> (temp_in_fahrenheit - 32) * (5/8)
3 17.5
4 >>> temp_in_fahrenheit = 80
5 >>> (temp_in_fahrenheit - 32) * (5/8)
6 30.0
7 >>> temp_in_fahrenheit = 100
8 >>> (temp_in_fahrenheit - 32) * (5/8)
9 42.5
Malheureusement, il y a une erreur dans la formule de conversion. En effet, la formule exacte est :
5
temp_celsius = (temp_fahrenheit − 32) ×
9
Il faut alors reprendre les lignes 2, 5 et 8 précédentes et les corriger. Cela n’est pas efficace, surtout si
le même code est utilisé à différents endroits dans le programme.
En écrivant qu’une seule fois la formule de conversion dans une fonction, on applique le principe DRY :
10.8 Exercices
Conseil
Pour le premier exercice, utilisez Python Tutor. Pour les exercices suivants, créez des scripts puis
exécutez-les dans un shell.
10.8.1 Carré et factorielle
Reprenez l’exemple précédent à l’aide du site Python Tutor :
1 # Définition d'une fonction carre().
2 def carre(x):
3 y = x**2
4 return y
5
6 # Programme principal.
7 z=5
8 resultat = carre(z)
9 print(resultat)
1 def calc_factorielle(n):
2 fact = 1
3 for i in range(2, n+1):
4 fact = fact * i
5 return fact
6
7 # Programme principal.
8 nb = 4
9 factorielle_nb = calc_factorielle(nb)
10 print(f"{nb}! = {factorielle_nb}")
11 nb2 = 10
12 print(f"{nb2}! = {calc_factorielle(nb2)}")
Testez ensuite cette portion de code avec Python Tutor, en cherchant à bien comprendre chaque
étape. Avez-vous réussi à prédire la sortie correctement ?
Remarque
Une remarque concernant l’utilisation des f-strings que nous avions abordées dans le chapitre 3 Affichage.
On découvre ici une autre possibilité des f-strings dans l’instruction f"{nb2}! =
{calc_factorielle(nb2)}" : il est en effet possible d’appeler entre les accolades une fonction (ici
{calc_factorielle(nb2)}) ! Ainsi, il n’est pas nécessaire de créer une variable intermédiaire dans
laquelle on stocke ce que retourne la fonction.
10.8.2 Puissance
Créez une fonction calc_puissance(x, y) qui renvoie xy en utilisant l’opérateur **. Pour rappel :
1 >>> 2**2
2 4
34 >>>
8 2**3
2^5 0 >=>> 2**41
2^6 1 1=6 2
2^ 2 = 4
[...]
2^20 = 1048576
Dans le programme principal, calculez et affichez à l’écran 2i avec i variant de 0 à 20 inclus. On
souhaite que le résultat soit présenté avec le formatage suivant :
10.8.3 Pyramide
Reprenez l’exercice du chapitre 5 Boucles et comparaisons qui dessine une pyramide.
Dans un script [Link], créez une fonction gen_pyramide() à laquelle vous passez un nombre
entier N et qui renvoie une pyramide de N lignes sous forme de chaîne de caractères. Le programme
principal demandera à l’utilisateur le nombre de lignes souhaitées (utilisez pour cela la fonction
input()) et affichera la pyramide à l’écran.
10.8.4 Nombres premiers
Reprenez l’exercice du chapitre 6 Tests sur les nombres premiers.
Créez une fonction est_premier() qui prend comme argument un nombre entier positif n
(supérieur à 2), et qui renvoie le booléen True si n est premier et False si n n’est pas premier.
Déterminez tous les nombres premiers de 2 à
100. On souhaite avoir une sortie similaire à celle-ci :
2 est premier
3 est premier
4 n'est pas premier
[...]
100 n'est pas premier
10.8.6 Distance 3D
Créez une fonction calc_distance_3D() qui calcule la distance euclidienne en trois dimensions entre deux atomes.
√
Testez votre fonction sur les 2 points A(0,0,0) et B(1,1,1). Trouvez-vous bien 3 ?
On rappelle que la distance euclidienne d entre deux points A et B de coordonnées cartésiennes
respectives (x A, yA, zA) et (xB, yB, zB) se calcule comme suit :
√
d = (xB − xA)2 + (yB − yA)2 + (zB − zA)2
Créez une autre fonction calc_dist2ori(), à laquelle vous passez en argument deux listes de floats
list_x et list_y représentant les coordonnées d’une fonction mathématique (par exemple x et sin(x)).
Cette fonction renverra une liste de floats représentant la distance entre chaque point de la fonction et
l’origine (de coordonnées (0, 0)).
La figure 10.2 montre un exemple sur quelques points de la fonction sin(x) (courbe en trait épais).
Chaque trait pointillé représente la distance que l’on cherche à calculer entre les points de la courbe et
l’origine du repère de coordonnées (0, 0).
Enfin, pour visualiser votre résultat, ajoutez le code suivant tout à la fin de votre script :
1 # Création d'une image pour la visualisation du résultat.
2 import [Link] as plt
3
4 x = []
5 y = []
6 with open("[Link]", "r") as f_in:
7 for line in f_in:
8 coords = [Link]()
9 [Link](float(coords[0]))
10 [Link](float(coords[1]))
11 fig, ax = [Link](figsize=(6, 6))
12 [Link](x, y)
13 ax.set_xlabel("x")
14 ax.set_ylabel("Distance de sin(x) à l'origine")
15 [Link]("[Link]")
Remarque
Le module matplotlib sera expliqué en détail dans le chapitre 21 Module matplotlib.
CHAPITRE 11
11.1 Préambule
Nous avons déjà abordé les chaînes de caractères dans les chapitres 2 Variables et 3 Affichage. Ici nous
allons un peu plus loin, notamment avec les méthodes associées aux chaînes de caractères 1.
Nous pouvons donc utiliser certaines propriétés des listes comme les tranches :
Mais a contrario des listes, les chaînes de caractères présentent toutefois une différence notable, ce
sont des listes non modifiables. Une fois une chaîne de caractères définie, vous ne pouvez plus modifier
un de ses éléments. Le cas échéant, Python renvoie un message d’erreur :
1. [Link]
102
1 >>> animaux = "girafe tigre"
2 >>> animaux[4]
3 'f'
4 >>> animaux[4] = "F"
5 Traceback (most recent call last):
6 File "<stdin>", line 1, in <module>
7 TypeError: 'str' object does not support item assignment
Par conséquent, si vous voulez modifier une chaîne de caractères, vous devez en construire une
nouvelle. Pour cela, n’oubliez pas que les opérateurs de concaténation (+) et de duplication (*)
(introduits dans le chapitre 2 Variables) peuvent vous aider. Vous pouvez également générer une
liste, qui elle est modifiable, puis revenir à une chaîne de caractères (voir plus bas).
Quand on souhaite écrire un texte sur plusieurs lignes, il est très commode d’utiliser les guillemets triples
qui conservent le formatage (notamment les retours à la ligne) :
1 >>> x = """souris
2 ... chat
3 ... abeille"""
4 >>> x
5 'souris\nchat\nabeille'
6 >>> print(x)
7 souris
8 chat
9 abeille
Attention, les caractères spéciaux n’apparaissent intérprétés que lorsqu’ils sont utilisés avec la
fonction print(). Par exemple, le \n n’apparait comme un retour à la ligne que lorsqu’il est dans une chaîne
de caractères passée à la fonction print() :
1 >>> "bla\nbla"
2 'bla\nbla'
3 >>> print("bla\nbla")
4 bla
5 bla
Remarque
Un stringprefix modifie la manière dont Python va interpréter ladite string. Celui-ci doit être
systématiquement « collé » à la chaîne de caractères, c’est-à-dire sans espace entre les deux.
Il existe différents stringprefixes en Python, nous vous montrons ici les deux qui nous apparaissent les plus
importants.
• Le préfixe r mis pour raw string, qui force la non-interprétation des caractères spéciaux :
L’ajout du r va forcer Python à ne pas interpréter le \n comme un retour à la ligne, mais comme un
backslash littéral suivi d’un n. Quand on demande à l’interpréteur d’afficher cette chaîne de
caractères, celui-ci met deux backslashes pour signifier qu’il s’agit d’un backslash littéral (le premier
échappe le second). Finalement, l’utilisation de la syntaxe r"Voici un retour à la ligne\nEt là une autre
ligne" renvoie une chaîne de caractères normale, puisqu’on voit ensuite que le r a disparu lorsqu’on
demande à Python d’afficher le contenu de la variable s. Comme dans var = 2
+ 2, d’abord Python évalue 2 + 2. Puis ce résultat est affecté à la variable var. Enfin, on notera que
seule l’utilisation du print() mène à l’interprétation des caractères spéciaux comme \n, comme expliqué
dans la rubrique précédente.
Les caractères spéciaux non interprétés dans les raw strings sont de manière générale tout ce dont le
backslash modifie la signification, par exemple un \n, un \t, etc.
• Le préfixe f mis pour formatted string, qui met en place l’écriture formatée comme vue au chapitre 3 Affichage :
La f-string remplace le contenu des variables situées entre les accolades et interprète le \n comme
un retour à la ligne. Pour rappel, consultez le chapitre 3 si vous souhaitez plus de détails sur le
fonctionnement des f-strings.
Conseil
Il existe de nombreux autres détails concernant les préfixes qui vont au-delà de ce cours. Pour en
savoir plus, vous pouvez consulter la documentations officielle 2.
11.5. Méthodes associées aux chaînes de caractères Chapitre 11. Plus sur les chaînes de caractères
1 >>> x = "girafe"
2 >>> [Link]()
3 'GIRAFE'
4 >>> x
5 'girafe'
6 >>> 'TIGRE'.lower()
7 'tigre'
Les méthodes .lower() et .upper() renvoient un texte en minuscule et en majuscule respectivement. On
remarque que l’utilisation de ces méthodes n’altère pas la chaîne de caractères de départ, mais renvoie
une chaîne de caractères transformée.
Pour mettre en majuscule la première lettre seulement, vous pouvez faire :
1 >>> [Link]()
2 'Girafe'
Il existe une méthode associée aux chaînes de caractères qui est particulièrement pratique, la méthode .split() :
Définition
Un espace blanc 4 (whitespace en anglais) correspond aux caractères qui sont invisibles à l’œil, mais
qui occupent de l’espace dans un texte. Les espaces blancs les plus classiques sont l’espace, la
tabulation et le retour à la ligne.
Si l’élément recherché est trouvé, alors l’indice du début de l’élément dans la chaîne de caractères
est renvoyé. Si l’élément n’est pas trouvé, alors la valeur -1 est renvoyée.
Si l’élément recherché est trouvé plusieurs fois, seul l’indice de la première occurrence est renvoyé :
On trouve aussi la méthode .replace() qui substitue une chaîne de caractères par une autre :
La méthode .count() compte le nombre d’occurrences d’une chaîne de caractères passée en argument :
La méthode .startswith() vérifie si une chaîne de caractères commence par une autre chaîne de caractères :
Cette méthode est particulièrement utile lorsqu’on lit un fichier et que l’on veut récupérer certaines lignes
commençant par un mot-clé. Par exemple dans un fichier PDB, les lignes contenant les coordonnées des
atomes commencent par le mot-clé ATOM.
Enfin, la méthode .strip() permet de « nettoyer les bords » d’une chaîne de caractères :
1 >>> chaine = " Comment enlever les espaces au début et à la fin ? "
2 >>> [Link]()
3 'Comment enlever les espaces au début et à la fin ?'
La méthode .strip() enlève les espaces situés sur les bords de la chaîne de caractère mais pas ceux situés
entre des caractères visibles. En réalité, cette méthode enlève n’importe quel combinaison « d’espace(s)
blanc(s) » sur les bords, par exemple :
1 >>> chaine = " \tfonctionne avec les tabulations et les retours à la ligne\n"
2 >>> [Link]()
3 'fonctionne avec les tabulations et les retours à la ligne'
Cette méthode est utile pour se débarrasser des retours à la ligne quand on lit un fichier.
11.6. Extraction de valeurs numériques d’une chaîne de caractères Chapitre 11. Plus sur les chaînes de
caractères
Remarque
Retenez bien l’utilisation des instructions précédentes pour extraire des valeurs numériques d’une chaîne de
caractères.
Elles sont régulièrement employées pour analyser des données extraites d’un fichier.
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
La fonction map() permet d’appliquer une fonction à plusieurs éléments d’un objet itérable. Par
exemple, si on a une chaîne de caractères avec trois entiers séparés par des espaces, on peut extraire et
convertir les trois nombres en entier en une seule ligne. La fonction map() produit un objet de type map
qui est itérable et transformable en liste :
1 >>> ligne = "67 946 -45"
2 >>> [Link]()
3 ['67', '946', '-45']
4 >>> map(int, [Link]())
5 <map object at 0x7fa34e573b20>
6 >>> for entier in map(int, [Link]()):
7 ... print(entier)
8 ...
9 67
10 946
11 -45
12 >>> list(map(int, [Link]()))
13 [67, 946, -45]
Remarque
La fonction map() prend deux arguments. Le second est un objet itérable, souvent une liste
comme dans notre exemple. Le premier argument est le nom d’une fonction qu’on souhaite appliquer à
chaque élément de la liste, mais sans les parenthèses (ici int et non pas int()). Une fonction passée en
argument d’une autre fonction est appelée fonction de rappel 5 ou callback en anglais. Nous reverrons
cette notion dans le chapitre 25 Fenêtres graphiques et Tkinter (en ligne).
La fonction map() est particulièrement utile lorsqu’on lit un fichier de valeurs numériques. Par
exemple, si on a un fichier [Link] contenant trois colonnes de nombres, map() en conjonction avec .split()
permet de séparer les trois nombres puis de les convertir en float en une seule ligne de code :
Sans map(), il aurait fallu une ligne pour séparer les données x, y, z = [Link]() et une autre pour
les transformer en float x, y, z = float(x), float(y), float(z).
Enfin, on peut utiliser map() avec ses propres fonctions :
L’opérateur in teste si une chaîne de caractères fait partie d’une autre chaîne de caractères :
Notez que la chaîne testée peut-être présente à n’importe quelle position dans l’autre chaîne. Par
ailleurs, le test est vrai si elle est présente une ou plusieurs fois.
La variation avec l’opérateur booléen not permet de vérifier qu’une chaîne n’est pas présente dans une autre chaîne
:
11.9 Conversion d’une liste de chaînes de caractères en une chaîne de ca- ractères
On a vu dans le chapitre 2 Variables la conversion d’un type simple (entier, float et chaîne de
caractères) en un autre avec les fonctions int(), float() et str(). La conversion d’une liste de chaînes de
caractères en une chaîne de caractères est moins intuitive. Elle fait appelle à la méthode .join() :
11.10. Method chaining Chapitre 11. Plus sur les chaînes de
caractères
Les éléments de la liste initiale sont concaténés les uns à la suite des autres et intercalés par un
séparateur, qui peut être n’importe quelle chaîne de caractères. Ici, on a utilisé un tiret, un espace et
rien (une chaîne de caractères vide).
Attention, la méthode .join() ne s’applique qu’à une liste de chaînes de caractères :
On espère qu’après ce petit tour d’horizon vous serez convaincu de la richesse des méthodes associées aux
chaînes de caractères. Pour avoir une liste exhaustive de l’ensemble des méthodes associées à une variable
particulière, vous pouvez utiliser la fonction dir() :
>>> help([Link])
Help on built-in function split:
split(...)
[Link]([sep [,maxsplit]]) -> list of strings
Attention à ne pas mettre les parenthèses à la suite du nom de la méthode. L’instruction correcte est
help(animaux
.split) et non pas help([Link]()).
Il existe de nombreuses méthodes pour traiter les chaînes de caractères. Ces méthodes renvoient la
plupart du temps une chaîne de caractères modifiée.
Par exemple, si on souhaite mettre une majuscule à tous les mots d’une chaîne de caractères, puis
remplacer un mot par un autre, puis transformer cette chaîne de caractères en une liste de chaînes de
caractères, on peut écrire :
1 >>> message = "salut patrick salut pierre"
2 >>> message1 = [Link]()
3 >>> message1
4 'Salut Patrick Salut Pierre'
5 >>> message2 = [Link]("Salut", "Bonjour")
6 >>> message2
7 'Bonjour Patrick Bonjour Pierre'
8 >>> [Link]()
9 ['Bonjour', 'Patrick', 'Bonjour', 'Pierre']
On a créé deux variables intermédiaires message1 et message2 pour stocker les chaînes de caractères
modifiées par les méthodes .title() et .replace().
Il est possible de faire la même chose en une seule ligne, en utilisant le chaînage de méthodes ou method chaining
:
On peut aussi utiliser des parenthèses pour couper une ligne de code en plusieurs lignes :
L’utilisation de parenthèses permet aussi de couper une chaîne de caractères en plusieurs lignes :
1 >>> ma_chaine = (
2 ... "voici une chaine de caractères "
3 ... "très longue "
4 ... "sur plusieurs lignes")
5 >>> ma_chaine
6 'voici une chaine de caractères très longue sur plusieurs lignes'
11.11 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
11.11.1 Parcours d’une liste de chaînes de caractères
Soit la liste ['girafe', 'tigre', 'singe', 'souris']. Avec une boucle, affichez chaque élément ainsi que sa
taille (nombre de caractères).
11.11.2 Lecture d’une séquence à partir d’un fichier FASTA
Le fichier UBI4_SCerevisiae.fasta6 contient une séquence d’ADN au format FASTA.
Créez une fonction lit_fasta() qui prend comme argument le nom d’un fichier FASTA sous la forme
d’une chaîne de caractères, lit la séquence dans le fichier FASTA et la renvoie sous la forme d’une
chaîne de caractères.
Utilisez ensuite cette fonction pour récupérer la séquence d’ADN dans la variable sequence puis
pour afficher les informations suivantes :
• le nom du fichier FASTA,
• la longueur de la séquence (c’est-à-dire le nombre de bases qu’elle contient),
• un message vérifiant que le nombre de bases est (ou non) un multiple de 3,
• le nombre de codons (on rappelle qu’un codon est un bloc de 3 bases),
• les 10 premières bases,
• les 10 dernières bases.
La sortie produite par le script devrait ressembler à ça :
UBI4_SCerevisiae.fasta
La séquence contient WWW bases
La longueur de la séquence est un multiple de 3
bases La séquence possède XXX codons
10 premières bases : YYYYYYYYYY
10 dernières bases : ZZZZZZZZZZ
où WWW et XXX sont des entiers et YYYYYYYYYY et ZZZZZZZZZZ sont des bases.
Conseil
Vous trouverez des explications sur le format FASTA et des exemples de code dans l’annexe A
Quelques formats de données en biologie.
Jason,17,3,1
William,9,18,1
5 Susan,3,8,10
[...]
Créez un programme qui lit chaque ligne du fichier et construit une liste de dictionnaire du style
[{"nom": "Jason", "geo": 17, "sport": 3, "anglais": 1}, ...]. Utilisez si possible la fonction map() pour
convertir les nombres lus dans le fichier en entiers. Réalisez ensuite une boucle sur cette liste de
dictionnaires, et affichez le nom de l’étudiant, sa note en sport et sa note en anglais. Affichez ensuite la
moyenne des notes de sport et de géographie pour tous les étudiants.
11.11.6 Conversion des acides aminés du code à trois lettres au code à une lettre
Créez une fonction convert_3_lettres_1_lettre() qui prend en argument une chaîne de caractères
avec des acides aminés en code à trois lettres et renvoie une chaîne de caractères avec les acides aminés
en code à une lettre. Vous pourrez tenter d’utiliser le method chaining dans cette fonction.
Utilisez cette fonction pour convertir la séquence protéique ALA GLY GLU ARG TRP TYR
SER GLY ALA TRP. Rappel de la nomenclature des acides aminés :
Acide aminé Code 3-lettres Code 1-lettre Acide aminé Code 3-lettres Code 1-lettre
Alanine Ala A Leucine Leu L
Arginine Arg R Lysine Lys K
Asparagine Asn N Méthionine Met M
Aspartate Asp D Phénylalanine Phe F
Cystéine Cys C Proline Pro P
Glutamate Glu E Sérine Ser S
Glutamine Gln Q Thréonine Thr T
Glycine Gly G Tryptophane Trp W
Histidine His H Tyrosine Tyr Y
Isoleucine Ile I Valine Val V
11.11.7 Palindrome
Un palindrome est un mot ou une phrase dont l’ordre des lettres reste le même si on le lit de gauche
à droite ou de droite à gauche. Par exemple, « ressasser » et « engage le jeu que je le gagne » sont des
palindromes.
Créez la fonction est_palindrome() qui prend en argument une chaîne de caractères et qui renvoie
un booléen (True si l’argument est un palindrome, False si ce n’est pas le cas). Dans le programme
principal, affichez xxx est un palindrome si la fonction est_palindrome() renvoie True sinon xxx n'est
pas un palindrome. Pensez à vous débarrasser au préalable des majuscules, des signes de
ponctuations et des espaces.
Testez ensuite si les expressions suivantes sont des palindromes :
• Radar
• Never odd or even
• Karine alla en Iran
• Un roc si biscornu
• Et la marine ira vers Malte
• Deer Madam, Reed
• rotator
• Was it a car or a cat I saw?
Conseil
Pour le nettoyage de la chaîne de caractères (retrait des majuscules, signes de ponctations et espaces),
essayer d’utiliser le method chaining.
11.11.8 Mot composable
Un mot est composable à partir d’une séquence de lettres si la séquence contient toutes les lettres du
mot. Chaque lettre de la séquence ne peut être utilisée qu’une seule fois. Par exemple, « coucou » est
composable à partir de « uocuoceokzefhu ».
Créez la fonction est_composable(), qui prend en argument un mot (sous la forme d’une chaîne de
caractères) et une séquence de lettres (aussi comme une chaîne de caractères), et qui renvoie True si le
mot est composable à partir de la séquence, sinon False.
Dans le programme principal, créez une liste de tuples contenant les couples mot / séquence, de la forme
[('mot1', 'sequence1'), ('mot2', 'sequence2'), ...]. Utilisez ensuite une boucle sur tous les couples mot /
séquence, et appelez à chaque itération la fonction est_composable(). Affichez enfin Le mot xxx est
composable à partir de yyy si le mot xxx est composable à partir de la séquence de lettres (yyy).
Affichez Le mot xxx n'est pas composable à partir de yyy si ce n’est pas le cas.
Testez cette fonction avec les mots et les séquences suivantes :
Mot Séquence
python aophrtkny
python aeiouyhpq
coucou
uocuoceokze
zh fonction
nhwfnitvkl
oco
11.11.10 Lecture d’une séquence à partir d’un fichier GenBank (exercice +++)
On cherche à récupérer la séquence d’ADN du chromosome I de la levure Saccharomyces cerevisiae
contenu dans le fichier au format GenBank NC_001133.gbk10.
Le format GenBank est présenté en détail dans l’annexe A Quelques formats de données en biologie. Pour cet
exercice,
vous devez savoir que la séquence démarre après la ligne commençant par le mot ORIGIN et se termine
avant la ligne commençant par les caractères // :
ORIGIN
1 ccacaccaca cccacacacc cacacaccac accacacacc acaccacacc cacacacaca
61 catcctaaca ctaccctaac acagccctaa tctaaccctg gccaacctgt ctctcaactt
[...]
230101 tgttagtgtt agtattaggg tgtggtgtgt gggtgtggtg tgggtgtggg
tgtgggtgtg 230161 ggtgtgggtg tgggtgtggt gtggtgtgtg ggtgtggtgt
gggtgtggtg tgtgtggg
//
Pour extraire la séquence d’ADN, nous vous proposons d’utiliser un algorithme de « drapeau », c’est -à-
dire une variable qui sera à True lorsqu’on lira les lignes contenant la séquence et à False pour les
autres lignes.
Créez une fonction lit_genbank() qui prend comme argument le nom d’un fichier GenBank sous la
forme d’une chaîne de caractères, lit la séquence dans le fichier GenBank et la renvoie sous la forme
d’une chaîne de caractères.
Utilisez ensuite cette fonction pour récupérer la séquence d’ADN dans la variable sequence dans
le programme principal. Le script affichera :
NC_001133.gbk
La séquence contient XXX bases
10 premières bases : YYYYYYYYYY
10 dernières bases : ZZZZZZZZZZ
où XXX est un entier et YYYYYYYYYY et ZZZZZZZZZZ sont des bases.
Vous avez toutes les informations pour effectuer cet exercice. Si toutefois vous coincez sur la mise en place
du drapeau, voici l’algorithme en pseudo-code pour vous aider :
Conseil
Vous trouverez des explications sur le format PDB et des exemples de code pour lire ce type de
fichier en Python dans l’annexe A Quelques formats de données en biologie.
11.11.12 Calcul des distances entre les carbones alpha consécutifs d’une structure de pro- téine
(exercice +++)
En utilisant la fonction trouve_calpha() précédente, calculez la distance interatomique entre les
carbones alpha des deux premiers résidus (avec deux chiffres après la virgule).
Rappel : la distance euclidienne d entre deux points A et B de coordonnées cartésiennes respectives
(xA, yA, zA) et (xB, yB, zB) se calcule comme suit :
√
d = (xB − xA)2 + (y B − y A)2 + (zB − zA)2
Créez ensuite la fonction calcule_distance() qui prend en argument la liste renvoyée par la fonction
trouve_calpha (), qui calcule les distances interatomiques entre carbones alpha consécutifs et affiche
ces distances sous la forme :
numero_calpha_1 numero_calpha_2 distance
Les numéros des carbones alpha seront affichés sur deux caractères. La distance sera affichée avec deux
chiffres après la virgule. Voici un exemple avec les premiers carbones alpha :
1 2 3.80
2 3 3.80
3 4 3.83
4 5 3.82
Modifiez maintenant la fonction calcule_distance() pour qu’elle affiche à la fin la moyenne des distances.
La distance inter-carbone alpha dans les protéines est très stable et de l’ordre de 3,8 angströms. Observez
avec attention les valeurs que vous avez calculées pour la protéine barstar. Repérez une valeur
surprenante. Essayez de l’expliquer.
Conseil
Vous trouverez des explications sur le format PDB et des exemples de code pour lire ce type de
fichier en Python dans l’annexe A Quelques formats de données en biologie.
gene complement(<1807..>2169)
gene <2480..>2707
gene complement(<7235..>9016)
gene complement(<11565..>11951)
gene
<12046..>12
426 [...]
Lorsque la ligne contient le mot complement le gène est situé sur le brin complémentaire, sinon il est
situé sur le brin direct. Votre code devra récupérer le premier et le second nombre indiquant
respectivement la position du début et de fin du gène. Attention à bien les convertir en entier afin de
pouvoir calculer la longueur du gène. Notez que les caractères
> et < doivent être ignorés, et que les .. servent à séparer la position de début et de fin.
On souhaite obtenir une sortie de la forme :
Conseil
Vous trouverez des explications sur le format GenBank dans l’annexe A Quelques formats de données en biologie.
CHAPITRE 12
Nous avons vu les listes dès le chapitre 4 et les avons largement utilisées depuis le début de ce cours. Dans
ce chapitre, nous allons plus loin avec les méthodes associées aux listes, ainsi que d’autres caractéristiques
très puissantes telles que les tests d’appartenance ou les listes de compréhension.
12.1.1 .append()
La méthode .append(), que l’on a déjà vu au chapitre 4 Listes, ajoute un élément à la fin d’une liste :
Conseil
Préférez la version avec .append() qui est plus compacte et facile à lire.
12.1.2 .insert()
La méthode .insert() insère un objet dans une liste à un indice déterminé :
116
1 >>> liste1 = [1, 2, 3]
2 >>> [Link](2, -15)
3 >>> liste1
4 [1, 2, -15, 3]
12.1.3 del
L’instruction del supprime un élément d’une liste à un indice déterminé :
Remarque
Contrairement aux méthodes associées aux listes présentées dans cette rubrique, del est une instruction
générale de Python, utilisable pour d’autres objets que des listes. Celle-ci ne prend pas de
parenthèse.
12.1.4 .remove()
La méthode .remove() supprime un élément d’une liste à partir de sa valeur :
S’il y a plusieurs fois la même valeur dans la liste, seule la première est retirée. Il faut appeler la méthode
.remove()
autant de fois que nécessaire pour retirer toutes les occurences d’un même élément :
12.1.5 .sort()
La méthode .sort() trie les éléments d’une liste du plus petit au plus grand :
L’argument reverse=True spécifie le tri inverse, c’est-à-dire du plus grand au plus petit élément :
12.1.6 sorted()
La fonction sorted() trie également une liste. Contrairement à la méthode précédente .sort(),
cette fonction renvoie la liste triée et ne modifie pas la liste initiale :
12.1.7 .reverse()
La méthode .reverse() inverse une liste :
12.1.8 .count()
La méthode .count() compte le nombre d’éléments (passés en argument) dans une liste :
Remarque
Pour exprimer la même idée, la documentation parle de modification de la liste « sur place » (in place en
anglais) :
Par ailleurs, certaines méthodes ou instructions des listes décalent les indices d’une liste (par exemple .insert(),
12.2. Construction d’une liste par itération Chapitre 12. Plus sur les listes
del, etc.).
Enfin, pour obtenir une liste exhaustive des méthodes disponibles pour les listes, utilisez la fonction dir(liste1)
(liste1 étant une liste).
Cette méthode est certes plus simple, mais il arrive parfois qu’on doive utiliser des boucles tout de
même, comme lorsqu’on lit un fichier. Nous vous rappellons que l’instruction list(seq) convertit un objet
de type chaîne de caractères en un objet de type liste (il s’agit donc d’une opération de casting). De même
que list(range(10)) convertit un objet de type range en un objet de type list.
1 liste1 = [1, 3, 5, 7, 9]
2 >>> 3 in liste1
3 True
4 >>> 4 in liste1
5 False
6 >>> 3 not in liste1
7 False
8 >>> 4 not in liste1
9 True
La variation avec not permet, a contrario, de vérifier qu’un élément n’est pas dans une liste.
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
La fonction zip() de Python permet d’itérer sur plusieurs listes en parallèle :
1 >>> animaux = ["poulain", "renard", "python"]
2 >>> couleurs = ["alezan", "roux", "vert"]
3 >>> zip(animaux, couleurs)
4 <zip object at 0x7f6cf954a480>
5 >>> type(zip(animaux, couleurs))
6 <class 'zip'>
7 >>> for element in zip(animaux, couleurs):
8 ... print(element)
9 ...
10 ('poulain', 'alezan')
11 ('renard', 'roux')
12 ('python', 'vert')
13 >>> for animal, couleur in zip(animaux, couleurs):
14 ... print(f"le {animal} est {couleur}")
15 ...
16 le poulain est alezan
17 le renard est roux
18 le python est vert
Lignes 3 et 6. On passe en argument deux listes à zip() qui génère un nouvel objet de type zip. Comme
pour les objets de type map vu au chapitre 11 Plus sur les chaînes de caractères, les objets zip sont
itérables.
Lignes 7 à 12. Lorsqu’on itère sur un objet zip, la variable d’itération est un tuple. À la première
itération, on a un tuple avec le premier élément de chaque liste utilisée pour générer l’objet zip, à la deuxième
itération, ce sera le deuxième élément, et ainsi de suite.
Lignes 13 à 18. Avec l’affectation multiple, on peut affecter à la volée les éléments à des variables
différentes, comme on l’a fait avec la fonction enumerate() (chapitre 5 Boucles) et la méthode .items()
des dictionnaires (chapitre 8 Dictionnaires et tuples).
Un objet zip est aussi utile pour générer facilement une liste de tuples.
Remarque
La fonction zip() fonctionne sur n’importe quel objet itérable : listes, tuples, dictionnaires, objets range, etc.
Conseil
Pour les débutants, vous pouvez sauter cette remarque.
12.5. Copie de listes Chapitre 12. Plus sur les listes
Un objet zip() comme présenté plus haut est ce qu’on appelle un itérateur. Cela implique un mode de
fonctionnement particulier, notamment le fait qu’on ne peut l’utiliser qu’une fois lorsqu’on l’a créé.
Vous trouverez plus d’explications sur la définition et le fonctionnement d’un itérateur dans le
chapitre 26 Remarques complémentaires.
Vous voyez que la modification de liste1 modifie liste2 aussi ! Pour comprendre ce qu’il se passe, nous
allons de nouveau utiliser le site Python Tutor avec cet exemple (Figure 12.1) :
Techniquement, Python utilise des pointeurs (comme dans le langage de programmation C) vers les
mêmes objets. Python Tutor l’illustre avec des flèches qui partent des variables liste1 et liste2 et qui
pointent vers la même liste. Donc, si on modifie la liste liste1, la liste liste2 est modifiée de la même
manière. Rappelez-vous de ceci dans vos futurs programmes, car cela pourrait avoir des effets
désastreux !
Pour éviter ce problème, il va falloir créer une copie explicite de la liste initiale. Observez cet exemple :
Si on regarde à nouveau dans Python Tutor (Figure 12.2), on voit clairement que l’utilisation d’une
tranche [:] ou de la fonction list() crée des copies explicites. Chaque flèche pointe vers une liste différente,
indépendante des autres.
FIGURE 12.2 – Copie de liste avec une tranche [:] et la fonction list().
Attention, les deux astuces précédentes ne fonctionnent que pour les listes à une dimension, autrement
dit les listes qui ne contiennent pas elles-mêmes d’autres listes. Voyez par exemple :
et
La méthode de copie qui fonctionne à tous les coups consiste à appeler la fonction deepcopy() du module copy :
Vous constatez qu’il est modifié dans chaque sous-liste ! À l’aide de Python Tutor on voit que
Python crée une référence vers la même sous-liste (Figure 12.3) :
Comme disent les auteurs dans la documentation officielle 1 : Note that items in the sequence are not
copied ; they are referenced multiple times. This often haunts new Python programmers. Pour éviter le
problème, on peut utiliser une boucle :
1 >>> liste1 = []
2 >>> for i in range(5):
3 ... [Link]([0, 0, 0])
4 ...
5 >>> liste1
6 [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
7 >>> liste1[2][0] = -12
8 >>> liste1
9 [[0, 0, 0], [0, 0, 0], [-12, 0, 0], [0, 0, 0], [0, 0, 0]]
On verra dans la rubrique suivante une manière très compacte de faire cela avec les listes de compréhension.
Attention
Même si une liste de listes peut représenter un tableau de nombres, il ne faut pas la voir comme un objet
mathématique de type matrice 2. En effet, le concept de lignes et colonnes n’est pas défini clairement, on
ne peut pas faire d’opérations matricielles simplement, etc. On verra dans le chapitre 20 Module Numpy
qu’il existe des objets appelés arrays qui sont faits pour ça.
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
En Python, la notion de liste de compréhension (ou compréhension de listes) représente une manière
originale et très puissante de générer des listes. La syntaxe de base consiste au moins en une boucle for
au sein de crochets précédés d’une variable (qui peut être la variable d’itération ou pas) :
1 >>> [i for i in range(10)]
2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3 >>> [2 for i in range(10)]
4 [2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
Conseil
Pour plus de lisiblité, il est possible de répartir la liste de compréhension sur plusieurs lignes.
1 >>> liste_a = []
2 >>> for idx_a in range(10):
3 ... liste_a.append(idx_a)
4 ...
5 >>> print(liste_a)
6 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
7 >>> print(idx_a)
8 9
9 >>>
10 >>> liste_b = [idx_b for idx_b in range(10)]
11 >>> print(liste_b)
12 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
13 >>> print(idx_b)
14 Traceback (most recent call last):
15 File "<stdin>", line 1, in <module>
16 NameError: name 'idx_b' is not defined. Did you mean: 'idx_a'?
La variable d’itération idx_a reste disponible en dehors de la boucle for. Par contre, la variable
d’itération idx_b n’est pas disponible en dehors de la liste de compréhension, car elle est créée « à la volée
» par Python puis éliminée une fois l’instruction exécutée.
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
Un peu plus haut nous avons évoqué la méthode .sort() qui trie une liste sur place, ainsi que la fonction
sorted() qui renvoie une nouvelle liste triée. Nous avons également vu qu’elles supportaient l’argument par
mot-clé reverse pour trier dans le sens inverse (décroissant ou anti-ASCII). Il existe un autre argument par
mot-clé nommé key permettant un tri avec des règles alternatives que nous pouvons customiser. On doit
passer à key une fonction callback (nous avions déjà croisé cette notion avec la fonction map() dans le
chapitre 11 Plus sur les chaînes de caractères, pour une définition voir le chapitre 25 Fenêtres graphiques et
Tkinter (en ligne)), c’est-à-dire, un nom de fonction sans les parenthèses. Par exemple, si on passe la
callback len comme ça :
1 >>> mots = ["babar", "bar", "ba", "bababar"]
2 >>> sorted(mots, key=len)
3 ['ba', 'bar', 'babar', 'bababar']
Python trie la liste mots en considérant la longeur de chaque élément, donc ici le nombre de lettres de
chaque chaîne de caractères. Si plusieurs mots ont la même longueur (bar et bam dans l’exemple
suivant), sorted() les laisse dans l’ordre de la liste initiale.
Là où key va se révéler puissant est quand nous allons lui passer une fonction « maison ». Voici une exemple :
Remarque
L’argument key fonctionne de la même manière entre sorted() et la méthode .sort() qui trie sur
place. Cet argument existe aussi avec les fonctions min() et max(). Par exemple :
12.9 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
12.9. Exercices Chapitre 12. Plus sur les listes
Conseil
Utilisez la fonction [Link]() avec les paramètres k et weights. Le paramètre k spécifie le
nombre de tirages aléatoires à réaliser et le paramètre weights indique les probabilités de tirage.
Par exemple, pour réaliser 10 tirages aléatoires entre les lettres A et B avec 80% de A et 20% de B,
on utilise la fonction [Link]() de la manière suivante :
1
11
121
1331
14641
1 5 10 10 5 1
[...]
Déduisez comment une ligne est construite à partir de la précédente. Par exemple, à partir de la
ligne 2 (1 1), construisez la ligne suivante (ligne 3 : 1 2 1) et ainsi de suite.
Implémentez cette construction en Python. Généralisez à l’aide d’une
boucle. Écrivez dans un fichier [Link] les 10 premières lignes du
triangle de Pascal.
CHAPITRE 13
Avant d’aborder ce chapitre, nous vous conseillons de relire le chapitre 10 Fonctions et de bien en assimiler
toutes les notions (et aussi d’en faire les exercices). Nous avons vu dans ce chapitre 10 le concept
incontournable que représentent les fonctions. Nous avons également introduit la notion de variables
locales et globales.
Dans ce chapitre, nous allons aller un peu plus loin sur la visibilité de ces variables dans et hors
des fonctions, et aussi voir ce qui se passe lorsque ces variables sont des listes. Attention, la plupart des
lignes de code ci-dessous sont données à titre d’exemple pour bien comprendre ce qui se passe, mais nombre
d’entre elles sont des aberrations en terme de programmation. Nous ferons un récapitulatif des bonnes
pratiques à la fin du chapitre. Enfin, nous vous conseillons de tester tous les exemples ci-dessous avec le
site Python Tutor 1 afin de suivre l’état des variables lors de l’exécution des exemples.
Nous appelons la fonction calc_somme_nb_pairs() depuis le programme principal, puis à l’intérieur de celle-ci nous
appelons l’autre fonction est_pair(). Regardons ce que Python Tutor nous montre lorsque la fonction
calc_somme_nb_pairs () est exécutée dans la Figure 13.1.
L’espace mémoire alloué à est_pair() est grisé, indiquant que cette fonction est en cours d’exécution.
La fonction appelante calc_somme_nb_pairs() est toujours là (sur un fond blanc) car son exécution
n’est pas terminée. Elle est en quelque sorte figée dans le même état qu’avant l’appel de est_pair(), et
on pourra ainsi noter que ses variables locales (debut, fin) sont toujours là. De manière générale, les
variables locales d’une fonction ne seront détruites que lorsque l’exécution de celle-ci sera terminée.
Dans notre exemple, les variables locales de calc_somme_nb_pairs() ne seront détruites que lorsque la
boucle sera terminée et que la variable somme sera retournée au programme principal. Enfin, notez
bien que la fonction calc_somme_nb_pairs() appelle la fonction est_pair() à chaque itération de la
boucle.
Ainsi, le programmeur est libre de faire tous les appels qu’il souhaite. Une fonction peut appeler une
autre fonction, cette dernière peut appeler une autre fonction et ainsi de suite (et autant de fois qu’on le
veut). Une fonction peut même s’appeler elle-même, cela s’appelle une fonction récursive (voir la
rubrique suivante). Attention toutefois à retrouver vos petits si vous vous perdez dans les appels
successifs !
Conseil
Dans la fonction est_pair() on teste si le nombre est pair et on renvoie True, sinon on renvoie
False. Cette fonction pourrait être écrite de manière plus compacte :
1 def est_pair(x):
2 return x % 2
Comme l’expression x % 2 renvoie un booléen directement, elle revient au même que le if / else ci-
dessus. C’est bien sûr cette dernière notation plus compacte que nous vous recommandons.
13.2 Fonctions récursives
Conseil
13.2. Fonctions récursives Chapitre 13. Plus sur les fonctions
Une fonction récursive est une fonction qui s’appelle elle-même. Les fonctions récursives permettent
d’obtenir une efficacité redoutable dans la résolution de certains algorithmes, comme le tri rapide 2 (en
anglais, quicksort).
Oublions la recherche d’efficacité pour l’instant et concentrons-nous sur l’exemple de la fonction
mathématique factorielle. Nous vous rappelons que la factorielle s’écrit avec un ! et se définit de la
manière suivante :
3! = 3 × 2 × 1 = 6
4! = 4 × 3 × 2 × 1 = 30
...
n! = n × n − 1 × . . . × 2 × 1
Voici le code Python avec une fonction récursive :
1 def calc_factorielle(nb):
2 if nb == 1:
3 return 1
4 else:
5 return nb * calc_factorielle(nb - 1)
6
7 # Programme principal.
8 print(calc_factorielle(4))
Pas si facile à comprendre, n’est-ce pas ? À nouveau, aidons nous de Python Tutor pour visualiser
ce qui se passe dans la figure 13.2 (nous vous conseillons bien sûr de tester vous-même cet exemple)
:
FIGURE 13.2 – Fonction récursive : factorielle.
Lorsque Python exécute le code de la fonction, il connaît le contenu de la variable x. Par contre, de
retour dans le module principal (dans ce cas, il s’agit de l’interpréteur Python), il ne la connaît plus,
d’où le message d’erreur.
De même, une variable passée en argument est considérée comme locale lorsqu’on arrive dans la fonction :
L’erreur renvoyée montre que Python pense que x est une variable locale qui n’a pas été encore
assignée. Si on veut vraiment modifier une variable globale dans une fonction, il faut utiliser le mot-clé
global :
Dans ce dernier cas, le mot-clé global a forcé la variable x à être globale plutôt que locale au sein de la fonction.
Attention
Les exemples de cette partie représentent des absurdités en termes de programmation. Ils sont donnés à
titre indicatif pour comprendre ce qui se passe, mais il ne faut surtout pas s’en inspirer !
Soyez extrêmement attentifs avec les types modifiables (tels que les listes) car vous pouvez les changer au
sein d’une fonction :
De même, si vous passez une liste en argument, elle est modifiable au sein de la fonction :
Pour bien comprendre l’origine de ce comportement, utilisons à nouveau le site Python Tutor 3. La
figure 13.3 vous montre le mécanisme à l’oeuvre lorsqu’on passe une liste à une fonction.
L’instruction pass dans la fonction est une instruction Python qui ne fait rien. Elle est là car une
fonction ne peut être vide et doit contenir au moins une instruction Python valide.
On voit très clairement que la variable liste1 passée en argument lors de l’appel de la fonction d’une
part, et la variable locale liste_tmp au sein de la fonction d’autre part, pointent vers le même objet
dans la mémoire. Ainsi, si on modifie liste_tmp, on modifie aussi liste1. C’est exactement le même
mécanisme que pour la copie de listes (cf. rubrique 11.4 Copie de listes du chapitre 12 Plus sur les
listes).
Si vous voulez éviter les problèmes de modification malencontreuse d’une liste dans une fonction, utilisez
des tuples (ils ont présentés dans le chapitre 8 Dictionnaires et tuples), Python renverra une erreur car ces
derniers sont non modifiables.
Une autre solution pour éviter la modification d’une liste, lorsqu’elle est passée comme argument à
une fonction, est de la passer explicitement (comme nous l’avons fait pour la copie de liste) afin qu’elle
reste intacte dans le programme principal :
FIGURE 13.3 – Passage d’une liste à une fonction.
De manière générale, la règle LGI découle de la manière dont Python gère ce que l’on appelle « les espaces
de noms ». C’est cette gestion qui définit la portée (visibilité) de chaque variable. Nous en parlerons plus
longuement dans le chapitre 24 Avoir plus la classe avec les objets (en ligne).
13.6 Recommandations
13.6.1 Évitez les variables globales
Dans ce chapitre nous avons joué avec les fonctions (et les listes) afin de vous montrer comment
Python réagissait. Toutefois, notez bien que l’utilisation de variables globales est à bannir
définitivement de votre pratique de la programmation.
Parfois on veut faire vite et on crée une variable globale visible partout dans le programme (donc
dans toutes les fonctions), car « Ça va plus vite, c’est plus simple ». C’est un très mauvais calcul, ne serait-ce
que parce que vos fonctions ne seront pas réutilisables dans un autre contexte si elles utilisent des variables
globales ! Ensuite, arriverez-vous à vous relire dans six mois ? Quelqu’un d’autre pourrait-il comprendre
votre programme ? Il existe de nombreuses autres raisons 4 que nous ne développerons pas ici, mais
libre à vous de consulter de la documentation externe.
Heureusement, Python est orienté objet et permet « d’encapsuler » des variables dans des objets et
de s’affranchir définitivement des variables globales (nous verrons cela dans le chapitre 23 Avoir la classe
avec les objets). En attendant, et si vous ne souhaitez pas aller plus loin sur les notions d’objet (on peut
tout à fait « pythonner » sans cela), retenez la chose suivante sur les fonctions et les variables globales
:
Conseil
Plutôt que d’utiliser des variables globales, passez vos variables explicitement aux fonctions comme des
argument(s).
1 def ajoute_un(liste):
2 for indice in range(len(liste)):
3 liste[indice] += 1
4 return liste
5
6 # Programme principal.
7 liste_notes = [10, 8, 16, 7, 15]
8 liste_notes = ajoute_un(liste_notes)
9 print(liste_notes)
La ligne 8 indique que la liste liste_notes passée à la fonction est écrasée par la liste renvoyée par la
fonction. Le code suivant produirait la même sortie :
1 def ajoute_un(liste):
2 for indice in range(len(liste)):
3 liste[indice] += 1
4
5 # Programme principal.
6 liste_notes = [10, 8, 16, 7, 15]
7 ajoute_un(liste_notes)
8 print(liste_notes)
Cela reste toutefois moins intuitif, car il n’est pas évident de comprendre que la liste est modifiée
dans la fonction en lisant la ligne 7. Dans un tel cas, il serait essentiel d’indiquer dans la
documentation de la fonction que la liste est
modifiée « sur place » (in place en anglais) dans la fonction. Vous verrez dans le chapitre 15 Création de
modules
comment documenter vos fonctions.
Conseil
Pour les raisons évoquées ci-dessus, nous vous conseillons de privilégier la première version :
1 liste_notes = ajoute_un(liste_notes)
13.6.3 Conclusion
Vous connaissez maintenant les fonctions sous tous leurs angles. Comme indiqué en introduction du
chapitre 10, elles sont incontournables et tout programmeur se doit de les maîtriser. Voici les derniers
conseils que nous pouvons vous donner :
• Lorsque vous débutez un nouveau projet de programmation, posez-vous la question : «
Comment pourrais-je décomposer en blocs chaque tâche à effectuer, chaque bloc pouvant être une
fonction ? ». Et n’oubliez pas que si une fonction s’avère trop complexe, vous pouvez la
décomposer en d’autres fonctions.
• Au risque de nous répéter, forcez-vous à utiliser des fonctions en permanence. Pratiquez,
pratiquez… et pratiquez encore !
13.7 Exercices
Conseil
Pour le second exercice, créez un script puis exécutez-le dans un shell.
[Link] Code 1
1 def hello(prenom):
2 print(f"Bonjour {prenom}")
3
4
5 # Programme principal.
6 hello("Patrick")
7 print(x)
[Link] Code 2
1 def hello(prenom):
2 print(f"Bonjour {prenom}")
3
4
5 # Programme principal.
6 x = 10
7 hello("Patrick")
8 print(x)
13.7. Exercices Chapitre 13. Plus sur les fonctions
[Link] Code 3
1 def hello(prenom):
2 print(f"Bonjour {prenom}")
3 print(x)
4
5
6 # Programme principal.
7 x = 10
8 hello("Patrick")
9 print(x)
[Link] Code 4
1 def hello(prenom):
2 x = 42
3 print(f"Bonjour {prenom}")
4 print(x)
5
6
7 # Programme principal.
8 x = 10
9 hello("Patrick")
10 print(x)
Conteneurs
Dans ce chapitre, nous allons aborder la notion de conteneur, revenir sur certaines propriétés avancées
des dictionnaires et tuples, et enfin aborder les types set et frozenset. Pour les débutants, ce chapitre
aborde des notions relativement avancées. Avant de vous lancer, nous vous conseillons vivement de bien
maitriser les chapitres 4 Listes et 12 Plus sur les listes, ainsi que le chapitre 8 Dictionnaires et tuples, d’avoir
effectué un maximum d’exercices, et de vous sentir à l’aise avec toutes les notions abordées jusque là.
14.1 Généralités
14.1.1 Définition et propriétés
Définition
Un conteneur (container en anglais) est un nom générique pour définir un objet Python qui contient
une collection d’autres objets.
Les conteneurs que nous connaissons depuis le début de ce cours sont les listes, les chaînes de caractères,
les diction- naires et les tuples. Même si on ne l’a pas vu explicitement, les objets de type range sont
également des conteneurs.
Dans la suite de cette rubrique, nous allons examiner les différentes propriétés des conteneurs. À la fin
de ce chapitre, nous ferons un tableau récapitulatif de ces propriétés.
Examinons d’abord les propriétés qui caractérisent tous les types de conteneur.
• Capacité à supporter le test d’appartenance. Souvenez-vous, il permet de vérifier si un élément était
présent dans une liste. Cela fonctionne donc aussi sur les chaînes de caractères ou tout autre
conteneur :
1 >>> liste1 = [4, 5, 6]
2 >>> 4 in liste1
3 True
4 >>> "to" in "toto"
5 True
138
14.1. Généralités Chapitre 14. Conteneurs
• Indexable (subscriptable en anglais) : on peut retrouver un élément par son indice (c’est-à-dire sa
position dans le conteneur) ou plusieurs éléments avec une tranche ; en général, tout conteneur
indexable est ordonné.
• Itérable (iterable en anglais) : on peut faire une boucle dessus.
Certains conteneurs sont appelés objets séquentiels ou séquence.
Définition
Un objet séquentiel ou séquence est un conteneur itérable, ordonné et indexable. Les objets
séquentiels sont les listes, les chaînes de caractères, les objets de type range, ainsi que les tuples.
Une autre propriété importante que l’on a déjà croisée, et qui nous servira dans ce chapitre,
concerne la possibilité ou non de modifier un objet.
• Un objet est dit non modifiable lorsqu’on ne peut pas le modifier, ou lorsqu’on ne peut pas en
modifier un de ses éléments si c’est un conteneur. On parle aussi d’objet immuable 1 (immutable
object en anglais). Cela signifie qu’une fois créé, Python ne permet plus de le modifier par la
suite.
Qu’en est-il des objets que nous connaissons ? Les listes sont modifiables, on peut modifier un ou
plusieurs de ses éléments et ajouter ou retirer un élément. Les dictionnaires sont modifiables : pour une
clé donnée, on peut changer la valeur correspondante et ajouter ou retirer un couple clé/valeur. Tous les
autres types que nous avons vus précédemment sont quant à eux non modifiables : les chaînes de caractères
ou strings, les tuples, les objets de type range, mais également des objets qui ne sont pas des conteneurs
comme les entiers, les floats et les booléens.
On comprend bien l’immutabilité des strings comme vu au chapitre 11 Plus sur les chaînes de
caractères, mais c’est moins évident pour les entiers, floats ou booléens. Nous allons démontrer cela, mais
avant nous avons besoin de définir la notion d’identifiant d’un objet.
Définition
L’identifiant d’un objet est un nombre entier qui est garanti constant pendant toute la durée de vie
de l’objet. Cet identifiant est en général unique pour chaque objet. Toutefois, pour des raisons
d’optimisation, Python crée parfois le même identifiant pour deux objets non modifiables différents
qui ont la même valeur. L’identifiant peut être assimilé à l’adresse mémoire de l’objet qui, elle aussi,
est unique. En Python, on utilise la fonction interne id() qui prend en
argument un objet et renvoie son identifiant.
Maintenant que l’identifiant est défini, regardons l’exemple suivant qui montre l’immutabilité des entiers :
1 >>> var = 4
2 >>> id(var)
3 140318876873440
4 >>> var = 5
5 >>> id(var)
6 140318876873472
Ligne 1 on définit l’entier var puis on regarde son identifiant. Ligne 4, on pourrait penser que
l’on modifie var. Toutefois, on voit que son identifiant ligne 6 est différent de la ligne 3. En fait,
l’affectation ligne 4 var = 5 écrase l’ancienne variable var et en crée une nouvelle, ce n’est pas la valeur de
var qui a été changée puisque l’identifiant n’est plus le même. Le même raisonnement peut être tenu pour
les autres types numériques comme les floats et booléens. Si on regarde maintenant ce qu’il se passe
pour une liste :
Définition
Un objet Python est dit hachable (hashable en anglais) s’il est possible de calculer une valeur de
hachage sur celui-ci avec la fonction interne hash(). En programmation, la valeur de hachage peut être vue
comme une empreinte numérique de l’objet. Elle est obtenue en passant l’objet dans une fonction de
hachage et dépend du contenu de l’objet. En Python, cette empreinte est, comme dans la plupart des
langages de programmation, un entier. Au sein d’une même session Python, deux objets hachables qui
ont un contenu identique auront strictement la même valeur de hachage.
Attention
La valeur de hachage d’un objet renvoyée par la fonction hash() n’a pas le même sens que son
identifiant renvoyé par la fonction id(). La valeur de hachage est obtenue en « moulinant » le contenu
de l’objet dans une fonction de hachage. L’identifiant est quant à lui attribué par Python à la création
de l’objet. Il est constant tout le long de la durée de vie de l’objet, un peu comme une carte d’identité.
Tout objet a un identifiant, mais il doit être hachable pour avoir une valeur de hachage.
Pourquoi évoquer cette propriété de hachabilité ? D’abord, parce qu’elle est étroitement liée à
l’immutabilité. En effet, un objet non modifiable est la plupart du temps hachable. Cela permet de
l’identifier en fonction de son contenu. Par ailleurs, l’hachabilité est une implémentation qui permet un
accès rapide aux éléments des conteneurs de type dictionnaire ou set (cf. rubriques suivantes).
Les objets hachables sont les chaînes de caractères, les entiers, les floats, les booléens, les objets de
type range, les tuples (sous certaines conditions) et les frozensets ; par contre, les listes, les sets et les
dictionnaires sont non hachables. Les sets et frozensets seront vus plus bas dans ce chapitre.
Voici un exemple :
1 >>> hash("Plouf")
2 5085648805260210718
3 >>> hash(5)
4 5
5 >>> hash(3.14)
6 322818021289917443
7 >>> hash([1, 2, 3])
8 Traceback (most recent call last):
9 File "<stdin>", line 1, in <module>
10 TypeError: unhashable type: 'list'
Les valeurs de hachage renvoyées par la fonction hash() de Python sont systématiquement des entiers.
Par contre, Python renvoie une erreur pour une liste, car elle est non hachable.
L’argument key=[Link] indique explicitement qu’il faut réaliser le tri par les valeurs du
dictionnaire. On retrouve la méthode .get() vue au chapitre 8 Dictionnaires et tuples, mais sans les
parenthèses : key=[Link], mais pas key
=[Link](). Une fonction ou méthode passée en argument sans les parenthèses est appelée callback,
nous reverrons cela en détail dans le chapitre 25 Fenêtres graphiques et Tkinter (en ligne).
Attention, ce sont les clés du dictionnaire qui sont renvoyées, pas les valeurs. Ces clés sont cependant
renvoyées dans un ordre qui permet d’obtenir les clés triées par ordre croissant :
On obtient ici une erreur, car Python ne sait pas comparer une chaîne de caractères (singe) avec
des valeurs numériques (70 et 1.75).
Si un des sous-éléments a plus de deux éléments (ou moins), Python renvoie une erreur :
Attention
Une manière intuitive utilise simplement des arguments par mot-clés, qui deviendront des clés sous
forme de chaîne de caractères :
Une dernière manière puissante pour générer des dictionnaires combine les fonctions dict() et zip(). On se
souvient que la fonction zip() peut générer une liste de tuples :
1 >>> animaux = ["poulain", "renard", "python"]
2 >>> couleurs = ["alezan", "roux", "vert"]
3 >>> list(zip(animaux, couleurs))
4 [('poulain', 'alezan'), ('renard', 'roux'), ('python', 'vert')]
Si on utilise l’objet zip avec la fonction dict(), on obtient un dictionnaire.
Ce message est similaire à celui que nous avions rencontré quand on essayait de modifier une chaîne
de caractères (voir chapitre 11 Plus sur les chaînes de caractères). De manière générale, Python renverra
un message TypeError: ' [...]' does not support item assignment lorsqu’on essaie de modifier un élément
d’un objet non modifiable. Si vous voulez ajouter un élément (ou le modifier), vous devez créer un
nouveau tuple :
La fonction id() montre que le tuple créé ligne 6 est bien différent de celui créé ligne 4, bien qu’ils
aient le même nom. Comme on a vu plus haut, ceci est dû à l’opérateur d’affectation utilisé ligne 6
(tuple1 = tuple1 + (2,)) qui crée un nouvel objet distinct de celui de la ligne 1. Cet exemple montre que les
tuples sont peu adaptés lorsqu’on a besoin d’ajouter, retirer, modifier des éléments. La création d’un
nouveau tuple à chaque étape s’avère lourde et il n’y a aucune méthode pour faire cela, puisque les
tuples sont non modifiables.
Conseil
Pour ce genre de tâche, les listes sont clairement mieux adaptées que les tuples.
14.3.2 Affectation multiple et fonctions
Dans le chapitre 8 Dictionnaires et tuples, nous avons abordé l’affectation multiple. Pour rappel, elle permet
d’effectuer sur une même ligne plusieurs affectations en même temps, par exemple : x, y, z = 1, 2, 3. On a vu
qu’il était possible de le faire également avec les listes : [x, y, z] = [1, 2, 3]. Toutefois, cette syntaxe étant
alourdie par la présence des crochets, on préfèrera toujours la première syntaxe avec les tuples sans
parenthèses.
Concernant les fonctions, nous avions croisé l’importance de l’affectation multiple dans le
chapitre 10 lorsqu’une fonction renvoyait plusieurs valeurs :
La syntaxe x, y = ma_fonction() permet de récupérer les deux valeurs renvoyées par la fonction et de
les affecter à la volée dans deux variables différentes. Cela évite l’opération laborieuse de récupérer d’abord
le tuple, puis de créer le s variables en utilisant l’indiçage :
Conseil
Lorsqu’une fonction renvoie plusieurs valeurs sous forme de tuple, privilégiez toujours la forme x, y = ma_fonction
().
Cela envoie le message à la personne qui lit le code « je ne m’intéresse pas aux valeurs récupérées dans les variables
_ ». Notez que l’on peut utiliser une ou plusieurs variables underscore(s). Dans l’exemple ci-dessus, la 2e et la
4e variable renvoyées par la fonction seront ignorées dans la suite du code. Cela présente le mérite d’éviter
de polluer l’attention de la personne qui lit le code.
Remarque
Dans l’interpréteur interactif, la variable _ a une signification différente. Elle prend automatiquement la
dernière valeur affichée :
1 >>> 3
2 3
3 >>> _
4 3
5 >>> "mésange"
6 'mésange'
7 >>> _
8 'mésange'
Attention, ceci n’est vrai que dans l’interpréteur !
Remarque
Le caractère underscore (_) est couramment utilisé dans les noms de variable pour séparer les mots et
être explicite, par exemple seq_ADN ou liste_listes_residus. On verra dans le chapitre 16 Bonnes
pratiques en programmation Python que ce style de nommage est appelé snake_case. Toutefois, il faut éviter
d’utiliser les underscores en début et/ou en fin de nom de variable (leading et trailing underscores en
anglais), par exemple : _var, var_, var, var . Onverra au chapitre 23 Avoir la classe avec les objets
que ces underscores ont aussi une signification particulière.
Si on modifie un élément de la liste liste1 (ligne 5) ou bien qu’on ajoute un élément à tuple1[0] (ligne
6), Python s’exécute et ne renvoie pas de message d’erreur. Or nous avions dit qu’un tuple était non
modifiable… Comment cela est-il possible ? Commençons d’abord par regarder comment les objets
sont agencés avec Python Tutor.
FIGURE 14.1 – Tuple contenant une liste.
La liste liste1 pointe vers le même objet que l’élément du tuple d’indice 0. Comme pour la copie
de liste (par exemple liste_b = liste_a), ceci est attendu car, par défaut, Python crée une copie par
référence (voir le chapitre 12 Plus sur les listes). Ainsi, qu’on raisonne en tant que premier élément du
tuple ou bien en tant que liste liste1, on pointe vers la même liste. Or, rappelez-vous, nous avons expliqué
au début de ce chapitre que lorsqu’on modifiait un
élément d’une liste, celle-ci gardait le même identifiant. C’est toujours le cas ici, même si celle-ci se
trouve dans un tuple. Regardons cela :
Nous confirmons ici le schéma de Python Tutor, c’est bien la même liste que l’on considère liste1 ou tuple1[0]
puisqu’on a le même identifiant. Maintenant, on modifie cette liste via la variable liste1 ou tuple1[0] :
Malgré la modification de cette liste, l’identifiant n’a toujours pas changé puisque la fonction id()
nous renvoie la même valeur depuis le début. Même si la liste a été modifiée « de l’intérieur », Python
considère que c’est toujours la même liste, puisqu’elle n’a pas changé d’identifiant. Si au contraire on
essaie de remplacer cette sous-liste par autre chose, Python renvoie une erreur :
Conseil
Cette digression avait pour objectif de vous faire comprendre ce qu’il se passe lorsqu’on met une liste
dans un tuple. Toutefois, pouvoir modifier une liste en tant qu’élément d’un tuple va à l’encontre de
l’intérêt d’un objet non modifiable. Dans la mesure du possible, nous vous déconseillons de créer des
listes dans des tuples afin d’éviter les déconvenues.
Conseil
Mettre une ou plusieurs liste(s) dans un tuple le rend non hachable. Ceci le rend inutilisable comme clé de
dictionnaire ou, on le verra ci-après, comme élément d’un set ou d’un frozenset. Donc, à nouveau, ne
mettez pas de listes dans vos tuples !
Remarquez que la répétition du chiffre 5 dans la définition du set ligne 1 produit finalement un
seul chiffre 5, car chaque élément ne peut être présent qu’une seule fois. Comme pour les dictionnaires
(jusqu’à la version 3.6), les sets sont non ordonnés. La manière dont Python les affiche n’a pas de sens
en tant que tel et peut être différente de celle utilisée lors de leur création.
Les sets ne peuvent contenir que des objets hachables. On a déjà eu le cas avec les clés de dictionnaire. Ceci
optimise l’accès à chaque élément du set. Pour rappel, les objets hachables que nous connaissons sont les
chaînes de caractères, les tuples, les entiers, les floats, les booléens et les frozensets (voir plus bas). Les
objets non hachables que l’on connait sont les listes, les sets et les dictionnaires. Si on essaie tout de
même de mettre une liste dans un set, Python renvoie une erreur :
1 >>> set1 = {3, 4, "Plouf", (1, 3)}
2 >>> set1
3 {(1, 3), 3, 4, 'Plouf'}
4 >>> set2 = {3.14, [1, 2]}
5 Traceback (most recent call last):
6 File "<stdin>", line 1, in <module>
7 TypeError: unhashable type: 'list'
14.4. Sets et frozensets Chapitre 14. Conteneurs
À quoi différencie-t-on un set d’un dictionnaire alors que les deux utilisent des accolades ? Le set sera défini
seulement par des valeurs {valeur_1, valeur_2, ...} alors que le dictionnaire aura toujours des couples
clé/valeur {clé_1: valeur_1, clé_2: valeur_2, ...}.
La fonction interne à Python set() convertit un objet itérable passé en argument en un nouveau set (opération de
casting) :
Nous avons dit plus haut que les sets ne sont ni ordonnés ni indexables, il est donc impossible de récupérer
un élément par sa position. Il est également impossible de modifier un de ses éléments par
l’indexation.
La méthode .add() ajoute au set l’élément passé en argument. Toutefois, si l’élément est déjà présent
dans le set, il n’est pas ajouté puisqu’on a au plus une copie de chaque élément. La méthode .discard()
retire du set l’élément passé en argument. Si l’élément n’est pas présent dans le set, il ne se passe rien, le
set reste intact. Comme les sets ne sont pas ordonnés ni indexables, il n’y a pas de méthode pour insérer
un élément à une position précise, contrairement aux listes. Dernier point sur ces méthodes, elles
modifient le set sur place (in place, en anglais) et ne renvoient rien, à l’instar des méthodes des listes
(.append(), .remove(), etc.).
Enfin, les sets ne supportent pas les opérateurs + et *.
14.4.2 Utilité
Les conteneurs de type set sont très utiles pour rechercher les éléments uniques d’une suite
d’éléments. Cela revient à éliminer tous les doublons. Par exemple :
On peut bien sûr transformer dans l’autre sens un set en liste. Cela permet par exemple d’éliminer
les doublons de la liste initiale, tout en récupérant une liste à la fin :
On peut faire des choses très puissantes. Par exemple, un compteur de lettres en combinaison
avec une liste de compréhension, le tout en une ligne !
Les sets permettent aussi l’évaluation d’union ou d’intersection mathématiques en conjonction avec les
opérateurs, respectivement | et & :
Notez qu’il existe les méthodes .union() et .intersection permettant de réaliser ces opérations
d’union et d’intersection :
L’instruction [Link](set2) renvoie sous la forme d’un nouveau set les éléments de set1 qui
ne sont pas dans set2. Et inversement pour [Link](set1) :
1 >>> [Link](set2)
2 {4}
3 >>> [Link](set1)
4 {0, 2}
Enfin, deux autres méthodes sont très utiles :
1 >>> set1 = set(range(10))
2 >>> set2 = set(range(3, 7))
3 >>> set3 = set(range(15, 17))
4 >>> set1
5 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
6 >>> set2
7 {3, 4, 5, 6}
8 >>> set3
9 {16, 15}
10 >>> [Link](set1)
11 True
12 >>> [Link](set1)
13 True
La méthode .issubset() indique si un set est inclus dans un autre set. La méthode isdisjoint() indique
si un set est disjoint d’un autre set, c’est-à-dire, s’ils n’ont aucun élément en commun indiquant que
leur intersection est nulle. Il existe de nombreuses autres méthodes que nous n’abordons pas ici, mais
qui peuvent être consultées sur la docu-
mentation officielle de Python . 4
14.4.3 Frozensets
Les frozensets sont des sets non modifiables et hachables. Ainsi, un set peut contenir des frozensets mais
pas l’inverse. À quoi servent-ils ? Comme la différence entre tuple et liste, l’immutabilité des frozensets
donne l’assurance de ne pas pouvoir les modifier par erreur. Pour créer un frozenset on utilise la fonction
interne frozenset(), qui prend en argument un objet itérable et le convertit (opération de casting) :
Les frozensets ne possèdent bien sûr pas les méthodes de modification des sets (.add(), .discard(), etc.)
puisqu’ils sont non modifiables. Par contre, ils possèdent toutes les méthodes de comparaisons de sets
(.union(), .intersection (), etc.).
Conseil
Pour aller plus loin sur les sets et les frozensets, voici deux articles sur les sites programiz 5 et towardsdatascience 6.
La méthode .items() vue dans le chapitre 8 Dictionnaires et tuples est particulièrement bien adaptée
pour créer un dictionnaire de compréhension, car elle permet d’itérer en même temps sur les clés et
valeurs d’un dictionnaire.
Avec un dictionnaire de compréhension, on peut rapidement compter le nombre de chaque base dans
une séquence d’ADN :
1 >>> sequence = "atctcgatcgatcgcgctagctagctcgccatacgtacgactacgt"
2 >>> {base:[Link](base) for base in set(sequence)}
3 {'a': 10, 'g': 10, 't': 11, 'c': 15}
De manière générale, tout objet sur lequel on peut faire une double itération du type for var1, var2
in obj est utilisable pour créer un dictionnaire de compréhension. Si vous souhaitez aller plus loin, vous
pouvez consulter cet article sur le site Datacamp 7.
Il est également possible de générer des sets de compréhension sur le même modèle que les listes de compréhension :
14.8 Exercices
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
14.8.4 Mots de deux lettres dans la séquence du chromosome I de Saccharomyces cerevi- siae
Créez une fonction lit_fasta() qui prend comme argument le nom d’un fichier FASTA sous la forme
d’une chaîne de caractères, lit la séquence dans le fichier FASTA et la renvoie sous la forme d’une chaîne
de caractères. Inspirez-vous d’un exercice similaire du chapitre 10 Plus sur les chaînes de caractères.
Utilisez cette fonction et la fonction compte_mots_2_lettres() de l’exercice précédent pour extraire
les mots de deux lettres et leurs occurrences dans la séquence du chromosome I de la levure du boulanger
Saccharomyces cerevisiae (fichier NC_001133.fna 13).
Le génome complet est fourni au format FASTA. Vous trouverez des explications sur ce format et
des exemples de code dans l’annexe A Quelques formats de données en biologie.
14.8.5 Mots de n lettres dans un fichier FASTA
Créez un script [Link] qui prend comme arguments le nom d’un fichier FASTA suivi d’un
entier compris entre 1 et 4. Ce script doit extraire du fichier FASTA tous les mots et leurs occurrences,
en fonction du nombre de lettres passé en option.
Utilisez pour ce script la fonction lit_fasta() de l’exercice précédent. Créez également la fonction
compte_mots_n_lettres () qui prend comme argument une séquence sous la forme d’une chaîne de caractères et
le nombre de lettres des mots
sous la forme d’un
entier. Testez ce
script avec :
• la séquence du chromosome I de la levure du boulanger Saccharomyces cerevisiae (fichier NC_001133.fna 14) ;
• le génome de la bactérie Escherichia coli (fichier NC_000913.fna15).
Les deux fichiers sont au format FASTA.
Cette méthode vous paraît-elle efficace sur un génome assez gros comme celui d’Escherichia coli ?
14.8.6 Atomes carbone alpha d’un fichier PDB
Téléchargez le fichier [Link] 16, qui correspond à la structure tridimensionnelle de la protéine
barstar 17 sur le site de la Protein Data Bank (PDB).
Créez la fonction trouve_calpha(), qui prend en argument le nom d’un fichier PDB (sous la forme
d’une chaîne de caractères), qui sélectionne uniquement les lignes contenant des carbones alpha et qui les
renvoie sous la forme d’une liste de dictionnaires. Chaque dictionnaire contient quatre clés :
• le numéro du résidu (resid) avec une valeur entière,
• la coordonnée atomique x (x) avec une valeur float,
• la coordonnée atomique y (y) avec une valeur float,
• la coordonnée atomique z (z) avec une valeur float.
Utilisez la fonction trouve_calpha() pour afficher à l’écran le nombre total de carbones alpha de
la barstar ainsi que les coordonnées atomiques des carbones alpha des deux premiers résidus
(acides aminés).
Conseil
Vous trouverez des explications sur le format PDB et des exemples de code pour lire ce type de
fichier en Python dans l’annexe A Quelques formats de données en biologie.
A0A087
X1C5
A0A0B4
J2F0
A0A0B4
J2F2
A0A0C5
B5G6
A0A0K2
S4Q6
A0A0U1
RRE5
A0A1B0
GTW7
A0AV02
A0AV96
[...]
L’objectif de cet exercice est de déterminer quelles sont les protéines humaines qui sont des kinases.
Chaque liste de protéines contenant plusieurs milliers d’éléments, il n’est pas possible de la faire à la
main. Vous aller utiliser Python et les sets pour cela.
1. Créez un script compare_proteins.py.
2. Dans ce script, créez une fonction read_protein_file() qui prend en argument le nom d’un fichier de
protéines sous la forme d’une chaîne de caractères et qui renvoie un set contenant la liste des
identifiants des protéines contenues dans le fichier passé en argument.
3. Affichez ensuite le nombre de protéines listées dans chaque fichier.
4. En utilisant uniquement des opérations sur les sets, déterminez et affichez :
• le nombre de protéines humaines qui sont des kinases ;
• le nombre de protéines humaines qui ne sont pas des kinases ;
• le nombre de kinases qui ne sont pas des protéines humaines.
CHAPITRE 15
Création de modules
158
15.3. Utilisation de son propre module Chapitre 15. Création de modules
Remarque
Une constante est, par définition, une variable dont la valeur n’est pas modifiée. Par convention, en
Python, le nom des constantes est écrit en majuscules (comme DATE dans notre exemple).
Remarque
Avec Mac OS X et Linux, il faut taper la commande suivante depuis un shell Bash pour modifier
la variable d’envi- ronnement PYTHONPATH :
export PYTHONPATH=$PYTHONPATH:/chemin/vers/mon/super/module
Avec Windows, mais depuis un shell PowerShell, il faut taper la commande suivante :
$env:PYTHONPATH += ";C:\chemin\vers\mon\super\module"
Une fois cette manipulation effectuée, vous pouvez contrôler que le chemin vers le répertoire
contenant vos modules a bien été ajouté à la variable d’environnement PYTHONPATH :
• sous Mac OS X et Linux : echo $PYTHONPATH
• sous Windows : echo $env:PYTHONPATH
Le chargement du module se fait avec la commande import message. Notez que le fichier est bien
enregistré avec une extension .py, pourtant on ne la précise pas lorsqu’on importe le module. Ensuite,
on peut utiliser les fonctions comme avec un module classique :
Remarque
La première fois qu’un module est importé, Python crée un répertoire nommé pycache
contenant un fichier avec une extension .pyc qui contient le bytecode 1, c’est-à-dire le code précompilé
du module.
Remarque
Pour quitter l’aide, pressez la touche Q.
Vous remarquez que Python a généré automatiquement cette page d’aide, tout comme il est capable
de le faire pour les modules internes à Python (random, math, etc.) et ce grâce aux docstrings. Notez que
l’on peut aussi appeler l’aide pour une seule fonction :
1 >>> help([Link])
2
3 Help on function ciao in module message:
4
5 ciao(nom)
6 Dit Ciao.
En résumé, les docstrings sont destinés aux utilisateurs du module. Leur but est différent des
commentaires qui, eux, sont destinés à celui qui lit le code (pour en comprendre les subtilités). Une
bonne docstring de fonction doit contenir tout ce dont un utilisateur a besoin pour utiliser cette
fonction. Une liste minimale et non exhaustive serait :
• ce que fait la fonction,
• ce qu’elle prend en argument,
• ce qu’elle renvoie.
Pour en savoir plus sur les docstrings et comment les écrire, nous vous recommandons de lire le
chapitre 16 Bonnes pratiques en programmation Python.
$ python [Link]
$
Cela s’explique par l’absence de programme principal, c’est-à-dire de lignes de code que l’interpréteur
exécute lorsqu’on lance le script.
À l’inverse, que se passe-t-il si on importe un script en tant que module alors qu’il contient un
programme principal avec des lignes de code ? Prenons par exemple le script [Link] suivant :
1 """Script de test."""
2
3
4 def bonjour(nom):
5 """Dit Bonjour."""
6 return f"Bonjour {nom}"
7
8
9 # Programme principal.
10 print(bonjour("Joe"))
• Si le programme [Link] est importé en tant que module, le résultat du test if sera alors False
et le bloc d’instructions correspondant ne sera pas exécuté :
1 >>> import message2
2 >>>
Ce comportement est possible grâce à la gestion des espaces de noms par Python (pour plus de détail,
consultez le chapitre 24 Avoir plus la classe avec les objets (en ligne)). Au delà de la commodité de
pouvoir utiliser votre script en tant que programme ou en tant que module, cela présente l’avantage de
signaler clairement où se situe le programme principal quand on lit le code.
Conseil
Ainsi, au lieu d’ajouter le commentaire :
# Programme principal.
comme nous vous l’avions suggéré dans les chapitres 10 Fonctions et 13 Plus sur les fonctions, nous vous
recomman- dons désormais d’utiliser la ligne :
if name == " main ":
15.7 Exercice
Conseil
Pour cet exercice, créez un script puis exécutez-le dans un shell.
Conseil
• Dans cet exercice, on supposera que toutes les séquences sont manipulées comme des chaînes de
caractères en majuscules.
• Pour les fonctions seq_alea() et comp_inv(), n’hésitez pas à jeter un œil aux exercices
correspondants dans le chapitre 12 Plus sur les listes.
• Voici un exemple de fichier FASTA adn.fasta2 pour tester la fonction lit_fasta().
CHAPITRE 16
Comme vous l’avez constaté dans tous les chapitres précédents, la syntaxe de Python est très
permissive. Afin d’uniformiser l’écriture de code en Python, la communauté des développeurs Python
recommande un certain nombre de règles afin qu’un code soit lisible. Lisible par quelqu’un d’autre,
mais également, et surtout, par soi-même. Essayez de relire un code que vous avez écrit « rapidement »
il y a un mois, six mois ou un an. Si le code ne fait que quelques lignes, il se peut que vous vous y retrouviez,
mais s’il fait plusieurs dizaines, voire centaines de lignes, vous serez perdus.
Dans ce contexte, le créateur de Python, Guido van Rossum, part d’un constat simple : « code is
read much more often than it is written » (« le code est plus souvent lu qu’écrit »). Avec l’expérience, vous
vous rendrez compte que cela est parfaitement vrai. Alors, plus de temps à perdre, voyons en quoi
consistent ces bonnes pratiques.
Plusieurs choses sont nécessaires pour écrire un code lisible : la syntaxe, l’organisation du code, le
découpage en fonctions (et possiblement en classes, que nous verrons dans le chapitre 23 Avoir la classe avec
les objets), mais souvent, aussi, le bon sens. Pour cela, les « PEP » peuvent nous aider.
Définition
Afin d’améliorer le langage Python, la communauté qui développe Python publie régulièrement des
Python Enhance- ment Proposal 1 (PEP), suivi d’un numéro. Il s’agit de propositions concrètes pour
améliorer le code, ajouter de nouvelles fonctionnalités, mais aussi des recommandations sur la manière
d’utiliser Python, bien écrire du code, etc.
On va aborder dans ce chapitre sans doute la plus célèbre des PEP, à savoir la PEP 8, qui est
incontournable lorsque l’on veut écrire du code Python correctement.
Définition
On parle de code pythonique lorsque ce dernier respecte les règles d’écriture définies par la
communauté Python, mais aussi les règles d’usage du langage.
16.1 De la bonne syntaxe avec la PEP 8
La PEP 8 Style Guide for Python Code 2 est une des plus anciennes PEP (les numéros sont croissants
avec le temps). Elle consiste en un nombre important de recommandations sur la syntaxe de Python. Il
est vivement recommandé de lire la PEP 8 en entier au moins une fois pour avoir une bonne vue
d’ensemble. On ne présentera ici qu’un rapide résumé de cette PEP 8.
16.1.1 Indentation
On a vu que l’indentation est obligatoire en Python pour séparer les blocs d’instructions. Cela
vient d’un constat simple : l’indentation améliore la lisibilité d’un code. La PEP 8 recommande d’utiliser
quatre espaces pour chaque niveau d’indentation. Nous vous recommandons de suivre
impérativement cette règle.
Attention
Afin de toujours utiliser cette règle des quatre espaces pour l’indentation, il est essentiel de régler
correctement votre éditeur de texte. Consultez pour cela l’annexe Installation de Python disponible en
ligne 3. Avant d’écrire la moindre ligne de code, faites en sorte que lorsque vous pressez la touche
tabulation, cela ajoute quatre espaces (et non pas un caractère tabulation).
1 import module_interne_1
2 import module_interne_2
3 from module_interne_3 import fonction_spécifique
4
5 import module_externe_1
6 import module_externe_2_qui_a_un_nom_long as mod2
7
8 import module_cree_par_vous
16.1.3 Règles de nommage
Les noms de variables, de fonctions et de modules doivent être de la forme :
1 ma_variable
2 fonction_test_27()
3 mon_module
c’est-à-dire en minuscules avec un caractère « souligné » (« tiret du bas », ou underscore en anglais) pour
séparer les différents « mots » dans le nom.
Les constantes sont écrites en majuscules :
1 MA_CONSTANTE
2 VITESSE_LUMIERE
Les noms de classes (voir le chapitre 23 Avoir la classe avec les objets) et les exceptions (voir le chapitre 26 Remarques
complémentaires (en ligne)) sont de la forme :
1 MaClasse
2 MyException
Remarque
• Le style recommandé pour nommer les variables et les fonctions en Python est appelé snake_case. Il
est différent du CamelCase utilisé pour les noms des classes et des exceptions.
• La variable _ est habituellement employée pour stocker des valeurs qui ne seront pas utilisées
par la suite. Par exemple, dans le cas d’une affectation multiple, on peut utiliser _ pour stocker une
valeur qui ne nous intéresse pas (voir chapitre 14 Conteneurs).
Pensez à donner à vos variables des noms qui ont du sens. Évitez autant que possible les a1, a2, i,
truc, toto… Les noms de variables à un caractère sont néanmoins autorisés pour les indices dans
les boucles :
1 # Code recommandé :
2 ma_variable = 3 + 7
3 mon_texte = "souris"
4 mon_texte == ma_variable
5 # Code non recommandé :
6 ma_variable=3+7
7 mon_texte="souris"
8 mon_texte== ma_variable
Il n’y a, par contre, pas d’espace à l’intérieur de crochets, d’accolades et de parenthèses :
1 # Code recommandé :
2 ma_liste[1]
3 mon_dico{"clé"}
4 ma_fonction(argument)
5 # Code non recommandé :
6 ma_liste[ 1 ]
7 mon_dico{"clé" }
8 ma_fonction( argument )
Ni juste avant la parenthèse ouvrante d’une fonction ou le crochet ouvrant d’une liste ou d’un dictionnaire :
1 # Code recommandé :
2 ma_liste[1]
3 mon_dico{"clé"}
4 ma_fonction(argument)
5 # Code non recommandé :
6 ma_liste [1]
7 mon_dico {"clé"}
8 ma_fonction (argument)
1 # Code recommandé :
2 ma_liste = [1, 2, 3]
3 mon_dico = {"clé1": "valeur1", "clé2": "valeur2"}
4 ma_fonction(argument1, argument2)
5 # Code non recommandé :
6 ma_liste = [1 , 2 ,3]
7 mon_dico = {"clé1" : "valeur1", "clé2":"valeur2"}
8 ma_fonction(argument1 ,argument2)
Par contre, pour les tranches de listes, on ne met pas d’espace autour du :
1 ma_liste = [1, 3, 5, 7, 9, 1]
2 # Code recommandé :
3 ma_liste[1:3]
4 ma_liste[1:4:2]
5 ma_liste[::2]
6 # Code non recommandé :
7 ma_liste[1 : 3]
8 ma_liste[1: 4:2 ]
9 ma_liste[ : :2]
Enfin, on n’ajoute pas plusieurs espaces autour du = ou des autres opérateurs pour faire joli :
1 # Code recommandé :
2 x1 = 1
3 x2 = 3
4 x_old = 5
5 # Code non recommandé :
6 x1 = 1
7 x2 = 3
8 x_old = 5
1 >>> ma_variable = 3
2 >>> if ma_variable > 1 and ma_variable < 10 \
3 ... and ma_variable % 2 == 1 and ma_variable % 3 == 0:
4 ... print(f"ma variable vaut {ma_variable}")
5 ...
6 ma variable vaut 3
1 >>> print("ATGCGTACAGTATCGATAAC"
2 ... "ATGACTGCTACGATCGGATA"
3 ... "CGGGTAACGCCATGTACATT")
4 ATGCGTACAGTATCGATAACATGACTGCTACGATCGGATACGGGTAACGCCATGTA
CATT
Notez qu’il n’y a pas d’opérateur + pour concaténer les trois chaînes de caractères, et que celles-ci ne
sont pas séparées par des virgules. À partir du moment où elles sont entre parenthèses, Python les
concatène automatiquement.
On peut aussi utiliser les parenthèses pour évaluer un expression trop longue :
1 >>> ma_variable = 3
2 >>> if (ma_variable > 1 and ma_variable < 10
3 ... and ma_variable % 2 == 1 and ma_variable % 3 == 0):
4 ... print(f"ma variable vaut {ma_variable}")
5 ...
6 ma variable vaut 3
Remarque
Les parenthèses sont aussi très utiles lorsqu’on a besoin d’enchaîner des méthodes les unes à la suite des
autres. Cette technique du method chaining a été introduite dans le chapitre 11 Plus sur les chaînes de
caractères et sera très utilisée
dans le chapitre 22 Module Pandas.
Enfin, il est possible de créer des listes ou des dictionnaires sur plusieurs lignes, en sautant une ligne après une virgule
:
16.1.7 Commentaires
Les commentaires débutent toujours par le symbole # suivi d’un espace. Ils fournissent des
explications sur l’utilité du code et permettent de comprendre son fonctionnement.
Les commentaires sont sur le même niveau d’indentation que le code qu’ils commentent. Les
commentaires sont constitués de phrases complètes, avec une majuscule au début (sauf si le premier
mot est une variable qui s’écrit sans majuscule) et un point à la fin.
La PEP 8 recommande d’écrire les commentaires en anglais, sauf si vous êtes absolument certains que
votre code ne sera lu que par des francophones. Dans la mesure où vous allez souvent développer des
programmes scientifiques, nous vous conseillons d’écrire vos commentaires en anglais.
Soyez également cohérent entre la langue utilisée pour les commentaires et la langue utilisée pour nommer
les variables. Pour un programme scientifique, les commentaires et les noms de variables sont en anglais.
Ainsi ma_liste deviendra my_list et ma_fonction deviendra my_function (par exemple).
Les commentaires qui suivent le code sur la même ligne sont à éviter le plus possible et doivent être
séparés du code par au moins deux espaces :
Vous constatez dans l’exemple ci-dessus que, pour Python, les guillemets simples et doubles sont
équivalents. Nous vous conseillons cependant d’utiliser les guillemets doubles car ceux-ci sont, de notre
point de vue, plus lisibles.
Remarque
La PEP 257 recommande d’écrire des docstrings avec trois doubles guillemets, c’est-à-dire :
"""Ceci est une docstring recommandée."""
mais pas :
'''Ceci n'est pas une docstring recommandée.'''
Comme indiqué dans le chapitre 15 Création de modules, n’oubliez pas que les docstrings sont destinées
aux utilisateurs des modules, fonctions, méthodes et classes que vous avez développés. Les éléments
essentiels pour les fonctions et les méthodes sont :
1. ce que fait la fonction ou la méthode,
2. ce qu’elle prend en argument,
3. ce qu’elle renvoie.
16.3. Outils de contrôle qualité du code Chapitre 16. Bonnes pratiques en programmation
Python
Pour les modules et les classes, on ajoute également des informations générales sur leur fonctionnement.
Pour autant, la PEP 257 ne dit pas explicitement comment organiser les docstrings pour les fonctions et les
méthodes.
Pour répondre à ce besoin, deux solutions ont émergées :
• la solution Google avec le Google Style Python Docstrings 6,
7
• la solution NumPy avec le NumPy Style Python Docstrings . NumPy est un module
complémentaire à Python, très utilisé en analyse de données et dont on parlera dans le
chapitre 20.
16.2.2 Un exemple concret
On illustre ici la solution de docstrings NumPy pour des raisons de goût personnel. Sentez-vous
libre d’explorer la proposition de Google.
Voici un exemple pour une fonction qui prend en argument deux entiers et qui renvoie leur produit :
Attention
L’être humain a une fâcheuse tendance à la procrastination (le fameux « Bah je le ferai
demain…») et écrire de la documentation peut être un sérieux motif de procrastination. Soyez
vigilant sur ce point, et rédigez vos docstrings au moment où vous écrivez vos modules, fonctions,
classes ou méthodes. Passer une journée (voire plusieurs) à écrire les
docstrings d’un gros projet est particulièrement pénible. Croyez-nous !
16.3 Outils de contrôle qualité du code
Pour évaluer la qualité d’un code Python, c’est-à-dire sa conformité avec les recommandations de
la PEP 8 et de la PEP 257, on peut utiliser les outils pycodestyle, pydocstyle et pylint.
Ces outils ne sont fournis dans l’installation de base de Python et doivent être installés sur votre
machine. Avec la distribution Miniconda, cette étape d’installation se résume à une ligne de
commande :
$ conda install -c conda-forge pycodestyle pydocstyle pylint
Définition
Les outils pycodestyle, pydocstyle et pylint sont des linters, c’est-à-dire des programmes qui vont chercher
les sources potentielles d’erreurs dans un code informatique. Ces erreurs peuvent être des erreurs de style
(PEP 8 et 257) ou
des erreurs logiques (manipulation d’une variable, chargement de module).
Voici le contenu du script script_quality_not_ok.py8 que nous allons analyser par la suite :
$ python script_quality_not_ok.py
2x3=6
4 x 5 = 20
On va tout d’abord vérifier la conformité avec la PEP 8 grâce à l’outil pycodestyle :
$ pycodestyle script_quality_not_ok.py
script_quality_not_ok.py:6:1: E302 expected 2 blank lines,
found 1 script_quality_not_ok.py:6:30: E231 missing
whitespace after ',' script_quality_not_ok.py:6:38: E202
whitespace before ')'
script_quality_not_ok.py:26:21: E225 missing whitespace around operator
script_quality_not_ok.py:31:10: E211 whitespace before '('
• Ligne 2. Le bloc script_quality_not_ok.py:6:1: désigne le nom du script
(script_quality_not_ok. py), le numéro de la ligne (6) et le numéro de la colonne (1) où se
trouve la non-conformité avec la PEP 8. Ensuite, pycodestyle fournit un code et un message
explicatif. Ici, il faut deux lignes vides avant la fonction Multiplie_nombres().
• Ligne 3. Il manque un espace après la virgule qui sépare les arguments nombre1 et nombre2 dans
la définition de la fonction Multiplie_nombres() à la ligne 6 (colonne 30) du script.
16.4. Outil de formatage automatique du code Chapitre 16. Bonnes pratiques en programmation
Python
• Ligne 4. Il y un espace de trop après le second argument nombre2 dans la définition de la fonction
Multiplie_nombres () à la ligne 6 (colonne 38) du script.
• Ligne 5. Il manque un espace après l’opérateur * à la ligne 26 (colonne 21) du script.
• Ligne 6. Il y a un espace de trop entre print et ( à la ligne 31 (colonne 10) du script.
Assez curieusement, pycodestyle n’a pas détecté que le nom de la fonction Multiplie_nombres() ne
respecte pas la convention de nommage (pas de majuscule).
Ensuite, l’outil pydocstyle va vérifier la conformité avec la PEP 257 et s’intéresser particulièrement aux docstrings
:
$ pydocstyle script_quality_not_ok.py
script_quality_not_ok.py:1 at module
level:
D200: One-line docstring should fit on one line with quotes (found 2)
script_quality_not_ok.py:7 in public function `Multiplie_nombres`:
D205: 1 blank line required between summary line and description (found 0)
script_quality_not_ok.py:7 in public function `Multiplie_nombres`:
D400: First line should end with a period (not 's')
• Lignes 2 et 3. pydocstyle indique que la docstring à la ligne 1 du script est sur deux lignes, alors
qu’elle devrait être sur une seule ligne.
• Lignes 4 et 5. Dans la docstring de la fonction Multiplie_nombres() (ligne 7 du script), il
manque une ligne vide entre la ligne résumé et la description plus complète.
• Lignes 6 et 7. Dans la docstring de la fonction Multiplie_nombres() (ligne 7 du script), il
manque un point à la fin de la première ligne.
Les outils pycodestyle et pydocstyle vont simplement vérifier la conformité aux PEP 8 et 257.
L’outil pylint va lui aussi vérifier une partie de ces règles mais il va également essayer de comprendre le
contexte du code et proposer des éléments d’amélioration. Par exemple :
$ pylint script_quality_not_ok.py
************* Module script_quality_not_ok
script_quality_not_ok.py:6:0: C0103: Function name
"Multiplie_nombres" doesn't conform to snake_case naming
style (invalid-name) script_quality_not_ok.py:4:0: W0611:
Unused import os (unused-import)
$ black script_quality_not_ok.py
reformatted
script_quality_not_ok.py
All done!
1 file reformatted.
Le script script_quality_not_ok.py a été modifié pour être conforme à la PEP 8, ce qu’on peut vérifier avec
pycodestyle :
$ pycodestyle script_quality_not_ok.py
black peut modifier votre code de manière significative. Il est donc recommandé de l’utiliser avec
l’option --diff au préalable pour afficher les modifications apportées. Par exemple, avec le programme
script_quality_not_ok.py qui n’aurait pas été modifié :
import os
-def Multiplie_nombres(nombre1,nombre2 ):
+
+def Multiplie_nombres(nombre1,
nombre2): [...]
Conseil
black est très pratique. N’hésitez pas à l’utiliser pour formater automatiquement votre code.
Attention
• black ne fait qu’une entorse à la PEP 8 : il autorise des longueurs de lignes jusqu’à 88 caractères. Si
vous souhaitez respecter strictement la PEP 8, utilisez l’option --line-length 79.
• black se limite à la PEP 8. Il ne vérifie pas la conformité avec la PEP 257 ni la qualité du code
(imports inutiles, etc.). Utilisez toujours pydocstyle et pylint en complément.
16.5 Organisation du code
Il est important de toujours structurer son code de la même manière. Ainsi, on sait tout de suite où
trouver l’information et un autre programmeur pourra s’y retrouver. Voici un exemple de code avec les
différents éléments dans le bon ordre :
16.6. Conseils sur la conception d’un script Chapitre 16. Bonnes pratiques en programmation
Python
Le module re permet d’utiliser des expressions régulières avec Python. Les expressions régulières sont
aussi appelées en anglais regular expressions, ou en plus court regex. Dans la suite de ce chapitre, nous
utiliserons souvent le mot regex pour désigner une expression régulière. Les expressions régulières sont
puissantes et incontournables en bioinformatique, surtout lorsque vous souhaitez récupérer des
informations dans de gros fichiers.
Cette action de recherche de données dans un fichier est appelée généralement parsing (qui signifie
littéralement « analyse syntaxique »). Le parsing fait partie du travail quotidien du bioinformaticien, il est
sans arrêt en train de « fouiller
» dans des fichiers pour en extraire des informations d’intérêt, par exemple récupérer les coordonnées
3D des atomes d’une protéine dans un fichier PDB, ou encore extraire les gènes d’un fichier
GenBank.
Dans ce chapitre, nous ne ferons que quelques rappels sur les expressions régulières. Pour une
documentation plus complète, référez-vous à la page d’aide des expressions régulières 1 sur le site officiel
de Python.
Ici, egrep affiche toutes les lignes du fichier du virus de l’herpès (herp_virus.gbk) dans lesquelles la regex ^DEF
(c’est-à-dire le mot DEF en début de ligne) est retrouvée.
176
17.1. Définition et syntaxe Chapitre 17. Expressions régulières et
parsing
Remarque
Il est intéressant de faire un point sur le vocabulaire utilisé en anglais et en français. En général, on
utilise le verbe to match pour indiquer qu’une regex « a fonctionné ». Bien qu’il n’y ait pas de
traduction littérale en français, on peut utiliser les verbes « retrouver » ou « correspondre ». Par
exemple, on pourra traduire l’expression « The regex matches the line » par « La regex est retrouvée
dans la ligne » ou encore « La regex correspond dans la ligne ».
Après avoir introduit le vocabulaire des regex, voici quelques éléments de syntaxe des métacaractères :
^ Début de chaîne de caractères ou de ligne.
Exemple : la regex ^ATG est retrouvée dans la chaîne de caractères ATGCGT mais pas dans la chaîne
CCATGTT.
$ Fin de chaîne de caractères ou de ligne.
Exemple : la regex ATG$ est retrouvée dans la chaîne de caractères TGCATG mais pas dans la chaîne
CCATGTT.
. N’importe quel caractère (mais un caractère quand même).
Exemple : la regex A.G est retrouvée dans ATG, AtG, A4G, mais aussi dans A-G ou dans A G.
[ABC] Le caractère A ou B ou C (un seul caractère).
Exemple : la regex T[ABC]G est retrouvée dans TAG, TBG ou TCG, mais pas dans TG.
[A-Z] N’importe quelle lettre majuscule.
Exemple : la regex C[A-Z]T est retrouvée dans CAT, CBT, CCT…
[a-z] N’importe quelle lettre minuscule.
[0-9] N’importe quel chiffre.
[A-Za-z0-9] N’importe quel caractère alphanumérique.
[^AB] N’importe quel caractère sauf A et B.
Exemple : la regex CG[^AB]T est retrouvée dans CG9T, CGCT… mais pas dans CGAT ni dans CGBT.
\ Caractère d’échappement (pour protéger certains caractères).
Exemple : la regex \+ désigne le caractère + littéral. La regex A\.G est retrouvée dans A.G et non pas
dans A suivi de n’importe quel caractère, suivi de G.
* 0 à n fois le caractère précédent ou l’expression entre parenthèses
précédente. Exemple : la regex A(CG)*T est retrouvée dans AT,
ACGT, ACGCGT…
+ 1 à n fois le caractère précédent ou l’expression entre parenthèses précédente.
Exemple : la regex A(CG)+T est retrouvée dans ACGT, ACGCGT… mais pas dans AT.
? 0 à 1 fois le caractère précédent ou l’expression entre parenthèses précédente.
Exemple : la regex A(CG)?T est retrouvée dans AT ou ACGT.
{n} n fois le caractère précédent ou l’expression entre parenthèses précédente.
Exemple : la regex A(CG){2}T est retrouvée dans ACGCGT mais pas dans ACGT, ACGCGCGT ou ACGCG.
{n,m} n à m fois le caractère précédent ou l’expression entre parenthèses précédente.
Exemple : la regex A(C){2,4}T est retrouvée dans ACCT, ACCCTet ACCCCTmais pas dans ACT,
ACCCCCTou ACCC.
{n,} Au moins n fois le caractère précédent ou l’expression entre parenthèses précédente.
Exemple : la regex A(C){2,}T est retrouvée dans ACCT, ACCCT et ACCCCT mais pas à ACT ou ACCC.
{,m} Au plus m fois le caractère précédent ou l’expression entre parenthèses précédente.
Exemple : la regex A(C){,2}T est retrouvée dans AT, ACT et ACCT mais pas dans ACCCT ou ACC.
(CG|TT) Les chaînes de caractères CG ou TT.
Exemple : la regex A(CG|TT)C est retrouvée dans ACGC ou ATTC.
Enfin, il existe des caractères spéciaux qui sont bien commodes et qui peuvent être utilisés en tant que métacaractères
:
\d remplace n’importe quel chiffre (d signifie digit), équivalent à [0-9].
\w remplace n’importe quel caractère alphanumérique et le caractère souligné (underscore) (w signifie word
character), équivalent à [0-9A-Za-z_].
\s remplace n’importe quel « espace blanc » (whitespace) (s signifie space), équivalent à [ \t\n\r\f]. La
notion d’espace blanc a été abordée dans le chapitre 11 Plus sur les chaînes de caractères. Les
espaces blancs les plus classiques sont l’espace , la tabulation \t, le retour à la ligne \n, mais il en existe
d’autres comme \r et \f que nous ne développerons pas ici. \s est très pratique pour détecter une
combinaison d’espace(s) et/ou de tabulation(s).
Comme vous le constatez, les métacaractères sont nombreux et leur signification est parfois difficile à
maîtriser. Faites particulièrement attention aux métacaractères ., + et * qui, combinés ensemble, peuvent
donner des résultats ambigus.
Attention
Il est important de savoir par ailleurs que les regex sont « avides » (greedy en anglais) lorsqu’on
utilise les métaca- ractères + et *. Cela signifie que la regex cherchera à « s’étendre » au maximum.
Par exemple, si on utilise la regex A+ pour faire une recherche dans la chaîne TTTAAAAAAAAGC,
tous les A de cette chaîne (huit en tout) seront concernés, bien
que AA, AAA, etc. « fonctionnent » également avec cette regex.
17.3 Le module re
17.3.1 La fonction search()
Dans le module re, la fonction search() est incontournable. Elle permet de rechercher un motif,
c’est-à-dire une regex, au sein d’une chaîne de caractères avec une syntaxe de la forme search(motif, chaine).
Si motif est retrouvé dans chaine, Python renvoie un objet du type SRE_Match.
Sans entrer dans les détails propres au langage orienté objet, si on utilise un objet du type SRE_Match
dans un test, il sera considéré comme vrai. Par exemple, si on recherche le motif tigre dans la chaîne de
caractères "girafe tigre singe" :
1 >>> import re
2 >>> animaux = "girafe tigre singe"
3 >>> [Link]("tigre", animaux)
4 <_sre.SRE_Match object at 0x7fefdaefe2a0>
5 >>> if [Link]("tigre", animaux):
6 ... print("OK")
7 ...
8 OK
Attention
Le motif que vous utilisez comme premier argument de la fonction search() sera interprété en tant que regex. Ainsi,
^DEF correspondra au mot DEF en début de chaîne et pas au caractère littéral ^suivi du mot DEF.
17.3. Le module re Chapitre 17. Expressions régulières et
parsing
17.3.4 Groupes
L’intérêt de l’objet de type SRE_Match renvoyé par Python lorsqu’une regex trouve une
correspondance dans une chaîne de caractères est de pouvoir ensuite récupérer certaines zones
précises :
L’utilisation des groupes entre parenthèses est également possible, ceux-ci sont alors renvoyés sous la forme de tuples
:
Encore plus puissant, il est possible d’utiliser dans le remplacement des groupes qui ont été «
capturés » avec des parenthèses :
1 >>> regex = [Link]("([0-9]+)\.([0-9]+)")
2 >>> phrase = "pi vaut 3.14 et e vaut 2.72"
3 >>> [Link]("approximativement \\1", phrase)
4 'pi vaut approximativement 3 et e vaut vaut approximativement 2'
5 >>> [Link]("approximativement \\1 (puis .\\2)",phrase)
6 'pi vaut approximativement 3 (puis .14) et e vaut approximativement 2 (puis .72)'
Si vous avez capturé des groupes, il suffit d’utiliser \\1, \\2 (etc.) pour utiliser les groupes
correspondants dans la chaîne de caractères substituée. On notera que la syntaxe générale pour récupérer
des groupes dans les outils qui gèrent les regex est \1, \2, etc. Toutefois, Python nous oblige à mettre un
deuxième backslash car il y a ici deux niveaux : un
premier niveau Python où on veut mettre un backslash littéral (donc \\), puis un second niveau regex
dans lequel on veut retrouver \1. Si cela est confus, retenez seulement qu’il faut mettre un \\ devant le
numéro de groupe.
Enfin, sachez que la réutilisation d’un groupe précédemment capturé est aussi utilisable lors d’une
utilisation classique de regex. Par exemple :
Conseil
Pour ces exercices, créez des scripts puis exécutez-les dans un shell.
Conseil
• Utilisez des regex pour trouver les lignes demandées.
• Des explications sur le format GenBank et des exemples de code sont fournies dans l’annexe A
Quelques formats de données en biologie.
Enzyme Site de
restriction HinFI
GANTC
EcoRII
CCW
GG BbvBI
GGYR
CC
BcoI
CYCGR
G Psp5II
RGGW
CCY
Enzyme Site de
restriction BbvAI
GAANNNNT
TC
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.
• La ligne de commentaire d’une séquence au format FASTA est de la forme
>sp|O95139|NDUB6_HUMAN NADH dehydrogenase [...]
Elle débute toujours pas le caractère >. Le numéro d’accession O95139 se situe entre le premier et le second
symbole
| (symbole pipe). Attention, il faudra « échapper » ce symbole car il a une signification particulière dans une regex.
• Le numéro qui s’incrémente débutera à 1 et sera affiché sur 5 caractères, avec des 0 à sa gauche
si nécessaires (formatage {:05d}).
Écrivez un script ote_doublons.py qui lit le fichier breves_doublons.txt et qui supprime tous les
doublons à l’aide d’une regex. Le script affichera le nouveau texte à l’écran.
Conseil
Utilisez la méthode .sub().
CHAPITRE 18
Les notebooks Jupyter sont des cahiers électroniques qui, dans le même document, peuvent
rassembler du texte, des images, des formules mathématiques, des tableaux, des graphiques et du code
informatique exécutable. Ils sont manipulables interactivement dans un navigateur web.
Initialement développés pour les langages de programmation Julia, Python et R (d’où le nom Jupyter), les notebooks
Jupyter supportent près de 40 langages différents.
La cellule est l’élément de base d’un notebook Jupyter. Elle peut contenir du texte formaté au format
Markdown ou du code informatique qui pourra être exécuté.
Voici un exemple de notebook Jupyter (figure 18.1) :
Ce notebook est constitué de cinq cellules : deux avec du texte en Markdown (la première et la dernière)
et trois avec du code Python (légèrement grisées).
18.1 Installation
Avec la distribution Miniconda, les notebooks Jupyter s’installent avec la commande :
18.2 JupyterLab
En 2018, le consortium Jupyter a lancé JupyterLab, qui est un environnement complet de programmation
et d’analyse de données.
Pour obtenir cette interface, lancez la commande suivante depuis un shell :
$ jupyter lab
Une nouvelle page devrait s’ouvrir dans votre navigateur web et vous devriez obtenir une interface
similaire à la figure 18.2, avec à gauche un navigateur de fichiers et à droite le « Launcher », qui permet
de créer un nouveau notebook Jupyter, de lancer un terminal ou d’éditer un fichier texte, un fichier
Mardown, un script Python…
L’interface proposée par JupyterLab est très riche. On peut y organiser un notebook Jupyter, un
éditeur de fichier texte, un terminal… Les possibilités sont nombreuses et nous vous invitons à explorer
cette interface par vous-même.
184
FIGURE 18.1 – Exemple de notebook Jupyter. Les chiffres entourés désignent les différentes cellules.
Remarque
L’extension .ipynb est l’extension de fichier des notebooks Jupyter.
Vous pouvez entrer des instructions Python dans la première cellule. Par exemple :
FIGURE 18.2 – Interface de JupyterLab.
Pour créer une nouvelle cellule, vous avez, ici encore, plusieurs possibilités :
• Cliquer sur l’icône + dans la barre de menu au dessus du notebook.
• Cliquer sur la 2e icône à partir de la droite (juste à côté de la poubelle), dans les icônes situées à
l’intérieur de la cellule, à droite.
Une nouvelle cellule vide devrait apparaître.
Vous pouvez également créer une nouvelle cellule, en positionnant votre curseur dans la première
cellule, puis en pressant simultanément les touches Alt + Entrée. Si vous utilisez cette combinaison de
touches, vous remarquerez que le numéro à gauche de la première cellule est passée de [1] à [2], car vous
avez exécuté une nouvelle fois la première cellule puis créé une nouvelle cellule.
Vous pouvez ainsi créer plusieurs cellules les unes à la suite des autres. Un objet créé dans une cellule
antérieure sera disponible dans les cellules suivantes. Par exemple, dans la figure 18.6, nous avons
quatre cellules.
FIGURE 18.6 – Notebook avec plusieurs cellules de code Python.
Dans un notebook Jupyter, il est parfaitement possible de réexécuter une cellule précédente. Par exemple
la première cellule, qui porte désormais à sa gauche la numérotation [5] (voir figure 18.7).
FIGURE 18.7 – Notebook avec une cellule ré-exécutée.
Attention
La possibilité d’exécuter les cellules d’un notebook Jupyter dans un ordre arbitraire peut prêter à
confusion, notamment si vous modifiez la même variable dans plusieurs cellules.
Nous vous recommandons de régulièrement relancer complètement l’exécution de toutes les cellules de
votre notebook, de la première à la dernière, en cliquant sur le menu Kernel puis Restart Kernel and Run
All Cells et enfin de valider le message Restart Kernel ? en cliquant sur le bouton Restart.
Remarque
Pour quitter l’interface JupyterLab, il y a plusieurs possibilités :
• Dans le menu en haut à gauche de l’interface, cliquer sur File, puis Shut Down, puis confirmer
en cliquant sur le bouton Shut Down.
• Une méthode plus radicale est de revenir sur le shell depuis lequel JupyterLab a été lancé, puis de
presser deux fois de suite la combinaison de touches Ctrl + C.
Remarque
Dans cette rubrique, nous vous montrerons quelques exemples d’utilisation de magic commands exécutées dans
un
notebook Jupyter.
18.6.1 %whos
La commande %whos liste tous les objets (variables, fonctions, modules…) utilisés dans
un notebook. Si une cellule précédente contenait le code :
1 a=2
2 b=3
3
4 def ma_fonction(x, y):
5 return x + y
6
7 resultat_1 = ma_fonction(a, 10)
8 resultat_2 = ma_fonction("Bonjour", "Jupyter")
alors l’exécution de :
1 %whos
renvoie :
18.6.2 %history
La commande %history liste toutes les commandes Python lancées dans un notebook :
1 %history
a=2
b=3
print(a +
b)
def ma_fonction(x, y):
return x + y
ma_fonction(a, 10)
ma_fonction("Bonjour",
"Jupyter")
%whos
%history
18.6.3 %%time
La commande %%time (avec deux symboles %) va mesurer le temps d’exécution d’une cellule.
C’est très utile pour faire des tests de performance. On peut, par exemple, comparer les vitesses de parcours
d’une liste avec une boucle for, par les éléments ou par les indices des éléments.
Ainsi, cette cellule :
1 %%time
2 concentrations = [5.5, 7.2, 11.8, 13.6, 19.1, 21.7, 29.4]
3 somme_carres = 0.0
4 for conc in concentrations:
5 somme_carres += conc**2
renvoie :
et celle-ci :
1 %%time
2 concentrations = [5.5, 7.2, 11.8, 13.6, 19.1, 21.7, 29.4]
3 somme_carres = 0.0
4 for idx in range(len(concentrations)):
5 somme_carres += concentrations[idx]**2
renvoie :
Comme attendu, la première méthode (itération par les éléments) est plus rapide que la seconde
(itération par les indices des éléments). Les temps obtenus dépendent de la machine sur laquelle vous
exécutez ces commandes. Mais, sur une même machine, les résultats peuvent fluctuer d’une exécution à
l’autre en fonction de l’activité de la machine. Ces fluctuations seront d’autant plus importantes que
le temps d’exécution est court.
18.6.4 %%timeit
Pour palier à ce problème, la magic command %%timeit va exécuter plusieurs fois la cellule et donner
une estimation du temps d’exécution moyen. Python détermine automatiquement le nombre d’itérations
et le nombre de répétitions à effectuer pour obtenir un temps global d’exécution raisonnable.
En reprenant l’exemple précédent, on obtient :
1 %%timeit
2 concentrations = [5.5, 7.2, 11.8, 13.6, 19.1, 21.7, 29.4]
3 somme_carres = 0.0
4 for conc in concentrations:
5 somme_carres += conc**2
492 ns ± 11.8 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
et
1 %%timeit
2 concentrations = [5.5, 7.2, 11.8, 13.6, 19.1, 21.7, 29.4]
3 somme_carres = 0.0
4 for idx in range(len(concentrations)):
5 somme_carres += concentrations[idx]**2
606 ns ± 21.6 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
Ici, chaque cellule sera exécutée un million de fois sur sept répétitions, soit sept millions de fois au
total. Comme nous l’avions expliqué dans le chapitre 5 Boucles et comparaisons, itérer une liste sur ses
éléments est la méthode la plus efficace (et la plus élégante).
18.7 Lancement d’une commande Unix
Enfin, dans les environnements Linux ou Mac OS X, il est possible de lancer une commande Unix
depuis un notebook Jupyter. Il faut pour cela faire précéder la commande du symbole « ! ». Par exemple, la
commande ls affiche le contenu du répertoire courant :
1 !ls
[Link] [Link]
[Link] jupyter-
[Link] [Link]
Conseil
Les notebooks Jupyter sont particulièrement adaptés à l’analyse de données en combinaison avec les
modules mat- plotlib et pandas, qui seront abordés dans les prochains chapitres.
FIGURE 18.8 – Notebook avec : (A) une cellule au format Markdown et (B) le rendu après exécution.
FIGURE 18.9 – Incorporation d’un graphique dans un notebook Jupyter.
CHAPITRE 19
Module Biopython
1 import Bio
Attention
Le nom du module Biopython n’est pas biopython, mais Bio (avec un B majuscule).
194
19.3. Manipulation de séquences Chapitre 19. Module Biopython
1 import Bio
2 from [Link] import Seq
3 ADN = Seq("ATATCGGCTATAGCATGC")
4 ADN
Seq('ATATCGGCTATAGCATGC')
• Ligne 1. Le module Biopython s’appelle Bio.
• Ligne 2. On charge la classe Seq du sous-module [Link].
• Ligne 3. La variable ADN est de type Seq, comme affiché dans le résultat.
1 [Link]()
Seq('TATAGCCGATATCGTACG')
1 ADN.reverse_complement()
Seq('GCATGCTATAGCCGATAT')
19.3.3 Traduction en séquence protéique
1 [Link]()
Seq('ISAIAC')
Conseil
Dans l’annexe A Quelques formats de données en biologie, vous trouverez de nombreux exemples d’utilisation
de
Biopython pour manipuler des données aux formats FASTA, GenBank et PDB.
1 res_esearch.keys()
1 res_esearch["IdList"]
1 len(res_esearch["IdList"])
20
Cette liste ne contient les identifiants que de 20 publications, alors que, si nous faisons cette même
requête directement sur le site de PubMed depuis un navigateur web, nous obtenons plus de 45 700
résultats.
En réalité, le nombre exact de publications (en janvier 2024) est connu :
1 res_esearch["Count"]
'45717'
Pour ne pas saturer les serveurs du NCBI, seulement 20 PMID sont renvoyés par défaut. Mais vous
pouvez augmenter cette limite en utilisant le paramètre retmax dans la fonction [Link]().
Nous pouvons maintenant récupérer des informations sur une publication précise en connaissant son
PMID, par
exemple, l’article avec le PMID 22294463 5, dont un aperçu est sur la figure 19.1.
FIGURE 19.1 – Aperçu de la publication Known and potential roles of transferrin in iron biology depuis le site
PubMed.
La variable res_esummary n’est pas réellement une liste (son type exacte est
[Link]), mais elle est indexable (voir chapitre 14 Conteneurs). Cette pseudo-
liste n’a qu’un seul élément, qui est lui-même un dictionnaire dont voici les clés :
1 res_esummary[0].keys()
Nous pouvons alors facilement obtenir le titre, le DOI et la date de publication (PubDate) de cet
article, ainsi que le journal (Source) dans lequel il a été publié :
1 res_esummary[0]["Title"]
1 res_esummary[0]["DOI"]
'10.1007/s10534-012-9520-3'
1 res_esummary[0]["PubDate"]
'2012 Aug'
1 res_esummary[0]["Source"]
'Biometals'
Enfin, pour récupérer le résumé de la publication précédente, nous allons utiliser la fonction [Link]() :
1 req_efetch = [Link](
2 db="pubmed", id="22294463",
3 rettype="abstract", retmode="text")
4 req_efetch.read()
1 req_efetch = [Link](
2 db="pubmed", id="22294463",
3 rettype="abstract", retmode="text")
4 print(req_efetch.read())
1. Biometals. 2012 Aug;25(4):677-86. doi: 10.1007/s10534-012-9520-3.
Bartnikas TB(1).
Author information:
(1)Department of Pathology, ’Childrens Hospital, Enders 1110, 300
Longwood Avenue, Boston, MA 02115, USA.
[Link]@[Link]
DOI: 10.1007/s10534-012-
9520-3 PMCID: PMC3595092
PMID: 22294463 [Indexed for MEDLINE]
Le résultat contient bien le résumé de la figure 19.1, mais aussi d’autres informations comme le
titre, le DOI, la date de publication…
19.5 Exercices
Conseil
Pour ces exercices, utilisez des notebooks Jupyter.
Conseil
Pour cet exercice, n’hésitez pas à consulter :
• Le chapitre 14 Conteneurs pour trier un dictionnaire.
• L’annexe A Quelques formats de données en biologie pour lire un fichier FASTA avec Biopython.
19.5.2 Années de publication des articles relatifs à la barstar
L’objectif de cet exercice est d’interroger automatiquement la base de données bibliographique
PubMed pour déter- miner le nombre d’articles relatifs à la protéine barstar publiés chaque année.
Vous utiliserez le module Biopython et le module matplotlib, qui sera vu un peu plus loin (les
principales instructions vous seront fournies).
Récupérez les informations de la publication dont le PMID est 29701945 8. Vous utiliserez la
méthode Entrez. esummary().
Affichez le titre, le DOI, le nom du journal (Source) et la date de publication (PubDate) de cet
article. Vérifiez que cela correspond bien à ce que vous avez lu sur PubMed.
Vous pouvez également ajouter un peu de cosmétique et enregistrer le graphique sur votre disque dur :
Module NumPy
Le module NumPy 1 est incontournable en bioinformatique. Il permet d’effectuer des calculs sur des
vecteurs ou des matrices, élément par élément, via un nouveau type d’objet appelé array.
20.1 Installation et convention
Contrairement aux modules vus précédemment, NumPy n’est pas fourni avec la distribution Python
de base. Avec la distribution Miniconda que nous vous conseillons d’utiliser (consultez pour cela la
documentation en ligne 2), vous pouvez rapidement l’installer avec la commande :
1 import numpy
Par convention, on utilise np comme nom raccourci pour NumPy :
1 import numpy as np
1 import numpy as np
2 a = [1, 2, 3]
3 [Link](a)
array([1, 2, 3])
1 b = [Link](a)
2 b
array([1, 2, 3])
1 type(b)
[Link]
Nous avons converti la liste [1, 2, 3] en array. La fonction [Link]() accepte aussi comme argument
un tuple, ou un objet de type range.
Par ailleurs, lorsqu’on demande à Python d’afficher le contenu d’un objet array, le mot array et les symboles ([ et
]) sont utilisés pour le distinguer d’une liste (délimitée par les caractères [ et ]) ou d’un tuple (délimité par les
caractères
( et )).
Remarque
Un objet array ne contient que des données homogènes, c’est-à-dire d’un type identique. Il est
possible de créer un objet array à partir d’une liste contenant des entiers et des chaînes de caractères,
mais, dans ce cas, toutes les valeurs
seront comprises par NumPy comme des chaînes de caractères :
1 a = [Link]([1, 2, "tigre"])
2 a
1 b = [Link]([1, 2, 3.5])
2 b
array([1. , 2. , 3.5])
Ici, la notation 1. indique qu’il s’agit du float 1.0000... et pas de l’entier 1.
Sur un modèle similaire à la fonction range(), la fonction arange() permet de construire un array à une dimension :
1 [Link](10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Comme avec range(), on peut spécifier en argument une borne de début, une borne de fin et un pas :
1 [Link](10, 0, -1)
array([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])
Un autre avantage de la fonction arange() est qu’elle génère des objets array qui contiennent des entiers ou des
floats (ce qui n’est pas possible avec range()) selon l’argument qu’on lui passe. D’abord un entier :
1 [Link](10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
Puis un float :
1 [Link](10.0)
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
La différence fondamentale entre un objet array à une dimension et une liste (ou un tuple) est
que celui-ci est considéré comme un vecteur. Par conséquent, on peut effectuer des opérations
vectorielles élément par élément sur ce type d’objet, ce qui est bien commode lorsqu’on analyse de
grandes quantités de données. Regardez ces exemples :
1 v = [Link](4)
2 v
array([0, 1, 2, 3])
On ajoute 1 à chacun des éléments de l’array v :
1 v+1
array([1, 2, 3, 4])
On multiplie par 2 chacun des éléments de l’array v :
1 v*2
array([0, 2, 4, 6])
Avec les listes, ces opérations n’auraient été possibles qu’en utilisant des boucles. Nous vous
encourageons donc à utiliser dorénavant les objets array lorsque vous aurez besoin de faire des
opérations élément par élément.
Il est aussi possible de multiplier deux arrays entre eux. Le résultat correspond alors à la
multiplication élément par élément des deux arrays initiaux :
1 v*v
array([0, 1, 4, 9])
20.3.1 Array et dimensions
Il est aussi possible de construire des objets arrays à deux dimensions, il suffit de passer en argument
une liste de listes à la fonction array() :
1 w = [Link]([[1, 2], [3, 4], [5, 6]])
2 w
array([[1, 2],
[3, 4],
[5, 6]])
On peut aussi créer des tableaux à trois dimensions en passant comme argument à la fonction array()
une liste de listes de listes :
[[4, 5],
[5, 6]]])
La fonction array() peut créer des tableaux à n’importe quel nombre de dimensions. Toutefois, cela
devient vite compliqué lorsqu’on dépasse trois dimensions. Retenez qu’un objet array à une dimension peut
être assimilé à un vecteur, un array à deux dimensions à une matrice. On peut généraliser ces objets
mathématiques avec un nombre arbitraires de dimensions, on parle alors de tenseur, qui sont représentés
avec NumPy en array à n dimensions. Nous nous focaliserons dans la suite sur des arrays à une dimension
(1D) ou deux dimensions (2D).
Avant de continuer, il est important de définir comment sont organisés ces arrays 2D qui représentent
des matrices. Il s’agit de tableaux de nombres qui sont organisés en lignes et en colonnes comme le montre
la figure 20.1. Les indices indiqués dans cette figure seront définis un peu plus loin dans la rubrique
Indices.
array 2D. Voici quelques attributs intéressants pour décrire un objet array :
1 v = [Link](4)
2 v
array([0, 1, 2, 3])
array([[1, 2],
[3, 4],
[5, 6]])
L’attribut .ndim renvoie le nombre de dimensions de l’array. Par exemple, 1 pour un vecteur et 2 pour une matrice
:
1 [Link]
1 [Link]
2
L’attribut .shape renvoie les dimensions sous forme d’un tuple. Dans le cas d’une matrice (array à deux
dimensions), la première valeur du tuple correspond au nombre de lignes et la seconde au nombre
de colonnes.
1 [Link]
(4,)
1 [Link]
(3, 2)
Enfin, l’attribut .size renvoie le nombre total d’éléments contenus dans l’array :
1 [Link]
1 [Link]
6
20.3.2 Redimensionnement d’array
La méthode .reshape() renvoie un nouvel array avec les dimensions spécifiées en argument :
1 a = [Link](0, 6)
2 a
array([0, 1, 2, 3, 4, 5])
1 [Link]
(6,)
1 b = [Link]((2, 3))
2 b
array([[0, 1, 2],
[3, 4, 5]])
1 [Link]
(2, 3)
1 a
array([0, 1, 2, 3, 4, 5])
Notez bien que l’array initial a n’a pas été modifié et que [Link]((2, 3)) n’est pas la même chose que
[Link]((3, 2)) :
1 c = [Link]((3, 2))
2 c
array([[0, 1],
[2, 3],
[4, 5]])
1 [Link]
(3, 2)
La méthode .reshape() attend que les nouvelles dimensions soient compatibles avec la dimension
initiale de l’objet array, c’est-à-dire que le nombre d’éléments contenus dans les différents arrays soit le
même. Dans nos exemples précédents, 6 = 2 × 3 = 3 × 2.
Si les nouvelles dimensions ne sont pas compatibles avec les dimensions initiales, la méthode .reshape()
génère une erreur.
1 a = [Link](0, 6)
2 a
array([0, 1, 2, 3, 4, 5])
1 [Link]
(6,)
1 d = [Link]((3, 4))
1 a = [Link](0, 6)
2 [Link]
(6,)
1 a
array([[0, 1, 2],
[3, 4, 5],
[0, 0, 0]])
1 b = [Link](0, 10)
2 [Link]
(10,)
(2, 3)
1 b
array([[0, 1, 2],
[3, 4, 5]])
Attention
• Cette modification de la forme de l’array par la méthode .resize() est faite « sur place » (in place),
c’est-à-dire que la méthode ne renvoie rien, mais l’array initial est bel et bien modifié (comme des
méthodes sur les listes telles que la méthode .reverse(), voir le chapitre 13 Plus sur les listes).
• Si l’option refcheck=False n’est pas présente, Python peut parfois renvoyer une erreur s’il existe
des références vers l’array qu’on souhaite modifier.
Enfin, il existe la fonction [Link]() qui, dans le cas d’un nouvel array plus grand que l’array
initial, va répéter l’array initial afin de remplir les cases manquantes :
1 a = [Link](0, 6)
2 [Link]
(6,)
(3, 5)
1 c
array([[0, 1, 2, 3, 4],
[5, 0, 1, 2, 3],
[4, 5, 0, 1, 2]])
1 a
array([0, 1, 2, 3, 4, 5])
Notez que la fonction [Link]() renvoie un nouvel array mais ne modifie pas l’array initial,
contrairement à la méthode .resize(), décrite ci-dessus.
Remarque
Depuis le début de ce chapitre, nous avons toujours montré l’affichage d’un array tel quel dans un notebook Jupyter
:
1 a = [Link](range(10))
2 a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
1 a2 = [Link]((3, 3))
2 a2
1 print(a)
[0 1 2 3 4 5 6 7 8 9]
1 print(a2)
[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
Ceci peut amener des confusions, en particulier entre un array 1D :
[0 1 2 3 4 5 6 7 8 9]
et une liste contenant les mêmes éléments :
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Dans ce cas, seule la présence ou l’absence de virgules permet de savoir s’il s’agit d’un array ou d’une liste.
[2, 7, 6, 4, 0, 3, 1, 5]
1 [Link]()
7
La méthode .max() a bien renvoyé la valeur maximale 7. Un argument très utile existant dans toutes
ces méthodes est axis. Pour un array 2D, axis=0 signifie qu’on fera l’opération le long de l’axe 0, à savoir
les lignes. C’est-à-dire que l’opération se fait en variant les lignes. On récupère ainsi une valeur par
colonne :
1 [Link](axis=0)
array([6, 7])
Dans l’array 1D récupéré, le premier élément vaut 6 (maximum de la 1ère colonne) et le second vaut
7 (maximum de la seconde colonne).
Avec axis=1, on fait une opération similaire, mais en faisant varier les colonnes. On récupère ainsi
une valeur par ligne :
1 [Link](axis=1)
array([7, 6, 3, 5])
L’array 1D récupéré a quatre éléments correspondant au maximum de chaque ligne.
On comprend la puissance de l’argument axis. À nouveau, il est possible, en une ligne, de faire
des calculs qui pourraient être fastidieux avec les listes traditionnelles.
20.3.4 Indices
Pour récupérer un ou plusieurs élément(s) d’un objet array, vous pouvez utiliser les indices, de la
même manière qu’avec les listes :
1 a = [Link](10)
2 a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
1 a[1]
1
L’utilisation des tranches est aussi possible :
1 a[5:]
array([5, 6, 7, 8, 9])
Ainsi que les pas :
1 a[::2]
array([0, 2, 4, 6, 8])
Dans le cas d’un objet array à deux dimensions, vous pouvez récupérer une ligne complète (d’indice
i), une colonne complète (d’indice j) ou bien un seul élément. La figure 20.1 montre comment sont
organisés les indices des lignes et des colonnes :
1 a = [Link]([[1, 2], [3, 4]])
2 a
array([[1, 2],
[3, 4]])
1 a[:,0]
array([1, 3])
1 a[0,:]
array([1, 2])
La syntaxe a[i,:] renvoie la ligne d’indice i, et a[:,j] renvoie la colonne d’indice j. Les tranches sont
aussi utilisables sur un array à deux dimensions.
1 a[1, 1]
4
La syntaxe a[i, j] renvoie l’élément à la ligne d’indice i et à la colonne d’indice j. Notez que NumPy
suit la convention mathématiques des matrices 4, à savoir, qu’on définit toujours un élément par sa
ligne puis par sa colonne. En mathématiques, l’élément ai j d’une matrice A se trouve à la ime ligne
et à la jme colonne :
Remarque
Pour un array 2D, si un seul indice est donné, par exemple a[i], on récupère la ligne d’indice i sous
forme d’array 1D :
array([[1, 2],
[3, 4]])
1 a[0]
array([1, 2])
1 a[1]
array([3, 4])
Pour cette raison, la syntaxe a[i][j] est également valide pour récupérer un élément :
1 a[1, 1]
1 a[1][1]
1 a = [Link](5)
2 a
array([0, 1, 2, 3, 4])
1 b=a
2 b[2] = -300
3 b
1 a
Attention
Par défaut la copie d’arrays se fait par référence, comme pour tous les conteneurs en Python (listes,
tuples, diction- naires, etc.).
Afin d’éviter le problème, vous pouvez soit utiliser la fonction [Link](), qui crée une nouvelle
copie distincte de l’array initial, soit la fonction [Link](), comme pour les listes (voir
chapitre 12 Plus sur les listes) :
1 a = [Link]((2, 2), 0)
2 a
array([[0, 0],
[0, 0]])
1 b = [Link](a)
2 b[1, 1] = -300
3 import copy
4 c = [Link](a)
5 c[1, 1] = -500
6 a
array([[0, 0],
[0, 0]])
1 b
array([[ 0, 0],
[ 0, -300]])
1 c
array([[ 0, 0],
[ 0, -500]])
La fonction [Link]() est expliquée dans la rubrique suivante.
Remarque
L’instruction b = [Link](a) réalise bien une copie distincte de l’array a, quelle que soit sa
dimensionnalité. Ceci n’était pas le cas avec la fonction list() pour les copies de listes à partir de la
dimension deux (liste de listes) :
1 liste_3
1 [Link]((2, 3))
1 [Link]((3, 3))
Par défaut, les fonctions zeros() et ones() génèrent des floats, mais vous pouvez demander des entiers en
passant le type (par exemple int, float, etc.) en second argument :
1 [Link]((2,3), int)
array([[0, 0, 0],
[0, 0, 0]])
20.5. Chargement d’un array depuis un fichier Chapitre 20. Module NumPy
Enfin, si vous voulez construire une matrice avec autre chose que des 0 ou des 1, vous avez à votre
disposition la fonction full() :
1 [Link]((2, 3), 7, int)
array([[7, 7, 7],
[7, 7, 7]])
1 7 310
15 -4 35
78 95 79
La fonction prend en argument le nom du fichier et renvoie un array 2D directement :
1 [Link]("[Link]")
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
1 [Link]("[Link]", a)
Ceci générera le fichier [Link] contenant les lignes suivantes :
1 a1 = [Link]((0, 1))
2 a2 = [Link]((3, 4))
3 a1
array([0, 1])
1 a2
array([3, 4])
1 [Link]((a1, a2))
array([0, 1, 3, 4])
L’ordre de la concaténation est important :
1 [Link]((a2, a1))
array([3, 4, 0, 1])
array([0, 1, 3, 4, 0, 1, 3, 4])
Pour les arrays 2D, ça se complique un peu, car on peut concaténer des lignes ou des colonnes !
Ainsi, np. concatenate() prend un argument optionnel, à savoir axis. Comme nous l’avions expliqué
plus haut, celui-ci va indiquer à NumPy si on veut concaténer le long de l’axe 0 (les lignes) ou le long
de l’axe 1 (les colonnes). Voyons un exemple :
1 a1 = [Link]([Link](range(6)), (3, 2))
2 a2 = a1 * 5
3 a1
array([[0, 1],
[2, 3],
[4, 5]])
1 a2
array([[ 0, 5],
[10, 15],
[20, 25]])
On concatène d’abord par ligne (axis=0), c’est-à-dire qu’on ajoute les lignes du second array a2 à celles de l’array
a1 :
1 [Link]((a1, a2), axis=0)
array([[ 0, 1],
[ 2, 3],
[ 4, 5],
[ 0, 5],
[10, 15],
[20, 25]])
Ensuite, on concatène par colonne (axis=1). Attention, il vaut bien veiller à ce que la concaténation
soit possible en terme de dimensionalité. Par exemple, lors de la concaténation par colonne, il faut que
les deux arrays a1 et a2 aient le même nombre de lignes :
array([[ 0, 1, 0, 5],
[ 2, 3, 10, 15],
[ 4, 5, 20, 25]])
Ces opérations de concaténation sont très importantes. On les utilise par exemple si on a des données
dans plusieurs fichiers différents et qu’on veut les agréger dans un array unique. On verra qu’on peut faire
le même genre de chose avec les fameux Dataframes du module pandas. Lisez bien également les
recommandations dans la dernière rubrique 17.1.10 Quelques conseils sur quand utiliser la
concaténation d’arrays avec NumPy.
1 [Link](a)
array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]])
Tout objet array possède un attribut .T qui contient la transposée, il est ainsi possible d’utiliser cette
notation objet plus compacte :
1 a.T
array([[1, 4, 7],
[2, 5, 8],
[3, 6, 9]])
array([[0, 1],
[2, 3]])
1 [Link](a, a)
array([[ 2, 3],
[ 6, 11]])
1 a*a
array([[0, 1],
[4, 9]])
Notez bien que dot(a, a) renvoie le produit matriciel entre deux matrices, alors que l’opération a * a
renvoie le produit élément par élément.
Remarque
Dans le module NumPy, il existe également des objets de type matrix pour lesquels les
multiplications de matrices sont différents, mais nous ne les aborderons pas ici.
Pour toutes les opérations suivantes, nous utiliserons des fonctions du sous-module linalg
de NumPy. La fonction diag() permet de générer une matrice diagonale :
1 a = [Link]((1, 2, 3))
2 a
array([[1, 0, 0],
[0, 2, 0],
[0, 0, 3]])
1 [Link](a)
array([[1. , 0. , 0. ],
[0. , 0.5 , 0. ],
[0. , 0. , 0.33333333]])
6.0
Enfin, la fonction eig() renvoie les vecteurs et valeurs propres :
1 [Link](a)
1 eigvecs
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
1 for row in a:
2 print(row, type(row))
[1 2 3] <class '[Link]'>
[4 5 6] <class '[Link]'>
[7 8 9] <class '[Link]'>
À chaque itération, la variable row est un array 1D correspondant à chaque ligne de la matrice a.
Cela est du au fait que l’utilisation d’un indiçage unique a[i] pour un array 2D correspond à sa ligne
d’indice i (voir la rubrique Indices ci- dessus).
Pour itérer sur les colonnes, on peut utiliser l’astuce d’itérer sur la transposée de l’array a, c’est-à-dire a.T :
1 a
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
1 a1, a2, a3 = a
2 a1
array([1, 2, 3])
1 a2
array([4, 5, 6])
1 a3
array([7, 8, 9])
Par défaut, l’affectation multiple se fait sur les lignes de l’array 2D. Cette fonctionnalité s’explique
à nouveau par le fait que pour NumPy, a[i] correspond à la ligne d’indice i d’un array 2D.
Pour utiliser l’affectation multiple sur les colonnes, il suffit d’utiliser la transposée a.T :
array([1, 4, 7])
1 c2
array([2, 5, 8])
1 c3
array([3, 6, 9])
array([[False,
False],
[False, False]])
Au premier abord, nous n’en voyons pas forcément l’utilité… Mais qu’en est-il lorsqu’on utilise
les opérateurs de comparaison avec un array ? Et bien cela renvoie un array de booléens !
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
1 a>5
array([[False, False,
False], [False,
False, True], [
True, True,
True]])
1 a == 2
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
1 (a > 3) & (a % 2 == 0)
array([[False, False,
False], [ True,
False, True], [False,
True, False]])
1 (a > 3) | (a % 2 == 0)
array([[False, True,
False], [ True,
True, True], [
True, True,
True]])
• Les opérateurs logiques & et | s’appliquent sur les arrays et sont différents des opérateurs logiques
and et or, qui eux s’appliquent sur les booléens (True ou False).
• Il est conseillé de mettre entre parenthèses chaque condition afin d’éviter les ambiguïtés.
Maintenant que les arrays de booléens ont été introduits, nous pouvons définir les masques booléens :
Définition
Les masques booléens sont des arrays de booléens qui sont utilisés en tant qu’« indice » d’un array
initial. Cela permet de récupérer ou de modifier une partie de l’array initial.
Concrètement, il suffira d’utiliser un array et un opérateur de comparaison entre les crochets qui
étaient dédiés à l’indiçage :
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
Pour isoler tous les éléments de l’array a qui sont supérieurs à 5 :
1 a[a > 5]
array([6, 7, 8, 9])
Pour isoler tous les éléments de l’array a qui sont égaux à 2 :
1 a[a == 2]
array([2])
Pour isoler tous les éléments de l’array a qui sont non nuls :
1 a[a != 0]
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
À chaque fois, on ne récupère que les éléments de l’array a qui satisfont la sélection. Toutefois, il est
important de remarquer que l’array renvoyé perd la dimensionnalité de l’array a initial, il s’agit
systématiquement d’un array 1D.
La grande puissance de ce mécanisme est que l’on peut utiliser les masques booléens pour modifier
les éléments que l’on sélectionne :
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
On sélectionne les éléments de l’array a supérieurs à 5 :
1 a[a > 5]
array([6, 7, 8, 9])
On affecte la valeur -1 aux éléments de l’array a supérieurs à 5 :
1 a[a > 5] = -1
2 a
array([[ 1, 2, 3],
[ 4, 5, -1],
[-1, -1, -1]])
On peut bien sûr combiner plusieurs conditions avec les opérateurs logiques :
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
array([[1, 0, 3],
[0, 0, 0],
[0, 0, 0]])
Ce mécanisme de sélection avec des masques booléens se révèle très puissant pour manipuler de
grandes quantités de données. On verra qu’il peut être également utilisé avec les Dataframes du
module pandas.
Remarque
Les masques booléens ne doivent pas être confondus avec les masked arrays 17, qui sont des arrays dans
lesquels on peut trouver des valeurs manquantes ou invalides.
Enfin, une application possible des masques est de « binariser » une matrice de nombre :
1 import random
2 import numpy as np
3 a = [Link]([[Link]() for i in range(16)], (4, 4))
4 a
1 seuil = 0.3
2 a[a < seuil] = 0
3 a[a > seuil] = 1
On obtient ce résultat avec deux lignes de code en utilisant des arrays, alors qu’il aurait fallu faire
des boucles avec des listes classiques.
20.11 Exercices
Conseil
Pour ces exercices, utilisez des notebooks Jupyter.
Les coordonnées cartésiennes (x, y, z) de chaque atome de phosphore (en Å) sont stockées dans le fichier
coors_P.dat 26, à raison d’un atome par ligne.
Nous vous proposons les étapes suivantes pour résoudre cet exercice à l’aide du module NumPy :
1. Récupérez les coordonnées des atomes de phosphore depuis le fichier coors_P.dat et stockez-les dans un array
2D (matrice) coors_P. La dimensionnalité de cette matrice est n × 3, avec n le nombre de phosphores.
2. Calculez le z moyen de tous les phosphores (nombre réel) et stockez-le dans la variable mean_z. La
méthode
.mean() vous sera utile.
3. Avec des masques de booléens, récupérez les coordonnées des phosphores de la monocouche du haut dans un array
2D upper. Faites de même avec la monocouche du bas dans un array 2D lower.
4. Calculez le centre de masse COM de la membrane, ainsi que de la monocouche du haut COM_upper et du
bas
COM_lower. Pensez aux méthodes de calcul sur les arrays et l’argument axis.
5. Une fois tout cela effectué, créez un graphique 3D pour représenter les différents centres de
masse. Utilisez la fonction scatter() du module matplotlib pour l’affichage en 3D 27. Voici un
squelette de programme pour vous
aider :
1 # Initialisation du graphique.
2 from mpl_toolkits.mplot3d import Axes3D
3 import [Link] as plt
4 fig = [Link]()
5 ax = fig.add_subplot(111, projection="3d")
6 [...]
7 # X, Y et Z sont des arrays 1D de n éléments.
8 # Par exemple X représente tous les x des P de la monocouche upper.
9 [...]
10 # Affichage de la couche upper.
11 [Link](X, Y, Z, c="salmon", marker="o")
12 # Affichage du COM de la couche upper.
13 [Link](x, y, z, c="red", marker="x")
14 [...]
15 # Affichage des étiquettes des axes et du titre.
16 ax.set_xlabel("x (Å)")
17 ax.set_ylabel("y (Å)")
18 ax.set_zlabel("z (Å)")
19 ax.set_title("Graphe 3D des phosphores")
20 [Link]()
CHAPITRE 21
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.
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
Temps Concentration
(h) (mg/L)
1 3.5
2 5.8
3 9.1
4 11.8
6 17.5
7 21.3
9 26.8
Dans un notebook Jupyter, vous devriez obtenir un graphique ressemblant à celui de la figure 21.1.
FIGURE 21.1 – Graphique produit par matplotlib.
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)
1 import numpy as np
2 import [Link] as plt
3
4 sequence = "ACGATCATAGCGAGCTACGTAGAA"
5 bases = ["A", "C", "G", "T"]
6 distribution = []
7 for base in bases:
8 [Link]([Link](base))
9
10 x = [Link](len(bases))
11
12 fig, ax = [Link]()
13 [Link](x, distribution)
14 ax.set_xticks(x, bases)
15 ax.set_xlabel("Bases")
16 ax.set_ylabel("Nombre")
17 ax.set_title(f"Distribution des bases\n dans la séquence {sequence}")
18 [Link]("distribution_bases.png", bbox_inches="tight", dpi=200)
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.
1 import pandas
Par convention, on utilise pd comme nom raccourci pour pandas :
1 import pandas as pd
233
22.3 Series
Le premier type de données apporté par pandas est la Series, qui correspond à un vecteur à une dimension.
a 10
b 20
c 30
d 40
dtype: int64
22.3.1 Sélections par étiquette ou indice
Avec pandas, chaque élément de la série de données possède une étiquette qui permet d’appeler les
éléments qui la composent. Ainsi, pour appeler le premier élément de la série, on peut utiliser son
étiquette (ici, "a") :
1 s["a"]
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) :
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
Voici quelques commentaires sur le code précédent :
• Ligne 1. On charge le module NumPy utilisé ensuite.
• Ligne 2. Le Dataframe est créé avec la fonction DataFrame() à laquelle on fournit plusieurs arguments. L’argument
columns indique le nom des colonnes, sous forme d’une liste.
• Ligne 3. L’argument index définit le nom des lignes, sous forme de liste également.
• Lignes 4 à 6. L’argument data fournit le contenu du Dataframe, sous la forme d’une liste de valeurs
correspon- dantes à des lignes. Ainsi, [Link](10, 14) qui est équivalent à [10, 11, 12, 13]
correspond à la première ligne du Dataframe.
Le même Dataframe peut aussi être créé à partir des valeurs fournies en colonnes sous la forme d’un
dictionnaire :
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].
1 [Link]
(3, 4)
Ici, le dataframe df possède trois lignes et quatre colonnes.
L’attribut .columns renvoie le nom des colonnes et permet aussi de renommer les colonnes d’un dataframe :
1 [Link]
1 [Link](2)
22.4.3 Sélections
Les mécanismes de sélection fournis avec pandas sont très puissants. En voici un rapide aperçu :
1 df["Lyon"]
chat 11
singe 21
souris 31
La notation df["Lyon"] sélectionne une colonne et renvoie un objet Series :
1 type(df["Lyon"])
[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
1 type(df[["Lyon", "Pau"]])
[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').
[Link] Sélection de lignes
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
Ici aussi, on peut sélectionner plusieurs lignes :
1 [Link][["singe", "chat"]]
1 [Link][1]
Paris 20
Lyon 21
Nantes 22
Pau 23
Name: singe, dtype: int64
1 [Link][[1, 0]]
1 [Link][0:2]
1 [Link]["souris", "Pau"]
33
Nantes Lyo
n
sing 22 21
e
souri 32 31
s
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).
1 df[ df["Pau"]>15 ]
singe 21
souris 31
Name: Lyon, dtype: int64
On peut aussi combiner plusieurs conditions avec & pour l’opérateur et :
Lyon Paris
chat 10 3
singe 23 15
souris 17 20
et
1 data2 = {"Nantes": [3, 9, 14], "Strasbourg": [5, 10, 8]}
2 df2 = [Link].from_dict(data2)
3 [Link] = ["chat", "souris", "lapin"]
4 df2
Nante Strasbou
s rg
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.
(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
1 %%timeit
2 for idx, row in [Link]():
3 [Link][idx, "d"] = (row["a"] * row["b"]) + row["c"]
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.
Pour illustrer les possibilités de pandas, voici un exemple plus concret sur un jeu de données de kinases 4.
Les kinases sont des protéines responsables de la phosphorylation d’autres protéines.
Le fichier [Link] que vous pouvez télécharger en ligne 5 contient des informations tirées de la base
de données de séquences UniProt pour quelques kinases.
Si vous n’êtes pas familier avec le format de fichier .csv, nous vous conseillons de consulter
l’annexe A Quelques formats de données en biologie.
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]()
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]()
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.
22.5.3 Conversion en date
Le type object correspond la plupart du temps à des chaînes de caractères. C’est tout à fait légitime pour la colonne
Organism. Mais on sait par contre que la colonne Creation date est une date sous la forme année-mois-jour.
Si le format de date utilisé est homogène sur tout le jeu de données et non ambigu, on peut
demander à pandas de considérer la colonne Creation Date comme une date. pandas détectera alors
1 df["Creation date"] = pd.to_datetime(df["Creation date"])
automatiquement le format de date utilisé :
L’affichage des données n’est pas modifié :
1 [Link]()
Organism object
Length int64
Creation date datetime64[ns]
Mass int64
PDB object
dtype: object
Organism
Human 489
Mouse 489
Rat 253
Fruit fly 103
Chicken 75
Rabbit 25
Maize 8
Name: , dty e: int64
count
On apprend ainsi que 489 protéines sont d’origine humaine (Human) et 8 proviennent du maïs (Maize).
22.5.5 Statistiques par groupe
On peut aussi déterminer, pour chaque organisme, la taille et la masse moyenne des kinases :
1 [Link](["Organism"])[["Length", "Mass"]].mean()
Length Mass
Organis
m
Chicken 720.1600 81120.8800
00 00
Fruit fly 784.8446 88154.6699
60 03
Human 771.0040 86281.1901
90 84
Maize 666.8750 73635.0000
00 00
Mouse 768.0920 85942.2740
25 29
Rabbit 591.4800 66754.2000
00 00
Rat 722.3794 81081.8221
47 34
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 )
mi m
n Ma ax Mass
Organis ss Leng
m Leng
th th
Chicke 303 3468 2311 26096
n 8 1
Fruit fly 294 3318 2554 28702
0 5
Human 253 2816 2986 34026
0 1
Maize 294 3383 996 10598
4 8
Mouse 244 2739 2964 33700
4 0
Rabbit 81 940 1382 15834
5 7
Rat 274 3116 2959 33658
2 7
• L’argument index précise la colonne dont on veut agréger les données.
• L’argument values indique sur quelles colonnes les statistiques sont calculées.
• Enfin, aggfunc liste les statistiques calculées, ici les valeurs minimale et maximale.
Notez que les valeurs renvoyées sont d’abord les valeurs minimales pour Length et Mass puis les valeurs
maximales pour Length et Mass.
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]")
On obtient ainsi le graphique de la figure 22.2.
22.5.7 Analyse de données temporelles
Il peut être intéressant de savoir, pour chaque organisme, quand les premières et les dernières
séquences de kinases ont été référencées dans UniProt.
FIGURE 22.2 – Masse en fonction de la taille des protéines.
mi max
n Creation Creation date
Organis
m date
Chicke 1986-07-21 2021-02-10
n
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
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- Name: count, dtype: int64
DEC,
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: type: int64
count, d
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,
on peut encore utiliser le method chaining :
1 import [Link] as plt
2 (df["Creation date"]
3 .value_counts()
4 .resample("YE")
5 .sum()
6 .plot()
7 )
8 [Link]("[Link]")
On observe un pic du nombre de kinases référencées dans UniProt sur la période 2003-2007.
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]()
Organi nb_structur
Entr sm es
y
P249 Human 453
41
P005 Human 284
33
Q165 Human 245
39
P684 Human 238
00
P113 Human 176
09
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 :
Une protéine kinase
(Une protéine qui nage… dans une piscine… Vous l’avez ?)
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
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
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 :
citron1 = Citron()
[Link] = "jaune"
>>> citron1. dict
{'couleur': 'jaune'}
>>> citron2 = Citron()
>>> citron2. dict
{}
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 :
>>> citron1. dict
{'couleur': 'jaune'}
>>> del [Link]
>>> citron1. dict
{}
Dans la suite, on montrera du code à tester dans un script : n’hésitez pas, comme d’habitude,
à le tester par vous-même.
23.1.3 Les attributs de classe
Si on ajoute une variable dans une classe comme on créait une variable locale dans une fonction,
on crée ce qu’on appelle un attribut de classe :
1 class Citron:
2 couleur = "jaune"
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 ":
citron1 = Citron()
[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
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]
FIGURE 23.1 – Fonctionnement d’un constructeur (étape 1).
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.
FIGURE 23.3 – Fonctionnement d’un constructeur (étape 3).
É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.
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 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 Chapitre 23. Avoir la classe avec les objets
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]
CHAPITRE 24
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
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.
24.1.1 Rappel sur la règle LGI
Comme vu dans le chapitre 10 Fonctions, la règle LGI peut être résumée ainsi : Local > Global >
Interne. Lorsque Python rencontre un objet, il utilise cette règle de priorité pour accéder à la valeur de
celui-ci. Si on est dans une fonction (ou une méthode), Python va d’abord chercher l’espace de noms
local à cette fonction. S’il ne trouve pas de nom il va ensuite chercher l’espace de noms du programme
principal (ou celui du module), donc des variables globales s’y trouvant. S’il ne trouve pas de nom, il va
chercher dans les commandes internes à Python (on parle des Built-in Functions 2 et des Built-in
Constants 3). Si aucun objet n’est trouvé, Python renvoie une erreur.
24.1.2 Gestion des noms dans les modules
Les modules représentent aussi un espace de noms en soi. Afin d’illustrer cela, jetons un coup d’œil à ce
programme
test_var_module.py :
1 import mod
2
3 i = 1000000
4 j = 2
5
6 print("Dans prog principal i:", i)
7 print("Dans prog principal j:", j)
8
9 [Link]()
10 mod.fct2()
11
12 print("Dans prog principal i:", i)
13 print("Dans prog principal j:", j)
2. [Link]
3. [Link]
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]
mère. Ces aspects sont présentés dans la rubrique Héritage de ce chapitre.
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.
24.2.2 Méthodes dunder ou magiques
Comment Python permet-il ces prouesses que sont le polymorphisme et la redéfinition des
opérateurs ? Et bien, il utilise des méthodes dites dunder ou magiques.
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.
24.2. Polymorphisme Chapitre 24. Avoir plus la classe avec les
objets
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-)
À 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
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 :
5. [Link]
6. [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]())
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
classe nommée [Link] dont nous allons voir la signification maintenant.
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].
En résumé, la syntaxe class Citron: sera équivalente à
class Citron([Link]):
ou à class Citron(object):.
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 :
1 >>> class Citron:
2 ... pass
3 ...
4 >>> c = Citron()
5 >>> dir(c)
6 [' class ', ' delattr ', ' dict ', ' dir ', ' doc ', ' eq ',
7 ' format ', ' ge ', ' getattribute ', ' gt ', ' hash ',
8 ' init ', ' le ', ' lt ', ' module ', ' ne ', ' new ',
9 ' reduce ', ' reduce_ex ', ' repr ', ' setattr ', ' sizeof ',
10 ' str ', ' subclasshook ', ' weakref ']
11 >>> o = object()
12 >>> dir(o)
13 [' class ', ' delattr ', ' dir ', ' doc ', ' eq ', ' format ',
14 ' ge ', ' getattribute ', ' gt ', ' hash ', ' init ', ' le ',
15 ' lt ', ' ne ', ' new ', ' reduce ', ' reduce_ex ', ' repr ',
16 ' setattr ', ' sizeof ', ' str ', ' subclasshook ']
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. L’exécution de ce code affichera la sortie suivante :
(1) Je rentre dans le constructeur de Citron, et je vais appeler
le constructeur de la classe mère Fruit !
(2) Je suis dans le constructeur de la classe Fruit
Je viens de créer [Link], [Link], [Link] et [Link]
(3) J'ai fini dans le constructeur de Citron, les attributs sont:
[Link]: petite, [Link]: 50
[Link]: acide, [Link]: ellipsoïde
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 7 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.
Dans cette rubrique, nous souhaitons éclairer le rôle des attributs de classe et des attributs
d’instance, et comment ils sont gérés par Python.
On a vu dans le chapitre précédent comment créer un attribut de classe, il suffit de créer une variable
au sein de la classe (en dehors de toute méthode). En général, les attributs de classe contiennent des
propriétés générales à la classe puisqu’ils vont prendre la même valeur quelle que soit l’instance.
Au contraire, les attributs d’instance sont spécifiques à chaque instance. Pour en créer, on a vu qu’il
suffisait de les initialiser dans la méthode . init () en utilisant une syntaxe self.nouvel_attribut =
valeur. On a vu aussi dans la rubrique Ajout d’un attribut d’instance que l’on pouvait ajouter un
attribut d’instance de l’extérieur avec une syntaxe instance.nouvel_attribut = valeur .
Bien que les deux types d’attributs soient fondamentalement différents au niveau de leur finalité, il existe
des similitudes lorsqu’on veut accéder à leur valeur. Le code suivant illustre cela :
7. [Link]
8. [Link]
9. [Link]
10. [Link]
11. [Link]
12. [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 13.
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
13. [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 :
1 Traceback (most recent call last):
2 File "[Link]", line 11, in <module>
3 del [Link]
4 ^^^^^^^^^^^^^^
5 AttributeError: 'Citron' object has no attribute 'saveur'
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é).
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.
L’exécution de ce code donnera la sortie suivante :
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).
24.6.3 Une meilleure solution : les décorateurs @property, @[Link] @at- [Link]
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 :
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 :
1 >>> citron = Citron()
2 Coucou je suis dans le set
3 >>> [Link]
4 Coucou je suis dans le get
5 0
6 >>> [Link] = -16
7 Coucou je suis dans le set
8 Traceback (most recent call last):
9 File "<stdin>", line 1, in <module>
10 File "<stdin>", line 11, in set_masse
11 ValueError: Un citron ne peut pas avoir de masse négative !
12 >>> [Link] = 16
13 Coucou je suis dans le set
14 >>> [Link]
15 Coucou je suis dans le get
16 16
17 >>> citron._masse
18 16
19 >>> citron._masse = -8364
20 >>> [Link]
21 Coucou je suis dans le get
22 -8364
23 >>>
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 14, 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 :
14. [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.).
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 15 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 :
15. [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 ».
16. [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 17).
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.
Molecule benzene
atom C1, type C, 12.0 amu, coor( - 0.973, -0.003)
mass = 2.145,
atom H1, type H, 1.0 amu, coor( - 0.460, -0.005)
mass = 3.103,
[...]
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)
18. %22[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 19. La page de tutorial 20 pourra vous être utile.
Par exemple :
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
FIGURE 25.1 – Exemple de GUI.
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]
FIGURE 25.2 – Exemple de fonction callback dans Python Tutor.
• 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 [Link] 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.
25.4.2 Le même exemple dans un script.
Tentons maintenant de faire la même chose dans un script tk_exemple.py :
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 !")
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]
25.5. Construire une application Tkinter avec une classe Chapitre 25. Fenêtres graphiques et Tkinter
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.
attributs de cette classe. Reprenons notre petit exemple avec un label et un bouton :
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.pyen 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).
Le code suivant crée une telle application :
10. [Link]
FIGURE 25.5 – Exemple 2 de canvas.
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,
fill=self.rd_col())
308 3356 Cours de Python / Université Paris Cité / UFR Sciences du Vivant
37 def dessine_lignes(self):
38 for i in range(20):
39 x, y, x2, y2 = [[Link](1, [Link]) for j in range(4)]
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]
310
1 lle
" 5 - clic droit: faire rétrécir la baballe
" 6 - clic central: relance la baballe (depuis le point du clic)
" 7 dans une direction aléatoire
S 8 - touche Esc: quitte l'appli baballe
u 9 """
pe 10
r 11 import tkinter as tk
ap 12 import random as rd
pl 13
i 14 class AppliBaballe([Link]):
ba 15 def init (self):
ba 16 """Constructeur de l'application."""
lle 17 [Link]. init (self)
!!! 18 # Coord baballe.
2 19 self.x, self.y = 200, 200
3 U 20 # Rayon baballe.
s 21 [Link] = 50
a 22 # Pas de deplacement.
g 23 [Link], [Link] = 20, 20
e 24 # Création et packing du canvas.
: 25 [Link] = [Link](self, bg='light gray', height=400, width=400)
p 26 [Link]()
y 27 # Création de la baballe.
t 28 [Link] = [Link].create_oval(self.x, self.y,
h 29 self.x+[Link],
o 30 self.y+[Link],
n 31 width=2, fill="blue")
t 32 # Binding des actions.
k 33 [Link]("<Button-1>", [Link])
_ 34 [Link]("<Button-2>", [Link])
b 35 [Link]("<Button-3>", [Link])
a 36 [Link]("<Escape>", [Link])
b 37 # Lancer la baballe.
a 38 [Link]()
l 39
l 40 def move(self):
e 41 """Déplace la baballe (appelée itérativement avec la méthode after)."""
. 42 # Incrémente coord baballe.
p 43 self.x += [Link]
y 44 self.y += [Link]
4 - 45 # Vérifier que la baballe ne sort pas du canvas (choc élastique).
c 46 if self.x < 10:
l 47 [Link] = abs([Link])
i 48 if self.x > [Link]-10:
49 [Link] = -abs([Link])
c 50 if self.y < 10:
g 51 [Link] = abs([Link])
a 52 if self.y > [Link]-10:
u 53 [Link] = -abs([Link])
c 54 # Mise à jour des coord.
h 55 [Link]([Link], self.x, self.y, self.x+[Link],
e 56 self.y+[Link])
: 57 # Rappel de move toutes les 50ms.
f 58 [Link](50, [Link])
a 59
i 60 def boom(self, mclick):
r 61 """Relance la baballe dans une direction aléatoire au point du clic."""
e 62 self.x = mclick.x
g 63 self.y = mclick.y
r 64 [Link].create_text(self.x, self.y, text="Boom !", fill="red")
o 65 [Link] = [Link]([-30, -20, -10, 10, 20, 30])
s 66 [Link] = [Link]([-30, -20, -10, 10, 20, 30])
s 67
i 68 def incr(self, lclick):
r 69 """Augmente la taille de la baballe."""
l 70 [Link] += 10
a 71 if [Link] > 200:
b 72 [Link] = 200
a 73
b 74 def decr(self, rclick):
a
75 """Diminue la taille de la baballe."""
76 s
e
l
f
.
s
i
z
e
-
=
1
0
77 i
f
s
e
l
f
.
s
i
z
e
<
1
0
:
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
1 import time
2
3 ...
4
5 while True:
6 move()
7 [Link](0.05) # attendre 50 ms
avec la boucle suivante :
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.
25.7.2 Autres pistes à approfondir
Si vous souhaitez aller un peu plus loin en Tkinter, voici quelques notions / remarques qui pourraient vous être utiles.
Conseil
Si vous êtes débutant, vous pouvez sauter cette rubrique.
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.
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.
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.
23. [Link]
24. [Link]
25. [Link]
26. [Link]
27. [Link]
28. [Link]
29. [Link]
30. [Link]
31. [Link]
32. [Link]
33. [Link]
34. [Link]
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]
définir les 3 sommets d'un triangle isocèle ou équilatéral
point <- coordonnées (x, y) du centre du trianle
dessiner(point) # un pixel de large
pour i de 0 à 25000:
sommet_tmp <- choisir un sommet du triangle au hasard
point <- calculer(coordonnées(x, y) du centre entre point et sommet_tmp)
dessiner(point)
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.
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'
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.
26.1.5 Encodage et utf-8
En Python 3, vous pouvez utiliser des caractères accentués dans les commentaires ou dans les chaînes de
caractères.
Ce n’est pas le cas en Python 2. Si un caractère accentué est présent dans votre code, cela
occasionnera une erreur de ce type lors de l’exécution de votre script :
SyntaxError: Non-ASCII character '\xc2' in file xxx on line yyy, but no encoding
declared; see [Link] for details
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 -*-
1. [Link]
2. [Link]
3. [Link]
26.2. Anciennes méthodes de formatage des chaînes de caractères Chapitre 26. Remarques complémentaires
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.
26.2.2 La méthode .format()
Depuis la version 2.7 de Python, la méthode .format() a apporté une nette amélioration pour
1 >>> x = 32
2 >>> nom = "John"
3 >>> print("{} a {} ans".format(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 {} G et {} C -> prop GC = {:.2f}".format(nb_G, nb_C, prop_GC))
9 On a 4500 G et 2575 C -> prop GC = 0.48
mettre en place l’écriture formatée. Celle-ci fonctionne de la manière suivante :
• 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 !
26.3.1 Définition
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]
1 >>> (lambda: 1/2)()
2 0.5
3 >>> (lambda x, y: x + y)(1, 2)
4 3
5 >>> (lambda x, y: x + y)(4, 5)
6 9
7 >>> (lambda: 1/2)(5)
8 Traceback (most recent call last):
9 File "<stdin>", line 1, in <module>
10 TypeError: <lambda>() takes 0 positional arguments but 1 was given
11 >>> (lambda x, y: x + y)(4)
12 Traceback (most recent call last):
13 File "<stdin>", line 1, in <module>
14 TypeError: <lambda>() missing 1 required positional argument: 'y'
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
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
pas le numéro de ligne dans la Traceback :
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'
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().
Conseil
Pour aller plus loin, vous pouvez consulter ces quelques articles : Dataquest 9, Trey Hunner 10,
RealPython 11 et Dan Bader 12.
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]
1 >>> iterateur = iter(animaux)
2 >>> for elt in iterateur:
3 ... print(elt)
4 ...
5 chien
6 chat
7 souris
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.
1 >>> next(animaux)
2 Traceback (most recent call last):
3 File "<stdin>", line 1, in <module>
4 TypeError: 'list' object is not an iterator
Pour prouver cela, essayons la fonction next() avec une liste :
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 :
1 >>> import iterator
2 >>> iter_alphabet = [Link]()
3 >>> iter_alphabet
4 <[Link] object at 0x7f308edc70b0>
5 >>> for lettre in iter_alphabet:
6 ... print(lettre)
7 ...
8 a
9 b
10 [...]
11 y
12 z
13 >>> list(iter_alphabet)
14 []
À 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 :
1 >>> animaux = ["chien", "chat", "souris"]
2 >>> obj_enum = enumerate(animaux)
3 >>> obj_enum
4 <enumerate object at 0x7f917ebf93a0>
5 >>> tuple(obj_enum)
6 ((0, 'chien'), (1, 'chat'), (2, 'souris'))
7 >>> tuple(obj_enum)
8 ()
13. [Link]
14. [Link]
15. [Link]
16. [Link]
1 >>> line = "9 11 25 92 49 98 62 72 63 74"
2 >>> obj_map = map(int, [Link]())
3 >>> obj_map
4 <map object at 0x7f029e47b9a0>
5 >>> min(obj_map)
6 9
7 >>> list(obj_map)
8 []
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é.
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]
1 >>> parcours_echiquier = [Link]("abcdefgh", "12345678")
2 >>> parcours_echiquier
3 <[Link] object at 0x7f192e412040>
4 >>> for col, ligne in parcours_echiquier:
5 ... print(col, ligne)
6 ...
7 a 1
8 a 2
9 [...]
10 h 7
11 h 8
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.
• 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
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 :
1 >>> def alphabet_generator():
2 ... for i in range(97, 123):
3 ... yield chr(i)
4 ...
5 >>> gen = alphabet_generator()
6 >>> gen
7 <generator object alphabet_generator at 0x7fe1dffe39f0>
8 >>> for lettre in gen:
9 ... print(lettre)
10 ...
11 a
12 b
13 c
14 [...]
15 y
16 z
17 >>>
18 >>> list(gen)
19 []
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]
26.4.5 Pourquoi utiliser des itérateurs ?
À ce stade, vous vous posez peut-être la question « Pourquoi utiliser des itérateurs ? ». Nous
donnons quelques réponses dans cette rubrique.
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.
[Link] Itérateurs infinis
Bien que la taille de la mémoire d’un ordinateur soit finie, il est possible de créer des itérateurs infinis
! Par exemple, la fonction count()25 du module itertools itère de 0 (lorsqu’on l’appelle sans argument)
jusqu’à l’infini :
22. [Link]
23. [Link]
24. [Link]
25. [Link]
1 >>> iterateur = [Link]()
2 >>> import itertools
3 >>> iterateur = [Link]()
4 >>> for i in iterateur:
5 ... print(i)
6 ...
7 0
8 1
9 2
10 3
11 [Boucle infinie]
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]
python/#How_iterators_ can_improve_your_code
29. [Link]
30. [Link]
1 >>> import math
2 >>> ligne = "9 11 25 92 49 98 62 72 63 74"
3 >>> nombres = (int(nb) for nb in [Link]())
4 >>> inverses = (nb**-1 for nb in nombres)
5 >>> cos_inverses = ([Link](nb) for nb in inverses)
6 >>> sum(cos_inverse)
7 9.988141056338993
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.
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]
1 >>> def fct(**kwargs):
2 ... print(kwargs)
3 ...
4 >>> fct()
5 {}
6 >>> fct(z=1, gogo="toto")
7 {'gogo': 'toto', 'z': 1}
8 >>> fct(z=1, gogo="toto", y=-67)
9 {'y': -67, 'gogo': 'toto', 'z': 1}
10 >>> fct(1, 2)
11 Traceback (most recent call last):
12 File "<stdin>", line 1, in <module>
13 TypeError: fct() takes 0 positional arguments but 2 were given
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
:
1 >>> def fct(a, b, c):
2 ... print(a,b,c)
3 ...
4 >>> t = (-5,6,7)
5 >>>
6 >>> fct(*t)
7 -5 6 7
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
26.8. Décorateurs Chapitre 26. Remarques
complémentaires
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
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()
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 :
26.9. Un peu de transformée de Fourier avec NumPy Chapitre 26. Remarques
Pain
tomate / mozza
Pain
Pain
jambon / fromage
Pain
complémentaires
Un exemple plus concret de décorateur pourrait être la mesure du temps d’exécution d’une fonction :
def mesure_temps(fonction_a_decorer):
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…
CHAPITRE 27
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.gbk4 ;
1. [Link]
2. [Link]
3. [Link]
4. [Link]
• 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).
27.1.3 Simulation d’un pendule
On se propose de réaliser une simulation d’un pendule simple 6 en Tkinter. Un pendule simple est
représenté par une masse ponctuelle (la boule du pendule) reliée à un pivot immobile par une tige rigide
et sans masse. On néglige les effets de frottement et on considère le champ gravitationnel comme uniforme.
Le mouvement du pendule sera calculé en résolvant numériquement l’équation différentielle suivante
:
d2θ g
a
θ (t) =
dt2 (t) = − l ∗ sin(θ(t))
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 l
g
angulaire au temps t : aθ (t)—= × 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.
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).
Téléchargez le fichier [Link] 9. Ce fichier contient les 3000 mots anglais les plus
fréquents, à raison d’1 mot par ligne.
Créez un script words_in_proteome.py et écrivez la fonction read_words() qui va lire les mots
contenus dans le fichier dont le nom est fourni en argument du script et renvoyer une liste contenant les
mots convertis en majuscule et composés de 3 caractères ou plus.
Dans le programme principal, affichez le nombre de mots sélectionnés.
Téléchargez maintenant le fichier [Link] 10. Attention, ce fichier est assez gros. Ce
fichier provient de la banque de données UniProt à partir de cette page 11.
Voici les premières lignes de ce fichier ([...] indique une coupure que nous avons faite) :
>sp|O95139|NDUB6_HUMAN NADH dehydrogenase [ubiquinone] 1 beta [...]
MTGYTPDEKLRLQQLRELRRRWLKDQELSPREPVLPPQKMGPMEKFWNKFLENKSPWRKM
VHGVYKKSIFVFTHVLVPVWIIHYYMKYHVSEKPYGIVEKKSRIFPGDTILETGEVIPPM
KEFPDQHH
>sp|O75438|NDUB1_HUMAN NADH dehydrogenase [ubiquinone] 1 beta [...]
MVNLLQIVRDHWVHVLVPMGFVIGCYLDRKSDERLTAFRNKSMLFKRELQPSEEVTWK
>sp|Q8N4C6|NIN_HUMAN Ninein OS=Homo sapiens OX=9606 GN=NIN PE=1 SV=4
MDEVEQDQHEARLKELFDSFDTTGTGSLGQEELTDLCHMLSLEEVAPVLQQTLLQDNLLG
RVHFDQFKEALILILSRTLSNEEHFQEPDCSLEAQPKYVRGGKRYGRRSLPEFQESVEEF
PEVTVIEPLDEEARPSHIPAGDCSEHWKTQRSEEYEAEGQLRFWNPDDLNASQSGSSPPQ
9. [Link]
10. [Link]
11. [Link]
[Link] À la pêche aux mots
É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
[...]
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.
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
et les gènes antisens de cette façon :
gene complement(55979..56935)
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
//
13. [Link]
• 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.
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]
FIGURE 27.2 – Bilan des forces dans un pendule simple.
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θ
Le mouvement du pendule ne dépend pas de sa masse !
Idéalement, nous souhaiterions résoudre cette équation en l’exprimant en fonction de θ seulement.
Cela est possible en reliant θ à la longueur effective de l’arc s parcourue par le pendule :
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 :
v = ds = Ldθ
dt dt
On peut aussi exprimer l’accélération a en dérivant l’arc s deux fois par rapport à t :
FIGURE 27.3 – Caractérisation géométrique d’un pendule simple.
d2s d2θ
a
= =L 2
dt2 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 :
2
Ld θ = −
dt2 gsinθ
Dans la section suivante, nous allons voir comment résoudre numériquement cette équation différentielle.
θ(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’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
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")
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 :
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)
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 [...]
La figure 27.4 vous montre un aperçu de ce que l’on voudrait obtenir.
Pour le moment, vous pouvez oublier la réglette fixant la valeur initiale de θ, les labels affichant la
valeur de θ et vθ ainsi que les points violets « laissés en route » par le pendule. De même, nous
dessinerons le pivot, la boule et la tige plus tard. À ce stade, il est fondamental de tout de suite lancer
votre application pour vérifier que les widgets sont bien placés. N’oubliez pas, un code complexe se
teste au fur et à mesure lors de son développement.
Conseil
FIGURE 27.4 – Application pendule.
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
1 self.x = [Link]([Link]) * self.L
2 self.y = -[Link]([Link]) * self.L
la solution :
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().
19. [Link]
27.3. Scripts de correction Chapitre 27. Mini-projets
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.
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).
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 :
from Bio import SeqIO
with open("[Link]", "r") as fasta_file:
for record in [Link](fasta_file, "fasta"):
print([Link])
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]
A.2. GenBank Annexe A. Quelques formats de données en
biologie
[...]
FEATUR Location/Qualifiers
ES 1..800
source /organism="Homo sapiens"
/mol_type="mRNA"
/db_xref="taxon:9606"
/map="7q32-qter"
/tissue_type="pancre
as" 1..800
gene /gene="TRY1"
7..750
CDS /gene="TRY1"
/codon_start=1
/product="trypsinogen"
/protein_id="AAA61231.1"
/db_xref="GDB:G00-119-620"
/translation="MNPLLILTFVAAALAAPFDDDDK
IVGGYNCEENSVPYQVSLNSG
YHFCGGSLINEQWVVSAGHCYKSRIQVRLGE
HNIEVLEGNEQFINAAKIIRHPQYDRK
TLNNDIMLIKLSSRAVINARVSTISLPTAPPAT
GTKCLISGWGNTASSGADYPDELQC
sig_pepti
de LDAPVLSQAKCEASYPGKITSNMFCVGFLEG
GKDSCQGDSGGPVVCNGQLQGVVSWGD
GCAQKNKPGVYTKVYNYVKWIKNTIAANS"
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.
A.2.4 Manipulation avec Python
À 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]
HEADER HYDROLASE (SERINE PROTEINASE) 26-OCT-81 2PTN
TITLE
TITLE 2 CHEMICAL LABELLING AND LOW-TEMPERATURE CRYSTALLOGRAPHY
COMPND
SEQR
COMPND
1 A 223 ILE VAL GLY GLY TYR THR CYS GLY ALA ASN THR VAL PRO
ES
COMPND
SEQR 2 A 223 TYR GLN VAL SER LEU ASN SER GLY TYR HIS PHE CYS GLY
ES
[...]
SEQR
SOURCE 3 ORGANISM_SCIENTIFIC:
2 A 223 GLY SER BOS LEU ILE ASN SER GLN TRP VAL VAL SER ALA ALA
TAURUS;
ES
[...]
SEQR
EXPDTA 4 A 223 HIS CYS TYR LYS SER GLY ILE GLN VAL ARG LEU GLY GLU
ES
[...]
REMARK
HEL
[...] 1 H1 16 ILE 176 1SNGL ALPHA 13
IX
DBREF SER A 4 A TURN,REST IRREG.
HEL 2 H2 23 VAL 235 5CONTIGUOUS WITH H3 6
IX LYS A 0 A
HEL 3 H3 23 ASN 245 1CONTIGUOUS WITH H2 10
IX SER A 6 A
SHE 1 A7 A 20 THR A 0
ET TYR 21
SHE 2 A7 A 156 PRO A -1 N CYS A O TYR 20
ET LYS 161 157 A
SSBO 1 CYS 2 CYS A 1555 1555 2.04
ND A 2 157
SSBO 2 CYS 4 CYS 58 1555 1555 2.02
ND A 2 A
[...]
• Ligne 1. Cette ligne HEADER contient :
— le nom de la protéine : HYDROLASE (SERINE PROTEINASE),
— la date de dépôt de cette structure dans la banque de données : 26 octobre 1981
— et l’identifiant de la structure dans la PDB, on parle souvent de « code PDB » : 2PTN.
• Ligne 2. TITLE correspond au titre de l’article scientifique dans lequel a été publié cette structure.
• Lignes 4-6. COMPND indique que la trypsine est composée d’une seule chaîne peptidique, appelée ici A.
• Ligne 8. SOURCE indique le nom scientifique de l’organisme dont provient cette protéine (ici, le bœuf).
• Ligne 10. EXPDTA précise la technique expérimentale employée pour déterminer cette structure. Ici, la
cristallogra- phie aux rayons X. Mais on peut également trouver SOLUTION NMR pour de la
résonance magnétique nucléaire en solution, ELECTRON MICROSCOPY pour de la
microscopie électronique.
• Ligne 12. REMARK 2 précise, dans le cas d’une détermination par cristallographie aux rayons
X, la résolution obtenue, ici 1,55 Angströms.
• Ligne 14. DBREF indique les liens éventuels vers d’autres banques de données. Ici, l’identifiant
correspondant à cette protéine dans UniProt (UNP) est P00760 11.
• Ligne 15-18. SEQRES donnent à la séquence de la protéine. Les résidus sont représentés par
leur code à trois lettres.
• Lignes 20-22 et 23-24. HELIX et SHEET correspondent aux structures secondaires hélices α et
brin β de cette protéine. Ici, H1 SER A 164 ILE A 176 indique qu’il y a une première hélice α
(H1), comprise entre les résidus Ser164 et Ile176 de la chaîne A.
• Lignes 26-27. SSBOND indique les ponts disulfures. Ici, entre les résidus Cys22 et Cys157 et entre les
résidus Cys42 et Cys58.
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]
[...]
ATO 601 N LEU 99 10.007 19.687 17.536 1.00 12.25 N
M A
ATO 602 C LEU 99 9.599 18.429 18.188 1.00 12.25 C
M A A
ATO 603 C LEU 99 10.565 17.281 17.914 1.00 12.25 C
M A
ATO 604 O LEU 99 10.256 16.101 18.215 1.00 12.25 O
M A
ATO 605 C LEU 99 8.149 18.040 17.853 1.00 12.25 C
M B A
ATO 606 C LEU 99 7.125 19.029 18.438 1.00 18.18 C
M G A
ATO 607 C LEU 99 5.695 18.554 18.168 1.00 18.18 C
M D1 A
ATO 608 C LEU 99 7.323 19.236 19.952 1.00 18.18 C
M D2 A
[...]
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 :
[...]
ATO 1 N ILE 16 -8.155 9.648 20.365 1.00 10.68 N
M A
ATO 2 CA ILE 16 -8.150 8.766 19.179 1.00 10.68 C
M A
ATO 3 C ILE A 16 -9.405 9.018 18.348 1.00 10.68 C
M
ATO 4 O ILE 16 -10.533 8.888 18.870 1.00 10.68 O
M A
ATO 5 CB ILE 16 -8.091 7.261 19.602 1.00 10.68 C
M A
ATO 6 CG1 16 -6.898 6.882 20.508 1.00 7.42 C
M ILE A
ATO 7 CG2 16 -8.178 6.281 18.408 1.00 7.42 C
M ILE A
ATO 8 CD1 16 -5.555 6.893 19.773 1.00 7.42 C
M ILE A
ATO 9N VAL 17 -9.224 9.305 17.090 1.00 9.63 N
M A
ATO 10CA VAL 17 -10.351 9.448 16.157 1.00 9.63 C
M A
ATO 11C VAL 17 -10.500 8.184 15.315 1.00 9.63 C
M A
ATO 12O VAL 17 -9.496 7.688 14.748 1.00 9.63 O
M A
ATO 13CB VAL 17 -10.123 10.665 15.222 1.00 9.63 C
M A
ATO 14 CG1 17 -11.319 10.915 14.278 1.00 11.95 C
M VAL A
AT 15 CG2 17 -9.737 11.970 15.970 1.00 11.95 C
OM VAL A
[...]
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.
[...]
ATO 762 HB1 44 37.162 -2.955 2.220 1.00 0.00 H
ALA A
M
ATO 763 HB2 44 38.306 -2.353 3.417 1.00 0.00 H
M ALA A
ATO 764 HB3 44 38.243 -1.621 1.814 1.00 0.00 H
M ALA A
TER 765 ALA A 44
ATO 766 N GLY 95 -18.564 3.009 13.772 1.00 0.00 N
M B
ATO 767 CA 95 -19.166 3.646 12.621 1.00 0.00 C
M GLY B
ATO 768 C GLY 95 -20.207 2.755 11.976 1.00 0.00 C
M B
[...]
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 :
TER 765 ALA A 44
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]
[...]
MO 1
DE
L
ATO 1 N MA 1 -27.283 -9.772 5.388 1.00 0.00 N
M ET
ATO 2 C MA 1 -28.233 -8.680 5.682 1.00 0.00 C
M A ET
[...]
ATO 149 H GL B 13 36.113 -5.242 2.536 1.00 0.00 H
M 9 G2 U 9
ATO 150 H GL B 13 37.475 -4.132 2.428 1.00 0.00 H
M 0 G3 U 9
TER 150 GL B 13
1 U 9
END 2
MDL
MO
DEL
ATO 1 N MA 1 -29.736 -10.759 4.394 1.00 0.00 N
M ET
ATO 2 C MA 1 -28.372 -10.225 4.603 1.00 0.00 C
M A ET
[...]
ATO 149 H GL B 13 36.113 -5.242 2.536 1.00 0.00 H
M 9 G2 U 9
ATO 150 H GL B 13 37.475 -4.132 2.428 1.00 0.00 H
M 0 G3 U 9
TER 150 GL B 13
1 U 9
END
MDL
MO 2
DEL
ATO 1 N MA 1 -29.736 -10.759 4.394 1.00 0.00 N
M ET
ATO 2 C MA 1 -28.372 -10.225 4.603 1.00 0.00 C
M A ET
[...]
et :
ENDMDL
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
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 :
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) :
res_start = 16
model = structure[0]
chain = model["A"]
for i in range(10):
idx = res_start + i
print(chain[idx].resname, idx, chain[idx]["CA"].coord)
IL 16 [- 8.7659997 19.17900085]
E 8.14999962 9
VA 17 [- 9.4479999 16.15699959]
L 10.35099983 5
GL 18 [- 6.6300001 14.25899982]
Y 12.02099991 1
GL 19 [- 3.8989999 16.68400002]
Y 10.90200043 3
TY 20 [- 1.4420000 19.01600075]
R 12.65100002 3
TH 21 [- 0.9380000 22.76000023]
R 13.01799965 2
CY 22 [- -1.16299999 23.76000023]
S 10.02000046
GL 23 [- -2.86500001 26.7140007 ]
Y 11.68299961
AL 24 [- -2.62700009 30.36100006]
A 10.64799976
AS 25 [- -3.43700004 31.02000046]
N 6.96999979
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 :
1 with open("[Link]", "r") as pdb_file:
2 res_count = 0
3 for line in pdb_file:
4 if [Link]("ATOM"):
5 atom_name = line[12:16].strip()
6 res_name = line[17:20].strip()
7 res_num = int(line[22:26])
8 if atom_name == "CA":
9 res_count += 1
10 x = float(line[30:38])
11 y = float(line[38:46])
12 z = float(line[46:54])
13 print(res_name, res_num, x, y, z)
14 if res_count >= 10:
15 break
ce qui donne :
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
<?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>
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]
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 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 :
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
Voici un exemple d’écriture de fichier CSV :
1 import csv
2
3 with open("[Link]", "w") as f_out:
4 fields = ["Name", "Quantity"]
5 f_writer = [Link](f_out, fieldnames=fields)
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})
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})
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.
FIGURE A.2 – Format PDB et les différents champs de coordonnées.
ANNEXE B
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]
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.
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
[/home/pierre/miniconda3] >>>
4. [Link]
5. [Link]
6. [Link]
Do you wish the installer to initialize Miniconda3
by running conda init? [yes|no]
[no] >>> yes
L’installation de Miniconda est terminée. L’espace utilisé par Miniconda sur votre disque dur est d’environ
450 Mo.
Quittez Python en tapant la commande exit() puis appuyant sur la touche Entrée.
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) !
Étape 2. Dans le fichier de configuration du shell Bash, supprimer les lignes comprises entre
# >>> conda initialize >>>
et
# <<< conda initialize <<<
$ bash Miniconda3-latest-MacOSX-x86_64.sh
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]
FIGURE B.2 – Installation Miniconda étape 2.
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 :
FIGURE B.4 – Installation Miniconda étape 4.
388 ==> For changes to take effect, close and re- Paris Cité UFR Sciences du Vivant
no change C:\Users\Pat\Miniconda3\Scripts\[Link]
FIGURE B.6 – Installation Miniconda étape 6.
Cela signifie que vous êtes bien dans l’interpréteur Python. À partir de là vous pouvez taper exit() puis appuyer sur
FIGURE B.11 – Lancement de l’interpréteur Python dans un
--no-plugins Disable all plugins that are not built into conda.
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) !
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 :
FIGURE B.14 – Désinstallation de Miniconda (étape 3).
Courasargon2-cffi-bindings-21.2.0|
rdgeonP2y-tchfofni-/21U.n3i.v0ersité Paris|Cité
py312h5eee18b_0 du Vivant15 KB
393
asttokens-2.0.5 pyhd3eb1b0_0
[...]
FIGURE B.16 – Désinstallation de Miniconda (étape 5).
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.
B.4.2 Installation et réglage de Notepad++ sous Windows
Sous Windows, nous vous recommandons l’excellent éditeur Notepad++ 9. Une fois cet éditeur installé, il
est important de le régler correctement. En suivant le menu Paramètres, Préférences, vous arriverez sur un
panneau vous permettant de configurer Notepad++.
En premier on va configurer l’appui sur la touche Tab afin qu’il corresponde à une indentation de 4
espaces, comme recommandé par la PEP 8 (chapitre 15 Bonnes pratiques en programmation Python).
Dans la liste sur la gauche, cliquez
8. [Link]
9. [Link]
B.4. Choisir un bon éditeur de texte Annexe B. Installation de Python
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 :
FIGURE B.21 – Configuration de Notepad++ : indentation avec des espaces.
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 ;
• La page vers de nombreuses ressources utiles ;
• Le manuel d’utilisation (avec toutes les instructions pour son installation au chapitre 2) ;
• Une page sur Stackoverflow 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]
B.5. Comment se mettre dans le bon répertoire dans le shell Annexe B. Installation de Python
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]
Il est possible d’ouvrir un PowerShell directement dans ce répertoire :
Première astuce
Il suffit de taper powershell dans la barre qui indique le chemin :
(étape 2). puis on appuie sur entrée et le PowerShell se lance en étant directement
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.
19. [Link]
20. [Link]
B.6. Python web et mobile Annexe B. Installation de Python
FIGURE B.26 – Lancement d’un powershell depuis un répertoire donné (étape 2bis).
FIGURE B.27 – Lancement d’un powershell depuis un répertoire donné (étape 3).