482 lines
19 KiB
Markdown
482 lines
19 KiB
Markdown
---
|
||
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
|
||
|
||
----
|
||
|
||
## Listes
|
||
|
||
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] # é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()
|
||
liste6 = [liste1] # Liste contenant 1 élément, qui est lui-même une liste
|
||
```
|
||
|
||
----
|
||
|
||
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]
|
||
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
|
||
|
||
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 = ["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 **non vide** en utilisant des indices négatifs.
|
||
|
||
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}
|
||
animals = ["dog", "cat", "rabbit", "hamster", "parrot"]
|
||
print(animals[-1]) # affiche "parrot"
|
||
print(animals[-2]) # affiche "hamster"
|
||
print(animals[-5]) # affiche "dog"
|
||
```
|
||
|
||
----
|
||
|
||
## 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 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] # 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
|
||
vous souhaitez appliquer un calcul sur une petite partition d'un jeu de données.
|
||
|
||
----
|
||
|
||
## 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) # 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
|
||
...
|
||
```
|
||
|
||
[Méthodes disponibles sur les ensembles](https://docs.python.org/fr/3/library/stdtypes.html#set)
|
||
|
||
----
|
||
|
||

|
||
|
||
----
|
||
|
||
### 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 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]{.naming}.
|
||
|
||
----
|
||
|
||
#### 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
|
||
|
||

|
||
|
||
----
|
||
|
||
#### 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 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** : (`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
|
||
print(9 in b) # Vérifier que 9 fait partie des clés de b
|
||
print(9 in b.values()) # Vérifier que 9 fait partie des valeurs de b
|
||
```
|
||
|
||
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}. 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}
|
||
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 profils:
|
||
print(prenom) # affiche uniquement une clé
|
||
print(profils[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`
|
||
profils = {...}
|
||
|
||
for item in profils.items(): # a.items() renvoie ((clé1, valeur1), (clé2, valeur2), …)
|
||
print(item) # est un tuple
|
||
|
||
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)
|
||
```
|
||
|
||
----
|
||
|
||
#### 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}
|
||
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 populations["Marseille"] # plus de clé pour Marseille !
|
||
print("Marseille" in populations) # renvoie si la clé "Marseille" existe
|
||
```
|
||
|
||
----
|
||
|
||
### 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
|
||
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}
|
||
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)
|
||
```
|
||
|
||
----
|
||
|
||
## 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}
|
||
|
||
|