31 mars 2025
Les développeurs travaillant dans l'écosystème Microsoft .NET s'appuient souvent fortement sur Language Integrated Query (LINQ). Cette fonctionnalité puissante permet d'interroger diverses sources de données — collections, bases de données, XML — en utilisant une syntaxe qui semble native à C# ou VB.NET. Elle transforme la manipulation de données, passant de boucles impératives à des instructions déclaratives, améliorant ainsi la lisibilité et la concision du code. Mais que se passe-t-il lorsque les développeurs sortent de la sphère .NET ? Comment les programmeurs obtiennent-ils des capacités expressives d'interrogation de données similaires dans des langages comme Python, Java ou C++ ? Heureusement, les concepts fondamentaux qui sous-tendent LINQ ne sont pas exclusifs à .NET, et des équivalents et alternatives robustes existent dans tout le paysage de la programmation.
Avant d'explorer les alternatives, rappelons brièvement ce que propose LINQ. Introduit avec .NET Framework 3.5, LINQ fournit une manière unifiée d'interroger les données quelle que soit leur origine. Il intègre les expressions de requête directement dans le langage, ressemblant aux instructions SQL. Les fonctionnalités clés incluent :
Where
(filtrage), Select
(projection/mapping), OrderBy
(tri), GroupBy
(regroupement), Join
, Aggregate
, et plus encore.La commodité d'écrire var results = collection.Where(x => x.IsValid).Select(x => x.Name);
est indéniable. Voyons comment d'autres langages abordent des tâches similaires.
Python offre plusieurs mécanismes, allant des fonctionnalités intégrées idiomatiques aux bibliothèques dédiées, fournissant des capacités similaires à LINQ. Ces approches permettent aux développeurs d'effectuer le filtrage, le mapping et l'agrégation de manière concise et lisible.
La manière la plus idiomatique en Python pour réaliser un filtrage simple (Where
) et un mapping (Select
) passe souvent par les listes en compréhension ou les expressions génératrices.
numbers = [1, 2, 3, 4, 5, 6]
# LINQ : numbers.Where(n => n % 2 == 0).Select(n => n * n)
squared_evens = [n * n for n in numbers if n % 2 == 0]
# Résultat : [4, 16, 36]
numbers = [1, 2, 3, 4, 5, 6]
# LINQ : numbers.Where(n => n % 2 == 0).Select(n => n * n)
squared_evens_gen = (n * n for n in numbers if n % 2 == 0)
# Pour obtenir les résultats, vous itérez dessus (ex: list(squared_evens_gen))
# Les valeurs ne sont calculées qu'au besoin pendant l'itération.
itertools
De nombreux opérateurs LINQ standard ont des équivalents directs ou proches dans les fonctions intégrées de Python ou dans le puissant module itertools
:
any()
, all()
: Correspondent directement à Any
et All
de LINQ pour vérifier des conditions sur les éléments.
fruit = ['apple', 'orange', 'banana']
# LINQ : fruit.Any(f => f.Contains("a"))
any_a = any("a" in f for f in fruit) # True
# LINQ : fruit.All(f => f.Length > 3)
all_long = all(len(f) > 3 for f in fruit) # True
min()
, max()
, sum()
: Similaires aux méthodes d'agrégation de LINQ. Peuvent opérer directement sur des itérables ou prendre une expression génératrice.
numbers = [1, 5, 2, 8, 3]
# LINQ : numbers.Max()
maximum = max(numbers) # 8
# LINQ : numbers.Where(n => n % 2 != 0).Sum()
odd_sum = sum(n for n in numbers if n % 2 != 0) # 1 + 5 + 3 = 9
filter()
, map()
: Homologues fonctionnels de Where
et Select
. Ils retournent des itérateurs en Python 3, favorisant l'évaluation paresseuse.
numbers = [1, 2, 3, 4]
# LINQ : numbers.Where(n => n > 2)
filtered_iter = filter(lambda n: n > 2, numbers) # produit 3, 4 lors de l'itération
# LINQ : numbers.Select(n => n * 2)
mapped_iter = map(lambda n: n * 2, numbers) # produit 2, 4, 6, 8 lors de l'itération
sorted()
: Correspond à OrderBy
. Prend une fonction key
optionnelle pour spécifier les critères de tri et retourne une nouvelle liste triée.
fruit = ['pear', 'apple', 'banana']
# LINQ : fruit.OrderBy(f => f.Length)
sorted_fruit = sorted(fruit, key=len) # ['pear', 'apple', 'banana']
itertools.islice(iterable, stop)
ou itertools.islice(iterable, start, stop[, step])
: Implémente Take
et Skip
. Retourne un itérateur.
from itertools import islice
numbers = [0, 1, 2, 3, 4, 5]
# LINQ : numbers.Take(3)
first_three = list(islice(numbers, 3)) # [0, 1, 2]
# LINQ : numbers.Skip(2)
skip_two = list(islice(numbers, 2, None)) # [2, 3, 4, 5]
# LINQ : numbers.Skip(1).Take(2)
skip_one_take_two = list(islice(numbers, 1, 3)) # [1, 2]
itertools.takewhile()
, itertools.dropwhile()
: Équivalents à TakeWhile
et SkipWhile
, opérant sur la base d'un prédicat.
from itertools import takewhile, dropwhile
numbers = [2, 4, 6, 7, 8, 10]
# LINQ : numbers.TakeWhile(n => n % 2 == 0)
take_evens = list(takewhile(lambda n: n % 2 == 0, numbers)) # [2, 4, 6]
# LINQ : numbers.SkipWhile(n => n % 2 == 0)
skip_evens = list(dropwhile(lambda n: n % 2 == 0, numbers)) # [7, 8, 10]
itertools.groupby()
: Similaire à GroupBy
, mais nécessite que l'itérable d'entrée soit trié d'abord par la clé de regroupement pour que les éléments soient regroupés correctement. Retourne un itérateur produisant des paires (clé, itérateur_groupe)
.
from itertools import groupby
fruit = ['apple', 'apricot', 'banana', 'blueberry', 'cherry']
# DOIT être trié d'abord par la clé pour que groupby fonctionne comme attendu dans la plupart des cas
keyfunc = lambda f: f[0] # Regrouper par première lettre
sorted_fruit = sorted(fruit, key=keyfunc)
# LINQ : fruit.GroupBy(f => f[0])
grouped_fruit = groupby(sorted_fruit, key=keyfunc)
for key, group_iter in grouped_fruit:
print(f"{key}: {list(group_iter)}")
# Sortie :
# a: ['apple', 'apricot']
# b: ['banana', 'blueberry']
# c: ['cherry']
set()
: Peut être utilisé pour Distinct
, mais ne préserve pas l'ordre original.
numbers = [1, 2, 2, 3, 1, 4, 3]
# LINQ : numbers.Distinct()
distinct_numbers_set = set(numbers) # Ordre non garanti, ex: {1, 2, 3, 4}
distinct_numbers_list = list(distinct_numbers_set) # ex: [1, 2, 3, 4]
# Pour des éléments distincts préservant l'ordre :
seen = set()
distinct_ordered = [x for x in numbers if not (x in seen or seen.add(x))] # [1, 2, 3, 4]
py-linq
Pour les développeurs qui préfèrent la syntaxe spécifique de chaînage de méthodes et les conventions de nommage de LINQ .NET, la bibliothèque py-linq
offre un portage direct. Après l'installation (pip install py-linq
), vous enveloppez votre collection dans un objet Enumerable
.
from py_linq import Enumerable
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f'{self.name} ({self.age})'
people = [Person('Alice', 30), Person('Bob', 20), Person('Charlie', 25)]
e = Enumerable(people)
# LINQ : people.Where(p => p.age > 21).OrderBy(p => p.name).Select(p => p.name)
results = e.where(lambda p: p.age > 21)\
.order_by(lambda p: p.name)\
.select(lambda p: p.name)\
.to_list()
# Résultat : ['Alice', 'Charlie']
# Exemple de Count
# LINQ : people.Count(p => p.age < 25)
young_count = e.count(lambda p: p.age < 25) # 1 (Bob)
La bibliothèque py-linq
implémente une grande partie des opérateurs de requête standard, offrant une interface familière pour ceux qui transitent depuis ou travaillent parallèlement au développement .NET.
La bibliothèque pipe
est une autre alternative, offrant une approche fonctionnelle utilisant l'opérateur pipe (|
) pour chaîner les opérations, que certains développeurs trouvent très lisible et expressive pour les flux de données complexes.
Depuis Java 8, l'équivalent principal et idiomatique de LINQ en Java est sans équivoque l'API Streams (java.util.stream
). Elle fournit une manière fluide et déclarative de traiter des séquences d'éléments, reflétant étroitement la philosophie et les capacités de LINQ, rendant les fonctionnalités de type LINQ une réalité au sein de la bibliothèque standard.
list.stream()
), les tableaux (Arrays.stream(array)
), les canaux d'E/S, ou les fonctions génératrices (Stream.iterate
, Stream.generate
).filter
(Where), map
(Select), sorted
(OrderBy), distinct
, limit
(Take), skip
(Skip), reduce
(Aggregate), collect
(ToList, ToDictionary, etc.).filter
, map
, sorted
) retournent un nouveau stream, leur permettant d'être chaînées pour former un pipeline représentant la requête.limit
, anyMatch
, findFirst
) peuvent arrêter le traitement prématurément une fois le résultat déterminé, améliorant l'efficacité.collect
, count
, sum
, findFirst
, anyMatch
) ou un effet de bord (par ex. forEach
).Voyons les équivalents LINQ en utilisant les Streams Java :
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static java.util.Comparator.comparing;
// Classe de données exemple
class Transaction {
int id; String type; int value;
Transaction(int id, String type, int value) { this.id = id; this.type = type; this.value = value; }
int getId() { return id; }
String getType() { return type; }
int getValue() { return value; }
@Override public String toString() { return "ID:" + id + " Type:" + type + " Value:" + value; }
}
public class StreamExample {
public static void main(String[] args) {
List<Transaction> transactions = Arrays.asList(
new Transaction(1, "GROCERY", 50),
new Transaction(2, "UTILITY", 150),
new Transaction(3, "GROCERY", 75),
new Transaction(4, "RENT", 1200),
new Transaction(5, "GROCERY", 25)
);
// --- Filtrage (Where) ---
// LINQ : transactions.Where(t => t.getType() == "GROCERY")
List<Transaction> groceryTransactions = transactions.stream()
.filter(t -> "GROCERY".equals(t.getType()))
.collect(Collectors.toList());
// Résultat : Contient les transactions avec les ID 1, 3, 5
// --- Mapping (Select) ---
// LINQ : transactions.Select(t => t.getId())
List<Integer> transactionIds = transactions.stream()
.map(Transaction::getId) // Utilisation de référence de méthode
.collect(Collectors.toList());
// Résultat : [1, 2, 3, 4, 5]
// --- Tri (OrderBy) ---
// LINQ : transactions.OrderByDescending(t => t.getValue())
List<Transaction> sortedByValueDesc = transactions.stream()
.sorted(comparing(Transaction::getValue).reversed())
.collect(Collectors.toList());
// Résultat : Transactions triées par valeur décroissante : [ID:4, ID:2, ID:3, ID:1, ID:5]
// --- Combinaison d'opérations ---
// Trouver les ID des transactions d'épicerie, triés par valeur décroissante
// LINQ : transactions.Where(t => t.getType() == "GROCERY").OrderByDescending(t => t.getValue()).Select(t => t.getId())
List<Integer> groceryIdsSortedByValueDesc = transactions.stream()
.filter(t -> "GROCERY".equals(t.getType())) // Where
.sorted(comparing(Transaction::getValue).reversed()) // OrderByDescending
.map(Transaction::getId) // Select
.collect(Collectors.toList()); // Exécuter et collecter
// Résultat : [3, 1, 5] (ID correspondant aux valeurs 75, 50, 25)
// --- Autres opérations courantes ---
// AnyMatch
// LINQ : transactions.Any(t => t.getValue() > 1000)
boolean hasLargeTransaction = transactions.stream()
.anyMatch(t -> t.getValue() > 1000); // true (transaction RENT)
// Équivalent FindFirst / FirstOrDefault
// LINQ : transactions.FirstOrDefault(t => t.getType() == "UTILITY")
Optional<Transaction> firstUtility = transactions.stream()
.filter(t -> "UTILITY".equals(t.getType()))
.findFirst(); // Retourne un Optional contenant la transaction ID:2
firstUtility.ifPresent(t -> System.out.println("Trouvé : " + t)); // Affiche la transaction trouvée si présente
// Count
// LINQ : transactions.Count(t => t.getType() == "GROCERY")
long groceryCount = transactions.stream()
.filter(t -> "GROCERY".equals(t.getType()))
.count(); // 3
// Sum (utilisation de streams numériques spécialisés pour l'efficacité)
// LINQ : transactions.Sum(t => t.getValue())
int totalValue = transactions.stream()
.mapToInt(Transaction::getValue) // Convertir en IntStream
.sum(); // 1500
System.out.println("Valeur totale : " + totalValue);
}
}
Les Streams Java peuvent être facilement parallélisés pour des gains de performance potentiels sur les processeurs multi-cœurs simplement en remplaçant .stream()
par .parallelStream()
. L'API Streams gère en interne la décomposition des tâches et la gestion des threads.
// Exemple : Filtrage et mapping parallèles
List<Integer> parallelResult = transactions.parallelStream() // Utiliser un stream parallèle
.filter(t -> t.getValue() > 100) // Traité en parallèle
.map(Transaction::getId) // Traité en parallèle
.collect(Collectors.toList()); // Combine les résultats
// Résultat : [2, 4] (L'ordre peut varier par rapport au stream séquentiel avant la collecte)
Notez que la parallélisation introduit un surcoût et n'est pas toujours plus rapide, en particulier pour les opérations simples ou les petits jeux de données. Le benchmarking est recommandé.
Bien que les Streams Java 8 soient l'équivalent standard et généralement préféré de LINQ en Java, d'autres bibliothèques existent :
C++ ne dispose pas d'une fonctionnalité de requête intégrée au langage directement comparable à LINQ de .NET ou aux Streams de Java. Cependant, les développeurs recherchant un équivalent LINQ en C++ ou des moyens d'implémenter les motifs LINQ en C++ peuvent obtenir des résultats similaires en utilisant une combinaison de fonctionnalités de la bibliothèque standard, de bibliothèques tierces puissantes et d'idiomes C++ modernes.
Les en-têtes <algorithm>
et <numeric>
fournissent une boîte à outils fondamentale de fonctions qui opèrent sur des plages d'itérateurs (begin
, end
). Ce sont les blocs de construction pour la manipulation de données en C++.
#include <vector>
#include <numeric>
#include <algorithm>
#include <iostream>
#include <string>
#include <iterator> // Pour std::back_inserter
struct Product {
int id;
double price;
std::string category;
};
int main() {
std::vector<Product> products = {
{1, 10.0, "A"}, {2, 25.0, "B"}, {3, 5.0, "A"}, {4, 30.0, "A"}
};
// --- Filtrage (Where) ---
// LINQ : products.Where(p => p.category == "A")
std::vector<Product> categoryA;
std::copy_if(products.begin(), products.end(), std::back_inserter(categoryA),
[](const Product& p){ return p.category == "A"; });
// categoryA contient maintenant les produits avec les ID 1, 3, 4
// --- Mapping (Select) ---
// LINQ : products.Select(p => p.price)
std::vector<double> prices;
prices.reserve(products.size()); // Réserver de l'espace
std::transform(products.begin(), products.end(), std::back_inserter(prices),
[](const Product& p){ return p.price; });
// prices contient maintenant [10.0, 25.0, 5.0, 30.0]
// --- Tri (OrderBy) ---
// LINQ : products.OrderBy(p => p.price)
// Note : std::sort modifie le conteneur original
std::vector<Product> sortedProducts = products; // Créer une copie pour trier
std::sort(sortedProducts.begin(), sortedProducts.end(),
[](const Product& a, const Product& b){ return a.price < b.price; });
// sortedProducts est maintenant : [ {3, 5.0}, {1, 10.0}, {2, 25.0}, {4, 30.0} ]
// --- Agrégation (Sum) ---
// LINQ : products.Where(p => p.category == "A").Sum(p => p.price)
double sumCategoryA = std::accumulate(products.begin(), products.end(), 0.0,
[](double current_sum, const Product& p){
return (p.category == "A") ? current_sum + p.price : current_sum;
});
// sumCategoryA = 10.0 + 5.0 + 30.0 = 45.0
// --- Recherche (équivalent FirstOrDefault) ---
// LINQ : products.FirstOrDefault(p => p.id == 3)
auto found_it = std::find_if(products.begin(), products.end(),
[](const Product& p){ return p.id == 3; });
if (found_it != products.end()) {
std::cout << "Produit trouvé avec ID 3, prix : " << found_it->price << std::endl;
} else {
std::cout << "Produit avec ID 3 non trouvé." << std::endl;
}
return 0;
}
Bien que puissants et efficaces, l'utilisation directe des algorithmes STL peut être verbeuse. Chaîner les opérations nécessite souvent de créer des conteneurs intermédiaires ou d'employer des techniques de composition de foncteurs plus complexes.
Les bibliothèques C++ modernes comme range-v3
d'Eric Niebler (qui a fortement influencé le standard std::ranges
introduit en C++20) fournissent une syntaxe composable basée sur le pipe (|
) qui est beaucoup plus proche dans l'esprit de LINQ ou des Streams Java.
#include <vector>
#include <string>
#include <iostream>
#ifdef USE_RANGES_V3 // Définir ceci si vous utilisez range-v3, sinon utilisez std::ranges
#include <range/v3/all.hpp>
namespace ranges = ::ranges;
#else // En supposant C++20 ou ultérieur avec support de <ranges>
#include <ranges>
#include <numeric> // Pour accumulate avec ranges
namespace ranges = std::ranges;
namespace views = std::views;
#endif
// En supposant la structure Product de l'exemple précédent...
int main() {
std::vector<Product> products = {
{1, 10.0, "A"}, {2, 25.0, "B"}, {3, 5.0, "A"}, {4, 30.0, "A"}
};
// LINQ : products.Where(p => p.category == "A").Select(p => p.price).Sum()
auto categoryAView = products
| ranges::views::filter([](const Product& p){ return p.category == "A"; })
| ranges::views::transform([](const Product& p){ return p.price; });
#ifdef USE_RANGES_V3
double sumCategoryA_ranges = ranges::accumulate(categoryAView, 0.0);
#else // C++20 std::ranges nécessite begin()/end() explicites pour accumulate
double sumCategoryA_ranges = std::accumulate(categoryAView.begin(), categoryAView.end(), 0.0);
#endif
std::cout << "Somme Catégorie A (ranges) : " << sumCategoryA_ranges << std::endl; // 45.0
// LINQ : products.Where(p => p.price > 15).OrderBy(p => p.id).Select(p => p.id)
// Note : Le tri avec ranges nécessite typiquement de collecter d'abord dans un conteneur,
// ou d'utiliser des actions/algorithmes de range spécifiques si disponibles et appropriés.
auto expensiveProducts = products
| ranges::views::filter([](const Product& p){ return p.price > 15.0; });
// Collecter dans un vecteur pour trier
std::vector<Product> expensiveVec;
#ifdef USE_RANGES_V3
ranges::copy(expensiveProducts, std::back_inserter(expensiveVec));
#else
ranges::copy(expensiveProducts.begin(), expensiveProducts.end(), std::back_inserter(expensiveVec));
#endif
ranges::sort(expensiveVec, [](const Product& a, const Product& b){ return a.id < b.id; }); // Trier le vecteur
auto ids_expensive_sorted = expensiveVec
| ranges::views::transform([](const Product& p){ return p.id; }); // Créer une vue des ID
std::cout << "ID Produits Chers (Triés) : ";
for(int id : ids_expensive_sorted) { // Itérer sur la vue finale
std::cout << id << " "; // 2 4
}
std::cout << std::endl;
return 0;
}
Les bibliothèques de plages offrent une expressivité, une paresse (via les vues) et une composabilité significativement améliorées par rapport aux algorithmes STL traditionnels, ce qui en fait des concurrents sérieux en tant qu'équivalent LINQ en C++.
Plusieurs bibliothèques tierces visent spécifiquement à imiter directement la syntaxe LINQ en C++ :
from
, where
, select
, orderBy
, etc.) avec un style de chaînage de méthodes ou de syntaxe de requête familier.Ces bibliothèques peuvent être attrayantes pour les développeurs déjà à l'aise avec C# LINQ. Cependant, elles introduisent des dépendances externes et pourraient ne pas toujours s'intégrer aussi harmonieusement avec les pratiques C++ standard ou offrir les mêmes optimisations de performance potentielles que les algorithmes standard ou les bibliothèques de plages bien établies.
Le concept fondamental d'interroger des collections de manière déclarative est répandu :
filter()
, map()
, reduce()
, sort()
, find()
, some()
, every()
qui permettent des opérations de style fonctionnel et chaînables similaires à LINQ. Des bibliothèques comme lodash
fournissent des utilitaires encore plus étendus.grep
(pour le filtrage) et map
(pour la transformation) fournissent des capacités essentielles de traitement de listes.array_filter
, array_map
, array_reduce
) et les bibliothèques de collections orientées objet (par ex. Collections Laravel, Collections Doctrine) offrent des fonctionnalités de manipulation de données déclaratives similaires.Les principes fondamentaux de LINQ — interrogation déclarative des données, transformations fonctionnelles, évaluation paresseuse et composabilité — ne sont pas confinés à .NET. Java offre une solution standard robuste via l'API Streams. Les développeurs Python exploitent les compréhensions intégrées, le module itertools
et des bibliothèques comme py-linq
. Les programmeurs C++ peuvent utiliser les algorithmes STL, les bibliothèques de plages modernes (std::ranges
, range-v3
), ou des bibliothèques d'émulation LINQ dédiées.
La vraie valeur ne réside pas dans la syntaxe, mais dans la reconnaissance de ces concepts comme une boîte à outils universelle pour un traitement de données propre et efficace. Une fois compris, ils deviennent transférables — que vous codiez en Java, Python, C++ ou tout autre langage adoptant des paradigmes déclaratifs.