# Bases de Python - Partie 4 - Structures de données

Les structures de données sont des éléments qui permettent de stocker des données de manière efficace pour pouvoir les utiliser facilement dans un programme. 

Elles permettent de regrouper plusieurs valeurs dans un ensemble et d’effectuer rapidement des opérations comme l’ajout, la suppression, la recherche ou la modification d’éléments. 

Chaque structure (liste, dictionnaire, tuple, set, etc.) s'utilisera dans des contextes différents selon les besoins du programme.

---
## Listes

Une **liste** est une structure de données ordonnée, c’est-à-dire que chaque élément possède une position fixe dans l'ensemble. On peut accéder à un élément par sa position (qu'on appellera indice). **Attention** les indices d'une liste commencent par zéro.

Les listes sont modifiables : il est donc possible d’ajouter, de supprimer ou de modifier leurs éléments à tout moment. Elles peuvent contenir des éléments de types différents (nombres, chaînes de caractères, booléens, ...).

### Créer et utiliser une liste

- Pour créer une liste on utilise des crochets `[]`, et on met les éléments séparés par des virgules à l'intérieur
- Une liste peut contenir plusieurs types
- Pour accéder à un élément précis, on peut écrire le nom de la liste et entre `[]` on met l'indice.
- Si on utilise un indice négatif, on part de la fin au lieu du début.


```admonish example title="Exemple"
~~~python
ma_liste = [1, 2, 3, "bonjour", 4.5]   # Mélange de types accepté
print(ma_liste)           # Affiche la liste entière
ma_liste[0]               # On accède au premier élément grâce à l'indice 0 : 1
ma_liste[-1]              # Dernier élément avec l'indice -1 : 4.5

# On peut directement afficher les éléments :
print(ma_liste[0])
print(ma_liste[-1])
~~~
```

### Modifier une liste

- On peut modifier un élément comme une variable, en l'appelant avec son indice, et on lui affecte une valeur.

```admonish example title="Exemple"
~~~python
ma_liste[1] = 9           # Remplace le deuxième élément par 9
print(ma_liste)           # [1, 9, 3, 'bonjour', 4.5]
~~~
```

### Autres actions possibles

On peut manipuler les listes avec des **méthodes**. 

Les méthodes sont presque comme des fonctions, mais au lieu d'exister seules, on les appelle à partir d'un **objet** (une sorte de variable complexe). Nous verrons cela en détail plus tard. En attendant, considérons qu'il s'agit d'une fonction qu'on peut appeler avec un `.` depuis des variables d'un certain type (les listes, les chaînes de caractères, et les autres structures de données par exemple).

- `append` ajoute un élément à la fin de la liste
- `insert` ajoute un élément à un endroit choisi de la liste
- `remove` retire un élement (une valeur précise)
- `del` permet de supprimer aussi un élément, mais en passant par l'indice cette fois
- `len` permet de connaître la longueur d'une liste. Attention len est une fonction, pas une méthode.

```admonish example title="Exemple"
~~~python
ma_liste.append("nouveau")   # Ajoute 'nouveau' à la fin de la liste
ma_liste.insert(1, "ok")     # Insère 'ok' en position 1
ma_liste.remove(3)           # Supprime la première occurrence de la valeur 3
del ma_liste[2]              # Supprime l’élément ayant pour indice 2
len(ma_liste)                # Renvoie le nombre d’éléments (la longueur) de la liste
~~~
```

### Itération

On peut boucler sur tous les éléments d'une liste avec `for`

```admonish example title="Exemple"
~~~python
ma_liste = ["Hello", "World"]
for element in ma_liste:
    print(element)

# Affichera : 
## Hello
## World
~~~
```

### Concaténer des chaînes avec `join`

Lorsque la liste contient uniquement des chaînes de caractères, il est possible de les concaténer en une seule chaîne grâce à la méthode `join`. Elle s'appelle depuis une chaîne de caractères qui servira de séparateur. Cela permet par exemple de créer une phrase à partir de mots stockés dans une liste.

```admonish warning title="Attention"
`join` peut paraître contre-intuitive : on l'appelle sous forme de méthode depuis la chaîne de caractères qu'on utilisera pour séparer les mots de la liste qu'on veut réunir.
```

```admonish example title="Exemple"
~~~python
mots = ["Python", "est", "simple", "et", "puissant"]
phrase = " ".join(mots)   # Le séparateur est un espace : " " et on réunit les mots de la liste `mots`
print(phrase)             # Affiche : Python est simple et puissant
~~~
```

---
## Dictionnaires

Un **dictionnaire** est une structure clé-valeur : chaque valeur est associée à une clé unique. L’accès à une valeur se fait via sa clé et non pas un indice. Les clés peuvent être des chaînes, des nombres, etc.

```admonish tip title="Remarque"
Avant Python 3.7 (donc des anciennes versions de Python), les dictionnaires n'étaient pas ordonnés. Aujourd'hui, les dictionnaires sont ordonnés comme les listes, mais on ne peut tout de même pas accéder à un élément avec un indice marquant sa position.
```

### Créer et utiliser un dictionnaire

- Créer un dictionnaire se fait à l'aide d'accolades `{}`, et on sépare chaque couple clé-valeur par une virgule. En revanche, les clés et les valeurs sont séparées par `:`
- Accéder à des valeurs peut se faire de deux manières :
    - Avec des accolades (un peu comme les indices d'une liste sauf qu'on mettra la clé). Si la clé n'existe pas, renvoie une erreur
    - Avec la méthode `get` qui renvoie `None` si la clé n'existe pas


