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.

## 10.7 Principe DRY

L'acronyme DRY<fnref n="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 :

```python
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 :

$$ \text{temp\_celsius} = (\text{temp\_fahrenheit} - 32) \times \frac{5}{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 :

```python
1 >>> def convert_fahrenheit_to_celsius(temperature):
2     ...     return (temperature - 32) * (5/9)
3 ...
4 >>> temp_in_fahrenheit = 60
5 >>> convert_fahrenheit_to_celsius(temp_in_fahrenheit)
6 15.555555555555557
7 >>> temp_in_fahrenheit = 80
8 >>> convert_fahrenheit_to_celsius(temp_in_fahrenheit)
9 26.666666666666668
10 >>> temp_in_fahrenheit = 100
11 >>> convert_fahrenheit_to_celsius(temp_in_fahrenheit)
12 37.777777777777778
```

Et s'il y a une erreur dans la formule, il suffira de ne la corriger qu'une seule fois, dans la fonction `convert_fahrenheit_to_celsius()`.

## 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*<fnref n="3" /> :

<footnote n="2">2. https://www.earthdatascience.org/courses/intro-to-earth-data-science/write-efficient-python-code/intro-to-clean-code/dry-modular-code/</footnote>

<footnote n="3">3. http://www.pythontutor.com/</footnote>
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)

Analysez ensuite le code suivant et tentez de prédire sa sortie :

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 $x^y$ en utilisant l'opérateur `**`. Pour rappel :

1 >>> 2**2

2 4

3 >>> 2**3

4 8

5 >>> 2**4

6 16

Dans le programme principal, calculez et affichez à l'écran $2^i$ avec $i$ variant de 0 à 20 inclus. On souhaite que le résultat soit présenté avec le formatage suivant :

2^ 0 = 1
2^ 1 = 2
2^ 2 = 4
[...]
2^20 = 1048576

### 10.8.3 Pyramide

Reprenez l'exercice du chapitre 5 *Boucles et comparaisons* qui dessine une pyramide.

Dans un script `pyra.py`, 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.5 Séquence complémentaire

Créez une fonction `seq_comp()` qui prend comme argument une liste de bases et qui renvoie la séquence complémentaire d'une séquence d'ADN sous forme de liste.

Dans le programme principal, à partir de la séquence d'ADN

```
seq = ["A", "T", "C", "G", "A", "T", "C", "G", "A", "T", "C"]
```

affichez `seq` et sa séquence complémentaire (en utilisant votre fonction `seq_comp()`).

Rappel : la séquence complémentaire s'obtient en remplaçant A par T, T par A, C par G et G par C.

### 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 $\sqrt{3}$ ?

On rappelle que la distance euclidienne `d` entre deux points A et B de coordonnées cartésiennes respectives $(x_A, y_A, z_A)$ et $(x_B, y_B, z_B)$ se calcule comme suit :

$$d = \sqrt{(x_B - x_A)^2 + (y_B - y_A)^2 + (z_B - z_A)^2}$$

### 10.8.7 Distribution et statistiques

Créez une fonction `gen_distrib()` qui prend comme argument trois entiers : `debut`, `fin` et `n`. La fonction renverra une liste de `n` floats aléatoires entre `debut` et `fin`. Pour générer un nombre aléatoire dans un intervalle donné, utilisez la fonction `uniform()` du module `random`, dont voici quelques exemples d'utilisation :

```
1 >>> import random
2 >>> random(uniform(1, 10)
3 8.199672607202174
4 >>> random(uniform(1, 10)
5 2.607528561528022
6 >>> random(uniform(1, 10)
7 9.000404025130946
```

Avec la fonction `random/uniform()`, les bornes passées en argument sont incluses, c'est-à-dire qu'ici, le nombre aléatoire renvoyé est dans l'intervalle $[1, 10]$.

Créez une autre fonction `calc_stat()` qui prend en argument une liste de floats et qui renvoie une liste de trois éléments contenant respectivement le minimum, le maximum et la moyenne de la liste.

Dans le programme principal, générez 20 listes aléatoires de 100 floats compris entre 0 et 100 et affichez le minimum (`min()`), le maximum (`max()`) et la moyenne pour chacune d'entre elles. La moyenne pourra être calculée avec les fonctions `sum()` et `len()`.

Pour chacune des 20 listes, affichez les statistiques (valeur minimale, valeur maximale et moyenne) avec deux chiffres après la virgule :

```
Liste 1 : min = 0.17 ; max = 99.72 ; moyenne = 57.38
Liste 2 : min = 1.25 ; max = 99.99 ; moyenne = 47.41
[...]
Liste 19 : min = 1.05 ; max = 99.36 ; moyenne = 49.43
Liste 20 : min = 1.33 ; max = 97.63 ; moyenne = 46.53
```
Les écarts sur les statistiques entre les différentes listes sont-ils importants ? Relancez votre script avec des listes de 1000 éléments, puis 10 000 éléments. Les écarts changent-ils quand le nombre d'éléments par liste augmente ?

10.8.8 Distance à l'origine (exercice +++)

En reprenant votre fonction de calcul de distance euclidienne en trois dimensions `calc_distance_3D()`, faites-en une version pour deux dimensions que vous appellerez `calc_distance_2D()`.

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).

<figure><img src="image_23.png" /><figcaption>FIGURE 10.2 – Illustration de la distance à l'origine.</figcaption></figure>

Votre programme générera un fichier `sin2ori.dat` qui contiendra deux colonnes : la première représente les `x`, la seconde la distance entre chaque point de la fonction `sin(x)` à l'origine.

Enfin, pour visualiser votre résultat, ajoutez le code suivant tout à la fin de votre script :
```python
1 # Création d'une image pour la visualisation du résultat.
2 import matplotlib.pyplot as plt
3
4 x = []
5 y = []
6 with open("sin2ori.dat", "r") as f_in:
7     for line in f_in:
8         coordinates = line.split()
9         x.append(floatibleline[0]))
10        y.append(floatibleline[1]))
11 fig, ax = plt.subplots(figsize=(6, 6))
12 ax.plot(x, y)
13 ax.set_xlabel("x")
14 ax.set_ylabel("Distance de sin(x) à l'origine")
15 fig.savefig("sin2ori.png")
```

Ouvrez l'image sin2ori.png.

Remarque

Le module `plotlib` sera expliqué en détail dans le chapitre 21 *Module plotlib*.
# Plus sur les chaînes de caractères

## 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<fnref n="1" />.

## 11.2 Chaînes de caractères et listes

Les chaînes de caractères peuvent être considérées comme des listes (de caractères) un peu particulières :

1 >>> animaux = "girafe tigre"
2 >>> animaux
3 'girafe tigre'
4 >>> len(animaux)
5 12
6 >>> animaux[3]
7 'a'

Nous pouvons donc utiliser certaines propriétés des listes comme les tranches :

1 >>> animaux = "girafe tigre"
2 >>> animaux[0:4]
3 'gira'
4 >>> animaux[9:]
5 'gre'
6 >>> animaux[:-2]
7 'girafe tig'
8 >>> animaux[1:-2:2]
9 'iaetg'

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 :

<footnote n="1">1. https://docs.python.org/fr/3/library/string.html</footnote>
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 concatenation (+) 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).

11.3 Caractères spéciaux

Il existe certains caractères spéciaux comme \n que nous avons déjà vu (pour le retour à la ligne). Le caractère \t produit une tabulation. Si vous voulez écrire des guillemets simples ou doubles et que ceux-ci ne soient pas confondus avec les guillemets de déclaration de la chaîne de caractères, vous pouvez utiliser \' ou ":

1 >>> print("Un backslash n\npuis un backslash t\t puis un guillemet\"")

2 Un backslash n

3 puis un backslash t puis un guillemet"

4 >>> print('J\'affiche un guillemet simple')

5 J'affiche un guillemet simple

Vous pouvez aussi utiliser astucieusement des guillemets doubles ou simples pour déclarer votre chaîne de caractères :

1 >>> print("Un brin d'ADN")

2 Un brin d'ADN

3 >>> print('Python est un "super" langage de programmation')

4 Python est un "super" langage de programmation

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'apparaît 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

11.4 Préfixe de chaîne de caractères

Nous avons vu au chapitre 3 *Affichage* la notion de *f-string*. Il s'agit d'un mécanisme pour formater du texte au sein d'une chaîne de caractères. Par exemple :

1 >>> var ="f-string"

2 >>> f"voici une belle {var}"

3 'voici une belle f-string'
Que signifie le f que l'on accole aux guillemets de la chaîne de caractères ? Celui-ci est appelé « préfixe de chaîne de caractères » ou `stringprefix`.

### 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 d'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 :

1 >>> s = "Voici un retour à la ligne\nEt là une autre ligne"
2 >>> s
3 'Voici un retour à la ligne\nEt là une autre ligne'
4 >>> print(s)
5 Voici un retour à la ligne
6 Et là une autre ligne
7 >>> s = r"Voici un retour à la ligne\nEt là une autre ligne"
8 >>> s
9 'Voici un retour à la ligne\\nEt là une autre ligne'
10 >>> print(s)
11 Voici un retour à la ligne\nEt là une autre ligne

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 :

1 >>> animal = "renard"
2 >>> animal2 = "poulain"
3 >>> s = f"Le {animal} est un animal gentil\nLe {animal2} aussi"
4 >>> s
5 'Le renard est un animal gentil\nLe poulain aussi'
6 >>> print(s)
7 Le renard est un animal gentil
8 Le poulain aussi
9 >>> s = "Le {animal} est un animal gentil\nLe {animal2} aussi"
10 >>> s
11 'Le {animal} est un animal gentil\nLe {animal2} aussi'
12 >>> print(s)
13 Le {animal} est un animal gentil
14 Le {animal2} aussi

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 documentation officielle<fnref n="2" />.

<footnote n="2">2. https://docs.python.org/fr/3/reference/lexical_analysis.html#grammar-token-stringprefix</footnote>
## 11.5 Méthodes associées aux chaînes de caractères

Voici quelques méthodes<fnref n="3" /> spécifiques aux objets de type `str` :

```python
1 >>> x = "girafe"
2 >>> x.upper()
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 :

```python
1 >>> x[0].upper() + x[1:]
2 'Girafe'
```

ou plus simplement utiliser la méthode adéquate :

```python
1 >>> x.capitalize()
2 'Girafe'
```

Il existe une méthode associée aux chaînes de caractères qui est particulièrement pratique, la méthode `.split()` :

```python
1 >>> animaux = "girafe tigre singe souris"
2 >>> animaux.split()
3 ['girafe', 'tigre', 'singe', 'souris']
4 >>> for animal in animaux.split():
5     ... print(animal)
6 ...
7 girafe
8 tigre
9 singe
10 souris
```

La méthode `.split()` découpe une chaîne de caractères en plusieurs éléments appelés *champs*, en utilisant comme séparateur n'importe quelle combinaison « d'espace(s) blanc(s) ».

### Définition

Un espace blanc<fnref n="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.

Il est possible de modifier le séparateur de champs, par exemple :

```python
1 >>> animaux = "girafe:tigre:singe::souris"
2 >>> animaux.split(":")
3 ['girafe', 'tigre', 'singe', '', 'souris']
```

Attention, dans cet exemple, le séparateur est un seul caractères « : » (et non pas une combinaison de un ou plusieurs :) conduisant ainsi à une chaîne vide entre singe et souris.

Il est également intéressant d'indiquer à `.split()` le nombre de fois qu'on souhaite découper la chaîne de caractères avec l'argument `maxsplit` :

```python
1 >>> animaux = "girafe tigre singe souris"
2 >>> animaux.split(maxsplit=1)
3 ['girafe', 'tigre singe souris']
4 >>> animaux.split(maxsplit=2)
5 ['girafe', 'tigre', 'singe souris']
```

<footnote n="3">3. https://docs.python.org/fr/3/library/string.html</footnote>

<footnote n="4">4. https://en.wikipedia.org/wiki/Whitespace_character</footnote>
La méthode `find()`, quant à elle, recherche une chaîne de caractères passée en argument :

```
1 >>> animal = "girafe"
2 >>> animal.find("i")
3 1
4 >>> animal.find("afe")
5 3
6 >>> animal.find("z")
7 -1
8 >>> animal.find("tig")
9 -1
```

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é :

```
1 >>> animaux = "girafe tigre"
2 >>> animaux.find("i")
3 1
```

On trouve aussi la méthode `replace()` qui substitue une chaîne de caractères par une autre :

```
1 >>> animaux = "girafe tigre"
2 >>> animaux.replace("tigre", "singe")
3 'girafe singe'
4 >>> animaux.replace("i", "o")
5 'gorafe togre'
```

La méthode `count()` compte le nombre d'occurrences d'une chaîne de caractères passée en argument :

```
1 >>> animaux = "girafe tigre"
2 >>> animaux.count("i")
3 2
4 >>> animaux.count("z")
5 0
6 >>> animaux.count("tigre")
7 1
```

La méthode `startwith()` vérifie si une chaîne de caractères commence par une autre chaîne de caractères :

```
1 >>> chaine = "Bonjour monsieur le capitaine !"
2 >>> chaine startwith("Bonjour")
3 True
4 >>> chaine startwith("Au revoir")
5 False
```

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 >>> chaine.strip()
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 >>> chaine.strip()
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

Une tâche courante en Python est de lire une chaîne de caractères (provenant par exemple d'un fichier), d'en extraire des valeurs pour ensuite les manipuler.

On considère par exemple la chaîne de caractères `chaîne1` :

```python
1 >>> chaîne1 = "3.4 17.2 atom"
```

On souhaite extraire les valeurs `3.4` et `17.2` pour ensuite les additionner.

D'abord, on découpe la chaîne de caractères avec la méthode `split()` :

```python
1 >>> liste1 = chaîne1.split()
2 >>> liste1
3 ['3.4', '17.2', 'atom']
4 >>> nb1, nb2, nom = liste1
5 >>> nb1
6 '3.4'
7 >>> nb2
8 '17.2'
```

On obtient alors une liste de chaînes de caractères `liste1`. Avec l'affectation multiple, on récupère les nombres souhaités dans `nb1` et `nb2`, mais ils sont toujours sous forme de chaîne de caractères. Il faut ensuite les convertir en `floats` pour pouvoir les additionner :

```python
1 >>> float(nb1) + floatnb2)
2 20.59999999999998
```

### 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.

## 11.7 Fonction map()

### 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 ou que l'on peut transformer en liste :

```python
1 >>> ligne = "67 946 -45"
2 >>> ligne.split()
3 ['67', '946', '-45']
4 >>> map(int, ligne.split())
5 <map object at 0x7fa34e573b20>
6 >>> for entier in map(int, ligne.split())
7 ...     print entier)
8 ...
9 67
10 946
11 -45
12 >>> list(map(int, ligne.split()))
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<fnref n="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 `data.dat` 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 :

```python
1 with open("data.dat", "r") as filin:
2     for line in filin:
3         x, y, z = map(float, line.split())
4         print(x + y + z)
```

Sans `map()`, il aurait fallu une ligne pour séparer les données `x`, `y`, `z = line.split()` 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 :

```python
1 >>> def calc cube(x):
2     ...     return x**3
3 ...
4 >>> list(map(calc cube, [1, 2, 3, 4]))
5 [1, 8, 27, 64]
```

## 11.8 Test d'appartenance

L'opérateur `in` teste si une chaîne de caractères fait partie d'une autre chaîne de caractères :

```python
1 >>> chaine = "Néfertiti"
2 >>> "toto" in chaine
3 False
4 >>> "titi" in chaine
5 True
6 >>> "ti" in chaine
7 True
```

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 :

```python
1 >>> not "toto" in chaine
2 True
3 >>> not "fer" in chaine
4 False
```

## 11.9 Conversion d'une liste de chaînes de caractères en une chaîne de caractè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()` :

<footnote n="5">5. https://fr.wikipedia.org/wiki/Fonction_de_rappel</footnote>
1 >>> seq = ["A", "T", "G", "A", "T"]

2 >>> seq

3 ['A', 'T', 'G', 'A', 'T']

4 >>> "-".join(seq)

5 'A-T-G-A-T'

6 >>> " ".join(seq)

7 'A T G A T'

8 >>> "".join(seq)

9 'ATGAT'

Les éléments de la liste initiale sont concatené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 :

1 >>> maliste = ["A", 5, "G"]

2 >>> " ".join(maliste)

3 Traceback (most recent call last):

4 File "<stdin>", line 1, in <module>

5 TypeError: sequence item 1: expected str instance, int found

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()` :

1 >>> animaux = "girafe tigre"

2 >>> dir(animaux)

3 ['_add ', '__class__ ', '__contains__ ', '__delattr__ ', '__dir__ ',

4 ...

5 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition',

6 'rsplit', 'rstrip', 'split', 'splitlines', 'startwith', 'strip',

7 'swapcase', 'title', 'translate', 'upper', 'zfill']

Pour l'instant, vous pouvez ignorer les méthodes qui commencent et qui se terminent par deux tirets bas (underscores) `__`. Nous n'avons pas mis l'ensemble de la sortie de cette commande `dir()` pour ne pas surcharger le texte, mais n'hésitez pas à la tester dans l'interpréteur.

Vous pouvez également accéder à l'aide et à la documentation d'une méthode particulière avec `help()`, par exemple pour la méthode `.split()` :

>>> help(animaux.split)
Help on built-in function split:

split(...)

S.split([sep [,maxsplit]]) -> list of strings

Return a list of the words in the string S, using sep as the delimiter string. If maxsplit is given, at most maxsplit splits are done. If sep is not specified or is None, any whitespace string is a separator.

(END)

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(animaux.split())`.

## 11.10 Method chaining

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 =message.title()

3 >>> message1

4 'Salut Patrick Salut Pierre'

5 >>> message2 =message1.replace("Salut","Bonjour")

6 >>> message2

7 'Bonjour Patrick Bonjour Pierre'

8 >>> message2.split()

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 :

1 >>> message ="salut patrick salut pierre"

2 >>> message.title() .replace("Salut","Bonjour").split()

3 ['Bonjour', 'Patrick', 'Bonjour', 'Pierre']

On évite ainsi de créer des variables intermédiaires.

Le method chaining peut créer des lignes de code très longues. On peut couper une ligne de code en plusieurs lignes en utilisant le caractère \ en fin de ligne :

1 >>> message ="salut patrick salut pierre"

2 >>> message.title() \

3 ... .replace("Salut", "Bonjour") \

4 ... .title()

5 'Bonjour Patrick Bonjour Pierre'

On peut aussi utiliser des parenthèses pour couper une ligne de code en plusieurs lignes :

1 >>> message ="salut patrick salut pierre"

2 >>> (message

3 ... .title()

4 ... .replace("Salut", "Bonjour")

5 ... .split()

6 ... )

7 ['Bonjour', 'Patrick', 'Bonjour', 'Pierre']

L'utilisation de parenthèses permet aussi de couper une chaîne de caractères en plusieurs lignes :

1 >>> ma_chaine =(

2 ... "voici une chaîne de caractères "

3 ... "très longue "

4 ... "sur plusieurs lignes")

5 >>> ma_chaine

6 'voici une chaîne de caractères très longue sur plusieurs lignes'

Nous reverrons le method chaining dans le chapitre 22 Module Pandas .

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.fasta`<fnref n="6" /> 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 : YYYYYYYYYYY
10 dernières bases : ZZZZZZZZZZZ
```

où WWW et XXX sont des entiers et YYYYYYYYYYY et ZZZZZZZZZZZ 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*.

### 11.11.3 Fréquence des bases dans une séquence d'ADN

Soit la séquence d'ADN `ATATACGGATCGGCTGTTGCCTGCGTAGTAGCGT`. On souhaite calculer la fréquence de chaque base A, T, C et G dans cette séquence et afficher le résultat à l'écran.

Créez pour cela une fonction `calc_composition()` à laquelle vous passez en argument votre séquence d'ADN sous forme d'une chaîne de caractères, et qui renvoie une liste de quatre `floats` indiquant respectivement la fréquence en bases A, T, G et C.

### 11.11.4 Distance de Hamming

La distance de Hamming<fnref n="7" /> mesure la différence entre deux séquences de même taille en comptant le nombre de positions qui, pour chaque séquence, ne correspondent pas au même acide aminé.

Créez la fonction `dist_hamming()` qui prend en argument deux chaînes de caractères et qui renvoie la distance de Hamming (sous la forme d'un entier) entre ces deux chaînes de caractères.

Calculez la distance de Hamming entre les séquences : `AGWPSGGASAGLAIL` et `IGWPSAGASAGLWIL` puis entre les séquences : `ATTCATACGTTACGATT` et `ATACTTACGTAACCATT`.

### 11.11.5 Moyenne de notes

Le fichier `notes.csv`<fnref n="8" /> contient des noms d'étudiant ainsi que leurs notes dans différentes matières. Chaque donnée est séparée par une virgule. On trouve dans l'ordre le nom de l'étudiant, la note en géographie, la note en sport, la note en anglais.

```
Jason,17,3,1
William,9,18,15
Susan,3,8,10
[...]
```

<footnote n="6">6. https://python.sdv.u-paris.fr/data-files/UBI4_SCerevisiae.fasta</footnote>

<footnote n="7">7. https://fr.wikipedia.org/wiki/Distance_de_Hamming</footnote>

<footnote n="8">8. https://python.sdv.u-paris.fr/data-files/notes.csv</footnote>
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 :

<table><thead><tr><th>Acide aminé</th><th>Code 3-lettres</th><th>Code 1-lettre</th><th>Acide aminé</th><th>Code 3-lettres</th><th>Code 1-lettre</th></tr></thead><tbody><tr><td>Alanine</td><td>Ala</td><td>A</td><td>Leucine</td><td>Leu</td><td>L</td></tr><tr><td>Arginine</td><td>Arg</td><td>R</td><td>Lysine</td><td>Lys</td><td>K</td></tr><tr><td>Asparagine</td><td>Asn</td><td>N</td><td>Méthionine</td><td>Met</td><td>M</td></tr><tr><td>Aspartate</td><td>Asp</td><td>D</td><td>Phénylalanine</td><td>Phe</td><td>F</td></tr><tr><td>Cystéine</td><td>Cys</td><td>C</td><td>Proline</td><td>Pro</td><td>P</td></tr><tr><td>Glutamate</td><td>Glu</td><td>E</td><td>Sérine</td><td>Ser</td><td>S</td></tr><tr><td>Glutamine</td><td>Gln</td><td>Q</td><td>Thréonine</td><td>Thr</td><td>T</td></tr><tr><td>Glycine</td><td>Gly</td><td>G</td><td>Tryptophane</td><td>Trp</td><td>W</td></tr><tr><td>Histidine</td><td>His</td><td>H</td><td>Tyrosine</td><td>Tyr</td><td>Y</td></tr><tr><td>Isoleucine</td><td>Ile</td><td>I</td><td>Valine</td><td>Val</td><td>V</td></tr></tbody></table>

### 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 ponctuations 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 :

<table><thead><tr><td>Mot</td><td>Séquence</td></tr></thead><tbody><tr><td>python</td><td>aophrtkny</td></tr><tr><td>python</td><td>aeiouyhpq</td></tr><tr><td>coucou</td><td>uocuoceokzezh</td></tr><tr><td>fonction</td><td>nhwfnitvkloco</td></tr></tbody></table>

### 11.11.9 Alphabet et pangramme

Les codes ASCII des lettres minuscules de l'alphabet vont de 97 (lettre « a ») à 122 (lettre « z »). La fonction `chr()` prend en argument un code ASCII sous la forme d'un entier et renvoie le caractère correspondant (sous la forme d'une chaîne de caractères). Ainsi `chr(97)` renvoie 'a', `chr(98)` renvoie 'b' et ainsi de suite.

Créez la fonction `get_alphabet()` qui utilise une boucle et la fonction `chr()` et qui renvoie une chaîne de caractères contenant toutes les lettres de l'alphabet.

Un pangramme<fnref n="9" /> est une phrase comportant au moins une fois chaque lettre de l'alphabet. Par exemple, « Portez ce vieux whisky au juge blond qui fume » est un pangramme.

Créez la fonction `est_pangramme()` qui utilise la fonction `get_alphabet()` précédente, qui prend en argument une chaîne de caractères xxx, et qui renvoie `True` si la phrase est un pangramme et `False` sinon.

Le programme affichera finalement `xxx est un pangramme ou xxx n'est pas un pangramme`. Pensez à vous débarrasser des majuscules le cas échéant.

Testez ensuite si les expressions suivantes sont des pangrammes :

* Portez ce vieux whisky au juge blond qui fume

* Monsieur Jack vous dactylographiez bien mieux que votre ami Wolf

* Buvez de ce whisky que le patron juge fameux

* Ceci n'est pas un pangramme

### 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.gbk<fnref n="10" />.

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 tgtagtgtt agtattaggg tgtggtgtgt gggtgtggtg tgggtgtggg tgtggtgtgtg
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.

<footnote n="9">9. http://fr.wikipedia.org/wiki/Pangramme</footnote>

<footnote n="10">10. https://python.sdv.u-paris.fr/data-files/NC_001133.gbk</footnote>
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 ZZZZZZZZZZZ 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 :

```
drapeau <- Faux
seq <- chaîne de caractères vide
Lire toutes les lignes du fichier:
  si la ligne contient //:
    drapeau <- Faux
  si drapeau est Vrai:
    on ajoute à seq la ligne (sans espace, chiffre et retour à la ligne)
  si la ligne contient ORIGIN:
    drapeau <- Vrai
```

### 11.11.11 Affichage des carbones alpha d'une structure de protéine

Téléchargez le fichier 1bta.pdb<fnref n="11" /> qui correspond à la structure tridimensionnelle de la protéine barstar<fnref n="12" /> 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, qui stocke ces lignes dans une liste et les renvoie sous la forme d'une liste de chaînes de caractères.

Utilisez la fonction trouve_calpha() pour afficher à l'écran les 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*.

### 11.11.12 Calcul des distances entre les carbones alpha consécutifs d'une structure de proté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 (x_A, y_A, z_A) et (x_B, y_B, z_B) se calcule comme suit :

$$d = \sqrt{(x_B - x_A)^2 + (y_B - y_A)^2 + (z_B - z_A)^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 :

<footnote n="11">11. https://files.rcsb.org/download/1BTA.pdb</footnote>

<footnote n="12">12. http://www.rcsb.org/pdb/explore.do?structureId=1BTA</footnote>
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*.

## 11.11.13 Compteur de gènes dans un fichier GenBank

Dans cet exercice, on souhaite compter le nombre de gènes du fichier GenBank NC_001133.gbk<fnref n="13" /> (chromosome I de la levure *Saccharomyces cerevisiae*) et afficher la longueur de chaque gène. Pour cela, il faudra récupérer les lignes décrivant la position des gènes. Voici par exemple les cinq premières lignes concernées dans le fichier NC_001133.gbk :

```
gene complement(<1807..>2169)
gene <2480..>2707
gene complement(<7235..>9016)
gene complement(<11565..>11951)
gene <12046..>12426
```

[...]

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 :

```
gène 1 complémentaire -> 362 bases
gène 2 direct -> 227 bases
gène 3 complémentaire -> 1781 bases
[...]
gène 99 direct -> 611 bases
gène 100 direct -> 485 bases
gène 101 direct -> 1403 bases
```

### Conseil

Vous trouverez des explications sur le format GenBank dans l'annexe A *Quelques formats de données en biologie*.

<footnote n="13">13. https://python.sdv.u-paris.fr/data-files/NC_001133.gbk</footnote>
# CHAPITRE 12

## Plus sur les listes

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 Méthodes associées aux listes

Comme pour les chaînes de caractères, les listes possèdent de nombreuses méthodes qui leurs sont propres. On rappelle qu'une méthode est une fonction qui agit sur l'objet auquel elle est attachée par un point.

#### 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 :

1 >>> list1 = [1, 2, 3]

2 >>> list1.append(5)

3 >>> list1

4 [1, 2, 3, 5]

qui est équivalent à :

1 >>> list1 = [1, 2, 3]

2 >>> list1 = list1 + [5]

3 >>> list1

4 [1, 2, 3, 5]

**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é :
```python
1 >>> listel = [1, 2, 3]
2 >>> listel.insert(2, -15)
3 >>> listel
4 [1, 2, -15, 3]
```

### 12.1.3 del

L'instruction `del` supprime un élément d'une liste à un indice déterminé :

```python
1 >>> listel = [1, 2, 3]
2 >>> del listel[1]
3 >>> listel
4 [1, 3]
```

#### 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 :

```python
1 >>> listel = [1, 2, 3]
2 >>> listel.remove(3)
3 >>> listel
4 [1, 2]
```

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 :

```python
1 >>> listel = [1, 2, 3, 4, 3]
2 >>> listel.remove(3)
3 >>> listel
4 [1, 2, 4, 3]
5 >>> listel.remove(3)
6 >>> listel
7 [1, 2, 4]
```

### 12.1.5 .sort()

La méthode `.sort()` trie les éléments d'une liste du plus petit au plus grand :

```python
1 >>> listel = [3, 1, 2]
2 >>> listel.sort()
3 >>> listel
4 [1, 2, 3]
```

L'argument `reverse=True` spécifie le tri inverse, c'est-à-dire du plus grand au plus petit élément :

```python
1 >>> listel = [3, 1, 2]
2 >>> listel.sort(reverse=True)
3 >>> listel
4 [3, 2, 1]
```

### 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 :

```python
1 >>> listel = [3, 1, 2]
2 >>> sorted(listel)
3 [1, 2, 3]
4 >>> listel
5 [3, 1, 2]
```
La fonction `sorted()` supporte aussi l'argument `reverse=True` :

```python
1 >>> liste1 = [3, 1, 2]
2 >>> sorted(liste1, reverse=True)
3 [3, 2, 1]
4 >>> liste1
5 [3, 1, 2]
```

### 12.1.7 .reverse()

La méthode `.reverse()` inverse une liste :

```python
1 >>> liste1 = [3, 1, 2]
2 >>> liste1.reverse()
3 >>> liste1
4 [2, 1, 3]
```

### 12.1.8 .count()

La méthode `.count()` compte le nombre d'éléments (passés en argument) dans une liste :

```python
1 >>> liste1 = [1, 2, 4, 3, 1, 1]
2 >>> liste1.count(1)
3 3
4 >>> liste1.count(4)
5 1
6 >>> liste1.count(23)
7 0
```

### 12.1.9 Particularités des méthodes associées aux listes

De nombreuses méthodes mentionnées précédemment (`.append()`, `.sort()`, etc.) modifient la liste, mais ne renvoient pas d'objet récupérable dans une variable. Il s'agit d'un exemple d'utilisation de méthode (donc de fonction particulière) qui fait une action, mais qui ne renvoie rien. Pensez-y dans vos utilisations futures des listes. Ainsi, même si l'instruction `var = liste1.reverse()` est une instruction Python valide, `var` ne contiendra que `None` c'est-à-dire un objet vide en Python, préférez-lui directement l'instruction `liste1.reverse()` :

```python
1 >>> liste1 = [1, 2, 3]
2 >>> var = liste1.reverse()
3 >>> var
4 >>> print(var)
5 None
6 >>> liste1
7 [3, 2, 1]
8 >>> liste2 = [5, 6, 7]
9 >>> liste2.reverse()
10 >>> liste2
11 [7, 6, 5]
```

**Remarque**

Pour exprimer la même idée, la documentation parle de modification de la liste « sur place » (in place en anglais) :

```python
1 >>> liste1 = [1, 2, 3]
2 >>> help(liste1.reverse)
3 Help on built-in function reverse:
4
5 reverse() method of builtins.list instance
6 Reverse *IN PLACE*.
```

Cela signifie que la liste est modifiée « sur place », c'est-à-dire **dans la méthode** au moment où elle s'exécute. La liste étant modifiée « en dur » dans la méthode, cette dernière ne renvoie donc rien. L'explication du mécanisme sous-jacent vous sera donnée dans la rubrique 13.4 *Portée des listes du chapitre 13 Plus sur les fonctions*.

Par ailleurs, certaines méthodes ou instructions des listes décalent les indices d'une liste (par exemple `.insert()`,
del, etc.).

Enfin, pour obtenir une liste exhaustive des méthodes disponibles pour les listes, utilisez la fonction `dir(liste1)` (`liste1` étant une liste).

## 12.2 Construction d'une liste par itération

La méthode `.append()` est très pratique car on peut l'utiliser pour construire une liste au fur et à mesure des itérations d'une boucle.

Pour cela, il est commode de définir préalablement une liste vide avec l'instruction `liste1 = []`. Voici un exemple où une chaîne de caractères est convertie en liste :

```python
1 >>> seq = "CAAAGGTAACGC"
2 >>> seq_list = []
3 >>> seq_list
4 []
5 >>> for base in seq:
6 ...     seq_list.append(base)
7 ...
8 >>> seq_list
9 ['C', 'A', 'A', 'A', 'G', 'G', 'T', 'A', 'A', 'C', 'G', 'C']
```

Remarquez que dans cet exemple, vous pouvez aussi utiliser directement la fonction `list()` qui prend n'importe quel objet séquentiel (liste, chaîne de caractères, etc.) et qui renvoie une liste :

```python
1 >>> seq = "CAAAGGTAACGC"
2 >>> list(seq)
3 ['C', 'A', 'A', 'A', 'G', 'G', 'T', 'A', 'A', 'C', 'G', 'C']
```

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`.

## 12.3 Test d'appartenance

L'opérateur `in` teste si un élément fait partie d'une liste :

```python
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.

## 12.4 Fonction zip()

**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)

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 avec `enumerate()` dans les chapitres 5 *Boucles* et 8 *Dictionnaires et tuples*).

Un objet `zip` est aussi utile pour générer facilement une liste de tuples.

1 >>> list(zip(animaux, couleurs))

2 [('poulain', 'alezan'), ('renard', 'roux'), ('python', 'vert')]

Si une des listes passée en argument n'a pas la même longueur, l'objet `zip` s'arrête sur la liste la plus courte :

1 >>> animaux = ["poulain", "renard", "python", "orque"]

2 >>> couleurs = ["alezan", "roux", "vert"]

3 >>> list(zip(animaux, couleurs))

4 [('poulain', 'alezan'), ('renard', 'roux'), ('python', 'vert')]

On peut empêcher ce comportement avec l'argument par mot-clé `strict`, qui renvoie une erreur si les listes n'ont pas la même longueur :

1 >>> list(zip(animaux, couleurs, strict=True))

2 Traceback (most recent call last):

3 File "<stdin>", line 1, in <module>

4 ValueError: zip() argument 2 is shorter than argument 1

Enfin, il est possible de créer des objets `zip` avec autant de listes que l'on veut :

1 >>> animaux = ["poulain", "renard", "python"]

2 >>> couleurs = ["alezan", "roux", "vert"]

3 >>> numero = [1, 2, 3]

4 >>> list(zip(numero, animaux, couleurs))

5 [(1, 'poulain', 'alezan'), (2, 'renard', 'roux'), (3, 'python', 'vert')]

### Remarque

La fonction `zip()` fonctionne sur n'importe quel objet itérable : listes, tuples, dictionnaires, objets `range`, etc.

## 12.5 Copie de listes

Il est très important de savoir que l'affectation d'une liste (à partir d'une liste préexistante) crée en réalité une référence et non une copie :
1 >>> liste1 = [1, 2, 3]

2 >>> liste2 = liste1

3 >>> liste2

4 [1, 2, 3]

5 >>> liste1[1] =-15

6 >>> liste1

7 [1, -15, 3]

8 >>> liste2

9 [1, -15, 3]

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) :

<figure><img src="image_24.png" /><figcaption>FIGURE 12.1 – Copie de liste.</figcaption></figure>

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 :

1 >>> liste1 = [1, 2, 3]

2 >>> liste2 = liste1[:]

3 >>> liste1[1] =-15

4 >>> liste2

5 [1, 2, 3]

L'instruction `liste1` a créé une copie « à la volée » de la liste `liste1`. Vous pouvez utiliser aussi la fonction `list()`, qui renvoie explicitement une liste :

1 >>> liste1 = [1, 2, 3]

2 >>> liste2 =list(liste1)

3 >>> liste1[1] =-15

4 >>> liste2

5 [1, 2, 3]

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.

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 :

1 >>> liste1 =[[1, 2], [3, 4]]

2 >>> liste1

3 [[1, 2], [3, 4]]

4 >>> liste2 =liste1[:]

5 >>> liste1[1][1] =55

6 >>> liste1

7 [[1, 2], [3, 55]]

8 >>> liste2

9 [[1, 2], [3, 55]]
<figure><img src="image_25.png" /><figcaption>FIGURE 12.2 – Copie de liste avec une tranche [:] et la fonction list().</figcaption></figure>

et

1 >>> liste2 = list(liste1)
2 >>> liste1[1][1] = 77
3 >>> liste1
4 [[1, 2], [3, 77]]
5 >>> liste2
6 [[1, 2], [3, 77]]

La méthode de copie qui fonctionne à tous les coups consiste à appeler la fonction deepcopy() du module copy :

1 >>> import copy
2 >>> liste1 = [[1, 2], [3, 4]]
3 >>> liste1
4 [[1, 2], [3, 4]]
5 >>> liste2 = copy.deepcopy(liste1)
6 >>> liste1[1][1] = 99
7 >>> liste1
8 [[1, 2], [3, 99]]
9 >>> liste2
10 [[1, 2], [3, 4]]

12.6 Initialisation d'une liste de listes

Un dernier écueil que vous pourrez rencontrer concerne l'initialisation d'une liste de listes avec l'opérateur *. Imaginons
que l'on souhaite représenter un tableau de nombre et l'initialiser avec des 0. Nous pourrions être tentés d'utiliser la
duplication de listes :

1 >>> liste1 = [[0, 0, 0]] * 5
2 >>> liste1
3 [[0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]

Le problème est que si on modifie un élément d'une des sous-listes :

1 >>> liste1[2][0] = -12
2 >>> liste1
3 [[-12, 0, 0], [-12, 0, 0], [-12, 0, 0], [-12, 0, 0]]

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<fnref n="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 :

<footnote n="1">1. https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range</footnote>
<figure><img src="image_26.png" /><figcaption>FIGURE 12.3 – Initialisation d'une liste de listes avec l'opérateur de duplication.</figcaption></figure>

```python
1 >>> liste1 = []
2 >>> for i in range(5):
3     ...     liste1.append([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<fnref n="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.

## 12.7 Liste de compréhension

### 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) :

```python
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]
```

Pour plus de détails, consultez à ce sujet le site de Python <fnref n="3" /> et celui de Wikipédia <fnref n="4" />.

Voici quelques exemples illustrant la puissance des listes de compréhension.

<footnote n="2">2. https://fr.wikipedia.org/wiki/Matrice</footnote>

<footnote n="3">3. http://www.python.org/dev/peps/pep-0202/</footnote>

<footnote n="4">4. http://fr.wikipedia.org/wiki/Comprehension_de_liste</footnote>
**Conseil**

Pour plus de lisiblité, il est possible de répartir la liste de compréhension sur plusieurs lignes.

### 12.7.7 Portée des variables dans une liste de compréhension

Contrairement à une boucle *for*, la variable d'itération d'une liste de compréhension n'est pas accessible en dehors de la liste de compréhension elle-même. Par exemple :

```python
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.

## 12.8 Exercices

**Conseil**

Pour ces exercices, créez des scripts puis exécutez-les dans un shell.

### 12.8.1 Tri de liste

Soit la liste de nombres `[8, 3, 12.5, 45, 25.5, 52, 1]`. Triez les nombres de cette liste par ordre croissant, sans utiliser la fonction `sort()`. Les fonctions et méthodes `min()`, `.append()` et `.remove()` vous seront utiles.

### 12.8.2 Séquence d'ADN aléatoire

Créez une fonction `seq_alea()` qui prend comme argument un entier positif `taille` représentant le nombre de bases de la séquence et qui renvoie une séquence d'ADN aléatoire sous forme d'une chaîne de caractères. Utilisez la fonction `random.choice()` présentée dans le chapitre 9 *Modules*.

Utilisez la fonction `seq_alea()` pour générer aléatoirement une séquence d'ADN de 15 bases.

### 12.8.3 Séquence d'ADN complémentaire inverse

Créez une fonction `gen_comp_inv()` qui prend comme argument une séquence d'ADN sous la forme d'une chaîne de caractères, qui renvoie la séquence complémentaire inverse sous la forme d'une autre chaîne de caractères et qui utilise des méthodes associées aux listes. Dans cette fonction, utilisez un dictionnaire `"A": "T", "T": "A", "G": "C", "C": "G"}` donnant la correspondance entre nucléotides des brins direct et complémentaire.

Utilisez cette fonction pour transformer la séquence d'ADN `TCTGTTAACCATCCACTTCG` en sa séquence complémentaire inverse.

Rappel : la séquence complémentaire inverse doit être « inversée ». Par exemple, la séquence complémentaire inverse de la séquence ATCG est CGAT.
### 12.8.4 Doublons

Soit la liste de nombres `list1 = [5, 1, 1, 2, 5, 6, 3, 4, 4, 4, 2]`. À partir de `list1`, créez une nouvelle liste sans les doublons, triez-la et affichez-la.

### 12.8.5 Séquence d'ADN aléatoire 2

Créez une fonction `seq_alea_2()` qui prend comme argument un entier et quatre `floats`, représentant respectivement la longueur de la séquence et les pourcentages de chacune des quatre bases A, T, G et C. La fonction générera aléatoirement une séquence d'ADN qui prend en compte les contraintes fournies en arguments et renverra la séquence sous forme d'une chaîne de caractères.

Utilisez cette fonction pour générer aléatoirement une séquence d'ADN de 50 bases contenant 10 % de A, 30 % de T, 50 % de G et 10 % de C.

**Conseil**

Utilisez la fonction `random.choises()` 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 `random.choises()` de la manière suivante :

```python
1 >>> import random
```

```python
2 >>> random.choises("AB", k=10, weights=[80, 20])
```

```python
3 ['A', 'A', 'A', 'A', 'A', 'A', 'A', 'B', 'A', 'B']
```

N'hésitez pas à consulter la documentation 6 de la fonction `random.choises()` pour plus de détails.

### 12.8.6 Le nombre mystère

Trouvez le nombre mystère qui répond aux conditions suivantes :

* Il est composé de trois chiffres.

* Il est strictement inférieur à 300.

* Il est pair.

* Deux de ses chiffres sont identiques.

* La somme de ses chiffres est égale à 7.

On vous propose d'employer une méthode dite « brute force », c'est-à-dire d'utiliser une boucle et à chaque itération de tester les différentes conditions.

### 12.8.7 Codes une et trois lettres des acides aminés

On donne les deux listes suivantes décrivant quelques acides aminés en code une et trois lettres :

```python
1 code_1_lettre = ["A", "V", "L", "M", "P"]
```

```python
2 code_3_lettres = ["Ala", "Val", "Leu", "Met", "Pro"]
```

Avec la fonction `zip()` et une boucle, générez la sortie suivante :

```
L'acide aminé se note A ou Ala
```

```
L'acide aminé se note V ou Val
```

```
L'acide aminé se note L ou Leu
```

```
L'acide aminé se note M ou Met
```

```
L'acide aminé se note P ou Pro
```

### 12.8.8 Triangle de Pascal (exercice +++)

Voici le début du triangle de Pascal :

<footnote n="6">6. https://docs.python.org/fr/3/library/random.html#random.choises</footnote>
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
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 pascal.out les 10 premières lignes du triangle de Pascal.
# CHAPITRE 13

## Plus sur les fonctions

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*<fnref n="1" /> afin de suivre l'état des variables lors de l'exécution des exemples.

### 13.1 Appel d'une fonction dans une fonction

Dans le chapitre 10, nous avons vu des fonctions qui étaient appelées depuis le programme principal. Il est en fait possible d'appeler une fonction depuis une autre fonction. Et plus généralement, on peut appeler une fonction de n'importe où à partir du moment où elle est visible par Python (c'est-à-dire chargée dans la mémoire). Observez cet exemple :

```python
1 # Définition des fonctions.
2 def est_pair(x):
3     if x % 2 == 0:
4         return True
5     else:
6         return False
7
8 def calc_somme_nb pairs(debut, fin):
9     somme = 0
10    for nombre in range(debut, fin+1):
11        if est_pair(name):
12            somme += nombre
13
14 # Programme principal.
15 somme = calc_somme_nb pairs(1, 5)
16 print(f"La somme des nombres pairs de 1 à 5 est {somme}")
```

Nous appelons la fonction `calc_somme_nb pairs()` depuis le programme principal, puis à l'intérieur de celle-ci nous

<footnote n="1">1. http://www.pythontutor.com/</footnote>
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**.

<figure><img src="image_27.png" /><figcaption>FIGURE 13.1 – Appel d'une fonction dans une fonction.</figcaption></figure>

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 :

```python
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
Si vous êtes débutant, vous pouvez sauter cette rubrique.

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<fnref n="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 :

$$ 
\begin{aligned}
3! &= 3 \times 2 \times 1 = 6 \\
4! &= 4 \times 3 \times 2 \times 1 = 30 \\
\dots \\
n! &= n \times n - 1 \times \dots \times 2 \times 1
\end{aligned}
$$

Voici le code Python avec une fonction récursive :

```python
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><img src="image_28.png" /><figcaption>FIGURE 13.2 – Fonction récursive : factorielle.</figcaption></figure>

Ligne 8, on appelle la fonction `calc_factorielle()` en passant comme argument l'entier 4. Dans la fonction, la variable locale qui récupère cet argument est `nb`. Au sein de la fonction, celle-ci se rappelle elle-même (ligne 5), mais cette fois-ci en passant la valeur 3. Au prochain appel, ce sera avec la valeur 2, puis finalement 1. Dans ce dernier cas, le test

<footnote n="2">2. https://fr.wikipedia.org/wiki/Tri_rapide</footnote>
if nb == 1: est vrai et l'instruction return 1 sera exécutée. À ce moment précis de l'exécution, les appels successifs forment une sorte de pile (voir la figure 13.2). La valeur 1 sera ainsi renvoyée au niveau de l'appel précédent, puis le résultat 2 × 1 = 2 (où 2 correspond à nb et 1 provient de calc_factorielle nb - 1), soit 1) va être renvoyé à l'appel précédent, puis 3 × 2 = 6 (où 3 correspond à nb et 2 provient de calc_factorielle nb - 1), soit 2) va être renvoyé à l'appel précédent, pour finir par 4 × 6 = 24 (où 4 correspond à nb et 6 provient de calc_factorielle nb - 1), soit 6), soit la valeur de 4!. Les appels successifs vont donc se « dépiler » et nous reviendrons dans le programme principal.

Même si les fonctions récursives peuvent être ardues à comprendre, notre propos est ici de vous illustrer qu'une fonction qui en appelle une autre (ici il s'agit d'elle-même) reste « figée » dans le même état, jusqu'à ce que la fonction appelée lui renvoie une valeur.

13.3 Portée des variables

Il est très important lorsque l'on manipule des fonctions de connaître la portée des variables (scope en anglais), c'est-à-dire savoir là où elles sont visibles. On a vu que les variables créées au sein d'une fonction ne sont pas visibles à l'extérieur de celle-ci car elles étaient locales à la fonction. Observez le code suivant :

1 >>> def ma_fonction():
2 ...     x = 2
3 ...     print(f"x vaut {x} dans la fonction")
4 ...
5 >>> ma_fonction()
6 x vaut 2 dans la fonction
7 >>> print(x)
8 Traceback (most recent call last):
9     File "<stdin>", line 1, in <module>
10 NameError: name 'x' is not defined

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 :

1 >>> def ma_fonction(x):
2 ...     print(f"x vaut {x} dans la fonction")
3 ...
4 >>> ma_fonction(2)
5 x vaut 2 dans la fonction
6 >>> print(x)
7 Traceback (most recent call last):
8     File "<stdin>", line 1, in <module>
9 NameError: name 'x' is not defined

Lorsqu'une variable est déclarée dans le programme principal, elle est visible dans celui-ci ainsi que dans toutes les fonctions. On a vu qu'on parlait de variable globale :

1 >>> def ma_fonction():
2 ...     print(x)
3 ...
4 >>> x = 3
5 >>> ma_fonction()
6 3
7 >>> print(x)
8 3

Dans ce cas, la variable x est visible dans le module principal et dans toutes les fonctions du module. Toutefois, Python ne permet pas la modification d'une variable globale dans une fonction :

1 >>> def ma_fonction():
2 ...     x = x + 1
3 ...
4 >>> x = 1
5 >>> ma_fonction()
6 Traceback (most recent call last):
7     File "<stdin>", line 1, in <module>
8     File "<stdin>", line 2, in ma_fonction
9 UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
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 :

```python
1 >>> def ma_fonction():
2     ...     global x
3     ...     x = x + 1
4     ...
5 >>> x = 1
6 >>> ma_fonction()
7 >>> x
8 2
```

Dans ce dernier cas, le mot-clé global a forcé la variable x à être globale plutôt que locale au sein de la fonction.

## 13.4 Portée des listes

### 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 :

```python
1 >>> def ma_fonction():
2     ...     listel[1] = -127
3     ...
4 >>> listel = [1,2,3]
5 >>> ma_fonction()
6 >>> listel
7 [1, -127, 3]
```

De même, si vous passez une liste en argument, elle est modifiable au sein de la fonction :

```python
1 >>> def ma_fonction(liste_tmp):
2     ...     liste_tmp[1] = -15
3     ...
4 >>> listel = [1,2,3]
5 >>> ma_fonction(listel)
6 >>> listel
7 [1, -15, 3]
```

Pour bien comprendre l'origine de ce comportement, utilisons à nouveau le site Python Tutor<fnref n="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 listel 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 listel, on modifie aussi listel. 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 :

<footnote n="3">3. http://www.pythontutor.com/</footnote>
<figure><img src="image_29.png" /><figcaption>FIGURE 13.3 – Passage d'une liste à une fonction.</figcaption></figure>

```python
1 >>> def ma_fonction(liste_tmp):
2     ...     liste_tmp[1] = -15
3     ...
4 >>> liste1 = [1, 2, 3]
5 >>> ma_fonction(liste1[:])
6 >>> liste1
7 [1, 2, 3]
8 >>> ma_fonction(liste1(y))
9 >>> liste1
10 [1, 2, 3]
```

Dans ces deux derniers exemples, une copie de y est créée à la volée lorsqu'on appelle la fonction, ainsi la liste y du module principal reste intacte.

D'autres suggestions sur l'envoi de liste dans une fonction vous sont données dans la rubrique *Recommandations ci-dessous*.

## 13.5 Règle LGI

Lorsque Python rencontre une variable, il va traiter la résolution de son nom avec des priorités particulières. D'abord il va regarder si la variable est **locale**, puis si elle n'existe pas localement, il vérifiera si elle est **globale** et enfin si elle n'est pas globale, il testera si elle est **interne** (par exemple la fonction `len()` est considérée comme une fonction interne à Python, elle existe à chaque fois que vous lancez Python). On appelle cela la règle **LGI** pour locale, globale, interne. En voici un exemple :

```python
1 >>> def ma_fonction():
2     ...     x = 4
3     ...     print(f"Dans la fonction x vaut {x}")
4     ...
5 >>> x = -15
6 >>> ma_fonction()
7 Dans la fonction x vaut 4
8 >>> print(f"Dans le module principal x vaut {x}")
9 Dans le module principal x vaut -15
```

Dans la fonction, `x` a pris la valeur qui lui était définie localement en priorité sur la valeur définie dans le module principal.

**Conseil**

Même si Python accepte qu'une variable ait le même nom que ses propres fonctions ou variables internes, évitez
d'utiliser de tels noms, car ceci rendra votre code confus !

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).

13.6.2 Modification d'une liste dans une fonction

Concernant les fonctions qui modifient une liste, nous vous conseillons de l'indiquer clairement dans votre code. Pour
cela, faites en sorte que la fonction renvoie la liste modifiée et de récupérer cette liste renvoyée dans une variable portant
le même nom. Par exemple :

1 def ajoute_un(liste):
2     for indice in range(len(liste)):
3         liste[index] += 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[index] += 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

<footnote n="4">4. http://wiki.c2.com/?GlobalVariablesAreBad</footnote>
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 :

```python
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.

### 13.7.1 Prédire la sortie

Prédisez le comportement des codes suivants, sans les recopier dans un script ni dans l'interpréteur Python :

#### 13.7.1.1 Code 1

```python
1 def hello(prenom):
2     print(f" Bonjour {prenom} ")
3
4
5 # Programme principal.
6 hello("Patrick")
7 print(x)
```

#### 13.7.1.2 Code 2

```python
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.1.3 Code 3

```python
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)
```

### 13.7.1.4 Code 4

```python
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)
```

## 13.7.2 Passage de liste à une fonction

Créez une fonction ajoute_nb_alea() qui prend en argument une liste et qui ajoute un nombre entier aléatoire entre -10 et 10 (inclus) à chaque élément. La fonction affichera à l'écran cette nouvelle liste modifiée.

Dans le programme principal, effectuez les actions suivantes :

1. Créez une variable ma_liste = [7, 3, 8, 4, 5, 1, 9, 10, 2, 6].

2. Affichez ma_liste à l'écran.

3. Appelez la fonction ajoute_nb_alea() en lui passant ma_liste en argument.

4. Affichez à nouveau ma_liste à l'écran.

Comment expliquez-vous le résultat obtenu ?
# CHAPITRE 14

## 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 dictionnaires 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

* Capacité à supporter la fonction `len()` renvoyant la longueur du conteneur.

Voici d'autres propriétés générales que nous avons déjà croisées. Un conteneur peut être :

* **Ordonné** (*ordered* en anglais) : il y a un ordre précis des éléments ; cet ordre correspond à celui utilisé lors de la création ou de la modification du conteneur (si cela est permis) ; ce même ordre est utilisé lorsqu'on itère dessus.
* **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<fnref n="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 :

```python
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 :

```python
1 >>> liste1 = [1, 2, 3]
2 >>> id(liste1)
3 140318850324832
4 >>> liste1[1] = -15
5 >>> id(liste1)
6 140318850324832
7 >>> liste1.append(5)
8 >>> id(liste1)
9 140318850324832
```

<footnote n="1">1. https://fr.wikipedia.org/wiki/Objet_immuable</footnote>
La liste `listel` a été modifiée ligne 4 (changement de l'élément d'indice 1) et ligne 7 (ajout d'un élément). Pour autant, l'identifiant de cette liste est resté identique tout du long. Ceci démontre la mutabilité des listes : quelle que soit la manière dont on modifie une liste, celle-ci garde le même identifiant.

* Une dernière propriété importante est la capacité d'un conteneur (ou tout autre objet Python) à être **hachable**.

### 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.

### Pour aller plus loin

Pour aller plus loin, vous pouvez consulter la page Wikipedia sur les fonctions de hachage<fnref n="2" />.

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 :

```python
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.

### 14.1.2 Conteneurs de type *range*

Revenons rapidement sur les objets de type *range*. Jusqu'à maintenant, on s'en est servi pour faire des boucles ou générer des listes de nombres. Toutefois, on a vu ci-dessus qu'ils étaient aussi des conteneurs. Ils sont ordonnés, indexables, itérables, hachables et non modifiables :

<footnote n="2">2. https://fr.wikipedia.org/wiki/Fonction_de_hachage</footnote>
```python
1 >>> range1 = range(3)
2 >>> range1[0]
3 0
4 >>> range1[0:1]
5 range(0, 1)
6 >>> for element in range1:
7     ...     print element
8 ...
9 0
10 1
11 2
12 >>> range1[2] = 10
13 Traceback (most recent call last):
14     File "<stdin>", line 1, in <module>
15TypeError: 'range' object does not support item assignment
16 >>> hash(range1)
17 5050907061201647097
```

La tentative de modification d'un élément ligne 12 conduit à la même erreur que lorsqu'on essaie de modifier un caractère d'une chaîne de caractères. Comme pour la plupart des objets Python non modifiables, les objets de type `range` sont hachables.

## 14.2 Plus sur les dictionnaires

Nous revenons sur les dictionnaires qui, on l'a vu, sont des conteneurs de correspondance où chaque valeur est associée à une clé plutôt qu'un indice. Nous allons voir certaines propriétés avancées des dictionnaires, notamment comment trier par clé ou par valeur.

### 14.2.1 Objets utilisables comme clé

Toutes les clés de dictionnaire vues dans le chapitre 8 *Dictionnaires et tuples* et utilisées jusqu'à présent étaient des chaînes de caractères. Toutefois, on peut utiliser d'autres types d'objets comme des entiers, des *floats*, voire des *tuples*, cela peut s'avérer parfois très utile. Une règle est toutefois requise : les objets utilisés comme clé doivent être **hachables** (voir la rubrique précédente pour la définition).

Pourquoi les clés doivent être des objets hachables ? C'est la raison d'être des dictionnaires qui d'ailleurs sont aussi appelés table de hachage<fnref n="3" /> dans d'autres langages, comme Perl. Convertir chaque clé en sa valeur de hachage permet un accès très rapide à chacun des éléments du dictionnaire, ainsi que des comparaisons de clés entre dictionnaires extrêmement efficaces. Même si on a vu que deux objets pouvaient avoir la même valeur de hachage, par exemple $a = 5$ et $b = 5$, on ne peut mettre qu'une seule fois la clé 5. Ceci assure que deux clés d'un même dictionnaire ont forcément une valeur de hachage différente.

Pouvoir utiliser autre chose qu'une chaîne de caractères comme clé peut se révéler très pratique. Par exemple, pour une protéine ou un peptide, on pourrait concevoir d'utiliser comme clé le numéro de résidu, et comme valeur le nom de résidu. Imaginons par ailleurs que nous commencions à compter le premier acide aminé à 3 (souvent les fichiers PDB ne commence pas à 1 pour le premier acide aminé). Par exemple :

```python
1 >>> sequence = {3: 'S', 4: 'E', 5: 'Q', 6: 'P', 7: 'E', 8: 'P', 9: 'T'}
2 >>> sequence[5]
3 'Q'
4 >>> sequence[9]
5 'T'
6 >>> for num, res in sequence.items():
7     ...     print(num, res)
8 ...
9 3 S
10 4 E
11 5 Q
12 6 P
13 7 E
14 8 P
15 9 T
```

<footnote n="3">3. https://fr.wikipedia.org/wiki/Table_de_hachage</footnote>
Vous voyez l'énorme avantage, d'utiliser comme clé le numéro de résidu. Avec une liste ou une chaîne de caractère, l'indécage commence à zéro. Ainsi, il faudrait utiliser les indices 2 et 6 pour retrouver respectivement les acides aminés 5 et 9 :

1 >>> sequence =['S','E','Q','P','E','P','T']
2 >>> sequence[2]
3 'Q'
4 >>> sequence[6]
5 'T'

14.2.2 Destruction d'une paire clé/valeur

Comme pour tous les objets Python, l'instruction del permet de détruire un couple clé/valeur :

1 >>> dico ={ 'nom':'girafe','taille':5.0,'poids':1100}
2 >>> del dico[ "nom"]
3 >>> dico
4 {'taille':5.0,'poids':1100}

Pour les listes, on utilise l'indice entre crochet pour détruire l'élément, par exemple del liste[2] . Ici, on utilise la
clé.

14.2.3 Tri par clés

On peut utiliser la fonction sorted() vue précédemment avec les listes pour trier un dictionnaire par ses clés :

1 >>> ani2 ={ 'nom':'singe','taille':1.75,'poids':70}
2 >>> sorted( ani2)
3 ['nom', 'poids', 'taille']

Les clés sont triées ici par ordre alphabétique.

14.2.4 Tri par valeurs

Pour trier un dictionnaire par ses valeurs, il faut utiliser la fonction sorted() avec l'argument key :

1 >>> dico ={ "a":15,"b":5,"c":20}
2 >>> sorted(dico, key=dico.get)
3 ['b', 'a', 'c']

L'argument key=dico.get 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=dico.get, mais pas key
=dico.get(). 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 :

1 >>> dico ={ "a":15,"b":5,"c":20}
2 >>> for key in sorted(dico, key=dico.get):
3 ...     print(key, dico[key])
4 ...
5 b 5
6 a 15
7 c 20

Enfin, l'argument reverse=True fonctionne également :

1 >>> dico ={ "a":15,"b":5,"c":20}
2 >>> sorted(dico, key=dico.get, reverse=True)
3 ['c', 'a', 'b']

Remarque

Lorsqu'on trie un dictionnaire par ses valeurs, il faut être sûr que cela soit possible. Ce n'est pas le cas lorsqu'on a un
mélange de valeurs numériques et chaînes de caractères :
1 >>> ani2 ={ 'nom':'singe','poids':70 ,'taille':1.75}

2 >>> sorted(ani2, key=ani2.get)

3 Traceback (most recent call last):

4 File "<stdin>", line 1, in <module>

5 TypeError: '<' not supported between instances of 'int' and 'str'

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).

