(Avertissement : Réponse de mammouth en perspective. La partie jusqu'à la première ligne horizontale fait une bonne section tl;dr, je suppose)
Je ne suis pas sûr d'être un gourou de Python... mais j'ai une bonne connaissance de l'itération en Python, alors essayons :)
Tout d'abord : A priori, les requêtes LINQ sont exécutées paresseusement - si c'est le cas, les expressions génératrices sont un concept Python plus proche (de toute façon, les compréhensions de liste, de dictée et d'ensemble sont conceptuellement juste des expressions génératrices alimentées au constructeur de liste/dict/ensemble !)
Il existe également une différence conceptuelle : LINQ sert, comme son nom l'indique, à interroger des structures de données. Les compréhensions de liste/dict/ensemble en sont une application possible (par exemple, filtrer et projeter les éléments d'une liste). Elles sont donc en fait moins générales (comme nous le verrons, de nombreuses choses intégrées à LINQ ne le sont pas). De même, les expressions de générateur sont un moyen de formuler un itérateur unique forward in-place (j'aime penser que c'est un lambda pour les fonctions de générateur, mais sans le mot-clé long et laid ;) ) et non un moyen de décrire une requête complexe. Ils se chevauchent, oui, mais ils ne sont pas identiques. Si vous voulez toute la puissance de LINQ en Python, vous devrez écrire un générateur à part entière. Ou combiner les nombreux générateurs puissants intégrés et dans itertools
.
Maintenant, les contreparties Python pour les capacités LINQ que Jon Skeet a nommées :
Projections : (x.foo for ...)
Filtrage : (... if x.bar > 5)
- Joins (x joint à y sur x.foo égale y.bar)
La chose la plus proche serait ((x_item, next(y_item for y_item in y if x_item.foo == y_item.bar)) for x_item in x)
Je suppose.
Notez que cette méthode n'itère pas sur l'ensemble des y pour chaque x_item, elle ne récupère que la première correspondance.
- Jointures de groupes (x jointures y sur x.foo égale y.bar dans g)
C'est plus difficile. Python n'a pas de types anonymes, bien qu'il soit trivial de les créer soi-même si cela ne vous dérange pas de vous embrouiller avec __dict__
:
class Anonymous(object):
def __init__(self, **kwargs):
self.__dict__ = kwargs
Ensuite, nous pourrions faire (Anonymous(x=x, y=y) for ...)
pour obtenir une liste d'objets qui ont x
y y
les membres avec les valeurs respectives. La bonne chose à faire est d'envoyer les résultats au constructeur d'une classe appropriée, disons XY.
- Regroupement (grouper x.foo par x.bar)
Maintenant, ça devient compliqué... il n'y a pas de moyen intégré, afaik. Mais nous pouvons la définir nous-mêmes si nous en avons besoin :
from collections import defaultdict
def group_by(iterable, group_func):
groups = defaultdict(list)
for item in iterable:
groups[group_func(item)].append(item)
return groups
Ejemplo:
>>> from operator import attrgetter
>>> group_by((x.foo for x in ...), attrgetter('bar'))
defaultdict(<class 'list'>, {some_value_of_bar: [x.foo of all x where x.bar == some_value_of_bar], some_other_value_of_bar: [...], ...})
Cela nécessite que ce que nous regroupons soit hachable, cependant. Il est possible d'éviter cela, et je m'y emploierai si le public le demande. Mais pour l'instant, je suis paresseux :)
Nous pouvons également retourner un itérable de groupes sans les valeurs par lesquelles nous avons groupé, en appelant .values()
sur le résultat (bien sûr, nous pouvons alimenter que a list
pour obtenir quelque chose que nous pouvons indexer et itérer plusieurs fois). Mais qui sait si nous n'aurons pas besoin des valeurs de groupe...
- Ordonnancement (orderby x.foo ascendant, y.bar descendant)
Le tri nécessite une syntaxe spéciale ? La fonction intégrée sorted
fonctionne aussi pour les itérables : sorted(x % 2 for x in range(10))
o sorted(x for x in xs, key=attrgetter('foo'))
. Trié par ordre croissant par défaut, l'argument mot-clé reverse
donne l'ordre décroissant.
Hélas, afaik trier par plusieurs attributs n'est pas si facile, surtout quand on mélange ascendant et descendant. Hmm... un sujet pour une recette ?
- Variables intermédiaires (let tmp = x.foo)
Non, ce n'est pas possible dans les compréhensions ou les expressions génératrices - elles sont, comme leur nom l'indique, censées être des expressions (et ne couvrent généralement qu'une ou deux lignes). C'est parfaitement possible dans les générateurs de fonctions, par contre :
(x * 2 for x in iterable)
réécrit comme un générateur avec une variable intermédiaire :
def doubles(iterable):
for x in iterable:
times2 = x * 2
yield times2
Aplatissement : (c for s in ("aa","bb") for c in s )
Notez que bien que LINQ to Objects traite des délégués, d'autres fournisseurs de requêtes (par exemple LINQ to SQL) peuvent traiter des arbres d'expression qui décrivent la requête au lieu de simplement présenter des délégués exécutables. Cela permet de traduire la requête en SQL (ou d'autres langages d'interrogation) - encore une fois, je ne sais pas si Python supporte ce genre de chose ou non. Mais c'est une partie importante de LINQ.
Python ne fait absolument rien de tel. Les expressions de liste correspondent à l'accumulation d'une liste simple dans une boucle for (éventuellement imbriquée), les expressions de générateur correspondent à un générateur. Étant donné le parser
y ast
module, il serait possible en théorie d'écrire une bibliothèque pour convertir une compréhension en, par exemple, une requête SQL. Mais personne ne s'en soucie.