```admonish example title="Exemple"
~~~python
mon_dict = {"nom": "Alice", "age": 25, "ville": "Paris"}
mon_dict["nom"]        # Accéder à 'Alice' avec la clé `nom`
mon_dict.get("age")    # Accéder à 25 avec la clé `age`. Mais si la clé n'existait pas, cela ne renverrait pas d'erreur.
~~~
```

### Ajouter des éléments, les modifier, les supprimer

Comme pour les listes, on peut modifier un élément grâce à sa clé

```admonish example title="Exemple"
~~~python
mon_dict["age"] = 26          # Modifie la valeur associée à 'age'
mon_dict["pays"] = "France"   # Ajoute une nouvelle clé 'pays'
del mon_dict["ville"]         # Supprime la clé 'ville'
~~~
```

## Itération

Comme pour les listes, on peut boucler sur tous les éléments d'un dictionnaire. Il y a trois manières possibles :

- Sans méthode, on boucle sur toutes les valeurs, comme pour une liste
- Avec la méthode `items()`, on récupère deux élements : la clé et la valeur, que l'on peut séparer et utiliser dans notre boucle (voir exemple). C'est la façon la plus courante de faire.
- Avec la méthode `keys()`, on boucle sur les clés du dictionnaire

```admonish example title="Exemple"
~~~python
for cle, valeur in mon_dict.items():
    print(cle, ":", valeur)   # Affiche chaque clé et sa valeur
~~~
```

---
## Tuples

Un **tuple** ressemble à une liste, mais il est **immuable** : une fois créé, il est impossible de modifier, d’ajouter ou de supprimer ses éléments. On les utilise souvent pour stocker des données qui ne doivent pas changer ou pour retourner plusieurs valeurs depuis une fonction.

### Créer et utiliser un tuple

- On crée un tuple à l'aide de parenthèses, et on sépare les éléments par une virgule
- Comme pour les listes, on peut accéder à un élément avec son indice entre `[]`

```admonish example title="Exemple"
~~~python
mon_tuple = (10, 20, 30)
mon_tuple[0]      # Premier élément : 10
mon_tuple[-1]     # Dernier élément : 30
~~~
```

### Décomposer un tuple

On peut décomposer un tuple et affecter chaque valeur à une variable différente. 

C'est ce qu'on fait implicitement quand on retourne plusieurs valeurs avec `return` dans une fonction et qu'on les affecte à plusieurs variables.
C'est également ce qu'il se passe lorsqu'on itère sur un dictionnaire avec items, et qu'on a deux variables `key` et `value` dans notre boucle.

```python
coord = (5, 7)
x, y = coord             # x reçoit 5, y reçoit 7
print(x, y)              # Affiche 5 7
```


## Sets

Un **set** est une structure **non ordonnée** qui contient des éléments uniques (pas de doublons). Les sets sont utilisés pour effectuer des opérations sur des ensembles comme l’union, l’intersection ou la différence. Les éléments doivent être hashables (par exemple, nombres, chaînes ou tuples).

### Créer et utiliser un set

- On crée un set à l'aide d'accolades `{}`. Mais contrairement aux dictionnaires, les sets n'ont pas de clé (donc pas de `key: value`, c'est ce qui permet de les différencier syntaxiquement)

```admonish example title="Exemple"
~~~python
mon_set = {1, 2, 3, 3, 2}
print(mon_set)           # {1, 2, 3}, les doublons sont éliminés
~~~
```

### Modifier un set
,
Les méthodes `add` et `remove` permettent d'ajouter et supprimer des éléments d'un set.

```admonish example title="Exemple"
~~~python
mon_set.add(4)           # Ajoute 4 au set
mon_set.remove(2)        # Supprime 2 du set
~~~
```


### Opérations avec les sets

On peut effectuer diverses opérations avec les sets :
- **Union** : combine tous les éléments des deux ensembles, sans doublons.
- **Intersection** : garde uniquement les éléments présents dans les deux ensembles.
- **Différence** : garde les éléments présents dans le premier ensemble mais pas dans le second.
- **Différence symétrique** : garde les éléments présents dans un seul des deux ensembles, mais pas dans les deux.

| Opération | Description | Méthode | Opérateur |
| :-- | :-- | :-- | :-- |
| Union | Tous les éléments de A et B | `A.union(B)` | `A | B` |
| Intersection | Éléments communs à A et B | `A.intersection(B)` | `A & B` |
| Différence | Éléments dans A mais pas dans B | `A.difference(B)` | `A - B` |
| Différence symétrique | Éléments dans A ou B, mais pas dans les deux | `A.symmetric_difference(B)` | `A ^ B` |

```admonish example title="Exemple"
~~~python
A = {1, 2, 3}
B = {2, 3, 4}

print(A | B)       # Union : {1, 2, 3, 4}
print(A.union(B))  # Même résultat que ci-dessus

print(A & B)       # Intersection : {2, 3}
print(A.intersection(B))  # Même résultat

print(A - B)       # Différence : {1}
print(A.difference(B))    # Même résultat

print(A ^ B)       # Différence symétrique : {1, 4}
print(A.symmetric_difference(B))  # Même résultat
~~~
```