<figure><img src="image_55.png" /><figcaption>FIGURE 24.2 – Illustration avec Python Tutor de la non destruction d'un attribut de classe (étape 1).</figcaption></figure>

Tutor nous montre que `citron1.saveur` n'existe pas dans l'espace `Citron` instance qui est vide; ainsi, Python utilisera l'attribut de classe `.saveur` qui contient toujours la valeur `acide` (cf. figure 24.3).

<figure><img src="image_56.png" /><figcaption>FIGURE 24.3 – Illustration avec Python Tutor de la non destruction d'un attribut de classe (étape 2).</figcaption></figure>

La ligne 11 va tenter de détruire l'attribut de classe `.saveur`. Toutefois, Python interdit cela, ainsi l'erreur suivante sera générée :
1. Traceback (most recent call last):

2. File "test.py", line 11, in <module>

3. del citron1.saveur

4. ^^^^^^^^^^^^^^^^^

5. AttributeError: 'Citron' object has no attribute 'saveur'

En fait, la seule manière de modifier un attribut de classe est d'utiliser une syntaxe `NomClasseattribut_de_classe = nouvelle_valeur`, dans l'exemple ci-dessus cela aurait été `Citron.saveur = "sucrée"`. De même, pour sa destruction, il faudra utiliser la même syntaxe : `del Citron.saveur`.

### Conseil

Même si on peut modifier un attribut de classe, nous vous déconseillons de le faire. Une utilité des attributs de classe est par exemple de définir des constantes (mathématique ou autre), donc cela n'a pas de sens de vouloir les modifier ! Il est également déconseillé de créer des attributs de classe avec des objets modifiables comme des listes et des dictionnaires, cela peut avoir des effets désastreux non désirés. Nous verrons plus bas un exemple concret d'attribut de classe qui est très utile, à savoir le concept d'objet de type `property`.

Si vous souhaitez avoir des attributs modifiables dans votre classe, créez plutôt des attributs d'instance dans le `__init__()`.

## 24.6 Accès et modifications des attributs depuis l'extérieur

### 24.6.1 Le problème

On a vu jusqu'à maintenant que Python était très permissif concernant le changement de valeur de n'importe quel attribut depuis l'extérieur. On a vu aussi qu'il était même possible de créer de nouveaux attributs depuis l'extérieur ! Dans d'autres langages orientés objet ceci n'est pas considéré comme une bonne pratique. Il est plutôt recommandé de définir une *interface*, c'est-à-dire tout un jeu de méthodes accédant ou modifiant les attributs. Ainsi, le concepteur de la classe a la garantie que celle-ci est utilisée correctement du « côté client ».

### Remarque

Cette stratégie d'utiliser uniquement l'interface de la classe pour accéder aux attributs provient des langages orientés objet comme Java et C++. Les méthodes accédant ou modifiant les attributs s'appellent aussi des *getters* et *setters* (en français on dit accesseurs et mutateurs). Un des avantages est qu'il est ainsi possible de vérifier l'intégrité des données grâce à ces méthodes : si par exemple on souhaitait avoir un entier seulement, ou bien une valeur bornée, on peut facilement ajouter des tests dans le *setter* et renvoyer une erreur à l'utilisateur de la classe s'il n'a pas envoyé le bon type (ou la bonne valeur dans l'intervalle imposé).

Regardons à quoi pourrait ressembler une telle stratégie en Python :
```python
1 class Citron:
2     def __init__(self, couleur="jaune", masse=0):
3         self.couleur = couleur
4         self.masse = masse  # masse en g
5
6     def get_couleur(self):
7         return self.couleur
8
9     def set_couleur(self, value):
10         self.couleur = value
11
12     def get_masse(self):
13         return self.masse
14
15     def set_masse(self, value):
16         if value < 0:
17             raise ValueError("Z'avez déjà vu une masse négative ?")
18         self.masse = value
19
20
21 if __name__ == "__main__":
22     # définition de citron1
23     citron1 = Citron()
24     print(citron1.get_couleur(), citron1.get_masse())
25     # on change les attributs de citron1 avec les setters
26     citron1.set_couleur("jaune foncé")
27     citron1.set_masse(100)
28     print(citron1.get_couleur(), citron1.get_masse())
```

Lignes 6 à 10. On définit deux méthodes *getters* pour accéder à chaque attribut.

Lignes 12 à 18. On définit deux méthodes *setters* pour modifier chaque attribut. Notez qu'en ligne 16 nous testons si la masse est négative, si tel est le cas nous générons une erreur avec le mot-clé *raise* (voir le chapitre 26 *Remarques complémentaires* (en ligne)). Ceci représente un des avantages des *setters* : contrôler la validité des attributs (on pourrait aussi vérifier qu'il s'agit d'un entier, etc.).

Lignes 22 à 28. Après instanciation, on affiche la valeur des attributs avec les deux fonctions *getters*, puis on les modifie avec les *setters* et on les affiche à nouveau.

L'exécution de ce code donnera la sortie suivante :

```
jaune 0
jaune foncé 100
```

Si on avait mis `citron1.set_masse(-100)` en ligne 26, la sortie aurait été la suivante :

```
1 jaune 0
2 Traceback (most recent call last):
3     File "getter_setter.py", line 27, in <module>
4         citron1.set_masse(-100)
5     File "getter_setter.py", line 17, in set_masse
6         raise ValueError("Z'avez déjà vu une masse négative ???")
7 ValueError: Z'avez déjà vu une masse négative ???
```

La fonction interne *raise* nous a permis de générer une erreur, car l'utilisateur de la classe (c'est-à-dire nous dans le programme principal) n'a pas rentré une valeur correcte.

On comprend bien l'utilité d'une stratégie avec des *getters* et *setters* dans cet exemple. Toutefois, en Python, on peut très bien accéder et modifier les attributs même si on a des *getters* et des *setters* dans la classe. Imaginons la même classe *Citron* que ci-dessus, mais on utilise le programme principal suivant (notez que nous avons simplement ajouté les lignes 9 à 12 ci-dessous) :
```python
1 if __name__ == "__main__":
2     # définition de citron1
3     citron1 = Citron()
4     print(citron1.get_couleur(), citron1.get_masse())
5     # on change les attributs de citron1 avec les setters
6     citron1.set_couleur("jaune foncé")
7     citron1.set_masse(100)
8     print(citron1.get_couleur(), citron1.get_masse())
9     # on les rechange sans les setters
10    citron1.couleur = "pourpre profond"
11    citron1.masse = -15
12    print(citron1.get_couleur(), citron1.get_masse())
```

Cela donnera la sortie suivante :

```
jaune 0
jaune foncé 100
pourpre profond -15
```

Malgré la présence des getters et des setters, nous avons réussi à accéder et à modifier la valeur des attributs. De plus, nous avons pu mettre une valeur aberrante (masse négative) sans que cela ne génère une erreur !

Vous vous posez sans doute la question : mais dans ce cas, quel est l'intérêt de mettre des getters et des setters en Python ? La réponse est très simple : cette stratégie n'est pas une manière « pythonique » d'opérer (voir le chapitre 16 *Bonnes pratiques en programmation Python* pour la définition de « pythonique »). En Python, la lisibilité est la priorité. Souvenez-vous du Zen de Python « *Readability counts* » (voir le chapitre 16).

De manière générale, une syntaxe avec des getters et setters du côté client surcharge la lecture. Imaginons que l'on ait une instance nommée `obj` et que l'on souhaite faire la somme de ses trois attributs `x`, `y` et `z` :

```python
1 # pythonique
2 obj.x + obj.y + obj.z
3
4 # non pythonique
5 obj.get_x() + obj.get_y() + obj.get_z()
```

La méthode pythonique est plus « douce » à lire, on parle aussi de *syntactic sugar* ou littéralement en français « *sucre syntaxique* » . De plus, à l'intérieur de la classe, il faut définir un *getter* et un *setter* pour chaque attribut, ce qui multiple les lignes de code.

Très bien. Donc en Python, on n'utilise pas comme dans les autres langages orientés objet les getters et les setters ? Mais, tout de même, cela avait l'air une bonne idée de pouvoir contrôler comment un utilisateur de la classe interagit avec certains attributs (par exemple, rentre-t-il une bonne valeur ?). N'existe-t-il pas un moyen de faire ça en Python ? La réponse est : bien sûr il existe un moyen pythonique, la classe *property*. Nous allons voir cette nouvelle classe dans la prochaine rubrique et nous vous dirons comment opérer systématiquement pour accéder, modifier, voire détruire, chaque attribut d'instance de votre classe.

## 24.6.2 La solution : la classe *property*

Dans la rubrique précédente, on vient de voir que les getters et setters traditionnels rencontrés dans d'autres langages orientés objet ne représentent pas une pratique pythonique. En Python, pour des raisons de lisibilité, il faudra dans la mesure du possible conserver une syntaxe `instance.attrIBUT` pour l'accès aux attributs d'instance, et une syntaxe `instance.attrIBUT = nouvelle_valeur` pour les modifier.