14.2.5 Clé associée au minimum ou au maximum des valeurs

Les fonctions min() et max(), que vous avez déjà manipulées dans les chapitres précédents, acceptent également l'argument key= . On peut ainsi obtenir la clé associée au minimum ou au maximum des valeurs d'un dictionnaire :

1 >>> dico ={ "a":15, "b":5, "c":20}

2 >>> max(dico, key=dico.get)

3 'c'

4 >>> min(dico, key=dico.get)

5 'b'

14.2.6 Fonction dict()

La fonction dict() va convertir l'argument qui lui est passé en dictionnaire. Il s'agit donc d'une fonction de casting, comme int() , str() , etc. Toutefois, l'argument qui lui est passé doit avoir une forme particulière : un objet séquentiel contenant d'autres objets séquentiels de deux éléments. Par exemple, une liste de listes de deux éléments :

1 >>> liste_animaux =[[ "girafe",2 ],[ "singe",3 ]]

2 >>> dict(liste_animaux)

3 {'girafe': 2, 'singe': 3}

Ou un tuple de tuples de deux éléments, ou encore une combinaison liste et tuple :

1 >>> tuple_animaux =(( "girafe",2 ),( "singe",3 ))

2 >>> dict Mediaux)

3 {'girafe': 2, 'singe': 3}

