@@ -6,6 +6,8 @@ Ce dépôt contient: | |||||
* Un répertoire par saison, contenant à chaque fois les sources des présentations (dans un sous-répertoire 'sessions/'), et le code écrit pendant les ateliers (dans un sous-répertoire 'sources/') | * Un répertoire par saison, contenant à chaque fois les sources des présentations (dans un sous-répertoire 'sessions/'), et le code écrit pendant les ateliers (dans un sous-répertoire 'sources/') | ||||
* Enfin, un répertoire cours/ contenant les sources du cours en ligne. | |||||
# License | # License | ||||
Le contenu de ce dépôt (cours et code source) est mis à disposition gratuitement selon les termes de la [Licence Creative Commons Attribution 3.0 France](https://creativecommons.org/licenses/by/3.0/fr/). | Le contenu de ce dépôt (cours et code source) est mis à disposition gratuitement selon les termes de la [Licence Creative Commons Attribution 3.0 France](https://creativecommons.org/licenses/by/3.0/fr/). |
@@ -0,0 +1,2 @@ | |||||
explication des exercices | |||||
insérer exercices pour chaque épisode de la saison 2 |
@@ -0,0 +1,6 @@ | |||||
--- | |||||
title: "{{ replace .Name "-" " " | title }}" | |||||
date: {{ .Date }} | |||||
draft: true | |||||
--- | |||||
@@ -0,0 +1,49 @@ | |||||
baseURL: https://dmerej.info/books/python | |||||
title: Programmation en Python | |||||
theme: book | |||||
disableKinds: ['taxonomy', 'taxonomyTerm'] | |||||
# Book configuration | |||||
disablePathToLower: true | |||||
enableGitInfo: true | |||||
# Code highlight | |||||
pygmentsStyle: native | |||||
pygmentsCodeFences: true | |||||
params: | |||||
# (Optional, default 6) Set how many table of contents levels to be showed on page. | |||||
# Use false to hide ToC, note that 0 will default to 6 (https://gohugo.io/functions/default/) | |||||
# You can also specify this parameter per page in front matter | |||||
BookToC: 3 | |||||
# (Optional, default none) Set the path to a logo for the book. If the logo is | |||||
# /static/logo.png then the path would be logo.png | |||||
# BookLogo: /logo.png | |||||
# (Optional, default none) Set leaf bundle to render as side menu | |||||
# When not specified file structure and weights will be used | |||||
# BookMenuBundle: /menu | |||||
# (Optional, default docs) Specify section of content to render as menu | |||||
# You can also set value to '*' to render all sections to menu | |||||
BookSection: docs | |||||
# Set source repository location. | |||||
# Used for 'Last Modified' and 'Edit this page' links. | |||||
BookRepo: ~ | |||||
# Enable "Edit this page" links for 'doc' page type. | |||||
# Disabled by default. Uncomment to enable. Requires 'BookRepo' param. | |||||
# Path must point to 'content' directory of repo. | |||||
BookEditPath: ~ | |||||
# Configure the date format used on the pages | |||||
# - In git information | |||||
# - In blog posts | |||||
BookDateFormat: '2 Jan 2006' | |||||
# (Optional, default true) Enables search function with flexsearch, | |||||
# Index is built on fly, therefore it might slowdown your website. | |||||
BookSearch: false |
@@ -0,0 +1,18 @@ | |||||
--- | |||||
title: Introduction | |||||
type: docs | |||||
--- | |||||
# Présentation de ce livre | |||||
Bienvenue! | |||||
Ce livre contient les supports de cours destinés aux élève de l'École du Logiciel Libre, | |||||
où je donne des cours sur Python depuis 2018. | |||||
Il est destiné à toute personne qui voudrait découvrir la programmation. | |||||
J'espère qu'il vous plaira! Sinon, vous pouvez vous rendre sur [ma page de contact](https://dmerej.info/blog/fr/pages/about/) et | |||||
me faire part de vos remarques. | |||||
@@ -0,0 +1,20 @@ | |||||
+++ | |||||
title = "Objet de ce livre" | |||||
weight = 1 | |||||
+++ | |||||
# Objet de ce livre | |||||
Apprendre la programmation en partant de rien, en utilisant Python et la ligne de commande | |||||
# Pourquoi Python? | |||||
* Excellent langage pour débuter | |||||
* Mon langage préféré | |||||
* Vraiment cross-platform (sauf pour le mobile) | |||||
# Pourquoi la ligne de commande? | |||||
Interface universelle | |||||
La plus simple à utiliser (en Python en tout cas) |
@@ -0,0 +1,54 @@ | |||||
+++ | |||||
title = "Le langage Python" | |||||
weight = 3 | |||||
+++ | |||||
# Présentation du langage Python | |||||
## Utilisation de Python | |||||
* Aussi appelé "langage de script", `glue language` | |||||
* Bon partout, excellent nulle part | |||||
* Exemples d'utilisation: | |||||
* Sciences (physique, chimie, linguistique ...) | |||||
* Animation (Pixar, Disney ...) | |||||
* Sites web (journaux, youtube, ...) | |||||
* Ligne de commande | |||||
* ... | |||||
## Petit détour: version d'un programme | |||||
* Comme les versions d'un document | |||||
* Si le nombre est plus grand, c'est plus récent | |||||
* Souvent en plusieurs morceaux: `1.3, 1.4, 3.2.5`. etc | |||||
* Plus l'écart est grand, plus le programme a changé. | |||||
* `3.2.5 -> 3.2.6`: pas grand-chose | |||||
* `1.5.1 -> 4.3`: beaucoup de changements | |||||
* On omet souvent le reste des numéros quand c'est pas nécessaire | |||||
## Historique | |||||
* Créé par Guido van Rossum. Conçu à la base pour l'enseignement. | |||||
* Le nom vient des Monty Python (si, si) | |||||
* Python 1: Sortie en 1991 | |||||
* Python 2: en 2000 | |||||
* Python 3: en 2008 | |||||
## Le grand schisme | |||||
La plupart des langages continuent à être compatibles d'une version à l'autre. | |||||
*Ce n'est pas le cas pour Python3*, et ça a causé beaucoup de confusion et de débats. | |||||
Heureusement, 10 ans plus tard, la situation s'est arrangée, et Python2 cessera d'être maintenu le premier janvier 2020. | |||||
## Python3 | |||||
Ce cours fonctionne donc uniquement avec Python3. | |||||
N'utilisez *pas* Python2, sinon certaines choses expliquées ici ne marcheront pas :/ | |||||
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 1 - Introduction" | |||||
weight = 1 | |||||
+++ |
@@ -0,0 +1,25 @@ | |||||
+++ | |||||
title = "Ligne de commande" | |||||
weight = 2 | |||||
+++ | |||||
# La ligne de commande | |||||
## Pourquoi la ligne de commande? | |||||
* Très puissant | |||||
* Ancien, mais toujours d'actualité | |||||
* Indispensable dans de nombreux cas | |||||
* Écrire des programmes qui marche dans la ligne de commande est (relativement) simple | |||||
* Possibilités infines, même si on ne fait que manipuler du texte | |||||
## Les bases | |||||
On tape un commande, on appuie sur entrée, l'ordinateur interprète ce qui a été tapé et affiche un message: | |||||
* `cd chemin/vers/fichier` | |||||
* `ls` (ou `dir` sous Windows) | |||||
* `pwd` | |||||
* Le premier mot est une *commande*, les autres mots sont des *arguments* | |||||
@@ -0,0 +1,35 @@ | |||||
+++ | |||||
title = "L'interpréteur interactif" | |||||
weight = 4 | |||||
+++ | |||||
# L'interpréteur interactif | |||||
## Installation | |||||
Il se lance depuis l'invite de commande du système d'exploitation: | |||||
``` | |||||
$ python3 | |||||
Python 3.7.1 (default, Oct 22 2018, 10:41:28) | |||||
[GCC 8.2.1 20180831] on linux | |||||
Type "help", "credits" or "license" for more information. | |||||
>>> | |||||
``` | |||||
## Deux invites de commandes | |||||
Notez les trois chevrons: `>>>`. Cela vous permet de différencier l'invite | |||||
de commandes du système d'exploitation de celle de Python. | |||||
* Système d'exploitation -> Python: taper `python3` (sans arguments) | |||||
* Python -> Système d'exploitation: taper `quit()` | |||||
## Note | |||||
À partir de maintenant, recopiez les entrées sur les slides dans votre propre | |||||
interpréteur. | |||||
Vous devez taper la même chose après l'invite de commande ('>>>') et vous devez voir les mêmes réponses. | |||||
Si ce n'est pas le cas, relisez attentitivement ce que vous avez tapé, sinon [contactez-moi](https://dmerej.info/blog/fr/pages/about/). |
@@ -0,0 +1,62 @@ | |||||
+++ | |||||
title = "Maths simples" | |||||
weight = 5 | |||||
+++ | |||||
# Maths simples | |||||
## Opérations avec des entiers | |||||
``` | |||||
>>> 1 | |||||
1 | |||||
>>> 2 | |||||
2 | |||||
>>> 1 + 2 | |||||
3 | |||||
>>> 2 * 3 | |||||
6 | |||||
``` | |||||
## Opérantions avec des flottants | |||||
C'est le `.` qui fait le flottant | |||||
``` | |||||
>>> 0.5 | |||||
0.5 | |||||
>>> 0.5 + 0.2 | |||||
0.7 | |||||
>>> 10 / 3 | |||||
3.3333333333333335 | |||||
``` | |||||
*Note: les flottants sont imprécis* | |||||
## Division entières et modulo | |||||
``` | |||||
>>> 14 // 3 | |||||
4 | |||||
>>> 14 % 3 | |||||
2 | |||||
``` | |||||
*Le `%` n'a rien à voir avec un pourcentage!* | |||||
## Priorité des opérations | |||||
``` | |||||
>>> 1 + 2 * 3 | |||||
7 | |||||
>>> (1 + 2) * 3 | |||||
9 | |||||
``` | |||||
*Les parenthèses permettent de grouper les expressions* | |||||
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 2 - Premiers pas" | |||||
weight = 2 | |||||
+++ |
@@ -0,0 +1,49 @@ | |||||
+++ | |||||
title = "Variables" | |||||
weight = 6 | |||||
+++ | |||||
# Variables | |||||
```python | |||||
>>> a = 2 | |||||
>>> a | |||||
2 | |||||
>>> b = 3 | |||||
>>> a + b | |||||
5 | |||||
``` | |||||
* On peut nommer des valeurs | |||||
* On peut afficher la valeur d'une variable entrant son nom dans le REPL | |||||
```python | |||||
>>> a = 2 | |||||
>>> a | |||||
2 | |||||
>>> a = 3 | |||||
>>> a | |||||
3 | |||||
``` | |||||
* On peut changer la valeur d'une variable (d'où son nom) | |||||
* Quand Python essaie d'exécuter du code, il commence par remplacer les | |||||
variables par leurs valeurs | |||||
## Nom des variables | |||||
Préférez des noms longs et descriptifs | |||||
Toujours en minuscules | |||||
Séparez les "mots" par des tirets bas (underscore) | |||||
```python | |||||
>>> score = 42 | |||||
>>> age_moyen = 22 | |||||
``` | |||||
@@ -0,0 +1,60 @@ | |||||
+++ | |||||
title = "Chaînes de caractères" | |||||
weight = 7 | |||||
+++ | |||||
# Chaînes de caractères | |||||
Aussi appelées "string". | |||||
Avec des simple quotes (`'`) | |||||
```python | |||||
>>> 'Bonjour monde!' | |||||
'Bonjour monde!' | |||||
``` | |||||
Marche aussi avec des double quotes (`"`) | |||||
```python | |||||
>>> "Bonjour, monde!" | |||||
'Bonjour monde' | |||||
``` | |||||
## Double et simple quotes | |||||
On peut mettre des simples quotes dans des double quotes et vice-versa. | |||||
```python | |||||
>>> "Il a dit: 'bonjour' ce matin." | |||||
"Il a dit: 'bonjour' ce matin." | |||||
>>> 'Il a dit: "bonjour" ce matin' | |||||
'Il a dit: "bonjour" ce matin!' | |||||
``` | |||||
## Échappement | |||||
Avec la barre oblique inversée "backslash" | |||||
```python | |||||
>>> 'Il a dit: "bonjour". C\'est sympa!' | |||||
'Il a dit: "bonjour". C\'est sympa!' | |||||
``` | |||||
## Concaténation | |||||
```python | |||||
>>> name = "John" | |||||
>>> message = "Bonjour " + name + " !" | |||||
>>> message | |||||
"Bonjour John !" | |||||
``` | |||||
@@ -0,0 +1,39 @@ | |||||
+++ | |||||
title = "Types" | |||||
weight = 8 | |||||
+++ | |||||
# Types | |||||
```python | |||||
>>> a = 42 | |||||
>>> message = "La réponse est: " + a | |||||
TypeError: can only concatenate str (not "int") to str | |||||
``` | |||||
*Notre premier message d'erreur !* | |||||
On ne mélange pas les torchons et les serviettes! | |||||
## Conversions | |||||
### Entier vers string | |||||
```python | |||||
>>> a = 42 | |||||
>>> message = "La réponse est: " + str(a) | |||||
>>> message | |||||
'La réponse est 42' | |||||
``` | |||||
### String vers entier | |||||
```python | |||||
>>> answer = int("42") | |||||
>>> answer | |||||
42 | |||||
``` | |||||
Notez les parenthèses autour des valeurs. | |||||
@@ -0,0 +1,60 @@ | |||||
+++ | |||||
title = "Booléens et conditions" | |||||
weight = 9 | |||||
+++ | |||||
# Booléens et conditions | |||||
## True et False | |||||
En Python ce sont des mots-clés et les valeurs sont en majuscules! | |||||
## Assignation | |||||
On peut assigner des variables aux valeurs True et False | |||||
``` | |||||
>>> la_terre_est_plate = False | |||||
... | |||||
>>> python_c_est_genial = True | |||||
``` | |||||
## Comparaisons | |||||
``` | |||||
>>> a = 2 | |||||
>>> b = 3 | |||||
>>> a > b | |||||
False | |||||
``` | |||||
``` | |||||
>>> 2 + 2 == 4 | |||||
True | |||||
``` | |||||
Note: `==` pour la comparaison, `=` pour l'assignation | |||||
``` | |||||
>>> a = 2 | |||||
>>> b = 3 | |||||
>>> a != b | |||||
True | |||||
>>> 2 + 2 >= 4 | |||||
True | |||||
``` | |||||
``` | |||||
>>> a = 2 | |||||
>>> a < 2 | |||||
False | |||||
>>> 1 < a < 3 | |||||
True | |||||
``` | |||||
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 3 - Variables et types" | |||||
weight = 3 | |||||
+++ |
@@ -0,0 +1,107 @@ | |||||
+++ | |||||
title = "Code source" | |||||
weight = 10 | |||||
+++ | |||||
# Code source | |||||
## Non persistance des variables | |||||
```python | |||||
$ python3 | |||||
>>> a = 2 | |||||
>>> quit() | |||||
``` | |||||
```python | |||||
$ python3 | |||||
>>> a | |||||
Traceback (most recent call last): | |||||
File "<stdin>", line 1, in <module> | |||||
NameError: name 'a' is not defined | |||||
``` | |||||
## Du code dans un fichier | |||||
Aussi appelé: "code source", ou "source". | |||||
L'essence du logiciel libre :) | |||||
## Installation d'un éditeur de texte simple | |||||
* Linux; `gedit`, `kate`, ... | |||||
* macOS: `CotEditor` | |||||
* Windows: `Notepad++` | |||||
J'insiste sur **simple**. Inutile d'installer un IDE pour le moment. | |||||
## Configuration | |||||
* Police de caractères à chasse fixe | |||||
* Indentation de *4 espaces* | |||||
* Remplacer tabulation par des espaces | |||||
* Activer la coloration syntaxique | |||||
## Notre premier fichier source | |||||
Insérez le code suivant dans votre éditeur de texte | |||||
```python | |||||
# Affiche un message | |||||
print("Bonjour, monde") | |||||
``` | |||||
Sauvegardez dans un fichier `bonjour.py` dans `Documents/e2l/python` par exemple | |||||
# Démonstration avec `kate` | |||||
// TODO: conseiller un éditeur par plateforme | |||||
C'est l'éditeur que nous utiliserons pour nos ateliers. | |||||
* Pour l'environement KDE, mais ça marche bien sous Gnome aussi | |||||
* Coloration syntaxique | |||||
* Auto-complétion | |||||
## Lancer du code en ligne de commande | |||||
```text | |||||
cd Documents/e2l/python/ | |||||
python3 bonjour.py | |||||
Bonjour, monde | |||||
``` | |||||
* Les lignes commençant par dièse (`#`) ont été ignorées - ce sont des *commentaires*. | |||||
* `print()` affiche la valeur, comme dans le REPL. | |||||
## Note importante | |||||
Vous avez juste besoin: | |||||
* d'un éditeur de texte | |||||
* de Python3 installé | |||||
* d'une ligne de commande | |||||
Pas la peine d'installer quoique ce soit de plus pour le moment | |||||
// TODO: dupliqué? | |||||
1. *Recopiez* le code affiché dans votre éditeur, à la suite du code | |||||
déjà écrit | |||||
2. Lancez le code depuis la ligne de commande | |||||
3. Réparez les erreurs éventuelles | |||||
4. Recommencez |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 4 - code source" | |||||
weight = 4 | |||||
+++ |
@@ -0,0 +1,115 @@ | |||||
+++ | |||||
title = "Flôt de contrôle" | |||||
weight = 11 | |||||
+++ | |||||
# Flot de contrôle | |||||
L'essence de la programmation! | |||||
## if | |||||
```python | |||||
a = 3 | |||||
b = 4 | |||||
if a == b: | |||||
print("a et b sont égaux") | |||||
print("on continue") | |||||
``` | |||||
Notes: | |||||
* deux points à la fin de la ligne | |||||
* indentation après les deux points | |||||
* si la condition n'est pas vraie, rien ne se passe | |||||
Notez qu'on peut mettre uniquement une variable ou une valeur | |||||
après le if. Ceci ne fonctionne pas: | |||||
```python | |||||
if a = 3: | |||||
print("a égale 3") | |||||
``` | |||||
et fait une erreur de syntaxe | |||||
## if / else | |||||
```python | |||||
a = 3 | |||||
b = 4 | |||||
if a == b: | |||||
print("a et b sont égaux") | |||||
else: | |||||
print("a et b sont différent") | |||||
``` | |||||
## if / elif | |||||
```python | |||||
if age < 10: | |||||
print("inférieur à dix") | |||||
elif 10 <= age < 20: | |||||
print("âge entre 10 et 20") | |||||
elif 20 <= age < 40: | |||||
print("âge entre 20 et 40") | |||||
else: | |||||
print("âge supérieur à 40") | |||||
``` | |||||
On peut mettre autont de `elif` qu'on veut! | |||||
Le derier `else` s'éxécute en dernier | |||||
## while | |||||
Répéter tant qu'une condition est vraie | |||||
```python | |||||
i = 0 | |||||
while i < 3: | |||||
print(i) | |||||
i = i + 1 | |||||
``` | |||||
``` | |||||
0 | |||||
1 | |||||
2 | |||||
``` | |||||
## Notre première boucle infinie | |||||
```python | |||||
while True: | |||||
print("spam!") | |||||
``` | |||||
CTRL-C pour interrompre | |||||
## Combiner while et if | |||||
On peut "sortir" de la boucle `while` avec `break` | |||||
```python | |||||
i = 0 | |||||
while True: | |||||
i = i + 1 | |||||
print(i) | |||||
if i > 3: | |||||
break | |||||
``` | |||||
``` | |||||
1 | |||||
2 | |||||
3 | |||||
4 | |||||
``` |
@@ -0,0 +1,38 @@ | |||||
+++ | |||||
title = "Exercice" | |||||
weight = 12 | |||||
+++ | |||||
# Exercice | |||||
// TODO: explication des exercises | |||||
## Lire une entrée utilisateur | |||||
* `input()` (encore des parenthèses ...) | |||||
* interrompt le script | |||||
* lit ce que l'utilisateur tape jusqu'à ce qu'il tape "entrée". | |||||
* renvoie une string | |||||
## Le jeu | |||||
On fait deviner un nombre à l'utilisateur, en affichant 'trop grand', 'trop petit' | |||||
jusqu'à ce qu'il trouve la valeur exacte. | |||||
## Squelette | |||||
// TODO: | |||||
* explication du Squelette | |||||
* pas de solution! | |||||
```python | |||||
# faites moi confiance, les deux lignes ci-dessous | |||||
# permettent de tirer un nombre au hasard entre 0 et 100 | |||||
import random | |||||
nombre = random.randint(0, 100) | |||||
print("devine le nombre auquel je pense") | |||||
# votre code ici | |||||
``` |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 5 - Flot de contrôle" | |||||
weight = 5 | |||||
+++ |
@@ -0,0 +1,75 @@ | |||||
+++ | |||||
title = "Fonctions" | |||||
weight = 1 | |||||
+++ | |||||
# Fonctions | |||||
## Fonction sans argument | |||||
Définition: | |||||
```python | |||||
def dire_bonjour(): | |||||
print("Bonjour") | |||||
``` | |||||
* avec `def` | |||||
* avec un `:` à la fin et un _bloc indenté_ (appelé le "corps") | |||||
Appel: | |||||
``` | |||||
>>> dire_bonjour() | |||||
Bonjour | |||||
``` | |||||
* avec le nom de la fonction et des parenthèses | |||||
## Le pouvoir des fonctions | |||||
Ici on vient de créer une nouvelle fonctionnalité | |||||
à Python. Avant qu'on définisse la fonction | |||||
`dire_bonjour()`, il ne savait pas dire bonjour, | |||||
il savait uniquement afficher des messages à | |||||
l'écran. | |||||
On dit qu'on a _créé une abstraction_. Et | |||||
c'est une technique extrêmement utile en | |||||
programmation. | |||||
## Fonction avec un argument | |||||
Définition: avec l'argument à l'intérieur des parenthèses | |||||
```python | |||||
def dire_bonjour(prénom): | |||||
print("Bonjour " + prénom) | |||||
``` | |||||
Appel: en passant une variable ou une valeur dans les parenthèses | |||||
```python | |||||
>>> dire_bonjour("Germaine") | |||||
Bonjour Germaine | |||||
>>> prénom_de_charlotte = "Charlotte" | |||||
>>> dire_bonjour(prénom_de_charlotte) | |||||
Bonjour Charlotte | |||||
``` | |||||
## Exécution d'une fonction | |||||
C'est exatement comme si on assignait les arguments de la fonction avant d'éxécuter le code | |||||
dans le corps | |||||
```python | |||||
# Ceci: | |||||
dire_bonjour("Dimitri") | |||||
# Est équivalent à cela: | |||||
prénom_de_dimitri = "Dimitri" | |||||
print("Bonjour " + prénom_de_dimitri) | |||||
# Lui-même équivalent à: | |||||
print("Bonjour " + "Dimitri") | |||||
``` |
@@ -0,0 +1,39 @@ | |||||
+++ | |||||
title = "Portée des variables" | |||||
weight = 2 | |||||
+++ | |||||
# Portée des variables | |||||
Les arguments d'une fonction n'existent que dans le corps de celle-ci | |||||
```python | |||||
def dire_bonjour(prénom): | |||||
print("Bonjour " + prénom) | |||||
dire_bonjour("Dimitri") # Ok | |||||
print(prénom) # Erreur | |||||
``` | |||||
Les variables en dehors des fonctions sont disponibles partout: | |||||
```python | |||||
salutation = "Bonjour " | |||||
def dire_bonjour(prénom): | |||||
print(salutation + prénom) | |||||
dire_bonjour("Dimitri") | |||||
``` | |||||
Une variable peut avoir en "cacher" une autre si elle a une portée différente | |||||
```python | |||||
def dire_bonjour(prénom): | |||||
print("Bonjour " + prénom) # portée: uniquement dans | |||||
# le corps dire_bonjour | |||||
prénom = "Dimitri" # portée: dans tout le programme | |||||
dire_bonjour(prénom) # Ok | |||||
``` |
@@ -0,0 +1,43 @@ | |||||
+++ | |||||
title = "Fonctions à plusieurs arguments" | |||||
weight = 2 | |||||
+++ | |||||
# Fonctions à plusieurs arguments | |||||
On peut mettre autant d'arguments qu'on veut, séparés | |||||
par des virgules: | |||||
```python | |||||
def afficher_addition(x, y): | |||||
résultat = x + y | |||||
print(résultat) | |||||
``` | |||||
```python | |||||
>>> a = 4 | |||||
>>> b = 5 | |||||
>>> afficher_addition(a, b) | |||||
9 | |||||
``` | |||||
## Arguments nommés | |||||
En Python, on peut aussi utiliser le *nom* des arguments au lieu de | |||||
leur position: | |||||
```python | |||||
def dire_bonjour(prénom): | |||||
print("Bonjour " + prénom) | |||||
``` | |||||
```python | |||||
>>> dire_bonjour(prénom="Gertrude") | |||||
Bonjour Gertrude | |||||
>>> afficher_addition(y=3, x=4) | |||||
7 | |||||
``` | |||||
// TODO: soustraction | |||||
@@ -0,0 +1 @@ | |||||
@@ -0,0 +1,28 @@ | |||||
+++ | |||||
title = "Arguments par défaut" | |||||
weight = 4 | |||||
+++ | |||||
# Arguments par défaut | |||||
On peut aussi mettre des valeurs par défaut: | |||||
Définition: | |||||
```python | |||||
def dire_bonjour(prénom, enthousiaste=False): | |||||
message = "Bonjour " + prénom | |||||
if enthousiaste: | |||||
message += "!" | |||||
print(message) | |||||
``` | |||||
Appel: | |||||
```python | |||||
>>> dire_bonjour("Thomas", enthousiaste=True) | |||||
Bonjour Thomas! | |||||
>>> dire_bonjour("Thomas", enthousiaste=False) | |||||
Bonjour Thomas | |||||
>>> dire_bonjour("Thomas") | |||||
Bonjour Thomas | |||||
``` | |||||
@@ -0,0 +1,46 @@ | |||||
+++ | |||||
title = "Fonctions natives" | |||||
weight = 5 | |||||
+++ | |||||
# Fonctions natives | |||||
Fonctions qui sont toujours présentes dans l'interpréteur. On en a déjà vu quelques unes: | |||||
* `print`, `input`: écrire et lire sur la ligne de commande | |||||
* `str`, `int`: convertir des entiers en strings et vice-versa | |||||
Il y en a tout un tas! | |||||
La liste ici: https://docs.python.org/fr/3/library/functions.html#built-in-funcs | |||||
## Retour sur print | |||||
On peut passer autant d'arguments qu'on veut à `print` et: | |||||
* Il les sépare par des espaces | |||||
* Ajoute un retour à la ligne à la fin: | |||||
```python | |||||
>>> prénom = "Charlotte" | |||||
print("Bonjour", pŕenom) | |||||
Bonjour Charlotte | |||||
``` | |||||
On peut demander à `print` de changer son séparateur: | |||||
```python | |||||
>>> a = "chauve" | |||||
>>> b = "souris" | |||||
>>> print(a, b, sep="-") | |||||
chauve-souris | |||||
``` | |||||
Ou de changer le caractère de fin: | |||||
```python | |||||
>>> print("Ceci tient", end="") | |||||
>>> print("sur une seule ligne") | |||||
Ceci tient sur une seule ligne | |||||
``` | |||||
@@ -0,0 +1,43 @@ | |||||
+++ | |||||
title = "return" | |||||
weight = 6 | |||||
+++ | |||||
# Valeur de retour d'une fonction | |||||
Définition avec le mot `return` | |||||
```python | |||||
def additionner(x, y): | |||||
return x + y | |||||
``` | |||||
Récupérer la valeur de retour | |||||
```python | |||||
>>> a = 3 | |||||
>>> b = 4 | |||||
>>> c = additionner(a, b) # encore une assignation | |||||
>>> c | |||||
7 | |||||
``` | |||||
# Sortir d'une fonction avec return | |||||
`return` interrompt également l'éxécution du | |||||
corps de la fonction: | |||||
```python | |||||
def dire_bonjour(prénom, première_fois=False): | |||||
print("Bonjour", prénom) | |||||
if not première_fois: | |||||
return | |||||
print("Heureux de faire votre connaissance") | |||||
``` | |||||
```python | |||||
>>> dire_bonjour("Dimitri", première_fois=True) | |||||
Bonjour Dimitri | |||||
Heureux de faire votre connaissance | |||||
>>> dire_bonjour("Dimitri", première_fois=False) | |||||
Bonjour Dimitri | |||||
``` |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 6 - Fonctions" | |||||
weight = 6 | |||||
+++ |
@@ -0,0 +1,164 @@ | |||||
+++ | |||||
title = "Listes" | |||||
weight = 1 | |||||
+++ | |||||
# Listes | |||||
// TODO: split in pages | |||||
## Définition | |||||
Une liste est une _suite ordonée_ d'éléments. | |||||
## Créer une liste | |||||
Avec `[]`, et les élements séparés par des virgules: | |||||
```python | |||||
liste_vide = [] | |||||
trois_entiers = [1, 2, 3] | |||||
``` | |||||
## Listes hétérogènes | |||||
On peut mettre des types différents dans la même liste | |||||
```python | |||||
ma_liste = [True, 2, "trois"] | |||||
``` | |||||
On peut aussi mettre des listes dans des listes: | |||||
```python | |||||
liste_de_listes = [[1, 2], ["Germaine", "Gertrude"]] | |||||
``` | |||||
## Connaître la taille d'une liste | |||||
Avec `len()` - encore une fonction native | |||||
```python | |||||
>>> liste_vide = [] | |||||
>>> len(liste_vide) | |||||
0 | |||||
>>> trois_entiers = [1, 2, 3] | |||||
>>> len(trois_entiers) | |||||
3 | |||||
``` | |||||
## Concaténation de listes | |||||
Avec `+` | |||||
```python | |||||
>>> prénoms = ["Alice", "Bob"] | |||||
>>> prénoms += ["Charlie", "Eve"] | |||||
>>> prénoms | |||||
['Alice', 'Bob', "Charlie", 'Eve'] | |||||
``` | |||||
On ne peut concaténer des listes que avec d'autres listes: | |||||
```python | |||||
>>> scores = [1, 2, 3] | |||||
>>> scores += 4 # TypeError | |||||
>>> scores += [4] # OK | |||||
``` | |||||
## Test d'appartenance | |||||
Avec `in`: | |||||
```python | |||||
>>> prénoms = ["Alice", "Bob"] | |||||
>>> "Alice" in prénoms | |||||
True | |||||
``` | |||||
```python | |||||
>>> prénoms = ["Alice", "Bob"] | |||||
>>> "Charlie" in prénoms | |||||
False | |||||
``` | |||||
## Itérer sur les élements d'une liste | |||||
Avec `for ... in` | |||||
```python | |||||
prénoms = ["Alice", "Bob", "Charlie"] | |||||
for prénom in prénoms: | |||||
# La variable 'prénom" est assignée à chaque | |||||
# élément de la liste | |||||
print("Bonjour", prénom) | |||||
Bonjour Alice | |||||
Bonjour Bob | |||||
Bonjour Charlie | |||||
``` | |||||
## Indéxer une liste | |||||
* Avec `[]` et un entier | |||||
* Les index valides vont de 0 à `n-1` où `n` est la | |||||
taille de la liste. | |||||
```python | |||||
>>> fruits = ["pomme", "orange", "poire"] | |||||
>>> fruits[0] | |||||
"pomme" | |||||
>>> fruits[1] | |||||
"orange" | |||||
>>> list[2] | |||||
"poire" | |||||
>>> fruits[3] # IndexError | |||||
``` | |||||
## Modifier une liste | |||||
Encore une assignation: | |||||
```python | |||||
>>> fruits = ["pomme", "orange", "poire"] | |||||
>>> fruits[0] = "abricot" | |||||
>>> fruits | |||||
["abricot", "orange", "poire"] | |||||
``` | |||||
## Les strings sont aussi des listes (presque) | |||||
On peut itérer sur les caractères d'une string: | |||||
```python | |||||
for c in "vache": | |||||
print(c) | |||||
v | |||||
a | |||||
c | |||||
h | |||||
e | |||||
``` | |||||
On peut tester si un caractère est présent: | |||||
```python | |||||
>>> "e" in "vache" | |||||
True | |||||
>>> "x" in "vache" | |||||
False | |||||
``` | |||||
Mais on neut peut pas modifier une string | |||||
```python | |||||
>>> prénom = "Charlotte" | |||||
>>> prénom[0] | |||||
"C" | |||||
>>> prénom[3] | |||||
"r" | |||||
>>> prénom[0] = "X" # TypeError | |||||
``` |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 7 - Listes" | |||||
weight = 7 | |||||
+++ |
@@ -0,0 +1,98 @@ | |||||
+++ | |||||
title = "None" | |||||
weight = 1 | |||||
+++ | |||||
# None | |||||
## Définition | |||||
`None` est une "valeur magique" natif en Python. Il est toujours présent, et il est unique. | |||||
Un peu comme `True` et `False` qui sont deux valeurs qui servent à représenter tous les booléens. | |||||
## Représenter l'absence | |||||
L'interpréteur intéractif n'affiche rien quand la valeur est None | |||||
```python | |||||
>>> a = 42 | |||||
>>> a | |||||
42 | |||||
>>> b = None | |||||
>>> b | |||||
``` | |||||
## Retourner None | |||||
En réalité, *toutes* les fonctions pythons retournent *quelque chose*, même quand | |||||
elle ne contiennent pas le mot-clé `return`. | |||||
```python | |||||
def ne_renvoie_rien(): | |||||
print("je ne fais qu'afficher quelque chose") | |||||
``` | |||||
```python | |||||
>>> resultat = ne_renvoie_rien() | |||||
"je ne fais qu'afficher quelque chose" | |||||
>>> resultat | |||||
``` | |||||
## Opérations avec None | |||||
La plupart des fonctions que nous avons vues échouent si on leur passe None | |||||
en argument: | |||||
```python | |||||
>>> len(None) | |||||
TypeError: object of type 'NoneType' has no len() | |||||
>>> None < 3 | |||||
TypeError: '<' not supported between instances of | |||||
'NoneType' and 'int' | |||||
>>> int(None) | |||||
TypeError: int() argument must be a string, | |||||
a bytes-like object or a number, | |||||
not 'NoneType' | |||||
>>> str(None) | |||||
'None' | |||||
``` | |||||
## Example d'utilisation: | |||||
```python | |||||
def trouve_dans_liste(valeur, liste): | |||||
for element in liste: | |||||
if element == valeur: | |||||
return element | |||||
return None | |||||
``` | |||||
```python | |||||
>>> trouve_dans_liste(2, [1, 2, 3]) | |||||
2 | |||||
>>> trouve_dans_liste(False, [True, False]) | |||||
False | |||||
>>> trouve_dans_liste(1, [3, 4]) | |||||
``` | |||||
```python | |||||
def trouve_dans_liste(liste, valeur): | |||||
for element in liste: | |||||
if element == valeur: | |||||
return element | |||||
``` | |||||
None est Falsy, et on peut vérifier si une variable vaut None avec `is None` | |||||
```python | |||||
# hypothèse: `ma_valeur` n'est pas None | |||||
mon_element = trouve_dans_liste(ma_valeur, ma_liste) | |||||
if mon_element is None: | |||||
print("élément absent de la liste") | |||||
if not mon_element: | |||||
# Peut-être que l'élément n'était pas dans la liste, | |||||
# ou peut-être y était-il, mais avec une valeur falsy | |||||
... | |||||
``` |
@@ -0,0 +1,34 @@ | |||||
+++ | |||||
title = "pass" | |||||
weight = 2 | |||||
+++ | |||||
# pass | |||||
En Python, à cause de l'organisation en blocs indentés, on ne | |||||
peut pas vraiment avoir de blocs vides. Mais parfois, on | |||||
a besoin d'un bloc qui ne fasse rien. | |||||
Dans ce cas, on peut utiliser le mot-clé `pass`, par exemple | |||||
après un if: | |||||
```python | |||||
une_condition = False | |||||
if une_condition: | |||||
pass | |||||
else: | |||||
print("une_condition n'est pas vraie") | |||||
``` | |||||
On peut aussi - et c'est l'usage le plus courant - utiliser `pass` pour | |||||
définir une fonction qui ne fait rien: | |||||
```python | |||||
def ne_fait_rien(): | |||||
pass | |||||
``` | |||||
```python | |||||
>>> ne_fait_rien() | |||||
<rien> | |||||
``` |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 8 - None et pass" | |||||
weight = 8 | |||||
+++ |
@@ -0,0 +1,205 @@ | |||||
+++ | |||||
title = "Dictionnaires" | |||||
weight = 1 | |||||
+++ | |||||
# Dictionnaires | |||||
## Définition | |||||
Un dictionaire est une _association_ entre des clés et des valeurs. | |||||
* Les clés sont uniques | |||||
* Les valeurs sont arbitraires | |||||
## Création de dictionnaires | |||||
```python | |||||
# dictionaire vide | |||||
>>> {} | |||||
# une clé, une valeur | |||||
>>> {"a": 42} | |||||
# deux clés, deux valeurs | |||||
>>> {"a": 42, "b": 53} | |||||
# les clés sont uniques: | |||||
>>> {"a": 42, "a": 53} | |||||
{"a": 53} | |||||
``` | |||||
Note: tous les dictionnaires sont truthy, sauf les dictionnaires vides. | |||||
## Accès aux valeurs | |||||
Avec `[]`, comme pour les listes, mais avec une *clé* à la place d'un *index*. | |||||
```python | |||||
>>> scores = {"john": 10, "bob": 42} | |||||
>>> scores["john"] | |||||
10 | |||||
>>> scores["bob"] | |||||
42 | |||||
>>> scores["charlie"] | |||||
KeyError | |||||
``` | |||||
## Test d'appartenance | |||||
Avec `in`, comme le listes: | |||||
```python | |||||
>>> scores = {"john": 10, "bob": 42} | |||||
>>> "charlie" in scores | |||||
False | |||||
``` | |||||
## Modifier la valeur d'une clé | |||||
Comme pour les listes: on assigne la nouvelle variable: | |||||
```python | |||||
>>> scores = {"john": 10, "bob": 42} | |||||
>>> scores["john"] = 20 | |||||
>>> scores | |||||
{"john": 20, "bob": 42} | |||||
``` | |||||
## Créer une nouvelle clé | |||||
Même méchanisme que pour la modification des clés existantes | |||||
```python | |||||
>>> scores = {"john": 10, "bob": 42} | |||||
>>> scores["charlie"] = 30 | |||||
>>> scores | |||||
{"john": 20, "bob": 42, "charlie": 30} | |||||
``` | |||||
*rappel*: ceci ne fonctionne pas avec les listes! | |||||
```python | |||||
>>> ma_liste = ["a", "b"] | |||||
>>> ma_liste[1] = "c" # ok | |||||
["a", "c"] | |||||
>>> ma_liste[3] = "d" | |||||
IndexError | |||||
``` | |||||
## Itérer sur les clés | |||||
Avec `for ... in ...`, comme pour les listes | |||||
```python | |||||
scores = {"john": 10, "bob": 42} | |||||
for nom in scores: | |||||
# `nom` est assigné à "john" puis "bob" | |||||
score_associé_au_nom = scores[nom] | |||||
print(nom, score_associé_au_nom) | |||||
``` | |||||
## Détruire une clé | |||||
Avec `del` - un nouveau mot-clé: | |||||
```python | |||||
>>> scores = {"john": 10, "bob": 42} | |||||
>>> del scores["bob"] | |||||
>>> scores | |||||
{"john": 10} | |||||
``` | |||||
## Détruire un élément d'une liste | |||||
```python | |||||
>>> fruits = ["pomme", "banane", "poire"] | |||||
>>> del fruits[1] | |||||
>>> fruits | |||||
["pomme", "poire"] | |||||
``` | |||||
## Détruire une variable | |||||
```python | |||||
>>> mon_entier = 42 | |||||
>>> mon_entier += 3 | |||||
>>> mon_entier | |||||
45 | |||||
>>> del mon_entier | |||||
>>> mon_entier == 45 | |||||
NameError: name 'mon_entier' is not defined | |||||
``` | |||||
## Détruire une fonction | |||||
On peu aussi supprimer des fonctions: | |||||
```python | |||||
def ma_fonction(): | |||||
print("bonjour") | |||||
del ma_fonction | |||||
>>> ma_fonction() | |||||
NameError: name 'ma_fonction' is not defined | |||||
``` | |||||
## Des dictionnaires partout | |||||
Les variables globales d'un programme Python sont dans un dictionnaire, | |||||
accessible avec la fonction native `globals()`: | |||||
```python | |||||
$ python3 | |||||
>>> globals() | |||||
{ | |||||
... | |||||
'__doc__': None, | |||||
'__name__': '__main__', | |||||
... | |||||
} | |||||
``` | |||||
On reparlera de `__doc__` et `__name__` un autre jour ... | |||||
```python | |||||
$ python3 | |||||
>>> a = 42 | |||||
>>> globals() | |||||
{ | |||||
... | |||||
'__doc__': None, | |||||
'__name__': '__main__', | |||||
... | |||||
'a': 42 | |||||
} | |||||
``` | |||||
```python | |||||
$ python3 | |||||
>>> a = 42 | |||||
>>> del globals()["a"] | |||||
>>> a | |||||
NameError: name 'a' is not defined | |||||
``` | |||||
On peut accéder aux variables locales d'une fonction avec `locals()` | |||||
```python | |||||
def ma_fonction(): | |||||
a = 42 | |||||
b = 3 | |||||
c = a + b | |||||
print(locals()) | |||||
>>> ma_fonction() | |||||
{'a': 42, 'b': 3, 'c': 45} | |||||
``` | |||||
En revanche, il n'est pas conseillé de modifier le dictionaire renvoyé par `locals()` ... | |||||
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 9 - Dictionnaires" | |||||
weight = 9 | |||||
+++ |
@@ -0,0 +1,169 @@ | |||||
+++ | |||||
title = "Tuples" | |||||
weight = 1 | |||||
+++ | |||||
# Tuples | |||||
## Définition | |||||
Un tuple est un ensemble *ordonné* et *immuable* d'éléments. Le nombre, l'ordre et la valeur des éléments sont fixes. | |||||
## Création de tuples | |||||
```python | |||||
# Un tuple vide | |||||
() | |||||
# Un tuple à un élément | |||||
(1,) # notez la virgule | |||||
# Un tuple à deux éléments, aussi appelé couple | |||||
(1, 2) | |||||
``` | |||||
Sauf pour le tuple vide, c'est la *virgule* qui fait le tuple | |||||
Note: tous les tuples sont truthy, sauf les tuples vides. | |||||
# Tuples hétérogènes | |||||
Comme les listes, les tuples peuvent contenir des éléments de types différents: | |||||
```python | |||||
# Un entier et une string | |||||
mon_tuple = (42, "bonjour") | |||||
# Un entier et un autre tuple | |||||
mon_tuple = (21, (True, "au revoir")) | |||||
``` | |||||
## Accès | |||||
Avec `[]` et l'index de l'élément dans le tuple: | |||||
```python | |||||
mon_tuple = (42, "bonjour") | |||||
mon_tuple[0] | |||||
42 | |||||
mon_tuple[1] | |||||
"bonjour" | |||||
``` | |||||
## Modification | |||||
Interdit! | |||||
```python | |||||
mon_tuple = (42, "bonjour") | |||||
mon_tuple[0] = 44 | |||||
TypeError: 'tuple' object does not support item assignment | |||||
``` | |||||
## Test d'appartenance | |||||
Avec `in` | |||||
```python | |||||
>>> mon_tuple = (42, 14) | |||||
>>> 42 in mon_tuple | |||||
True | |||||
>>> 14 in mon_tuple | |||||
True | |||||
>>> 13 in mon_tuple | |||||
False | |||||
``` | |||||
## Déstructuration | |||||
Créer plusieurs variables en une seule ligne: | |||||
```python | |||||
>>> couple = ("Batman", "Robin") | |||||
>>> héros, side_kick = couple | |||||
>>> héros | |||||
'Batman' | |||||
>>> side_kick | |||||
'Robin' | |||||
``` | |||||
## Quelques erreurs classiques | |||||
```python | |||||
>>> héros, side_kick, ennemi = couple | |||||
ValueError (3 != 2) | |||||
>>> (héros,) = couple | |||||
ValueError (1 != 2) | |||||
# Gare à la virgule: | |||||
>>> héros, = couple | |||||
ValueError (1 != 2) | |||||
``` | |||||
## Pièges | |||||
```python | |||||
f(a, b, c) # appelle f() avec trois arguments | |||||
f((a, b, c)) # appelle f() avec un seul argument | |||||
# (qui est lui-même un tuple à 3 valeurs) | |||||
f(()) # appelle f() avec un tuple vide | |||||
(a) # juste la valeur de a entre parenthèses | |||||
(a,) # un tuple à un élément, qui vaut la valeur de a | |||||
``` | |||||
## On peut aussi déstructurer des listes | |||||
```python | |||||
>>> fruits = ["pomme", "banane", "orange"] | |||||
>>> premier, deuxième, troisième = fruits | |||||
>>> premier | |||||
"pomme" | |||||
>>> deuxième | |||||
"banane" | |||||
>>> troisième | |||||
"orange" | |||||
``` | |||||
On dit aussi: unpacking | |||||
## Utilisations des tuples | |||||
Pour simplifier des conditions: | |||||
```python | |||||
# Avant | |||||
if ( | |||||
ma_valeur == "nord" or | |||||
ma_valeur == "sud" or | |||||
ma_valeur == "ouest" or | |||||
ma_valeur == "est"): | |||||
print("direction", ma_valeur) | |||||
``` | |||||
```python | |||||
# Après | |||||
if ma_valeur in ("nord", "sud", "est", "ouest"): | |||||
print("direction", ma_valeur) | |||||
``` | |||||
## Pour retourner plusieurs valeurs | |||||
```python | |||||
def tire_carte(): | |||||
valeur = "10" | |||||
couleur = "trèfle" | |||||
return (valeur, couleur) | |||||
v, c = tire_carte() | |||||
print(v, "de", c) | |||||
# 10 de trèfle | |||||
``` | |||||
Ce n'est pas une nouvelle syntaxe, juste de la manipulation de tuples! |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 10 - tuples" | |||||
weight = 10 | |||||
+++ |
@@ -0,0 +1,319 @@ | |||||
+++ | |||||
title = "Classes" | |||||
weight = 1 | |||||
+++ | |||||
# Classes | |||||
Ce qu’on a vu jusqu’ici: | |||||
* Des types simples (entiers, booléens, ...) | |||||
* Des structures de données (listes, dictionnaires, ...) | |||||
* Des fonctions qui manipulent ces types ou ces types | |||||
* Des fonctions qui s’appellent les unes les autres | |||||
On appelle cet ensemble de concepts, cette façon d'écrire du code, un *paradigme* - | |||||
et c'est un paradigme *procédural*. | |||||
On va passer à un autre paradigme: l'*orienté objet*. | |||||
## Orienté objet - une première définition | |||||
Un "objet" informatique *représente* un véritable "objet" physique | |||||
dans le vrai monde véritable. | |||||
Ce n'est pas une très bonne définition: | |||||
1. Ce n'est pas nécessaire | |||||
2. Ce n'est même pas forcément souhaitable! | |||||
Je le mentionne juste parce que c'est une idée reçue très répandue. | |||||
## Orienté objet - 2ème définition | |||||
Une meilleure définition, c'est de dire que la programmation | |||||
orientée objet permet de mettre au même endroit: | |||||
* des données | |||||
* des fonctions qui opèrent sur ces données | |||||
L'important c'est que les deux aillent ensemble! | |||||
*Note: ce n'est pas **la** meilleure définition de l'orienté objet, mais on s'en contentera | |||||
pour le moment ...* | |||||
## Les classes | |||||
On va parler *d'une* façon de faire de l'orienté objet: avec des classes. | |||||
Mais notez bien qu'on peut faire de l'orienté objet *sans* classes! | |||||
## Le plan de construction | |||||
On dit souvent qu'en Python, "tout est objet". | |||||
Pour bien comprendre cela, il faut d'abord parler des *classes* et des *instances de classes*. | |||||
Une classe est un *plan de construction*, et est définie ainsi: | |||||
```python | |||||
class MaClasse: | |||||
# du code ici | |||||
``` | |||||
Comme les fonctions, les classes contienent un *corps*, qui est le bloc *identé* en dessous | |||||
du mot-clé `class`, de nom de la classe et du `:` en fin de ligne. | |||||
Les classes sont utilisées pour construire des *instances*. | |||||
## Créons des instances | |||||
On peut faire un plan de construction vide avec le mot clé pass: | |||||
```python | |||||
class MaClasse: | |||||
pass | |||||
``` | |||||
Dans ce cas, on crée une instance en mettant le nom de la classe suivi d'une paire de parenthèses - | |||||
un peu comme pour appeler une fonction: | |||||
```python | |||||
>>> mon_instance = MaClasse() | |||||
``` | |||||
Ici, `mon_instance` est une *instance* de la classe `MaClasse`. | |||||
## Attributs | |||||
Les attributs sont des éléments **nommés** à *l'intérieur* d'une instance. | |||||
On peut y accéder avec la syntaxe `<instance>.<attribut>`: | |||||
```python | |||||
y = a.x | |||||
``` | |||||
Ici, `y` est l'attribut `x` de l'instance `a`. | |||||
Les attributs peuvent être des fonctions: | |||||
```python | |||||
func = a.x | |||||
func(10) | |||||
``` | |||||
Ici, on crée une variable `func` qui prend la valeur de l'attribut `x` dans l'instance `a`, puis | |||||
on l'appelle avec l'argument `10` à la ligne suivante. | |||||
Le code suivant fait exactement la même chose, mais avec une ligne de moins: | |||||
```python | |||||
a.x(10) | |||||
``` | |||||
On peut *créer* des attributs dans *n'importe quel instance*, en utilisant l'*assignation*: | |||||
```python | |||||
>>> mon_instance = MaClasse() | |||||
# Création de l'attribut `x` dans `mon_instance` | |||||
>>> mon_instance.x = 42 | |||||
# Accés à l'attribut `x` dans `mon_instance` | |||||
>>> mon_instance.mon_attribut | |||||
42 | |||||
``` | |||||
## Méthodes - définition | |||||
On peut aussi mettre des *méthodes* dans des classes. | |||||
On utilise `def`, comme pour les fonctions, mais les méthodes *doivent* avoir au | |||||
moins un argument appelé `self`, et être à l'intérieur du bloc de la classe: | |||||
```python | |||||
class MaClasse: | |||||
def ma_méthode(self): | |||||
return 42 | |||||
``` | |||||
## Méthodes - appel | |||||
Une méthode ne peut être appelée que depuis une *instance* de | |||||
la classe: | |||||
```python | |||||
class MaClasse: | |||||
def ma_méthode(self): | |||||
return 42 | |||||
>>> ma_méthode() | |||||
Erreur | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.ma_méthode() | |||||
42 | |||||
``` | |||||
Notez qu'on ne passe *pas* d'argument quand on apelle `ma_méthode` depuis l'instance. | |||||
## Méthodes et attributs | |||||
`self` *prend la valeur de l'instance courante* quand la méthode est appelée. | |||||
On peut le voir en utilisant des attributs: | |||||
```python | |||||
class MaClasse: | |||||
def affiche_attribut_x(self): | |||||
# Accès à l'attribut `x` dans `self` | |||||
print(self.x) | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.x = 42 | |||||
>>> mon_instance.affiche_attribut_x() | |||||
42 | |||||
``` | |||||
On peut aussi *créer* des attributs dans une méthode: | |||||
```python | |||||
class MaClasse: | |||||
def crée_attribut_x(self): | |||||
self.x = 42 | |||||
def affiche_attribut_x(self): | |||||
print(self.x) | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.affiche_attribut_x() | |||||
# Erreur: `mon_instance` n'a pas d'attribut `x` | |||||
>>> mon_instance.crée_attribut_x() | |||||
>>> mon_instance.affiche_attribut_x() | |||||
42 | |||||
``` | |||||
Les méthodes peuveunt aussi prendre plusieurs arguments, en plus de `self` - mais `self` doit | |||||
toujours être le premier argument. | |||||
Par example, pour créer un attribut avec une certaine valeur: | |||||
```python | |||||
class MaClasse | |||||
def crée_attribut_x(self, valeur_de_x): | |||||
self.x = valeur_de_x | |||||
def affiche_attribut_x(self); | |||||
print(self.x) | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.crée_attribut_x(42) | |||||
>>> mon_instance.affiche_attribut_x() | |||||
42 | |||||
``` | |||||
## Méthodes appelant d'autres méthodes | |||||
Comme les méthodes sont *aussi* des attributs, les méthodes d'une instance peuvent s'appeler | |||||
les unes les autres: | |||||
```python | |||||
class MaClasse: | |||||
def méthode_1(self): | |||||
print("démarrage de la méthode 1") | |||||
print("la méthode 1 affiche bonjour") | |||||
print("bonjour") | |||||
print("fin de la méthode 1") | |||||
def méthode_2(self): | |||||
print("la méthode 2 appelle la méthode 1") | |||||
self.méthode_1() | |||||
print("fin de la méthode 2") | |||||
``` | |||||
```python | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.méthode_2() | |||||
``` | |||||
```text | |||||
la méthode 2 appelle la méthode 1 | |||||
démarrage de la méthode 1 | |||||
la méthode 1 affiche bonjour | |||||
bonjour | |||||
fin de la méthode 1 | |||||
fin de la méthode 2 | |||||
``` | |||||
## Une méthode spéciale | |||||
Si vous définissez une méthode `__init__`, celle-ci est appelée *automatiquement* | |||||
quand l'instance est construite. | |||||
On dit que c'est une méthode "magique" parce qu'elle fait quelque chose _sans_ qu'on | |||||
l'appelle explicitement. | |||||
On utilise souvent `__init__` pour créer des attributs | |||||
```python | |||||
class MaClasse: | |||||
def __init__(self): | |||||
self.x = 1 | |||||
self.y = 2 | |||||
>>> mon_instance = MaClasse() | |||||
# __init__ est appelée automatiquement! | |||||
>>> mon_instance.x | |||||
1 | |||||
>>> mon_instance.y | |||||
2 | |||||
``` | |||||
On prend souvent les *valeurs* des attributs à créer en arguments de la méthode `__init__ `. | |||||
```python | |||||
class MaClasse: | |||||
def __init__(self, x, y): | |||||
self.x = x | |||||
self.y = y | |||||
``` | |||||
Dans ce cas, les arguments de la méthode `__init__` apparaissent à l'intérieur des parenthèses après le | |||||
nom de la classe: | |||||
``` | |||||
>>> mon_instance = MaClasse(3, 4) | |||||
>>> mon_instance.x | |||||
3 | |||||
>>> mon_instance.y | |||||
4 | |||||
``` | |||||
*Pour cette raison, `__init__` est souvent appelé le _constructeur_ de la classe.* | |||||
## Récapitulatif | |||||
* Classe: plan de construction | |||||
* Instance: valeur issue d'une classe | |||||
* Attribut: variable dans une instance | |||||
* Méthode: fonction dans une instance (qui prend `self` en premier argument) | |||||
* `__init__`: méthode magique appelée automatiquement pendant l'instanciation | |||||
## Classes et programmation orienté objet | |||||
Ainsi, on peut ranger au même endroit des données et des fonctions opérant sur ces données. | |||||
Les données sont les attributs, et les fonctions opérant sur ces attributs sont les méthodes. | |||||
On peut ainsi séparer les *responsabilités* à l'intérieur d'un code en les répartissant | |||||
entres plusieurs classes. |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 11 - Classes (1ère partie)" | |||||
weight = 11 | |||||
+++ |
@@ -0,0 +1,148 @@ | |||||
+++ | |||||
title = "Modules" | |||||
weight = 1 | |||||
+++ | |||||
# Modules | |||||
## Un fichier = un module | |||||
Et oui, vous faites des modules sans le savoir depuis le début :) | |||||
Un fichier `foo.py` correspond *toujours* module `foo` | |||||
**Attention: Ce n'est pas tout à fait réciproque. Le module `foo` peut venir d'autre chose | |||||
qu'un fichier foo.py.** | |||||
# Importer un module | |||||
Ou: accéder à du code provenant d'un *autre* fichier source. | |||||
Imaginons un fichier `bonjour.py` contenant seulement une assignation | |||||
d'une variable `a` à l'entier 42 : | |||||
```python | |||||
# Dans bonjour.py | |||||
a = 42 | |||||
``` | |||||
On peut accéder à cette variable en important le module, par | |||||
exemple depuis l'interpréteur, en utilisant le mot-clé `import` | |||||
suivi du nom du module: | |||||
```python | |||||
$ python | |||||
>>> import bonjour | |||||
>>> bonjour.a | |||||
42 | |||||
``` | |||||
Notez que pour que cela fonctionne: | |||||
* Le nom du module est écrit directement, ce n'est *pas* une | |||||
chaîne de caractères. | |||||
* Il faut lancer la commande `python` sans argument | |||||
* Il faut la lancer depuis le répertoire qui contient `bonjour.py`. | |||||
On voit que l'assignation de la variable `a` dans `bonjour.py` est devenue | |||||
un *attribut* du module `bonjour` lorsque `bonjour` a été importé | |||||
\newpage | |||||
Si maintenant on rajoute une fonction `dire_bonjour` dans `bonjour.py`: | |||||
```python | |||||
# toujours dans bonjour.py | |||||
a = 42 | |||||
def dire_bonjour(): | |||||
print("Bonjour!") | |||||
``` | |||||
On peut appeler la fonction `dire_bonjour` depuis l'interpréteur en accédant | |||||
à l'attribut `dire_bonjour` du module `bonjour`: | |||||
```python | |||||
>>> import bonjour | |||||
>>> bonjour.dire_bonjour() | |||||
Bonjour! | |||||
``` | |||||
## Différence avec la commande python | |||||
Notez bien que lancer l'interpréteur et taper `import bonjour` dedans n'est pas | |||||
la même chose que lancer `python bonjour.py`. | |||||
Dans le deuxième cas, tout le code dans `bonjour.py` est exécuté, puis la commande python | |||||
se termine. | |||||
Dans le cas de l'interpréteur, on peut utiliser tous les attributs du module et appeler | |||||
les fonctions autant de fois qu'on veut: | |||||
```python | |||||
>>> import bonjour | |||||
>>> bonjour.dire_bonjour() | |||||
Bonjour! | |||||
>>> bonjour.dire_bonjour() | |||||
Bonjour! | |||||
``` | |||||
On peut aussi modifier les valeurs des attributs: | |||||
```python | |||||
>>> import bonjour | |||||
>>> bonjour.a | |||||
42 | |||||
>>> bonjour.a = 36 | |||||
>>> bonjour.a | |||||
36 | |||||
``` | |||||
## Les imports ne sont faits qu'une seule fois | |||||
Il est important de noter que le code à l'intérieur d'un | |||||
module n'est *pas* ré-éxécuté si le module a déjà été | |||||
importé auparavant. | |||||
On peut le voir en mettant du code dans `bonjour.py`, | |||||
en plus des simples définitions de fonctions et assignations | |||||
de variables | |||||
```python | |||||
# Dans bonjour.py | |||||
print("Je suis le module bonjour et tu viens de m’importer") | |||||
``` | |||||
```python | |||||
>>> import bonjour | |||||
Je suis le module foo et tu viens de m’importer | |||||
>>> import bonjour | |||||
<rien> | |||||
``` | |||||
Il faudra donc redémarrer l'interpréteur à chaque fois que le code dans `bonjour.py` change. | |||||
## La bibliothèque standard | |||||
La bibliothèque standard est une collection de modules directement utilisables fournis à l'installation de Python. | |||||
Exemple: `sys`, `random`, ... | |||||
Toute la bibliothèque standard est documentée - et la traduction en Français est en cours: | |||||
https://docs.python.org/fr/3/library/index.html | |||||
Mettez ce lien dans vos favoris - il vous sera très utile. | |||||
## Quelques exemples de modules de la bibliothèque standard | |||||
### Easter eggs | |||||
(Ou fonctionnalités cachées) | |||||
* `import antigravity` | |||||
* `import this` | |||||
Je vous laisse découvrir ce que fait le premier. Quant au deuxième, il contient | |||||
une liste de préceptes que la plupart des développeurs Python s'efforcent de | |||||
respecter. On en reparlera ... |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 12 - Modules - 1ère partie" | |||||
weight = 12 | |||||
+++ |
@@ -0,0 +1,186 @@ | |||||
+++ | |||||
title = "Rappels" | |||||
weight = 1 | |||||
+++ | |||||
# Rappels | |||||
_Note: ceci est surtout un rappel du chapitre 11. N'hésitez pas à vous y | |||||
reporter si les exemples de code ne vous paraissent pas clairs._ | |||||
## Classes vides | |||||
Définition: | |||||
```python | |||||
class MaClasse: | |||||
pass | |||||
``` | |||||
Instanciation: | |||||
```python | |||||
>>> instance_1 = MaClasse() | |||||
``` | |||||
## Attributs | |||||
Un attribut est une variable _à l'intérieur_ d'autre chose (par exemple une instance de classe). | |||||
La syntaxe consiste en l'instance à gauche et l'attribut à droite après un point: | |||||
```python | |||||
>>> mon_instance = MaClasse() | |||||
# création de l'attribut `x` dans mon_instance: | |||||
>>> mon_instance.x = 42 | |||||
# accès à l'attribut `x` dans mon_instance: | |||||
>>> mon_instance.x | |||||
42 | |||||
``` | |||||
## Méthodes | |||||
Une méthode est une fonction définie à l'intérieur d'une classe: | |||||
Définition: | |||||
```python | |||||
class MaClasse: | |||||
def ma_méthode(self): | |||||
return 42 | |||||
``` | |||||
Les méthodes sont des attributs des instances de classes: | |||||
```python | |||||
class MaClasse: | |||||
def ma_méthode(self): | |||||
return 42 | |||||
>>> ma_méthode() | |||||
Erreur | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.ma_méthode() | |||||
42 | |||||
``` | |||||
## self | |||||
`self` *prend la valeur de l'instance courante* quand la méthode est appelée. | |||||
```python | |||||
class MaClasse: | |||||
def affiche_attribut_x(self): | |||||
print(self.x) | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.x = 42 | |||||
>>> mon_instance.affiche_attribut_x() | |||||
42 | |||||
``` | |||||
On peut aussi *créer* des attributs dans une méthode: | |||||
```python | |||||
class MaClasse: | |||||
def crée_attribut_x(self): | |||||
self.x = 42 | |||||
def affiche_attribut_x(self): | |||||
print(self.x) | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.affiche_attribut_x() | |||||
# Erreur: `mon_instance` n'a pas d'attribut `x` | |||||
>>> mon_instance.crée_attribut_x() | |||||
>>> mon_instance.affiche_attribut_x() | |||||
42 | |||||
``` | |||||
## Méthodes avec arguments | |||||
```python | |||||
class MaClasse | |||||
def crée_attribut_x(self, valeur_de_x): | |||||
self.x = valeur_de_x | |||||
def affiche_attribut_x(self); | |||||
print(self.x) | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.crée_attribut_x(42) | |||||
>>> mon_instance.affiche_attribut_x() | |||||
42 | |||||
``` | |||||
## Méthodes appelant d'autres méthodes | |||||
```python | |||||
class MaClasse: | |||||
def méthode_1(self): | |||||
print("démarrage de la méthode 1") | |||||
print("la méthode 1 affiche bonjour") | |||||
print("bonjour") | |||||
print("fin de la méthode 1") | |||||
def méthode_2(self): | |||||
print("la méthode 2 appelle la méthode 1") | |||||
self.méthode_1() | |||||
print("fin de la méthode 2") | |||||
``` | |||||
```python | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.méthode_2() | |||||
``` | |||||
```text | |||||
la méthode 2 appelle la méthode 1 | |||||
démarrage de la méthode 1 | |||||
la méthode 1 affiche bonjour | |||||
bonjour | |||||
fin de la méthode 1 | |||||
fin de la méthode 2 | |||||
``` | |||||
## Constructeur sans arguments | |||||
Un constructeur en Python désigne la méthode nomée `__init__`, | |||||
quand celle-ci existe. | |||||
La méthode `__init__` est appelée automatiquement quand la | |||||
classe est instanciée: | |||||
```python | |||||
class MaClasse: | |||||
def __init__(self): | |||||
self.x = 1 | |||||
self.y = 2 | |||||
>>> mon_instance = MaClasse() | |||||
>>> mon_instance.x | |||||
1 | |||||
>>> mon_instance.y | |||||
2 | |||||
``` | |||||
## Constructeur avec arguments | |||||
La méthode `__init__` peut avoir des arguments, | |||||
dans ce cas, ceux ci doivent être fournis | |||||
lors de l'instanciation: | |||||
```python | |||||
class MaClasse: | |||||
def __init__(self, x, y): | |||||
self.x = x | |||||
self.y = y | |||||
``` | |||||
```python | |||||
>>> mon_instance = MaClasse(3, 4) | |||||
>>> mon_instance.x | |||||
3 | |||||
>>> mon_instance.y | |||||
4 | |||||
``` |
@@ -0,0 +1,91 @@ | |||||
+++ | |||||
title = "Couplage" | |||||
weight = 2 | |||||
+++ | |||||
# Couplage | |||||
## Définition | |||||
Un couplage décrit une relation entre deux classes. | |||||
## Exemple | |||||
Ici on veut représenter des chats et des humains qui adoptent (on non) des chats. | |||||
Tous les chats ont un nom, et tous les humains ont un prénom. | |||||
On peut utiliser pour cela deux classes: `Chat` et `Humain`: | |||||
```python | |||||
class Chat: | |||||
def __init__(self, nom): | |||||
self.nom = nom | |||||
>>> chat = Chat("Monsieur Moustaches") | |||||
>>> chat.nom | |||||
'Monsieur Moustaches' | |||||
``` | |||||
```python | |||||
class Humain: | |||||
def __init__(self, prénom): | |||||
self.prénom = prénom | |||||
>>> alice = Humain(prénom="Alice") | |||||
>>> alice.prénom | |||||
"Alice" | |||||
``` | |||||
Maintenant on veut que les humains puissent adopter des chats. | |||||
Pour cela, on peut rajouter la méthode `adopte` dans la classe | |||||
`Humain`. | |||||
Cette méthode va prendre un argument - une instance de la | |||||
classe `Chat`: | |||||
```python | |||||
class Humain: | |||||
def __init__(self, prénom): | |||||
self.prénom = prénom | |||||
def adopte(self, chat): | |||||
print(self.prénom, "adopte un chat") | |||||
>>> boule_de_poils = Chat("Boule de Poils") | |||||
>>> alice = Humain("Alice") | |||||
>>> alice.adopte(boule_de_poils) | |||||
"Alice adopte un chat" | |||||
``` | |||||
On peut accéder au nom du chat depuis la méthode `adopte`, | |||||
en utilisant la syntaxe `nom.attribut` vue précédemment: | |||||
```python | |||||
class Humain: | |||||
def __init__(self, prénom): | |||||
self.prénom = prénom | |||||
def adopte(self, chat): | |||||
print(self.prénom, "adopte", chat.nom) | |||||
>>> boule_de_poils = Chat("Boule de Poils") | |||||
>>> alice = Humain("Alice") | |||||
>>> alice.adopte(boule_de_poils) | |||||
"Alice adopte Boule de Poils" | |||||
``` | |||||
## Couplage | |||||
```python | |||||
class Humain: | |||||
... | |||||
def adopte(self, chat): | |||||
print(self.prénom, "adopte", chat.nom) | |||||
``` | |||||
Notez également que nous avons écrit `chat.nom`. ainsi, la méthode `adopte()` | |||||
ne peut être appelée que part une instance qui a un attribut `nom` - sinon | |||||
on aura une erreur. | |||||
Donc si on modifie la classe `Chat` et qu'on renomme l'attribut `nom` en `surnom` par exemple, | |||||
la méthode `adopte()` de la classe `Humain` cessera de fonctionner: on dit | |||||
qu'on a un *couplage* entre les classes `Chat` et `Humain`. |
@@ -0,0 +1,117 @@ | |||||
+++ | |||||
title = "Composition" | |||||
weight = 3 | |||||
+++ | |||||
# Composition | |||||
## Définition | |||||
Une classe à l'intérieur d'une autre classe. | |||||
## Dépendances entre fonctions | |||||
Exemple: on veut dessiner un sapin dans le terminal: | |||||
```python | |||||
def main(): | |||||
largeur = demander_largeur() | |||||
dessine_sapin(largeur) | |||||
main() | |||||
``` | |||||
On voit que la fonction `dessine_sapin()` prend un argument `largeur`, qui est retourné | |||||
par la fonction `demander_largeur()`. | |||||
`dessine_sapin()` doit donc être appelée *après* `demander_largeur()`. On dit que `dessine_sapin()` | |||||
_dépend_ de `demander_largeur()`. | |||||
## Dépendances entre classes | |||||
Un bon moyen d'introduire une dépendance entre deux classes est d'utiliser les constructeurs. | |||||
Revoyons la classe Chat: | |||||
```python | |||||
class Chat: | |||||
def __init__(self, nom): | |||||
self.nom = nome | |||||
``` | |||||
Comme le constructeur de la classe Chat prend un nom en argument, il est impossible de construire | |||||
des chats sans nom: | |||||
```python | |||||
>>> chat = Chat() | |||||
TypeError: __init__() missing 1 required positional argument: 'nom' | |||||
``` | |||||
De la même façon, si on veut que tous les enfants aient un chat (pourquoi pas, après tout), on peut | |||||
avoir une classe Enfant, dont le constructeur prend une instance de chat en plus du prénom: | |||||
```python | |||||
class Enfant: | |||||
def __init__(self, prénom, chat): | |||||
self.prénom = prénom | |||||
self.chat = chat | |||||
>>> alice = Enfant("Alice") | |||||
TypeError: __init__() missing 1 required positional argument: 'chat' | |||||
>>> boule_de_poils = Chat("Boule de Poils") | |||||
>>> alice = Enfant("Alice", boule_de_poils) | |||||
# OK! | |||||
``` | |||||
## Utilisation de la composition | |||||
Maintenant qu'on vit dans un monde où tous les enfants ont chacun un chat, on peut | |||||
par exemple consoler tous les enfants en leur demandant de caresser leur chat, chat | |||||
qui va ronronner et faire plaisir à son propriétaire. | |||||
Voici comment on peut coder cela: d'abord, on rajoute les méthodes `caresse()` | |||||
et `ronronne()` dans la classe Chat: | |||||
```python | |||||
class Chat: | |||||
def __init__(self, nom): | |||||
self.nom = nom | |||||
def ronronne(self): | |||||
print(self.nom, 'fait: "prrrrr"') | |||||
def caresse(self): | |||||
self.ronronne() | |||||
>>> boule_de_poils = Chat("Boule de Poils") | |||||
>>> boule_de_poils.caresse() | |||||
Boule de Poils fait "prrrrr" | |||||
``` | |||||
Ensuite, on peut rajouter la méthode `console()` dans la classe Enfant, | |||||
qui va: | |||||
* récupérer l'instance de la classe Chat dans `self` - comme n'importe quel attribut | |||||
* puis appeler la méthode `caresse()` de cette instance | |||||
```python | |||||
class Enfant: | |||||
def __init__(self, prénom, chat): | |||||
self.chat = chat | |||||
def console(self): | |||||
self.chat.caresse() | |||||
>>> boule_de_poils = Chat("Boule de Poils") | |||||
>>> alice = Enfant("Alice", boule_de_poils) | |||||
# Alice est triste, on la console | |||||
>>> alice.console() | |||||
Boule de Poils fait "prrrrr" | |||||
# Alice est consolée :) | |||||
``` | |||||
On dit parfois qu'on a *délégué* l'implémentation de la méthode `console()` de la classe Enfant | |||||
à la méthode `caresse()` de la classe Chat. |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 13 - Classes (2ème partie)" | |||||
weight = 13 | |||||
+++ |
@@ -0,0 +1,42 @@ | |||||
+++ | |||||
title = "Introduction" | |||||
weight = 1 | |||||
+++ | |||||
# Introduction | |||||
## Importer un module | |||||
Souvenez-vous, dans le chapitre 12 nous avons vu que le code suivant | |||||
Ce code fonctionne s'il y a un ficher `foo.py` quelque part qui contient la fonction | |||||
`bar` [^1]: | |||||
```python | |||||
import foo | |||||
foo.bar() | |||||
``` | |||||
Ce fichier peut être présent soit dans le répertoire courant, soit dans la bibliothèque standard Python. | |||||
## La variable PATH | |||||
Vous connaissez peut-être le rôle de la variable d'environnement `PATH`. Celle-ci contient une liste de chemins, | |||||
séparés par le caractère `:` et est utilisée par votre shell pour trouver le chemin complet des commandes que vous lancez. | |||||
Par exemple: | |||||
```bash | |||||
PATH="/bin:/usr/bin:/usr/sbin" | |||||
$ ifconfig | |||||
# lance le binaire /usr/sbin/ifconfig | |||||
$ ls | |||||
# lance le binaire /bin/ls | |||||
``` | |||||
Le chemin est "résolu" par le shell en parcourant la liste de tout les | |||||
chemins de la variable `PATH`, et en regardant si le chemin complet | |||||
existe. La résolution s'arrête dès le premier chemin trouvé. | |||||
Par exemple, si vous avez `PATH="/home/user/bin:/usr/bin"` et un fichier `ls` dans `/home/user/bin/ls`, c'est ce fichier-là | |||||
(et non `/bin/ls`) qui sera utilisé quand vous taperez `ls`. |
@@ -0,0 +1,439 @@ | |||||
+++ | |||||
title = "sys.path" | |||||
weight = 2 | |||||
+++ | |||||
# sys.path | |||||
En Python, il existe une variable `path` prédéfinie dans le module `sys` qui fonctionne de manière similaire | |||||
à la variable d'environnement `PATH`. | |||||
Si j'essaye de l'afficher sur ma machine, voici ce que j'obtiens : | |||||
```python | |||||
import sys | |||||
print(sys.path) | |||||
``` | |||||
``` | |||||
[ | |||||
"", | |||||
"/usr/lib/python3.8", | |||||
"/usr/lib/python3.8/lib-dynload", | |||||
"/home/dmerej/.local/lib/python3.8/", | |||||
"/usr/lib/python3.8/site-packages", | |||||
] | |||||
``` | |||||
Le résultat dépend: | |||||
* du système d'exploitation | |||||
* de la façon dont Python a été installé | |||||
* et de la présence ou non de certains réportoires. | |||||
En fait, `sys.path` est construit dynamiquement par l'interpréteur Python au démarrage. | |||||
Notez également que `sys.path` commence par une chaîne vide. En pratique, cela signifie que le répertoire courant a la priorité sur tout le reste. | |||||
## Priorité du répertoire courant | |||||
Prenons un exemple. Si vous ouvrez un explorateur de fichiers dans le deuxième | |||||
élément de la liste de `sys.path` (`/usr/lib/python3.8/` sur ma machine), vous trouverez | |||||
un grand nombre de fichiers Python. | |||||
Notamment, vous devriez trouver un fichier `random.py` dans ce répertoire. | |||||
En fait, vous trouverez la plupart des modules de la bibliothèque standard dans | |||||
ce répertoire. | |||||
Maintenant, imaginons que vous avez un deuxième fichier `random.py` dans votre répertoire courant. Finalement, imaginez | |||||
que vous lancez un fichier `foo.py` contentant `import random` dans ce même réportoire. | |||||
Et bien, c'est le fichier `random.py` de votre répertoire qui sera utilisé, et non celui de la bibliothèque standard! | |||||
## Permissions des répertoires de sys.path | |||||
Un autre aspect notable de `sys.path` est qu'il ne contient que deux répertoires dans lesquels l'utilisateur courant peut potentiellement écrire : le chemin courant et le chemin dans `~/.local/lib`. Tous les autres (`/usr/lib/python3.8/`, etc.) sont des chemins "système" et ne peuvent être modifiés que par un compte administrateur (avec `root` ou `sudo`, donc). | |||||
La situation est semblable sur macOS et Windows [^2]. | |||||
## Bibliothèques tierces | |||||
Prenons un exemple : | |||||
```python | |||||
# dans foo.py | |||||
import tabulate | |||||
scores = [ | |||||
["John", 345], | |||||
["Mary-Jane", 2], | |||||
["Bob", 543], | |||||
] | |||||
table = tabulate.tabulate(scores) | |||||
print(table) | |||||
``` | |||||
``` | |||||
$ python3 foo.py | |||||
--------- --- | |||||
John 345 | |||||
Mary-Jane 2 | |||||
Bob 543 | |||||
--------- --- | |||||
``` | |||||
Ici, le module `tabulate` n'est ni dans la bibliothèque standard, ni écrit par l'auteur du script `foo.py`. On dit que c'est une bibliothèque tierce. | |||||
On peut trouver [le code source de tabulate](https://bitbucket.org/astanin/python-tabulate/src/master/) facilement. La question qui se pose alors est: comment faire en sorte que `sys.path` contienne le module `tabulate`? | |||||
Eh bien, plusieurs solutions s'offrent à vous. | |||||
# Le gestionnaire de paquets | |||||
Si vous utilisez une distribution Linux, peut-être pourrez-vous utiliser votre gestionnaire de paquets : | |||||
```bash | |||||
$ sudo apt install python3-tabulate | |||||
``` | |||||
Comme vous lancez votre gestionnaire de paquets avec `sudo`, celui-ci sera capable d'écrire dans les chemins système de `sys.path`. | |||||
# À la main | |||||
Une autre méthode consiste à partir des sources - par exemple, si le paquet de votre distribution n'est pas assez récent, ou si vous avez besoin de modifier le code de la bibliothèque en question. | |||||
Voici une marche à suivre possible : | |||||
1. Récupérer les sources de la version qui vous intéresse dans la [section téléchargement de bitbucket](https://bitbucket.org/astanin/python-tabulate/downloads/?tab=tags). | |||||
1. Extraire l'archive, par exemple dans `src/tabulate` | |||||
1. Se rendre dans `src/tabulate` et lancer `python3 setup.py install --user` | |||||
# Anatomie du fichier setup.py | |||||
La plupart des bibliothèques Python contiennent un `setup.py` à | |||||
la racine de leurs sources. Il sert à plein de choses, la commande `install` | |||||
n'étant qu'une parmi d'autres. | |||||
Le fichier `setup.py` contient en général simplement un `import` de `setuptools`, et un appel à la fonction `setup()`, avec de nombreux arguments : | |||||
```python | |||||
# tabulate/setup.py | |||||
from setuptools import setup | |||||
setup( | |||||
name='tabulate', | |||||
version='0.8.1', | |||||
description='Pretty-print tabular data', | |||||
py_modules=["tabulate"], | |||||
scripts=["bin/tabulate"], | |||||
... | |||||
) | |||||
``` | |||||
# Résultat de l'invocation de setup.py | |||||
Par défaut, `setup.py` essaiera d'écrire dans un des chemins système de | |||||
`sys.path` [^3], d'où l'utilisation de l'option `--user`. | |||||
Voici à quoi ressemble la sortie de la commande : | |||||
```bash | |||||
$ cd src/tabulate | |||||
$ python3 setup.py install --user | |||||
running install | |||||
... | |||||
Copying tabulate-0.8.4-py3.7.egg to /home/dmerej/.local/lib/python3.7/site-packages | |||||
... | |||||
Installing tabulate script to /home/dmerej/.local/bin | |||||
``` | |||||
Notez que module a été copié dans `~/.local/lib/python3.7/site-packages/` et le script dans `~/.local/bin`. Cela signifie que *tous* les scripts Python lancés par l'utilisateur courant auront accès au module `tabulate`. | |||||
Notez également qu'un script a été installé dans `~/.local/bin` - Une bibliothèque Python peut contenir aussi bien des modules que des scripts. | |||||
Un point important est que vous n'avez en général pas besoin de lancer le script directement. Vous pouvez utiliser `python3 -m tabulate`. Procéder de cette façon est intéressant puisque vous n'avez pas à vous soucier de rajouter le chemin d'installation des scripts dans la variable d'environnement PATH. | |||||
# Dépendances | |||||
Prenons une autre bibliothèque : `cli-ui`. | |||||
Elle permet d'afficher du texte en couleur dans un terminal | |||||
```python | |||||
import cli_ui | |||||
cli_ui.info("Ceci est en", cli_ui.red, "rouge") | |||||
``` | |||||
Elle permet également d'afficher des tableaux en couleur : | |||||
```python | |||||
headers=["name", "score"] | |||||
data = [ | |||||
[(bold, "John"), (green, 10.0)], | |||||
[(bold, "Jane"), (green, 5.0)], | |||||
] | |||||
cli_ui.info_table(data, headers=headers) | |||||
``` | |||||
Pour ce faire, elle repose sur la bibliothèque `tabulate` vue précédemment. On dit que `cli-ui` *dépend* de `tabulate`. | |||||
# Déclaration des dépendances | |||||
La déclaration de la dépendance de `cli-ui` vers `tabulate` s'effectue également dans le fichier `setup.py`: | |||||
```python | |||||
setup( | |||||
name="cli-ui", | |||||
version="0.9.1", | |||||
install_requires=[ | |||||
"tabulate", | |||||
... | |||||
], | |||||
... | |||||
) | |||||
``` | |||||
# pypi.org | |||||
On comprend dès lors qu'il doit nécessairement exister un *annuaire* permettant de relier les noms de dépendances à leur code source. | |||||
Cet annuaire, c'est le site [pypi.org](https://pypi.org/). Vous y trouverez les pages correspondant à [tabulate](https://pypi.org/project/tabulate/) et [cli-ui](https://pypi.org/project/python-cli-ui/). | |||||
# pip | |||||
`pip` est un outil qui vient par défaut avec Python3[^4]. Vous pouvez également l'installer grâce au script [get-pip.py](https://bootstrap.pypa.io/get-pip.py), en lançant `python3 get-pip.py --user`. | |||||
Il est conseillé de *toujours* lancer `pip` avec `python3 -m pip`. De cette façon, vous êtes certains d'utiliser le module `pip` correspondant à votre binaire `python3`, et vous ne dépendez pas de ce qu'il y a dans votre `PATH`. | |||||
`pip` est capable d'interroger le site `pypi.org` pour retrouver les dépendances, et également de lancer les différents scripts `setup.py`. | |||||
Comme de nombreux outils, il s'utilise à l'aide de *commandes*. Voici comment installer `cli-ui` à l'aide de la commande 'install' de `pip`: | |||||
```bash | |||||
$ python3 -m pip install cli-ui --user | |||||
Collecting cli-ui | |||||
... | |||||
Requirement already satisfied: unidecode in /usr/lib/python3.7/site-packages (from cli-ui) (1.0.23) | |||||
Requirement already satisfied: colorama in /usr/lib/python3.7/site-packages (from cli-ui) (0.4.1) | |||||
Requirement already satisfied: tabulate in /mnt/data/dmerej/src/python-tabulate (from cli-ui) (0.8.4) | |||||
Installing collected packages: cli-ui | |||||
Successfully installed cli-ui-0.9.1 | |||||
``` | |||||
On constate ici quelques limitations de `pip`: | |||||
* Il faut penser à utiliser `--user` (de la même façon que lorsqu'on lance `setup.py` à la main) | |||||
* Si le paquet est déjà installé dans le système, pip ne saura pas le mettre à jour - il faudra passer par le gestionnaire de paquet de | |||||
la distribution | |||||
En revanche, `pip` contient de nombreuses fonctionnalités intéressantes: | |||||
* Il est capable de désinstaller des bibliothèques (à condition toutefois qu'elles ne soient pas dans un répertoire système) | |||||
* Il est aussi capable d'afficher la liste complète des bibliothèques Python accessibles par l'utilisateur courant avec `freeze`. | |||||
Voici un extrait de la commande `python3 -m pip freeze` au moment de la rédaction de cet article sur ma machine: | |||||
``` | |||||
$ python3 -m pip freeze | |||||
apipkg==1.5 | |||||
cli-ui==0.9.1 | |||||
gaupol==1.5 | |||||
tabulate==0.8.4 | |||||
``` | |||||
On y retrouve les bibliothèques `cli-ui` et `tabulate`, bien sûr, mais aussi la bibliothèque `gaupol`, qui correspond au [programme d'édition de sous-titres](https://otsaloma.io/gaupol/) que j'ai installé à l'aide du gestionnaire de paquets de ma distribution. Précisons que les modules de la bibliothèque standard et ceux utilisés directement par pip sont omis de la liste. | |||||
On constate également que chaque bibliothèque possède un *numéro de version*. | |||||
# Numéros de version | |||||
Les numéros de version remplissent plusieurs rôles, mais l'un des principaux est de spécifier des changements incompatibles. | |||||
Par exemple, pour `cli-ui`, la façon d'appeler la fonction `ask_choice` a changé entre les versions 0.7 et 0.8, comme le montre cet extrait du [changelog](https://tankerhq.github.io/python-cli-ui/changelog.html#v0-8-0): | |||||
> the list of choices used by ask_choice is now a named keyword argument: | |||||
```python | |||||
# Old (<= 0.7) | |||||
ask_choice("select a fruit", ["apple", "banana"]) | |||||
# New (>= 0.8) | |||||
ask_choice("select a fruit", choices=["apple", "banana"]) | |||||
``` | |||||
Ceci s'appelle un *changement d'API*. | |||||
# Réagir aux changements d'API | |||||
Plusieurs possibilités: | |||||
* On peut bien sûr adapter le code pour utiliser la nouvelle API, mais cela n'est pas toujours possible ni souhaitable. | |||||
* Une autre solution est de spécifier des *contraintes* sur le numéro de version dans la déclaration des dépendances. Par exemple : | |||||
```python | |||||
setup( | |||||
install_requires=[ | |||||
"cli-ui < 0.8", | |||||
... | |||||
] | |||||
) | |||||
``` | |||||
# Aparté : pourquoi éviter sudo pip | |||||
Souvenez-vous que les fichiers systèmes sont contrôlés par votre gestionnaire de paquets. | |||||
Les mainteneurs de votre distribution font en sorte qu'ils fonctionnent bien les uns | |||||
avec les autres. Par exemple, le paquet `python3-cli-ui` ne sera mis à jour que lorsque tous les paquets qui en dépendent seront prêts à utiliser la nouvelle API. | |||||
En revanche, si vous lancez `sudo pip` (où `pip` avec un compte root), vous allez écrire dans ces mêmes répertoire et vous risquez de "casser" certains programmes de votre système. | |||||
Mais il y a un autre problème encore pire. | |||||
# Conflit de dépendances | |||||
Supposons deux projets A et B dans votre répertoire personnel. Ils dépendent tous les deux de `cli-ui`, mais l'un des deux utilise `cli-ui 0.7` et l'autre `cli-ui 0.9`. Que faire ? | |||||
# Environnements virtuels | |||||
La solution est d'utiliser un environnement virtuel (*virtualenv* en abrégé). C'est un répertoire *isolé* du reste du système. | |||||
Il se crée par exemple avec la commande `python3 -m venv foo-venv`. où `foo-venv` est un répertoire quelconque. | |||||
## Aparté : python3 -m venv sur Debian | |||||
La commande `python3 -m venv` fonctionne en général partout, dès l'installation de Python3 (*out of the box*, en Anglais), *sauf* sur Debian et ses dérivées [^5]. | |||||
Si vous utilisez Debian, la commande pourrait ne pas fonctionner. En fonction des messages d'erreur que vous obtenez, il est possible de résoudre le problème en : | |||||
* installant le paquet `python3-venv`, | |||||
* ou en utilisant d'abord `pip` pour installer `virtualenv`, avec `python3 -m pip install virtualenv --user` puis en lançant `python3 -m virtualenv foo-venv`. | |||||
## Comportement de python dans le virtualenv | |||||
Ce répertoire contient de nombreux fichiers et dossiers, et notamment un binaire dans `foo-venv/bin/python3`. | |||||
Voyons comment il se comporte en le comparant au binaire `/usr/bin/python3` habituel : | |||||
``` | |||||
$ /usr/bin/python3 -c 'import sys; print(sys.path)' | |||||
['', | |||||
... | |||||
'/usr/lib/python3.7', | |||||
'/usr/lib/python3.7.zip', | |||||
'/usr/lib/python3.7/lib-dynload', | |||||
'/home/dmerej/.local/lib/python3.7/site-packages', | |||||
'/usr/lib/python3.7/site-packages' | |||||
] | |||||
$ /home/dmerej/foo-venv/bin/python -c 'import sys; print(sys.path)' | |||||
['', | |||||
'/usr/lib/python3.7', | |||||
'/usr/lib/python3.7.zip', | |||||
'/usr/lib/python3.7/lib-dynload', | |||||
'/home/dmerej/foo-venv/lib/python3.7/site-packages, | |||||
] | |||||
``` | |||||
À noter: | |||||
* Le répertoire "global" dans `~/.local/lib` a disparu | |||||
* Seuls quelques répertoires systèmes sont présents (ils correspondent plus ou moins à l'emplacement des modules de la bibliothèque standard) | |||||
* Un répertoire *au sein* du virtualenv a été rajouté | |||||
Ainsi, l'isolation du virtualenv est reflété dans la différence de la valeur de `sys.path`. | |||||
Il faut aussi préciser que le virtualenv n'est pas complètement isolé du reste du système. En particulier, il dépend encore du binaire Python utilisé pour le créer. | |||||
Par exemple, si vous utilisez `/usr/local/bin/python3.7 -m venv foo-37`, le virtualenv dans `foo-37` utilisera Python 3.7 et fonctionnera tant que le binaire `/usr/local/bin/python3.7` existe. | |||||
Cela signifie également qu'il est possible qu'en mettant à jour le paquet `python3` sur votre distribution, vous rendiez inutilisables les virtualenvs créés avec l'ancienne version du paquet. | |||||
## Comportement de pip dans le virtualenv | |||||
D'après ce qui précède, le virtualenv ne devrait contenir aucun module en dehors de la bibliothèque standard et de `pip` lui-même. | |||||
On peut s'en assurer en lançant `python3 -m pip freeze` depuis le virtualenv et en vérifiant que rien ne s'affiche. | |||||
``` | |||||
$ python3 -m pip freeze | |||||
# de nombreuses bibliothèques en dehors du virtualenv | |||||
apipkg==1.5 | |||||
cli-ui==0.9.1 | |||||
gaupol==1.5 | |||||
tabulate==0.8.4 | |||||
$ /home/dmerej/foo-venv/bin/python3 -m pip freeze | |||||
# rien :) | |||||
``` | |||||
On peut alors utiliser le module `pip` *du virtualenv* pour installer des bibliothèques dans celui-ci : | |||||
``` | |||||
$ /home/dmerej/foo-venv/bin/python3 -m pip install cli-ui | |||||
Collecting cli-ui | |||||
Using cached https://pythonhosted.org/..cli_ui-0.9.1-py3-none-any.whl | |||||
Collecting colorama (from cli-ui) | |||||
Using cached https://pythonhosted.org/..colorama-0.4.1-py2.py3-none-any.whl | |||||
Collecting unidecode (from cli-ui) | |||||
Using cached https://pythonhosted.org/..Unidecode-1.0.23-py2.py3-none-any.whl | |||||
Collecting tabulate (from cli-ui) | |||||
Installing collected packages: colorama, unidecode, tabulate, cli-ui | |||||
Successfully installed cli-ui-0.9.1 colorama-0.4.1 tabulate-0.8.3 | |||||
unidecode-1.0.23 | |||||
``` | |||||
Cette fois, aucune bibliothèque n'est marquée comme déjà installée, et on récupère donc `cli-ui` et toutes ses dépendances. | |||||
On a enfin notre solution pour résoudre notre conflit de dépendances : | |||||
on peut simplement créer un virtualenv par projet. Ceci nous permettra | |||||
d'avoir effectivement deux versions différentes de `cli-ui`, isolées les | |||||
unes des autres. | |||||
# Activer un virtualenv | |||||
Devoir préciser le chemin du virtualenv en entier pour chaque commande peut devenir fastidieux ; heureusement, il est possible *d'activer* un virtualenv, en lançant une des commandes suivantes : | |||||
* `source foo-venv/bin/activate` - si vous utilisez un shell POSIX | |||||
* `source foo-venv/bin/activate.fish` - si vous utilisez Fish | |||||
* `foo-venv\bin\activate.bat` - sous Windows | |||||
Une fois le virtualenv activé, taper `python`, `python3` ou `pip` utilisera les binaires correspondants dans le virtualenv automatiquement, | |||||
et ce, tant que la session du shell sera ouverte. | |||||
Le script d'activation ne fait en réalité pas grand-chose à part modifier la variable `PATH` et rajouter le nom du virtualenv au début de l'invite de commandes : | |||||
``` | |||||
# Avant | |||||
user@host:~/src $ source foo-env/bin/activate | |||||
# Après | |||||
(foo-env) user@host:~/src $ | |||||
``` | |||||
Pour sortir du virtualenv, entrez la commande `deactivate`. | |||||
# Conclusion | |||||
Le système de gestions des dépendances de Python peut paraître compliqué et bizarre, surtout venant d'autres langages. | |||||
Mon conseil est de toujours suivre ces deux règles : | |||||
* Un virtualenv par projet et par version de Python | |||||
* Toujours utiliser `pip` *depuis* un virtualenv | |||||
Certes, cela peut paraître fastidieux, mais c'est une méthode qui vous évitera probablement de vous arracher les cheveux (croyez-en mon expérience). | |||||
Dans un futur article, nous approfondirons la question, en évoquant d'autres sujets comme `PYTHONPATH`, le fichier `requirements.txt` ou des outils comme `poetry` ou `pipenv`. À suivre. | |||||
[^1]: C'est une condition suffisante, mais pas nécessaire - on y reviendra. | |||||
[^2]: Presque. Il peut arriver que l'utilisateur courant ait les droits d'écriture dans *tous* les segments de `sys.path`, en fonction de l'installation de Python. Cela dit, c'est plutôt l'exception que la règle. | |||||
[^3]: Cela peut vous paraître étrange à première vue. Il y a de nombreuses raisons historiques à ce comportement, et il n'est pas sûr qu'il puisse être changé un jour. | |||||
[^4]: Presque. Parfois il faut installer un paquet supplémentaire, notamment sur les distributions basées sur Debian | |||||
[^5]: Je n'ai pas réussi à trouver une explication satisfaisante à ce choix des mainteneurs Debian. Si vous avez des informations à ce sujet, je suis preneur. _Mise à jour: Il se trouve que cette décision s'inscrit au sein de la "debian policy", c'est à dire une liste de règles que doivent respecter tous les programmes maintenus par Debian._ |
@@ -0,0 +1,106 @@ | |||||
+++ | |||||
title = "Bibliothèques tierces" | |||||
weight = 3 | |||||
+++ | |||||
# Bibliothèques tierces | |||||
Prenons un exemple : | |||||
```python | |||||
# dans foo.py | |||||
import tabulate | |||||
scores = [ | |||||
["John", 345], | |||||
["Mary-Jane", 2], | |||||
["Bob", 543], | |||||
] | |||||
table = tabulate.tabulate(scores) | |||||
print(table) | |||||
``` | |||||
``` | |||||
$ python3 foo.py | |||||
--------- --- | |||||
John 345 | |||||
Mary-Jane 2 | |||||
Bob 543 | |||||
--------- --- | |||||
``` | |||||
Ici, le module `tabulate` n'est ni dans la bibliothèque standard, ni écrit par l'auteur du script `foo.py`. On dit que c'est une bibliothèque tierce. | |||||
On peut trouver [le code source de tabulate](https://bitbucket.org/astanin/python-tabulate/src/master/) facilement. La question qui se pose alors est: comment faire en sorte que `sys.path` contienne le module `tabulate`? | |||||
Eh bien, plusieurs solutions s'offrent à vous. | |||||
## Le gestionnaire de paquets | |||||
Si vous utilisez une distribution Linux, peut-être pourrez-vous utiliser votre gestionnaire de paquets : | |||||
```bash | |||||
$ sudo apt install python3-tabulate | |||||
``` | |||||
Comme vous lancez votre gestionnaire de paquets avec `sudo`, celui-ci sera capable d'écrire dans les chemins système de `sys.path`. | |||||
## À la main | |||||
Une autre méthode consiste à partir des sources - par exemple, si le paquet de votre distribution n'est pas assez récent, ou si vous avez besoin de modifier le code de la bibliothèque en question. | |||||
Voici une marche à suivre possible : | |||||
1. Récupérer les sources de la version qui vous intéresse dans la [section téléchargement de bitbucket](https://bitbucket.org/astanin/python-tabulate/downloads/?tab=tags). | |||||
1. Extraire l'archive, par exemple dans `src/tabulate` | |||||
1. Se rendre dans `src/tabulate` et lancer `python3 setup.py install --user` | |||||
## Anatomie du fichier setup.py | |||||
La plupart des bibliothèques Python contiennent un `setup.py` à | |||||
la racine de leurs sources. Il sert à plein de choses, la commande `install` | |||||
n'étant qu'une parmi d'autres. | |||||
Le fichier `setup.py` contient en général simplement un `import` de `setuptools`, et un appel à la fonction `setup()`, avec de nombreux arguments : | |||||
```python | |||||
# tabulate/setup.py | |||||
from setuptools import setup | |||||
setup( | |||||
name='tabulate', | |||||
version='0.8.1', | |||||
description='Pretty-print tabular data', | |||||
py_modules=["tabulate"], | |||||
scripts=["bin/tabulate"], | |||||
... | |||||
) | |||||
``` | |||||
## Résultat de l'invocation de setup.py | |||||
Par défaut, `setup.py` essaiera d'écrire dans un des chemins système de | |||||
`sys.path` [^3], d'où l'utilisation de l'option `--user`. | |||||
Voici à quoi ressemble la sortie de la commande : | |||||
```bash | |||||
$ cd src/tabulate | |||||
$ python3 setup.py install --user | |||||
running install | |||||
... | |||||
Copying tabulate-0.8.4-py3.7.egg to /home/dmerej/.local/lib/python3.7/site-packages | |||||
... | |||||
Installing tabulate script to /home/dmerej/.local/bin | |||||
``` | |||||
Notez que module a été copié dans `~/.local/lib/python3.7/site-packages/` et le script dans `~/.local/bin`. Cela signifie que *tous* les scripts Python lancés par l'utilisateur courant auront accès au module `tabulate`. | |||||
Notez également qu'un script a été installé dans `~/.local/bin` - Une bibliothèque Python peut contenir aussi bien des modules que des scripts. | |||||
Un point important est que vous n'avez en général pas besoin de lancer le script directement. Vous pouvez utiliser `python3 -m tabulate`. Procéder de cette façon est intéressant puisque vous n'avez pas à vous soucier de rajouter le chemin d'installation des scripts dans la variable d'environnement PATH. | |||||
@@ -0,0 +1,147 @@ | |||||
+++ | |||||
title = "Dépendances" | |||||
weight = 4 | |||||
+++ | |||||
# Dépendances | |||||
## Un autre exemple | |||||
Prenons une autre bibliothèque : `cli-ui`. | |||||
Elle permet d'afficher du texte en couleur dans un terminal | |||||
```python | |||||
import cli_ui | |||||
cli_ui.info("Ceci est en", cli_ui.red, "rouge") | |||||
``` | |||||
Elle permet également d'afficher des tableaux en couleur : | |||||
```python | |||||
headers=["prénom", "score"] | |||||
data = [ | |||||
[(bold, "John"), (green, 10.0)], | |||||
[(bold, "Jane"), (green, 5.0)], | |||||
] | |||||
cli_ui.info_table(data, headers=headers) | |||||
``` | |||||
Pour ce faire, elle repose sur la bibliothèque `tabulate` vue précédemment. On dit que `cli-ui` *dépend* de `tabulate`. | |||||
## Déclaration des dépendances | |||||
La déclaration de la dépendance de `cli-ui` vers `tabulate` s'effectue également dans le fichier `setup.py`: | |||||
```python | |||||
setup( | |||||
name="cli-ui", | |||||
version="0.9.1", | |||||
install_requires=[ | |||||
"tabulate", | |||||
... | |||||
], | |||||
... | |||||
) | |||||
``` | |||||
## pypi.org | |||||
On comprend dès lors qu'il doit nécessairement exister un *annuaire* permettant de relier les noms de dépendances à leur code source. | |||||
Cet annuaire, c'est le site [pypi.org](https://pypi.org/). Vous y trouverez les pages correspondant à [tabulate](https://pypi.org/project/tabulate/) et [cli-ui](https://pypi.org/project/python-cli-ui/). | |||||
# pip | |||||
`pip` est un outil qui vient par défaut avec Python3[^4]. Vous pouvez également l'installer grâce au script [get-pip.py](https://bootstrap.pypa.io/get-pip.py), en lançant `python3 get-pip.py --user`. | |||||
Il est conseillé de *toujours* lancer `pip` avec `python3 -m pip`. De cette façon, vous êtes certains d'utiliser le module `pip` correspondant à votre binaire `python3`, et vous ne dépendez pas de ce qu'il y a dans votre `PATH`. | |||||
`pip` est capable d'interroger le site `pypi.org` pour retrouver les dépendances, et également de lancer les différents scripts `setup.py`. | |||||
Comme de nombreux outils, il s'utilise à l'aide de *commandes*. Voici comment installer `cli-ui` à l'aide de la commande 'install' de `pip`: | |||||
```bash | |||||
$ python3 -m pip install cli-ui --user | |||||
Collecting cli-ui | |||||
... | |||||
Requirement already satisfied: unidecode in /usr/lib/python3.7/site-packages (from cli-ui) (1.0.23) | |||||
Requirement already satisfied: colorama in /usr/lib/python3.7/site-packages (from cli-ui) (0.4.1) | |||||
Requirement already satisfied: tabulate in /mnt/data/dmerej/src/python-tabulate (from cli-ui) (0.8.4) | |||||
Installing collected packages: cli-ui | |||||
Successfully installed cli-ui-0.9.1 | |||||
``` | |||||
On constate ici quelques limitations de `pip`: | |||||
* Il faut penser à utiliser `--user` (de la même façon que lorsqu'on lance `setup.py` à la main) | |||||
* Si le paquet est déjà installé dans le système, pip ne saura pas le mettre à jour - il faudra passer par le gestionnaire de paquet de | |||||
la distribution | |||||
En revanche, `pip` contient de nombreuses fonctionnalités intéressantes: | |||||
* Il est capable de désinstaller des bibliothèques (à condition toutefois qu'elles ne soient pas dans un répertoire système) | |||||
* Il est aussi capable d'afficher la liste complète des bibliothèques Python accessibles par l'utilisateur courant avec `freeze`. | |||||
Voici un extrait de la commande `python3 -m pip freeze` au moment de la rédaction de cet article sur ma machine: | |||||
``` | |||||
$ python3 -m pip freeze | |||||
apipkg==1.5 | |||||
cli-ui==0.9.1 | |||||
gaupol==1.5 | |||||
tabulate==0.8.4 | |||||
``` | |||||
On y retrouve les bibliothèques `cli-ui` et `tabulate`, bien sûr, mais aussi la bibliothèque `gaupol`, qui correspond au [programme d'édition de sous-titres](https://otsaloma.io/gaupol/) que j'ai installé à l'aide du gestionnaire de paquets de ma distribution. Précisons que les modules de la bibliothèque standard et ceux utilisés directement par pip sont omis de la liste. | |||||
On constate également que chaque bibliothèque possède un *numéro de version*. | |||||
## Numéros de version | |||||
Les numéros de version remplissent plusieurs rôles, mais l'un des principaux est de spécifier des changements incompatibles. | |||||
Par exemple, pour `cli-ui`, la façon d'appeler la fonction `ask_choice` a changé entre les versions 0.7 et 0.8, comme le montre cet extrait du [changelog](https://tankerhq.github.io/python-cli-ui/changelog.html#v0-8-0): | |||||
> the list of choices used by ask_choice is now a named keyword argument: | |||||
```python | |||||
# Old (<= 0.7) | |||||
ask_choice("select a fruit", ["apple", "banana"]) | |||||
# New (>= 0.8) | |||||
ask_choice("select a fruit", choices=["apple", "banana"]) | |||||
``` | |||||
Ceci s'appelle un *changement d'API*. | |||||
## Réagir aux changements d'API | |||||
Plusieurs possibilités: | |||||
* On peut bien sûr adapter le code pour utiliser la nouvelle API, mais cela n'est pas toujours possible ni souhaitable. | |||||
* Une autre solution est de spécifier des *contraintes* sur le numéro de version dans la déclaration des dépendances. Par exemple : | |||||
```python | |||||
setup( | |||||
install_requires=[ | |||||
"cli-ui < 0.8", | |||||
... | |||||
] | |||||
) | |||||
``` | |||||
# Aparté : pourquoi éviter sudo pip | |||||
Souvenez-vous que les fichiers systèmes sont contrôlés par votre gestionnaire de paquets. | |||||
Les mainteneurs de votre distribution font en sorte qu'ils fonctionnent bien les uns | |||||
avec les autres. Par exemple, le paquet `python3-cli-ui` ne sera mis à jour que lorsque tous les paquets qui en dépendent seront prêts à utiliser la nouvelle API. | |||||
En revanche, si vous lancez `sudo pip` (où `pip` avec un compte root), vous allez écrire dans ces mêmes répertoire et vous risquez de "casser" certains programmes de votre système. | |||||
Mais il y a un autre problème encore pire. | |||||
# Conflit de dépendances | |||||
Supposons deux projets A et B dans votre répertoire personnel. Ils dépendent tous les deux de `cli-ui`, mais l'un des deux utilise `cli-ui 0.7` et l'autre `cli-ui 0.9`. Que faire ? |
@@ -0,0 +1,137 @@ | |||||
+++ | |||||
title = "Environnements virtuels" | |||||
weight = 5 | |||||
+++ | |||||
# Environnements virtuels | |||||
La solution à la question de la fin du chapitre précédent est d'utiliser un *environnement virtuel* | |||||
(*virtualenv* en abrégé). | |||||
C'est un répertoire *isolé* du reste du système. | |||||
Il se crée par exemple avec la commande `python3 -m venv foo-venv`. où `foo-venv` est un répertoire quelconque. | |||||
## Aparté : python3 -m venv sur Debian | |||||
La commande `python3 -m venv` fonctionne en général partout, dès l'installation de Python3 (*out of the box*, en Anglais), *sauf* sur Debian et ses dérivées [^5]. | |||||
Si vous utilisez Debian, la commande pourrait ne pas fonctionner. En fonction des messages d'erreur que vous obtenez, il est possible de résoudre le problème en : | |||||
* installant le paquet `python3-venv`, | |||||
* ou en utilisant d'abord `pip` pour installer `virtualenv`, avec `python3 -m pip install virtualenv --user` puis en lançant `python3 -m virtualenv foo-venv`. | |||||
## Comportement de python dans le virtualenv | |||||
Ce répertoire contient de nombreux fichiers et dossiers, et notamment un binaire dans `foo-venv/bin/python3`. | |||||
Voyons comment il se comporte en le comparant au binaire `/usr/bin/python3` habituel : | |||||
``` | |||||
$ /usr/bin/python3 -c 'import sys; print(sys.path)' | |||||
['', | |||||
... | |||||
'/usr/lib/python3.7', | |||||
'/usr/lib/python3.7.zip', | |||||
'/usr/lib/python3.7/lib-dynload', | |||||
'/home/dmerej/.local/lib/python3.7/site-packages', | |||||
'/usr/lib/python3.7/site-packages' | |||||
] | |||||
$ /home/dmerej/foo-venv/bin/python -c 'import sys; print(sys.path)' | |||||
['', | |||||
'/usr/lib/python3.7', | |||||
'/usr/lib/python3.7.zip', | |||||
'/usr/lib/python3.7/lib-dynload', | |||||
'/home/dmerej/foo-venv/lib/python3.7/site-packages, | |||||
] | |||||
``` | |||||
À noter: | |||||
* Le répertoire "global" dans `~/.local/lib` a disparu | |||||
* Seuls quelques répertoires systèmes sont présents (ils correspondent plus ou moins à l'emplacement des modules de la bibliothèque standard) | |||||
* Un répertoire *au sein* du virtualenv a été rajouté | |||||
Ainsi, l'isolation du virtualenv est reflété dans la différence de la valeur de `sys.path`. | |||||
Il faut aussi préciser que le virtualenv n'est pas complètement isolé du reste du système. En particulier, il dépend encore du binaire Python utilisé pour le créer. | |||||
Par exemple, si vous utilisez `/usr/local/bin/python3.7 -m venv foo-37`, le virtualenv dans `foo-37` utilisera Python 3.7 et fonctionnera tant que le binaire `/usr/local/bin/python3.7` existe. | |||||
Cela signifie également qu'il est possible qu'en mettant à jour le paquet `python3` sur votre distribution, vous rendiez inutilisables les virtualenvs créés avec l'ancienne version du paquet. | |||||
## Comportement de pip dans le virtualenv | |||||
D'après ce qui précède, le virtualenv ne devrait contenir aucun module en dehors de la bibliothèque standard et de `pip` lui-même. | |||||
On peut s'en assurer en lançant `python3 -m pip freeze` depuis le virtualenv et en vérifiant que rien ne s'affiche. | |||||
``` | |||||
$ python3 -m pip freeze | |||||
# de nombreuses bibliothèques en dehors du virtualenv | |||||
apipkg==1.5 | |||||
cli-ui==0.9.1 | |||||
gaupol==1.5 | |||||
tabulate==0.8.4 | |||||
$ /home/dmerej/foo-venv/bin/python3 -m pip freeze | |||||
# rien :) | |||||
``` | |||||
On peut alors utiliser le module `pip` *du virtualenv* pour installer des bibliothèques dans celui-ci : | |||||
``` | |||||
$ /home/dmerej/foo-venv/bin/python3 -m pip install cli-ui | |||||
Collecting cli-ui | |||||
Using cached https://pythonhosted.org/..cli_ui-0.9.1-py3-none-any.whl | |||||
Collecting colorama (from cli-ui) | |||||
Using cached https://pythonhosted.org/..colorama-0.4.1-py2.py3-none-any.whl | |||||
Collecting unidecode (from cli-ui) | |||||
Using cached https://pythonhosted.org/..Unidecode-1.0.23-py2.py3-none-any.whl | |||||
Collecting tabulate (from cli-ui) | |||||
Installing collected packages: colorama, unidecode, tabulate, cli-ui | |||||
Successfully installed cli-ui-0.9.1 colorama-0.4.1 tabulate-0.8.3 | |||||
unidecode-1.0.23 | |||||
``` | |||||
Cette fois, aucune bibliothèque n'est marquée comme déjà installée, et on récupère donc `cli-ui` et toutes ses dépendances. | |||||
On a enfin notre solution pour résoudre notre conflit de dépendances : | |||||
on peut simplement créer un virtualenv par projet. Ceci nous permettra | |||||
d'avoir effectivement deux versions différentes de `cli-ui`, isolées les | |||||
unes des autres. | |||||
## Activer un virtualenv | |||||
Devoir préciser le chemin du virtualenv en entier pour chaque commande peut devenir fastidieux ; heureusement, il est possible *d'activer* un virtualenv, en lançant une des commandes suivantes : | |||||
* `source foo-venv/bin/activate` - si vous utilisez un shell POSIX | |||||
* `source foo-venv/bin/activate.fish` - si vous utilisez Fish | |||||
* `foo-venv\bin\activate.bat` - sous Windows | |||||
Une fois le virtualenv activé, taper `python`, `python3` ou `pip` utilisera les binaires correspondants dans le virtualenv automatiquement, | |||||
et ce, tant que la session du shell sera ouverte. | |||||
Le script d'activation ne fait en réalité pas grand-chose à part modifier la variable `PATH` et rajouter le nom du virtualenv au début de l'invite de commandes : | |||||
``` | |||||
# Avant | |||||
user@host:~/src $ source foo-env/bin/activate | |||||
# Après | |||||
(foo-env) user@host:~/src $ | |||||
``` | |||||
Pour sortir du virtualenv, entrez la commande `deactivate`. | |||||
## Conclusion | |||||
Le système de gestions des dépendances de Python peut paraître compliqué et bizarre, surtout venant d'autres langages. | |||||
Mon conseil est de toujours suivre ces deux règles : | |||||
* Un virtualenv par projet et par version de Python | |||||
* Toujours utiliser `pip` *depuis* un virtualenv | |||||
Certes, cela peut paraître fastidieux, mais c'est une méthode qui vous évitera probablement de vous arracher les cheveux (croyez-en mon expérience). |
@@ -0,0 +1,4 @@ | |||||
+++ | |||||
title = "Chapitre 14 - Bibliothèques" | |||||
weight = 14 | |||||
+++ |
@@ -0,0 +1,37 @@ | |||||
import argparse | |||||
import subprocess | |||||
import sys | |||||
def build(): | |||||
process = subprocess.run(["hugo"]) | |||||
if process.returncode != 0: | |||||
sys.exit("build failed") | |||||
def deploy(*, dry_run): | |||||
cmd = [ | |||||
"rsync", | |||||
"--itemize-changes", | |||||
"--recursive", | |||||
"--delete", | |||||
"public/", | |||||
"dedi3:/srv/nginx/html/books/python/", | |||||
] | |||||
if dry_run: | |||||
cmd += ["--dry-run"] | |||||
process = subprocess.run(cmd) | |||||
if process.returncode != 0: | |||||
sys.exit("deployment failed") | |||||
def main(): | |||||
parser = argparse.ArgumentParser() | |||||
parser.add_argument("-n", "--dry-run", action="store_true") | |||||
args = parser.parse_args() | |||||
build() | |||||
deploy(dry_run=args.dry_run) | |||||
if __name__ == "__main__": | |||||
main() |