Comment filtrer efficacement les valeurs calculées dans une compréhension de liste Python?
-
02-07-2019 - |
Question
La syntaxe de compréhension de liste Python facilite le filtrage des valeurs dans une compréhension. Par exemple:
result = [x**2 for x in mylist if type(x) is int]
Renverra une liste des carrés d'entiers dans mylist. Cependant, que se passe-t-il si le test implique des calculs (coûteux) et que vous souhaitez filtrer le résultat? Une option est:
result = [expensive(x) for x in mylist if expensive(x)]
Cela entraînera une liste de messages "" false". cher (x), bien que cher () est appelé deux fois pour chaque x. Existe-t-il une syntaxe de compréhension vous permettant de faire ce test en appelant seulement une fois par x cher?
La solution
Si les calculs sont déjà bien intégrés dans les fonctions, pourquoi ne pas utiliser filtre
et map
?
result = filter (None, map (expensive, mylist))
Vous pouvez utiliser itertools.imap
si la liste est très longue.
Autres conseils
Je suis venu avec ma propre réponse après une minute de réflexion. Cela peut être fait avec des compréhensions imbriquées:
result = [y for y in (expensive(x) for x in mylist) if y]
Je suppose que cela fonctionne, même si je trouve que les compréhensions imbriquées ne sont lisibles que marginalement
La réponse la plus évidente (et la plus lisible, selon moi) est de ne pas utiliser une compréhension de liste ou une expression de générateur, mais plutôt un véritable générateur:
def gen_expensive(mylist):
for item in mylist:
result = expensive(item)
if result:
yield result
Cela prend plus d’espace horizontal, mais il est beaucoup plus facile de voir ce qu’il fait en un coup d’œil, et vous finissez par ne pas vous répéter.
result = [x for x in map(expensive,mylist) if x]
map () retournera une liste des valeurs de chaque objet de la liste mylist passée à Cher (). Ensuite, vous pouvez comprendre cette liste et ignorer les valeurs inutiles.
Cela ressemble un peu à une compréhension imbriquée, mais devrait être plus rapide (puisque l’interpréteur python peut l’optimiser assez facilement).
C’est exactement ce que les générateurs sont adaptés à gérer:
result = (expensive(x) for x in mylist)
result = (do_something(x) for x in result if some_condition(x))
...
result = [x for x in result if x] # finally, a list
- Cela rend parfaitement clair ce qui se passe à chaque étape du pipeline.
- Explicite sur implicite
- Utilise des générateurs partout jusqu'à la dernière étape, donc pas de grandes listes intermédiaires
cf: "Astuces du générateur pour les programmeurs système" par David Beazley
Vous pouvez toujours mémoriser la fonction onéreuse ()
afin que le fait d'appeler la deuxième fois n'est qu'une recherche de la valeur calculée de x
.
Voici une des nombreuses implémentations de la mémoire en tant que décorateur . / p>
Vous pouvez mémoriser cher (x) (et si vous appelez souvent (x) fréquemment, vous devriez probablement le mémoriser de quelque façon que ce soit. Cette page donne une implémentation de memoize pour python:
http://code.activestate.com/recipes/52201/
Cela présente l’avantage supplémentaire que les opérations onéreuses (x) peuvent être exécutées moins que N fois, car toute entrée dupliquée utilisera le mémo de l’exécution précédente.
Notez que cela suppose que cher (x) est une vraie fonction et ne dépend pas d'un état externe susceptible de changer. Si cher (x) dépend d'un état externe et que vous pouvez détecter le moment où cet état change, ou si vous savez qu'il ne changera pas pendant la compréhension de votre liste, vous pouvez réinitialiser les mémos avant la compréhension.
J'aurai une préférence pour:
itertools.ifilter(bool, (expensive(x) for x in mylist))
Cela présente l'avantage de:
- évitez None en tant que fonction (sera éliminée dans Python 3): http://bugs.python.org / issue2186
- utilisez uniquement des itérateurs.
Il y a l'utilisation ancienne et simple d'une boucle pour
à ajouter à une liste:
result = []
for x in mylist:
expense = expensive(x)
if expense:
result.append(expense)