4 >>>

5 >>> dict([ ("girafe", 2), ("singe", 3)])

6 {'girafe': 2, 'singe': 3}

Si un des sous-éléments a plus de deux éléments (ou moins), Python renvoie une erreur :

1 >>> dict([ ("girafe", 2), ("singe", 3, 4)])

2 Traceback (most recent call last):

3 File "<stdin>", line 1, in <module>

4 ValueError: dictionary update sequence element #1 has length 3; 2 is required

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 :

1 >>> dict(un=1, deux=2, trois=3)

2 {'un': 1, 'deux': 2, 'trois': 3}

Nous vous déconseillons toutefois cette manière de faire, car on ne peut pas mettre d'arguments par mot-clé variables, on doit les écrire explicitement.

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.

1 >>> dict(zip(animaux, couleurs))

2 {'poulain': 'alezan', 'renard': 'roux', 'python': 'vert'}

Attention à ne passer que deux listes à la fonction zip(), sinon Python renvoie une erreur :

1 >>> dict(zip([1, 2, 3], animaux, couleurs))

2 Traceback (most recent call last):

3 File "<stdin>", line 1, in <module>

4 ValueError: dictionary update sequence element #0 has length 3; 2 is required

## 14.3 Plus sur les tuples

Nous revenons sur les tuples, que nous avons défini dans le chapitre 8 *Dictionnaires et tuples* et que nous avons croisé à de nombreuses reprises, notamment avec les fonctions. Les tuples sont des objets séquentiels correspondant aux listes, donc ils sont itérables, ordonnés et indexables, mais ils sont toutefois non modifiables. On verra plus bas qu'ils sont hachables sous certaines conditions. L'intérêt des tuples par rapport aux listes réside dans leur immutabilité. Cela accélère considérablement la manière dont Python accède à chaque élément et ils prennent moins de place en mémoire. Par ailleurs, on ne risque pas de modifier un de ses éléments par mégarde.