Toutefois, si on souhaite contrôler l'accès, la modification (voire la destruction) de certains attributs stratégiques, Python met en place une classe nommée *property*. Celle-ci permet de combiner le maintien de la syntaxe lisible `instance.attrIBUT`, tout en utilisant en filigrane des fonctions pour accéder, modifier, voire détruire l'attribut (à l'image des getters et setters évoqués ci-dessus, ainsi que des deleters ou encore destructeurs en français). Pour faire cela, on utilise la fonction Python interne `property()` qui crée un objet (ou instance) *property* :

```python
1 attribut = property(fget=accesseur, fset=mutateur, fdel=destructeur)
```

Les arguments passés à `property()` sont systématiquement des méthodes dites *callback*, c'est-à-dire des noms de méthodes que l'on a définies précédemment dans notre classe, mais on ne précise ni argument, ni parenthèse, ni self
(voir le chapitre 25 *Fenêtres graphiques et Tkinter* (en ligne)). Avec cette ligne de code, attribut est un objet de type *property* qui fonctionne de la manière suivante à l'extérieur de la classe :

* L'instruction `instance.attrribut` appellera la méthode `accesseur()`.

* L'instruction `instance.attrribut = valeur` appellera la méthode `.mutateur()`.

* L'instruction `del instance.attrribut` appellera la méthode `.destructeur()`.

L'objet attribut est de type *property*, et la vraie valeur de l'attribut est stockée par Python dans une variable d'instance qui s'appellera par exemple `__attribut` (même nom, mais commençant par un underscore unique, envoyant un message à l'utilisateur qu'il s'agit d'une variable associée au comportement interne de la classe).

Comment cela fonctionne-t-il concrètement dans un code ? Regardons cet exemple (nous avons mis des `print()` un peu partout pour bien comprendre ce qui se passe) :

```python
1 class Citron:
2     def __init__(self, masse=0):
3         print("(2) J'arrive dans le __init__()")
4         self.masse = masse
5
6     def get_masse(self):
7         print("Coucou je suis dans le get")
8         return self._masse
9
10    def set_masse(self, valeur):
11        print("Coucou je suis dans le set")
12        if valeur < 0:
13            raise ValueError("Un citron ne peut pas avoir")
14            " de masse négative !")
15        self._masse = valeur
16
17    masse = property(fget=get_masse, fset/set_masse)
18
19
20 if __name__ == "__main__":
21     print("(1) Je suis dans le programme principal, ")
22     "je vais instancier un Citron")
23     citron = Citron(masse=100)
24     print("(3) Je reviens dans le programme principal")
25     print(f"La masse de notre citron est {citron.masse} g")
26     # On mange le citron.
27     citron.masse = 25
28     print(f"La masse de notre citron est {citron.masse} g")
29     print(citron.__dict__)
```

Pour une fois, nous allons commenter les lignes dans le désordre :

Ligne 17. Il s'agit de la commande clé pour mettre en place le système : `masse` devient ici un objet de type *property* (si on regarde son contenu avec une syntaxe `NomClasse.attrribut_property`, donc ici `Citron.masse`, Python nous renverra quelque chose de ce style : `<property object at 0x7fd3615aeef8>`). Qu'est-ce que cela signifie ? Et bien la prochaine fois qu'on voudra accéder au contenu de cet attribut `.masse`, Python appellera la méthode `get_masse()`, et quand on voudra le modifier, Python appellera la méthode `set_masse()` (ceci sera valable de l'intérieur ou de l'extérieur de la classe). Comme il n'y a pas de méthode destructeur (passée avec l'argument `fdel`), on ne pourra pas détruire cet attribut : un del `c.masse` conduirait à une erreur de ce type : `AttributeError: can't delete attribute`.

Ligne 4. Si vous avez bien suivi, cette commande `self.masse = masse` dans le constructeur va appeler automatiquement la méthode `set_masse()`. Attention, dans cette commande, la variable `masse` à droite du signe = est une variable locale passée en argument. Par contre, `self.masse` sera l'objet de type *property*. Si vous avez bien lu la rubrique *Différence entre les attributs de classe et d'instance*, l'objet `masse` créé en ligne 16 est un attribut de classe, on peut donc y accéder avec une syntaxe `self.masse` au sein d'une méthode.

### Conseil

Notez bien l'utilisation de `self.masse` dans le constructeur (en ligne 4) plutôt que `self._masse`. Comme `self.masse` appelle la méthode `set_masse()`, cela permet de contrôler si la valeur est correcte dès l'instanciation. C'est
donc une pratique que nous vous recommandons. Si on avait utilisé `self._masse`, il n'y aurait pas eu d'appel à la fonction mutateur et on aurait pu mettre n'importe quoi, y compris une valeur aberrante, lors de l'instanciation.

Lignes 6 à 15. Dans les méthodes accesseur et mutateur, on utilise la variable `self._masse` qui contiendra la vraie valeur de la masse du citron (cela serait vrai pour tout autre objet de type `property`).

Attention

Dans les méthodes accesseur et mutateur, il ne faut surtout pas utiliser `self.masse` à la place de `self._masse`. Pourquoi ? Par exemple, dans l'accesseur, si on met `self.masse` cela signifie que l'on souhaite accéder à la valeur de l'attribut (comme dans le constructeur !). Ainsi, Python rappellera l'accesseur et retombera sur `self.masse`, ce qui rappellera l'accesseur et ainsi de suite : vous l'aurez compris, cela partira dans une récursion infinie et mènera à une erreur du type `RecursionError: maximum recursion depth exceeded`. Cela serait vrai aussi si vous aviez une fonction destructeur, il faudrait utiliser `self._masse`.

L'exécution de ce code donnera :

(1) Dans le programme principal, je vais instancier un Citron
(2) J'arrive dans le `__init__()`
Coucou je suis dans le set
(3) Je reviens dans le programme principal
Coucou je suis dans le get
La masse de notre citron est 100 g
Coucou je suis dans le set
Coucou je suis dans le get
La masse de notre citron est 25 g
`{'_masse': 25}`

Cette exécution montre qu'à chaque appel de `self.masse` ou `citron.masse` on va utiliser les méthodes accesseur ou mutateur. La dernière commande qui affiche le contenu de `citron.__dict__` montre que la vraie valeur de l'attribut est stockée dans la variable d'instance `._masse` (`instance._masse` de l'extérieur et `self._masse` de l'intérieur).

### 24.6.3 Une meilleure solution : les décorateurs @property, @attribut.setter et @attribut.deleter

Nous venons de voir les objets `property` pour contrôler l'accès, la mutation et la suppression d'attributs. Toutefois la syntaxe est relativement lourde. Afin de la simplifier, une manière plus pythonique (sucre syntaxique) est d'utiliser un décorateur. La syntaxe pour décorer une fonction est la suivante :

1. `@decorateur`

2. `def fonction():`

3. `[...]`

La ligne 1 précise que `fonction()` va être modifiée par une autre fonction nommée `decorateur()`. Le symbole `@` en ligne 1 attend un nom de fonction qui sera la fonction décoratrice. Pour plus de détails sur comment les décorateurs fonctionnent, vous pouvez consulter le chapitre 26 *Remarques complémentaires* où une rubrique leur est consacrée. Ici, nous avons juste à savoir qu'un décorateur est une fonction qui modifie le comportement d'une autre fonction.

En reprenant l'exemple vu dans la rubrique précédente, voici comment on peut l'écrire avec des décorateurs :
```python
1 class Citron:
2     def __init__(self, masse=0):
3         print(f"(2) J'arrive dans le __init__() , je vais mettre la masse = {masse}")
4         self.masse = masse
5
6     @property
7     def masse(self):
8         print("Coucou je suis dans le getter")
9         return self._masse
10
11     @masse.setter
12     def masse(self, valeur):
13         print("Coucou je suis dans le setter")
14         if valeur < 0:
15             raise ValueError("Un citron ne peut pas avoir")
16             " de masse négative !")
17         self._masse = valeur
18
19     @masse.deleter
20     def masse(self):
21         print("Coucou, je suis dans le deleter")
22         del self._masse
```

On voit que la syntaxe est plus lisible que celle de la rubrique précédente. Examinons les différences. La première chose est que les méthodes getter (ligne 7), setter (ligne 11) et deleter (ligne 19) s'appellent toutes .masse(), masse étant le nom de notre objet property. Comme dans la syntaxe de la rubrique précédente, la masse réelle se trouve dans un attribut nommée _masse pour ne pas confondre avec notre objet property. Afin de comprendre ce qu'il se passe, nous vous avons concocté le programme principal suivant avec des print() un peu partout :

```python
1 if __name__ == "__main__":
2     print("(1) Je suis dans le programme principal et ")
3     "je vais instancier un Citron")
4 print()
5 citron = Citron(masse=100)
6 print()
7 print("(3) Je reviens dans le programme principal, je vais afficher ")
8     "la masse du citron")
9 print(f"La masse de notre citron est {citron.masse} g")
10 print()
11 # On mange une partie du citron.
12 print("(4)Je suis dans le prog principal ")
13     "et je vais changer la masse du citron")
14 citron.masse = 25
15 print()
16 print(f"(5) Je suis dans le prog principal, je vais afficher ")
17     "la masse du citron")
18 print(f"La nouvelle masse de notre citron est {citron.masse} g")
19 print(f"L'attribut citron.__dict__ m'indique bien le nom réel ")
20     f"de l'attribut contenant la masse :")
21 print(citron.__dict__)
22 print()
23 # On mange la fin du citron.
24 print(f"(6) Je suis dans le prog principal, ")
25     f"je détruis l'attribut .masse")
26 del citron.masse
27 print(f"Ainsi, citron.__dict__ est maintenant vide :")
28 print(citron.__dict__)
```

L'exécution donnera la sortie suivante :
(1) Je suis dans le programme principal et je vais instancier un Citron

(2) J'arrive dans le `.__init__()`, je vais mettre la masse = 100
Coucou je suis dans le setter

(3) Je reviens dans le programme principal, je vais afficher la masse du citron
Coucou je suis dans le getter
La masse de notre citron est 100 g

(4) Je suis dans le prog principal et je vais changer la masse du citron
Coucou je suis dans le setter

(5) Je suis dans le prog principal, je vais afficher la masse du citron
Coucou je suis dans le getter
La nouvelle masse de notre citron est 25 g
L'attribut citron.__dict__ m'indique bien le nom réel de l'attribut contenant la masse :
{'_masse': 25}

(6) Je suis dans le prog principal, je détruis l'attribut .masse
Coucou, je suis dans le deleter
Ainsi, citron.__dict__ est maintenant vide :
{}

Examinez bien les phrases `Coucou je suis dans [...]` et essayez de comprendre pourquoi elles apparaissent. Bien que nos trois méthodes soient définies comme `def masse()`, vous pouvez constater qu'elles sont appelées lorsque on invoque `citron.masse`, `citron.masse = 25` ou del `citron.masse` (à l'intérieur de la classe, ce serait `self.masse`, `self.masse = 25` ou del `self.masse`). Autrement dit, on n'utilise jamais la syntaxe `.masse()`. Ceci est justement dû au fait que `.masse` est un objet de type `property`.

### Conseil

Lorsque vous souhaitez créer des objets `property`, nous vous conseillons la syntaxe pythonique `@property`, `@nom_attrIBUT setter` et `@nom_attrIBUT.deleter` plutôt que celle de la rubrique précédente avec la ligne `masse = property(fget,get_masse, fset/set_masse, fdel=del_masse)`. Cette syntaxe améliore grandement la lisibilité.

## 24.6.4 Le décorateur @property seul

Une méthode décorée avec `@property` peut être utile seule sans avoir le setter et/ou le deleter correspondant(s). On rencontre cela lorsqu'on souhaite créer un « d'attribut dynamique » plutôt qu'avoir un appel de méthode explicite. Regardons un exemple :

```python
1 class ADN:
2     def __init__(self):
3         self.Sequence = []
4
5     def __repr__(self):
6         return f"La séquence de mon brin d'ADN est {self.Sequence}"
7
8     def ajoute_base(self, nom_base):
9         self.Sequence.append(nom_base)
10
11 @property
12 def len(self):
13     return len(self.Sequence)
```

Voici un exemple :
```python
>>> brin_adn = ADN()
>>> brin_adn.ajoute_base("A")
>>> brin_adn.ajoute_base("T")
>>> brin_adn
La séquence de mon brin d'ADN est ['A', 'T']
>>> brin_adn.len
2
>>> brin_adn.ajoute_atome("G")
>>> brin_adn
La séquence de mon brin d'ADN est ['A', 'T', 'G']
>>> brin_adn.len
3
```

Lorsqu'on utilise l'attribut `brin_adn.len`, ceci invoque finalement l'appel de l'objet `property len` qui, in fine, est une méthode. Ainsi, la valeur renvoyée sera calculée à chaque fois, bien que dans la syntaxe on n'a pas une notation `.methode()`, mais plutôt `.attribut`. Voilà pourquoi nous avons parlé d'attribut dynamique. Cela permet d'alléger la syntaxe quand il n'y a pas spécifiquement d'arguments à passer à la méthode qui se trouve derrière cet attribut.

## 24.7 Bonnes pratiques pour construire et manipuler ses classes

Nous allons voir dans cette rubrique certaines pratiques que nous vous recommandons lorsque vous construisez vos propres classes.

### 24.7.1 L'accès aux attributs

On a vu dans la rubrique *Accès et modifications des attributs depuis l'extérieur* que nous avions le moyen de contrôler cet accès avec la classe *property*. Toutefois, cela peut parfois alourdir inutilement le code, ce qui va à l'encontre de certains préceptes de la PEP 20 comme « *Sparse is better than dense* », « *Readability counts* », etc. (voir le chapitre 16 *Bonnes pratiques en programmation Python*).

**Conseil**

Si on souhaite contrôler ce que fait le client de la classe pour certains attributs « délicats » ou « stratégiques », on peut utiliser la classe *property*. Toutefois, nous vous conseillons de ne l'utiliser que lorsque cela se révèle vraiment nécessaire, donc avec parcimonie. Le but étant de ne pas surcharger le code inutilement. Cela va dans le sens des recommandations des développeurs de Python (comme décrit dans la PEP8).

Les objets *property* ont deux avantages principaux :

* ils permettent de garder une lisibilité du côté client avec une syntaxe `instance.attrribut`;

* même si un jour vous décidez de modifier votre classe et de mettre en place un contrôle d'accès à certains attributs avec des objets *property*, cela ne changera rien du côté client. Ce dernier utilisera toujours `instance.attrribut` ou `instance.attrribut = valeur`. Tout cela contribuera à une meilleure maintenance du code client utilisant votre classe.

Certains détracteurs disent qu'il est parfois difficile de déterminer qu'un attribut est contrôlé avec un objet *property*. La réponse à cela est simple, dites-le clairement dans la documentation de votre classe via les *docstrings* (voir la rubrique *ci-dessous*).

### 24.7.2 Note sur les attributs publics et non publics

Certains langages orientés objet mettent en place des attributs dits *privés* dont l'accès est impossible de l'extérieur de la classe. Ceux-ci existent afin d'éviter qu'un client n'aille perturber ou casser quelque chose dans la classe. Les arguments auxquels l'utilisateur a accès sont dits *publics*.

**Attention**
En Python, il n'existe pas d'attributs privés comme dans d'autres langages orientés objet. L'utilisateur a accès à tous les attributs quels qu'ils soient, même s'ils contiennent un ou plusieurs caractère(s) underscore(s) (voir ci-dessous) !

Au lieu de ça, on parle en Python d'attributs publics et non publics.

## Définition

En Python les attributs non publics sont des attributs dont le nom commence par un ou deux caractère(s) underscore. Par exemple, `_attribut`, ou `__attribut`.

La présence des *underscores* dans les noms d'attributs est un signe clair que le client ne doit pas y toucher. Toutefois, cela n'est qu'une convention, et comme dit ci-dessus le client peut tout de même modifier ces attributs.

Par exemple, reprenons la classe `Citron` de la rubrique précédente dont l'attribut `.masse` est contrôlé avec un objet `property` :

```python
1 >>> citron = Citron()
2 Coucou je suis dans le set
3 >>> citron.masse
4 Coucou je suis dans le get
5 0
6 >>> citron.masse = -16
7 Coucou je suis dans le set
8 Traceback (most recent call last):
9     File "<stdin>", line 1, in <module>
10  File "<stdin>", line 11, in set_masse
11  ValueError: Un citron ne peut pas avoir de masse négative !
12 >>> citron.masse = 16
13 Coucou je suis dans le set
14 >>> citron.masse
15 Coucou je suis dans le get
16 16
17 >>> citron._masse
18 16
19 >>> citron._masse = -8364
20 >>> citron.masse
21 Coucou je suis dans le get
22 -8364
23 >>>
```

Malgré l'objet `property`, nous avons pu modifier l'attribut non public `._masse` directement !

Il existe également des attributs dont le nom commence par deux caractères *underscores*. Nous n'avons encore jamais croisé ce genre d'attribut. Ces derniers mettent en place le *name mangling*.

## Définition

Le *name mangling*<fnref n="13" />, ou encore substantypage ou déformation de nom en français, correspond à un mécanisme de changement du nom d'un attribut selon si on est à l'intérieur ou à l'extérieur d'une classe.

Regardons un exemple :

<footnote n="13">13. https://en.wikipedia.org/wiki/Name_mangling</footnote>
```python
1 class Citron:
2     def __init__(self):
3         self.__mass = 100
4
5     def get_mass(self):
6         return self.__mass
7
8
9     if __name__ == "__main__":
10        citron1 = Citron()
11        print(citron1.get大人)
12        print(citron1.__mass)
```

Ce code va donner la sortie suivante :

```
1   100
2 Traceback (most recent call last):
3     File "mangling.py", line 12, in <module>
4       print(citron1.__mass)
5       ^^^^^^^^^^^^^^^^^^
6 AttributeError: 'Citron' object has no attribute '__mass'
```

La ligne 12 du code a donc conduit à une erreur : Python prétend ne pas connaître l'attribut `.__mass`. On pourrait croire que cela constitue un mécanisme de protection des attributs. En fait il n'en est rien, car on va voir que l'attribut est toujours accessible et modifiable. Si on modifiait le programme principal comme suit :

```
1   if __name__ == "__main__":
2       citron1 = Citron()
3       print(citron1.__dict__)
```

On obtiendrait en sortie le dictionnaire `{'_Citron__mass': 100}`.

Le *name mangling* est donc un mécanisme qui transforme le nom `self.__attribut` à l'intérieur de la classe en `instance.__NomClasse__attribut` à l'extérieur de la classe. Ce mécanisme a été conçu initialement pour pouvoir retrouver des noms d'attributs identiques lors de l'héritage. Si par exemple une classe mère et une classe fille ont chacune un attribut nommé `__attribut`, le *name mangling* permet d'éviter les conflits de nom. Par exemple :

```python
1 class Fruit:
2     def __init__(self):
3         self.__mass = 100
4
5
6 class Citron(Fruit):
7     def __init__(self):
8         Fruit.__init__(self)
9         self.__mass = 200
10
11     def print masses(self):
12         print(self._Fruit__mass)
13         print(self.__mass)
14
15
16     if __name__ == "__main__":
17         citron1 = Citron()
18         citron1.print大人
```

Ce code affiche 100 puis 200. La ligne 12 a permis d'accéder à l'attribut `.__mass` de la classe mère `Fruit`, et la ligne 13 a permis d'accéder à l'attribut `.__mass` de la classe `Citron`.

Le *name mangling* n'est donc pas un mécanisme de « protection » d'un attribut, il n'a pas été conçu pour ça. Les concepteurs de Python le disent clairement dans la PEP 8 : « Generally, double leading underscores should be used only to avoid name conflicts with attributes in classes designed to be subclassed ».

Donc en Python, on peut tout détruire, même les attributs délicats contenant des underscores. Pourquoi Python permet-il un tel paradoxe ? Selon le concepteur de Python, Guido van Rossum : « We're all consenting adults here », nous sommes ici entre adultes, autrement dit nous savons ce que nous faisons !
### Conseil

En résumé, n'essayez pas de mettre des barrières inutiles vers vos attributs. Cela va à l'encontre de la philosophie Python. Soignez plutôt la documentation et faites confiance aux utilisateurs de votre classe !

### 24.7.3 Classes et docstrings

Les classes peuvent bien sûr contenir des docstrings comme les fonctions et les modules. C'est d'ailleurs une pratique vivement recommandée. Voici un exemple sur notre désormais familière classe Citron :

```python
1 class Citron:
2     """Voici la classe Citron.
3
4     Il s'agit d'une classe assez impressionnante qui crée des objets
citrons.
5
6     Par défaut une instance de Citron contient l'attribut de classe
saveur.
7
8     """
9     saveur = "acide"
10
11    def __init__(self, couleur="jaune", taille="standard"):
12        """Constructeur de la classe Citron.
13
14        Ce constructeur prend deux arguments par mot-clé
couleur et taille."""
15
16        self.couleur = couleur
17        self.taille = taille
18
19    def __str__(self):
20        """Redéfinit le comportement avec print()."""
21        return f"saveur: {saveur}, couleur: {couleur}, taille: {taille}"
22
23    def affiche_coucou(self):
24        """Méthode inutile qui affiche coucou."""
25        print("Coucou !")
```

Si on fait help(Citron) dans l'interpréteur, on obtient :
```python
1 Help on class Citron in module __main__:
2
3 class Citron(builtinsobject)
4 | Citron(couleur='jaune', taille='standard')
5 |
6 | Voici la classe Citron.
7 |
8 | Il s'agit d'une classe assez impressionnante qui crée des objets
citrons.
9 | Par défaut une instance de Citron contient l'attribut de classe
10 | saveur.
11 |
12 |
13 | Methods defined here:
14 |
15 | __init__(self, couleur='jaune', taille='standard')
16 | Constructeur de la classe Citron.
17 |
18 | Ce constructeur prend deux arguments par mot-clé
couleur et taille.
19 |
20 |
21 | __str__(self)
22 | Redéfinit le comportement avec print().
23 |
24 | affiche_coucou(self)
25 | Méthode inutile qui affiche coucou.
26 |
27 | [...]
28 |
29 | Data and other attributes defined here:
30 |
31 | saveur = 'acide'
```

Python formate automatiquement l'aide comme il le fait avec les modules (voir chapitre 15 Création de modules). Comme nous l'avons dit dans le chapitre 16 Bonnes pratiques en programmation Python, n'oubliez pas que les docstrings sont destinées aux utilisateurs de votre classe. Elles doivent donc contenir tout ce dont un utilisateur a besoin pour comprendre ce que fait la classe et comment l'utiliser.

Notez que si on instancie la classe `citron1 = Citron()` et qu'on invoque l'aide sur l'instance `help(citron1)`, on obtient la même page d'aide. Comme pour les modules, si on invoque l'aide pour une méthode de la classe `help(citron1.affiche_coucou)`, on obtient l'aide pour cette méthode seulement.

Toutes les docstrings d'une classe sont en fait stockées dans un attribut spécial nommé `instance.__doc__`. Cet attribut est une chaîne de caractères contenant la docstring générale de la classe. Ceci est également vrai pour les modules, méthodes et fonctions. Si on reprend notre exemple ci-dessus :

```python
1 >>> citron1 = Citron()
2 >>> print(citron1.__doc__)
3 Voici la classe Citron.
4
5 Il s'agit d'une classe assez impressionnante qui crée des objets
citrons.
6
7 Par défaut une instance de Citron contient l'attribut de classe
saveur.
8
9
10 >>> print(citron1.affiche_coucou.__doc__)
11 Méthode inutile qui affiche coucou.
```

L'attribut `.__doc__` est automatiquement créé par Python au moment de la mise en mémoire de la classe (ou module, méthode, fonction, etc.).

### 24.7.4 Autres bonnes pratiques

Voici quelques points en vrac auxquels nous vous conseillons de faire attention :

* Une classe ne se conçoit pas sans méthode. Si on a besoin d'une structure de données séquentielles ou si on veut donner des noms aux variables (plutôt qu'un indice), utilisez plutôt les dictionnaires. Une bonne alternative peut être les `namedtuples` (voir la rubrique suivante).
1 >>> citron[0]
2 10
3 >>> citron[3]
4 'ellipsoide'
5 >>> citron.masse = 100
6 Traceback (most recent call last):
7     File "<stdin>", line 1, in <module>
8 AttributeError: can't set attribute
9 >>> for elt in citron:
10    ...     print(elt)
11 ...
12 10
13 jaune
14 acide
15 ellipsoide

Lignes 1 à 4. On peut retrouver les attributs également par indice.

Lignes 5 à 8. Les attributs / éléments sont non modifiables !

Lignes 9 à 15. Les `namedtuples` sont itérables.

Un `namedtuple` est non modifiable, mais on peut en générer un nouveau avec la méthode `._replace()`, à l'image de la méthode `replace()` pour les chaînes de caractères :

1 >>> citron._replace(masse=30)
2 Citron(masse=30, couleur='jaune', saveur='acide', forme='ellipsoide')
3 >>> citron
4 Citron(masse=10, couleur='jaune', saveur='acide', forme='ellipsoide')
5 >>> citron = citron._replace(masse=30)
6 >>> citron
7 Citron(masse=30, couleur='jaune', saveur='acide', forme='ellipsoide')

Lignes 1 et 2. On crée un nouveau `namedtuples` avec la méthode `._replace()`. Notez qu'il faut passer un (ou plusieurs) argument(s) par mot-clé à cette méthode désignant les attributs à modifier.

Lignes 3 et 4. L'objet initial `citron` est intact puisqu'un `namedtuples` est non modifiable.

Lignes 5 à 7. En ré-affectant ce que renvoie la méthode `._replace()` dans un objet de même nom `citron`, on peut faire évoluer son contenu comme on a pu le faire avec les chaînes de caractères.

Enfin, il est possible de convertir un `namedtuple` en dictionnaire (ordonné) avec la méthode `._asdict()` :

1 >>> citron._asdict()
2 OrderedDict([('masse', 10), ('couleur', 'jaune'), ('saveur', 'acide'), ('forme', 'ellipsoide')])

Quand utiliser les `namedtuples` ? Vous souvenez-vous de la différence entre les listes et les dictionnaires ? Ici, c'est un peu la même chose entre les `tuples` et les `namedtuples`. Les `namedtuples` permettent de créer un code plus lisible en remplaçant des numéros d'indice par des noms. Le fait qu'ils soient non modifiables peut aussi avoir un avantage par rapport à l'intégrité des données. Si vous trouvez les `namedtuples` limités, sachez que vous pouvez créer votre propre classe qui hérite d'un `namedtuple` afin de lui ajouter de nouvelles méthodes « maison ».

Pour aller plus loin

Pour aller plus loin, vous pouvez consulter le très bon article<fnref n="15" /> de Dan Bader.

## 24.8 Note finale de sémantique

Jusqu'à présent, lorsque nous avons évoqué les outils pour créer ou convertir des objets Python tels que `int()`, `list()`, `range()`, etc., nous avons toujours parlé de fonctions. Ceci parce-que nous avions une syntaxe `fonction()`, c'est-à-dire `fonction` suivie de parenthèses `()`. Toutefois, vous vous êtes peut-être déjà demandé pourquoi Python indiquait `class lorsqu'on tapait le nom de ces fonctions dans l'interpréteur (ou en invoquant help())` :

<footnote n="15">15. https://dbader.org/blog/writing-clean-python-with-namedtuples</footnote>
1 >>> int

2 <class 'int'>

3 >>> list

4 <class 'list'>

5 >>> range

6 <class 'range'>

7 >>> property

8 <class 'property'>

Et bien, c'est parce-que ce sont bel et bien des classes ! Donc, lorsqu'on invoque par exemple `list1 = list()`, on crée finalement une instance de la classe `list`. Python ne met pas `list` en `CamelCase` car ce sont des classes natives (*built-in classes*). En effet, les auteurs de Python ont décidé que les classes et fonctions natives sont en minuscules, et les exceptions en `CamelCase` (voir ce lien<fnref n="16" />).

Finalement, la création d'une instance à partir d'une classe ou l'appel d'une fonction possède la même syntaxe `mot_clé()` :

1 >>> class Citron:

2 ... pass

3 ...

4 >>> Citron()

5 <__main__.Citron object at 0x7fb776308a10>

6 >>> def fct():

7 ... return "Coucou"

8 ...

9 >>> fct()

10 'Coucou'

On peut le voir aussi quand on invoque l'aide sur un de ces outils, par exemple `help(int)` :

Help on class int in module builtins:

class int(object)
| int([x]) -> integer
| int(x, base=10) -> integer
[...]

Il est bien précise que `int` est une classe.

Si on prend des fonctions natives (*built-in functions*) de Python comme `len()` ou `sorted()`, l'interpréteur nous confirme bien qu'il s'agit de fonctions :

1 >>> len

2 <built-in function len>

3 >>> sorted

4 <built-in function sorted>

Par conséquent, d'un point de vue purement sémantique nous devrions parler de classe plutôt que de fonction pour les instructions comme `list()`, `range()`, etc. Toutefois, nous avons décidé de garder le nom fonction pour ne pas compliquer les premiers chapitres de ce cours.

## 24.9 Exercices

**Conseil**

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

### 24.9.1 Classe molécule

Pour illustrer le mécanisme de la composition en POO, on se propose de créer un programme `molecule.py` qui permettra de décrire une molécule en utilisant les classes. Nous allons créer une classe représentant une molécule (qui sera notre classe `Composite`) et celle-ci contiendra des instances d'une classe décrivant un atome (classe `Component`).

<footnote n="16">16. https://peps.python.org/pep-0008/#class-names</footnote>
Après les import nécessaires, le programme contiendra une constante donnant les masses des atomes sous forme de dictionnaire : `ATOM_MASSES` = `{"C": 12.0, "O": 16.0, "H": 1.0}`.

Créer une classe `Atom` en vous inspirant des exercices du chapitre 23 *Avoir la classe avec les objets*. Cette classe devra instancier des objets contenant les attributs d'instance suivants :

* nom d'atome (par exemple `C1`)

* type d'atome (une seule lettre, déduit du nom d'atome, par exemple `C`)

* coordonnée `x`

* coordonnée `y`

* coordonnée `z`

Le nom d'atome et coordonnées cartésiennes seront passés au constructeur.

Ajouter les méthodes `calc_distance()`, `calc_com()` (center of mass). Ajouter une méthode `mute_atom(name)` qui change le nom de l'atome, où `name` est un nouveau nom d'atome (par exemple `O1`). Cette méthode changera également l'attribut d'instance décrivant le type d'atome.

Créer une classe `Molecule` qui construit les attributs d'instance : - Nom de la molécule - Une liste d'atomes (vide à l'instanciation) : `listAtoms` - Une liste indiquant la connectivité (la liste des atomes connectés, vide à l'instanciation) : `listConnectivity`

Le constructeur prendra en argument seulement le nom de la molécule.

Créer une méthode `add_atom.atom()` qui vérifie si l'argument passé est bien une instance de la classe `Atom`, et qui ajoute `atom` dans la liste d'atomes.

Créer une autre méthode `build_mlc_from_pdb(filename)` qui prend en argument un nom de fichier `pdb`. La méthode lit le fichier `pdb`, et pour chaque atome `lu`, crée une instance de la classe `Atom`, et ajouter celle-ci à `listAtoms`.

Ajouter une méthode `calc CulMass()` qui calcule et renvoie masse de la molécule.

Créer une méthode `calc_com()` qui cette fois-ci calcule et renvoie le centre de masse de la molécule entière.

Ajouter la méthode `calc_connectivity()` qui calcule et renvoie une liste décrivant la connectivité entre les atomes. Deux atomes sont considérés connectés s'il y a une liaison covalente entre eux, on peut pour cela calculer la distance entre eux qui doit être inférieure à 1.6 Å. La liste de connectivité pourra être construite dans ce style : `[("C1", "H1"), ("C1", "C2"), ...]`.

Chaque paire d'atome doit apparaître une seule fois (pas de `[("C1", "H1")`, `[("H1", "C1")`, ...].

Créer une méthode spéciale affichant les caractéristiques de la molécule lorsqu'on utilise `print()` avec une instance de cette classe `Molecule`, par exemple `print(benzene)`. Cette méthode pourra par exemple afficher avant d'avoir créé la molécule :

```
Molecule benzene
No atom for the moment
No connectivity for the moment
```

Ou bien, lorsque la molécule est créée et la connectivité déterminée, elle s'affichera comme ceci :

```
Molecule benzene
atom C1, type C, mass = 12.0 amu, coor(-2.145, 0.973, -0.003)
atom H1, type H, mass = 1.0 amu, coor(-3.103, 0.460, -0.005)
[...]
Connectivity
C1 connected to H1
C1 connected to C2
[...]
```

Pour lancer le programme dans un premier temps, vous pourrez instancier une molécule benzene, puis y ajouter les atomes :

1. `if __name__ == "__main__":`

2. `benzene = Molecule("benzene")`

3. `print(benzene)`

4. `benzene.build_mlc_from_pdb("benzene.pdb")`

5. `print(benzene)`

Dans un deuxième temps, le programme principal calculera la masse et le centre de masse de benzene et les affichera. Muter ensuite l'atome `H1` en `O1` et recalculer la masse et le centre de masse et les afficher.
Pour aller plus loin, vous pouvez ajouter une méthode qui calcule et affiche un graphe de la molécule avec le module `networkx`<fnref n="17" />. La page de tutorial<fnref n="18" /> pourra vous être utile.

Par exemple :

<figure><img src="image_57.png" /><figcaption>FIGURE 24.4 – Graphe représentant une molécule de benzène.</figcaption></figure>

<footnote n="17">17. https://networkx.org/</footnote>

<footnote n="18">18. https://networkx.org/documentation/latest/tutorial.html#drawing-graphs</footnote>
# CHAPITRE 25

## Fenêtres graphiques et Tkinter

**Conseil**

Dans ce chapitre, nous allons utiliser des classes, nous vous conseillons de bien relire les chapitres 23 *Avoir la classe avec les objets* et 24 *Avoir plus la classe avec les objets* (en ligne). Par ailleurs, nous vous conseillons de relire également la rubrique *Arguments positionnels et arguments par mot-clé* du chapitre 10 sur les fonctions.

### 25.1 Utilité d'une GUI

Dans votre carrière « pythonesque » il se peut que vous soyez amené à vouloir développer une application graphique, on parle encore de *graphical user interface* ou GUI. Jusqu'à maintenant, vous avez fait en sorte qu'un utilisateur interagisse avec votre code via la ligne de commande, par exemple :

```
$ python mon_script.py file.gbk blabla blublu
```

Les arguments passés à la ligne de commande sont tout à fait classiques dans le monde de la bioinformatique. Toutefois, il se peut que vous développiez un programme pour une communauté plus large, qui n'a pas forcément l'habitude d'utiliser un shell et la ligne de commande. Une GUI permettra un usage plus large de votre programme, il est donc intéressant de regarder comment s'y prendre. Dans notre exemple ci-dessus on pourrait par exemple développer une interface où l'utilisateur choisirait le nom du fichier d'entrée par l'intermédiaire d'une boîte de dialogue, et de contrôler les options en cliquant sur des boutons, ou des « listes de choix ». Une telle GUI pourrait ressembler à la figure 25.1.

Au delà de l'aspect convivial pour l'utilisateur, vous pourrez, avec une GUI, construire des fenêtres illustrant des éléments que votre programme génère à la volée. Ainsi, vous « verrez » ce qui se passe de manière explicite et en direct ! Par exemple, si on réalise une simulation de particules, on a envie de voir un « film » des particules en mouvement, c'est-à-dire comment ces particules bougent au fur et à mesure que les pas de simulation avacent. Une GUI vous permettra une telle prouesse ! Enfin, sachez que certains logiciels scientifiques ont été développés avec la bibliothèque graphique Tk (par exemple pymol, vmd, etc.). Qui sait, peut-être serez-vous le prochain développeur d'un outil incontournable ?

Il existe beaucoup de modules pour construire des applications graphiques. Par exemple : Tkinter<fnref n="1" />, wxpython<fnref n="2" />, PyQt<fnref n="3" />,

<footnote n="1">1. https://wiki.python.org/moin/TkInter</footnote>

<footnote n="2">2. http://www.wxpython.org/</footnote>

<footnote n="3">3. https://pyqt.readthedocs.io</footnote>
<figure><img src="image_58.png" /><figcaption>FIGURE 25.1 – Exemple de GUI.</figcaption></figure>

PyGObject<fnref n="4" />, etc. Nous présentons dans ce chapitre le module `Tkinter` qui est présent de base dans les distributions Python (pas besoin *a priori* de faire d'installation de module externe). `Tkinter` permet de piloter la bibliothèque graphique Tk (Tool Kit), `Tkinter` signifiant `tk interface`. On pourra noter que cette bibliothèque Tk peut être également pilotée par d'autres langages (Tcl, perl, etc.).

## 25.2 Quelques concepts liés à la programmation graphique

Lorsque l'on développe une GUI, nous créons une fenêtre graphique contenant notre application, ainsi que des widgets inclus dans la fenêtre.

### Définition

Les widgets (*window gadget*) sont des objets graphiques permettant à l'utilisateur d'interagir avec votre programme Python de manière conviviale. Par exemple, dans la fenêtre sur la figure 25.1, les boutons, les listes de choix, ou encore la zone de texte sont des widgets.

L'utilisation d'une GUI va amener une nouvelle manière d'aborder le déroulement d'un programme, il s'agit de la programmation dite « événementielle ». Jusqu'à maintenant vous avez programmé « linéairement », c'est-à-dire que les instructions du programme principal s'enchaînaient les unes derrière les autres (avec bien sûr de possibles appels à des fonctions). Avec une GUI, l'exécution est décidée par l'utilisateur en fonction de ses interactions avec les différents widgets. Comme c'est l'utilisateur qui décide quand et où il clique dans l'interface, il va falloir mettre en place ce qu'on appelle un « gestionnaire d'événements ».

### Définition

Le gestionnaire d'événements est une sorte de « boucle infinie » qui est à l'affût de la moindre action de la part de l'utilisateur. C'est lui qui effectuera une action lors de l'interaction de l'utilisateur avec chaque widget de la GUI. Ainsi, l'exécution du programme sera réellement guidée par les actions de l'utilisateur.

La bibliothèque Tk que nous piloterons avec le module Python `Tkinter` propose tous les éléments cités ci-dessus (fe-nêtre graphique, widgets, gestionnaire d'événements). Nous aurons cependant besoin d'une dernière notion : les fonctions callback.

### Définition

<footnote n="4">4. https://pygobject.readthedocs.io/en/latest/</footnote>
Une fonction *callback* est une fonction passée en argument d'une autre fonction.

Un exemple de fonction *callback* est présenté dans la rubrique suivante.

## 25.3 Notion de fonction callback

**Conseil**

Si vous êtes débutant, vous pouvez sauter cette rubrique.

Jusqu'à maintenant nous avons toujours appelé les fonctions ou les méthodes de cette manière :

1 var = fct(arg1, arg2)

2

3 obj.methode(arg)

où les arguments étaient des objets « classiques » (par exemple une chaîne de caractères, un entier, un *float*, etc.). Sachez qu'il est possible de passer en argument une fonction à une autre fonction ! Par exemple :

1 def fct_callback(arg):

2 print(f"J'aime bien les {arg} !")

3

4

5 def une_fct(ma_callback):

6 print("Je suis au début de une_fct(), ")

7 "et je vais exécuter la fonction callback :")

8 ma_callback("fraises")

9 print("une_fct() se termine.")

10

11 if __name__ == "__main__":

12 une_fct(fct_callback)

Si on exécute ce code, on obtient :

```
Je suis au début de une_fct() et je vais exécuter la fonction callback :
J'aime bien les fraises !
une_fct() se termine.
```

Vous voyez que dans le programme principal, lors de l'appel de une_fct(), on lui passe comme argument une autre fonction mais sans **aucune parenthèses ni argument**, c'est-à-dire fct_callback tout court. En d'autres termes, cela est différent de
une_fct(fct_callback("scoubidous")).

Dans une telle construction, fct_callback("scoubidous") serait d'abord évaluée, puis ce serait la valeur renvoyée par cet appel qui serait passée à une_fct() (n'essayez pas sur notre exemple car cela mènerait à une erreur !). Que se passe-t-il en filigrane lors de l'appel une_fct(fct_callback)? Python passe une référence vers la fonction fct_callback (en réalité il s'agit d'un pointeur, mais tout ceci est géré par Python et est transparent pour l'utilisateur). Vous souvenez-vous ce qui se passait avec une liste passée en argument à une fonction (voir le chapitre 13 *Plus sur les fonctions*) ? C'était la même chose, une référence était envoyée plutôt qu'une copie. *Python Tutor*<fnref n="5" /> nous confirme cela (cf. figure 25.2).

Lorsqu'on est dans une_fct() on pourra utiliser bien sûr des arguments lors de l'appel de notre fonction callback si on le souhaite. Notez enfin que dans une_fct() la fonction callback reçue en argument peut avoir un nom différent (comme pour tout type de variable).

À quoi cela sert-il ? À première vue cette construction peut sembler ardue et inutile. Toutefois, vous verrez que dans le module `Tkinter` les fonctions `callback` sont incontournables. En effet, on utilise cette construction pour lancer une fonction lors de l'interaction de l'utilisateur avec un `widget`: par exemple, lorsque l'utilisateur clique sur un bouton et qu'on souhaite lancer une fonction particulière suite à ce clic. Notez enfin que nous les avons déjà croisées avec le tri de

<footnote n="5">5. http://pythontutor.com</footnote>
<figure><img src="image_59.png" /><figcaption>FIGURE 25.2 – Exemple de fonction callback dans Python Tutor.</figcaption></figure>

dictionnaire par valeur (avec une syntaxe `sorted(dico, key=dico.get)`), voir le chapitre 14 Conteneurs) ainsi que les objets property (voir le chapitre 23 Avoir la classe avec les objets).

## 25.4 Prise en main du module Tkinter

Le module `Tkinter` est très vaste. Notre but n'est pas de vous faire un cours exhaustif mais plutôt de vous montrer quelques pistes. Pour apprendre à piloter ce module, nous pensons qu'il est intéressant de vous montrer des exemples. Nous allons donc en présenter quelques-uns qui pourraient vous être utiles, à vous ensuite de consulter de la documentation supplémentaire si vous souhaitez aller plus loin (cf. la rubrique *Bibliographie pour aller plus loin*).

### 25.4.1 Un premier exemple dans l'interpréteur

Commençons par construire un script qui affichera une simple fenêtre avec un message et un bouton. Regardons d'abord comment faire dans l'interpréteur (nous vous conseillons de tester ligne par ligne ce code tout en lisant les commentaires ci-dessous) :

```python
1 >>> import tkinter as tk
2 >>> racine = tk.Tk()
3 >>> label = tk.Label(racine, text="J'adore Python !")
4 >>> bouton = tk Button(racine, text="Quitter", fg="red",
5                         command=racine destroy)
6 >>> label pack()
7 >>> bouton .pack()
8 >>>
```

Ligne 2. On crée la fenêtre principale (vous la verrez apparaître !). Pour cela, on crée une instance de la classe `tk.Tk` dans la variable `racine`. Tous les `widgets` que l'on créera ensuite seront des fils de cette fenêtre. On pourra d'ailleurs noter que cette classe `tk.Tk` ne s'instancie en général qu'une seule fois par programme. Vous pouvez, par curiosité, lancer une commande `dir(racine)` ou `help(racine)`, vous verrez ainsi les très nombreuses méthodes et attributs associés à un tel objet `Tk`.

Ligne 3. On crée un *label*, c'est-à-dire une zone dans la fenêtre principale où on écrit un texte. Pour cela, on a créé une variable *label* qui est une instance de la classe `tk.Label`. Cette variable *label* contient donc notre *widget*, nous la réutiliserons plus tard (par exemple pour placer ce *widget* dans la fenêtre). Notez le premier argument `positionnel` *racine* passé à la classe `tk.Label`, celui-ci indique la fenêtre parente où doit être dessinée le *label*. Cet argument doit toujours être passé en premier et il est vivement conseillé de le préciser. Nous avons passé un autre argument avec le nom *text* pour indiquer, comme vous l'avez deviné, le texte que nous souhaitons voir dans ce *label*. La classe `tk.Label` peut recevoir de nombreux autres arguments, en voici la liste exhaustive<fnref n="6" />. Dans les fonctions `Tkinter` qui construisent un *widget*, les arguments possibles pour la mise en forme de celui-ci sont nombreux, si bien qu'ils sont toujours des arguments par mot-clé. Si on ne précise pas un de ces arguments lors de la création du *widget*, l'argument prendra alors une valeur par défaut. Cette liste des arguments par mot-clé est tellement longue qu'en général on ne les précisera

<footnote n="6">6. http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/label.html</footnote>
pas tous. Heureusement, Python autorise l'utilisation des arguments par mot-clé dans un ordre quelconque. Comme nous l'avons vu dans le chapitre 10 *Fonctions*, souvenez vous que leur utilisation dans le désordre implique qu'il faudra toujours préciser leur nom : par exemple vous écrirez `text="blabla"` et non pas `"blabla"` tout court.

Ligne 4. De même, on crée un bouton « Quitter » qui provoquera la fermeture de la fenêtre et donc l'arrêt de l'application si on clique dessus. À nouveau, on passe la fenêtre parente en premier argument, le texte à écrire dans le bouton, puis la couleur de ce texte. Le dernier argument `command=racine destroy` va indiquer la fonction / méthode à exécuter lorsque l'utilisateur clique sur le bouton. On pourra noter que l'instance de la fenêtre mère `tk.Tk` (que nous avons nommée `racine`) possède une méthode `.destroy()` qui va détruire le widget sur lequel elle s'applique. Comme on tue la fenêtre principale (que l'on peut considérer comme un widget contenant d'autres widgets), tous les widgets fils seront détruits et donc l'application s'arrêtera. Vous voyez par ailleurs que cette méthode `racine destroy` est passée à l'argument `command= sans parenthèses ni arguments`: il s'agit donc d'une fonction *callback* comme expliqué ci-dessus. Dans tous les widgets Tkinter, on doit passer à l'argument `command=...` une fonction / méthode *callback*. La liste exhaustive des arguments possibles de la classe `tk.button` se trouve ici[^7].

Lignes 6 et 7. Vous avez noté que lors de la création de ce *label* et de ce bouton, rien ne s'est passé dans la fenêtre. C'est normal, ces deux widgets existent bien, mais il faut maintenant les placer à l'intérieur de la fenêtre. On appelle pour ça la méthode `.pack()`, avec une notation objet `widget.pack()`: à ce moment précis, vous verrez votre *label* apparaître ainsi que la fenêtre qui se redimensionne automatiquement en s'adaptant à la grandeur de votre *label*. L'invocation de la même méthode pour le bouton va faire apparaître celui-ci juste en dessous du *label* et redimensionner la fenêtre. Vous l'aurez compris la méthode `.pack()` place les widgets les uns en dessous des autres et ajuste la taille de la fenêtre. On verra plus bas que l'on peut passer des arguments à cette méthode pour placer les widgets différemment (en haut, à droite, à gauche).

Au final, vous devez obtenir une fenêtre comme sur la figure 25.3.

## 25.4.2 Le même exemple dans un script.

Tentons maintenant de faire la même chose dans un script `tk_exemple.py` :

```python
1 import tkinter as tk
2
3 racine = tk.Tk()
4 label = tk.Label(racine, text="J'adore Python !")
5 bouton = tk Button(racine, text="Quitter", command=racine.quit)
6 bouton["fg"] = "red"
7 label.pack()
8 bouton.pack()
9 racine.mainloop()
10 print("C'est fini !")
```

puis lançons ce script depuis un shell :

```
$ python tk_exemple.py
```

Vous voyez maintenant la même fenêtre avec les mêmes fonctionnalités par rapport à la version dans l'interpréteur (voir la figure 25.3). Nous commentons ici les différences (dans le désordre) :

Ligne 6. Le bouton a été créé en ligne 5, mais on voit qu'il est possible de préciser une option de rendu du widget après cette création (ici on met le texte en rouge avec l'option `"fg"`). La notation ressemble à celle d'un dictionnaire avec une syntaxe générale `widget["option"] = valeur`.

Ligne 9. L'instruction `racine.mainloop()` va lancer le gestionnaire d'événements que nous avons évoqué ci-dessus. C'est lui qui interceptera la moindre action de l'utilisateur, et qui lancera les portions de code associées à chacune de ses actions. Bien sûr, comme nous développerons dans ce qui va suivre toutes nos applications Tkinter dans des scripts (et non pas dans l'interpréteur), cette ligne sera systématiquement présente. Elle sera souvent à la fin du script, puisque, à l'image de ce script, on écrit d'abord le code construisant l'interface, et on lance le gestionnaire d'événements une fois l'interface complètement décrite, ce qui lancera au final l'application.

Ligne 10. Cette ligne ne s'exécute qu'après l'arrêt de l'application (soit en cliquant sur le bouton « Quitter », soit en cliquant sur la croix).

Ligne 5. Pour quitter l'application, on utilise ici la méthode `.quit()`. Celle-ci casse la `.mainloop()` et arrête ainsi

<footnote n="7">7. http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/button.html</footnote>
le gestionnaire d'événements. Cela mène à l'arrêt de l'application. Dans le premier exemple dans l'interpréteur, on avait utilisé la méthode `.destroy()` sur la fenêtre principale. Comme son nom l'indique, celle-ci détruit la fenêtre principale et mène aussi à l'arrêt de l'application. Cette méthode aurait donc également fonctionné ici. Par contre, la méthode `.quit()` n'aurait pas fonctionné dans l'interpréteur car, comme on l'a vu, la boucle `.mainloop()` n'y est pas présente. Comme nous écrirons systématiquement nos applications Tkinter dans des scripts, et que la boucle `.mainloop()` y est obligatoire, vous pourrez utiliser au choix `.quit()` ou `.destroy()` pour quitter l'application.

<figure><img src="image_60.png" /><figcaption>FIGURE 25.3 – Exemple basique de fenêtre Tkinter.</figcaption></figure>

## 25.5 Construire une application Tkinter avec une classe

De manière générale, il est vivement conseillé de développer ses applications Tkinter en utilisant une classe. Cela présente l'avantage d'encapsuler l'application de manière efficace et d'éviter ainsi l'utilisation de variables globales. Souvenez-vous, elles sont à bannir définitivement ! Une classe crée un espace de noms propre à votre application, et toutes les variables nécessaires seront ainsi des attributs de cette classe. Reprenons notre petit exemple avec un label et un bouton :

```python
1 import tkinter as tk
2
3 class Applicationrac Tk:
4     def __init__(self):
5         tk.Tk.__init__(self)
6         self.creer_widgots()
7
8     def creer_widgots(self):
9         self.label = tk.Label(self, text="J'adore Python !")
10        self.bouton = tk.button(self, text="Quitter", command =self.quit)
11        self.label.pack()
12        self.bouton.pack()
13
14
15 if __name__ == "__main__":
16     app = Application()
17     app.title("Ma Première App :-")
18     app.mainloop()
```

Ligne 3. On crée notre application en tant que classe. Notez que cette classe porte un nom qui commence par une majuscule (comme recommandé dans les bonnes pratiques de la PEP8⁸, voir le chapitre 16 *Bonnes pratiques en programmation Python*). L'argument passé dans les parenthèses indique que notre classe *Application* hérite de la classe `tk.Tk`. Par ce mécanisme, nous héritons ainsi de toutes les méthodes et attributs de cette classe mère, mais nous pouvons en outre en ajouter de nouvelles/nouveaux (on parle aussi de « redéfinition » de la classe `tk.Tk`)!

Ligne 4. On crée un constructeur, c'est-à-dire une méthode qui sera exécutée lors de l'instanciation de notre classe (à la ligne 16).

Ligne 5. On appelle ici le constructeur de la classe mère `tk.Tk.__init__()`. Pourquoi fait-on cela ? On se souvient dans la version linéaire de l'application, on avait utilisé une instanciation classique : `racine = tk.Tk()`. Ici, l'effet de l'appel du constructeur de la classe mère permet d'instancier la fenêtre Tk dans la variable `self` directement. C'est-à-dire que la prochaine fois que l'on aura besoin de cette instance (lors de la création des *widgets* par exemple, cf. lignes 9 et 10), on utilisera directement `self` plutôt que `racine` ou tout autre nom donné à l'instance. Comme vu dans le chapitre 23 *Avoir la classe avec les objets*, appeler le constructeur de la classe mère est une pratique classique lorsqu'une classe hérite d'une autre classe.

<footnote n="8">8. https://www.python.org/dev/peps/pep-0008/</footnote>
Ligne 6. On appelle la méthode `self.creer widgets()` de notre classe `Application`. Pour rappel, le self avant
le `.creer widgets()` indique qu'il s'agit d'une méthode de notre classe (et non pas d'une fonction classique).

Ligne 8. La méthode `.creer widgets()` va créer des `widgets` dans l'application.

Ligne 9. On crée un label en instanciant la classe `tk.Label()`. Notez que le premier argument passé est maintenant self (au lieu de `racine précédemment`) indiquant la fenêtre dans laquelle sera construit ce `widget`.

Ligne 10. De même on crée un `widget` bouton en instanciant la classe `tk.button()`. Là aussi, l'appel à la méthode `quit()` se fait par `self.quit` puisque la fenêtre est instanciée dans la variable self. Par ailleurs, on ne met ni parenthèses ni arguments à `self.quit` car il s'agit d'une fonction *callback* (comme dans la rubrique précédente).

Lignes 11 et 12. On place les deux `widgets` dans la fenêtre avec la méthode `pack()`.

Ligne 15. Ici on autorise le lancement de notre application `Tkinter` en ligne de commande (`python tk_application.py`), ou bien de réutiliser notre classe en important `tk_application.py` en tant que module (`import tk_application`) (voir le chapitre 15 *Création de modules*).

Ligne 16. On instancie notre application.

Ligne 17. On donne un titre dans la fenêtre de notre application. Comme on utilise de petits `widgets` avec la méthode `pack()`, il se peut que le titre ne soit pas visible lors du lancement de l'application. Toutefois, si on « étire » la fenêtre à la souris, le titre le deviendra. On pourra noter que cette méthode `title()` est héritée de la classe mère `Tk`.

Ligne 18. On lance le gestionnaire d'événements.

Au final, vous obtiendrez le même rendu que précédemment (cf. figure 25.3). Alors vous pourrez-vous poser la question, « pourquoi ai-je besoin de toute cette structure alors que le code précédent semblait plus direct ? ». La réponse est simple, lorsqu'un projet de GUI grossit, le code devient très vite illisible s'il n'est pas organisé en classe. De plus, la non-utilisation de classe rend quasi-obligatoire l'utilisation de variables globales, ce qui on l'a vu, est à proscire définitivement ! Dans la suite du chapitre, nous verrons quelques exemples qui illustrent cela (cf. la rubrique suivante).

## 25.6 Le widget canvas

### 25.6.1 Un canvas simple et le système de coordonnées

Le *widget canvas*<fnref n="9" /> de `Tkinter` est très puissant. Il permet de dessiner des formes diverses (lignes, cercles, etc.), et même de les animer !

La classe `tk Canvas` crée un *widget canvas* (ou encore *canevas* en français). Cela va créer une zone (*i.e.* le canevas en tant que tel) dans laquelle nous allons dessiner divers objets tels que des ellipses, lignes, polygones, etc., ou encore insérer du texte ou des images. Regardons tout d'abord un code minimal qui construit un *widget canvas*, dans lequel on y dessine un cercle et deux lignes :

```python
1 import tkinter as tk
2
3 racine = tk.Tk()
4 canv = tk Canvas(racine, bg="white", height=200, width=200)
5 canv.pack()
6 canv.create_oval(0, 0, 200, 200, outline="red", width=10)
7 canv.create_line(0, 0, 200, 200, fill="black", width=10)
8 canv.create_line(0, 200, 200, 0, fill="black", width=10)
9 racine mainloop()
```

Ligne 4. On voit qu'il faut d'abord créer le *widget canvas*, comme d'habitude en lui passant l'instance de la fenêtre principale en tant qu'argument positionnel, puis les options. Notons que nous lui passons comme options la hauteur et la largeur du *canvas*. Même s'il s'agit d'arguments par mot-clé, donc optionnels, c'est une bonne pratique de les préciser. En effet, les valeurs par défaut risqueraient de nous mener à dessiner hors de la zone visible (cela ne génère pas d'erreur mais n'a guère d'intérêt).

Ligne 6 à 8. Nous dessinons maintenant des objets graphiques à l'intérieur du canevas avec les méthodes `create_oval()` (dessine une ellipse) et `create_line()` (dessine une ligne). Les arguments positionnels sont les coordonnées de l'ellipse (les deux points englobant l'ellipse, cf. ce lien<fnref n="10" /> pour la définition exacte) ou de la ligne. Ensuite, on passe comme d'habitude des arguments par mot-clé (vous commencez à avoir l'habitude !) pour mettre en forme ces objets graphiques.

<footnote n="9">9. http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/canvas.html</footnote>

<footnote n="10">10. http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/create_oval.html</footnote>
Le rendu de l'image est montré dans la figure 25.4 ainsi que le système de coordonnées associé au *canvas*. Comme dans la plupart des bibliothèques graphiques, l'origine du repère du *canvas* (i.e. la coordonnée (0,0)) est en haut à gauche. Les *x* vont de gauche à droite, et les *y* vont de haut en bas.

<figure><img src="image_61.png" /><figcaption>FIGURE 25.4 – Exemple 1 de *canvas* avec le système de coordonnées. Le système de coordonnées est montré en vert et n'apparaît pas sur la vraie fenêtre Tkinter.</figcaption></figure>

**Attention**

L'axe des *y* est inversé par rapport à ce que l'on représente en mathématique. Si on souhaite représenter une fonction mathématique (ou tout autre objet dans un repère régi par un repère mathématique), il faudra faire un changement de repère.

### 25.6.2 Un *canvas* encapsulé dans une classe

Voici un exemple un peu plus conséquent d'utilisation du *widget canvas* qui est inclus dans une classe. Il s'agit d'une application dans laquelle il y a une zone de dessin, un bouton dessinant des cercles, un autre des lignes et un dernier bouton qui quitte l'application (figure 25.5).

Le code suivant crée une telle application :
<figure><img src="image_62.png" /><figcaption>FIGURE 25.5 – Exemple 2 de canvas.</figcaption></figure>

```python
1 import tkinter as tk
2 import random as rd
3
4 class AppliCanevas.tk.Tk):
5     def __init__(self):
6         tk.Tk.__init__(self)
7         self.size = 500
8         self.creer_widgets()
9
10 def creer_widgets(self):
11     # création canevas
12     self.canv = tk CANvas(self, bg="light gray", height self.size,
13                           width self.size)
14     self.canv.pack(side = tk LEFT)
15     # boutons
16     self.bouton_cercles = tk Button(self, text="Cercle !",
17                                     command = self.dessine_cercles)
18     self.bouton_cercles.pack(side = tk TOP)
19     self.bouton_lignes = tk Button(self, text="Lignes !",
20                                     command = self.dessine_lignes)
21     self.bouton_lignes.pack()
22     self.bouton_quitter = tk Button(self, text="Quitter",
23                                     command = self.quit)
24     self.bouton_quitter.pack(side = tk BOTTOM)
25
26 def rd_col(self):
27     return rd.choice(("black", "red", "green", "blue", "yellow", "magenta",
28                       "cyan", "white", "purple"))
29
30 def dessine_cercles(self):
31     for i in range(20):
32         x, y = [rd .randint(1, self.size) for j in range(2)]
33         diameter = rd .randint(1, 50)
34         self.canv.create_oval(x, y, x+diameter, y+diameter,
35                               fill = self.rd_col())
36
Cours de Python / Université Paris Cité / UFR Sciences du Vivant
37 def dessine_lignes(self):
38     for i in range(20):
39         x, y, x2, y2 = [rd .randint(1, self.size) for j in range(4)]
```
Lignes 4 à 6. Comme montré dans la rubrique *Construire une application Tkinter avec une classe*, notre classe `AppliCanevas` hérite de la classe générale `tk.Tk` et la fenêtre `Tk` se retrouve dans la variable `self`.

Ligne 7. On crée un attribut de la classe `self.size` qui contiendra la taille (hauteur et largeur) du `canvas`. On rappelle que cet attribut sera visible dans l'ensemble de la classe puisqu'il est « accroché » à celle-ci par le `self`.

Ligne 8. On lance la méthode `creer widgets()` (qui est elle aussi « accrochée » à la classe par le `self`).

Lignes 12 à 14. On crée un `widget canvas` en instanciant la classe `tk Canvas`. On place ensuite le `canvas` dans la fenêtre avec la méthode `pack()` en lui précisant où le placer avec la variable `Tkinter` `tk LEFT`.

Lignes 15 à 24. On crée des `widgets` boutons et on les place dans la fenêtre. À noter que chacun de ces `widgets` appelle une méthode différente, dont deux que nous avons créées dans la classe `(.dessine_cercle()` et `.dessine_lignes()`).

Ligne 26 à 28. Cette méthode renvoie une couleur au hasard sous forme de chaîne de caractères.

Lignes 30 à 40. On définit deux méthodes qui vont dessiner des paquets de 20 cercles (cas spécial d'une ellipse) ou 20 lignes aléatoires. Lors de la création de ces cercles et lignes, on ne les récupère pas dans une variable `car` on ne souhaite ni les réutiliser ni changer leurs propriétés par la suite. Vous pourrez noter ici l'avantage de programmer avec une classe, le `canvas` est directement accessible dans n'importe quelle méthode de la classe avec `self.canv` (pas besoin de le passer en argument ou de créer une variable globale).

### 25.6.3 Un `canvas` animé dans une classe

Dans ce dernier exemple, nous allons illustrer la puissance du `widget canvas` en vous montrant que l'on peut animer les objets se trouvant à l'intérieur. Nous allons également découvrir une technique intéressante, à savoir, comment « intercepter » des clics de souris générés ou des touches pressées par l'utilisateur. L'application consiste en une « baballe » qui se déplace dans la fenêtre et dont on contrôle les propriétés à la souris (cf. figure 25.6). Vous pouvez télécharger le script ici<fnref n="11" />.

<footnote n="11">11. https://python.sdv.u-paris.fr/data-files/tk_baballe.py</footnote>
1 """Super appli baballe !""
2
3 Usage: python tk_baballe.py
4 - clic gauche: faire grossir la baballe
5 - clic droit: faire rétrécir la baballe
6 - clic central: relance la baballe (depuis le point du clic)
7 dans une direction aléatoire
8 - touche Esc: quitte l'appli baballe
9 """
10
11 import tkinter as tk
12 import random as rd
13
14 class AppliBaballe (tk.Tk):
15 def __init__ (self):
16 """Constructeur de l'application."""
17 tk.Tk.__init__ (self)
18 # Coord baballe.
19 self.x, self.y = 200, 200
20 # Rayon baballe.
21 self.size = 50
22 # Pas de deplacement.
23 self(dx, self.dy = 20, 20
24 # Création et packing du canvas.
25 self.canv = tk CANv self, bg='light gray', height=400, width=400)
26 self.canv.pack()
27 # Création de la baballe.
28 self.baballe = self.canv.create_oval (self.x, self.y,
29 self.x+self.size,
30 self.y+self.size,
31 width=2, fill="blue")
32 # Binding des actions.
33 self.canv.bind( "<Button-1>", self .incr)
34 self.canv.bind( "<Button-2>", self .boom)
35 self.canv.bind( "<Button-3>", self .decr)
36 self.bind( "<Escape>", self .stop)
37 # Lancer la baballe.
38 self.move()
39
40 def move (self):
41 """Déplace la baballe (appelée itérativement avec la méthode after)."""
42 # Incrémente coord baballe.
43 self.x += self .dx
44 self.y += self .dy
45 # Vérifier que la baballe ne sort pas du canvas (choc élastique).
46 if self.x < 10:
47 self .dx = abs(self .dx)
48 if self.x > 400 - self .size-10:
49 self .dx = -abs(self .dx)
50 if self.y < 10:
51 self .dy = abs(self .dy)
52 if self.y > 400 - self .size-10:
53 self .dy = -abs(self .dy)
54 # Mise à jour des coord.
55 self.canv self.baballe, self.x, self.y, self.x+self.size,
56 self.y+self.size)
57 # Rappel de move toutes les 50ms.
58 self .after(50, self .move)
59
60 def boom (self, mclick):
61 """Relance la baballe dans une direction aléatoire au point du clic."""
62 self.x = mclick.x
63 self.y = mclick.y
64 self .canv .create_text (self.x, self.y, text="Boom !", fill="red")
65 self .dx = rd.choice([-30, -20, -10, 10, 20, 30])
66 self .dy = rd.choice([-30, -20, -10, 10, 20, 30])
67
68 def incr (self, lclick):
69 """Augmente la taille de la baballe."""
70 self.size += 10
71 if self.size > 200:
72 self.size = 200
73
74 def decr (self, rclick):
75 """Diminue la taille de la baballe."""
76 self.size -= 10
77 if self.size < 10:
Lignes 19 à 23. Les coordonnées de la baballe, ses pas de déplacement, et sa taille sont créés en tant qu'attributs de notre classe. Ainsi ils seront visibles partout dans la classe.

Lignes 25 à 31. Le `canvas` est ensuite créé et placé dans la fenêtre, puis on définit notre fameuse baballe. À noter, les coordonnées `self.x` et `self.y` de la baballe représentent en fait son côté « nord-ouest » (en haut à gauche, voir le point $(x_0, y_0)$ dans la documentation officielle<fnref n="12" />).

Lignes 33 à 35. Jusqu'à maintenant, nous avons utilisé des événements provenant de clics sur des boutons. Ici, on va « intercepter » des événements générés par des clics de souris sur le `canvas` et les lier à une fonction / méthode (comme nous l'avions fait pour les clics sur des boutons avec l'option `command=...`). La méthode pour faire cela est `bind()`, voilà pourquoi on parle de *event binding* en anglais. Cette méthode prend en argument le type d'événement à capturer en tant que chaîne de caractères avec un format spécial : par exemple `"<Button-1>"` correspond à un clic gauche de la souris (de même `"<Button-2>"` et `"<Button-3>"` correspondent aux clics central et droit respectivement). Le deuxième argument de la méthode `bind()` est une méthode / fonction *callback* à appeler lors de la survenue de l'événement (comme pour les clics de bouton, vous vous souvenez ? On l'appelle donc sans parenthèses ni arguments). On notera que tous ces événements sont liés à des clics sur le `canvas`, mais il est possible de capturer des événements de souris sur d'autres types de *widgets*.

Ligne 36. De même, on peut « intercepter » un événement lié à l'appui sur une touche, ici la touche Esc.

Ligne 38. La méthode `move()` est appelée, ainsi l'animation démarrera dès l'exécution du constructeur, donc peu après l'instanciation de notre application (Ligne 86).

Lignes 40 à 58. On définit une méthode `move()` qui va gérer le déplacement de la baballe avec des chocs élastiques sur les parois (et faire en sorte qu'elle ne sorte pas du `canvas`).

Lignes 55 et 56. On utilise la méthode `coords()` de la classe `Canvas`, qui « met à jour » les coordonnées de n'importe quel objet dessiné dans le `canvas` (c'est-à-dire que cela déplace l'objet).

Ligne 58. Ici, on utilise une autre méthode spécifique des objets `Tkinter`. La méthode `after()` rappelle une autre méthode ou fonction (second argument) après un certain laps de temps (ici 50 ms, passé en premier argument). Ainsi la méthode `move()` se rappelle elle-même, un peu comme une fonction récursive. Toutefois, ce n'est pas une vraie fonction récursive comme celle vue dans le chapitre 13 (exemple du calcul de factorielle), car Python ne conserve pas l'état de la fonction lors de l'appel de `after()`. C'est comme si on avait un `return`, tout l'espace mémoire alloué à la méthode `move()` est détruit lorsque Python rencontre la méthode `after()`. On obtiendrait un résultat similaire avec la boucle suivante :

```python
1 import time
2
3 ...
4
5 while True:
6     move()
7     time.sleep(0.05) # attendre 50 ms
```

Le temps de 50 ms donne 20 images (ou clichés) par seconde. Si vous diminuez ce temps, vous aurez plus d'images par secondes et donc un « film » plus fluide.

Ligne 60 à 66. On définit la méthode `boom()` de notre classe qui on se souvient est appelée lors d'un événement clic central sur le `canvas`. Vous noterez qu'outre le `self`, cette fonction prend un autre argument que nous avons nommé ici `mclick`. Il s'agit d'un objet spécial géré par `Tkinter` qui va nous donner des informations sur l'événement généré par l'utilisateur. Dans les lignes 62 et 63, cet objet `mclick` récupère les coordonnées où le clic a eu lieu grâce aux attributs `mclick.x` et `mclick.y`. Ces coordonnées sont réaffectées à la baballe pour la faire repartir de l'endroit du clic. Nous créons ensuite un petit texte dans le canevas et affectons des valeurs aléatoires aux variables de déplacement pour faire repartir la baballe dans une direction aléatoire.

Lignes 68 à 78. On a ici deux méthodes `incr()` et `decr()` appelées lors d'un clic gauche ou droit. Deux choses sont à noter : i) l'attribut `self.size` est modifié dans les deux fonctions, mais le changement de diamètre de la boule ne sera effectif dans le `canvas` que lors de la prochaine exécution de l'instruction `self.canv coods()` (dans la méthode `move()`); ii) de même que pour la méthode `boom()`, ces deux méthodes prennent un argument après le `self(lclick ou rclick)` récupérant ainsi des informations sur l'événement de l'utilisateur. Même si on ne s'en sert pas, cet argument après le `self` est obligatoire car il est imposé par la méthode `bind()`.

Lignes 80 à 82. Cette méthode quitte l'application lorsque l'utilisateur fait un clic sur la touche Esc.

<footnote n="12">12. http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/create_oval.html</footnote>
<figure><img src="image_63.png" /><figcaption>FIGURE 25.6 – Exemple de canvas animé à deux instants de l'exécution (panneau de gauche : au moment où on effectue un clic central ; panneau de droite : après avoir effectué plusieurs clics gauches).</figcaption></figure>

Il existe de nombreux autres événements que l'on peut capturer et lier à des méthodes / fonctions *callback*. Vous trouverez une liste complète ici<fnref n="13" />.

## 25.7 Pour aller plus loin

### 25.7.1 D'autres widgets

Jusqu'à maintenant nous avons vu les *widgets Button*, *Canvas*, *Label*, mais il en existe bien d'autres. En voici la liste avec une brève explication pour chacun :

* *Checkbutton* : affiche des cases à cocher.

* *Entry* : demande à l'utilisateur de saisir une valeur / une phrase.

* *Listbox* : affiche une liste d'options à choisir (comme dans la figure 25.1).

* *Radiobutton* : implémente des « boutons radio ».

* *Menubutton* et *Menu* : affiche des menus déroulants.

* *Message* : affiche un message sur plusieurs lignes (extensions du *widget Label*).

* *Scale* : affiche une règle graduée pour que l'utilisateur choisisse parmi une échelle de valeurs.

* *Scrollbar* : affiche des ascenseurs (horizontaux et verticaux).

* *Text* : crée une zone de texte dans lequel l'utilisateur peut saisir un texte sur plusieurs lignes (comme dans la figure 25.1).

* *Spinbox* : sélectionne une valeur parmi une liste de valeurs.

* *tkMessageBox* : affiche une boîte avec un message.

Il existe par ailleurs des *widgets* qui peuvent contenir d'autres *widgets* et qui organisent le placement de ces derniers :

* *Frame* : *widget* conteneur pouvant contenir d'autres *widgets* classiques, particulièrement utile lorsqu'on réalise une GUI complexe avec de nombreuses zones.

* *LabelFrame* : comme *Frame* mais affiche aussi un *label* sur le bord.

* *TLEVEL* : pour créer des fenêtres indépendantes.

* *PanedWindow* : conteneur pour d'autres *widgets*, mais ici l'utilisateur peut réajuster les zones affectées à chaque *widget* fils.

Vous trouverez la documentation exhaustive pour tous ces *widgets* (ainsi que ceux que nous avons décrits dans les rubriques précédentes) sur le site de l'Institut des mines et de technologie du Nouveau Mexique <fnref n="14" /> (MNT). Par ailleurs, la

<footnote n="13">13. http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm</footnote>

<footnote n="14">14. http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/index.html</footnote>
page Universal widget methods<fnref n="15" /> vous donnera une vue d'ensemble des différentes méthodes associées à chaque widget.

Il existe également une extension de Tkinter nommée `ttk`, réimplémentant la plupart des widgets de base de Tkinter et qui en propose de nouveaux (Combobox, Notebook, Progressbar, Separator, Sizegrip et Treeview). Typiquement, si vous utilisez `ttk`, nous vous conseillons d'utiliser les widgets `ttk` en priorité, et pour ceux qui n'existent pas dans `ttk`, ceux de Tkinter (comme Canvas qui n'existe que dans Tkinter). Vous pouvez importer le sous-module `ttk` de cette manière : `import tkinter.ttk` as `ttk`.

Vous pourrez alors utiliser les classes de widget de `ttk` (par exemple `ttk.button`, etc.). Si vous souhaitez importer `ttk` et `Tkinter`, il suffit d'utiliser ces deux lignes :

1. `import Tkinter as tk`

2. `import Tkinter.ttk` as `ttk`

Ainsi vous pourrez utiliser des widgets de Tkinter et de `ttk` en même temps.

Pour plus d'informations, vous pouvez consulter la documentation officielle de Python <fnref n="16" />, ainsi que la documentation très complète du site du MNT <fnref n="17" />.

## 25.7.2 Autres pistes à approfondir

Si vous souhaitez aller un peu plus loin en `Tkinter`, voici quelques notions / remarques qui pourraient vous être utiles.

### Conseil

Si vous êtes débutant, vous pouvez sauter cette rubrique.

### 25.7.2.1 Les variables de contrôle

Lorsque vous souhaitez mettre un jour un *widget* avec une certaine valeur (par exemple le texte d'un *label*), vous ne pouvez pas utiliser une variable Python ordinaire, il faudra utiliser une variable `Tkinter` dite de contrôle. Par exemple, si on souhaitait afficher les coordonnées de notre baballe (cf. rubrique précédente) dans un *label*, et que cet affichage se mette à jour au fur et à mesure des mouvements de la baballe, il faudrait utiliser des variables de contrôle. On peut créer de telles variables avec les classes `tk.StringVar` pour les chaînes de caractères, `tk DoubleVar` pour les *floats*, et `tk.IntVar` pour les entiers. Une fois créée, par exemple avec l'instruction `var = tk.StringVar()`, on peut modifier la valeur d'une variable de contrôle avec la méthode `var.set(nouvelle_valeur)` : ceci mettra à jour tous les *widgets* utilisant cette variable `var`. Il existe aussi la méthode `var.get()` qui récupère la valeur actuelle contenue dans `var`. Enfin, il faudra lors de la création du label utiliser l'option `textvariable=` avec votre variable de contrôle (par exemple `tk.Label(..., textvariable=var, ...)`)) pour que cela soit fonctionnel.

À nouveau, vous trouverez une documentation précise sur le site du MNT <fnref n="18" />.

### 25.7.2.2 Autres méthodes de placement des widgets dans la fenêtre Tk

Dans les exemples montrés dans ce chapitre, nous avons systématiquement utiliser la méthode `.pack()` pour placer les *widgets*. Cette méthode très simple et directe « empaquette » les *widgets* les uns contre les autres et redimensionne la fenêtre automatiquement. Avec l'option `side=` et les variables `tk.BOTTOM`, `tk LEFT`, `tk. TOP` et `tk. RIGHT` on place facilement les *widgets* les uns par rapport aux autres. Toutefois, la méthode `.pack()` peut parfois présenter des limites, il existe alors deux autres alternatives.

La méthode `.grid()` permet, grâce à l'utilisation d'une grille, un placement mieux contrôlé des différents *widgets*. La méthode `.place()` place enfin les *widgets* en utilisant les coordonnées de la fenêtre principale. Nous ne développerons pas plus ces méthodes, mais voici de la documentation supplémentaire en accès libre :

* `.pack()` <fnref n="19" />;

<footnote n="15">15. https://infohost.nmt.edu/tcc/help/pubs/tkinter/web/universal.html</footnote>

<footnote n="16">16. https://docs.python.org/3/library/tkinter.ttk.html</footnote>

<footnote n="17">17. http://infohost.nmt.edu/tcc/help/pubs/tkinter/web/ttk.html</footnote>

<footnote n="18">18. https://infohost.nmt.edu/tcc/help/pubs/tkinter/web/control-variables.html</footnote>

<footnote n="19">19. http://effbot.org/tkinterbook/pack.htm</footnote>
* `.grid()`<fnref n="20" />, <fnref n="21" />;

* `.place()`<fnref n="22" />.

### 25.7.2.3 Hériter de la classe Frame pour vos applications ?

Comme illustré dans nos exemples, nous vous recommandons pour vos classes applications `Tkinter` d'hériter de la classe mère `tk.Tk` et d'utiliser le constructeur de la classe mère `tk.Tk.__init__()`. Toutefois, il se peut qu'en consultant d'autres ressources certains auteurs utilisent la technique d'héritage de la classe mère `tk Frame` :

```python
1 import tkinter as tk
2
3 class Application(tk Frame):
4     def __init__(self, racine=None):
5         tk Frame.__init__(self, racine)
6         self.racine = racine
7         self.createWidgets()
8
9     def createWidgets(self):
10         self.label = tk Label(self.racine, text="J'adore Python !")
11         self.bouton = tk Button(self.racine, text="Quitter",
12                                 fg="green", command =self.quit)
13     self.label.pack()
14     self.bouton.pack()
15
16
17 if __name__ == "__main__":
18     racine = tk.Tk()
19     racine.title("Ma Première App :-)")
20     app = Application(racine)
21     racine mainloop()
```

Lignes 17 à 21. Commentons d'abord le programme principal : ici on crée la fenêtre principale dans l'instance `racine` puis on instancie notre classe en passant `racine` en argument.

Lignes 4 et 5. Ici réside la principale différence par rapport à ce que nous vous avons montré dans ce chapitre : en ligne 4 on passe l'argument `racine` à notre constructeur, puis en ligne 5 on passe ce même argument `racine` lors de l'appel du constructeur de la classe `tk Frame` (ce qui était inutile lorsqu'on héritait de la classe `Tk`).

Ligne 6. L'argument `racine` passé à la méthode `__init__()` est finalement une variable locale. Comme il s'agit de l'instance de notre fenêtre principale à passer à tous nos `widgets`, il faut qu'elle soit visible dans toute la classe. La variable `self.racine` est ainsi créée afin d'être réutilisée dans d'autres méthodes.

Vous pourrez vous posez la question : « Pourquoi en ligne 4 l'argument par mot-clé `racine=None` prend la valeur `None` par défaut ? ». Et bien, c'est parce que notre classe `Application` peut s'appeler sans passer d'instance de fenêtre `Tk`. Voici un exemple avec les lignes qui changent seulement (tout le reste est identique au code précédent) :

```python
1 [...]
2 class Application.tk Frame):
3     def __init__(self, racine=None):
4         tk Frame.__init__(self)
5         self.racine = racine
6 [...]
7 [...]
8 if __name__ == "__main__":
9     app = Application()
10     app mainloop()
```

Dans un tel cas, l'argument `racine` prend la valeur par défaut `None` lorsque la méthode `__init__()` de notre classe est exécutée. L'appel au constructeur de la classe `Frame` en ligne 4 instancie automatiquement une fenêtre `Tk` (car cela est strictement obligatoire). Dans la suite du programme, cette instance de la fenêtre principale sera `self.racine` et il n'y aura pas de changement par rapport à la version précédente. Cette méthode reste toutefois peu intuitive car cette instance de la fenêtre principale `self.racine` vaut finalement `None` !

<footnote n="20">20. http://effbot.org/tkinterbook/grid.htm</footnote>

<footnote n="21">21. https://infohost.nmt.edu/tcc/help/pubs/tkinter/web/grid.html</footnote>

<footnote n="22">22. http://effbot.org/tkinterbook/place.htm</footnote>
Hériter de la classe Frame ou de la classe Tk sont deux manières tout à fait valides pour créer des applications Tkinter. Le choix de l'une ou de l'autre relève plus de préférences que l'on acquiert en pratiquant, voire de convictions philosophiques sur la manière de programmer. Toutefois, nous pensons qu'hériter de la classe `tk.Tk` est une manière plus générale et plus compacte : tout ce qui concerne le fenêtrage Tkinter se situera dans votre classe Application, et le programme principal n'aura qu'à instancier l'application et à lancer le gestionnaire d'événements (les choses seront ainsi mieux « partitionnées »). C'est donc la méthode que nous vous recommandons.

### 25.7.2.4 Passage d'arguments avec *args et **kwargs

Si vous allez chercher de la documentation supplémentaire sur `Tkinter`, il se peut que vous tombiez sur ce style de syntaxe lorsque vous créez votre classe contenant l'application graphique :

```python
1 class MonApplication.tk.Tk):
2     def __init__(self, *args, **kwargs):
3         tk.Tk.__init__(self, *args, **kwargs)
4         # ici débute la construction de votre appli
5     [...]
6
7     # programme principal
8     if __name__ == "__main__":
9         [...]
10     app = MonApplication()
11     [...]
```

Les arguments `*args` et `**kwargs` récupèrent facilement tous les arguments « positionnels » et « par mot-clé ». Pour plus de détails sur comment `*args` et `**kwargs` fonctionnent, reportez-vous au chapitre 26 *Remarques complémentaires* (en ligne).

Dans l'exemple ci-dessus, `*args` et `**kwargs` sont inutiles car lors de l'instanciation de notre application, on ne passe aucun argument : `app = MonApplication()`. Toutefois, on pourrait être intéressé à récupérer des arguments passés au constructeur, par exemple :

```python
app = MonApplication(arg1, arg2, option1=val1, option2=val2)
```

Ainsi certains auteurs laissent toujours ces `*args` et `**kwargs` au cas où on en ait besoin dans le futur. Cela est bien utile lorsqu'on distribue notre classe à la communauté et que l'on souhaite que les futurs utilisateurs puissent passer des arguments `Tkinter` au constructeur de notre classe.

Toutefois, même si cela « ne coûte rien », nous vous recommandons de ne pas mettre ces `*args` et `**kwargs` si vous n'en avez pas besoin, comme nous vous l'avons montré dans les exemples de ce chapitre. Rappelons nous de la PEP 20 (voir le chapitre 16 *Bonnes Pratiques en programmation Python*), les assertions « *Simple is better than complex* » ou « *Sparse is better than dense* » nous suggèrent qu'il est inutile d'ajouter des choses dont on ne se sert pas.

### 25.7.2.5 Toujours préciser l'instance de la fenêtre principale

`Tkinter` est parfois surprenant. Dans le code suivant, on pourrait penser que celui-ci n'est pas fonctionnel :

```python
1 >>> import tkinter as tk
2 >>> bouton = tk.button(text="Quitter")
3 >>> bouton.pack()
```

Pour autant, cela fonctionne et on voit un bouton apparaître ! En fait, `Tkinter` va automatiquement instancier la fenêtre principale, si bien qu'il n'est pas obligatoire de passer cette instance en argument d'un `widget`. À ce moment, on peut se demander où est passé cette instance. Heureusement, `Tkinter` garde toujours une filiation des `widgets` avec les attributs `.master` et `.children` :

```python
1 >>> racine = bouton.master
2 >>> racine
3 <tkinter.Tk object .>
4 >>> racine.children
5 {'!button': <tkinter.button object .!button>}
6 >>> bouton["command"] = racine destroyed
```
Ligne 1. On « récupère » l'instance de la fenêtre principale dans la variable `racine`.

Les lignes 4 et 5 montrent que le bouton est un « enfant » de cette dernière.

Enfin, ligne 6, on réassigne la destruction de la fenêtre lorsqu'on clique sur le bouton.

Ces attributs `.master` et `.children` existent pour tous `widgets` et sont bien pratiques lorsqu'on crée de grosses applications graphiques (où on utilise souvent des `widgets` parents contenant d'autres `widgets` enfants). Une autre source d'information sur les `widgets` se trouvent dans les méthodes dont le nom commence par `winfo`. Par exemple, la méthode `.winfo_tLEVEL()` renvoie la même information que l'attribut `.master` (une référence vers le `widget` parent).

### Conseil

Même si cela est possible, nous vous conseillons de systématiquement préciser l'instance de la fenêtre principale lors de la création de vos `widgets`.

### 25.7.2.6 Passage d'arguments à vos fonctions callback

Comme vu dans nos exemples ci-dessus, les fonctions `callback` ne prennent pas d'arguments ce qui peut se révéler parfois limitant. Il existe toutefois une astuce qui utilise les fonctions `lambda`; nous expliquons brièvement les fonctions `lambda` dans le chapitre 26 *Remarques complémentaires* (en ligne). Toutefois, nous ne développons pas leur utilisation avec `Tkinter` et les fonctions `callback` car cela dépasse le cadre de cet ouvrage. Pour de plus amples explications sur cette question, vous pouvez consulter le site `pythonprogramming`<fnref n="23" /> et le livre de Gérard Swinnen<fnref n="24" />.

### 25.7.2.7 Application Tkinter avec plusieurs pages

Dans ce chapitre d'introduction, nous vous avons montré des GUI simples avec une seule page. Toutefois, si votre projet se complexifie, il se peut que vous ayez besoin de créer plusieurs fenêtre différentes. Le livre de Gérard Swinnen<fnref n="25" /> et le site `pythonprogramming`<fnref n="26" /> sont des bonnes sources pour commencer et voir concrètement comment faire cela.

### 25.7.3 Bibliographie pour aller plus loin

Voici quelques ressources que vous pouvez utiliser pour continuer votre apprentissage de `Tkinter` :

1. En anglais :

* La Documentation officielle<fnref n="27" /> de Python.

* Le manuel<fnref n="28" /> de référence sur le site du MNT.

* Le site<fnref n="29" /> de Fredrik Lundh est également très complet.

* Pour avoir un exemple<fnref n="30" /> rapide de code pour chaque `widget`.

* Le livre<fnref n="31" /> de David Love *Learn Tkinter By Example* qui montre des exemples concrets d'applications `Tkinter` de plus en plus complexes (pdf en libre téléchargement).

* Le site<fnref n="32" /> très bien fait de Harisson (avec vidéos!) vous guidera dans la construction d'une GUI complète et complexe avec de nombreuses fonctions avancées (comme par exemple mettre des graphes ` matplotlib` qui se mettent à jour dans la GUI!).

2. En français :

* Le site<fnref n="33" /> bien complet d'Étienne Florent.

* Le livre<fnref n="34" /> de Gérard Swinnen qui montre de nombreux exemples d'applications `tkinter` (pdf en libre téléchargement).

<footnote n="23">23. https://pythonprogramming.net/passing-functions-parameters-tkinter-using-lambda/</footnote>

<footnote n="24">24. https://inforef.be/swi/python.htm</footnote>

<footnote n="25">25. https://inforef.be/swi/python.htm</footnote>

<footnote n="26">26. https://pythonprogramming.net/change-show-new-frame-tkinter/</footnote>

<footnote n="27">27. https://wiki.python.org/moin/TkInter</footnote>

<footnote n="28">28. https://infohost.nmt.edu/tcc/help/pubs/tkinter/web/index.html</footnote>

<footnote n="29">29. http://effbot.org/tkinterbook/</footnote>

<footnote n="30">30. https://www.tutorialspoint.com/python/pythonlection.htm</footnote>

<footnote n="31">31. https://github.com/Dvlv/Tkinter-By-Example</footnote>

<footnote n="32">32. https://pythonprogramming.net/tkinter-depth-tutorial-making-actual-program/</footnote>

<footnote n="33">33. http://tkinter.fdex.eu/index.html</footnote>

<footnote n="34">34. https://inforef.be/swi/python.htm</footnote>
## 25.8 Exercices

**Conseil**

Pour ces exercices, créez des scripts puis exécutez-les dans un shell. Nous vous recommandons de concevoir une classe pour chaque exercice.

### 25.8.1 Application de base

Concevez une application qui affiche l'heure dans un *label* (par exemple `09:10:55`) et qui possède un bouton quitter. L'heure affichée sera celle au moment du lancement de l'application. Pour « attraper » l'heure, vous pourrez utiliser la fonction `strftime()` du module `time`.

### 25.8.2 Horloge

Sur la base de l'application précédente, faites une application qui affiche l'heure dans un *label* en se mettant à jour sur l'heure de l'ordinateur une fois par seconde. Vous concevrez une méthode `mise_a_jour Heure()` qui met à jour l'heure dans le *label* et qui se rappelle elle-même toutes les secondes (n'oubliez pas la méthode `after()`, cf. rubrique *Un canvas animé dans une classe ci-dessus*). Pour cette mise à jour, vous pourrez utiliser la méthode `configure()`, par exemple : `self.label.configure(text=heure)` où heure est une chaîne de caractères représentant l'heure actuelle.

### 25.8.3 Compte à rebours

Créer une application affichant un compte à rebours dans un *label*. L'utilisateur choisira entre 1 et 240 minutes en passant un argument au lancement du script, par exemple : `python tk_compte_a_rebours.py 34` signifiera un compte à rebours de 34' (le programme vérifiera qu'il s'agit d'un entier entre 1 et 240 inclus). Il y aura un bouton « Lancer » pour démarrer le compte à rebours et un bouton « Quitter » au cas où on veuille quitter avant la fin. À la fin du rebours, le programme affichera 10 fois la phrase « C'est fini !!! » dans le *shell* et quittera automatiquement le script. Une image du résultat attendu est montrée dans la figure 25.7.

<figure><img src="image_64.png" /><figcaption>FIGURE 25.7 – Compte à rebours.</figcaption></figure>

### 25.8.4 Triangle de Sierpinski

Le triangle de Sierpinski<sup>35</sup> est une fractale classique. On se propose ici de la dessiner avec un algorithme tiré du jeu du chaos<sup>36</sup>. Celui-ci se décompose en pseudo-code de la façon suivante :

<footnote n="35">35. https://fr.wikipedia.org/wiki/Triangle_de_Sierpinski%C5%84ski</footnote>

<footnote n="36">36. https://fr.wikipedia.org/wiki/Jeu_du_chaos</footnote>
définir les 3 sommets d'un triangle isocèle ou équilatéral
point <- coordonnées (x, y) du centre du trianle
dessiner(point) # un pixel de large
pour i de 0 à 25000:
sommet_tmp <- choisir un sommet du triangle au hasard
point <- calculer(coordonnées(x, y) du centre entre point et sommet_tmp)
dessiner(point)

Le rendu final attendu est montré dans la figure 25.8. On utilisera un canevas de 400x400 pixels. Il y a aura un bouton « Quitter » et un bouton « Launch! » qui calculera et affichera 10000 points supplémentaires dans le triangle de Sierpinski.

<figure><img src="image_65.png" /><figcaption>FIGURE 25.8 – Triangle de Sierpinski.</figcaption></figure>

25.8.5 Polygone de Sierpinski (exercice +++)

Améliorer l'application précédente en proposant une liste de choix supplémentaire demandant à l'utilisateur de choisir
le nombre de sommets (de 3 à 10). Le programme calculera automatiquement la position des sommets. Pour prendre en
main le *widget Listbox*, voici un code minimal qui pourra vous aider. Celui-ci contient une *Listbox* et permet d'afficher
dans le terminal l'élément sélectionné. Nous vous conseillons de bien étudier le code ci-dessous et d'avoir résolu l'exercice
précédent avant de vous lancer !
```python
1 import tkinter as tk
2
3 class MaListbox(tk.Tk):
4     def __init__(self):
5         # Instanciation fenêtre Tk.
6         tk.Tk.__init__(self)
7         self.listbox = tk.Listbox(self, height=10, width=4)
8         self.listbox.pack()
9         # Ajout des items à la listbox (entiers).
10        for i in range(1, 10+1):
11            # Utilisation de ma méthode .insert(index, element)
12            # Ajout de l'entier i (tk.END signifie en dernier).
13            self.listbox.insert(tk.END, i)
14         # Selection du premier élément de listbox.
15         self.listbox.select_set(0)
16         # Liaison d'une méthode quand clic sur listbox.
17         self.listbox.bind("<<ListboxSelect>>", self.clic_listbox)
18
19     def clic_listbox(self, event):
20         # Récupération du widget à partir de l'objet event.
21         widget = event.widget
22         # Récupération du choix sélectionné dans la listbox (tuple).
23         # Par exemple renvoie `(5,)` si on a cliqué sur `5`.
24         selection = widget.curselection()
25         # Récupération du nombre sélectionné (déjà un entier).
26         choix_select = widget.get选型[0])
27         # Affichage.
28         print(f"Le choix sélectionné est {choix_select}",")
29         f"son type est {type(choix_select)}")
30
31
32
33     if __name__ == "__main__":
34         app = MaListbox()
35         app.title("MaListbox")
36         app.mainloop()
```

### 25.8.6 Projet simulation d'un pendule

Vous souhaitez aller plus loin après ces exercices de « mise en jambe » ? Nous vous conseillons d'aller directement au chapitre 27 *Mini projets* (en ligne). Nous vous proposons de réaliser une application Tkinter qui simule le mouvement d'un pendule. En réalisant une application complète de ce genre, un peu plus conséquente, vous serez capable de construire vos propres applications.
# Remarques complémentaires

## 26.1 Différences Python 2 et Python 3

Python 3 est la version de Python qu'il faut utiliser.

Néanmoins, Python 2 a été employé pendant de nombreuses années par la communauté scientifique et vous serez certainement confrontés à un programme écrit en Python 2. Voici quelques éléments pour vous en sortir :

### 26.1.1 Le mot-clé print / la fonction print()

En Python 2 `print` est un mot-clé du langage (en anglais *statement*) au même titre que `for`, `if`, `def`, etc. Il s'utilise ainsi sans parenthèse. Par exemple :

1 >>> print 12

2 12

3 >>> print "girafe"

4 girafe

Par contre, en Python 3, `print()` est une fonction. Ainsi, si vous n'utilisez pas de parenthèse, Python vous renverra une erreur :

1 >>> print 12

2 File "<stdin>", line 1

3 print 12

4 ^

5 SyntaxError: Missing parentheses in call to 'print'

### 26.1.2 Division d'entiers

En Python 3, la division de deux entiers, se fait *naturellement*, c'est-à-dire que l'opérateur `/` renvoie systématiquement un *float*. Par exemple :

1 >>> 3 / 4

2 0.75

Il est également possible de réaliser une division entière avec l'opérateur `//` :

