Aller au contenu

Les collections

Introduction

Les collections permettent de regrouper des données ou des objets afin de les parcourir, de les rechercher, d’y accéder et de les manipuler. Python propose plusieurs types de collections intégrés au langage : les listes, les tuples, les ensembles et les dictionnaires.

Pour un développeur C#, leurs usages rappellent ceux de List<T>, des tuples, de HashSet<T> et de Dictionary<TKey, TValue>. Leurs comportements présentent toutefois quelques différences.

Les listes

Présentation

Une liste (list) est une collection ordonnée et modifiable, qui accepte les doublons. Chaque élément est accessible par son indice, le premier élément ayant l’indice 0. Elle se rapproche de la collection List<T> en C#.

Créer une liste

Une liste peut être créée avec des crochets :

names = ["Baptiste", "Mathieu", "Damien"]

Pour créer une liste vide :

names = []

Une liste peut contenir des éléments de types différents. Cependant, conserver des éléments de même type facilite la manipulation des données. Nous pouvons préciser le type attendu des éléments grâce à une annotation :

names: list[str] = ["Baptiste", "Mathieu", "Damien"]

Cette annotation est utilisée par les outils d’analyse de code, tels que Pylance ou mypy, pour détecter les incohérences de types. Elle ne contrôle pas les types des éléments.

Accéder aux éléments

Les indices positifs ou nuls permettent d’accéder aux éléments depuis le début de la liste. Les indices négatifs permettent d’y accéder à partir de la fin : -1 désigne le dernier élément, -2 l’avant-dernier, et ainsi de suite.

names = ["Baptiste", "Mathieu", "Damien"]

print(names[0])   # Baptiste
print(names[1])   # Mathieu
print(names[-2])  # Mathieu
print(names[-1])  # Damien

Un indice situé en dehors des limites de la liste entraîne la levée d’une exception de type IndexError.

Modifier une liste

Nous pouvons remplacer un élément, en ajouter ou en supprimer :

names = ["Baptiste", "Mathieu", "Damien"]

names[1] = "James"
names.append("Karl")
names.insert(0, "Ludwig")
names.remove("Damien")

print(names)
# ['Ludwig', 'Baptiste', 'James', 'Karl']

Les principales opérations sont :

Opération Description
append(value) Ajoute un élément à la fin
extend(values) Ajoute tous les éléments d’une autre collection
insert(index, value) Insère un élément à une position donnée
remove(value) Supprime la première occurrence d’une valeur ; lève une exception de type ValueError si elle est absente
pop(index) Supprime et renvoie l’élément à l’indice indiqué. Sans indice, retire le dernier
clear() Supprime tous les éléments

Extraire une partie d’une liste

La syntaxe de découpage, appelée slicing, permet d’extraire une partie d’une liste :

numbers = [10, 20, 30, 40, 50]

print(numbers[1:4])  # [20, 30, 40]
print(numbers[:3])   # [10, 20, 30]
print(numbers[2:])   # [30, 40, 50]

Dans numbers[start:end], l’indice de début est inclus et celui de fin est exclu. Lorsque l’indice de début est omis, la nouvelle liste contient les éléments à partir du premier. Lorsque l’indice de fin est omis, elle contient les éléments jusqu’au dernier. Le résultat est une nouvelle liste ; la liste d’origine n’est pas modifiée.

Les tuples

Présentation

Un tuple est une collection ordonnée et immuable, qui accepte les doublons. Une fois créé, nous ne pouvons pas supprimer ou remplacer les éléments qu'il contient, ni en ajouter.

Créer et utiliser un tuple

Voici un tuple (tuple) représentant les coordonnées d’un point :

position = (10, 20)

print(position[0])  # 10
print(position[1])  # 20

Nous pouvons décomposer le tuple pour affecter ses valeurs à plusieurs variables :

x, y = position

print(x)  # 10
print(y)  # 20

Cette opération est appelée déballage (unpacking). Elle correspond à la déconstruction des tuples en C#.

