17. Ensembles : travailler avec des données uniques et non ordonnées
Dans les chapitres précédents, nous avons travaillé avec des listes(list) (des collections ordonnées et mutables) et des dictionnaires(dictionary) (des associations clé-valeur). Nous allons maintenant explorer les ensembles (sets), le type de collection de Python conçu spécifiquement pour stocker des éléments uniques et effectuer efficacement des opérations mathématiques sur les ensembles.
Les ensembles sont particulièrement puissants lorsque vous devez éliminer les doublons, tester rapidement l’appartenance, ou effectuer des opérations comme trouver des éléments communs entre des collections. Contrairement aux listes, les ensembles ne sont pas ordonnés et ne peuvent pas contenir de valeurs en double — tenter d’ajouter deux fois le même élément n’a aucun effet.
17.1) Créer des ensembles et opérations de base
17.1.1) Créer des ensembles avec des accolades
La manière la plus courante de créer un ensemble est d’utiliser des accolades {} avec des valeurs séparées par des virgules :
# Création d'un ensemble de langages de programmation
languages = {"Python", "JavaScript", "Java", "C++"}
print(languages) # Output: {'Python', 'JavaScript', 'Java', 'C++'}
print(type(languages)) # Output: <class 'set'>Important : l’ordre des éléments lorsque vous affichez un ensemble peut différer de l’ordre dans lequel vous les avez saisis. Les ensembles sont des collections non ordonnées, ce qui signifie que Python ne maintient aucune séquence particulière :
numbers = {5, 2, 8, 1, 9}
print(numbers) # Output might be: {1, 2, 5, 8, 9} or another orderL’ordre d’affichage peut varier selon les exécutions et les versions de Python. Ne vous fiez jamais au fait que les ensembles conservent un ordre spécifique — si l’ordre compte, utilisez plutôt une liste.
17.1.2) Les ensembles suppriment automatiquement les doublons
L’une des propriétés les plus utiles des ensembles est qu’ils éliminent automatiquement les valeurs dupliquées. Si vous essayez de créer un ensemble avec des éléments en double, une seule copie de chaque valeur unique est conservée :
# Création d'un ensemble avec des valeurs dupliquées
student_ids = {101, 102, 103, 102, 101, 104}
print(student_ids) # Output: {101, 102, 103, 104}
# Cette propriété rend les ensembles parfaits pour supprimer les doublons
grades = [85, 90, 85, 78, 90, 92, 78, 85]
unique_grades = set(grades)
print(unique_grades) # Output: {78, 85, 90, 92}Cette déduplication automatique se produit parce que les ensembles utilisent un modèle mathématique dans lequel chaque élément ne peut apparaître qu’une seule fois. Lorsque vous ajoutez une valeur qui existe déjà, l’ensemble ignore simplement le doublon.
17.1.3) Créer des ensembles avec le constructeur set()
Vous pouvez créer des ensembles à partir d’autres itérables(iterable) en utilisant le constructeur set(). C’est particulièrement utile pour convertir des listes, des tuples ou des chaînes de caractères en ensembles :
# Création d'un ensemble à partir d'une liste
colors_list = ["red", "blue", "green", "red", "yellow"]
colors_set = set(colors_list)
print(colors_set) # Output: {'red', 'blue', 'green', 'yellow'}
# Création d'un ensemble à partir d'une chaîne (chaque caractère devient un élément)
letters = set("programming")
print(letters) # Output: {'p', 'r', 'o', 'g', 'a', 'm', 'i', 'n'}
# Création d'un ensemble à partir d'un tuple
coordinates = set((10, 20, 30, 20, 10))
print(coordinates) # Output: {10, 20, 30}Lorsque vous créez un ensemble à partir d’une chaîne, chaque caractère unique devient un élément séparé. C’est utile pour trouver tous les caractères distincts dans un texte :
text = "Mississippi"
unique_chars = set(text.lower())
print(unique_chars) # Output: {'m', 'i', 's', 'p'}
print(f"The word contains {len(unique_chars)} unique letters")
# Output: The word contains 4 unique letters17.1.4) Créer un ensemble vide
Voici un piège important : vous ne pouvez pas créer un ensemble vide avec {} parce que Python interprète cela comme un dictionnaire vide. À la place, vous devez utiliser set() :
# WRONG - This creates an empty dictionary, not a set
empty_dict = {}
print(type(empty_dict)) # Output: <class 'dict'>
# CORRECT - This creates an empty set
empty_set = set()
print(type(empty_set)) # Output: <class 'set'>
print(empty_set) # Output: set()Cette distinction existe parce que les dictionnaires ont été ajoutés à Python avant les ensembles ; {} était donc déjà réservé aux dictionnaires vides. Lorsque vous affichez un ensemble vide, Python l’affiche sous la forme set() pour éviter toute confusion.
Confusion courante chez les débutants : lors de la création d’un ensemble avec un seul élément à partir d’une variable, l’ensemble contient la valeur de la variable, pas le nom de la variable :
# Comprendre la création d'ensemble avec des variables
x = 5
my_set = {x} # Crée {5}, pas {'x'}
print(my_set) # Output: {5}
# Si vous voulez un ensemble contenant la chaîne 'x' :
my_set = {'x'}
print(my_set) # Output: {'x'}
# Cela s'applique à n'importe quelle expression
result = 10 + 5
my_set = {result} # Crée {15}
print(my_set) # Output: {15}17.1.5) Propriétés et opérations de base des ensembles
Les ensembles prennent en charge plusieurs opérations fondamentales qui les rendent utiles pour le traitement de données :
# Vérifier le nombre d'éléments uniques
website_visitors = {"alice", "bob", "charlie", "alice", "david"}
print(f"Unique visitors: {len(website_visitors)}")
# Output: Unique visitors: 4
# Tester l'appartenance avec 'in' (très rapide pour les ensembles)
if "alice" in website_visitors:
print("Alice visited the website")
# Output: Alice visited the website
# Tester la non-appartenance
if "eve" not in website_visitors:
print("Eve has not visited yet")
# Output: Eve has not visited yetLe test d’appartenance avec in est l’un des principaux avantages des ensembles. Pour de grandes collections, vérifier si un élément existe dans un ensemble est bien plus rapide que dans une liste. Nous verrons pourquoi cela compte dans la section 17.5.
17.2) Ajouter et supprimer des éléments dans des ensembles
Contrairement aux tuples (qui sont immuables), les ensembles sont mutables — vous pouvez ajouter et supprimer des éléments après leur création. Cependant, les éléments eux-mêmes doivent être des types immuables (nous explorerons cette restriction dans la section 17.7).
17.2.1) Ajouter des éléments uniques avec add()
Ajouter des éléments individuels à un ensemble est simple avec la méthode add(). Si l’élément existe déjà, l’ensemble reste inchangé — aucune erreur n’est levée et aucun doublon n’est créé :
# Construire un ensemble de tâches terminées
completed_tasks = {"task1", "task2"}
print(completed_tasks) # Output: {'task1', 'task2'}
# Ajouter une nouvelle tâche
completed_tasks.add("task3")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}
# Ajouter un doublon n'a aucun effet
completed_tasks.add("task1")
print(completed_tasks) # Output: {'task1', 'task2', 'task3'}Ce comportement rend les ensembles idéaux pour suivre des occurrences uniques. Vous pouvez appeler add() sans vérifier si l’élément existe déjà — l’ensemble gère les doublons automatiquement.
17.2.2) Ajouter plusieurs éléments avec update()
Pour ajouter plusieurs éléments en une fois, utilisez update() qui accepte n’importe quel itérable (liste, tuple, un autre ensemble, etc.) et ajoute tous ses éléments à l’ensemble :
# Commencer avec un petit ensemble de compétences
skills = {"Python", "SQL"}
print(skills) # Output: {'Python', 'SQL'}
# Ajouter plusieurs compétences depuis une liste
new_skills = ["JavaScript", "Docker", "Python"]
skills.update(new_skills)
print(skills) # Output: {'Python', 'SQL', 'JavaScript', 'Docker'}Remarquez que "Python" apparaissait à la fois dans l’ensemble original et dans la liste ajoutée, mais l’ensemble ne contient toujours qu’une seule copie. La méthode update() peut accepter plusieurs itérables en arguments :
# Combiner des compétences provenant de plusieurs sources
current_skills = {"Python"}
course_skills = ["JavaScript", "HTML"]
job_requirements = {"SQL", "Python", "Docker"}
current_skills.update(course_skills, job_requirements)
print(current_skills)
# Output: {'Python', 'JavaScript', 'HTML', 'SQL', 'Docker'}17.2.3) Supprimer des éléments avec remove()
La suppression d’éléments demande de la prudence. La méthode remove() supprime un élément d’un ensemble, mais lève une KeyError si l’élément n’existe pas :
# Gérer les utilisateurs actifs
active_users = {"alice", "bob", "charlie", "david"}
# Supprimer un utilisateur qui s'est déconnecté
active_users.remove("bob")
print(active_users) # Output: {'alice', 'charlie', 'david'}
# Tenter de supprimer un élément inexistant provoque une erreur
# active_users.remove("eve") # Raises: KeyError: 'eve'Comme remove() lève une erreur quand l’élément est absent, il est préférable de l’utiliser lorsque vous êtes certain que l’élément existe, ou lorsque vous voulez intercepter l’erreur s’il n’existe pas :
# Suppression sûre avec gestion d'erreurs (nous en apprendrons plus sur try/except au chapitre 28)
users = {"alice", "bob", "charlie"}
user_to_remove = "david"
if user_to_remove in users:
users.remove(user_to_remove)
print(f"Removed {user_to_remove}")
else:
print(f"{user_to_remove} was not in the set")
# Output: david was not in the set17.2.4) Supprimer des éléments en toute sécurité avec discard()
Pour une suppression plus sûre qui ne lèvera pas d’erreurs, discard() fournit une alternative plus tolérante. Elle supprime l’élément s’il est présent, mais ne fait rien si l’élément n’existe pas :
# Gérer un panier d'achats
cart_items = {"apple", "banana", "orange"}
# Supprimer des éléments en toute sécurité (pas d'erreur si l'élément n'existe pas)
cart_items.discard("banana")
print(cart_items) # Output: {'apple', 'orange'}
cart_items.discard("grape") # No error, even though grape isn't in the set
print(cart_items) # Output: {'apple', 'orange'}Utilisez discard() lorsque vous voulez vous assurer qu’un élément n’est pas dans l’ensemble, qu’il y ait été initialement ou non. Utilisez remove() lorsque l’absence de l’élément indique une condition d’erreur que vous souhaitez détecter.
17.2.5) Supprimer et renvoyer un élément arbitraire avec pop()
La méthode pop() supprime et renvoie un élément arbitraire de l’ensemble. Comme les ensembles ne sont pas ordonnés, vous ne pouvez pas prédire quel élément sera supprimé :
# Traiter une file de tâches en attente (l'ordre n'a pas d'importance)
pending_tasks = {"email", "report", "meeting", "review"}
# Traiter une tâche (peu importe laquelle)
task = pending_tasks.pop()
print(f"Processing: {task}") # Output: Processing: email (or another task)
print(f"Remaining: {pending_tasks}")
# Output: Remaining: {'report', 'meeting', 'review'} (without the popped task)Si vous appelez pop() sur un ensemble vide, cela lève une KeyError :
empty_set = set()
# empty_set.pop() # Raises: KeyError: 'pop from an empty set'La méthode pop() est utile lorsque vous devez traiter tous les éléments d’un ensemble mais que l’ordre n’a pas d’importance :
# Traiter tous les éléments d'un ensemble
items_to_process = {"item1", "item2", "item3"}
while items_to_process:
item = items_to_process.pop()
print(f"Processing {item}")
# Traiter l'élément...
print("All items processed")
# Output:
# Processing item1
# Processing item2
# Processing item3
# All items processed17.2.6) Supprimer tous les éléments avec clear()
La méthode clear() supprime tous les éléments d’un ensemble, le laissant vide :
# Réinitialiser les données d'une session
session_data = {"user_id", "timestamp", "ip_address"}
print(session_data) # Output: {'user_id', 'timestamp', 'ip_address'}
session_data.clear()
print(session_data) # Output: set()
print(len(session_data)) # Output: 0C’est plus efficace que de créer un nouvel ensemble vide si vous souhaitez réutiliser le même objet ensemble.
17.3) Opérations sur les ensembles : union, intersection, différence et différence symétrique
Les ensembles prennent en charge des opérations mathématiques qui vous permettent de combiner, comparer et analyser des collections efficacement. Ces opérations sont fondamentales en théorie des ensembles et ont de nombreuses applications pratiques en traitement de données.
17.3.1) Union : combiner des ensembles
Commençons par un scénario pratique pour comprendre pourquoi l’union est importante. Imaginez que vous gérez des inscriptions d’étudiants sur différents cours :
# Étudiants inscrits à différents cours
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Trouver tous les étudiants suivant l'un ou l'autre cours (ou les deux)
all_students = python_students | javascript_students
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}L’union (union) de deux ensembles contient tous les éléments qui apparaissent dans l’un ou l’autre ensemble (ou les deux). Python fournit deux manières de calculer les unions : l’opérateur | (montré ci-dessus) et la méthode union() :
# Même résultat avec la méthode union()
all_students = python_students.union(javascript_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david', 'eve'}La méthode union() peut accepter plusieurs ensembles en arguments, ce qui la rend pratique pour combiner des données provenant de nombreuses sources :
# Étudiants dans trois cours différents
python_students = {"alice", "bob"}
javascript_students = {"bob", "charlie"}
sql_students = {"charlie", "david"}
# Tous les étudiants sur l'ensemble des cours
all_students = python_students.union(javascript_students, sql_students)
print(all_students)
# Output: {'alice', 'bob', 'charlie', 'david'}Un autre exemple d’union est la combinaison de listes d’emails issues de différents départements :
# Combiner des listes d'emails de différents départements
marketing_contacts = {"alice@company.com", "bob@company.com"}
sales_contacts = {"bob@company.com", "charlie@company.com"}
support_contacts = {"david@company.com", "alice@company.com"}
# Tous les contacts uniques sur l'ensemble des départements
all_contacts = marketing_contacts | sales_contacts | support_contacts
print(f"Total unique contacts: {len(all_contacts)}")
# Output: Total unique contacts: 417.3.2) Intersection : trouver les éléments communs
Comprendre quels éléments apparaissent dans plusieurs ensembles est crucial pour de nombreuses tâches d’analyse de données. L’opération d’intersection (intersection) répond à la question : « Qu’ont ces ensembles en commun ? »
# Trouver les clients qui ont acheté les deux produits
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# Clients qui ont acheté les deux produits
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}L’intersection ne contient que les éléments qui apparaissent dans les deux ensembles. Vous pouvez aussi utiliser la méthode intersection(), qui accepte plusieurs ensembles :
# Trouver les étudiants inscrits aux trois cours
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "charlie", "david"}
sql_students = {"charlie", "eve", "bob"}
# Étudiants suivant les trois cours
all_three = python_students.intersection(javascript_students, sql_students)
print(all_three) # Output: {'bob', 'charlie'}Voici un cas d’usage pratique pour trouver des produits disponibles dans plusieurs entrepôts :
# Trouver les produits disponibles dans plusieurs entrepôts
warehouse_a = {"laptop", "mouse", "keyboard", "monitor"}
warehouse_b = {"mouse", "keyboard", "printer", "scanner"}
warehouse_c = {"keyboard", "monitor", "mouse", "desk"}
# Produits disponibles dans tous les entrepôts
available_everywhere = warehouse_a & warehouse_b & warehouse_c
print(f"Available in all locations: {available_everywhere}")
# Output: Available in all locations: {'mouse', 'keyboard'}17.3.3) Différence : trouver les éléments présents dans un ensemble mais pas dans un autre
Parfois, vous devez identifier ce qui est spécifique à une collection. L’opération de différence (difference) trouve les éléments qui sont dans le premier ensemble mais pas dans le second :
# Gestion d'inventaire : trouver des écarts
expected_items = {"item001", "item002", "item003", "item004"}
actual_items = {"item001", "item003", "item005"}
# Éléments manquants dans l'inventaire
missing = expected_items - actual_items
print(f"Missing items: {missing}")
# Output: Missing items: {'item002', 'item004'}
# Éléments inattendus dans l'inventaire
unexpected = actual_items - expected_items
print(f"Unexpected items: {unexpected}")
# Output: Unexpected items: {'item005'}Vous pouvez aussi utiliser la méthode difference() :
# Étudiants uniquement en cours Python (pas en JavaScript)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
python_only = python_students.difference(javascript_students)
print(python_only) # Output: {'alice', 'charlie'}Important : l’opération de différence n’est pas commutative — l’ordre compte :
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
# Étudiants en Python mais pas en JavaScript
python_only = python_students - javascript_students
print(f"Python only: {python_only}")
# Output: Python only: {'alice', 'charlie'}
# Étudiants en JavaScript mais pas en Python
javascript_only = javascript_students - python_students
print(f"JavaScript only: {javascript_only}")
# Output: JavaScript only: {'david', 'eve'}17.3.4) Différence symétrique : éléments dans l’un ou l’autre ensemble, mais pas dans les deux
La différence symétrique (symmetric difference) trouve les éléments qui sont dans l’un ou l’autre ensemble mais pas dans les deux. Cette opération est particulièrement utile pour identifier des changements entre deux versions :
# Comparer deux versions d'une configuration
old_settings = {"debug", "logging", "cache", "compression"}
new_settings = {"logging", "cache", "monitoring", "security"}
# Paramètres qui ont changé (ajoutés ou supprimés)
changes = old_settings ^ new_settings
print(f"Changed settings: {changes}")
# Output: Changed settings: {'debug', 'compression', 'monitoring', 'security'}
# Pour voir précisément ce qui a été ajouté vs supprimé :
removed = old_settings - new_settings
added = new_settings - old_settings
print(f"Removed: {removed}") # Output: Removed: {'debug', 'compression'}
print(f"Added: {added}") # Output: Added: {'monitoring', 'security'}Vous pouvez aussi utiliser la méthode symmetric_difference() :
# Étudiants dans exactement un seul cours (pas les deux)
python_students = {"alice", "bob", "charlie"}
javascript_students = {"bob", "david", "eve"}
one_course_only = python_students.symmetric_difference(javascript_students)
print(one_course_only)
# Output: {'alice', 'charlie', 'david', 'eve'}Contrairement à la différence, la différence symétrique est commutative — l’ordre n’a pas d’importance :
result1 = python_students ^ javascript_students
result2 = javascript_students ^ python_students
print(result1 == result2) # Output: True17.4) Relations de sous-ensemble et sur-ensemble (issubset, issuperset, isdisjoint)
Au-delà de la combinaison d’ensembles, nous devons souvent comprendre les relations entre eux. Python fournit des méthodes pour tester si un ensemble est contenu dans un autre, contient un autre, ou ne partage aucun élément avec un autre.
17.4.1) Tester les sous-ensembles avec issubset() et <=
Un ensemble A est un sous-ensemble (subset) de l’ensemble B si chaque élément de A est aussi dans B. En d’autres termes, B contient tous les éléments de A (et éventuellement davantage).
# Prérequis de cours
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# Vérifier si les compétences de base sont un sous-ensemble des compétences intermédiaires
print(basic_skills.issubset(intermediate_skills)) # Output: True
print(basic_skills <= intermediate_skills) # Output: True (same result)Un ensemble est toujours un sous-ensemble de lui-même :
skills = {"Python", "SQL", "JavaScript"}
print(skills.issubset(skills)) # Output: True
print(skills <= skills) # Output: TrueSi vous voulez tester un sous-ensemble strict (A est un sous-ensemble de B mais n’est pas égal à B), utilisez l’opérateur < :
basic_skills = {"reading", "writing"}
intermediate_skills = {"reading", "writing", "analysis"}
# Sous-ensemble strict : basic est un sous-ensemble de intermediate ET ils ne sont pas égaux
print(basic_skills < intermediate_skills) # Output: True
# Pas un sous-ensemble strict de lui-même (ils sont égaux)
print(basic_skills < basic_skills) # Output: FalseUn exemple pratique de test de sous-ensemble est la vérification des permissions ou des exigences :
# Système de permissions utilisateur
required_permissions = {"read", "write"}
user_permissions = {"read", "write", "delete", "admin"}
# Vérifier si l'utilisateur a toutes les permissions requises
if required_permissions.issubset(user_permissions):
print("Access granted")
else:
print("Access denied - missing permissions")
# Output: Access granted
# Un autre utilisateur avec des permissions insuffisantes
limited_user = {"read"}
if required_permissions.issubset(limited_user):
print("Access granted")
else:
missing = required_permissions - limited_user
print(f"Access denied - missing: {missing}")
# Output: Access denied - missing: {'write'}17.4.2) Tester les sur-ensembles avec issuperset() et >=
Un ensemble A est un sur-ensemble (superset) de l’ensemble B si A contient tous les éléments de B. C’est l’inverse de la relation de sous-ensemble — si A est un sous-ensemble de B, alors B est un sur-ensemble de A.
# Niveaux de compétences
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis", "research"}
# Vérifier si les compétences avancées sont un sur-ensemble des compétences de base
print(advanced_skills.issuperset(basic_skills)) # Output: True
print(advanced_skills >= basic_skills) # Output: True (same result)Comme pour les sous-ensembles, un ensemble est toujours un sur-ensemble de lui-même :
skills = {"Python", "SQL"}
print(skills.issuperset(skills)) # Output: TruePour un sur-ensemble strict (A est un sur-ensemble de B mais n’est pas égal à B), utilisez l’opérateur > :
basic_skills = {"reading", "writing"}
advanced_skills = {"reading", "writing", "analysis"}
# Sur-ensemble strict : advanced contient toutes les compétences de basic ET en a plus
print(advanced_skills > basic_skills) # Output: True
# Pas un sur-ensemble strict de lui-même
print(advanced_skills > advanced_skills) # Output: False17.4.3) Tester des ensembles disjoints avec isdisjoint()
Deux ensembles sont disjoints (disjoint) s’ils n’ont aucun élément en commun — leur intersection est vide. La méthode isdisjoint() renvoie True si les ensembles ne partagent aucun élément :
# Vérifier des conflits dans une planification
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "art", "music"}
# Vérifier s'il y a des conflits (même cours dans les deux sessions)
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: No scheduling conflictsQuand les ensembles ne sont pas disjoints :
morning_classes = {"math", "english", "history"}
afternoon_classes = {"science", "math", "music"}
if morning_classes.isdisjoint(afternoon_classes):
print("No scheduling conflicts")
else:
conflicts = morning_classes & afternoon_classes
print(f"Conflicts: {conflicts}")
# Output: Conflicts: {'math'}Les ensembles vides sont disjoints avec tous les ensembles (y compris d’autres ensembles vides) :
empty = set()
numbers = {1, 2, 3}
print(empty.isdisjoint(numbers)) # Output: True
print(empty.isdisjoint(empty)) # Output: True17.5) Quand utiliser des ensembles plutôt que des listes
Comprendre quand utiliser des ensembles plutôt que des listes est crucial pour écrire du code Python efficace. Bien que les deux stockent des collections d’éléments, ils ont des caractéristiques différentes qui rendent chacun adapté à des tâches différentes.
17.5.1) Utiliser des ensembles pour un test d’appartenance rapide
L’un des avantages les plus importants des ensembles est leur vitesse pour les tests d’appartenance. Vérifier si un élément existe dans un ensemble est bien plus rapide que dans une liste, surtout pour de grandes collections :
# Vérifier si un utilisateur est dans une grande collection
active_users_list = []
for i in range(10000):
active_users_list.append("user" + str(i))
# Avec une liste (lent pour de grandes collections)
print("user5000" in active_users_list) # Checks each element until found
active_users_set = set()
for i in range(10000):
active_users_set.add("user" + str(i))
# Avec un ensemble (rapide quelle que soit la taille)
print("user5000" in active_users_set) # Direct lookupBien que les deux produisent le même résultat, la version avec un ensemble est nettement plus rapide pour de grandes collections. Cela s’explique par le fait que les ensembles utilisent une table de hachage(hash table) en interne, permettant des recherches quasi instantanées quelle que soit la taille, tandis que les listes doivent vérifier chaque élément séquentiellement.
17.5.2) Utiliser des ensembles pour éliminer les doublons
Lorsque vous devez supprimer des doublons d’une collection, la conversion en ensemble est l’approche la plus simple :
# Supprimer des entrées en double provenant d'une saisie utilisateur
survey_responses = [
"yes", "no", "yes", "maybe", "yes", "no", "maybe", "yes"
]
# Obtenir les réponses uniques
unique_responses = set(survey_responses)
print(unique_responses) # Output: {'yes', 'no', 'maybe'}
# Si vous avez besoin de récupérer une liste (avec les doublons supprimés)
unique_list = list(unique_responses)
print(unique_list) # Output: ['yes', 'no', 'maybe'] (order may vary)17.5.3) Utiliser des ensembles pour les opérations mathématiques d’ensemble
Lorsque vous devez trouver des éléments communs, des différences ou des unions entre des collections, les ensembles fournissent des opérations claires et efficaces :
# Analyser des schémas d'achat clients
customers_product_a = {101, 102, 103, 104, 105}
customers_product_b = {103, 104, 105, 106, 107}
# Clients qui ont acheté les deux produits
both_products = customers_product_a & customers_product_b
print(f"Bought both: {both_products}")
# Output: Bought both: {103, 104, 105}
# Clients qui n'ont acheté que le produit A
only_a = customers_product_a - customers_product_b
print(f"Only product A: {only_a}")
# Output: Only product A: {101, 102}
# Tous les clients ayant acheté au moins un produit
all_customers = customers_product_a | customers_product_b
print(f"Total customers: {len(all_customers)}")
# Output: Total customers: 717.5.4) Utiliser des listes lorsque l’ordre compte
Les ensembles ne sont pas ordonnés, donc si la séquence des éléments est importante, vous devez utiliser une liste :
# WRONG - Order is not preserved with sets
task_order = {"wake up", "breakfast", "work", "lunch", "work", "dinner"}
print(task_order) # Order is unpredictable and "work" appears only once
# CORRECT - Use a list when order matters
task_order = ["wake up", "breakfast", "work", "lunch", "work", "dinner"]
print(task_order)
# Output: ['wake up', 'breakfast', 'work', 'lunch', 'work', 'dinner']17.5.5) Utiliser des listes lorsque les doublons sont significatifs
Si les valeurs dupliquées portent une information (comme la fréquence ou des occurrences multiples), utilisez une liste :
# Enregistrer des scores de quiz (les doublons montrent combien d'élèves ont obtenu chaque score)
quiz_scores = [85, 90, 85, 78, 90, 92, 85, 88]
# Avec une liste, on peut compter les occurrences
score_85_count = quiz_scores.count(85)
print(f"Students who scored 85: {score_85_count}")
# Output: Students who scored 85: 3
# Avec un ensemble, on perdrait cette information
unique_scores = set(quiz_scores)
print(unique_scores) # Output: {78, 85, 88, 90, 92}
# On ne peut pas savoir combien d'élèves ont obtenu chaque score17.5.6) Utiliser des listes lorsque vous avez besoin d’indexation
Les ensembles ne prennent pas en charge l’indexation parce qu’ils ne sont pas ordonnés. Si vous devez accéder à des éléments par position, utilisez une liste :
# WRONG - Sets don't support indexing
colors = {"red", "blue", "green"}
# first_color = colors[0] # Raises: TypeError: 'set' object is not subscriptable
# CORRECT - Use a list for indexed access
colors = ["red", "blue", "green"]
first_color = colors[0]
print(first_color) # Output: red17.6) Frozensets et ensembles immuables
Jusqu’à présent, nous avons travaillé avec des ensembles classiques, qui sont mutables — vous pouvez ajouter et supprimer des éléments après leur création. Python fournit également les frozensets(frozenset), qui sont des versions immuables des ensembles. Une fois créé, un frozenset ne peut pas être modifié.
17.6.1) Créer des frozensets
Vous créez un frozenset en utilisant le constructeur frozenset(), de la même manière que vous créez un ensemble classique avec set() :
# Créer un frozenset à partir d'une liste
colors = frozenset(["red", "blue", "green"])
print(colors) # Output: frozenset({'red', 'blue', 'green'})
print(type(colors)) # Output: <class 'frozenset'>
# Créer un frozenset à partir d'un tuple
numbers = frozenset((1, 2, 3, 4, 5))
print(numbers) # Output: frozenset({1, 2, 3, 4, 5})
# Créer un frozenset vide
empty = frozenset()
print(empty) # Output: frozenset()Comme les ensembles classiques, les frozensets éliminent automatiquement les doublons :
# Les doublons sont supprimés
values = frozenset([1, 2, 2, 3, 3, 3, 4])
print(values) # Output: frozenset({1, 2, 3, 4})17.6.2) Les frozensets sont immuables
Une fois créé, vous ne pouvez pas modifier un frozenset. Des méthodes comme add(), remove(), discard(), pop() et clear() n’existent pas pour les frozensets :
# Créer un frozenset
languages = frozenset(["Python", "JavaScript", "Java"])
# Tenter de modifier provoque une erreur
# languages.add("C++") # AttributeError: 'frozenset' object has no attribute 'add'
# languages.remove("Java") # AttributeError: 'frozenset' object has no attribute 'remove'Cette immuabilité est la caractéristique déterminante des frozensets. Si vous devez « modifier » un frozenset, vous devez en créer un nouveau :
# Frozenset original
original = frozenset([1, 2, 3])
# Créer un nouveau frozenset avec un élément supplémentaire
modified = frozenset(list(original) + [4])
print(original) # Output: frozenset({1, 2, 3})
print(modified) # Output: frozenset({1, 2, 3, 4})17.6.3) Les opérations d’ensemble fonctionnent avec les frozensets
Les frozensets prennent en charge toutes les mêmes opérations d’ensemble que les ensembles classiques (union, intersection, différence, etc.) :
# Opérations d'ensemble avec des frozensets
set_a = frozenset([1, 2, 3, 4])
set_b = frozenset([3, 4, 5, 6])
# Union
print(set_a | set_b) # Output: frozenset({1, 2, 3, 4, 5, 6})
# Intersection
print(set_a & set_b) # Output: frozenset({3, 4})
# Différence
print(set_a - set_b) # Output: frozenset({1, 2})
# Différence symétrique
print(set_a ^ set_b) # Output: frozenset({1, 2, 5, 6})Vous pouvez aussi mélanger des ensembles classiques et des frozensets dans des opérations :
regular_set = {1, 2, 3}
frozen_set = frozenset([3, 4, 5])
# Opérations entre un ensemble classique et un frozenset
result = regular_set | frozen_set
print(result) # Output: {1, 2, 3, 4, 5}
print(type(result)) # Output: <class 'set'> (result is a regular set)17.6.4) Pourquoi utiliser des frozensets ?
La raison principale d’utiliser des frozensets est qu’ils peuvent être utilisés comme clés de dictionnaire ou comme éléments dans d’autres ensembles, ce que les ensembles classiques ne peuvent pas faire :
# WRONG - Regular sets cannot be dictionary keys
# regular_set = {1, 2, 3}
# my_dict = {regular_set: "value"} # TypeError: unhashable type: 'set'
# CORRECT - Frozensets can be dictionary keys
frozen_set = frozenset([1, 2, 3])
my_dict = {frozen_set: "value"}
print(my_dict) # Output: {frozenset({1, 2, 3}): 'value'}
print(my_dict[frozen_set]) # Output: valueUn exemple pratique utilisant des frozensets comme clés de dictionnaire :
# Stocker des informations sur des paires de coordonnées
# Chaque coordonnée est un frozenset de valeurs (x, y)
location_data = {
frozenset([0, 0]): "origin",
frozenset([1, 0]): "east",
frozenset([1, 1]): "northeast"
}
# Rechercher un emplacement
point = frozenset([1, 0])
print(location_data[point]) # Output: eastLes frozensets peuvent aussi être des éléments dans d’autres ensembles :
# WRONG - Regular sets cannot be elements of sets
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# CORRECT - Frozensets can be elements of sets
set_of_frozensets = {
frozenset([1, 2]),
frozenset([3, 4]),
frozenset([5, 6])
}
print(set_of_frozensets)
# Output: {frozenset({1, 2}), frozenset({3, 4}), frozenset({5, 6})}Un exemple pratique représentant des groupes :
# Représenter des équipes où chaque équipe est un frozenset d'ID de joueurs
tournament_teams = {
frozenset([101, 102, 103]), # Team A
frozenset([201, 202, 203]), # Team B
frozenset([301, 302, 303]) # Team C
}
# Vérifier si une équipe spécifique est enregistrée
team_to_check = frozenset([101, 102, 103])
if team_to_check in tournament_teams:
print("Team is registered")
else:
print("Team not found")
# Output: Team is registered17.6.5) Convertir entre ensembles et frozensets
Vous pouvez facilement convertir entre des ensembles classiques et des frozensets :
# Convertir un ensemble classique en frozenset
regular = {1, 2, 3, 4}
frozen = frozenset(regular)
print(frozen) # Output: frozenset({1, 2, 3, 4})
# Convertir un frozenset en ensemble classique
frozen = frozenset([5, 6, 7, 8])
regular = set(frozen)
print(regular) # Output: {5, 6, 7, 8}
# Maintenant, on peut modifier l'ensemble classique
regular.add(9)
print(regular) # Output: {5, 6, 7, 8, 9}17.7) Types hachables et non hachables : ce qui peut être des clés de dictionnaire ou des éléments d’ensemble (et une brève note sur le hachage)
Tout au long de ce chapitre, nous avons vu que les ensembles peuvent contenir certains types d’objets mais pas d’autres. Par exemple, vous pouvez créer un ensemble d’entiers ou de chaînes, mais pas un ensemble de listes. Cette restriction existe parce que les éléments d’un ensemble (et les clés de dictionnaire, comme nous l’avons appris au chapitre 16) doivent être hachables(hashable).
17.7.1) Que signifie « hachable » ?
Un objet hachable(hashable) est un objet qui possède une valeur de hachage qui ne change jamais au cours de sa durée de vie. Python calcule cette valeur de hachage en utilisant une fonction intégrée appelée hash() :
# Les types hachables ont une valeur de hachage
print(hash(42)) # Output: 42
print(hash("Python")) # Output: (some large integer)
print(hash((1, 2, 3))) # Output: (some large integer)La valeur de hachage est un entier que Python utilise en interne pour localiser rapidement des objets dans des ensembles et des dictionnaires. Voyez cela comme une adresse ou un index qui aide Python à retrouver les choses efficacement.
Propriété clé : pour qu’un objet soit hachable, sa valeur de hachage doit rester constante pendant toute sa durée de vie. Cela signifie que l’objet lui-même doit être immuable — si l’objet pouvait changer, sa valeur de hachage devrait aussi changer, ce qui casserait les ensembles et les dictionnaires.
17.7.2) Les types immuables sont hachables
Tous les types intégrés immuables de Python sont hachables et peuvent être utilisés comme éléments d’ensemble ou clés de dictionnaire :
# Les entiers sont hachables
numbers = {1, 2, 3, 4, 5}
print(numbers) # Output: {1, 2, 3, 4, 5}
# Les chaînes sont hachables
words = {"apple", "banana", "cherry"}
print(words) # Output: {'apple', 'banana', 'cherry'}
# Les tuples sont hachables (s'ils ne contiennent que des éléments hachables)
coordinates = {(0, 0), (1, 1), (2, 2)}
print(coordinates) # Output: {(0, 0), (1, 1), (2, 2)}
# Les frozensets sont hachables
frozen_sets = {frozenset([1, 2]), frozenset([3, 4])}
print(frozen_sets) # Output: {frozenset({1, 2}), frozenset({3, 4})}
# Les booléens et None sont hachables
mixed = {True, False, None, 42, "text"}
print(mixed) # Output: {False, True, None, 42, 'text'}17.7.3) Les types mutables ne sont pas hachables
Les types mutables comme les listes, les ensembles classiques et les dictionnaires ne sont pas hachables parce que leur contenu peut changer :
# Les listes ne sont PAS hachables
# my_set = {[1, 2, 3]} # TypeError: unhashable type: 'list'
# Les ensembles classiques ne sont PAS hachables
# set_of_sets = {{1, 2}, {3, 4}} # TypeError: unhashable type: 'set'
# Les dictionnaires ne sont PAS hachables
# my_set = {{"key": "value"}} # TypeError: unhashable type: 'dict'Pourquoi la mutabilité compte-t-elle ? Considérez ce qui se passerait si nous pouvions ajouter une liste à un ensemble :
# Scénario hypothétique (cela ne fonctionne pas réellement)
# my_list = [1, 2, 3]
# my_set = {my_list} # Suppose this worked
#
# # Python computes hash based on [1, 2, 3]
# # Now we modify the list:
# my_list.append(4) # Now it's [1, 2, 3, 4]
#
# # The hash value would be wrong! The set would be corrupted.C’est pourquoi Python empêche les objets mutables d’être dans des ensembles ou d’être utilisés comme clés de dictionnaire — cela casserait la structure de données interne.
Confusion courante chez les débutants : même si les ensembles eux-mêmes sont mutables (vous pouvez ajouter et supprimer des éléments), les éléments doivent être immuables. Les débutants essaient parfois de modifier des objets après les avoir ajoutés à des ensembles, sans se rendre compte de cette distinction conceptuelle :
# Confusion courante : l'ensemble est mutable, mais les éléments doivent être immuables
# L'ensemble est mutable - vous pouvez changer son contenu
fruits = {'apple', 'banana'}
fruits.add('orange') # ✓ Works
fruits.remove('apple') # ✓ Works
# Mais les éléments doivent être immuables - ils ne peuvent pas être modifiés
my_list = [1, 2, 3]
# my_set = {my_list} # ✗ TypeError: unhashable type: 'list'
# Pourquoi ? Si vous pouviez modifier my_list après l'avoir ajoutée, la structure interne
# de l'ensemble serait corrompue.
# Cela fonctionne parce que les tuples sont immuables
my_tuple = (1, 2, 3)
my_set = {my_tuple} # ✓ Works - tuples can't be modified17.7.4) Le cas particulier des tuples
Les tuples ne sont hachables que si tous leurs éléments sont hachables. Un tuple contenant des objets mutables n’est pas hachable :
# Tuple avec uniquement des éléments immuables - hachable
good_tuple = (1, 2, "three")
my_set = {good_tuple} # Works: good_tuple is hashable
print(my_set) # Output: {(1, 2, 'three')}
# Tuple contenant une liste - PAS hachable
bad_tuple = (1, 2, [3, 4])
# my_set = {bad_tuple} # TypeError: unhashable type: 'list'Cela a du sens : même si le tuple lui-même est immuable (vous ne pouvez pas changer quels objets il contient), si l’un de ces objets est mutable, la « valeur » globale du tuple peut changer :
# Montrer pourquoi les tuples avec des éléments mutables ne peuvent pas être hachés
inner_list = [1, 2]
my_tuple = (inner_list, 3)
# La structure du tuple est fixe, mais la liste à l'intérieur peut changer
inner_list.append(3) # Now inner_list is [1, 2, 3]
# Le tuple « contient » maintenant des données différentes, mais c'est le même objet tuple17.7.5) Tester la hachabilité
Vous pouvez tester si un objet est hachable en essayant de calculer son hachage :
# Tester la hachabilité
def is_hashable(obj):
"""Check if an object is hashable."""
try:
hash(obj)
return True
except TypeError:
return False
# Tester différents types
print(is_hashable(42)) # Output: True
print(is_hashable("text")) # Output: True
print(is_hashable((1, 2, 3))) # Output: True
print(is_hashable([1, 2, 3])) # Output: False
print(is_hashable({1, 2, 3})) # Output: False
print(is_hashable({"key": "value"})) # Output: False17.7.6) Résumé des types hachables
Hachables (peuvent être des éléments d’ensemble ou des clés de dict) :
- Entiers :
42 - Flottants :
3.14 - Chaînes :
"text" - Tuples (si tous les éléments sont hachables) :
(1, 2, "three") - Frozensets :
frozenset([1, 2, 3]) - Booléens :
True,False - None :
None
Non hachables (ne peuvent pas être des éléments d’ensemble ou des clés de dict) :
- Listes :
[1, 2, 3] - Ensembles classiques :
{1, 2, 3} - Dictionnaires :
{"key": "value"} - Tuples contenant des éléments non hachables :
(1, [2, 3])
Comprendre la hachabilité vous aide à choisir les bonnes structures de données et à éviter des erreurs courantes lorsque vous travaillez avec des ensembles et des dictionnaires. Le principe clé est simple : si un objet peut changer, il ne peut pas être haché ; s’il ne peut pas être haché, il ne peut pas être dans un ensemble ni être utilisé comme clé de dictionnaire.