Aller au contenu

Les compréhensions

Introduction

En Python, les compréhensions permettent de créer une collection à partir d’une structure de données ou d'objets itérable, en regroupant le parcours des éléments, leur transformation et leur éventuel filtrage et ordonnancement dans une même expression.

Pour un développeur C#, ces opérations peuvent être rapprochées de LINQ : Select pour transformer les éléments, Where pour les filtrer et SelectMany pour parcourir des collections imbriquées. Les exemples C# suivants impliquent que l’espace de noms System.Linq est importé.

Les collections pouvant être parcourues

Une compréhension peut parcourir tout objet itérable, notamment :

  • Une liste.
  • Un tuple.
  • Un ensemble.
  • Un dictionnaire.
  • Une chaîne de caractères.
  • Une collection d’objets.
  • Une séquence produite par range.
  • Un générateur.

Cette possibilité se rapproche de LINQ to Objects, dont les opérateurs s'appliquent à des sources implémentant l'interface IEnumerable<T>.

Le type de la source ne détermine pas celui du résultat. Par exemple, nous pouvons parcourir un tuple et produire une liste :

numbers = (1, 2, 3)

doubled_numbers = [number * 2 for number in numbers]

print(doubled_numbers)
# [2, 4, 6]

Les compréhensions de liste

Du traitement itératif à une compréhension

Pour créer une liste contenant le double de chaque nombre, nous pouvons écrire une boucle.

En Python :

numbers = [12, 7, 25, 8, 14, 3, 19]

doubled_numbers = []

for number in numbers:
    doubled_numbers.append(number * 2)

En C# :

var numbers = new List<int> { 12, 7, 25, 8, 14, 3, 19 };

var doubledNumbers = new List<int>();

foreach (int number in numbers)
{
    doubledNumbers.Add(number * 2);
}

En Python, une compréhension permet d’obtenir la liste souhaitée :

doubled_numbers = [number * 2 for number in numbers]

print(doubled_numbers)
# [24, 14, 50, 16, 28, 6, 38]

En C#, nous pouvons utiliser Select, puis ToList :

var doubledNumbers = numbers
    .Select(number => number * 2)
    .ToList();

Syntaxe générale

[expression for element in iterable]

Chaque partie joue un rôle précis :

Élément Rôle
iterable Source à parcourir
element Variable recevant chaque élément
expression Valeur à ajouter à la nouvelle liste

Dans l’expression suivante :

[number * 2 for number in numbers]

La partie for number in numbers parcourt la source. L’expression number * 2 transforme chaque élément, comme la lambda transmise à la méthode Select.

Comprendre la répétition de la variable

Considérons cette compréhension :

copied_numbers = [number for number in numbers]

Les deux occurrences de number ont des rôles différents :

  • Dans for number in numbers, number est la variable de parcours.
  • Le premier number désigne la valeur à ajouter à la nouvelle liste.

En C#, une projection identique s’écrit :

var copiedNumbers = numbers
    .Select(number => number)
    .ToList();

Pour simplement copier cette liste, numbers.ToList() est suffisant. Dans les deux langages, la création d’une nouvelle liste ne clone pas les objets qu’elle contient.

Filtrer les éléments

Une condition placée après le for permet de conserver uniquement certains éléments.

Syntaxe

[expression for element in iterable if condition]

Exemple

Conservons uniquement les nombres pairs.

En Python :

even_numbers = [
    number
    for number in numbers
    if number % 2 == 0
]

print(even_numbers)
# [12, 8, 14]

En C# :

var evenNumbers = numbers
    .Where(number => number % 2 == 0)
    .ToList();

La compréhension Python se lit ainsi :

Pour chaque nombre de numbers, s’il est pair, ajouter ce nombre à la nouvelle liste.

L’opérateur % calcule le reste d’une division. Un entier est pair lorsque le reste de sa division par 2 vaut zéro.

Combiner filtrage et transformation

Conservons les nombres pairs, puis calculons leur double.

En Python :

doubled_even_numbers = [
    number * 2
    for number in numbers
    if number % 2 == 0
]

print(doubled_even_numbers)
# [24, 16, 28]

En C# :

var doubledEvenNumbers = numbers
    .Where(number => number % 2 == 0)
    .Select(number => number * 2)
    .ToList();

La condition est évaluée avant l’expression produisant le résultat.

Plusieurs conditions peuvent être combinées avec and et or, qui correspondent ici aux opérateurs C# && et || :

selected_numbers = [
    number
    for number in numbers
    if number % 2 == 0 and number > 10
]
var selectedNumbers = numbers
    .Where(number => number % 2 == 0 && number > 10)
    .ToList();

Choisir une valeur selon une condition

Une expression conditionnelle permet de produire une valeur différente selon une condition, sans exclure d’éléments.

En Python :

labels = [
    "pair" if number % 2 == 0 else "impair"
    for number in numbers
]

En C# :

var labels = numbers
    .Select(number => number % 2 == 0 ? "pair" : "impair")
    .ToList();

L’expression Python :

value_if_true if condition else value_if_false

correspond à l’expression conditionnelle C# :

condition ? valueIfTrue : valueIfFalse

La position du if dans la compréhension permet de distinguer deux usages :

Syntaxe Python Usage
[expression for x in source if condition] Filtrer les éléments
[a if condition else b for x in source] Choisir la valeur "produite" pour chaque élément

Parcourir une collection d’objets

Les compréhensions permettent d’accéder aux attributs et aux méthodes des objets parcourus.

En Python :

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

products = [
    Product("Clavier", 49.90),
    Product("Souris", 24.90),
    Product("Écran", 199.00)
]

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

print(affordable_names)
# ['Clavier', 'Souris']

En C#, avec une classe Product exposant les propriétés Name et Price :