>>> 3 // 4

0
La division entière renvoie finalement la partie entière du nombre 0.75, c'est-à-dire 0.

Attention ! En Python 2, la division de deux entiers avec l'opérateur / correspond à la division entière, c'est-à-dire le résultat arrondi à l'entier inférieur. Par exemple :

```
1 >>> 3 / 5
2 0
3 >>> 4 / 3
4 1
```

Faites très attention à cet aspect si vous programmez encore en Python 2, c'est une source d'erreur récurrente.

### 26.1.3 La fonction range()

En Python 3, la fonction `range()` renvoie un objet de type `range` (voir les chapitres 5 *Boucles et comparaisons* et 14 *Conteneurs*) :

```
1 >>> range(3)
2 range(0, 3)
```

Comme on a vu au chapitre 5 *Boucles et comparaisons*, ces objets sont itérables produisant successivement les nombres 0, puis 1 puis 2 sur notre exemple :

```
1 >>> for i in range(3):
2     ...     print(i)
3 ...
4 0
5 1
6 2
```

En Python 2, la fonction `range()` renvoie une liste. Par exemple :

```
1 >>> range(3)
2 [0, 1, 2]
3 >>> range(2, 6)
4 [2, 3, 4, 5]
```

La création de liste avec `range()` était pratique, mais très peu efficace en mémoire lorsque l'argument donné à `range()` était un grand nombre.

