Update first chapters and new questions

Updated first chapter slides.
Added new questions in the new training section.
This commit is contained in:
2025-07-07 21:18:04 +02:00
parent bea28eca14
commit 77aa231f5b
11 changed files with 416 additions and 250 deletions

View File

@ -1,30 +1,39 @@
---
title: Découvrir le langage - types de données avancés
title: Découvrir le langage - collections
author: Steve Kossouho
---
# Collections de données
Ce chapitre va présenter 4 types de données de base, qui permettent de manipuler plusieurs informations avec une seule
variable :
- Listes
- Tuples
- Ensembles
- Dictionnaires
----
## Les listes
## Listes
Type de collection de données : `list`{.python}. Une liste peut contenir une **séquence** d'éléments
de n'importe quel type pris en charge par Python, y compris d'autres listes. Le contenu d'une liste est modifiable (ajouter, retirer des éléments…)
Type de collection de données le plus utile : `list`{.python}. Un objet de type liste peut contenir une **séquence** d'éléments
de n'importe quel type en Python, y compris d'autres listes. Le contenu d'une liste est modifiable (ajouter, retirer des éléments…)
```python {.numberLines}
liste1 = [1, 2, 3, 4, 5, 6] # types cohérents
liste2 = [1, 2, 3, "a", "b", "c"] # types divers
liste3 = [None, None, True, False]
liste1 = [1, 2, 3, 4, 5, 6] # éléments de types cohérents
liste2 = [1, 2, 3, "a", "b", "c"] # pas de restriction sur les types
liste3 = [None, None, True, False] # None n'est pas une valeur spéciale
liste4 = [[1, 2, 3], [4, 5, 6]]
liste5 = [] # liste vide, équivalent à list()
liste5 = [] # liste vide. Équivalent à list()
liste6 = [liste1] # Liste contenant 1 élément, qui est lui-même une liste
```
----
Il est possible de manipuler une liste facilement. On peut ajouter des éléments à la fin, à une
position précise, retirer un élément ou encore récupérer un élément seul.
Il est possible d'exploiter une liste facilement; on peut ajouter de nouveaux éléments à la fin, les insérer à une
position précise, retirer un élément ou encore récupérer un élément seul. Notez que les positions ([index]{.naming})
des éléments démarrent à zéro (`0`) et qu'une liste n'a jamais d'emplacement vide.
```python {.numberLines}
liste1 = [1, 2, 3, 4, 5, 6]
@ -34,8 +43,10 @@ liste2 = list() # Créer une nouvelle liste vide
length = len(liste1) # Récupérer le nombre d'éléments
position = liste1.index(3) # Renvoie l'index de la valeur 3, ou erreur si introuvable
nine_trouvable = 9 in liste1 # Renvoie si l'élément 9 existe dans la liste
# Récupérer des éléments
print(liste1[0]) # Affiche le premier élément si la liste est non vide
# Manipuler le contenu
liste1.append(7) # Ajoute 7 comme nouvel élément à la fin
liste1.insert(0, 99) # Insère 99 comme nouvel élément au tout début
@ -64,31 +75,33 @@ if len(liste1) >= 4:
### Erreurs de méthodes
La méthode `.remove(valeur)`{.python} provoque également une erreur (`ValueError`{.python}) si
l'élément en argument n'existe pas dans la liste. De la même façon, il peut être utile de tester
Les méthodes `list.remove()`{.python} et `list.index()`{.python} provoquent également une erreur (`ValueError`{.python}) si
l'élément en argument est introuvable dans la liste. De la même façon, il peut être utile de tester
qu'un élément est présent dans la liste avant d'essayer de l'en supprimer :
```python {.numberLines}
liste1 = [1, 2, 3]
if 8 in liste1:
liste1 = ["fred", "denis", 14, 23, 32]
if 8 in liste1: # Tester que 8 a été trouvé dans la liste
liste1.remove(8)
```
**Question** : Qu'écririez-vous pour supprimer toutes les occurrences d'une valeur en utilisant `list.remove()`{.python} ?
----
### Indices de liste négatifs
Il est également possible d'accéder à des éléments de liste en utilisant des indices négatifs.
Il est également possible d'accéder à des éléments de liste **non vide** en utilisant des indices négatifs.
Si la liste n'est pas vide, l'élément d'indice `-1` est le dernier élément de la liste (équivalent
à `len(liste) - 1`{.python}), et l'élément à l'indice `-len(liste)`{.python} est le premier (
équivalent à `0`).
Tout nombre inférieur génère une erreur de type `IndexError`{.python}.
Dans ce cas, l'élément à l'index `-1` d'une liste `l` est celui à l'index `len(l) - 1`{.python}, à savoir
le dernier élément. L'élément à l'index `-len(l)`{.python} sera celui à l'index `0`, à savoir le premier
élément. Tout index hors de la plage disponible provoquera une erreur de type `IndexError`{.python}.
```python {.numberLines}
liste1 = [1, 2, 4, 6, 9, 11]
print(liste1[-1])
animals = ["dog", "cat", "rabbit", "hamster", "parrot"]
print(animals[-1]) # affiche "parrot"
print(animals[-2]) # affiche "hamster"
print(animals[-5]) # affiche "dog"
```
----
@ -96,7 +109,7 @@ print(liste1[-1])
## Parcourir une liste
Comme on l'a vu avec la boucle `for`{.python}, utilisée avec `range()`{.python}, on peut utiliser la
boucle `for`{.python} sur une liste :
boucle `for`{.python} sur une liste :
```python {.numberLines}
prime_numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]
@ -110,37 +123,28 @@ les valeurs `2`{.python}, puis `3`{.python}, puis `5`{.python}, etc.
----
## Opérations sur listes
### Opérations sur listes
Récupérer des portions ou partitions de listes (slicing) :
Récupérer des portions ou partitions de listes est possible en Python avec la syntaxe suivante (slicing).
Le comportement de cette syntaxe est basé sur le fonctionnement de la fonction `range`{.python}.
```python {.numberLines}
a = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55]
b = a[0:5] # Index 0 à 5 non inclus. Marche aussi sur les chaînes.
c = a[5:0] # L'index de fin est inférieur au départ, renvoie une liste vide
d = a[:] # Renvoie une copie de toute la liste
e = a[::-1] # Tout parcourir à l'envers
a = [10, 15, 20, 25, 30, 35, 40, 45, 50, 55] # liste de base
b = a[0:5] # copier dans une nouvelle liste les éléments aux index 0 à 4.
c = a[5:0] # créer une nouvelle liste, mais ne rien y copier
d = a[:] # copier de l'extrêmité gauche à droite incluses
e = a[::-1] # copier tous les éléments à l'envers
f = a[::2] # Tout parcourir 2 par 2
g = a[5:2:-1] # L'indice de début est supérieur à l'indice de fin
h = a[2:5:-1] # Le pas est négatif, start < end, renvoie une liste vide
```
Pourquoi créer des portions de listes ? Cela peut-être utile, par exemple, si
Pourquoi créer des portions de listes ? Cela peut-être utile, par exemple, si
vous souhaitez appliquer un calcul sur une petite partition d'un jeu de données.
----
## Autres collections
Outre les listes, il existe 3 types de base pour collectionner des éléments :
- Tuple : `tuple()`{.python}
- Set (jeu d'éléments uniques) : `set()`{.python}
- Dictionnaires (association) : `dict()`{.python}
----
### Tuples
## Tuples
**Tuple** : Un `tuple`{.python} fonctionne quasiment trait pour trait comme une liste, à la différence qu'une
fois que vous avez défini ses éléments, vous ne pouvez plus en changer. On parle de « collection immuable ».
@ -157,7 +161,7 @@ ce type : [opérations des tuples](https://docs.python.org/3/library/stdtypes.h
----
### Ensembles (données uniques)
## Ensembles (données uniques)
**Set** (ensemble) : Un `set`{.python} peut contenir plusieurs éléments et est modifiable au même titre
qu'une liste, mais n'est pas une séquence; aucune notion d'ordre ou d'index.
@ -168,11 +172,14 @@ dans votre objet.
a = {1, 1, 2, 3, 3, 3} # équivaut à {1, 2, 3}, les valeurs étant uniques à la fin
b = {2, 4, 5, "hello", (1, 2, 3)}
c = set() # obligatoire pour déclarer un set vide, sinon considéré dict
print(len(b))
a.add(9.5) # ajoute la valeur 9.5 (float) au set
a.discard(9.5) # retire la valeur 9.5, ou ne fait rien
d = a.intersection(b) # renvoie un set avec les éléments communs à a et b
e = a.union(b) # avec les éléments présents dans a ou b
d = a.intersection(b) # récupérer un ensemble avec les éléments communs à a et b
e = a.union(b) # récupérer un ensemble avec les éléments présents dans a ou b
...
```
@ -184,18 +191,18 @@ e = a.union(b) # avec les éléments présents dans a ou b
----
#### Bonus: Particularités des ensembles
### Bonus: Particularités des ensembles
Les ensembles sont des structures de données très efficaces. Techniquement, vous pouvez attendre une
complexité en `O(1)` pour que Python sache si un élément est déjà présent ou pas; cela signifie qu'il
faut plus ou moins le même nombre d'opérations pour trouver un élément, que votre `set`{.python} en possède un
faut plus ou moins le même nombre d'opérations pour trouver un élément, que votre `set`{.python} en contienne un
seul ou un milliard.
L'algorithme et la structure de données interne à Python derrière cette efficacité se nomme **Table de hachage**.
L'algorithme et la structure de données interne à Python derrière cette efficacité se nomme [Table de hachage]{.naming}.
----
##### Tables de hachage
#### Tables de hachage
Une table de hachage fonctionne en trois temps:
@ -206,13 +213,13 @@ Une table de hachage fonctionne en trois temps:
----
##### Exemple de hachage
#### Exemple de hachage
![Table de hachage](assets/images/basics-sets-hashing.png)
![Table de hachage](assets/images/basics-sets-hash-table.png)
----
##### Hachage
#### Hachage
La signature est un nombre calculé qui est sensé dépendre du contenu d'un objet. Python propose une fonction
à cet effet, `hash()`{.python}, pour laquelle chaque classe propose sa propre implémentation.
@ -223,7 +230,7 @@ il n'y a pas de collision.
----
##### Hachage en général dans Python
#### Hachage en général dans Python
Entre deux lancements d'un interpréteur Python, le hash de la majorité des objets est différent;
l'algorithme utilise une valeur aléatoire pour générer les hashs, de façon qu'il soit impossible de créer un
@ -238,7 +245,7 @@ Le hachage est en général imprévisible pour les valeurs suivantes:
----
##### Hashes prévisibles
#### Hashes prévisibles
Le hash est par contre prévisible pour les valeurs suivantes:
@ -255,7 +262,7 @@ Les objets `int`{.python} ont généralement un `hash()`{.python} identique, sau
----
##### Hashes impossibles
#### Hashes impossibles
L'algorithme de la table de hachage nécessite qu'un objet stocké dans un bucket possède en
permanence un `hash` correspondant à son bucket. Cela pose un problème avec les objets modifiables
@ -263,50 +270,57 @@ permanence un `hash` correspondant à son bucket. Cela pose un problème avec le
La solution adoptée par Python consiste à interdire le calcul de signature desdits objets.
Les `tuple`{.python} demeurent des données valides, puisque nous avons la garantie de ne jamais pouvoir
changer leur contenu, et ainsi leur signature.
changer leur contenu, et par là-même leur signature.
```python {.numberLines}
# La fonction hash est disponible par défaut en Python
print(hash([1, 2, 3])) # Provoque une exception
print(hash({1, 2, 3})) # Même problème
```
----
### Dictionnaires : associations entre clés et valeurs
## Dictionnaires : associations entre clés et valeurs
**Dictionnaires** : (`dict`{.python}) C'est un **ensemble** d'associations où l'on définit clés et valeurs. C'est le même
fonctionnement qu'un dictionnaire lexicographique, où, lorsque vous avez le
mot (la clé), vous retrouvez la définition s'il y en a une (la valeur). D'autres langages ont des structures
similaires et appellent ça des `HashMap`{.java} ou des `object`{.javascript}.
**Dictionnaires** : (`dict`{.python}) C'est un **ensemble** d'associations où l'on définit [clés]{.naming} et [valeurs]{.naming}.
C'est le même fonctionnement qu'un dictionnaire lexicographique, où, lorsque vous avez le
mot (la clé), vous retrouvez la définition s'il y en a une (la valeur).
D'autres langages ont des structures similaires et appellent ça des `HashMap`{.java} ou des `object`{.javascript}.
```python {.numberLines}
a = {"server1": "192.168.1.2", "server2": "192.168.1.3", "server3": "192.168.1.5"} # serveurs et adresses IP
b = {8: "Mme Garnier", 10: "M. Dubois", 11: "Mlle Yousfi", 12: "Mme Préjean"} # rendez-vous horaires
d = {1.2: "flottant", True: "booléen", None: "rien", (1, 2, 3): "tuple"} # clés de plusieurs types
c = {} # ou dict(), ceci est un dictionnaire vide
print(a["server1"]) # affiche "192.168.1.2"
print(b[10]) # affiche "M. Dubois"
print(b[9]) # provoque une erreur
```
Les dictionnaires ont les mêmes contraintes que les ensembles uniquement au niveau de leurs clés; vous
pouvez uniquement utiliser des valeurs de types immuables, tels que `int`{.python}, `float`{.python}, `tuple`{.python}...
----
#### Parcourir un dictionnaire
Il est possible de parcourir un dictionnaire avec une boucle `for`{.python}. De base, ce sont les
clés du dictionnaire qui sont parcourues. Mais il existe des variantes assez pratiques pour
parcourir un dictionnaire :
Il est possible de parcourir un dictionnaire avec une boucle `for`{.python}. Dans ce cas, ce sont
uniquement les clés du dictionnaire qui sont parcourues. Mais il existe des variantes assez pratiques pour
parcourir un dictionnaire :
```python {.numberLines}
a = {
"Jérémy": (25, "M", "Lille"),
"Hélène": (30, "F", "Ambérieu-en-Bugey"),
"Gwladys": (35, "F", "Nyons"),
profils = {
"Jérémy": (25, "M", "Lille"),
"Hélène": (30, "F", "Ambérieu-en-Bugey"),
"Gwladys": (35, "F", "Nyons"),
"Thierry": (40, "M", "Montceau-les-Mines"),
"Carole": (45, "F", "Saint-Rémy-lès-Chevreuse"),
}
for prenom in a:
for prenom in profils:
print(prenom) # affiche uniquement une clé
print(a[prenom]) # retrouve la valeur associée à la clé
print(profils[prenom]) # retrouve la valeur associée à la clé
```
----
@ -316,11 +330,12 @@ de `n` éléments, vous pouvez les dépaqueter (**unpacking**) :
```python {.numberLines}
# Avancé, unpacking via la méthode `items`
profils = {...}
for item in a.items(): # a.items() renvoie ((clé1, valeur1), (clé2, valeur2), …)
for item in profils.items(): # a.items() renvoie ((clé1, valeur1), (clé2, valeur2), …)
print(item) # est un tuple
for key, value in a.items():
for key, value in profils.items():
# on peut utiliser 2 variables de boucle si chaque élément parcouru est
# une séquence de taille 2. Merci l'unpacking !
print(key, value)
@ -334,18 +349,22 @@ Pour changer la valeur associée à une clé existante, ou par la même occasion
une nouvelle clé, il suffit d'écrire :
```python {.numberLines}
a = {"Bordeaux": 250000}
a["Marseille"] = 800000
a["Bordeaux"] = 90
print(a["Bordeaux"]) # Affiche 90
populations = {"Bordeaux": 250000, "Cherbourg": 80000}
print(populations)
# Modifier le contenu
populations["Marseille"] = 800000
populations["Bordeaux"] = 90
print(populations["Bordeaux"]) # Affiche 90
# On peut supprimer du dictionnaire une association en écrivant
del a["Marseille"] # plus de clé pour Marseille !
print("Marseille" in a) # renvoie si la clé "Marseille" existe
del populations["Marseille"] # plus de clé pour Marseille !
print("Marseille" in populations) # renvoie si la clé "Marseille" existe
```
----
### Plus: méthodes de dictionnaires
### Extra: méthodes de dictionnaires
La méthode `get(key, default)`{.python} des dictionnaires renvoie la valeur associée à une clé. Si
l'on ne passe pas de valeur pour l'argument `default`{.python}, pour une clé introuvable, la méthode
@ -415,9 +434,9 @@ Nous n'avons pas vu grand chose sur les chaînes de caractères, mais certaines
- [Documentation officielle sur le format des interpolations](https://docs.python.org/3/library/string.html#format-string-syntax)
```python {.numberLines}
a = 19
b = f"Le serveur qui doit être vérifié aujourd'hui est le numéro {a}"
c = f"Formatage de la variable : {a:f}" # affiché comme flottant (6 chiffres après la virgule)
number = 2_643
print(f"Le serveur qui doit être vérifié aujourd'hui est le numéro {number}")
print(f"Formatage de la variable : {number:f}") # affiché comme flottant (6 chiffres après la virgule)
```
----
@ -430,7 +449,7 @@ print(chaine.upper(), chaine.lower())
print(chaine.center(50, " ")) # Centre la chaîne originale sur 50 caractères
```
Méthodes qui renvoient une copie modifiée d'une chaîne
Méthodes qui renvoient une **copie** modifiée d'une chaîne
----
@ -457,37 +476,4 @@ print("17 juillet" in chaine)
Exemple de `str.split()`{.python} et `str.join()`{.python}
----
## Bonus : Convertir des données d'un type à un autre
Nous avons vu, ici et là, quelques fonctions pour déclarer des valeurs de base, ou convertir des
valeurs. En voici une liste plus complète :
- `bool(val)`{.python}
- `int(val)`{.python}, `float(val)`{.python}
- `str(val)`{.python}
- `list(val)`{.python}, `tuple(val)`{.python}
- `set(val)`{.python}, `dict(val)`{.python}
Toutes ces fonctions renvoient un nouveau booléen, entier, flottant etc. correspondant à une
conversion de l'expression passée en argument. Cela fonctionne uniquement lorsque la conversion a du sens.
----
Appelées sans argument, ces fonctions vous renvoient `False`{.python}, `0`{.python}, une chaîne ou une
collection vide (des valeurs considérées neutres, qui renvoient toujours `False`{.python} lorsqu'on les convertit en booléen).
Lorsque vous passez un argument à ces fonctions, elles vous renvoient une nouvelle valeur,
qui est une conversion de l'argument passé vers le type
représenté par la fonction. Ce n'est pas toujours possible d'effectuer une conversion; par exemple, il est impossible de
convertir une liste vers un nombre flottant, ou encore de convertir la chaîne `"bonjour"`{.python} vers un nombre entier.
```python {.numberLines}
converted1 = int(3.14159) # tronque le flottant / retire la partie décimale
converted3 = float("3.14159") # comprend le texte et génère un flottant
converted2 = list(1) # erreur
converted4 = float("salut") # aucun caractère valide pour représenter un nombre, échoue
```