Files
training.python.beginner/documentation/02-language-basics-data.md
2025-07-04 19:26:39 +02:00

494 lines
19 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Découvrir le langage - types de données avancés
author: Steve Kossouho
---
# Collections de données
----
## Les 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…)
```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]
liste4 = [[1, 2, 3], [4, 5, 6]]
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.
```python {.numberLines}
liste1 = [1, 2, 3, 4, 5, 6]
liste2 = list() # Créer une nouvelle liste vide
# Infos sur la liste
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
liste1[0] = 98 # Remplace la valeur à l'index 0
liste1.remove(4) # Enlève la première occurrence du nombre 4, ou erreur si introuvable
del liste1[3] # Retire l'élément à l'index 3
```
[Méthodes accessibles sur les listes](https://docs.python.org/3/tutorial/datastructures.html#more-on-lists)
----
### Erreurs d'accès
Accéder à un élément de liste n'existant pas génère une erreur (`IndexError`{.python}) et interrompt
votre programme; il peut être utile d'utiliser la fonction `len()`{.python} pour tester que vous
accédez à un indice valide.
```python {.numberLines}
liste1 = [1, 2, 3]
if len(liste1) >= 4:
print(liste1[3])
```
----
### 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
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.remove(8)
```
----
### Indices de liste négatifs
Il est également possible d'accéder à des éléments de liste 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}.
```python {.numberLines}
liste1 = [1, 2, 4, 6, 9, 11]
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 :
```python {.numberLines}
prime_numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]
for number in prime_numbers:
print(number)
```
La variable `number`{.python} déclarée pour la boucle contiendra tour à tour
les valeurs `2`{.python}, puis `3`{.python}, puis `5`{.python}, etc.
----
## Opérations sur listes
Récupérer des portions ou partitions de listes (slicing) :
```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
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
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
**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 ».
```python {.numberLines}
a = (1, 2, 3)
b = () # ou tuple()
c = (1,) # il vous faut au minimum une virgule
d = (None, "Salut", 15, 2.718)
```
Les méthodes de modification de contenu, telles que `append`{.python} etc. ne sont logiquement pas disponibles avec
ce type : [opérations des tuples](https://docs.python.org/3/library/stdtypes.html#common-sequence-operations)
----
### 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.
La collection garantit l'unicité des éléments, un élément ne pouvant apparaître qu'une fois au maximum
dans votre objet.
```python {.numberLines}
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
...
```
[Méthodes disponibles sur les ensembles](https://docs.python.org/fr/3/library/stdtypes.html#set)
----
![Opérations sur les ensembles](assets/images/basics-sets-operations.jpg)
----
#### 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
seul ou un milliard.
L'algorithme et la structure de données interne à Python derrière cette efficacité se nomme **Table de hachage**.
----
##### Tables de hachage
Une table de hachage fonctionne en trois temps:
- On réserve des zones de mémoire d'avance (**buckets**), au départ vides;
- lorsque l'on souhaite vérifier si un élément y est présent, on lui calcule une signature (`hash()`{.python});
- la signature de l'objet permet de savoir quelle zone de mémoire consulter pour le trouver.
- si la zone de mémoire est vide, c'est que l'objet n'était pas présent.
----
##### Exemple de hachage
![Table de hachage](assets/images/basics-sets-hashing.png)
----
##### 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.
L'objectif d'un hash est simple; deux objets différents doivent avoir un hash différent.
Dans Python ce dernier est un nombre entier sur 64 bits. Si deux objets a et b ont le même hash, ils seront généralement
considérés comme équivalents et donc en **collision** (dans ce cas, `a == b`{.python}). S'ils ne sont pas considérés équivalents,
il n'y a pas de collision.
----
##### 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
dictionnaire de signatures dédié à créer des collisions.
Le hachage est en général imprévisible pour les valeurs suivantes:
- `str`{.python} non vides
- `float`{.python} non équivalents à un `int`{.python}
- `None`{.python}
- `tuple`{.python}
----
##### Hashes prévisibles
Le hash est par contre prévisible pour les valeurs suivantes:
- `0`{.python}, `0.0`{.python}, `False`{.python} et `""`{.python} ont un hash de `0`{.python} mais `""`{.python} n'est pas équivalent;
- `1`{.python}, `1.0`{.python} et `True`{.python} ont un hash de `1`{.python} et sont tous en collision.
- `int`{.python}, où le hash est identique au nombre...
Les objets `int`{.python} ont généralement un `hash()`{.python} identique, sauf cas suivants:
- `-1`{.python} a un hash de `-2`{.python} car la valeur `-1`{.python} est un code d'erreur;
- `2 ** 61 - 2`{.python} est le dernier nombre positif avec un hash identique;
- à partir de `2 ** 61 - 1`{.python}, le hash revient à 0 etc.
- le même algorithme fonction sur les entiers négatifs, mais avec des hashes négatifs.
----
##### 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
à tout moment, tels que les `list`{.python}, `set`{.python} ou encore les `dict`{.python}.
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.
```python {.numberLines}
# La fonction hash est disponible par défaut en Python
print(hash([1, 2, 3])) # Provoque une exception
```
----
### 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}.
```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
```
----
#### 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 :
```python {.numberLines}
a = {
"Jérémy": (25, "M", "Lille"),
"Hélène": (30, "F", "Ambérieu-en-Bugey"),
"Gwladys": (35, "F", "Nyons"),
}
for prenom in a:
print(prenom) # affiche uniquement une clé
print(a[prenom]) # retrouve la valeur associée à la clé
```
----
Si vous parcourez une collection dont tous les éléments sont eux-mêmes des séquences
de `n` éléments, vous pouvez les dépaqueter (**unpacking**) :
```python {.numberLines}
# Avancé, unpacking via la méthode `items`
for item in a.items(): # a.items() renvoie ((clé1, valeur1), (clé2, valeur2), …)
print(item) # est un tuple
for key, value in a.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)
```
----
#### Manipuler un dictionnaire
Pour changer la valeur associée à une clé existante, ou par la même occasion, associer une valeur à
une nouvelle clé, il suffit d'écrire :
```python {.numberLines}
a = {"Bordeaux": 250000}
a["Marseille"] = 800000
a["Bordeaux"] = 90
print(a["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
```
----
### Plus: 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
renvoie `None`{.python} (au lieu de planter comme lorsqu'on écrit `dict[key]`{.python}).
```python {.numberLines}
populations = {"Paris": 2.6e6, "Marseille": 8e5, "Lyon": 5e5, "Bordeaux": 2.5e5}
print(populations.get("Tarbes")) # renvoie None
print(populations.get("Mulhouse", -1)) # renvoie -1
print(populations.get("Marseille", -1)) # renvoie 800 000.0
```
Si l'on passe une valeur pour l'argument `default`{.python}, alors, si la clé n'a pas été trouvée,
la méthode renvoie la valeur de repli que vous avez définie.
----
## Compréhensions de listes, tuples etc.
Déclarer des listes, tuples, sets et dictionnaires avec la syntaxe de compréhension :
```python {.numberLines}
a = [x * 2 for x in range(100)] # tous les nombres pairs de 0 à 198
b = [x * 2 for x in range(100) if x != 10] # nombres pairs de 0 à 198 sauf 20
c = (x for x in range(100)) # pas un tuple mais un générateur, nombres de 0 à 99
d = {x / 2 for x in range(100)} # on peut faire pareil avec les sets
pre = {"Jon": None, "Pam": None, "Mel": None, "Kim": None}
e = {k: pre[k] for k in pre} # on copie pre
```
----
## Récapitulatif des collections
| Type | Propriétés |
|---------------|--------------------------------------------------------------------------------------------------------------------------|
| `list` | `[...,]`{.python}. Séquence dont le contenu est modifiable (ajout, suppression). |
| `tuple` | `(...,)`{.python}. Séquence dont le contenu est gelé après déclaration. Utilisable comme élément d'un `set`. |
| `set` | `{...,}`{.python}. Déduplique les éléments et permet des opérations entre ensembles <br/>(union, intersection etc.) |
| `dict` | `{..:..,}`{.python}. Structure de **recherche** rapide où l'on peut associer des valeurs quelconques à des identifiants. |
----
### Usages d'exemple
Le `tuple`{.python} n'est jamais très important, et il sera souvent plus pratique de travailler sur des listes. Les seuls
cas de figure qui obligent à utiliser des `tuple`{.python} sont les cas où vous voulez ajouter une séquence comme élément d'un `set`
ou comme clé d'un `dict`{.python}.
- `list`{.python} : si vous avez des données séquentielles à stocker, par exemple lues depuis un fichier.
- `set`{.python} : si vous souhaitez enlever des doublons, connaître le nombre d'éléments uniques.
- `dict`{.python} : si vous avez besoin d'une "table de référence" pour stocker des données que vous allez rechercher fréquemment.
----
# Chaînes de caractères
----
## Opérations sur les chaînes de caractères
Nous n'avons pas vu grand chose sur les chaînes de caractères, mais certaines informations peuvent
être fréquemment utiles aux développeurs :
- [Méthodes sur les objets de type `str`{.python}](https://docs.python.org/3/library/stdtypes.html#string-methods)
- [Formatage de chaînes via les f-strings](https://zetcode.com/python/fstring/)
- [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)
```
----
## Les chaînes de caractères et leurs méthodes
```python {.numberLines}
chaine = "Bonjour"
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 fréquemment utiles sur les chaînes de caractères
- `upper()`{.python} : renvoie une copie en majuscules
- `lower()`{.python} : renvoie une copie en minuscules
- `strip()`{.python} : retire les espaces aux extrêmités
- `replace(old, new, count=None)`{.python} : remplace `old` par `new`
- `index(sub, start=None)`{.python} : renvoie la position de `sub`
- `sub in text`{.python} : renvoie si `sub` est inclus dans `text`
- `split(sep=None)`{.python} : découpe en liste autour du séparateur, par défaut autour des espaces
- `str.join(iterable)`{.python} : sert de séparateur et joint une liste de chaînes
----
```python {.numberLines}
chaine = "Bonjour, nous sommes le 17 juillet 2063."
words = chaine.split() # renvoie une liste de chaînes autour des espaces
rejoin = " ".join(words) # renvoie une chaîne en insérant l'espace comme séparateur
print(chaine.upper(), chaine.lower(), rejoin)
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
```