D'ailleurs la fonction `xrange()` est disponible en Python 2 pour faire la même chose que la fonction `range()` en Python 3. Attention, ne vous mélangez pas les pinceaux !

```
1 >>> range(3)
2 [0, 1, 2]
3 >>>ugen(3)
4 xrange(3)
```

**Remarque**

Pour générer une liste d'entiers avec la fonction `range()` en Python 3, vous avez vu dans le chapitre 4 *Listes* qu'il suffit de l'associer avec la fonction `list()`. Par exemple :

```
1 >>> list(range(10))
2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

**Remarque** ________ En Python 3, la fonction `range()` est ce qu'on appelle un **générateur** dans le sens où elle génère un objet contenant une série de valeurs, utilisables une à la fois, par itération dans une boucle `for`. L'objet de type `range` renvoyé par la fonction est quant à lui appelé **itérateur**. Si vous souhaitez en savoir un peu plus sur la différence entre un générateur et un itérateur, vous pouvez consulter cette ressource<fnref n="1" />.

<footnote n="1">1. https://data-flair.training/blogs/python-generator-vs-iterator/</footnote>
### 26.1.4 Encodage et utf-8

En Python 3, vous pouvez utiliser des caractères accentués dans les commentaires ou dans les chaînes de caractères.

Ce n'est pas le cas en Python 2. Si un caractère accentué est présent dans votre code, cela occasionnera une erreur de ce type lors de l'exécution de votre script :

```
SyntaxError: Non-ASCII character '\xc2' in file xxx on line yyy, but no encoding
declared; see http://python.org/dev/peps/pep-0263/ for details
```

Pour éviter ce genre de désagrément, ajoutez la ligne suivante en tout début de votre script :

```
1 # coding: utf-8
```

Si vous utilisez un shebang (voir rubrique précédente), il faudra mettre la ligne `# coding: utf-8` sur la deuxième ligne (la position est importante<fnref n="2" />) de votre script :

