Quel est le chemin le plus court pour compter le nombre d'éléments dans un générateur / iterator?
Question
Si je veux que le nombre d'éléments dans un itérables sans se soucier des éléments eux-mêmes, ce qui serait le moyen pythonique pour obtenir cela? En ce moment, je définirais
def ilen(it):
return sum(itertools.imap(lambda _: 1, it)) # or just map in Python 3
mais je comprends lambda
est proche d'être considéré comme nuisible et lambda _: 1
est certainement pas assez.
(Le cas d'utilisation de ce compte est le nombre de lignes dans un fichier de texte correspondant à une expression régulière, c.-à-grep -c
.)
La solution
La manière habituelle est
sum(1 for i in it)
Autres conseils
Méthode qui est significativement plus rapide que sum(1 for i in it)
lorsque le itérables peut être long (et non de manière significative plus lente lorsque la itérables est courte), tout en maintenant la mémoire fixe le comportement au-dessus (contrairement len(list(it))
) pour éviter raclée d'échange et les frais généraux de réaffectation pour les entrées plus grandes:
# On Python 2 only, get zip that lazily generates results instead of returning list
from future_builtins import zip
from collections import deque
from itertools import count
def ilen(it):
# Make a stateful counting iterator
cnt = count()
# zip it with the input iterator, then drain until input exhausted at C level
deque(zip(it, cnt), 0) # cnt must be second zip arg to avoid advancing too far
# Since count 0 based, the next value is the count
return next(cnt)
Comme len(list(it))
il réalise la boucle dans le code C sur CPython (deque
, count
et zip
sont toutes mises en œuvre en C); en évitant l'exécution de code d'octet par boucle est généralement la clé de la performance en CPython.
Il est étonnamment difficile de trouver des cas de test juste pour comparer les performances (tricheurs list
utilisant __length_hint__
qui ne sont pas susceptibles d'être disponibles pour iterables d'entrée arbitraires, les fonctions de itertools
qui ne fournissent pas __length_hint__
ont souvent des modes de fonctionnement spéciaux que le travail plus rapide lorsque la valeur retournée sur chaque boucle est libérée avant la valeur suivante est demandée, qui deque
avec maxlen=0
fera). Le cas de test I utilisé était de créer une fonction de générateur qui prendrait une entrée et renvoyer un générateur de niveau de C qui manquait optimisations de conteneurs itertools
spéciale de retour ou __length_hint__
, en utilisant le yield from
Python 3.3:
def no_opt_iter(it):
yield from it
Ensuite, en utilisant la magie de ipython
%timeit
(en remplaçant différentes constantes pour 100):
>>> %%timeit -r5 fakeinput = (0,) * 100
... ilen(no_opt_iter(fakeinput))
Lorsque l'entrée est pas assez grand que len(list(it))
causerait des problèmes de mémoire, sur une machine Linux x64 Python 3.5, ma solution prend environ 50% plus longtemps que def ilen(it): return len(list(it))
, quelle que soit la longueur d'entrée.
Pour les plus petits des intrants, les coûts d'installation pour appeler deque
/ zip
/ count
/ next
moyens qu'il faut infinitésimale plus de cette façon que def ilen(it): sum(1 for x in it)
(environ 200 ns plus sur ma machine pour une longueur 0 entrée, ce qui est une augmentation de 33% sur l'approche simple sum
), mais pour les entrées plus, il fonctionne à peu près la moitié du temps par élément supplémentaire; pour une longueur de 5 entrées, le coût est équivalent, et quelque part dans la gamme de longueur 50-100, la surcharge initiale est imperceptibles par rapport au travail réel; l'approche sum
prend à peu près deux fois plus longtemps.
En fait, si les questions d'utilisation de la mémoire ou les entrées ne sont pas de taille limitée et la vitesse vous intéresse plus de brièveté, utiliser cette solution. Si les entrées sont bornés et un peu petite, len(list(it))
est probablement le meilleur, et si elles sont sans bornes, mais la simplicité compte / brièveté, vous utiliseriez sum(1 for x in it)
.
Une façon courte est:
def ilen(it):
return len(list(it))
Notez que si vous générez un beaucoup d'éléments (par exemple, des dizaines de milliers ou plus), puis les mettre dans une liste peut devenir un problème de performance. Cependant, ceci est une simple expression de l'idée où la performance ne va pas à la matière pour la plupart des cas.
more_itertools
est une bibliothèque tierce qui implémente une ilen
outil. pip install more_itertools
import more_itertools as mit
mit.ilen(x for x in range(10))
# 10
Je aime le paquet cardinalité pour cela, il est très léger et tente d'utiliser la la plus rapide possible mise en œuvre disponible selon le itérables.
Utilisation:
>>> import cardinality
>>> cardinality.count([1, 2, 3])
3
>>> cardinality.count(i for i in range(500))
500
>>> def gen():
... yield 'hello'
... yield 'world'
>>> cardinality.count(gen())
2
Voici mes choix un ou l'autre:
print(len([*gen]))
print(len(list(gen)))