Obtenir le nombre d'éléments dans un itérateur en Python
138
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?
Alternativement, def gen(): yield random.randint(0, 1)est infini, vous ne pourrez donc jamais trouver une longueur en l'itérant.
tgray
1
Donc, pour valider l'évidence: la meilleure façon d'obtenir la "taille" d'un itérateur est simplement de compter le nombre de fois que vous avez traversé l'itération, non? Dans ce cas, ce serait numIters = 0 ; while iterator: numIters +=1?
Mike Williamson
Intéressant, c'est donc le problème qui s'arrête
Akababa
231
Ce code devrait fonctionner:
>>> iter =(i for i in range(50))>>> sum(1for _ 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(1for _ 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(1for _ 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.
Il me semble que cela fait exactement ce que OP ne veut pas faire: parcourir l'itérateur et compter.
Adam Crossland
36
C'est un moyen peu encombrant de compter les éléments dans un itérable
Captain Lepton
9
Bien que ce ne soit pas ce que souhaite OP, étant donné que sa question n'a pas de réponse, cette réponse évite l'instanciation d'une liste, et elle est empiriquement plus rapide par une constante que la méthode de réduction répertoriée ci-dessus.
Phillip Nordwall
5
Je ne peux pas aider: est-ce que la _référence à Perl $_? :)
Alois Mahdal
17
@AloisMahdal Non. Il est classique en Python d'utiliser le nom _d'une variable factice dont vous ne vous souciez pas de la valeur.
Taymon
67
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(1for _ in iterable)
comme une autre personne a posté. Cela évitera de le garder en mémoire.
le problème est que je lis un fichier avec "pysam" qui contient des millions d'entrées. Pysam renvoie un itérateur. Pour calculer une certaine quantité, j'ai besoin de savoir combien de lectures sont dans le fichier, mais je n'ai pas besoin de lire chacune d'entre elles ... c'est le problème.
6
Je ne suis pas utilisateur de pysam, mais il est probablement en train de lire le fichier "paresseux". Cela a du sens car vous ne voulez pas avoir de gros fichiers en mémoire. Donc, si vous devez savoir non. des enregistrements avant l'itération, la seule façon est de créer deux itérateurs, et d'utiliser le premier pour compter les éléments et le second pour lire le fichier. BTW. Ne pas l'utiliser 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.
Tomasz Wysocki
1
@ user248237: pourquoi dites-vous que vous avez besoin de savoir combien d'entrées sont disponibles pour calculer une certaine quantité? Vous pouvez simplement en lire un montant fixe et gérer le cas lorsqu'il y en a moins que ce montant fixe (très simple à faire en utilisant iterslice). Y a-t-il une autre raison pour laquelle vous devez lire toutes les entrées?
kriss
1
@Tomasz Notez que la réduction est obsolète et disparaîtra dans Python 3 et plus.
Wilduck
7
@Wilduck: Ce n'est pas parti, juste déménagé àfunctools.reduce
Daenyth
33
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)
+1: dans une comparaison temporelle avec sum(1 for _ in iterator), c'était presque deux fois plus rapide.
augustomen
1
Il est plus exact de dire qu'il consomme un itérable en lisant chaque élément en mémoire et en le jetant immédiatement.
Rockallite
Il est important de noter (ce que j'ai négligé) que l' ordre des arguments zipcompte : si vous réussissez zip(counter, iterable), vous obtiendrez en fait 1 de plus que le nombre itérable!
Kye W Shi
très belle réponse. donnerait une prime là-dessus.
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. :)
Ce n'est plus le cas depuis PEP 424 et Python 3.4. __length_hint__est maintenant documenté, mais c'est un indice et ne donne aucune garantie de précision.
gsnedders
12
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 else0
Je suppose que vous pouvez toujours parcourir l'itérateur si vous utilisez cette fonction, oui?
jcollum
12
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:
Comment avez-vous mesuré la consommation de mémoire?
normanius
1
Pouvez-vous expliquer pourquoi len(list(gen))devrait consommer moins de mémoire que l'approche basée sur la réduction? Le premier crée un nouveau listqui 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.
normanius
FYI: Je peux reproduire pour python 3.6.8 (sur un MacBookPro) que la méthode 1 surpasse les autres méthodes en termes d'exécution (j'ai sauté la méthode 4).
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).
Un itérateur ne ressemble en rien à une liste chaînée. Un objet renvoyé par un itérateur ne pointe pas vers l'objet suivant, et ces objets ne sont pas (nécessairement) stockés en mémoire. Au contraire, il peut produire un objet l'un après l'autre, basé sur n'importe quelle logique interne (qui pourrait être, mais ne doit pas être, basée sur une liste stockée).
Tom
1
@Tom J'utilisais LinkedList comme exemple principalement en ce que vous ne savez pas combien vous en avez puisque vous ne savez ce qui va suivre que dans un sens (s'il y a quelque chose). Je m'excuse si mon libellé semble un peu décalé ou si j'ai laissé entendre qu'ils ne font qu'un.
Jesus Ramos
8
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.
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.
Il est courant de mettre ce type d'informations dans l'en-tête du fichier, et pour que pysam vous en donne accès. Je ne connais pas le format, mais avez-vous vérifié l'API?
Comme d'autres l'ont dit, vous ne pouvez pas connaître la longueur de l'itérateur.
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.
Cela ne viole rien, et il n'y a rien de mal à appliquer les connaissances antérieures lors de l'utilisation d'un itérateur. Il y a des millions d'itérateurs autour, où vous savez, que le nombre d'éléments est limité. Pensez simplement à filtrer une liste, vous pouvez facilement donner la longueur maximale, vous ne savez tout simplement pas combien d'éléments correspondent réellement à votre condition de filtre. Vouloir connaître le nombre d'éléments correspondants est une application valide, ne violant aucune idée mystérieuse d'un itérateur.
Michael
0
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 copyoudeepcopy
import copy
def get_iter_len(iterator):return sum(1for _ 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 lenfonction 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(1for _ in copy.copy(obj))else:
r = len(obj)finally:passreturn r
Les plages ne sont pas des itérateurs. Certains types d'itérateur peuvent être copiés, mais d'autres provoqueront l'échec de ce code avec un TypeError (par exemple, des générateurs), et l'itération à travers un itérateur copié peut provoquer des effets secondaires deux fois, ou provoquer une rupture arbitraire dans le code qui, par exemple, a renvoyé un mapitérateur s'attendant à ce que les appels de fonction résultants ne se produisent qu'une seule fois.
Réponses:
Non, ce n'est pas possible.
Exemple:
La longueur de
iterator
est inconnue jusqu'à ce que vous l'itériez.la source
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:
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:
Bien sûr, il s'exécute pour toujours pour une entrée infinie, alors rappelez-vous que les itérateurs peuvent être infinis:
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.
la source
_
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
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 fairecomme une autre personne a posté. Cela évitera de le garder en mémoire.
la source
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:
(Pour Python 3.x remplacer
itertools.izip
parzip
).la source
sum(1 for _ in iterator)
, c'était presque deux fois plus rapide.zip
compte : si vous réussissezzip(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 nonStopIteration
. Heureusement, ce comportement est la plupart du temps transparent pour le codeur. :)la source
__length_hint__
est maintenant documenté, mais c'est un indice et ne donne aucune garantie de précision.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:
La
count()
mise en œuvre réelle est la suivante:la source
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:
''
1: test_list.py:8: 0,492 Ko
('liste, sec', 1.9684218849870376)
2: test_list_compr.py:8: 0,867 Ko
('liste_compr, sec', 2.5885991149989422)
3: test_sum.py:8: 0,859 Ko
('somme, sec', 3.441088170016883)
4: more_itertools / more.py: 413: 1,266 Kio
('ilen, sec', 9.812256851990242)
5: test_reduce.py:8: 0,859 Ko
('réduire, sec', 13.436614598002052) ``
Donc,
len(list(gen))
est la mémoire la plus fréquente et la moins consommablela source
len(list(gen))
devrait consommer moins de mémoire que l'approche basée sur la réduction? Le premier crée un nouveaulist
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 MinarUn 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).
la source
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.
la source
Un benchmark rapide:
Les resultats:
Ie le simple count_iter_items est la voie à suivre.
Ajustement de cela pour python3:
la source
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.
la source
Il est courant de mettre ce type d'informations dans l'en-tête du fichier, et pour que pysam vous en donne accès. Je ne connais pas le format, mais avez-vous vérifié l'API?
Comme d'autres l'ont dit, vous ne pouvez pas connaître la longueur de l'itérateur.
la source
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.
la source
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.
la source
la source
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
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:la source
map
itérateur s'attendant à ce que les appels de fonction résultants ne se produisent qu'une seule fois.