```
1 #! /usr/bin/env python
```

```
2 # coding: utf-8
```

#### Remarque

L'encodage `utf-8` peut aussi être déclaré de cette manière :

```
1 # -*- coding: utf-8 -*-
```

mais c'est un peu plus long à écrire.

## 26.2 Anciennes méthodes de formatage des chaînes de caractères

Dans les premières versions de Python jusqu'à la 2.6, il fallait utiliser l'opérateur %, puis de la version 2.7 jusqu'à la 3.5 il était plutôt conseillé d'utiliser la méthode `.format()` (voir la rubrique suivante pour la définition du mot « méthode »). Même si les *f*-strings sont devenues la manière conseillée pour mettre en place l'écriture formatée, ces deux anciennes manières, sont encore pleinement compatibles avec les versions modernes de Python.

Même si elle fonctionne encore, la première manière avec l'opérateur % est maintenant clairement déconseillée pour un certain nombre de raisons<fnref n="3" />. Néanmoins, nous rappelons ci-dessous son fonctionnement, car il se peut que vous tombiez dessus dans d'anciens livres ou si vous lisez de vieux programmes Python.

La deuxième manière avec la méthode `.format()` est encore largement utilisée et reste tout à fait valide. Elle est clairement plus puissante et évite un certain nombre de désagréments par rapport à l'opérateur %. Vous la croiserez sans doute très fréquemment dans des programmes et ouvrages récents. Heureusement elle a un fonctionnement relativement proche des *f*-strings, donc vous ne serez pas totalement perdus !

Enfin, nous indiquons à la fin de cette rubrique nos conseils sur quelle méthode utiliser.

### 26.2.1 L'opérateur %

On a vu avec les entiers que l'opérateur % ou modulo renvoyait le reste d'une division entière. Cet opérateur existe aussi pour les chaînes de caractères mais il met en place l'écriture formatée. En voici un exemple :

```
1 >>> x = 32
```

```
2 >>> nom = "John"
```

```
3 >>> print("%s a %d ans" % (nom, x))
```

```
4 John a 32 ans
```

```
5 >>> nb_G = 4500
```

```
6 >>> nb_C = 2575
```

```
7 >>> prop_GC = (nb_G + nb_C)/14800
```

```
8 >>> print("On a %d G et %d C -> prop GC = %.2f" % (nb_G, nb_C, prop_GC))
```

```
9 On a 4500 G et 2575 C -> prop GC = 0.48
```

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

<footnote n="3">3. https://docs.python.org/fr/3/library/stdtypes.html?highlight=sprintf#printf-style-string-formatting</footnote>
La syntaxe est légèrement différente. Le symbole % est d'abord appelé dans la chaîne de caractères (dans l'exemple ci-dessus %d, %d et %.2f) pour :

* Désigner l'endroit où sera placée la variable dans la chaîne de caractères.

* Préciser le type de variable à formater, d pour un entier (i fonctionne également) ou f pour un float.

* Éventuellement pour indiquer le format voulu. Ici .2 signifie une précision de deux décimales.

Le signe % est rappelé une seconde fois (% (nb_G, nb_C, prop_GC)) pour indiquer les variables à formater.

### 26.2.2 La méthode .format()

Depuis la version 2.7 de Python, la méthode `.format()` (voir la rubrique suivante pour la définition d'une méthode) a apporté une nette amélioration pour mettre en place l'écriture formatée. Celle-ci fonctionne de la manière suivante :

```
1 >>> x = 32
2 >>> nom = "John"
3 >>> print("{} a {} ans".format(nom, x))
4 John a 32 ans
5 >>> nb_G = 4500
6 >>> nb_C = 2575
7 >>> prop_GC = (nb_G + nb_C)/14800
8 >>> print("On a {} G et {} C -> prop GC =”.format nb_G, nb_C, prop_GC))
9 On a 4500 G et 2575 C -> prop GC = 0.48
```

* Dans la chaîne de caractères, les accolades vides {} précisent l'endroit où le contenu de la variable doit être inséré.

* Juste après la chaîne de caractères, l'instruction `.format(nom, x)` fournit la liste des variables à insérer, d'abord la variable nom puis la variable x.

* On peut éventuellement préciser le formatage en mettant un caractère deux-points : puis par exemple ici `.2f` qui signifie deux chiffres après la virgule.

* La méthode `.format()` agit sur la chaîne de caractères à laquelle elle est attachée par le point.

Tout ce que nous avons vu avec les *f-strings* sur la manière de formater l'affichage d'une variable (après les : au sein des accolades) est identique avec la méthode `.format()`. Par exemple `{:.2f}`, `{:.0>6d}`, `{:.6e}`, etc., fonctionneront de la même manière. La différence notable est qu'on ne met pas directement le nom de la variable au sein des accolades. Comme pour l'opérateur %, c'est l'emplacement dans les arguments passés à la méthode `.format()` qui dicte quelle variable doit être remplacée. Par exemple, dans `"{} {} {}"`.format(bidule, machin, truc), les premières accolades remplaceront la variable bidule, les deuxièmes la variable machin, les troisièmes la variable truc.

Le formatage avec la méthode `.format()` se rapproche de la syntaxe des *f-strings* (accolades, deux-points), mais présente l'inconvénient – comme avec l'opérateur % – de devoir mettre la liste des variables tout à la fin, alourdissant ainsi la syntaxe. En effet, dans l'exemple avec la proportion de GC, la ligne équivalente avec une *f-string* apparaît tout de même plus simple à lire :

```
1 >>> print(f"On a {nb_G} G et {nb_C} C -> prop GC = {prop_GC:.2f}")
2 On a 4500 G et 2575 C -> prop GC = 0.48
```

#### Conseil

Pour conclure, ces deux anciennes façons de formater une chaîne de caractères avec l'opérateur % ou la méthode `.format()` vous sont présentées à titre d'information. La première avec l'opérateur % est clairement déconseillée. La deuxième avec la méthode `.format()` est encore tout à fait valable. Si vous débutez Python, nous vous conseillons fortement d'apprendre et d'utiliser les *f-strings*. C'est ce que vous rencontrerez dans la suite de ce cours. Si vous connaissez déjà Python et que vous utilisez la méthode `.format()`, nous vous conseillons de passer aux *f-strings*. Depuis que nous les avons découvertes, aucun retour n'est envisageable pour nous tant elles sont puissantes et plus claires à utiliser !

Enfin, si vous souhaitez aller plus loin, voici deux articles (en anglais) très bien faits sur le site *RealPython* : sur l'écriture formatée<fnref n="4" /> et sur les *f-strings*<fnref n="5" />

<footnote n="4">4. https://realpython.com/python-string-formatting</footnote>

<footnote n="5">5. https://realpython.com/python-f-strings/</footnote>
## 26.3 Gestion des exceptions

Les langages de programmation comme Python contiennent un système de gestion des **exceptions** 6. Qu'est-ce qu'une exception ? Sur la page anglaise de Wikipedia 7, une exception est définie comme une anomalie de l'exécution d'un programme requérant une action spéciale, en général l'arrêt de l'exécution. Le plus souvent, **une exception correspond à une erreur** que Python rencontre lorsqu'il tente d'exécuter les lignes de code qu'on lui soumet. Par exemple, un problème de syntaxe, une variable ou objet qui prend une valeur aberrante (par exemple diviser par 0, parcourir une liste au-delà du nombre d'éléments, etc.).

Le système de gestion des exceptions évite que votre programme « plante » en prévoyant vous-même les sources d'erreurs éventuelles.

Voici un exemple dans lequel on demande à l'utilisateur d'entrer un nombre entier, puis on affiche ce nombre.

1 >>> nb = int(input("Entrez un nombre entier : "))

2 Entrez un nombre entier : 23

3 >>> print nb

4 23

La fonction `input()` demande à l'utilisateur de saisir une chaîne de caractères. Cette chaîne de caractères est ensuite transformée en nombre entier avec la fonction `int()`.

Si l'utilisateur ne rentre pas un nombre, voici ce qui se passe :

1 >>> nb = int(input("Entrez un nombre entier : "))

2 Entrez un nombre entier : ATCG

3 Traceback (most recent call last):

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

5 ValueError: invalid literal for int() with base 10: 'ATCG'

L'erreur provient de la fonction `int()` qui n'a pas pu convertir la chaîne de caractères "ATCG" en nombre entier, ce qui est parfaitement normal. En termes plus techniques, on dira que « Python a levé une exception de type `ValueError` ». Eh oui il y a de nombreux types d'exceptions différents (voir plus bas) ! Le nom de l'exception apparaît toujours comme le premier mot de la dernière ligne du message d'erreur. Si nous lancions ces lignes de code sous forme de script (du style `python script.py`), cet exemple conduirait à l'arrêt de l'exécution du programme.

Le jeu d'instructions `try / except` permet de tester l'exécution d'une commande et d'intervenir en cas de levée d'exception.

1 >>> try:

2 ... nb = int(input("Entrez un nombre entier : "))

3 ... except:

4 ... print("Vous n'avez pas entré un nombre entier !")

5 ...

6 Entrez un nombre entier : ATCG

7 Vous n'avez pas entré un nombre entier !

Dans cet exemple, l'exception levée par la fonction `int()` (qui ne peut pas convertir "ATCG" en nombre entier) est interceptée et déclenche l'affichage du message d'avertissement.

On peut ainsi redemander sans cesse un nombre entier à l'utilisateur, jusqu'à ce que celui-ci en rentre bien un.

<footnote n="6">6. https://fr.wikipedia.org/wiki/Syst%C3%A8me_de_gestion_d%27exceptions</footnote>

<footnote n="7">7. https://en.wikipedia.org/wiki/Exception_handling</footnote>
```python
1 >>> while True:
2     ...     try:
3         ...     nb = int(input("Entrez un nombre entier : "))
4         ...     print("Le nombre est", nb)
5         ...     break
6     ... except:
7         ...     print("Vous n'avez pas entré un nombre entier !")
8         ...     print("Essayez encore")
9     ...
10 Entrez un nombre entier : ATCG
11 Vous n'avez pas entré un nombre entier !
12 Essayez encore
13 Entrez un nombre entier : toto
14 Vous n'avez pas entré un nombre entier !
15 Essayez encore
16 Entrez un nombre entier : 3.2
17 Vous n'avez pas entré un nombre entier !
18 Essayez encore
19 Entrez un nombre entier : 55
20 Le nombre est 55
```

Notez que dans cet exemple, l'instruction `while True` est une boucle infinie car la condition `True` est toujours vérifiée. L'arrêt de cette boucle est alors forcé par la commande `break` lorsque l'utilisateur a effectivement entré un nombre entier.

La gestion des exceptions est très utile dès lors que des données extérieures entrent dans un programme Python, que ce soit directement par l'utilisateur (avec la fonction `input()`) ou par des fichiers. Cela est fondamental si vous distribuez votre code à la communauté : si les utilisateurs ne connaissent pas Python, un message comme "Vous n'avez pas entré un nombre entier !" reste plus clair que `ValueError: invalid literal for int() with base 10: 'ATCG'`.

Vous pouvez par exemple vérifier qu'un fichier a bien été ouvert.

```python
1 >>> nom = "toto.pdb"
2 >>> try:
3     ...     with open(nom, "r") as fichier:
4         ...     for ligne in fichier:
5         ...         print(ligne)
6     ... except:
7     ...     print("Impossible d'ouvrir le fichier", nom)
```

Si une erreur est déclenchée, c'est sans doute que le fichier n'existe pas à l'emplacement indiqué sur le disque ou que vous n'avez pas les droits pour le lire.

Il est également possible de spécifier le type d'erreur à gérer. Le premier exemple que nous avons étudié peut s'écrire :

```python
1 >>> try:
2     ...     nb = int(input("Entrez un nombre entier : "))
3     ... except ValueError:
4         ...     print("Vous n'avez pas entré un nombre entier !")
5     ...
6 Entrez un nombre entier : ATCG
7 Vous n'avez pas entré un nombre entier !
```

Ici, on intercepte une exception de type `ValueError`, ce qui correspond bien à un problème de conversion avec `int()`.

Attention, si vous précisez le type d'exception comme `ValueError`, le `except ValueError` n'empêchera pas la levée d'une autre exception.

```python
1 >>> try:
2     ...     nb = int(variable)
3     ... except ValueError:
4         ...     print("Vous n'avez pas entré un nombre entier !")
5     ...
6 Traceback (most recent call last):
7     File "<stdin>", line 2, in <module>
8 NameError: name 'variable' is not defined. Did you mean: 'callable'?
```

Ici l'exception levée est de type `NameError`, car variable n'existe pas. Alors que si vous mettez `except` tout court,
cela intercepte n'importe quelle exception.

```python
1 >>> try:
2     ... nb = int(variable)
3 >>> except:
4     ... print("Vous n'avez pas entré un nombre entier !")
5 ...
6 Vous n'avez pas entré un nombre entier !
7 >>>
```

Vous voyez qu'ici cela pose un nouveau problème : le message d'erreur ne correspond pas à l'exception levée !

### Conseil

* Nous vous conseillons vivement de toujours préciser le type d'exception dans vos `except`. Cela évite d'intercepter une exception que vous n'aviez pas prévue. Il est possible d'intercepter plusieurs types d'exceptions en passant un tuple à `except`, par exemple : `except (Exception1, Exception2)`.

* Pa ailleurs, ne mettez pas trop de lignes dans le bloc du `try`. Dans un tel cas, il peut être très pénible de trouver la ligne qui a conduit à l'exécution du `except`. Pire encore, il se peut que des lignes que vous aviez prévues ne soient pas exécutées ! Donc gardez des choses simples dans un premier temps, comme par exemple tester les conversions de type ou vérifier qu'un fichier existe bien et que vous pouvez l'ouvrir.

Il existe de nombreux types d'exception comme `RuntimeError`, `TypeError`, `NameError`, `IOError`, etc. Vous pouvez aller voir la liste complète<fnref n="8" /> sur le site de Python. Nous avions déjà croisé des noms d'exception au chapitre 23 (Avoir la classe avec les objets) en regardant ce que contient le module `builtins`.

```python
1 >>> import builtins
2 >>> dir(builtins)
3 ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
4 4 [...] 5 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError'
6 [...] 6
```

Leur présence dans le module `builtins` signifie qu'elles font partie du langage lui même, au même titre que les fonctions de base comme `range()`, `list()`, etc.

Avez-vous aussi remarqué que leur nom commence toujours par une majuscule et qu'il peut en contenir plusieurs à la façon `CamelCase` ? Si vous avez bien lu le chapitre 16 *Bonnes pratiques en programmation Python*, avez-vous deviné pourquoi ? Et bien, c'est parce que **les exceptions sont des classes**. C'est très intéressant car il est ainsi possible d'utiliser l'héritage pour créer ses propres exceptions à partir d'exceptions pré-existantes. Nous ne développerons pas cet aspect, mais en guise d'illustration, regardez ce que renvoit un `help()` de l'exception `OverflowError`.

```python
1 >>> help(OverflowError)
2 [...]
3 class OverflowError(ArithmeticError)
4     Result too large to be represented.
5
6     Method resolution order:
7         OverflowError
8         ArithmeticError
9         Exception
10      BaseException
11      object
```

L'exception `OverflowError` hérite de `ArithmeticError`, c'est-à-dire qu'`OverflowError` a été conçue à partir de `ArithmeticError` et en hérite de tous ses attributs.

Un autre aspect très important que nous avons croisé au chapitre 24 *Avoir plus la classe avec les objets* est la possibilité de lever vous-même une exception avec le mot-clé `raise`. Nous avions vu le code suivant :

```python
if valeur < 0:
    raise ValueError("Z'avez déjà vu une masse négative ?")
```

<footnote n="8">8. https://docs.python.org/fr/3.12/library/exceptions.html#exceptions.TypeError</footnote>
La ligne 2 lève une exception `ValueError` lorsque la variable `valeur` est négative. L'instruction `raise` est bien pratique lorsque vous souhaitez stopper l'exécution d'un programme si une variable ne se trouve pas dans le bon intervalle ou ne contient pas la bonne valeur. Vous avez sans doute compris maintenant pourquoi on parlait de « levée » d'exception...

Enfin, on peut aussi être très précis dans le message d'erreur. Observez la fonction `download_page()` qui, avec le module `urllib`, télécharge un fichier sur internet.

```python
1 import urllib.request
2
3 def download_page(address):
4     error = ""
5     page = ""
6     try:
7         data = urllib.request.urlopen(address)
8         page = data.read()
9     except HTTPError as e:
10         if hasattr(e, 'reason'):
11             error = "Cannot reach web server: " + str(e.reason)
12         if hasattr(e, 'code'):
13             error = f"Server failed {e.code:d}"
14     return page, error
15
16 data, error = download_page("https://files.rcsb.org/download/1BTA.pdb")
17
18 if error:
19     print(f"Error rencontrée : {error}")
20 else:
21     with open("proteine.pdb", "w") as prot:
22         prot.write(data.decode("utf-8"))
23     print("Protéine enregistrée")
```

La variable `e` est une instance de l'exception `IOError`. Certains de ses attributs sont testés avec la fonction `hasattr()` pour ainsi affiner le message renvoyé (ici contenu dans la variable `error`).

Si tout se passe bien, la page est téléchargée est stockée dans la variable `data`, puis enregistrée sur le disque dur.

## 26.4 Shebang et /usr/bin/env python3

Lorsque l'on programme sur un système Unix (Mac OS X ou Linux par exemple), on peut exécuter directement un script Python, sans appeler explicitement la commande `python`.

Pour cela, deux opérations sont nécessaires :

**Étape 1.** Préciser la localisation de l'interpréteur Python en indiquant dans la première ligne du script :

```bash
1 #!/usr/bin/env python
```

Par exemple, si le script `test.py` contenait :

```bash
1 print("Hello World !")
```

il va alors contenir :

```bash
1 #!/usr/bin/env python
2
3 print("Hello World !")
```

**Étape 2.** Rendre le script Python exécutable en lançant l'instruction :

```bash
$ chmod +x test.py
```

### Remarque

La ligne `#! /usr/bin/env python` n'est pas considérée comme un commentaire par Python, ni comme une instruction Python d'ailleurs. Cette ligne a une signification particulière pour le système d'exploitation Unix.

Pour exécuter le script, il suffit alors de taper son nom précédé des deux caractères `./` (afin de préciser au shell où se
trouve le script) :

```shell
$ ./test.py
Hello World !
```

## Définition

Le shebang<fnref n="9" /> correspond aux caractères #! qui se trouvent au début de la première ligne du script test.

Le shebang est suivi du chemin complet du programme qui interprète le script ou du programme qui sait où se trouve l'interpréteur Python. Dans l'exemple précédent, c'est le programme `/usr/bin/env` qui indique où se trouve l'interpréteur Python.

## 26.5 Passage d'arguments avec *args et **kwargs

Avant de lire cette rubrique, nous vous conseillons de bien relire et maîtriser la rubrique *Arguments positionnels et arguments par mot-clé* du chapitre 10 *Fonctions*.

Dans le chapitre 10, nous avons vu qu'il était nécessaire de passer à une fonction tous les arguments positionnels définis dans celle-ci. Il existe toutefois une astuce permettant de passer un nombre arbitraire d'arguments positionnels :

```python
1 >>> def fct(*args):
2     ...     print(args)
3     ...
4 >>> fct()
5 ()
6 >>> fct(1)
7 (1,)
8 >>> fct(1, 2, 5, "Python")
9 (1, 2, 5, 'Python')
10 >>> fct(z=1)
11 Traceback (most recent call last):
12     File "<stdin>", line 1, in <module>
13 TypeError: fct() got an unexpected keyword argument 'z'
```

L'utilisation de la syntaxe `*args` permet d'empaqueter tous les arguments positionnels dans un tuple unique `args` récupéré au sein de la fonction. L'avantage est que nous pouvons passer autant d'arguments positionnels que l'on veut. Toutefois, on s'aperçoit en ligne 10 que cette syntaxe ne fonctionne pas avec les arguments par mot-clé.

Il existe un équivalent avec les arguments par mot-clé :

```python
1 >>> def fct(**kwargs):
2     ...     print kwargs)
3     ...
4 >>> fct()
5 {}
6 >>> fct(z=1, gogo="toto")
7 {'gogo': 'toto', 'z': 1}
8 >>> fct(z=1, gogo="toto", y=-67)
9 {'y': -67, 'gogo': 'toto', 'z': 1}
10 >>> fct(1, 2)
11 Traceback (most recent call last):
12     File "<stdin>", line 1, in <module>
13 TypeError: fct() takes 0 positional arguments but 2 were given
```

La syntaxe `**kwargs` permet d'empaqueter l'ensemble des arguments par mot-clé, quel que soit leur nombre, dans un dictionnaire unique `kwargs` récupéré dans la fonction. Les clés et valeurs de celui-ci sont les noms des arguments et les valeurs passées à la fonction. Toutefois, on s'aperçoit en ligne 9 que cette syntaxe ne fonctionne pas avec les arguments positionnels.

Si on attend un mélange d'arguments positionnels et par mot-clé, on peut utiliser `*args` et `**kwargs` en même temps :

<footnote n="9">9. http://fr.wikipedia.org/wiki/Shebang</footnote>
```python
  1 >>> def fct(*args, **kwargs):
  2     ...     print(args)
  3     ...     print kwargs)
  4     ...
  5 >>> fct()
  6 ()
  7 {}
  8 >>> fct(1, 2)
  9 (1, 2)
10 {}
11 >>> fct(z=1, y=2)
12 ()
13 {'y': 2, 'z': 1}
14 >>> fct(1, 2, 3, z=1, y=2)
15 (1, 2, 3)
16 {'y': 2, 'z': 1}
```

Deux contraintes sont toutefois à respecter. Il faut toujours :

* mettre `*args` avant `**kwargs` dans la définition de la fonction;

* passer les arguments positionnels avant ceux par mot-clé lors de l'appel de la fonction.

Il est possible de combiner des arguments positionnels avec `*args` et `**kwargs`, par exemple :