### 14.3.1 Immutabilité

Nous avions vu que les tuples étaient immuables :

1 >>> tuple1 = (1, 2, 3)

2 >>> tuple1[2] = 15

3 Traceback (most recent call last):

4 File "<stdin>", line 1, in <module>

5 TypeError: 'tuple' object does not support item assignment

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 :`

1 >>> tuple1 = (1, 2, 3)

2 >>> tuple1

3 (1, 2, 3)

4 >>> id Sec( tuple1)

5 139971081704464

6 >>> tuple1 = tuple1 + (2,)

7 >>> tuple1

8 (1, 2, 3, 2)

9 >>> id Sec( tuple1)

10 139971081700368

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 :

```python
1 >>> def ma_fonction():
2     ...     return 3, 14
3 ...
4 >>> x, y = ma_fonction()
5 >>> print(x, y)
6 3 14
```

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 les variables en utilisant l'indécage :

```python
1 >>> resultat = ma_fonction()
2 >>> resultat
3 (3, 14)
4 >>> x = resultat[0]
5 >>> y = resultat[1]
6 >>> print(x, y)
7 3 14
```

**Conseil**

Lorsqu'une fonction renvoie plusieurs valeurs sous forme de tuple, privilégiez toujours la forme `x, y = ma_fonction()`.

### 14.3.3 Affectation multiple et nom de variable _

Quand une fonction renvoie plusieurs valeurs, mais que l'on ne souhaite pas les utiliser toutes dans la suite du code, on peut utiliser le nom de variable `_` (caractère underscore) pour indiquer que certaines valeurs ne nous intéressent pas :

```python
1 >>> def ma_fonction():
2     ...     return 1, 2, 3, 4
3 ...
4 >>> x,_,y,_ = ma_fonction()
5 >>> x
6 1
7 >>> y
8 3
```

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__`. On verra au chapitre 23 *Avoir la classe avec les objets* que ces underscores ont aussi une signification particulière.