Pour créer un tuple contenant un seul élément, une virgule est nécessaire :

single_value = (10,)

Sans cette virgule, (10) représente simplement le nombre 10 entre parenthèses.

Comprendre l’immutabilité

L’immutabilité concerne la structure du tuple : nous ne pouvons pas ajouter, supprimer ou remplacer ses éléments. En revanche, un objet modifiable contenu dans le tuple peut toujours être modifié :

data = ("Équipe", ["Baptiste", "Mathieu"])

data[1].append("Damien")

print(data)
# ('Équipe', ['Baptiste', 'Mathieu', 'Damien'])

Un tuple Python ne possède pas exactement le même comportement qu’un tuple C#, dont les éléments peuvent être modifiés.

Les ensembles

Présentation

Un ensemble (set) est une collection modifiable d’éléments uniques. Il ne garantit pas l’ordre des éléments et ne permet pas d’y accéder via un indice.

Il correspond à la classe HashSet<T> en C#. Il convient pour éliminer les doublons, tester l’appartenance d’une valeur ou comparer plusieurs groupes de données.

Créer un ensemble

Un ensemble peut être créé en plaçant ses éléments entre accolades :

languages = {"C#", "Python", "C#", "Java"}

print(len(languages))  # 3

La valeur "C#" n’est conservée qu’une seule fois.

Pour créer un ensemble vide, nous utilisons set() :

languages = set()

Les accolades vides {} créent un dictionnaire, et non un ensemble.

Modifier un ensemble

La méthode add(<item>) permet d’ajouter un élément. S’il est déjà présent, l'élément n'est pas ajouté :

languages = {"C#", "Python"}

languages.add("Java")
languages.discard("Python")

La méthode discard(<item>) supprime un élément s’il existe. La méthode remove(<item>) permet aussi de le supprimer, mais lève une exception de type KeyError s’il est absent.

Les opérations entre ensembles

Les ensembles permettent de réaliser des opérations d’union, d’intersection et de différence :

first = {"C#", "Python", "Java"}
second = {"Python", "JavaScript"}

union = first | second
intersection = first & second
difference = first - second
symmetric_difference = first ^ second
Opération Résultat
first \| second Éléments présents dans au moins un des deux ensembles.
first & second Éléments communs aux deux ensembles.
first - second Éléments présents dans first, mais absents de second.
first ^ second Éléments présents dans un seul des deux ensembles.

Les éléments hachables

