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?

Tomasz Wysocki
la source

Réponses:

101

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 iteratorest inconnue jusqu'à ce que vous l'itériez.

Tomasz Wysocki
la source
14
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(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.

John Howard
la source
10
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(1 for _ in iterable)

comme une autre personne a posté. Cela évitera de le garder en mémoire.

Daenyth
la source
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)

(Pour Python 3.x remplacer itertools.izippar zip).

zuo
la source
3
+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.
Reut Sharabani
18

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. :)

badp
la source
5
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 else 0
Erwin Mayer
la source
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:

  • 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

gen = (i for i in data*1000); t0 = monotonic(); len(list(gen))

('liste, sec', 1.9684218849870376)

2: test_list_compr.py:8: 0,867 Ko

gen = (i for i in data*1000); t0 = monotonic(); len([i for i in gen])

('liste_compr, sec', 2.5885991149989422)

3: test_sum.py:8: 0,859 Ko

gen = (i for i in data*1000); t0 = monotonic(); sum(1 for i in gen); t1 = monotonic()

('somme, sec', 3.441088170016883)

4: more_itertools / more.py: 413: 1,266 Kio

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)

5: test_reduce.py:8: 0,859 Ko

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

Alex-Bogdanov
la source
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).
normanius
len(tuple(iterable))peut être encore plus efficace: article de Nelson Minar
VMAtm
9

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).

Jésus Ramos
la source
2
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.

Kevin Jacobs
la source
6

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)
Michael
la source
Remarque: ce test est basé sur python2
normanius
3

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.

Wayne Werner
la source
0

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.

tom10
la source
0

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.

FCAlive
la source
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
Jason R. Coombs
la source
-1
def count_iter(iter):
    sum = 0
    for _ in iter: sum += 1
    return sum
hasen
la source
-1

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(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 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(1 for _ in copy.copy(obj))
        else:
            r = len(obj)
    finally:
        pass
    return r
Cure-dents Anémone
la source
1
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.
user2357112 prend en charge Monica