Existe-t-il un moyen efficace de savoir combien d'éléments se trouvent dans un itérateur en Python, en général, sans itérer sur chacun et sans compter?
Existe-t-il un moyen efficace de savoir combien d'éléments se trouvent dans un itérateur en Python, en général, sans itérer sur chacun et sans compter?
Réponses:
Non, ce n'est pas possible.
Exemple:
import random
def gen(n):
for i in xrange(n):
if random.randint(0, 1) == 0:
yield i
iterator = gen(10)
La longueur de iterator
est inconnue jusqu'à ce que vous l'itériez.
def gen(): yield random.randint(0, 1)
est infini, vous ne pourrez donc jamais trouver une longueur en l'itérant.
numIters = 0 ; while iterator: numIters +=1
?
Ce code devrait fonctionner:
>>> iter = (i for i in range(50))
>>> sum(1 for _ in iter)
50
Bien qu'il effectue une itération à travers chaque élément et les compte, c'est le moyen le plus rapide de le faire.
Cela fonctionne également lorsque l'itérateur n'a pas d'élément:
>>> sum(1 for _ in range(0))
0
Bien sûr, il s'exécute pour toujours pour une entrée infinie, alors rappelez-vous que les itérateurs peuvent être infinis:
>>> sum(1 for _ in itertools.count())
[nothing happens, forever]
Sachez également que l'itérateur sera épuisé en faisant cela et que les tentatives ultérieures de l'utiliser ne verront aucun élément . C'est une conséquence inévitable de la conception de l'itérateur Python. Si vous souhaitez conserver les éléments, vous devrez les stocker dans une liste ou autre.
_
référence à Perl $_
? :)
_
d'une variable factice dont vous ne vous souciez pas de la valeur.
Non, toute méthode vous demandera de résoudre chaque résultat. Tu peux faire
iter_length = len(list(iterable))
mais exécuter cela sur un itérateur infini ne reviendra bien sûr jamais. Il consommera également l'itérateur et il devra être réinitialisé si vous souhaitez utiliser le contenu.
Le fait de nous indiquer le vrai problème que vous essayez de résoudre peut nous aider à trouver une meilleure façon d'atteindre votre objectif réel.
Edit: Utiliser list()
lira tout l'itérable en mémoire à la fois, ce qui peut être indésirable. Une autre façon est de faire
sum(1 for _ in iterable)
comme une autre personne a posté. Cela évitera de le garder en mémoire.
len(list(iterable))
chargera toutes les données en mémoire. Vous pouvez utiliser: reduce(lambda x, _: x+1, iterable, 0)
. Edit: le code Zonda333 avec somme est également bon.
functools.reduce
Vous ne pouvez pas (sauf que le type d'un itérateur particulier implémente certaines méthodes spécifiques qui le rendent possible).
Généralement, vous ne pouvez compter les éléments de l'itérateur qu'en consommant l'itérateur. L'un des moyens probablement les plus efficaces:
import itertools
from collections import deque
def count_iter_items(iterable):
"""
Consume an iterable not reading it into memory; return the number of items.
"""
counter = itertools.count()
deque(itertools.izip(iterable, counter), maxlen=0) # (consume at C speed)
return next(counter)
(Pour Python 3.x remplacer itertools.izip
par zip
).
sum(1 for _ in iterator)
, c'était presque deux fois plus rapide.
zip
compte : si vous réussissez zip(counter, iterable)
, vous obtiendrez en fait 1 de plus que le nombre itérable!
Kinda. Vous pouvez vérifier la __length_hint__
méthode, mais sachez que (au moins jusqu'à Python 3.4, comme le souligne utilement gsnedders) c'est un détail d'implémentation non documenté ( message suivant dans le fil de discussion ), qui pourrait très bien disparaître ou invoquer des démons nasaux à la place.
Sinon, non. Les itérateurs ne sont qu'un objet qui expose uniquement la next()
méthode. Vous pouvez l'appeler autant de fois que nécessaire et ils peuvent éventuellement augmenter ou non StopIteration
. Heureusement, ce comportement est la plupart du temps transparent pour le codeur. :)
J'aime le package 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
La count()
mise en œuvre réelle est la suivante:
def count(iterable):
if hasattr(iterable, '__len__'):
return len(iterable)
d = collections.deque(enumerate(iterable, 1), maxlen=1)
return d[0][0] if d else 0
Donc, pour ceux qui voudraient connaître le résumé de cette discussion. Les meilleurs scores finaux pour compter une expression de générateur de 50 millions de longueur en utilisant:
len(list(gen))
, len([_ for _ in gen])
, sum(1 for _ in gen),
ilen(gen)
(de more_itertool ),reduce(lambda c, i: c + 1, gen, 0)
, triés par performances d'exécution (y compris la consommation de mémoire), vous surprendra:
''
gen = (i for i in data*1000); t0 = monotonic(); len(list(gen))
('liste, sec', 1.9684218849870376)
gen = (i for i in data*1000); t0 = monotonic(); len([i for i in gen])
('liste_compr, sec', 2.5885991149989422)
gen = (i for i in data*1000); t0 = monotonic(); sum(1 for i in gen); t1 = monotonic()
('somme, sec', 3.441088170016883)
d = deque(enumerate(iterable, 1), maxlen=1)
test_ilen.py:10: 0.875 KiB
gen = (i for i in data*1000); t0 = monotonic(); ilen(gen)
('ilen, sec', 9.812256851990242)
gen = (i for i in data*1000); t0 = monotonic(); reduce(lambda counter, i: counter + 1, gen, 0)
('réduire, sec', 13.436614598002052) ``
Donc, len(list(gen))
est la mémoire la plus fréquente et la moins consommable
len(list(gen))
devrait consommer moins de mémoire que l'approche basée sur la réduction? Le premier crée un nouveau list
qui implique l'allocation de mémoire tandis que le second ne devrait pas. Je m'attendrais donc à ce que ce dernier soit plus efficace en mémoire. De plus, la consommation de mémoire dépendra du type d'élément.
len(tuple(iterable))
peut être encore plus efficace: article de Nelson Minar
Un itérateur est juste un objet qui a un pointeur vers le prochain objet à lire par une sorte de tampon ou de flux, c'est comme une LinkedList où vous ne savez pas combien de choses vous avez jusqu'à ce que vous les parcouriez. Les itérateurs sont censés être efficaces car ils ne font que vous dire ce qui est ensuite par des références au lieu d'utiliser l'indexation (mais comme vous l'avez vu, vous perdez la capacité de voir combien d'entrées sont les suivantes).
En ce qui concerne votre question d'origine, la réponse est toujours qu'il n'y a aucun moyen en général de connaître la longueur d'un itérateur en Python.
Étant donné que votre question est motivée par une application de la bibliothèque pysam, je peux donner une réponse plus précise: je suis un contributeur à PySAM et la réponse définitive est que les fichiers SAM / BAM ne fournissent pas un nombre exact de lectures alignées. Ces informations ne sont pas non plus facilement disponibles à partir d'un fichier d'index BAM. La meilleure chose à faire est d'estimer le nombre approximatif d'alignements en utilisant l'emplacement du pointeur de fichier après avoir lu un certain nombre d'alignements et extrapolé en fonction de la taille totale du fichier. Cela suffit pour implémenter une barre de progression, mais pas une méthode de comptage des alignements en temps constant.
Un benchmark rapide:
import collections
import itertools
def count_iter_items(iterable):
counter = itertools.count()
collections.deque(itertools.izip(iterable, counter), maxlen=0)
return next(counter)
def count_lencheck(iterable):
if hasattr(iterable, '__len__'):
return len(iterable)
d = collections.deque(enumerate(iterable, 1), maxlen=1)
return d[0][0] if d else 0
def count_sum(iterable):
return sum(1 for _ in iterable)
iter = lambda y: (x for x in xrange(y))
%timeit count_iter_items(iter(1000))
%timeit count_lencheck(iter(1000))
%timeit count_sum(iter(1000))
Les resultats:
10000 loops, best of 3: 37.2 µs per loop
10000 loops, best of 3: 47.6 µs per loop
10000 loops, best of 3: 61 µs per loop
Ie le simple count_iter_items est la voie à suivre.
Ajustement de cela pour python3:
61.9 µs ± 275 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
74.4 µs ± 190 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
82.6 µs ± 164 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Il existe deux façons d'obtenir la longueur de «quelque chose» sur un ordinateur.
La première façon est de stocker un compte - cela nécessite tout ce qui touche le fichier / les données pour le modifier (ou une classe qui expose uniquement les interfaces - mais cela revient à la même chose).
L'autre façon est de l'itérer et de compter sa taille.
Cela va à l'encontre de la définition même d'un itérateur, qui est un pointeur vers un objet, ainsi que des informations sur la façon d'accéder à l'objet suivant.
Un itérateur ne sait pas combien de fois il pourra encore itérer jusqu'à la fin. Cela pourrait être infini, donc l'infini pourrait être votre réponse.
Bien qu'il ne soit pas possible en général de faire ce qui a été demandé, il est encore souvent utile de compter le nombre d'éléments qui ont été itérés après les avoir itérés. Pour cela, vous pouvez utiliser jaraco.itertools.Counter ou similaire. Voici un exemple utilisant Python 3 et rwt pour charger le package.
$ rwt -q jaraco.itertools -- -q
>>> import jaraco.itertools
>>> items = jaraco.itertools.Counter(range(100))
>>> _ = list(counted)
>>> items.count
100
>>> import random
>>> def gen(n):
... for i in range(n):
... if random.randint(0, 1) == 0:
... yield i
...
>>> items = jaraco.itertools.Counter(gen(100))
>>> _ = list(counted)
>>> items.count
48
Vraisemblablement, vous voulez compter le nombre d'éléments sans itérer, afin que l'itérateur ne soit pas épuisé et que vous l'utilisiez à nouveau plus tard. Ceci est possible avec copy
oudeepcopy
import copy
def get_iter_len(iterator):
return sum(1 for _ in copy.copy(iterator))
###############################################
iterator = range(0, 10)
print(get_iter_len(iterator))
if len(tuple(iterator)) > 1:
print("Finding the length did not exhaust the iterator!")
else:
print("oh no! it's all gone")
La sortie est "Finding the length did not exhaust the iterator!
"
En option (et sans avis), vous pouvez observer la len
fonction intégrée comme suit:
import copy
def len(obj, *, len=len):
try:
if hasattr(obj, "__len__"):
r = len(obj)
elif hasattr(obj, "__next__"):
r = sum(1 for _ in copy.copy(obj))
else:
r = len(obj)
finally:
pass
return r
map
itérateur s'attendant à ce que les appels de fonction résultants ne se produisent qu'une seule fois.