Les éléments d’un ensemble doivent être "hachables" : Python doit pouvoir calculer un code de hachage (hashcode) stable (pendant la durée de vie de l'objet) permettant de les retrouver. Deux éléments considérés comme égaux ont le même code de hachage, mais deux éléments ayant le même code ne sont pas nécessairement égaux.

Un code de hachage n’est pas un identifiant unique : des valeurs différentes peuvent produire le même code. Cela s’appelle une collision.

Voici un exemple en Python :

print(-1 == -2)  # False

print(hash(-1))  # -2
print(hash(-2))  # -2

Les nombres -1 et -2 sont différents, mais leurs codes de hachage sont identiques.

Un ensemble utilise le code de hachage pour rechercher un élément, puis une comparaison d’égalité pour distinguer les éléments ayant le même code. Il conserve donc bien les deux valeurs -1 et -2 :

numbers = {-1, -2}

print(len(numbers))  # 2

Les nombres et les chaînes de caractères sont hachables. Les listes et les dictionnaires ne le sont pas et ne peuvent donc pas être directement ajoutés à un ensemble.

Python propose également frozenset, une version immuable d’un ensemble. Un frozenset se crée à partir d’une collection, par exemple une liste :

languages = frozenset(["C#", "Python", "C#", "Java"])

print(len(languages))         # 3
print("Python" in languages)  # True

Comme avec les ensembles set, les doublons sont éliminés. En revanche, nous ne pouvons pas ajouter ou supprimer d’éléments : les méthodes add(<item>), remove(<item>) et discard(<item>) ne sont pas disponibles.

languages.add("JavaScript")  # Lève une exception AttributeError

Les dictionnaires

Présentation

Un dictionnaire (dict) associe des valeurs à des clés uniques. Il est modifiable et conserve l’ordre d’insertion des clés. Il se rapproche de la classe Dictionary<TKey, TValue> en C#.

Créer un dictionnaire

Un dictionnaire peut être créé avec des accolades, chaque clé étant séparée de sa valeur par deux-points. Dans cet exemple, l’annotation dict[int, str] indique que les clés attendues sont des entiers et les valeurs des chaînes de caractères :

products: dict[int, str] = {
    101: "Clavier",
    102: "Souris",
    103: "Écran"
}

Les clés doivent être hachables. Les valeurs peuvent être des données ou des objets de n’importe quel type.

Accéder aux valeurs

Nous pouvons retrouver une valeur à partir de sa clé :

print(products[101])  # Clavier

Si la clé est absente, cet accès lève une exception de type KeyError.

La méthode get permet de fournir une valeur par défaut :

name = products.get(999, "Produit inconnu")

print(name)  # Produit inconnu

Sans valeur par défaut explicite, la méthode get renvoie None lorsque la clé est absente du dictionnaire.

Modifier un dictionnaire

L’affectation ajoute une association clé - valeur si la clé est absente, ou remplace sa valeur si elle existe déjà. L’instruction del permet de supprimer une association :

products[104] = "Casque"      # Ajout
products[102] = "Souris USB"  # Modification

del products[103]             # Suppression

Ce comportement est similaire à celui de l’indexeur dictionary[key] en C#. Cependant, la méthode Add de Dictionary<TKey, TValue> en C# lève une exception si la clé existe déjà.

Si la clé à supprimer est absente, l’instruction del lève une exception de type KeyError.

Parcourir un dictionnaire

Par défaut, un traitement itératif for sur un dictionnaire parcourt ses clés :

for identifier in products:
    print(identifier)

La méthode items() permet de récupérer les clés et les valeurs :

for identifier, name in products.items():
    print(f"{identifier} : {name}")

Les méthodes keys() et values() donnent respectivement accès aux clés et aux valeurs du dictionnaire.

Les opérations communes

Obtenir le nombre d’éléments

La fonction len() renvoie le nombre d’éléments d’une collection :

names = ["Baptiste", "Mathieu", "Damien"]

print(len(names))  # 3

Elle correspond, selon le type C# considéré, à l’utilisation de Count ou de Length.

Vérifier la présence d’un élément

Les opérateurs in et not in permettent de tester l’appartenance :

names = ["Baptiste", "Mathieu"]

print("Baptiste" in names)      # True
print("Damien" not in names)    # True

Sur un dictionnaire, ces opérateurs recherchent une clé :

products = {101: "Clavier"}

print(101 in products)  # True

Parcourir une collection

Une boucle for permet de parcourir les éléments, comme foreach en C# :

names = ["Baptiste", "Mathieu", "Damien"]

for name in names:
    print(name)

Pour numéroter les éléments pendant le parcours, nous utilisons enumerate(). Le compteur commence à 0 et correspond ici à l’indice dans la liste :

for index, name in enumerate(names):
    print(f"{index} : {name}")

Les collections d’objets

Les collections peuvent contenir des instances de classes :

class Product:
    def __init__(self, name: str, price: float):
        self.name = name
        self.price = price


products: list[Product] = [
    Product("Clavier", 69.90),
    Product("Souris", 29.90),
    Product("Écran", 249.90)
]

for product in products:
    print(f"{product.name} : {product.price:.2f} €")

Nous pouvons aussi utiliser un dictionnaire pour retrouver un objet à partir de son identifiant :

products_by_id: dict[int, Product] = {
    101: Product("Clavier", 69.90),
    102: Product("Souris", 29.90)
}

product = products_by_id[101]

Filtrer et transformer les données

Les compréhensions de liste

Une compréhension de liste permet de créer une nouvelle liste en parcourant une collection, en transformant éventuellement ses éléments et en les filtrant selon une condition.

Pour sélectionner les produits dont le prix est inférieur à 100 euros :

affordable_products = [
    product
    for product in products
    if product.price < 100
]

Cette opération rappelle l’utilisation de la méthode Where() suivie de ToList() en LINQ lorsqu'on développe en C#.

Pour extraire les noms des produits :

names = [product.name for product in products]

Cette transformation se rapproche de Select() suivi de ToList() en LINQ lorsqu'on développe en C#.

Nous pouvons combiner filtrage et transformation :

names = [
    product.name
    for product in products
    if product.price < 100
]

Ces compréhensions créent une nouvelle liste.

Les compréhensions d’ensemble et de dictionnaire

Le même principe s’applique aux ensembles. Cet exemple crée un ensemble contenant les noms des produits, sans doublons :

unique_names = {product.name for product in products}

Une compréhension de dictionnaire permet de créer des associations clé-valeur. Ici, chaque nom de produit est associé à son prix :

prices_by_name = {
    product.name: product.price
    for product in products
}

Si plusieurs produits portent le même nom, seul le prix du dernier produit rencontré est conservé pour cette clé.

Trier les données

La fonction sorted() crée une nouvelle liste triée :

numbers = [30, 10, 20]

ordered_numbers = sorted(numbers)

print(ordered_numbers)  # [10, 20, 30]
print(numbers)          # [30, 10, 20]

Pour trier des objets, le paramètre key reçoit une fonction qui renvoie la valeur utilisée pour le tri. Dans cet exemple, l’expression lambda product: product.price permet de trier les produits par prix croissant :

products_by_price = sorted(
    products,
    key=lambda product: product.price
)

En ajoutant le paramètre reverse=True, les produits sont triés par prix décroissant.

La méthode sort() trie une liste existante et renvoie None :

products.sort(key=lambda product: product.price)

Copier une liste

Affecter une liste à une autre variable ne crée pas une nouvelle liste :

first = ["Baptiste", "Mathieu"]
second = first

second.append("Damien")

print(first)  # ['Baptiste', 'Mathieu', 'Damien']

Les deux variables désignent le même objet, comme deux variables C# référençant la même instance de List<T>.

Pour créer une nouvelle liste contenant les mêmes éléments, nous utilisons la méthode copy() :

second = first.copy()

Il s’agit d’une copie superficielle : les deux listes sont distinctes, mais leurs éléments désignent les mêmes objets. Si un objet contenu dans ces listes est modifiable, sa modification sera visible depuis les deux listes.

Choisir la collection appropriée

Besoin Collection
Gérer une suite d’éléments modifiable list
Regrouper des valeurs dans une collection ordonnée et immuable tuple
Gérer des valeurs uniques et comparer des groupes set
Gérer une collection immuable d’éléments uniques frozenset
Retrouver une valeur ou un objet à partir d’une clé dict

La bibliothèque standard propose aussi des collections spécialisées, notamment avec la classe collections.deque pour les files :

from collections import deque

tasks = deque(["Importer", "Vérifier"])

tasks.append("Enregistrer")  # Ajout à la fin
task = tasks.popleft()       # Retrait au début

print(task)         # Importer
print(list(tasks))  # ['Vérifier', 'Enregistrer']

Et la classe collections.Counter pour compter les occurrences :

from collections import Counter

languages = ["C#", "Python", "C#", "Java", "Python", "C#"]

counts = Counter(languages)

print(counts["C#"])          # 3
print(counts["Python"])      # 2
print(counts["Java"])        # 1
print(counts["JavaScript"])  # 0

Pour les tableaux numériques et le calcul scientifique, NumPy propose des structures adaptées. Pour les données tabulaires organisées en lignes et en colonnes, pandas fournit notamment le type DataFrame. NumPy et pandas sont des bibliothèques tierces.