### 14.3.4 Tuples contenant des listes

On a vu que les tuples étaient **non modifiables**. Que se passe-t-il alors si on crée un tuple contenant des objets modifiables comme des listes ? Examinons le code suivant :

1 >>> liste1 = [1, 2, 3]

2 >>> tuple1 = (liste1, "Plouf")

3 >>> tuple1

4 ([1, 2, 3], 'Plouf')

5 >>> liste1[0] = -15

6 >>> tuple1[0].append(-632)

7 >>> tuple1

8 ([-15, 2, 3, -632], 'Plouf')

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><img src="image_30.png" /><figcaption>FIGURE 14.1 – Tuple contenant une liste.</figcaption></figure>

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 :

```python
1 >>> liste1 = [1, 2, 3]
2 >>> tuple1 = (liste1, "Plouf")
3 >>> tuple1
4 ([1, 2, 3], 'Plouf')
5 >>> id(liste1)
6 139971081980816
7 >>> id(3[0])
8 139971081980816
```

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]` :

```python
1 >>> liste1[2] = -15
2 >>> tuple1[0].append(-632)
3 >>> tuple1
4 ([1, 2, -15, -632], 'Plouf')
5 >>> id(liste1)
6 139971081980816
7 >>> id Responsable(3[0])
8 139971081980816
```

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 :

```python
1 >>> tuple1[0] = "Plif"
2 Traceback (most recent call last):
3     File "<stdin>", line 1, in <module>
4 TypeError: 'tuple' object does not support item assignment
```

Cette erreur s'explique par le fait que le nouvel objet `"Plif"` n'a pas le même identifiant que la sous-liste initiale. En fait, l'immutabilité selon Python signifie qu'un objet créé doit toujours garder le même identifiant. Cela est valable pour tout objet non modifiable, comme un élément d'un tuple, un caractère dans une chaîne de caractères, etc.

### 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.

### 14.3.5 Fonction tuple()

Nous avions vu également la fonction `tuple()`, qui permet de convertir un objet séquentiel en tuple (opération de casting). Cela est possible seulement si l'objet passé en argument est itérable :

```python
1 >>> tuple([1, 3])
2 (1, 3)
3 >>> tuple("a")
4 ('a',)
5 >>> tuple(2)
6 Traceback (most recent call last):
7     File "<stdin>", line 1, in <module>
8 TypeError: 'int' object is not iterable
9 >>> tuple(True)
10 Traceback (most recent call last):
11     File "<stdin>", line 1, in <module>
12 TypeError: 'bool' object is not iterable
```

Bien sûr, un entier ou un booléen ne sont pas itérables.
### 14.3.6 Hachabilité des tuples

Les tuples sont hachables s'ils ne contiennent que des éléments hachables. Si un tuple contient un ou plusieurs objet(s) non hachable(s), comme une liste, il devient non hachable :

```python
1 >>> tuple1 = tuple(range(10))
2 >>> tuple1
3 (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
4 >>> hasharts tuple1)
5 -4181190870548101704
6 >>> tuple2 = ("Plouf", 2, (1, 3))
7 >>> tuple2
8 ('Plouf', 2, (1, 3))
9 >>> hasharts tuple2)
10 286288423668065022
11 >>> tuple3 = (1, (3, 4), "Plaf", [3, 4, 5])
12 >>> tuple3
13 (1, (3, 4), 'Plaf', [3, 4, 5])
14 >>> hasharts tuple3)
15 Traceback (most recent call last):
16     File "<stdin>", line 1, in <module>
17 TypeError: unhashable type: 'list'
```

Les tuples `tuple1` et `tuple2` sont hachables car ils ne contiennent que des éléments hachables. Par contre, `tuple3` ne l'est pas, car un de ses éléments est une liste.

**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 !

## 14.4 Sets et frozensets

### 14.4.1 Définition et propriétés

Les objets de type `set` représentent un autre type de conteneur qui peut se révéler très pratique. Ils ont la particularité d'être modifiables, non hachables, non ordonnés, non indexables et de ne contenir qu'une seule copie maximum de chaque élément. Pour créer un nouveau `set` on peut utiliser les accolades :

```python
1 >>> set1 = {4, 5, 5, 12}
2 >>> set1
3 {12, 4, 5}
4 >>> type(set1)
5 <class 'set'>
```

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 :

```python
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'
```
À 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) :

```python
1 >>> set([1, 2, 4, 1])
2 {1, 2, 4}
3 >>> set((2, 2, 2, 1))
4 {1, 2}
5 >>> set(range(5))
6 {0, 1, 2, 3, 4}
7 >>> set({"clé_1": 1, "clé_2": 2})
8 {'clé_1', 'clé_2'}
9 >>> set(["ti", "to", "to"])
10 {'ti', 'to'}
11 >>> set("Maître Corbeau et Maître Renard")
12 {'e', 'd', 'M', 'r', 'n', 't', 'a', 'C', 'î', ' ', 'o', 'u', 'R', 'b'}
```

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.

```python
1 >>> set1 = set([1, 2, 4, 1])
2 >>> set1[1]
3 Traceback (most recent call last):
4     File "<stdin>", line 1, in <module>
5 TypeError: 'set' object is not subscriptable
6 >>> set1[1] = 5
7 Traceback (most recent call last):
8     File "<stdin>", line 1, in <module>
9 TypeError: 'set' object does not support item assignment
```

Par contre, les sets sont itérables :

```python
1 >>> for element in set1:
2     ... print element)
3 ...
4 1
5 2
6 4
```

Les sets ne peuvent être modifiés que par des méthodes spécifiques :

```python
1 >>> set1 = set(range(5))
2 >>> set1
3 {0, 1, 2, 3, 4}
4 >>> set1.add(4)
5 >>> set1
6 {0, 1, 2, 3, 4}
7 >>> set1.add(472)
8 >>> set1
9 {0, 1, 2, 3, 4, 472}
10 >>> set1.discard(0)
11 >>> set1
12 {1, 2, 3, 4, 472}
```

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 :

1 >>> import random
2 >>> liste1 = [random.randint(0, 9) for i in range(10)]
3 >>> liste1
4 [7, 9, 6, 6, 7, 3, 8, 5, 6, 7]
5 >>> set(liste1)
6 {3, 5, 6, 7, 8, 9}

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 :

1 >>> list(set([7, 9, 6, 6, 7, 3, 8, 5, 6, 7]))
2 [3, 5, 6, 7, 8, 9]

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 !

1 >>> seq = "atctcgatcgatcgcgtagctagctgccatacgtacgactacgt"
2 >>> set(seq)
3 {'c', 'g', 't', 'a'}
4 >>> [(base, seq.count(base)) for base in set(seq)]
5 [('c', 15), ('g', 10), ('t', 11), ('a', 10)]

Les sets permettent aussi l'évaluation d'union ou d'intersection mathématiques en conjonction avec les opérateurs,
respectivement | et & :

1 >>> liste1 = [3, 3, 5, 1, 3, 4, 1, 1, 4, 4]
2 >>> liste2 = [3, 0, 5, 3, 3, 1, 1, 1, 2, 2]
3 >>> set(liste1) | set(liste2)
4 {0, 1, 2, 3, 4, 5}
5 >>> set(liste1) & set(liste2)
6 {1, 3, 5}

Notez qu'il existe les méthodes .union() et .intersection permettant de réaliser ces opérations d'union et d'intersection :

1 >>> set1 = {1, 3, 4, 5}
2 >>> set2 = {0, 1, 2, 3, 5}
3 >>> set1.union(set2)
4 {0, 1, 2, 3, 4, 5}
5 >>> set1.intersection(set2)
6 {1, 3, 5}

L'instruction set1.difference(set2) renvoie sous la forme d'un nouveau set les éléments de set1 qui ne sont pas dans set2. Et inversement pour set2.difference(set1) :

1 >>> set1.difference(set2)
2 {4}
3 >>> set2.difference(set1)
4 {0, 2}

Enfin, deux autres méthodes sont très utiles :
```python
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 >>> set2.issubset(set1)
11 True
12 >>> set3.isdisjoint(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 documentation officielle de Python <fnref n="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) :

```python
1 >>> frozen1 = frozenset([3, 3, 5, 1, 3, 4, 1, 1, 4, 4])
2 >>> frozen2 = frozenset([3, 0, 5, 3, 3, 1, 1, 1, 2, 2])
3 >>> frozen1
4 frozenset({1, 3, 4, 5})
5 >>> frozen2
6 frozenset({0, 1, 2, 3, 5})
7 >>> frozen1.add(5)
8 Traceback (most recent call last):
9     File "<stdin>", line 1, in <module>
10AttributeError: 'frozenset' object has no attribute 'add'
11 >>> frozen1.union(frozen2)
12 frozenset({0, 1, 2, 3, 4, 5})
13 >>> frozen1.intersection(frozen2)
14 frozenset({1, 3, 5})
```

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<fnref n="5" /> et towardsdatascience<fnref n="6" />.

## 14.5 Récapitulation des propriétés des conteneurs

Après ce tour d'horizon des différents conteneurs, voici des tableaux qui résument leurs propriétés. La mention « in et len() » indique que l'on peut tester l'appartenance d'un élément à un conteneur avec l'opérateur in, et que l'on peut connaître le nombre d'éléments du conteneur avec la fonction len(). Les mentions « index. » et « modif. » indiquent respectivement « indexable » et « modifiable ».

### 14.5.1 Objets séquentiels

<footnote n="4">4. https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset</footnote>

<footnote n="5">5. https://www.programiz.com/python-programming/set</footnote>

<footnote n="6">6. https://towardsdatascience.com/python-sets-and-set-theory-2ace093d1607</footnote>
<table><thead><tr><th>Conteneur</th><th>in et len()</th><th>itérable</th><th>ordonné</th><th>index.</th><th>modif.</th><th>hachable</th></tr></thead><tbody><tr><td>liste</td><td>oui</td><td>oui</td><td>oui</td><td>oui</td><td>oui</td><td>non</td></tr><tr><td>chaîne de caractères</td><td>oui</td><td>oui</td><td>oui</td><td>oui</td><td>non</td><td>oui</td></tr><tr><td>range</td><td>oui</td><td>oui</td><td>oui</td><td>oui</td><td>non</td><td>oui</td></tr><tr><td>tuple</td><td>oui</td><td>oui</td><td>oui</td><td>oui</td><td>non</td><td>oui*</td></tr></tbody></table>

### 14.5.2 Objets de mapping

<table><thead><tr><th>Conteneur</th><th>in et len()</th><th>itérable</th><th>ordonné</th><th>index.</th><th>modif.</th><th>hachable</th></tr></thead><tbody><tr><td>dictionnaire</td><td>oui</td><td>oui sur les clés</td><td>oui*</td><td>non</td><td>oui</td><td>non</td></tr></tbody></table>

### 14.5.3 Objets sets

<table><thead><tr><th>Conteneur</th><th>in et len()</th><th>itérable</th><th>ordonné</th><th>index.</th><th>modif.</th><th>hachable</th></tr></thead><tbody><tr><td>sets</td><td>oui</td><td>oui</td><td>non</td><td>non</td><td>oui</td><td>non</td></tr><tr><td>frozensets</td><td>oui</td><td>oui</td><td>non</td><td>non</td><td>non</td><td>oui</td></tr></tbody></table>

### 14.5.4 Types de base

Il est aussi intéressant de comparer ces propriétés avec celles des types numériques de base qui ne sont pas des conteneurs.

<table><thead><tr><th>Objet numérique</th><th>in et len()</th><th>itérable</th><th>ordonné</th><th>index.</th><th>modif.</th><th>hachable</th></tr></thead><tbody><tr><td>entier</td><td>non</td><td>non</td><td>non</td><td>non</td><td>non</td><td>oui</td></tr><tr><td>float</td><td>non</td><td>non</td><td>non</td><td>non</td><td>non</td><td>oui</td></tr><tr><td>booléen</td><td>non</td><td>non</td><td>non</td><td>non</td><td>non</td><td>oui</td></tr></tbody></table>

### 14.5.5 Copie de conteneurs

Un dernier point qu'il peut être utile de mentionner concerne la copie de conteneurs. On avait vu dans le chapitre 12 *Plus sur les listes* que la copie de listes se fait par référence. Cela est un mécanisme général pour tous les types de conteneurs, sauf pour les chaînes de caractères. *Python Tutor* nous permet de visualiser cela (Figure 14.2).

Ainsi, il faut toujours faire attention quand on fait une copie d'un conteneur modfiable (liste, dictionnaire, set, etc.). On verra que Python se comporte de la même manière avec les objets *arrays* (chapitre 20 *module Numpy*) ou *Dataframes* (chapitre 22 *Module pandas*), car on peut les considérer également comme des conteneurs.

## 14.6 Dictionnaires et sets de compréhension

Nous avons abordé les listes de compréhension dans le chapitre 12 *Plus sur les listes*. Il est également possible de générer des dictionnaires de compréhension :

1 >>> dico = {'a': 10, 'g': 10, 't': 11, 'c': 15}

2 >>> dico.items()

3 dict_items(('a', 10), ('g', 10), ('t', 11), ('c', 15)])