```python
def fct(a, b, *args, **kwargs):
```

Dans un tel cas, il faudra obligatoirement passer les deux arguments `a` et `b` à la fonction, ensuite on pourra mettre un nombre arbitraire d'arguments positionnels (récupérés dans le tuple `args`), puis un nombre arbitraire d'arguments par mot-clé (récupérés dans le dictionnaire `kwargs`).

### Conseil

Les noms `*args` et `**kwargs` sont des conventions en Python, ils rappellent les mots arguments et keyword arguments. Bien qu'on puisse mettre ce que l'on veut, nous vous conseillons de respecter ces conventions pour faciliter la lecture de votre code par d'autres personnes.

L'utilisation de la syntaxe `*args` et `**kwargs` est très classique dans le module `Fenêtres graphiques et Tkinter` présenté dans le chapitre 25 (en ligne).

Il est possible d'utiliser ce mécanisme d'empaquetage / désempaquetage (`packing` / `unpacking`) dans l'autre sens :

```python
  1 >>> def fct(a, b, c):
  2     ...     print(a,b,c)
  3     ...
  4 >>> t = (-5,6,7)
  5 >>>
  6 >>> fct(*t)
  7 -5 6 7
```

Avec la syntaxe `*t` on désempaquette le tuple à la volée lors de l'appel à la fonction. Cela est aussi possible avec un dictionnaire :

```python
  1 >>> def fct(x, y, z):
  2     ...     print(x, y, z)
  3     ...
  4 >>> dico = {'x': -1, 'y': -2, 'z': -3}
  5 >>> fct(**dico)
  6 -1 -2 -3
```

Attention toutefois à bien respecter deux choses :

* la concordance entre le nom des clés du dictionnaire et le nom des arguments dans la fonction (sinon cela renvoie une erreur);

* l'utilisation d'une double étoile pour désempaqueter les valeurs du dictionnaire (si vous utilisez une seule étoile, Python désempaquettera les clés!).

Ce mécanisme de désempaquetage est aussi utilisable avec les objets `zip`, on parle de `zip unpacking`. Souvenons-nous, un objet `zip` permettait d'assembler plusieurs listes, éléments par éléments (voir Chapitre 12 *Plus sur les listes*) :
1 >>> animaux = ["poulain", "renard", "python"]
2 >>> couleurs = ["alezan", "roux", "vert"]
3 >>> zip(range(3), animaux, couleurs)
4 <zip object at 0x7f333febc880>
5 >>> triplets = list.zip(range(3), animaux, couleurs))
6 >>> triplets
7 [(0, 'poulain', 'alezan'), (1, 'renard', 'roux'), (2, 'python', 'vert')]

**Lignes 1 à 4.** On crée un objet *zip* avec trois objets de trois éléments.

**Lignes 5 à 7.** Cet objet *zip* en conjonction avec la fonction `list()` nous permet d'associer les éléments par ordre d'apparition (tous les éléments à la position 1 se retrouve ensemble, idem pour les positions 2 et 3). Au final, l'objet `triplets` est une liste de *tuples* de trois éléments.

L'opérateur `*` en combinaison avec la fonction `zip` va nous permettre de désempaqueter `triplets` pour récupérer les listes initiales (`range(3)`, `animaux` et `couleurs`):

1 >>> zip(*triplets)
2 <zip object at 0x7f333fd44980>
3 >>> list.zip(*triplets))
4 [(0, 1, 2), ('poulain', 'renard', 'python'), ('alezan', 'roux', 'vert')]

Bien sûr, on peut l'utiliser l'affectation multiple :

1 >>> numéros2, animaux2, couleurs2 = zip(*triplets)
2 >>> numéros2
3 (0, 1, 2)
4 >>> animaux2
5 ('poulain', 'renard', 'python')
6 >>> couleurs2
7 ('alezan', 'roux', 'vert')

Au final, on récupère des *tuples* au lieu des listes initiales. Mais à ce stade, vous devriez être capable de les retransformer en liste ;-).

## 26.6 Décorateurs

Dans le chapitre 24, nous avons rencontré la notion de décorateur pour déclarer des objets de type *property*. Cela permettait de rendre des méthodes accessibles comme des attributs (décorateur `@property`), et plus généralement de contrôler l'accès, la modification et la destruction d'attributs (décorateurs `@nom_attrIBUT.setter` et `@nom_attrIBUT.deleter`). Il existe d'autres décorateurs prédéfinis en Python (e.g. `@staticmethod`, `@classmethod`, etc.). Nous allons voir dans cette section comment on crée ses propres décorateurs et les mécanismes sous-jacents. Nous vous conseillons de bien relire commment fonctionne les fonctions de rappel, ou fonctions *callback* (chapitre 25 *Tkinter*).

### Définition

Un décorateur est une fonction qui modifie le comportement d'une autre fonction.

Ceci étant dit, comme cela fonctionne-t-il ? Commençons par une fonction simple qui affiche de la nourriture :

1 def imprime_victuaille():
2 print("tomate / mozza")

On souhaite améliorer cette fonction et transformer cette victuaille en sandwich, en affichant une tranche de pain avant et après. La stratégie va être de créer une fonction spéciale, qu'on appelle **décorateur**, modifiant `imprime_victuaille()`.

1 def transforme_en_sandwich(fonction_a_decorer):
2 def emballage():
3 print("Pain")
4 fonction_a_decorer()
5 print("Pain")
6 return emballage
La fonction `transforme_en_sandwich()` est notre décorateur, elle prend en argument la fonction que l'on souhaite décorer sous forme de *callback* (donc sans les parenthèses). On voit qu'à l'intérieur, on définit une sous-fonction `emballage()` qui va littéralement « emballer » (*wrap*) notre fonction à décorer, c'est-à-dire, effectuer une action avant et après l'appel de la fonction à décorer. Enfin, le décorateur renvoie cette sous-fonction `emballage` sous forme de *callback*. Pour que le décorateur soit actif, il faudra « transformer » la fonction à décorer avec notre fonction décoratrice :

```python
1 imprime_victuaille = transforme_en_sandwich(imprime_victuaille)
```

Voici le code complet implémentant la fonction `imprime_victuaille()` décorée :

```python
1 def transforme_en_sandwich(fonction_a_decorer):
2     def emballage():
3         print("Pain")
4         fonction_a_decorer()
5         print("Pain")
6         return emballage
7
8 def imprime_victuaille():
9         print("tomate/ mozza")
10
11 if __name__ == "__main__":
12     print("Fonction non décorée:")
13     imprime_victuaille()
14     print()
15     print("Fonction décorée:")
16     imprime_victuaille = transforme_en_sandwich(imprime_victuaille)
17     imprime_victuaille()
```

Au final l'idée est d'appeler la fonction décoratrice plutôt que la fonction `imprime_victuaille()` elle-même. Regardons ce que donne l'exécution de la fonction avant et après décoration :

```
Fonction non décorée:
tomate/ mozza
```

```
Fonction décorée:
Pain
tomate/ mozza
Pain
```

Le premier appel en ligne 13 exécute la fonction simple, alors que le second en ligne 17 exécute la fonction décorée. Cette construction peut sembler ardue et difficile à comprendre. Heureusement, Python a une notation en « *sucre syntaxique* » (*syntactic sugar*) qui en facilite la lecture. Celle-ci utilise le symbole `@` :

```python
1 def transforme_en_sandwich(fonction_a_decorer):
2     def emballage():
3         print("Pain")
4         fonction_a_decorer()
5         print("Pain")
6         return emballage
7
8 @transforme_en_sandwich
9 def imprime_victuaille():
10     print("tomate / mozza")
11
12 if __name__ == "__main__":
13     imprime_victuaille()
```

La ligne 8 transforme irrémédiablement la fonction `imprime_victuaille()` en fonction décorée. Cela parait déjà un peu plus lisible. L'exécution donnera bien sûr :

```
Pain
tomate / mozza
Pain
```

Au final, la notation :
1 @decorator

2 def fct():

3 [... ]

est équivalente à :

1 fct =decorator(fct)

Cela fonctionne avec n'importe quelle fonction prenant en argument une autre fonction.

Conseil

Nous vous conseillons bien sûr d'utiliser systématiquement la notation @decorator qui est plus lisible et intuitive.

Si tout cela vous semble ardu (on vous comprend...), vous devez vous dire « pourquoi utiliser une construction aussi complexe ? ». Et bien, c'est tout simplement parce qu'un décorateur est ré-utilisable dans n'importe quelle fonction. Si on reprend la même fonction décoratrice que ci-dessus :

1 @transforme_en_sandwich
2 def imprime_victuaille1():
3     print("tomate / mozza")
4
5 @transforme_en_sandwich
6 def imprime_victuaille2():
7     print("jambon / fromage")
8
9 if __name__ == "__main__":
10     imprime_victuaille1()
11     print()
12     imprime_victuaille2()

On a donc un décorateur permettant de transformer en sandwich n'importe quelle fonction imprimant une victuaille !
Ceci renverra :

Pain
tomate / mozza

Pain

Pain
jambon / fromage

Pain

Un exemple plus concret de décorateur pourrait être la mesure du temps d'exécution d'une fonction :

import time
def mesure_temp(s(fonction_adecorer):
    def emballage():
        temps1 = time.time()
        fonction_adecorer()
        temps2 = time.time()
        print(f"Le temps d'exécution de {fonction_adecorer.__name__} est ")
        f"{temps2 - temps1} s")
    return emballage

En ligne 8, l'attribut __name__ renvoie le nom de la fonction sous forme de chaîne de caractères. Dans cet exemple, le décorateur @mesure_temp mis devant n'importe quelle fonction affichera systématiquement le temps d'exécution de celle-ci.

Pour finir, si on revient sur le décorateur @property vu dans le chapitre 24 Avoir plus la classe avec les objets, nous
avions vu également qu'il existait une fonction property(). Donc pour les décorateurs pré-existants que nous avons
abordés dans le chapitre 24, il existe des fonctions équivalentes. Comme dans notre exemple, la notation @decorateur
va finalement appeler la fonction décoratrice. Donc derrière une notation @quelquechose, il existe toujours une fonction
quelquechose() remplissant ce rôle de décorateur.
## Pour aller plus loin

Pour aller plus loin, vous pouvez consulter ce très bon article<fnref n="10" /> sur le site *RealPython*. Il y est expliqué en outre comment on peut gérer le passage d'arguments quand on utilise des décorateurs, ainsi que l'utilisation de décorateurs multiples.

## 26.7 Un peu de transformée de Fourier avec NumPy

La transformée de Fourier est très utilisée pour l'analyse de signaux, notamment lorsqu'on souhaite extraire des périodicités au sein d'un signal bruité. Le module `NumPy` possède la fonction `fft()` (dans le sous-module `fft`) permettant de calculer des transformées de Fourier.

Voici un petit exemple sur la fonction cosinus de laquelle on souhaite extraire la période à l'aide de la fonction `fft()` :

```python
1 import numpy as np
2
3 debut = -2 * np.pi
4 fin = 2 * np.pi
5 pas = 0.1
6 x = np.arange(debut, fin, pas)
7 y = np.cos(x)
8
9 TF = np Bridge Bridge(y)
10 ABSTF = np .abs(TF)
11 pas_xABSTF = 1/(fin-debut)
12 x_xABSTF = np .arange(0, pas_xABSTF * len(ABSTF), pas_xABSTF)
```

Plusieurs commentaires sur cet exemple :

Ligne 1. On charge le module `NumPy` avec le nom raccourci `np`.

Lignes 3 à 6. On définit l'intervalle (de $-2\pi$ à $2\pi$ radians) pour les valeurs en abscisse ainsi que le pas (0,1 radians).

Lignes 7. On calcule directement les valeurs en ordonnées avec la fonction cosinus du module `NumPy`. On constate ici que `NumPy` redéfinit certaines fonctions ou constantes mathématiques de base, comme `pi`, `cos()` ou `abs()` (valeur absolue, ou module d'un nombre complexe). Ces fonctions sont directement utilisables avec un objet `array`.

Ligne 9. On calcule la transformée de Fourier avec la fonction `fft()` qui renvoie un vecteur (objet `array` à une dimension) de nombres complexes. Eh oui, le module `NumPy` gère aussi les nombres complexes !

Ligne 10. On extrait le module du résultat précédent avec la fonction `abs()`.

Ligne 11. La variable `x_ABSTFL` représente l'abscisse du spectre (en radian$^{-1}$).

Ligne 12. La variable `ABSTF` contient le spectre lui même. L'analyse de ce dernier nous donne un pic à $0,15 \text{ radian}^{-1}$, ce qui correspond bien à $2\pi$ (c'est plutôt bon signe de retrouver ce résultat).

## 26.8 Sauvegardez votre historique de commandes

Vous pouvez sauvegarder l'historique des commandes utilisées dans l'interpréteur Python avec le module `readline`.

```python
1 >>> print("hello")
2 hello
3 >>> a = 22
4 >>> a = a + 11
5 >>> print(a)
6 33
7 >>> import readline
8 >>> readline.write_history_file()
```

Quittez Python. L'historique de toutes vos commandes est dans votre répertoire personnel, dans le fichier `.history`. Relancez l'interpréteur Python.

```python
1 >>> import readline
2 >>> readline.read_history_file()
```

<footnote n="10">10. https://realpython.com/primer-on-python-decorators/</footnote>
Vous pouvez accéder aux commandes de la session précédente avec la flèche du haut de votre clavier. D'abord les commandes `readline.read_history_file()` et `import readline` de la session actuelle, puis `print(a)`, `a = a + 11`, `a = 22...`
# CHAPITRE 27

## Mini-projets

Dans ce chapitre, nous vous proposons quelques scénarios pour développer vos compétences en Python et mettre en œuvre les concepts que vous avez rencontrés dans les chapitres précédents.

### 27.1 Description des projets

#### 27.1.1 Mots anglais dans le protéome humain

L'objectif de ce premier projet est de découvrir si des mots anglais peuvent se retrouver dans les séquences du protéome humain, c'est-à-dire dans les séquences de l'ensemble des protéines humaines.

Vous aurez à votre disposition :

* Le fichier *english-common-words.txt*<fnref n="1" />, qui contient les 3 000 mots anglais les plus fréquents, à raison d'1 mot par ligne.

* Le fichier *human-proteome.fasta*<fnref n="2" /> qui contient le protéome humain sous la forme de séquences au format FASTA. Attention, ce fichier est assez gros. Ce fichier provient de la banque de données UniProt à partir de cette page<fnref n="3" />.

**Conseil**

Des explications sur le format FASTA et des exemples de code sont fournis dans l'annexe A *Quelques formats de données en biologie*.

#### 27.1.2 Genbank2fasta

Ce projet consiste à écrire un convertisseur de fichier, du format GenBank au format FASTA.

Pour cela, nous allons utiliser le fichier GenBank du chromosome I de la levure de boulanger *Saccharomyces cerevisiae*.
Vous pouvez télécharger ce fichier :

* soit via le lien sur le site du cours NC_001133.gbk<fnref n="4" />;

<footnote n="1">1. https://python.sdv.u-paris.fr/data-files/english-common-words.txt</footnote>

<footnote n="2">2. https://python.sdv.u-paris.fr/data-files/human-proteome.fasta</footnote>

<footnote n="3">3. https://www.uniprot.org/help/human_proteome</footnote>

<footnote n="4">4. https://python.sdv.u-paris.fr/data-files/NC_001133.gbk</footnote>
* soit directement sur la page de Saccharomyces cerevisiae S288c chromosome I, complete sequence<fnref n="5" /> sur le site du NCBI, puis en cliquant sur Send to, puis Complete Record, puis Choose Destination : File, puis Format : GenBank (full) et enfin sur le bouton Create File.

Vous trouverez des explications sur les formats FASTA et GenBank ainsi que des exemples de code dans l'annexe A *Quelques formats de données en biologie*.

Vous pouvez réaliser ce projet sans ou avec des expressions régulières (abordées dans le chapitre 17).

### 27.1.3 Simulation d'un pendule

On se propose de réaliser une simulation d'un pendule simple<fnref n="6" /> en Tkinter. Un pendule simple est représenté par une masse ponctuelle (la boule du pendule) reliée à un pivot immobile par une tige rigide et sans masse. On néglige les effets de frottement et on considère le champ gravitationnel comme uniforme. Le mouvement du pendule sera calculé en résolvant numériquement l'équation différentielle suivante :

$$a_{\theta}(t) = \frac{d^2\theta}{dt^2}(t) = -\frac{g}{l} * \sin(\theta(t))$$

où $\theta$ représente l'angle entre la verticale et la tige du pendule, $a_{\theta}$ l'accélération angulaire, $g$ la gravité, et $l$ la longueur de la tige (note : pour la dérivation d'une telle équation vous pouvez consulter la page wikipedia<fnref n="7" /> ou l'accompagnement pas à pas, cf. la rubrique suivante).

Pour trouver la valeur de $\theta$ en fonction du temps, on pourra utiliser la méthode semi-implicite d'Euler<fnref n="8" /> de résolution d'équation différentielle. La formule ci-dessus donne l'accélération angulaire au temps $t$ : $a_{\theta}(t) = -\frac{g}{l} * \sin(\theta(t))$. À partir de celle-ci, la méthode propose le calcul de la vitesse angulaire au pas suivant : $v_{\theta}(t + \delta t) = v_{\theta}(t) + a_{\theta}(t) \times \delta t$ (où $\delta t$ représente le pas de temps entre deux étapes successives de la simulation). Enfin, cette vitesse $v_{\theta}(t + \delta t)$ donne l'angle $\theta$ au pas suivant : $\theta(t + \delta t) = \theta(t) + v_{\theta}(t + \delta t) \times \delta t$. On prendra un pas de temps $\delta t = 0.05$ s, une accélération gravitationnelle $g = 9.8 \text{ m.s}^{-2}$ et une longueur de tige de $l = 1$ m.

<figure><img src="image_66.png" /><figcaption>FIGURE 27.1 – Application pendule.</figcaption></figure>

Pour la visualisation, vous pourrez utiliser le *widget canvas* du module `Tkinter` (voir le chapitre 25 *Fenêtres graphiques* et `Tkinter` (en ligne), rubrique *Un canvas animé dans une classe*). On cherche à obtenir un résultat comme montré dans la figure 27.1.

<footnote n="5">5. https://www.ncbi.nlm.nih.gov/nuccore/NC_001133</footnote>

<footnote n="6">6. https://fr.wikipedia.org/wiki/Pendule_simple</footnote>

<footnote n="7">7. https://en.wikipedia.org/wiki/Pendulum_(mathematics)#math_Eq._1</footnote>

<footnote n="8">8. https://en.wikipedia.org/wiki/Euler_method</footnote>
Nous vous conseillons de procéder d'abord à la mise en place du simulateur physique (c'est-à-dire obtenir $\theta$ en fonction du temps ou du pas de simulation). Faites par exemple un premier script Python qui produit un fichier à deux colonnes (temps et valeur de $\theta$). Une fois que cela fonctionne bien, il vous faudra construire l'interface Tkinter et l'animer. Vous pouvez ajouter un bouton pour démarrer / stopper le pendule et une règle pour modifier sa position initiale.

N'oubliez pas, il faudra mettre dans votre programme final une fonction qui convertit l'angle $\theta$ en coordonnées cartésiennes x et y dans le plan du canvas. Faites également attention au système de coordonnées du canvas où les ordonnées sont inversées par rapport à un repère mathématique. Pour ces deux aspects, reportez-vous à l'exercice Polygone de Sierpinski du chapitre 25 Fenêtres graphiques et Tkinter (en ligne).

## 27.2 Accompagnement pas à pas

Vous trouverez ci-après les différentes étapes pour réaliser les mini-projets proposés. Prenez le temps de bien comprendre une étape avant de passer à la suivante.

### 27.2.1 Mots anglais dans le protéome humain

L'objectif de ce premier projet est de découvrir si des mots anglais peuvent se retrouver dans les séquences du protéome humain, c'est-à-dire dans les séquences de l'ensemble des protéines humaines.

#### 27.2.1.1 Composition aminée

Dans un premier temps, composez 5 mots anglais avec les 20 acides aminés.

#### 27.2.1.2 Des mots

Téléchargez le fichier english-common-words.txt <fnref n="9" />. Ce fichier contient les 3000 mots anglais les plus fréquents, à raison d'1 mot par ligne.

Créez un script words_in_proteome.py et écrivez la fonction read_words() qui va lire les mots contenus dans le fichier dont le nom est fourni en argument du script et renvoyer une liste contenant les mots convertis en majuscule et composés de 3 caractères ou plus.

Dans le programme principal, affichez le nombre de mots sélectionnés.

#### 27.2.1.3 Des protéines

Téléchargez maintenant le fichier human-proteome.fasta <fnref n="10" />. Attention, ce fichier est assez gros. Ce fichier provient de la banque de données UniProt à partir de cette page <fnref n="11" />.

Voici les premières lignes de ce fichier ([...] indique une coupure que nous avons faite) :

>sp|095139|NDUB6_HUMAN NADH dehydrogenase [ubiquinone] 1 beta [...]
MTGYTPDEKRLQLQRELRRWLKDQELSPREPVLPPQKMGPMEKFWNKFLENKSPWRKM
VHGKYKKSIFVFTHVLVPVWIIHYYMKYHVSEKPYGIVEKKSRIFPGDTILETGEVIPPM
KEFPDQHH
>sp|075438|NDUB1_HUMAN NADH dehydrogenase [ubiquinone] 1 beta [...]
MVNLLQIVRDHWVHVLVPMGFVIGCYLDRKSDERLTAFRNKSMLFKRELQPSEEVTWK
>sp|Q8N4C6|NIN_HUMAN Ninein OS=Homo sapiens OX=9606 GN=NIN PE=1 SV=4
MDEVEQDQHEARLKELFDSFDTTGTGSLGQEELTDLCHMLSLEEVAPVLQQTLLQDNLLG
RVHFDQFKEALILILSRTLSNEEHFQEPDCSLEAQPKYVRGGKRYGRRSLPEFQESVEEF
PEVTVIEPLDEEARPSHIPAGDCSEHWKTQRSEEYEAEGQLRFWNPDDLLASQSGSSPPQ

Toujours dans le script words_in_proteome.py, écrivez la fonction read_sequences() qui va lire le protéome dans le fichier dont le nom est fourni en second argument du script. Cette fonction va renvoyer un dictionnaire dont les clefs sont les identifiants des protéines (par exemple, 095139, 075438, Q8N4C6) et dont les valeurs associées sont les séquences.

Dans le programme principal, affichez le nombre de séquences lues. À des fins de test, affichez également la séquence associée à la protéine 095139.

<footnote n="9">9. https://python.sdv.u-paris.fr/data-files/english-common-words.txt</footnote>

<footnote n="10">10. https://python.sdv.u-paris.fr/data-files/human-proteome.fasta</footnote>

<footnote n="11">11. https://www.uniprot.org/help/human_proteome</footnote>
### 27.2.1.4 À la pêche aux mots

Écrivez maintenant la fonction `search_words_in_proteome()` qui prend en argument la liste de mots et le dictionnaire contenant les séquences des protéines et qui va compter le nombre de séquences dans lesquelles un mot est présent. Cette fonction renverra un dictionnaire dont les clefs sont les mots et les valeurs le nombre de séquences qui contiennent ces mots. La fonction affichera également le message suivant pour les mots trouvés dans le protéome :

```
ACCESS found in 1 sequences
ACID found in 38 sequences
ACT found in 805 sequences
[...]
```

Cette étape prend quelques minutes. Soyez patient.

### 27.2.1.5 Et le mot le plus fréquent est...

Pour terminer, écrivez maintenant la fonction `find.most.frequent.word()` qui prend en argument le dictionnaire renvoyé par la précédente fonction `search_words_in_proteome()` et qui affiche le mot trouvé dans le plus de protéines, ainsi que le nombre de séquences dans lesquelles il a été trouvé, sous la forme :

```
=> xxx found in yyy sequences
```

Quel est ce mot ?

Quel pourcentage des séquences du protéome contiennent ce mot ?

### 27.2.1.6 Pour être plus complet

Jusqu'à présent, nous avions déterminé, pour chaque mot, le nombre de séquences dans lesquelles il apparaissait. Nous pourrions aller plus loin et calculer aussi le nombre de fois que chaque mot apparaît dans les séquences.

Pour cela modifier la fonction `search_words_in_proteome()` de façon à compter le nombre d'occurrences d'un mot dans les séquences. La méthode `count()` vous sera utile.

Déterminez alors quel mot est le plus fréquent dans le protéome humain.

## 27.2.2 genbank2basta (sans expression régulière)

Ce projet consiste à écrire un convertisseur de fichier, du format GenBank au format FASTA. L'annexe A *Quelques formats de données en biologie* rappelle les caractéristiques de ces deux formats de fichiers.

Le jeu de données avec lequel nous allons travailler est le fichier GenBank du chromosome I de la levure du boulanger *Saccharomyces cerevisiae*. Les indications pour le télécharger sont indiqués dans la description du projet.

Dans cette rubrique, nous allons réaliser ce projet **sans expression régulière**.

### 27.2.2.1 Lecture du fichier

Créez un script `genbank2basta.py` et créez la fonction `lit_fichier()` qui prend en argument le nom du fichier et qui renvoie le contenu du fichier sous forme d'une liste de lignes, chaque ligne étant elle-même une chaîne de caractères.

Testez cette fonction avec le fichier GenBank `NC_001133.gbk` et affichez le nombre de lignes lues.

### 27.2.2.2 Extraction du nom de l'organisme

