--- 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
(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 ```