4 >>> {key:val*2 for key, val in dico.items()}

5 {'a': 20, 'g': 20, 't': 22, 'c': 30}

6 >>>

7 >>> animaux = (("singe", 3), ("girafe", 4), ("rhinocéros", 2))

8 >>> {animal:nombre for animal, nombre in animaux}

9 {'singe': 3, 'girafe': 4, 'rhinocéros': 2}

<footnote>* s'il ne contient que des objets hachables</footnote>

<footnote>* à partir de Python 3.7 uniquement</footnote>
<figure><img src="image_31.png" /><figcaption>FIGURE 14.2 – Copie de conteneurs.</figcaption></figure>

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 = "atctcgatcgatcgcgtagctagctcgccatacgtacgactacgt"

2 >>> {base:seq.count(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*<fnref n="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 :

1 >>> {i for i in range(10)}

2 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

3 >>> {i**2 for i in range(10)}

4 {0, 1, 64, 4, 36, 9, 16, 49, 81, 25}

5 >>>

6 >>> animaux = (("singe", 3), ("girafe", 4), ("rhinocéros", 2))

7 >>> {ani for ani, _ in animaux}

8 {'girafe', 'singe', 'rhinocéros'}

## 14.7 Module *collections*

Le module *collections*<fnref n="8" /> contient d'autres types de conteneurs qui peuvent se révéler utiles, c'est une véritable mine d'or ! Nous n'aborderons pas tous ces objets ici, mais nous pouvons citer tout de même certains d'entre eux si vous souhaitez aller un peu plus loin :

* Les dictionnaires ordonnés<fnref n="9" />, qui se comportent comme les dictionnaires classiques, mais qui sont ordonnés, c'est-à-dire que si on les affiche ou on itère dessus, l'ordre sera le même que celui utilisé pour sa création. Avant la version

<footnote n="7">7. https://www.datacamp.com/community/tutorials/python-dictionary-comprehension</footnote>

<footnote n="8">8. https://docs.python.org/fr/3/library/collections.html</footnote>

<footnote n="9">9. https://docs.python.org/fr/3/library/collections.html#collections.OrderedDict</footnote>
3.6 de Python, ces dictionnaires ordonnés avaient un intérêt, car l'ordre des dictionnaires normaux était arbitraire. Désormais, les dictionnaires normaux se comportent presque en tout point comme les dictionnaires ordonnés.

* Les `defaultdicts`<fnref n="10" />, qui génèrent des valeurs par défaut quand on demande une clé qui n'existe pas (cela évite que Python génère une erreur).

* Les compteurs<fnref n="11" />, dont un exemple est présenté ci-dessous.

* Les `namedtuples`<fnref n="12" />, que nous évoquerons au chapitre 24 *Avoir plus la classe avec les objets* (en ligne).

L'objet `collection.Counter()` est particulièrement intéressant et simple à utiliser. Il crée des compteurs à partir d'objets itérables, par exemple :

```python
1 >>> import collections
2 >>> compo_seq = collections.Counter("aatctccgatcgatcgatcgatgatc")
3 >>> compo_seq
4 Counter({'a': 7, 't': 7, 'c': 7, 'g': 5})
5 >>> type(compo_seq)
6 <class 'collections.Counter'>
7 >>> compo_seq["a"]
8 7
9 >>> compo_seq["n"]
10 0
```

Dans cet exemple, Python a automatiquement compté chaque caractère a, t, g et c de la chaîne de caractères passée en argument. Cela crée un objet de type `Counter` qui se comporte ensuite comme un dictionnaire, à une exception près : si on appelle une clé qui n'existe pas dans l'itérable initiale (comme le `n` ci-dessus), la valeur renvoyée est 0.

## 14.8 Exercices

### Conseil

Pour ces exercices, créez des scripts puis exécutez-les dans un shell.

### 14.8.1 Séquence peptidique et dictionnaire

Les numéros d'acides aminés commencent rarement à 1 dans les fichiers PDB. Créez un dictionnaire où chaque clé est un numéro de résidu de 3 à 9, et chaque valeur est un acide aminé de la séquence peptidique suivante : `SEQPEPT`. Utilisez pour cela les fonctions `dict()` et `zip()`.

### 14.8.2 Composition en acides aminés

En utilisant un `set` et la méthode `.count()` des chaînes de caractères, déterminez le nombre d'occurrences de chaque acide aminé dans la séquence

`AGWPSGGASAGLAILWGASAIMPGALW`.

### 14.8.3 Mots de deux et trois lettres dans une séquence d'ADN

Créez une fonction `compte_mots_2_lettres()`, qui prend comme argument une séquence sous la forme d'une chaîne de caractères et qui renvoie tous les mots de deux lettres qui existent dans la séquence sous la forme d'un dictionnaire. Par exemple pour la séquence `ACCTAGCCCTA`, le dictionnaire renvoyée serait :

`{'AC': 1, 'CC': 3, 'CT': 2, 'TA': 2, 'AG': 1, 'GC': 1}`

Créez une nouvelle fonction `compte_mots_3_lettres()`, qui a un comportement similaire à `compte_mots_2_lettres()`, mais avec des mots de trois lettres.

Utilisez ces fonctions pour afficher les mots de deux et trois lettres et leurs occurrences trouvés dans la séquence d'ADN :

`ACCTAGCCATGTAGAATCGCCTAGGCTTTAGCTAGCTCTAGCTAGCTG`

Voici un exemple de sortie attendue :

<footnote n="10">10. https://docs.python.org/fr/3/library/collections.html#collections.defaultdict</footnote>

<footnote n="11">11. https://docs.python.org/fr/3/library/collections.html#collections.Counter</footnote>

<footnote n="12">12. https://docs.python.org/fr/3/library/collections.html#collections.namedtuple</footnote>
```
Mots de 2 lettres
AC : 1
CC : 3
CT : 8
[...]
Mots de 3 lettres
ACC : 1
CCT : 2
CTA : 5
[...]
```

### 14.8.4 Mots de deux lettres dans la séquence du chromosome I de *Saccharomyces cerevisiae*

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`<fnref n="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 `extract-words.py` 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`<fnref n="14" />);

* le génome de la bactérie *Escherichia coli* (fichier `NC_000913.fna`<fnref n="15" />).

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 `1bta.pdb`<fnref n="16" />, qui correspond à la structure tridimensionnelle de la protéine barstar<fnref n="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**

<footnote n="13">13. https://python.sdv.u-paris.fr/data-files/NC_001133.fna</footnote>

<footnote n="14">14. https://python.sdv.u-paris.fr/data-files/NC_001133.fna</footnote>

<footnote n="15">15. https://python.sdv.u-paris.fr/data-files/NC_000913.fna</footnote>

<footnote n="16">16. https://files.rcsb.org/download/1BTA.pdb</footnote>

<footnote n="17">17. http://www.rcsb.org/pdb/explore.do?structureId=1BTA</footnote>
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*.

### 14.8.7 Barycentre d'une protéine (exercice +++)

Téléchargez le fichier 1bta.pdb<fnref n="18" /> qui correspond à la structure tridimensionnelle de la protéine barstar<fnref n="19" /> sur le site de la Protein Data Bank (PDB).

Un carbone alpha est présent dans chaque résidu (acide aminé) d'une protéine. On peut obtenir une bonne approximation du barycentre d'une protéine en calculant le barycentre de ses carbones alpha.

Le barycentre G de coordonnées ($G_x, G_y, G_z$) est obtenu à partir des n carbones alpha (CA) de coordonnées ($CA_x, CA_y, CA_z$) avec :

$$G_x = \frac{1}{n} \sum_{i=1}^{n} CA_{i,x}$$

$$G_y = \frac{1}{n} \sum_{i=1}^{n} CA_{i,y}$$

$$G_z = \frac{1}{n} \sum_{i=1}^{n} CA_{i,z}$$

Créez une fonction `calcule_barycentre()`, qui prend comme argument une liste de dictionnaires dont les clés (resid, x, y et z) sont celles de l'exercice précédent et qui renvoie les coordonnées du barycentre sous la forme d'une liste de `floats`.

Utilisez la fonction `trouvealpha()` de l'exercice précédent et la fonction `calcule_barycentre()` pour afficher, avec deux chiffres significatifs, les coordonnées du barycentre des carbones alpha de la barstar.

### 14.8.8 Kinases et protéines humaines

Nous avons extrait de la base de données de protéines UniProt la liste des protéines humaines (dans le fichier `human_proteins.txt`<fnref n="20" />) et la liste des kinases (dans le fichier `kinases_proteins.txt`<fnref n="21" />), qui sont une famille de protéines enzymatiques<fnref n="22" /> impliquées dans la phosphorylation d'autres protéines.

Chaque fichier contient un identifiant de protéine par ligne. Voici un exemple pour le fichier `human_proteins.txt`:

```
A0A087X1C5
A0A0B4J2F0
A0A0B4J2F2
A0A0C5B5G6
A0A0K2S4Q6
A0A0U1RRE5
A0A1B0GTW7
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.

<footnote n="18">18. https://files.rcsb.org/download/1BTA.pdb</footnote>

<footnote n="19">19. http://www.rcsb.org/pdb/explore.do?structureId=1BTA</footnote>

<footnote n="20">20. https://python.sdv.u-paris.fr/data-files/human_proteins.txt</footnote>

<footnote n="21">21. https://python.sdv.u-paris.fr/data-files/kinase_proteins.txt</footnote>

<footnote n="22">22. https://fr.wikipedia.org/wiki/Kinase</footnote>
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

### 15.1 Pourquoi créer ses propres modules ?

Dans le chapitre 9 *Modules*, nous avons découvert quelques modules Python comme *random*, *math*, etc. Nous avons vu par ailleurs dans les chapitres 10 *Fonctions* et 13 *Plus sur les fonctions* que les fonctions sont utiles pour réutiliser une fraction de code plusieurs fois au sein d'un même programme, sans avoir à dupliquer ce code. On peut imaginer qu'une fonction utile pourrait être judicieusement réutilisée dans un autre programme Python. C'est justement l'intérêt de créer un module. On y regroupe un ensemble de fonctions que l'on peut être amené à utiliser souvent. En général, les modules sont regroupés autour d'un thème précis. Par exemple, on pourrait concevoir un module d'analyse de séquences biologiques ou encore de gestion de fichiers PDB.

### 15.2 Création d'un module

En Python, la création d'un module est très simple. Il suffit d'écrire un ensemble de fonctions (et éventuellement de constantes) dans un fichier, puis d'enregistrer ce dernier avec une extension `.py` (comme n'importe quel script Python). À titre d'exemple, nous allons créer un module simple que nous enregistrerons sous le nom `message.py` :

```python
1 """Module inutile qui affiche des messages."""
2
3 DATE = "2024-01-05"
4
5
6 def bonjour(nom):
7     """Dit Bonjour."""
8     return f" Bonjour {nom} "
9
10
11 def ciao(nom):
12     """Dit Ciao."""
13     return f" Ciao {nom} "
14
15
16 def hello(nom):
17     """Dit Hello."""
18     return f" Hello {nom} "
```

Les chaînes de caractères entre triple guillemets en tête du module et en tête de chaque fonction sont facultatives mais elles jouent néanmoins un rôle essentiel dans la documentation du code.
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).

## 15.3 Utilisation de son propre module

Pour appeler une fonction ou une variable de ce module, il faut que le fichier `message.py` soit dans le répertoire courant (dans lequel on travaille) ou bien dans un répertoire listé par la variable d'environnement `PYTHONPATH` de votre système d'exploitation. Ensuite, il suffit d'importer le module et toutes ses fonctions (et constantes) vous sont alors accessibles.

Remarque

Avec Mac OS X et Linux, il faut taper la commande suivante depuis un shell Bash pour modifier la variable d'environnement `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 :

```
1 >>> import message
2 >>> message.hello("Joe")
3 'Hello Joe'
4 >>> message.ciao("Bill")
5 'Ciao Bill'
6 >>> message.bonjour("Monsieur")
7 'Bonjour Monsieur'
8 >>> message.DATE
9 '2024-01-05'
```

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<fnref n="1" />, c'est-à-dire le code précompilé du module.

## 15.4 Les docstrings

Lorsqu'on écrit un module, il est important de créer de la documentation pour expliquer ce que fait le module et comment utiliser chaque fonction. Les chaînes de caractères entre triple guillemets, situées en début du module et de chaque fonction, sont là pour cela : on les appelle *docstrings* (« chaînes de documentation » en français). Les *docstrings* seront détaillées dans le chapitre 16 *Bonnes pratiques en programmation Python*.

Les *docstrings* permettent notamment de fournir de l'aide lorsqu'on invoque la commande `help()` :

<footnote n="1">1. https://docs.python.org/fr/3/glossary.html#term-bytecode</footnote>
```python
1 >>> import message
2 >>> help(message)
3
4 Help on module message:
5
6 NAME
7 message - Module inutile qui affiche des messages.
8
9 FUNCTIONS
10 bonjour(nom)
11 Dit Bonjour.
12
13 ciao(nom)
14 Dit Ciao.
15
16 hello(nom)
17 Dit Hello.
18
19 DATA
20 DATE = '2024-01-05'
21
22 FILE
23 /home/pierre message.py
```

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 :

```python
1 >>> help(message.ciao)
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*.

## 15.5 Visibilité des fonctions dans un module

La visibilité des fonctions au sein des modules suit des règles simples :

* Les fonctions dans un même module peuvent s'appeler les unes les autres.

* Les fonctions dans un module peuvent appeler des fonctions situées dans un autre module s'il a été préalablement importé. Par exemple, si la commande `import autremodule` est utilisée dans un module, il est possible d'appeler une fonction avec `autremodule.fonction()`.

Toutes ces règles viennent de la manière dont Python gère les **espaces de noms**. De plus amples explications sont données sur ce concept dans le chapitre 24 *Avoir plus la classe avec les objets* (en ligne).
## 15.6 Module ou script ?

Vous avez remarqué que notre module `message` ne contient que des fonctions et une constante. Si on l'exécutait comme un script classique, cela n'afficherait rien :

```
$ python message.py
$
```

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 `message2.py` suivant :

```python
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 on l'importe dans l'interpréteur, on obtient :

```
1 >>> import message2
2 Bonjour Joe
```

Ceci n'est pas le comportement voulu pour un module, car on n'attend pas d'affichage particulier lors de son charge-ment. Par exemple la commande `import math` n'affiche rien dans l'interpréteur.

Afin de pouvoir utiliser un code Python en tant que module ou en tant que script, nous vous conseillons la structure suivante :

```python
1 """Script de test."""
2
3
4 def bonjour(nom):
5     """Dit Bonjour."""
6     return f"Bonjour {nom}"
7
8
9 if __name__ == "__main__":
10 print(bonjour("Joe"))
```

À la ligne 9, l'instruction `if __name__ == "__main__":` indique à Python :

* Si le programme `message2.py` est exécuté en tant que script dans un shell, le résultat du test `if` sera alors `True` et le bloc d'instructions correspondant (ligne 10) sera exécuté :

```
$ python message2.py
Bonjour Joe
```

* Si le programme `message2.py` 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 recommandons 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.

### 15.7.1 Module ADN

Dans le script `adn.py`, construisez un module qui va contenir les fonctions et constantes suivantes.

* Fonction `lit_fasta()` : prend en argument un nom de fichier sous forme d'une chaîne de caractères et renvoie la séquence d'ADN lue dans le fichier sous forme d'une chaîne de caractères.

* Fonction `seq_alea()` : prend en argument une taille de séquence sous forme d'un entier et renvoie une séquence aléatoire d'ADN de la taille correspondante sous forme d'une chaîne de caractères.

* Fonction `comp_inv()` : prend en argument une séquence d'ADN sous forme d'une chaîne de caractères et renvoie la séquence complémentaire inverse (aussi sous forme d'une chaîne de caractères).

* Fonction `prop_gc()` : prend en argument une séquence d'ADN sous forme d'une chaîne de caractères et renvoie la proportion en GC de la séquence sous forme d'un *float*. Nous vous rappelons que la proportion de GC s'obtient comme la somme des bases Guanine (G) et Cytosine (C), divisée par le nombre total de bases (A, T, C, G).

* Constante `BASE_COMP` : dictionnaire qui contient la complémentarité des bases d'ADN ($A \to T, T \to C, G \to C$ et $C \to G$). Ce dictionnaire sera utilisé par la fonction `comp_inv()`.

À la fin de votre script, proposez des exemples d'utilisation des fonctions que vous aurez créées. Ces exemples d'utilisation ne devront pas être exécutés lorsque le script est chargé comme un module.

**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.fasta`<fnref n="2" /> pour tester la fonction `lit_fasta()`.

<footnote n="2">2. https://python.sdv.u-paris.fr/data-files/adn.fasta</footnote>
# CHAPITRE 16

## Bonnes pratiques en programmation Python

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 Enhancement Proposal*<fnref n="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.

<footnote n="1">1. https://www.python.org/dev/peps/</footnote>
# 16.1 De la bonne syntaxe avec la PEP 8

La PEP 8 *Style Guide for Python Code*<fnref n="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 <fnref n="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).

## 16.1.2 Importation des modules

Comme on l'a vu dans le chapitre 9 *Modules*, le chargement d'un module est réalisé avec l'instruction `import module` plutôt qu'avec `from module import *`.

Si on souhaite ensuite utiliser une fonction d'un module, la première syntaxe conduit à `module.fonction()`, ce qui rend explicite la provenance de la fonction. Avec la seconde syntaxe, il faudrait écrire `fonction()`, ce qui peut :

* mener à un conflit si une de vos fonctions a le même nom ;

* rendre difficile la recherche de documentation si on ne sait pas d'où vient la fonction, notamment si plusieurs modules sont chargés avec l'instruction `from module import *`

Par ailleurs, la première syntaxe définit un « espace de noms » spécifique au module (voir le chapitre 24 *Avoir plus la classe avec les objets* (en ligne)).

Dans un script Python, on importe un module par ligne. D'abord les modules internes (classés par ordre alphabétique), c'est-à-dire les modules de base de Python, puis les modules externes (ceux que vous avez installés en plus), et enfin, les modules que vous avez créés.

Si le nom du module est trop long, on peut utiliser un alias. L'instruction `from` est tolérée si vous n'importez que quelques fonctions clairement identifiées.

En résumé :

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`

<footnote n="2">2. https://www.python.org/dev/peps/pep-0008/</footnote>

<footnote n="3">3. https://python.sdv.u-paris.fr/livre-dunod</footnote>
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 >>> ma_liste = [1, 3, 5, 7, 9, 11]

2 >>> for i in range(len(ma_liste)):

3 ... print(ma_liste[i])

Bien sûr, une écriture plus « pythonique » de l'exemple précédent permet de se débarrasser de l'indice `i` :

1 >>> ma_liste = [1, 3, 5, 7, 9, 11]

2 >>> for entier in ma_liste:

3 ... print entier

Enfin, des noms de variable à une lettre peuvent être utilisés lorsque cela a un sens mathématique (par exemple, les noms `x`, `y` et `z` évoquent des coordonnées cartésiennes).

### 16.1.4 Gestion des espaces

La PEP 8 recommande d'entourer les opérateurs (`+`, `-`, `/`, `*`, `==`, `!=`, `>=`, `not`, `in`, `and`, `or...`) d'un espace avant et d'un espace après. Par exemple :

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)

On met un espace après les caractères : et , (mais pas avant) :

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

16.1.5 Longueur de ligne

Une ligne de code ne doit pas dépasser 79 caractères, pour des raisons tant historiques que de lisibilité.

On a déjà vu dans le chapitre 1 *Introduction* que le caractère \ permet de couper des lignes trop longues. Par exemple :

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

À l'intérieur de parenthèses, on peut revenir à la ligne sans utiliser le caractère \. C'est particulièrement utile pour
préciser les arguments d'une fonction ou d'une méthode, lors de sa création ou lors de son utilisation :

1 >>> def ma_fonction (argument_1, argument_2,
2 ...             argument_3, argument_4):
3 ...     return argument_1 +argument_2
4 ...
5 >>> ma_fonction("texte très long", "tigre",
6 ...             "singe", "souris")
7 'texte très longtigre'

Les parenthèses sont également très pratiques, pour répartir sur plusieurs lignes une chaîne de caractères qui sera
ensuite affichée sur une seule ligne :

1 >>> print("ATGCGTACAGTATCGATAAC")

2 ... "ATGACTGCTACGATCGGATA"

3 ... "CGGGTAACGCCATGTACATT")

