Si je veux le nombre d'éléments dans un itérable sans se soucier des éléments eux-mêmes, quelle serait la façon Pythonique d'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 que lambda
est sur le point d'être considéré comme nuisible, et lambda _: 1
n'est certainement pas joli.
(Le cas d'utilisation de ceci est le comptage du nombre de lignes dans un fichier texte correspondant à une expression régulière, c'est-à-dire grep -c
.)
La manière habituelle est
sum(1 for i in it)
Méthode significativement plus rapide que sum(1 for i in it)
lorsque l'itérable peut être long (et non plus lentement lorsque l'itérable est court), tout en conservant un comportement de surcharge de mémoire fixe (contrairement à len(list(it))
) pour éviter le débordement de swap et la réallocation frais généraux pour les entrées plus importantes:
# 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 exécute la boucle en code C sur CPython (deque
, count
et Zip
sont tous implémentés en C); éviter l'exécution de code octet par boucle est généralement la clé des performances dans CPython.
Il est étonnamment difficile de trouver des cas de test équitables pour comparer les performances (list
astuces en utilisant __length_hint__
Qui n'est probablement pas disponible pour les itérables d'entrée arbitraires, itertools
fonctions qui ne ne pas fournir __length_hint__
ont souvent des modes de fonctionnement spéciaux qui fonctionnent plus rapidement lorsque la valeur retournée sur chaque boucle est libérée avant que la valeur suivante ne soit demandée, ce que deque
avec maxlen=0
fera ). Le cas de test que j'ai utilisé était de créer une fonction de générateur qui prendrait une entrée et retournerait un générateur de niveau C qui manquait de itertools
optimisations de conteneur de retour ou __length_hint__
, En utilisant Python 3.3's yield from
:
def no_opt_iter(it):
yield from it
Puis en utilisant la magie ipython
%timeit
(En remplaçant 100 par différentes constantes):
>>> %%timeit -r5 fakeinput = (0,) * 100
... ilen(no_opt_iter(fakeinput))
Lorsque l'entrée n'est pas suffisamment grande pour que len(list(it))
entraîne des problèmes de mémoire, sur une boîte Linux exécutant Python 3,5 x64, ma solution prend environ 50% de plus que def ilen(it): return len(list(it))
, quelle que soit la longueur d'entrée.
Pour la plus petite des entrées, la configuration coûte pour appeler deque
/Zip
/count
/next
signifie que cela prend infiniment plus de temps de cette façon que def ilen(it): sum(1 for x in it)
(environ 200 ns de plus sur ma machine pour une entrée de longueur 0, ce qui représente une augmentation de 33% par rapport à la simple approche sum
), mais pour des entrées plus longues, elle s'exécute environ la moitié du temps par élément supplémentaire; pour les entrées de longueur 5, le coût est équivalent, et quelque part dans la plage de longueur 50-100, la surcharge initiale est imperceptible par rapport au travail réel; l'approche sum
prend environ deux fois plus de temps.
Fondamentalement, si l'utilisation de la mémoire est importante ou si les entrées n'ont pas de taille limitée et que la rapidité est plus importante que la concision, utilisez cette solution. Si les entrées sont limitées et de petite taille, len(list(it))
est probablement la meilleure solution, et si elles ne sont pas limitées, mais la simplicité/concision compte, vous utiliseriez sum(1 for x in it)
.
Un moyen court est:
def ilen(it):
return len(list(it))
Notez que si vous générez un lot d'éléments (par exemple, des dizaines de milliers ou plus), les mettre dans une liste peut devenir un problème de performances. Cependant, il s'agit d'une simple expression de l'idée selon laquelle les performances n'auront pas d'importance dans la plupart des cas.
more_itertools
est une bibliothèque tierce qui implémente un outil ilen
. pip install more_itertools
import more_itertools as mit
mit.ilen(x for x in range(10))
# 10
J'aime le paquet cardinality pour cela, il est très léger et essaie d'utiliser l'implémentation la plus rapide possible en fonction de l'itérable.
Usage:
>>> 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
Ce seraient mes choix l'un ou l'autre:
print(len([*gen]))
print(len(list(gen)))