var affordableNames = products
    .Where(product => product.Price < 100)
    .Select(product => product.Name)
    .ToList();

Pour conserver les objets eux-mêmes, nous utilisons directement la variable de parcours :

affordable_products = [
    product
    for product in products
    if product.price < 100
]
var affordableProducts = products
    .Where(product => product.Price < 100)
    .ToList();

La nouvelle liste contient les objets existants, sans les cloner.

Les compréhensions d’ensemble

Un ensemble contient des éléments uniques. Une compréhension d’ensemble utilise des accolades :

{expression for element in iterable if condition}

En Python :

numbers = [1, -2, 2, -3, 3]

absolute_numbers = {abs(number) for number in numbers}

print(sorted(absolute_numbers))
# [1, 2, 3]

En C# :

var numbers = new List<int> { 1, -2, 2, -3, 3 };

var absoluteNumbers = numbers
    .Select(number => Math.Abs(number))
    .ToHashSet();

Les valeurs absolues sont calculées, puis les doublons sont éliminés.

Un ensemble Python ne garantit pas l’ordre d’itération. Dans l’exemple, la fonction sorted est utilisée pour obtenir un affichage ordonné.

Les compréhensions de dictionnaire

Syntaxe

Une compréhension de dictionnaire produit des couples clé-valeur :

{key_expression: value_expression for element in iterable}

Créer des associations

En Python :

squares = {
    number: number ** 2
    for number in range(1, 6)
}

print(squares)
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

En C# :

var squares = Enumerable.Range(1, 5)
    .ToDictionary(
        number => number,
        number => number * number);

La fonction range(1, 6) produit les entiers de 1 à 5, la borne supérieure étant exclue. Enumerable.Range(1, 5) reçoit une valeur de départ et un nombre d’éléments.

Transformer un dictionnaire existant

En Python, items() permet de parcourir les couples clé-valeur :

prices = {
    "Clavier": 50,
    "Souris": 25,
    "Écran": 200
}

discounted_prices = {
    name: price * 0.9
    for name, price in prices.items()
    if price < 100
}

print(discounted_prices)
# {'Clavier': 45.0, 'Souris': 22.5}

En C# :

var discountedPrices = prices
    .Where(entry => entry.Value < 100)
    .ToDictionary(
        entry => entry.Key,
        entry => entry.Value * 0.9);

Attention aux clés identiques

Une différence existe avec ToDictionary.

En Python, si plusieurs éléments produisent la même clé, la dernière valeur produite est conservée :

words = ["chat", "lion", "maison"]

words_by_length = {
    len(word): word
    for word in words
}

print(words_by_length)
# {4: 'lion', 6: 'maison'}

En C#, ToDictionary lève une exception lorsque plusieurs éléments produisent une même clé selon le comparateur utilisé.

Les parcours imbriqués

Une compréhension peut contenir plusieurs clauses for. Leur ordre correspond à celui des boucles imbriquées.

Créer des combinaisons

En Python :

colors = ["rouge", "bleu"]
sizes = ["S", "M"]

combinations = [
    (color, size)
    for color in colors
    for size in sizes
]

print(combinations)
# [('rouge', 'S'), ('rouge', 'M'), ('bleu', 'S'), ('bleu', 'M')]

En C#, la syntaxe de requête LINQ présente une structure comparable :

var combinations = (
    from color in colors
    from size in sizes
    select (color, size)
).ToList();

Aplatir des collections

En Python :

matrix = [
    [1, 2, 3],
    [4, 5, 6]
]

flattened = [
    number
    for row in matrix
    for number in row
]

print(flattened)
# [1, 2, 3, 4, 5, 6]

En C#, cette opération correspond à SelectMany :

var flattened = matrix
    .SelectMany(row => row)
    .ToList();

Les expressions génératrices

Produire les valeurs à la demande

Une compréhension de liste construit immédiatement la liste obtenue.

Une expression génératrice utilise des parenthèses et produit les valeurs au fur et à mesure du parcours :

squares = (
    number ** 2
    for number in range(1, 6)
)

for square in squares:
    print(square)

Ce fonctionnement se rapproche de l’exécution différée de la méthode Select en LINQ :

var squares = Enumerable.Range(1, 5)
    .Select(number => number * number);

foreach (int square in squares)
{
    Console.WriteLine(square);
}

Dans ces deux exemples, les carrés sont calculés pendant le parcours.

Comprendre les parcours successifs

Un générateur de données numériques en Python est consommé pendant son parcours. Une fois le traitement terminé, il ne produit plus de valeurs, même s'il est exécuté une seconde fois :

squares = (number ** 2 for number in range(1, 4))

print(list(squares))
# [1, 4, 9]

print(list(squares))
# []

Une requête LINQ to Object sur une source réitérable, comme une liste, peut généralement être parcourue plusieurs fois. Chaque parcours exécute de nouveau la requête.

Pour conserver les résultats en Python, nous pouvons les stocker dans une liste, et réutiliser cette liste dans les compréhensions.

Utiliser un générateur pour une agrégation

Une expression génératrice peut alimenter directement une fonction comme sum, sans créer de liste intermédiaire :

total = sum(number ** 2 for number in range(1, 6))

print(total)
# 55

En C# :

int total = Enumerable.Range(1, 5)
    .Sum(number => number * number);

Construire un tuple

Les parenthèses d’une expression génératrice ne créent pas un tuple et il n’existe pas de compréhension de tuple. Pour construire un tuple, nous utilisons la fonction tuple :

doubled_numbers = tuple(
    number * 2 for number in [1, 2, 3]
)

print(doubled_numbers)
# (2, 4, 6)

Un tuple Python est une séquence immuable. Il ne correspond pas à 100% aux tuples .NET, dont chaque position possède un type déterminé.