4 ATGCGTACAGTATCGATAACATGACTGCTACGATCGGATACGGGTAACGCCATGTACATT

Notez qu'il n'y a pas d'opérateur + pour concatené 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 concatené 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 :

1 >>> ma_liste = [1, 2, 3,

2 ... 4, 5, 6,

3 ... 7, 8, 9]

4 >>> mon_dico = {"clé1": 13,

5 ... "clé2": 42,

6 ... "clé3": -10}

16.1.6 Lignes vides

Dans un script, les lignes vides sont utiles pour séparer visuellement les différentes parties du code.

Il est recommandé de laisser deux lignes vides avant la définition d'une fonction ou d'une classe, et de laisser une seule ligne vide avant la définition d'une méthode (dans une classe).

On peut aussi laisser une ligne vide dans le corps d'une fonction pour séparer les sections logiques de la fonction, mais cela est à utiliser avec parcimonie.

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_function` 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 :

1 var_x = number / total * 100 # My useful comment.
### Remarque

La PEP 8 ne fournit pas de recommandation<fnref n="4" /> quant à l'usage de guillemets simples ou de guillemets doubles pour déclarer une chaîne de caractères.

```python
1 >>> var_1 = "Ma chaîne de caractères"
2 >>> var_1
3 'Ma chaîne de caractères'
4 >>> var_2 = 'Ma chaîne de caractères'
5 >>> var_2
6 'Ma chaîne de caractères'
7 >>> var_1 == var_2
8 True
```

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.

## 16.2 Les docstrings et la PEP 257

Les docstrings, que l'on pourrait traduire par « chaînes de documentation » en français, sont un élément essentiel des programmes Python, comme on l'a vu au chapitre 15 *Création de modules*. À nouveau, les développeurs de Python ont émis des recommandations dans la PEP 8, et plus exhaustivement dans la PEP 257⁵, sur la manière de rédiger correctement les docstrings. En voici un résumé succinct.

### 16.2.1 Les principales règles

De manière générale, écrivez des docstrings pour les modules, les fonctions, les classes et les méthodes que vous développez.

Lorsque l'explication est courte et compacte, comme dans certaines fonctions ou méthodes simples, utilisez des docstrings d'une ligne :

```python
1 """Docstring simple d'une ligne se finissant par un point."""
```

Lorsque vous avez besoin de décrire plus en détail un module, une fonction, une classe ou une méthode, utilisez une docstring sur plusieurs lignes :

```python
1 """Docstring de plusieurs lignes, la première ligne est un résumé.
2
3 Après avoir sauté une ligne, on décrit les détails de cette docstring.
4 On termine la docstring avec les triples guillemets
5 sur la ligne suivante.
6 """
```

### Remarque

La PEP 257 recommande d'écrire des docstrings avec trois doubles guillemets, c'est-à-dire :

```python
"""Ceci est une docstring recommandée."""
```

mais pas :

```python
'''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.

<footnote n="4">4. https://peps.python.org/pep-0008/#string-quotes</footnote>

<footnote n="5">5. https://www.python.org/dev/peps/pep-0257/</footnote>
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*<fnref n="6" />,

* la solution NumPy avec le NumPy Style Python Docstrings<fnref n="7" />. 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 :

```python
1 def multiply_nombres(nombre1, nombre2):
2     """Multiplication de deux nombres entiers.
3 
4     Cette fonction ne sert pas à grand chose.
5 
6     Parameters
7         nombre1 : int
8         nombre2 : int
9         Le premier nombre entier.
10        nombre2 : int
11         Le second nombre entier,
12         très important pour cette fonction.
13 
14     Returns
15         int
16     int
17         Le produit des deux nombres.
18     """
19     return nombre1 * nombre2
```

* **Lignes 6 et 7.** La section *Parameters* précise les paramètres de la fonction. Les tirets sur la ligne 7 soulignent le nom de la section pour la rendre visible.

* **Lignes 8 et 9.** On indique le nom et le type du paramètre, séparés par le caractère deux-points. Le type n'est pas obligatoire. En dessous, on indique une description du paramètre en question. La description est indentée.

* **Lignes 10 à 12.** Même chose pour le second paramètre. La description du paramètre peut s'étaler sur plusieurs lignes.

* **Lignes 14 et 15.** La section *Returns* indique ce qui est renvoyé par la fonction (le cas échéant).

* **Lignes 16 et 17.** La mention du type renvoyé est obligatoire. En dessous, on indique une description de ce qui est renvoyé par la fonction. Cette description est aussi indentée.

**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 :

<footnote n="6">6. https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example Google.html</footnote>

<footnote n="7">7. https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example numpy.html</footnote>
```
$ 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质量_not_ok.py`[^8] que nous allons analyser par la suite :

```python
# """Un script de multiplication.
2 """
3
4 import os
5
6 def Multiplie_nombres(nombre1, nombre2 ):
7 """Multiplication de deux nombres entiers
8
9 Cette fonction ne sert pas à grand chose.
10
11 Parameters
12
13 nombre1 : int
13 Le premier nombre entier.
14 nombre2 : int
15 Le second nombre entier.
16 Très utile.
17
18 Returns
19
20 int
21 Le produit des deux nombres.
22
23 """
24 return nombre1 *nombre2
25
26
27 if __name__ == "__main__":
28 print(f"2 x 3 = {Multiplie_nombres(2, 3)}")
29 print(f"4 x 5 = {Multiplie_nombres(4, 5)}")
```

Ce script est d'ailleurs parfaitement fonctionnel :

```
$ python script质量_not_ok.py
2 x 3 = 6
4 x 5 = 20
```

On va tout d'abord vérifier la conformité avec la PEP 8 grâce à l'outil `pycodestyle` :

```shell
$ pycodestyle script质量_not_ok.py
script质量_not_ok.py:6:1: E302 expected 2 blank lines, found 1
script质量_not_ok.py:6:30: E231 missing whitespace after ','
script质量_not_ok.py:6:38: E202 whitespace before ')'
script质量_not_ok.py:26:21: E225 missing whitespace around operator
script质量_not_ok.py:31:10: E211 whitespace before '('
```

* **Ligne 2.** Le bloc `script质量_not_ok.py:6:1:` désigne le nom du script (`script质量_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.

<footnote n="8">8. https://python.sdv.u-paris.fr/data-files/script质量_not_ok.py</footnote>
* **Ligne 4.** Il y a 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* :

```python
$ 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 :

```shell
$ 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)
```

```
Your code has been rated at 6.67/10
```

* **Lignes 3 et 4.** `pylint` indique que nom de la fonction `Multiplie_nombres()` ne respecte pas la convention PEP 8 (ligne 6 du script).

* **Ligne 5.** Le module `os` est chargé mais pas utilisé (ligne 4 du script).

* **Ligne 8.** `pylint` produit également une note sur 10. Ne soyez pas surpris si cette note est très basse (voire négative) la première fois que vous analysez votre script avec `pylint`. Cet outil fournit de nombreuses suggestions d'amélioration et la note attribuée à votre script devrait rapidement augmenter. Pour autant, la note de 10 est parfois difficile à obtenir. Ne soyez pas trop exigeant.

Une version améliorée du script précédent est disponible en ligne 9.

## 16.4 Outil de formatage automatique du code

Se souvenir de toutes les règles PEP 8 est fastidieux. Il existe des outils pour formater automatiquement le code Python pour qu'il soit conforme à la PEP 8. L'outil le plus connu est `black`.

Cet outil n'est pas fourni dans l'installation de base de Python et doit être installé sur votre machine. Avec la distribution Miniconda, cette étape d'installation se résume à une ligne de commande :

```shell
$ conda install -c conda-forge black
```

Voici un exemple d'utilisation :

```shell
$ black script_quality_not_ok.py
reformatted script_quality_not_ok.py

All done!
1 file reformatted.
```

<footnote n="9">9. https://python.sdv.u-paris.fr/data-files/script质量_ok.py</footnote>
Le script `script质量_not_ok.py` a été modifié pour être conforme à la PEP 8, ce qu'on peut vérifier avec `pycodestyle` :

```
$ pycodestyle script质量_not_ok.py
```

qui ne renvoie aucune erreur.

`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质量_not_ok.py` qui n'aurait pas été modifié :

```bash
$ black --diff script质量_not_ok.py
--- script质量_not_ok.py      2024-02-05 12:07:04.851491+00:00
+++ script质量_not_ok.py      2024-02-05 12:07:10.418009+00:00
@@ -1,11 +1,12 @@
"""Un script de multiplication.
"""

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 :
```python
  1 """Docstring d'une ligne décrivant brièvement ce que fait le programme.
  2
  3 Usage:
  4 ========
  5         python nom_de_ce super_script.py argument1 argument2
  6
  7         argument1: un entier signifiant un truc
  8         argument2: une chaîne de caractères décrivant un bidule
  9 """
10
11 __authors__ = ("Johny B Good", "Hubert de la Pâte Feuilletée")
12 __contact__ = ("johny@bgood.us", "hub@pate.feuilletee.fr")
13 __copyright__ = "MIT"
14 __date__ = "2030-01-01"
15 __version__ = "1.2.3"
16
17 import module_interne
18 import module_interne_2 as mod2
19
20 import module_externe
21
22 import my_module
23
24 UNE_CONSTANTE = valeur
25
26
27 def une_fonction_complexe(arg1, arg2, arg3):
28     """Résumé de la docstring décrivant la fonction.
29     """
30     Description détaillée.
31     """
32     [...]
33     return une_chose
34
35
36 def une_fonction_simple(arg1, arg2):
37     """Docstring d'une ligne décrivant la fonction."""
38     [...]
39     return autre_chose
40
41
42 if __name__ == "__main__":
43     # Ici débute le programme principal.
44     [...]
```

* **Lignes 1 à 9.** Cette docstring décrit globalement le script. Cette docstring (ainsi que les autres) seront visibles si on importe le script en tant que module, puis en invoquant la commande `help()` (voir chapitre 15 *Création de modules*).

* **Lignes 11 à 15.** On définit ici un certain nombre de variables avec des doubles underscores donnant quelques informations sur la version du script, les auteurs, etc. Il s'agit de métadonnées que la commande `help()` pourra afficher. Ces métadonnées sont utiles lorsque le code est distribué à la communauté.

* **Lignes 17 à 22.** Importation des modules. D'abord les modules internes à Python (fournis en standard), puis les modules externes (ceux qu'il faut installer en plus), puis les modules créés localement. Un module par ligne.

* **Ligne 24.** Définition des constantes. Le nom des constantes est en majuscule.

* **Lignes 27 à 39.** Définition des fonctions. Avant chaque fonction, on laisse deux lignes vides.

* **Lignes 42 à 44.** On écrit le programme principal. Le test ligne 42 n'est vrai que si le script est utilisé en tant que programme.

## 16.6 Conseils sur la conception d'un script

Voici quelques conseils pour vous aider à concevoir un script Python.

* **Réfléchissez** avec un papier, un crayon... et un cerveau (voire même plusieurs)! Reformulez avec vos propres mots les consignes qui vous ont été données. Dessinez des schémas si cela vous aide.

* **Découpez en fonctions** chaque élément de votre programme. Vous pourrez ainsi tester chaque élément indépen-
damment du reste. Pensez à écrire les *docstrings* en même temps que vous écrivez vos fonctions.

* **Documentez-vous.** L'algorithme dont vous avez besoin existe-t-il déjà dans un autre module ? De quels outils mathématiques avez-vous besoin dans votre algorithme ?

* Quand l'algorithme est complexe, **commentez votre code** pour expliquer votre raisonnement. Utiliser des fonctions (ou méthodes) encore plus petites peut aussi être une solution.

* Utilisez des **noms de variables explicites**, qui signifient quelque chose. En lisant votre code, on doit comprendre ce que vous faites. Choisir des noms de variables pertinents permet aussi de réduire les commentaires.

* Quand vous construisez une structure de données complexe (par exemple une liste de dictionnaires contenant d'autres objets), **documentez** l'organisation de cette structure de données avec un exemple simple.

* Si vous créez ou manipulez une entité cohérente avec des propriétés propres, essayez de construire une **classe**. Reportez-vous, pour cela, au chapitre 23 *Avoir la classe avec les objets*.

* **Testez** votre code sur un **petit jeu de données**, pour comprendre rapidement ce qui se passe et corriger d'éventuelles erreurs. Par exemple, une séquence d'ADN de 1 000 bases est plus facile à manipuler que le génome humain (3 × 10⁹ bases) !

* Lorsque votre programme « plante », **lisez le message d'erreur**. Python tente de vous expliquer ce qui ne va pas. Le numéro de la ligne qui pose problème est aussi indiqué.

* **Discutez avec des gens.** Faites tester votre programme par d'autres. Les instructions d'utilisation sont-elles claires ?

* Enfin, si vous **distribuez votre code** :

— Rédigez une **documentation** claire.

— Testez votre programme (jetez un œil aux tests unitaires<fnref n="10" />).

— Précisez une **licence** d'utilisation (voir le site *Choose an open source license*<fnref n="11" />).

## 16.7 Pour terminer : la PEP 20

La PEP 20 est une sorte de réflexion philosophique avec des phrases simples qui devraient guider tout programmeur. Comme les développeurs de Python ne manque pas d'humour, celle-ci est accessible sous la forme d'un « œuf de Pâques » (*easter egg*, en anglais) ou encore « fonctionnalité cachée d'un programme » en important un module nommé *this* :

1 >>> import this
2 The Zen of Python, by Tim Peters
3
4 Beautiful is better than ugly.
5 Explicit is better than implicit.
6 Simple is better than complex.
7 Complex is better than complicated.
8 Flat is better than nested.
9 Sparse is better than dense.
10 Readability counts.
11 Special cases aren't special enough to break the rules.
12 Although practicality beats purity.
13 Errors should never pass silently.
14 Unless explicitly silenced.
15 In the face of ambiguity, refuse the temptation to guess.
16 There should be one-- and preferably only one --obvious way to do it.
17 Although that way may not be obvious at first unless you're Dutch.
18 Now is better than never.
19 Although never is often better than *right* now.
20 If the implementation is hard to explain, it's a bad idea.
21 If the implementation is easy to explain, it may be a good idea.
22 Namespaces are one honking great idea -- let's do more of those!

Et si l'aventure et les *easter eggs* vous plaisent, testez également la commande

1 >>> import antigravity

Il vous faudra un navigateur et une connexion internet.

<footnote n="10">10. https://fr.wikipedia.org/wiki/Test_unitaire</footnote>

<footnote n="11">11. https://choosealicense.com/</footnote>
Pour aller plus loin

* L'article *Python Code Quality : Tools & Best Practices*<fnref n="12" /> du site *Real Python* est une ressource intéressante pour explorer plus en détail la notion de qualité pour un code Python. De nombreux *linters* y sont présentés.

* Les articles *Assimilez les bonnes pratiques de la PEP 8*<fnref n="13" /> du site *OpenClassrooms* et *Structuring Python Programs*<fnref n="14" /> du site *Real Python* rappellent les règles d'écriture et les bonnes pratiques vues dans ce chapitre.

<footnote n="12">12. https://realpython.com/python-code-quality/</footnote>

<footnote n="13">13. https://openclassrooms.com/fr/courses/4425111-perfectionnez-vous-en-python/4464230-assimilez-les-bonnes-pratiques/</footnote>

<footnote n="14">14. https://realpython.com/python-program-structure/</footnote>
# Expressions régulières et parsing

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<fnref n="1" /> sur le site officiel de Python.

## 17.1 Définition et syntaxe

Une expression régulière est une suite de caractères qui a pour but de décrire un fragment de texte. Cette suite de caractères est encore appelée **motif** (en anglais, *pattern*), qui est constitué de deux types de caractères :

* les caractères dits *normaux* ;

* les métacaractères ayant une signification particulière, par exemple le caractère `^` signifie début de ligne, et non pas le caractère « chapeau » littéral.

Avant de présenter les regex en Python, nous allons faire un petit détour par Unix. En effet, certains programmes, comme `egrep`, `sed` ou encore `awk`, savent interpréter les expressions régulières. Tous ces programmes fonctionnent généralement selon le schéma suivant :

* le programme lit un fichier ligne par ligne ;

* pour chaque ligne lue, si l'expression régulière passée en argument est retrouvée dans la ligne, alors le programme effectue une action.

Par exemple, pour le programme `egrep` :

```
$ egrep "^DEF" herp_virus.gbk
DEFINITION Human herpesvirus 2, complete genome.
```

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.

<footnote n="1">1. https://docs.python.org/fr/3/library/re.html</footnote>
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, ACCCT et ACCCCT mais pas dans ACT, ACCCCCT ou 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étacaractè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.2 Quelques ressources en ligne

Nous vous conseillons de tester systématiquement vos expressions régulières sur des exemples simples. Pour vous aider, nous vous recommandons plusieurs sites internet :

* `RegexOne`<fnref n="2" /> : tutoriel en ligne très bien fait.

* `RegExr`<fnref n="3" /> et `ExtendsClass`<fnref n="4" /> : visualisent tous les endroits où une regex est retrouvée dans un texte.

* `pythex.org`<fnref n="5" /> : interface simple et efficace, dédiée à Python.

* `Regular-Expressions.info`<fnref n="6" /> : documentation exhaustive sur les regex (il y a même une section sur Python).

N'hésitez pas à explorer ces sites avant de vous lancer dans les exercices ou dans l'écriture de vos propres 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, chaîne)`. Si motif est retrouvé dans `chaîne`, 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 >>> re.search("tigre", animaux)

4 <_sre.SRE_Match object at 0x7fefdaefe2a0>

5 >>> if re.search("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`.

<footnote n="2">2. https://regexone.com/</footnote>

<footnote n="3">3. https://regexr.com/</footnote>

<footnote n="4">4. https://extendsclass.com/regex-tester.html#python</footnote>

<footnote n="5">5. https://pythex.org/</footnote>

<footnote n="6">6. https://www.regular-expressions.info</footnote>
## 17.3.2 Les fonctions match() et fullmatch()

Il existe aussi la fonction match() dans le module re qui fonctionne sur le modèle de search(). La différence est qu'elle renvoie un objet du type SRE_Match seulement lorsque la regex correspond au début de la chaîne de caractères (à partir du premier caractère) :

```python
1 >>> animaux = "girafe tigre singe"
2 >>> re.search("tigre", animaux)
3 <_sre.SRE_Match object at 0x7fefdaefe718>
4 >>> re.match("tigre", animaux)
5 >>>
6 >>> animaux = "tigre singe"
7 >>> re.match("tigre", animaux)
8 <_sre.SRE_Match object; span=(0, 5), match='tigre'>
9 >>>
```

Il existe également la fonction fullmatch(), qui renvoie un objet du type SRE_Match si et seulement si l'expression régulière correspond **exactement** à la chaîne de caractères.

```python
1 >>> animaux = "tigre "
2 >>> re.fullmatch("tigre", animaux)
3 >>> animaux = "tigre"
4 >>> re.fullmatch("tigre", animaux)
5 <_sre.SRE_Match object; span=(0, 5), match='tigre'>
```

De manière générale, nous vous recommandons l'usage de la fonction search(). Si vous souhaitez avoir une correspondance avec le début de la chaîne de caractères comme dans la fonction match(), vous pouvez toujours utiliser l'accroche de début de ligne ^ . Si vous voulez une correspondance exacte, comme dans la fonction fullmatch(), vous pouvez utiliser les métacaractères ^ et $ , par exemple ^tigre$ .

## 17.3.3 Compilation d'expressions régulières

Lorsqu'on a besoin de tester la même expression régulière sur plusieurs milliers de chaînes de caractères, il est pratique de compiler préalablement la regex à l'aide de la fonction compile(), qui renvoie un objet de type SRE_Pattern :

```python
1 >>> regex = re.compile("^tigre")
2 >>> regex
3 <_sre.SRE_Pattern object at 0x7fefdafd0df0>
```

On peut alors utiliser directement cet objet avec la méthode .search() :

```python
1 >>> animaux = "girafe tigre singe"
2 >>> regex.search(animaux)
3 >>> animaux = "tigre singe"
4 >>> regex.search(animaux)
5 <_sre.SRE_Match object at 0x7fefdaefe718>
6 >>> animaux = "singe tigre"
7 >>> regex.search(animaux)
```

## 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 :

```python
1 >>> regex = re.compile("([0-9]+)\.( [0-9]+)")
```

Dans cet exemple, on recherche un nombre décimal, c'est-à-dire une chaîne de caractères :

* qui débute par un ou plusieurs chiffres [0-9]+ ,

* suivi d'un point \ . (le point a d'habitude une signification de métacaractère, donc il faut l'échapper avec \ pour qu'il retrouve sa signification de point),

* et qui se termine encore par un ou plusieurs chiffres [0-9]+ .

Les parenthèses dans la regex créent des groupes ( [0-9]+ deux fois) qui seront récupérés ultérieurement par la méthode .group() .
```python
1 >>> resultat = regex.search("pi vaut 3.14")
2 >>> resultat.group(0)
3 '3.14'
4 >>> resultat.group(1)
5 '3'
6 >>> resultat.group(2)
7 '14'
8 >>> resultat.start()
9 8
10 >>> resultat.end()
11 12
```

La totalité de la correspondance est donnée par `.group(0)`, le premier élément entre parenthèses est donné par `.group(1)` et le second par `.group(2)`.

Les méthodes `.start()` et `.end()` donnent respectivement la position de début et de fin de la zone qui correspond à la regex. Notez que la méthode `.search()` ne renvoie que la première zone qui correspond à l'expression régulière, même s'il en existe plusieurs :

```python
1 >>> resultat = regex.search("pi vaut 3.14 et e vaut 2.72")
2 >>> resultat.group(0)
3 '3.14'
```

### 17.3.5 La méthode `.findall()`

Pour récupérer chaque zone dans la regex, s'il y en a plusieurs, vous pouvez utiliser la méthode `.findall()` qui renvoie une liste des éléments en correspondance :

```python
1 >>> regex = re.compile("[0-9]+\.[0-9]+")
2 >>> resultat = regex.findall("pi vaut 3.14 et e vaut 2.72")
3 >>> resultat
4 ['3.14', '2.72']
```

L'utilisation des groupes entre parenthèses est également possible, ceux-ci sont alors renvoyés sous la forme de tuples :

```python
1 >>> regex = re.compile("([0-9]+)\.( [0-9]+)")
2 >>> resultat = regex.findall("pi vaut 3.14 et e vaut 2.72")
3 >>> resultat
4 [('3', '14'), ('2', '72')]
```

### 17.3.6 La méthode `.sub()`

Enfin, la méthode `.sub()` permet d'effectuer des remplacements assez puissants. Par défaut, la méthode `.sub(chaine1, chaine2)` remplace toutes les occurrences trouvées par l'expression régulière dans `chaine2` par `chaine1`. Si vous souhaitez ne remplacer que les n premières occurrences, utilisez l'argument `count=n` :

```python
1 >>> regex = re.compile("[0-9]+\.[0-9]+")
2 >>> regex.sub("quelque chose", "pi vaut 3.14 et e vaut 2.72")
3 'pi vaut quelque chose et e vaut quelque chose'
4 >>> regex.sub("quelque chose", "pi vaut 3.14 et e vaut 2.72", count=1)
5 'pi vaut quelque chose et e vaut 2.72'
```

Encore plus puissant, il est possible d'utiliser dans le remplacement des groupes qui ont été « capturés » avec des parenthèses :

```python
1 >>> regex = re.compile("([0-9]+)\.( [0-9]+)")
2 >>> phrase = "pi vaut 3.14 et e vaut 2.72"
3 >>> regex.sub("approximativement \\1", phrase)
4 'pi vaut approximativement 3 et e vaut vaut approximativement 2'
5 >>> regex.sub("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 :

1 >>> re.search("(pan)\\1", "bambi et panpan")

2 <_sre.SRE_Match object; span=(9, 15), match='panpan'>

3 >>> re.search("(pan)\\1", "le pistolet a fait pan !")

4 >>>

Dans la regex (pan)\\1, on capture d'abord le groupe (pan) grâce aux parenthèses (il s'agit du groupe 1, puisque c'est le premier jeu de parenthèses), immédiatement suivi du même groupe grâce au \\1. Dans cet exemple, on capture donc le mot panpan (lignes 1 et 2). Si, par contre, on a une seule occurrence du mot pan, cette regex ne fonctionne pas, ce qui est le cas ligne 3.

Bien sûr, si on avait eu un deuxième groupe, on aurait pu le réutiliser avec \\2, un troisième groupe avec \\3, etc.

Nous espérons vous avoir convaincu de la puissance du module re et des expressions régulières. Alors, plus de temps à perdre, à vos regex !

## 17.4 Exercices

### Conseil

Pour ces exercices, créez des scripts puis exécutez-les dans un shell.

### 17.4.1 Regex de base

Dans cet exercice, nous allons manipuler le fichier GenBank NC_001133.gbk<fnref n="7" /> correspondant au chromosome I de la levure Saccharomyces cerevisiae.

Créez un script regex_genbank.py :

* qui recherche le mot DEFINITION en début de ligne dans le fichier GenBank, puis affiche la ligne correspondante;

* qui recherche tous les journaux (mot-clé JOURNAL) dans lesquels ont été publiés les travaux sur cette séquence, puis affiche les lignes correspondantes.

### 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*.

### 17.4.2 Enzyme de restriction

Une enzyme de restriction est une protéine capable de couper une molécule d'ADN. Cette coupure se fait sur le site de restriction de l'ADN qui correspond à une séquence particulière de nucléotides (bases).

Pour chacune des enzymes ci-dessous, déterminez les expressions régulières qui décrivent leurs sites de restriction. Le symbole N correspond aux bases A, T, C ou G. W correspond à A ou T. Y correspond à C ou T. R correspond à A ou G.

<table><thead><tr><td>Enzyme</td><td>Site de restriction</td></tr></thead><tbody><tr><td>HinFI</td><td>GANTC</td></tr><tr><td>EcoRII</td><td>CCWGG</td></tr><tr><td>BbvBI</td><td>GGYRCC</td></tr><tr><td>BcoI</td><td>CYCGRG</td></tr><tr><td>Psp5II</td><td>RGGWCCY</td></tr></tbody></table>

<footnote n="7">7. https://python.sdv.u-paris.fr/data-files/NC_001133.gbk</footnote>