Dans le même script, ajoutez la fonction `extrait_organisme()` qui prend en argument le contenu du fichier précédemment obtenu avec la fonction `lit_fichier()` (sous la forme d'une liste de lignes) et qui renvoie le nom de l'organisme. Pour récupérer la bonne ligne vous pourrez tester si les premiers caractères de la ligne contiennent le mot-clé `ORGANISM`.

Testez cette fonction avec le fichier GenBank `NC_001133.gbk` et affichez le nom de l'organisme.
### 27.2.2.3 Recherche des gènes

Dans le fichier GenBank, les gènes sens sont notés de cette manière :

<table><tr><td>gene</td><td>58..272</td></tr></table>

ou

<table><tr><td>gene</td><td>&lt;2480..&gt;2707</td></tr></table>

et les gènes antisens (ou encore complémentaires) de cette façon :

<table><tr><td>gene</td><td>complement(55979..56935)</td></tr></table>

ou

<table><tr><td>gene</td><td>complement(&lt;13363..&gt;13743)</td></tr></table>

Les valeurs numériques séparées par `..` indiquent la position du gène dans le génome (numéro de la première base, numéro de la dernière base).

#### Remarque

Le symbole `<` indique un gène partiel sur l'extrémité 5', c'est-à-dire que le codon START correspondant est incomplet. Respectivement, le symbole `>` désigne un gène partiel sur l'extrémité 3', c'est-à-dire que le codon STOP correspondant est incomplet. Pour plus de détails, consultez la documentation du NCBI sur les délimitations des gènes<fnref n="12" />. Nous vous proposons ici d'ignorer ces symboles `>` et `<`.

Repérez ces différents gènes dans le fichier `NC_001133.gbk`. Pour récupérer ces lignes de gènes il faut tester si la ligne commence par

<table><tr><td>gene</td></tr></table>

(c'est-à-dire 5 espaces, suivi du mot gene, suivi de 12 espaces). Pour savoir s'il s'agit d'un gène sur le brin direct ou complémentaire, il faut tester la présence du mot *complement* dans la ligne lue.

Ensuite si vous souhaitez récupérer la position de début et de fin de gène, nous vous conseillons d'utiliser la fonction `replace()` et de ne garder que les chiffres et les `.` Par exemple

<table><tr><td>gene</td><td>&lt;2480..&gt;2707</td></tr></table>

sera transformé en

2480..2707

Enfin, avec la méthode `split()` vous pourrez facilement récupérer les deux entiers de début et de fin de gène.

Dans le même script `genbank2 fasta.py`, ajoutez la fonction `recherche_genes()` qui prend en argument le contenu du fichier (sous la forme d'une liste de lignes) et qui renvoie la liste des gènes.

Chaque gène sera lui-même une liste contenant le numéro de la première base, le numéro de la dernière base et une chaîne de caractère `"sens"` pour un gène sens et `"antisens"` pour un gène antisens.

Testez cette fonction avec le fichier GenBank `NC_001133.gbk` et affichez le nombre de gènes trouvés, ainsi que le nombre de gènes sens et antisens.

### 27.2.2.4 Extraction de la séquence nucléique du génome

La taille du génome est indiqué sur la première ligne d'un fichier GenBank. Trouvez la taille du génome stocké dans le fichier `NC_001133.gbk`.

Dans un fichier GenBank, la séquence du génome se trouve entre les lignes

ORIGIN

<footnote n="12">12. https://www.ncbi.nlm.nih.gov/Sitemap/samplerecord.html#BaseSpanB</footnote>
### 27.2.2.7 Extraction des gènes

Toujours dans le même script, ajoutez la fonction `extrait_genes()` qui prend en argument la liste des gènes, la séquence nucléotidique complète (sous forme d'une chaîne de caractères) et le nom de l'organisme (sous forme d'une chaîne de caractères) et qui pour chaque gène :

* extrait la séquence du gène dans la séquence complète;

* prend la séquence complémentaire inverse (avec la fonction `construit_compInverse()` si le gène est antisens;

* enregistre le gène dans un fichier au format FASTA (avec la fonction `ecrit_fasta()`);

* affiche à l'écran le numéro du gène et le nom du fichier FASTA créé.

La première ligne des fichiers FASTA sera de la forme :

>nom-organisme|numéro-du-gène|début|fin|sens ou antisens

Le numéro du gène sera un numéro consécutif depuis le premier gène jusqu'au dernier. Il n'y aura pas de différence de numérotation entre les gènes sens et les gènes antisens.

Testez cette fonction avec le fichier GenBank NC_001133.gbk.

### 27.2.2.8 Assemblage du script final

Pour terminer, modifiez le script `genbank2fasta.py` de façon à ce que le fichier GenBank à analyser (dans cet exemple `NC_001133.gbk`), soit entré comme argument du script.

Vous afficherez un message d'erreur si :

* le script `genbank2fasta.py` est utilisé sans argument,

* le fichier fourni en argument n'existe pas.

Pour vous aider, n'hésitez pas à jeter un œil aux descriptions des modules `sys` et `pathlib` dans le chapitre 9 *Modules*.

Testez votre script ainsi finalisé.

Bravo, si vous êtes arrivés jusqu'à cette étape.

### 27.2.3 genbank2fasta (avec expressions régulières)

Ce projet consiste à écrire un convertisseur de fichier, du format GenBank au format FASTA. L'annexe A *Quelques formats de données en biologie* rappelle les caractéristiques de ces deux formats de fichiers.

Le jeu de données avec lequel nous allons travailler est le fichier GenBank du chromosome I de la levure du boulanger *Saccharomyces cerevisiae*. Les indications pour le télécharger sont indiqués dans la description du projet.

Dans cette rubrique, nous allons réaliser ce projet **avec des expressions régulières** en utilisant le module `re`.

#### 27.2.3.1 Lecture du fichier

Créez un script `genbank2fasta.py` et créez la fonction `lit_fichier()` qui prend en argument le nom du fichier et qui renvoie le contenu du fichier sous forme d'une liste de lignes, chaque ligne étant elle-même une chaîne de caractères.

Testez cette fonction avec le fichier GenBank NC_001133.gbk et affichez le nombre de lignes lues.

#### 27.2.3.2 Extraction du nom de l'organisme

Dans le même script, ajoutez la fonction `extrait_organisme()` qui prend en argument le contenu du fichier précédemment obtenu avec la fonction `lit_fichier()` (sous la forme d'une liste de lignes) et qui renvoie le nom de l'organisme. Utilisez de préférence une expression régulière.

Testez cette fonction avec le fichier GenBank NC_001133.gbk et affichez le nom de l'organisme.

#### 27.2.3.3 Recherche des gènes

Dans le fichier GenBank, les gènes sens sont notés de cette manière :

<table><tr><td>gene</td><td>58..272</td></tr><tr><td>ou</td><td></td></tr><tr><td>gene</td><td>&lt;2480..&gt;2707</td></tr></table>
et les gènes antisens de cette façon :

gene complement(55979..56935)

ou

gene complement(<13363..>13743)

Les valeurs numériques séparées par .. indiquent la position du gène dans le génome (numéro de la première base, numéro de la dernière base).

### Remarque

Le symbole < indique un gène partiel sur l'extrémité 5', c'est-à-dire que le codon START correspondant est incomplet. Respectivement, le symbole > désigne un gène partiel sur l'extrémité 3', c'est-à-dire que le codon STOP correspondant est incomplet. Pour plus de détails, consultez la documentation du NCBI sur les délimitations des gènes<fnref n="13" />.

Repérez ces différents gènes dans le fichier `NC_001133.gbk`. Construisez deux expressions régulières pour extraire du fichier GenBank les gènes sens et les gènes antisens.

Modifiez ces expressions régulières pour que les numéros de la première et de la dernière base puissent être facilement extraits.

Dans le même script `genbank2 fasta.py`, ajoutez la fonction `recherche_genes()` qui prend en argument le contenu du fichier (sous la forme d'une liste de lignes) et qui renvoie la liste des gènes.

Chaque gène sera lui-même une liste contenant le numéro de la première base, le numéro de la dernière base et une chaîne de caractère `"sens"` pour un gène sens et `"antisens"` pour un gène antisens.

Testez cette fonction avec le fichier GenBank `NC_001133.gbk` et affichez le nombre de gènes trouvés, ainsi que le nombre de gènes sens et antisens.

### 27.2.3.4 Extraction de la séquence nucléique du génome

La taille du génome est indiqué sur la première ligne d'un fichier GenBank. Trouvez la taille du génome stocké dans le fichier `NC_001133.gbk`.

Dans un fichier GenBank, la séquence du génome se trouve entre les lignes

ORIGIN

et

//

Trouvez dans le fichier `NC_001133.gbk` la première et dernière ligne de la séquence du génome.

Construisez une expression régulière pour extraire du fichier GenBank les lignes correspondantes à la séquence du génome.

Modifiez ces expressions régulières pour que la séquence puisse être facilement extraite.

Toujours dans le même script, ajoutez la fonction `extrait_sequence()` qui prend en argument le contenu du fichier (sous la forme de liste de lignes) et qui renvoie la séquence nucléique du génome (dans une chaîne de caractères). La séquence ne devra pas contenir d'espaces.

Testez cette fonction avec le fichier GenBank `NC_001133.gbk` et affichez le nombre de bases de la séquence extraite. Vérifiez que vous n'avez pas fait d'erreur en comparant la taille de la séquence extraite avec celle que vous avez trouvée dans le fichier GenBank.

### 27.2.3.5 Construction d'une séquence complémentaire inverse

Toujours dans le même script, ajoutez la fonction `construit_comp_inverse()` qui prend en argument une séquence d'ADN sous forme de chaîne de caractères et qui renvoie la séquence complémentaire inverse (également sous la forme d'une chaîne de caractères).

On rappelle que construire la séquence complémentaire inverse d'une séquence d'ADN consiste à :

<footnote n="13">13. https://www.ncbi.nlm.nih.gov/Sitemap/samplerecord.html#BaseSpanB</footnote>
### 27.2.4 Simulation d'un pendule

L'objectif de ce projet est de simuler un pendule simple<fnref n="14" /> en deux dimensions, puis de le visualiser à l'aide du module `tkinter`. Le projet peut s'avérer complexe. Tout d'abord, il y a l'aspect physique du projet. Nous allons faire ici tous les rappels de mécanique nécessaires à la réalisation du projet. Ensuite, il y a la partie `tkinter` qui n'est pas évidente au premier abord. Nous conseillons de bien séparer les deux parties. D'abord réaliser la simulation physique et vérifier qu'elle fonctionne (par exemple, en écrivant un fichier de sortie permettant cette vérification). Ensuite passer à la partie graphique `tkinter` si et seulement si la première partie est fonctionnelle.

#### 27.2.4.1 Mécanique d'un pendule simple

Nous allons décrire ici ce dont nous avons besoin concernant la mécanique d'un pendule simple. Notamment, nous allons vous montrer comment dériver l'équation différentielle permettant de calculer la position du pendule à tout moment en fonction des conditions initiales. Cette page est largement inspirée de la page Wikipedia en anglais<fnref n="15" />. Dans la suite, une grandeur représentée en gras, par exemple **P**, représente un vecteur avec deux composantes dans le plan 2D ($P_x, P_y$). Cette notation en gras est équivalente à la notation avec une flèche au dessus de la lettre. La même grandeur représentée en italique, par exemple **P**, représente le nombre scalaire correspondant. Ce nombre peut être positif ou négatif, et sa valeur absolue vaut la norme du vecteur.

Un pendule simple est représenté par une masse ponctuelle (la boule du pendule) reliée à un axe immobile par une tige rigide et sans masse. Le pendule simple est un système idéal. Ainsi, on néglige les effets de frottement et on considère le champ gravitationnel comme uniforme. La figure 27.2 montre un schéma du système ainsi qu'un bilan des forces agissant sur la masse. Les deux forces agissant sur la boule sont son poids **P** et la tension **T** due à la tige.

La figure 27.3 montre un schéma des différentes grandeurs caractérisant le pendule. La coordonnée naturelle pour définir la position du pendule est l'angle $\theta$. Nous verrons plus tard comment convertir cet angle en coordonnées cartésiennes pour l'affichage dans un `canvas` `tkinter`. Nous choisissons de fixer $\theta = 0$ lorsque le pendule est à sa position d'équilibre. Il s'agit de la position où la boule est au plus bas. C'est une position à laquelle le pendule ne bougera pas s'il n'a pas une vitesse préexistante. Nous choisissons par ailleurs de considérer $\theta$ positif lorsque le pendule se balance à droite, et négatif de l'autre côté. **g** décrit l'accélération due à la gravité, avec $\mathbf{P} = mg$, ou si on raisonne en scalaire $P = mg$. Les deux vecteurs représentant les composantes tangentielle et orthogonale au mouvement du pendule de **P** sont représentées sur le schéma (les annotations indiquent leur norme).

Si on déplace le pendule de sa position d'équilibre, il sera mû par la force **F** résultant de la tension **T** et de son poids **P** (cf. plus bas). Comme le système est considéré comme parfait (pas de frottement, gravité uniforme, etc.), le pendule ne s'arrêtera jamais. Si on le monte à $\theta = +20$ deg et qu'on le lâche, le pendule redescendra en passant par $\theta = 0$ deg, remontera de l'autre côté à $\theta = -20$ deg, puis continuera de la sorte indéfiniment, grâce à la conservation de l'énergie dans un système fermé (c'est-à-dire sans « fuite » d'énergie).

Ici, on va tenter de simuler ce mouvement en appliquant les lois du mouvement de Newton<fnref n="16" /> et en résolvant les équations correspondantes numériquement. D'après la seconde loi de Newton, la force (F) agissant sur la boule est égale à sa masse (m) fois son accélération (a) :

$$F = ma$$

Cette loi est exprimée ici dans le système de coordonnées cartésiennes (le plan à 2 dimensions). La force **F** et l'accélération **a** sont des vecteurs dont les composantes sont respectivement ($F_x, F_y$) et ($a_x, a_y$). La force **F** correspond à la somme vectorielle de **T** et **P**. La tige du pendule étant rigide, le mouvement de la boule est restreint sur le cercle de rayon égal à la longueur $L$ de la tige (dessiné en pointillé). Ainsi, seule la composante tangentielle de l'accélération **a** sera prise en compte dans ce mouvement. Comment la calculer ? La force de tension **T** étant orthogonale au mouvement du pendule, celle-ci n'aura pas d'effet. De même, la composante orthogonale $mg\cos\theta$ due au poids **P** n'aura pas d'effet non plus. Au final, on ne prendra en compte que la composante tangentielle due au poids, c'est-à-dire $mgsin\theta$ (cf. figure 27.3). Au final, on peut écrire l'expression suivante en raisonnant sur les valeurs scalaires :

$$F = ma = -mgsin\theta$$

<footnote n="14">14. https://fr.wikipedia.org/wiki/Pendule_simple</footnote>

<footnote n="15">15. https://en.wikipedia.org/wiki/Pendulum_(mathematics)</footnote>

<footnote n="16">16. https://fr.wikipedia.org/wiki/Lois_du_mouvement_de_Newton</footnote>
<figure><img src="image_67.png" /><figcaption>FIGURE 27.2 – Bilan des forces dans un pendule simple.</figcaption></figure>

Le signe – dans cette formule est très important. Il indique que l'accélération s'oppose systématiquement à $\theta$. Si le pendule se balance vers la droite et que $\theta$ devient plus positif, l'accélération tendra toujours à faire revenir la boule dans l'autre sens vers sa position d'équilibre à $\theta = 0$. On peut faire un raisonnement équivalent lorsque le pendule se balance vers la gauche et que $\theta$ devient plus négatif.

Si on exprime l'accélération en fonction de $\theta$, on trouve ce résultat qui peut sembler peu intuitif au premier abord :

$$a = -g\sin\theta$$

Le mouvement du pendule ne dépend pas de sa masse !

Idéalement, nous souhaiterions résoudre cette équation en l'exprimant en fonction de $\theta$ seulement. Cela est possible en reliant $\theta$ à la longueur effective de l'arc $s$ parcourue par le pendule :

$$s = \theta L$$

Pour bien comprendre cette formule, souvenez-vous de la formule bien connue du cercle $l = 2\pi r$ (où $l$ est la circonférence, et $r$ le rayon)! Elle relie la valeur de $\theta$ à la distance de l'arc entre la position actuelle de la boule et l'origine (à $\theta = 0$). On peut donc exprimer la vitesse du pendule en dérivant $s$ par rapport au temps $t$ :

$$v = \frac{ds}{dt} = L\frac{d\theta}{dt}$$

On peut aussi exprimer l'accélération $a$ en dérivant l'arc $s$ deux fois par rapport à $t$ :
<figure><img src="image_68.png" /><figcaption>FIGURE 27.3 – Caractérisation géométrique d'un pendule simple.</figcaption></figure>

$$a = \frac{d^2 s}{dt^2} = L \frac{d^2 \theta}{dt^2}$$

A nouveau, cette dernière formule exprime l'accélération de la boule lorsque le mouvement de celle-ci est restreint sur le cercle pointillé. Si la tige n'était pas rigide, l'expression serait différente.

Si on remplace a dans la formule ci-dessus, on trouve :

$$L \frac{d^2 \theta}{dt^2} = -g \sin \theta$$

Soit en remaniant, on trouve l'équation différentielle en θ décrivant le mouvement du pendule :

$$\frac{d^2 \theta}{dt^2} + \frac{g}{L} \sin \theta = 0$$

Dans la section suivante, nous allons voir comment résoudre numériquement cette équation différentielle.

### 27.2.4.2 Résolution de l'équation différentielle du pendule

Il existe de nombreuses méthodes numériques de résolution d'équations différentielles<fnref n="17" />. L'objet ici n'est pas de faire un rappel sur toutes ces méthodes ni de les comparer, mais juste d'expliquer une de ces méthodes fonctionnant efficacement pour simuler notre pendule.

Nous allons utiliser la méthode semi-implicite d'Euler<fnref n="18" />. Celle-ci est relativement intuitive à comprendre.

<footnote n="17">17. https://en.wikipedia.org/wiki/Numerical_methods_for_ordinary_differential_equations</footnote>

<footnote n="18">18. https://en.wikipedia.org/wiki/Semi-implicit_Euler_method</footnote>
Commençons d'abord par calculer l'accélération angulaire $a_\theta$ au temps $t$ en utilisant l'équation différentielle précédemment établie :

$$a_{\theta}(t) = \frac{d^2\theta}{dt^2}(t) = -\frac{g}{L}\sin\theta(t)$$

L'astuce sera de calculer ensuite la vitesse angulaire au pas suivant $t + \delta t$ grâce à la relation :

$$v_{\theta}(t + \delta t) = \frac{d\theta}{dt}(t + \delta t) \approx v_{\theta}(t) + a_{\theta}(t) \times \delta t$$

Cette équation est ni plus ni moins qu'un remaniement de la définition de l'accélération, à savoir, la variation de vitesse par rapport à un temps. Cette vitesse $v_\theta(t + \delta t)$ permettra au final de calculer $\theta$ au temps $t + \delta t$ (c'est-à-dire ce que l'on cherche !) :

$$\theta(t + \delta t) \approx \theta(t) + v_{\theta}(t + \delta t) \times \delta t$$

Dans une réalisation algorithmique, il suffira d'initialiser les variables de notre système puis de faire une boucle sur un nombre de pas de simulation. A chaque pas, on calculera $a_\theta(t)$, puis $v_\theta(t + \delta t)$ et enfin $\theta(t + \delta t)$ à l'aide des formules ci-dessus.

L'initialisation des variables pourra ressembler à cela :

```python
L <- 1      # longueur tige en m

g <- 9.8    # accélération gravitationnelle en m/s^2
t <- 0      # temps initial en s
dt <- 0.05  # pas de temps en s

# conditions initiales
theta <- pi / 4  # angle initial en rad
dtheta <- 0     # vitesse angulaire initiale en rad/s

afficher_position_pendule(t, theta) # afficher position de départ
```

L'initialisation des valeurs de $\theta$ et $d\theta$ est très importante, car elle détermine le comportement du pendule. Nous avons choisi ici d'avoir une vitesse angulaire nulle et un angle de départ du pendule $\theta = \pi/4$ rad = 45 deg. Le pas $dt$ est également très important, c'est lui qui déterminera l'erreur faite sur l'intégration de l'équation différentielle. Plus ce pas est petit, plus on est précis, mais plus le calcul sera long. Ici, on choisit un pas $dt$ de 0.05 s qui constitue un bon compromis.

À ce stade, vous avez tous les éléments pour tester votre pendule. Essayez de réaliser un petit programme python `pendule basic.py` qui utilise les conditions initiales ci-dessus et simule le mouvement du pendule. À la fin de cette rubrique, nous proposons une solution en langage algorithmique. Essayez dans un premier temps de le faire vous-même. À chaque pas, le programme écrira le temps $t$ et l'angle $\theta$ dans un fichier `pendule basic.dat`. Dans les équations, $\theta$ doit être exprimé en radian, mais nous vous conseillons de convertir cet angle en degré dans le fichier (plus facile à comprendre pour un humain !). Une fois ce fichier généré, vous pourrez observer le graphe correspondant avec ` matplotlib` en utilisant le code suivant :

```python
import matplotlib.pyplot as plt
import numpy as np

# La fonction np.genfromtxt() renvoie un array à 2 dim.
array_data = np.genfromtxt("pendule_bASIC.dat")

# col 0: t, col 1: theta
7 t = array_data[:,0]
8 theta = array_data[:,1]
9
10 # Figure.
11 fig, ax = plt.subplots(figsize=(8, 8))
12 mini = min theta) * 1.2
13 maxi = max theta) * 1.2
14 ax.set_xlim(0, maxi)
15 ax.set_ylim(mini, maxi)
16 ax.set_xlabel("t (s)")
17 ax.set_ylabel("theta (deg)")
18 ax.plot(t, theta)
19 fig.savefig("pendule_bASIC.png")
```
Si vous observez une sinusoïde, bravo, vous venez de réaliser votre première simulation de pendule ! Vous avez maintenant le « squelette » de votre « moteur » de simulation. N'hésitez pas à vous amuser avec d'autres conditions initiales. Ensuite vous pourrez passer à la rubrique suivante.

Si vous avez bloqué dans l'écriture de la boucle, voici à quoi elle pourrait ressembler en langage algorithmique :

tant qu'on n'arrête pas le pendule:
```
# acc angulaire au tps t (en rad/s^2)
d2theta <- -(g/L) * sin theta
# v angulaire mise à jour de t -> t + dt
dtheta <- dtheta + d2theta * dt
# theta mis à jour de t -> t + dt
theta <- theta + dtheta * dt
# t mis à jour
t <- t + dt
# mettre à jour l'affichage
afficher_position_pendule(t, theta)
```

### 27.2.4.3 Constructeur de l'application en `tkinter`

Nous allons maintenant construire l'application `tkinter` en vous guidant pas à pas. Il est bien sûr conseillé de relire le chapitre 25 sur *Fenêtres graphiques et Tkinter* (en ligne) avant de vous lancer dans cette partie.

Comme expliqué largement dans les chapitres 23 *Avoir la classe avec les objets* et 24 *Avoir plus la classe avec les objets* (en ligne), nous allons construire l'application avec une classe. Le programme principal sera donc très allégé et se contentera d'instancier l'application, puis de lancer le gestionnaire d'événements :

```python
1 if __name__ == "__main__":
2     """Programme principal (instancie la classe principale, donne un
titre et lance le gestionnaire d'événements)
3
4 """
5 app_pendule = AppliPendule()
6 app_pendule.title("Pendule")
7 app_pendule.mainloop()
```

Ensuite, nous commençons par écrire le constructeur de la classe. Dans ce constructeur, nous aurons une section initialisant toutes les variables utilisées pour simuler le pendule (voir rubrique précédente), puis, une autre partie générant les *widgets* et tous les éléments graphiques. Nous vous conseillons vivement de bien les séparer, et surtout de **mettre des commentaires** pour pouvoir s'y retrouver. Voici un « squelette » pour vous aider :

```python
1 class AppliPendule.tk.Tk):
2     def __init__(self):
3         # Instanciation de la classe Tk.
4         tk.Tk.__init__(self)
5         # Ici vous pouvez définir toutes les variables
6         # concernant la physique du pendule.
7         selfopp = np.pi / 4  # valeur intiale theta
8         self.dtheta = 0    # vitesse angulaire initiale
9         [...]
10        self.g = 9.8    # cst gravitationnelle en m/s^2
11         [...]
12         # Oci vous pouvez construire l'application graphique.
13         self.canv = tk.Canvas(self, bg='gray', height=400, width=400)
14         # Création d'un bouton demarrer, arreter, quitter.
15         # Pensez à placer les widgets avec .pack()
16         [...]
```

La figure 27.4 vous montre un aperçu de ce que l'on voudrait obtenir.

Pour le moment, vous pouvez oublier la réglette fixant la valeur initiale de $\theta$, les *labels* affichant la valeur de $\theta$ et $v_\theta$ ainsi que les points violets « laissés en route » par le pendule. De même, nous dessinerons le pivot, la boule et la tige plus tard. À ce stade, il est fondamental de tout de suite lancer votre application pour vérifier que les *widgets* sont bien placés. N'oubliez pas, un code complexe se teste **au fur et à mesure** lors de son développement.

**Conseil**
<figure><img src="image_69.png" /><figcaption>FIGURE 27.4 – Application pendule.</figcaption></figure>

Pour éviter un message d'erreur si toutes les méthodes n'existe pas encore, vous pouvez indiquer `command self.quit` pour chaque bouton (vous le changerez après).

### 27.2.4.4 Créations des dessins dans le canvas

Le pivot et la boule pourront être créés avec la méthode `create_oval()`, la tige le sera avec la méthode `create_line()`. Pensez à créer des variables pour la tige et la boule lors de l'instanciation car celles-ci bougeront par la suite.

Comment placer ces éléments dans le `canvas` ? Vous avez remarqué que lors de la création de ce dernier, nous avons fixé une dimension de 400 × 400 pixels. Le pivot se trouve au centre, c'est-à-dire au point (200,200). Pour la tige et la boule, il sera nécessaire de connaître la position de la boule **dans le repère du canvas**. Or, pour l'instant, nous définissons la position de la boule avec l'angle `θ`. Il va donc nous falloir convertir `θ` en coordonnées cartésiennes `(x,y)` dans le repère mathématique défini dans la figure 27.3, puis dans le repère du `canvas` `(x_c, y_c)` (cf. rubrique suivante).

**Conversion de θ en coordonnées (x,y)** Cette étape est relativement simple si on considère le pivot comme le centre du repère. Avec les fonctions trigonométriques `sin()` et `cos()`, vous pourrez calculer la position de la boule (voir l'exercice sur la spirale dans le chapitre 7 *Fichiers*). Faites attention toutefois aux deux aspects suivants :

* la trajectoire de la boule suit les coordonnées d'un cercle de rayon L (si on choisit $L = 1$ m, ce sera plus simple);

* nous sommes décalés par rapport au cercle trigonométrique classique ; si on considère $L = 1$ m :

— quand $\theta = 0$, on a le point $(0, -1)$ (pendule en bas);

— quand $\theta = +\pi/2 = 90$ deg, on a $(1, 0)$ (pendule à droite);

— quand $\theta = -\pi/2 = -90$ deg, on a $(-1, 0)$ (pendule à gauche);

— quand $\theta = \pm\pi = \pm 180$ deg, on a $(0, 1)$ (pendule en haut).

La figure 27.3 montre graphiquement les valeurs de $\theta$.

Si vous n'avez pas trouvé, voici la solution :

1. `self.x = np.sin(self theta) * self.L`

2. `self.y = -np.cos(self theta) * self.L`
**Conversion des coordonnées (x,y) en (x_c, y_c)** Il nous faut maintenant convertir les coordonnées naturelles mathématiques du pendule (x,y) en coordonnées dans le *canvas* (x_c, y_c). Plusieurs choses sont importantes pour cela :

* le centre du repère mathématique (0,0) a la coordonnée (200,200) dans le *canvas*;

* il faut choisir un facteur de conversion : par exemple, si on choisit $L = 1 \text{ m}$, on peut proposer le facteur $1 \text{ m} \rightarrow 100 \text{ pixels}$;

* l'axe des ordonnées dans le *canvas* est **inversé** par rapport au repère mathématique.

**Conseil**

Dans votre classe, cela peut être une bonne idée d'écrire une méthode qui réalise cette conversion. Celle-ci pourrait s'appeler par exemple `map_realcoor2canvas()`.

Si vous n'avez pas trouvé, voici la solution :

1. `self(conv_factor = 100`

2. `self.x_c = self.x self(conv_factor + 200`

3. `self.y_c = -self.y self(conv_factor + 200`

### 27.2.4.5 Gestion des boutons

Il reste maintenant à gérer les boutons permettant de démarrer / stopper le pendule. Pour cela il faudra créer trois méthodes dans notre classe :

* La méthode `start()` : met en mouvement le pendule; si le pendule n'a jamais été en mouvement, il part de son point de départ; si le pendule avait déjà été en mouvement, celui-ci repart d'où on l'avait arrêté (avec la vitesse qu'il avait à ce moment-là).

* La méthode `stop()` : arrête le mouvement du pendule.

* La méthode `move()` : gère le mouvement du pendule (génère les coordonnées du pendule au pas suivant).

Le bouton « Démarrer » appellera la méthode `start()`, le bouton « Arrêter » appellera la méthode `stop()` et le bouton « Quitter » quittera l'application. Pour lier une action au clic d'un bouton, on se souvient qu'il faut donner à l'argument par mot-clé *command* une *callback* (c'est-à-dire le nom d'une fonction ou méthode sans les parenthèses) :

* `btn1 = tk Button(self, text="Quitter", command = self.quit)`

* `btn2 = tk Button(self, text="Demarrer", command = self.start)`

* `btn3 = tk Button(self, text="Arrêter", command = self.stop)`

Ici, `self.start()` et `self.stop()` sont des méthodes que l'on doit créer, `self.quit()` pré-existe lorsque la fenêtre `tkinter` est créée.

Nous vous proposons ici une stratégie inspirée du livre de Gérard Swinnen<fnref n="19" />. Créons d'abord un attribut d'instance `self.is_moving` dans le constructeur. Celui-ci va nous servir de « drapeau » pour définir le mouvement du pendule. Il contiendra un entier positif ou nul. Lorsque ce drapeau sera égal à 0, le pendule sera immobile. Lorsqu'il sera > 0, le pendule sera en mouvement. Ainsi :

* la méthode `start()` ajoutera 1 à `self.is_moving`. Si `self.is_moving` est égal à 1 alors la méthode `self.move()` sera appelée;

* la méthode `stop()` mettra la valeur de `self.is_moving` à 0.

Puisque `start()` ajoute 1 à `self.is_moving`, le premier clic sur le bouton « Démarrer » appellera la méthode `move()` car `self.is_moving` vaudra 1. Si l'utilisateur appuie une deuxième fois sur le bouton « Démarrer », `self.is_moving` vaudra 2, mais n'appellera pas `move()` une deuxième fois; cela sera vrai pour tout clic ultérieur de l'utilisateur sur ce bouton. Cette astuce évite des appels concurrents de la méthode `move()`.

### 27.2.4.6 Le coeur du programme : la méthode `move()`

Il nous reste maintenant à générer la méthode `move()` qui meut le pendule. Pour cela vous pouvez vous inspirer de la rubrique *Un canvas animé dans une classe du chapitre 25 Fenêtres graphiques et Tkinter* (en ligne).

Cette méthode va réaliser un pas de simulation de $t$ à $t + \delta t$. Il faudra ainsi réaliser dans l'ordre :

<footnote n="19">19. https://inforef.be/swi/python.htm</footnote>
* Calculer la nouvelle valeur de θ (self Bout) au pas t + δt comme nous l'avons fait précédemment avec la méthode semi-implicite d'Euler.

* Convertir la nouvelle valeur de θ (self Bout) en coordonnées cartésiennes dans le repère du pendule (self.x et self.y).

* Convertir ces coordonnées cartésiennes dans le repère du Canvas (self.x_c et self.y_c).

* Mettre à jour le dessin de la baballe et de la tige avec la méthode self.canv.coords().

• Incrémenter le pas de temps.

* Si le drapeau self.is_moving est supérieur à 0, la méthode self.move() est rappelée après 20 millisecondes (Conseil : la méthode .after() est votre amie).

27.2.4.7 Ressources complémentaires

Si vous êtes arrivé jusqu'ici, bravo, vous pouvez maintenant admirer votre superbe pendule en mouvement :-) !

Voici quelques indications si vous voulez aller un peu plus loin.

Si vous souhaitez mettre une réglette pour modifier la position de départ du pendule, vous pouvez utiliser la classe tk Scale(). Si vous souhaitez afficher la valeur de θ qui se met à jour au fur et à mesure, il faudra instancier un objet avec la classe StringVar(). Cet objet devra être passé à l'argument textvariable lors de la création de ce Label avec la Label(). Ensuite, vous pourrez mettre à jour le texte du Label avec la méthode self instance StringVar .set() .

Pour le fun, si vous souhaitez laisser une « trace » du passage du pendule avec des points colorés, vous pouvez utiliser tout simplement la méthode self.canv.create_line() et créer une ligne d'un pixel de hauteur et de largeur pour dessiner un point. Pour améliorer l'esthétique, vous pouvez faire en sorte que ces points changent de couleur aléatoirement à chaque arrêt / redémarrage du pendule.

27.3 Scripts de correction

Voici les scripts corrigés pour les différents mini-projets.

Remarque

* Prenez le temps de chercher par vous-même avant de télécharger les scripts de correction.

* Nous proposons une correction. D'autres solutions sont possibles.

* Mots anglais dans le protéome humain : words_in_proteome.py 20

* Genbank2 fasta (sans expression régulière) : genbank2 fasta_sans regex.py 21

* Genbank2 fasta (avec expressions régulières) : genbank2 fasta_avec regex.py 22

* Simulation d'un pendule version simple : tk_pendule.py 23

* Simulation d'un pendule++ (avec réglette et affichage se mettant à jour) : tk_pendule.py 24

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

<footnote n="21">21. https://python.sdv.u-paris.fr/data-files/genbank2 fasta_sans regex.py</footnote>

<footnote n="22">22. https://python.sdv.u-paris.fr/data-files/genbank2 fasta_avec regex.py</footnote>

<footnote n="23">23. https://python.sdv.u-paris.fr/data-files/tk_pendule_simple.py</footnote>

<footnote n="24">24. https://python.sdv.u-paris.fr/data-files/tk_pendule.py</footnote>
# Quelques formats de données en biologie

## A.1 FASTA

Le format FASTA est utilisé pour stocker une ou plusieurs séquences, d'ADN, d'ARN ou de protéines. Ces séquences sont classiquement représentées sous la forme :

```
>en-tête
séquence avec un nombre maximum de caractères par ligne
séquence avec un nombre maximum de caractères par ligne
séquence avec un nombre maximum de caractères par ligne
séquence avec un nombre maximum de caractères par ligne
séquence avec un nombre max
```

La première ligne débute par le caractère > et contient une description de la séquence. On appelle souvent cette ligne « ligne de description » ou « ligne de commentaire ».

Les lignes suivantes contiennent la séquence à proprement dite, mais avec un nombre maximum fixe de caractères par ligne. Ce nombre maximum est généralement fixé à 60, 70 ou 80 caractères. Une séquence de plusieurs centaines de bases ou de résidus est donc répartie sur plusieurs lignes.

Un fichier est dit *multifasta* lorsqu'il contient plusieurs séquences au format FASTA, les unes à la suite des autres.

Les fichiers contenant une ou plusieurs séquences au format FASTA portent la plupart du temps l'extension `.fasta` mais on trouve également `.seq`, `.fas`, `.fna` ou `.faa`.

### A.1.1 Exemples

La séquence protéique au format FASTA de l'insuline humaine<fnref n="1" />, extraite de la base de données UniProt, est :

```
>sp|P01308|INS_HUMAN Insulin OS=Homo sapiens OX=9606 GN=INS PE=1 SV=1
MALWMRLLPLLALLALWGPDPAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAED
LQVGQVELGGGPAGSLQPLALEGSLQKRGIVEQCCTSICSLYQLENYCN
```

La première ligne contient la description de la séquence (*Insulina*), le type de base de données (ici, *sp* qui signifie Swiss-Prot), son identifiant (*P01308*) et son nom (*INS_HUMAN*) dans cette base de données, ainsi que d'autres informations (*OS=Homo sapiens OX=9606 GN=INS PE=1 SV=1B*).

Les lignes suivantes contiennent la séquence sur des lignes ne dépassant pas, ici, 60 caractères. La séquence de l'insuline humaine est composée de 110 acides aminés, soit une ligne de 60 caractères et une seconde de 50 caractères.

<footnote n="1">1. https://www.uniprot.org/uniprot/P01308</footnote>
## Définition

UniProt<fnref n="2" /> est une base de données de séquences de protéines. Ces séquences proviennent elles-mêmes de deux autres bases de données : Swiss-Prot (où les séquences sont annotées manuellement) et TrEMBL (où les séquences sont annotées automatiquement).

Voici maintenant la séquence nucléique (ARN), au format FASTA, de l'insuline humaine<fnref n="3" />, extraite de la base de données GenBank<fnref n="4" /> :

>BT006808.1 Homo sapiens insulin mRNA, complete cds
ATGGCCCTGTGGATGCGCCTCCTGCCCCTGCTGGCGCTGCTGGCCCTCTGGGGACCTGACCCAGCCGCAG
CCTTTGTGAACCAACACCTGTGCGGCTCACACCTGGTGAAGCTCTCTACCTAGTGTGCGGGGAACGAGG
CTTCTTCTACACACCCAAGACCCGCCGGGAGGCAGAGGACCTGCAGGTGGGGCAGGTGGAGCTGGGCGGG
GGCCCTGGTCAGGCAGCCTGCAGCCCTTGGCCCTGGAGGGGTCCCTGCAGAAGCGTGGCATTGTGGAAC
AATGCTGTACCAGCATCTGCTCCCTCTACCAGCTGGAGAACTACTGCAACTAG

On retrouve sur la première ligne la description de la séquence (*Homo sapiens insulin mRNA*), ainsi que son identifiant (*BT006808.1*) dans la base de données GenBank.

Les lignes suivantes contiennent les 333 bases de la séquence, réparties sur cinq lignes de 70 caractères maximum. Il est curieux de trouver la base T (thymine) dans une séquence d'ARN, qui ne devrait contenir normalement que les bases A, U, G et C. Ici, la représentation d'une séquence d'ARN avec les bases de l'ADN est une convention.

Pour terminer, voici trois séquences protéiques, au format FASTA, qui correspondent à l'insuline humaine (*Homo sapiens*), féline (*Felis catus*) et bovine (*Bos taurus*):

>sp|P01308|INS_HUMAN Insulin OS=Homo sapiens OX=9606 GN=INS PE=1 SV=1
MALWMWLLPLLALLALWGPDPAAAFVNQHLCGSHLVEALYLVCGERGFFYTPKTRREAED
LQVGQVELGGPGAGSLQPLALEGSLQKRGIVEQCCTSICSLYQLENYCN
>sp|P06306|INS_FELCA Insulin OS=Felis catus OX=9685 GN=INS PE=1 SV=2
MAPWTRLLPLALLSLWIPAPTRAFVNQHLCGSHLVEALYLVCGERGFFYTPKARREAED
LQGKDAELGEAPGAGLQPSALEAPLQKRGIVEQCCASVCSLYQLEHYCN
>sp|P01317|INS_BOVIN Insulin OS=Bos taurus OX=9913 GN=INS PE=1 SV=2
MALWTRLRPLLALLALWPPPPARAFVNQHLCGSHLVEALYLVCGERGFFYTPKARREVEG
PQVGALELAGPGAGGLEGPQPKRGIVEQCCASVCSLYQLENYCN

Ces séquences proviennent de la base de données UniProt<fnref n="5" />. Chaque séquence est délimitée par la ligne d'en-tête qui débute par >.

## A.1.2 Manipulation avec Python

À partir de l'exemple précédent des 3 séquences d'insuline, voici un exemple de code qui lit un fichier FASTA avec Python :

1 prot_dict = {}
2 with open("insulin.fasta", "r") as fasta_file:
3 prot_id ="
4 for line in fasta_file:
5 if line startswith (">"):
6 prot_id =line [1: ].split ()[0]
7 prot_dict[prot_id] ="
8 else:
9 prot_dict[prot_id] +=line.strip()
10 for id in prot_dict:
11 print(id)
12 print(prot_dict[id] [ :30])

Pour chaque séquence lue dans le fichier FASTA, on affiche son identifiant et son nom, puis les 30 premiers résidus de sa séquence :

<footnote n="2">2. https://www.uniprot.org/</footnote>

<footnote n="3">3. https://www.ncbi.nlm.nih.gov/nuccore/BT006808.1?report= fasta</footnote>

<footnote n="4">4. https://www.ncbi.nlm.nih.gov/nuccore/AY899304.1?report=genbank</footnote>

<footnote n="5">5. https://www.uniprot.org/</footnote>
sp|P06306|INS_FELCA
MAPWTRLLPLALLSLWIPAPTRAFVNQHL
sp|P01317|INS_BOVIN
MALWTRLRPLLALLALWPPPPARAFVNQHL
sp|P01308|INS_HUMAN
MALWMRLLPLLALLALWGPDPAAAFVNQHL

Notez que les protéines sont stockées dans un dictionnaire (`prot_dict`) où les clefs sont les identifiants et les valeurs les séquences.

On peut faire la même chose avec le module `Biopython` :

1. from Bio import SeqIO
2. with open("insulin.fasta", "r") as fasta_file:
3. for record in SeqIO.parse(fasta_file, " fasta"):
4. print(record.id)
5. print(str(record.seq)[:30])

Cela produit le même résultat. L'utilisation de `Biopython` rend le code plus compacte car on utilise ici la fonction `SeqIO.parse()` qui s'occupe de lire le fichier FASTA.

### Remarque

L'attribut `.id` renvoie l'identifiant d'une séquence, c'est-à-dire la première partie de l'entête, sans le caractère `>`. Pour obtenir l'entête complet (toujours sans le caractère `>`), il faut utiliser l'attribut `.description`.

## A.2 GenBank

GenBank est une banque de séquences nucléiques. Le format de fichier associé contient l'information nécessaire pour décrire un gène ou une portion d'un génome. Les fichiers GenBank portent le plus souvent l'extension `.gbk`.

Le format GenBank est décrit de manière très complète sur le site du NCBI<fnref n="6" />. En voici néanmoins les principaux éléments, avec l'exemple du gène qui code pour la trypsin<fnref n="7" /> chez l'Homme.

### A.2.1 L'en-tête

LOCUS HUMTRPSGNA 800 bp mRNA linear PRI 14-JAN-1995
DEFINITION Human pancreatic trypsin 1 (TRY1) mRNA, complete cds.
ACCESSION M22612
VERSION M22612.1
KEYWORDS trypsinogen.
SOURCE Homo sapiens (human)
ORGANISM Homo sapiens
Eukaryota; Metazoa; Chordata; Craniata; Vertebrata; Euteleostomi;
Mammalia; Eutheria; Euarchontoglires; Primates; Haplorrhini;
Catarrhini; Hominidae; Homo.
[...]]

* **Ligne 1 (LOCUS)** : le nom du locus (*HUMTRPSGNA*), la taille du gène (800 paires de bases), le type de molécule (ARN messager).

* **Ligne 3 (ACCESSION)** : l'identifiant de la séquence (*M22612*).

* **Ligne 4 (VERSION)** : la version de la séquence (*M22612.1*). Le nombre qui est séparé de l'identifiant de la séquence par un point est incrémenté pour chaque nouvelle version de la fiche GenBank. Ici, `.1` indique que nous en sommes à la première version.

* **Ligne 6 (SOURCE)** : la provenance de la séquence (souvent l'organisme d'origine).

* **Ligne 7 (ORGANISME)** : le nom scientifique de l'organisme, suivi de sa taxonomie (**lignes 8 à 10**).

<footnote n="6">6. https://www.ncbi.nlm.nih.gov/Sitemap/samplerecord.html</footnote>

<footnote n="7">7. https://www.ncbi.nlm.nih.gov/nuccore/M22612.1</footnote>
## A.2.2 Les features

[...]

<table><thead><tr><th>FEATURES</th><th>Location/Qualifiers</th></tr></thead><tbody><tr><td>source</td><td>1..800<br/>/organism="Homo sapiens"<br/>/mol_type="mRNA"<br/>/db_xref="taxon:9606"<br/>/map="7q32-qter"<br/>/tissue_type="pancreas"</td></tr><tr><td>gene</td><td>1..800<br/>/gene="TRY1"</td></tr><tr><td>CDS</td><td>7..750<br/>/gene="TRY1"<br/>/codon_start=1<br/>/product="trypsinogen"<br/>/protein_id="AAA61231.1"<br/>/db_xref="GDB:G00-119-620"<br/>/translation="MNPLLILTFVAAALAAPFDDDDKIVGGYNCEENSVPYQVSLNSG YHFCGGSLINEQWVVSAGHCYKSRIQVRLGEHNIEVLEGNEQFINAAKIIRHPQYDRK TLNNDIMLIKLSSRAVINARVSTISLPTAPPATGTKCLISGWGNTASSGADYPDELQC LDAPVLSQAKCEASYPGKITSNMFCVGFLEGGKDSCQGDSGGPVVCNGQLQGVVSWGD GCAQKNKPGVYTKVYNYVKWIKNΤIAANS"</td></tr><tr><td>sig_peptide</td><td>7..51<br/>/gene="TRY1"<br/>/note="G00-119-620"</td></tr></tbody></table>

[...]

* **Ligne 9 (gene 1..800)** : la délimitation du gène. Ici, de la base 1 à la base 800. Par ailleurs, la notation `<x..y>` indique que la séquence est partielle sur l'extrémité 5'. Réciproquement, `x..y>` indique que la séquence est partielle sur l'extrémité 3'. Enfin, pour les séquences d'ADN, la notation `complement(x..y)` indique que le gène se trouve de la base x à la base y, mais sur le brin complémentaire.

* **Ligne 10 (gene="TRY1")** : le nom du gène.

* **Ligne 11 (CDS 7..750)** : la délimitation de la séquence codante.

* **Ligne 14 (product="trypsinogen")** : le nom de la protéine produite.

* **Lignes 17 à 20 (translation="MNPLLIL...)** : la séquence protéique issue de la traduction de la séquence codante.

* **Ligne 22 (sig_peptide 7..51)** : la délimitation du peptide signal.

## A.2.3 La séquence

[...]

ORIGIN

<table><tr><td>1</td><td>accaccatga</td><td>atccactcct</td><td>gatccttacc</td><td>tttgtggcag</td><td>ctgctcttgc</td><td>tgcccccttt</td></tr><tr><td>61</td><td>gatgatgatg</td><td>acaagatcgt</td><td>tgggggctac</td><td>aactgtgagg</td><td>agaattctgt</td><td>cccctaccag</td></tr><tr><td>121</td><td>gtgtccctga</td><td>attctggcta</td><td>ccacttctgt</td><td>ggtggctccc</td><td>tcatcaacga</td><td>acagtgggtg</td></tr><tr><td>181</td><td>gtatcagcag</td><td>gccactgcta</td><td>caagtcccgc</td><td>atccaggtga</td><td>gactgggaga</td><td>gcacaacatc</td></tr><tr><td>241</td><td>gaagtcctgg</td><td>aggggaatga</td><td>gcagttcatc</td><td>aatgcagcca</td><td>agatcatccg</td><td>ccacccccaa</td></tr><tr><td>301</td><td>tacgacagga</td><td>agactctgaa</td><td>caatgacatc</td><td>atgttaatca</td><td>agctctcctc</td><td>acgtgcagta</td></tr><tr><td>361</td><td>atcaacgccc</td><td>gcgtgtccac</td><td>catctctctg</td><td>cccacggcc</td><td>ctccagccac</td><td>tggcacgaag</td></tr><tr><td>421</td><td>tgccctcatct</td><td>ctggctggg</td><td>caacactgcg</td><td>agctctggcg</td><td>ccgactacc</td><td>agacgagctg</td></tr><tr><td>481</td><td>cagtgccctgg</td><td>atgctcctgt</td><td>gctgagccag</td><td>gctaagtgtg</td><td>aagcctccta</td><td>ccctggaaag</td></tr><tr><td>541</td><td>attaccagca</td><td>acatgttctg</td><td>tgtgggcttc</td><td>cttgaggag</td><td>gcaaggattc</td><td>atgtcaggtg</td></tr><tr><td>601</td><td>gattctggtg</td><td>gccctgtggt</td><td>ctgcaatgga</td><td>cagctccaag</td><td>gagttgtctc</td><td>ctggggtgat</td></tr><tr><td>661</td><td>ggctgtgccc</td><td>agaagaacaa</td><td>gcctggagtc</td><td>tacaccaagg</td><td>tctacaacta</td><td>cgtgaaatgg</td></tr><tr><td>721</td><td>attaagaaca</td><td>ccatagctgc</td><td>caatagctaa</td><td>agccccccag</td><td>atctcttcag</td><td>tctctatacc</td></tr><tr><td>781</td><td>aataaagtga</td><td>ccctgttctc</td><td></td><td></td><td></td><td></td></tr></table>

//

La séquence est contenue entre les balises `ORIGIN` (ligne 2) et `//` (ligne 17).

Chaque ligne est composée d'une série d'espaces, puis du numéro du premier nucléotide de la ligne, puis d'au plus 6 blocs de 10 nucléotides. Chaque bloc est précédé d'un espace. Par exemple, ligne 10, le premier nucléotide de la ligne (t) est le numéro 421 dans la séquence.
### A.2.4 Manipulation avec Python

À partir de l'exemple précédent, voici comment lire un fichier GenBank avec Python et le module `Biopython` :

1. from Bio import SeqIO

2. with open("M22612.gbk", "r") as gbk_file:

3. record =SeqIO.read(gbk_file,"genbank")

4. print(record.id)

5. print(record description)

6. print(record.seq[:60])

Pour la séquence lue dans le fichier GenBank, on affiche son identifiant, sa description et les 60 premiers résidus :

M22612.1

Human pancreatic trypsin 1 (TRY1) mRNA, complete cds.

ACCACCATGAATCCACTCCTGATCCTTACCTTTGTGGCAGCTGCTCTTGCTGCCCCCTTT

Il est également possible de lire un fichier GenBank sans le module `Biopython`. Une activité dédiée est proposée dans le chapitre 27 *Mini-projets* (en ligne).

## A.3 PDB

La Protein Data Bank<fnref n="8" /> (PDB) est une banque de données qui contient les structures de biomacromolécules (protéines, ADN, ARN, virus...). Historiquement, le format de fichier qui y est associé est le PDB, dont une documentation détaillée est disponible sur le site éponyme<fnref n="9" />. Les extensions de fichier pour ce format de données sont `.ent` et surtout `.pdb`.

Un fichier PDB est constitué de deux parties principales : l'en-tête et les coordonnées.

* L'en-tête est lisible et utilisable par un être humain (comme par une machine).

* À l'inverse, les **coordonnées** sont surtout utilisables par un programme pour calculer certaines propriétés de la structure ou simplement la représenter sur l'écran d'un ordinateur. Bien sûr, un utilisateur expérimenté peut parfaitement jeter un œil à cette seconde partie.

Examinons ces deux parties avec la trypsine bovine<fnref n="10" />.

### A.3.1 En-tête

Pour la trypsine bovine, l'en-tête compte 510 lignes. En voici quelques unes :

<footnote n="8">8. https://www.rcsb.org/</footnote>

<footnote n="9">9. http://www.wwpdb.org/documentation/file-format-content/format33/v3.3.html</footnote>

<footnote n="10">10. https://www.rcsb.org/structure/2PTN</footnote>