Ottenere il numero di elementi in un iteratore in Python


Risposte:


101

No. Non è possibile.

Esempio:

import random

def gen(n):
    for i in xrange(n):
        if random.randint(0, 1) == 0:
            yield i

iterator = gen(10)

La lunghezza di iteratorè sconosciuta fino a quando non viene ripetuta.


14
In alternativa, def gen(): yield random.randint(0, 1)è infinito, quindi non sarai mai in grado di trovare una lunghezza iterando attraverso di essa.
Tgray,

1
Quindi, per convalidare l'ovvio: il modo migliore per ottenere la "dimensione" di un iteratore è semplicemente contare il numero di volte che hai passato l'iterazione, giusto? In questo caso, sarebbe numIters = 0 ; while iterator: numIters +=1?
Mike Williamson,

Interessante, quindi è l'arresto del problema
Akababa,

231

Questo codice dovrebbe funzionare:

>>> iter = (i for i in range(50))
>>> sum(1 for _ in iter)
50

Anche se scorre ogni elemento e li conta, è il modo più veloce per farlo.

Funziona anche quando l'iteratore non ha elementi:

>>> sum(1 for _ in range(0))
0

Ovviamente, funziona all'infinito per un input infinito, quindi ricorda che gli iteratori possono essere infiniti:

>>> sum(1 for _ in itertools.count())
[nothing happens, forever]

Inoltre, tieni presente che l'iteratore sarà esaurito in questo modo e ulteriori tentativi di utilizzarlo non vedranno alcun elemento . Questa è una conseguenza inevitabile del design dell'iteratore Python. Se vuoi conservare gli elementi, dovrai memorizzarli in un elenco o qualcosa del genere.


10
Mi sembra che questo faccia esattamente ciò che OP non vuole fare: iterare attraverso l'iteratore e contare.
Adam Crossland,

36
Questo è un modo efficiente in termini di spazio per contare gli elementi in un iterabile
Capitano Lepton

9
Anche se questo non è ciò che OP vuole, dato che la sua domanda non ha una risposta, questa risposta evita l'istanza di un elenco ed è empiricamente più veloce di una costante rispetto al metodo di riduzione sopra elencato.
Phillip Nordwall,

5
Non posso fare a meno: è il _riferimento a Perl $_? :)
Alois Mahdal,

17
@AloisMahdal No. In Python è convenzionale usare il nome _per una variabile fittizia di cui non ti interessa il valore.
Taymon,

67

No, qualsiasi metodo richiederà di risolvere ogni risultato. Tu puoi fare

iter_length = len(list(iterable))

ma eseguirlo su un iteratore infinito ovviamente non tornerà mai più. Consumerà anche l'iteratore e dovrà essere ripristinato se si desidera utilizzare i contenuti.

Raccontaci quale problema reale stai cercando di risolvere potrebbe aiutarci a trovare un modo migliore per raggiungere il tuo obiettivo reale.

Modifica: l'utilizzo list()leggerà l'intero iterabile in memoria in una sola volta, il che potrebbe essere indesiderabile. Un altro modo è fare

sum(1 for _ in iterable)

come postato da un'altra persona. Ciò eviterà di tenerlo in memoria.


il problema è che sto leggendo un file con "pysam" che ha milioni di voci. Pysam restituisce un iteratore. Per calcolare una certa quantità, devo sapere quante letture ci sono nel file, ma non ho bisogno di leggere ognuna ... questo è il problema.

6
Non sono un utente Pysam, ma probabilmente sta leggendo il file "pigro". Ha senso perché non si desidera avere file di grandi dimensioni in memoria. Quindi se devi sapere di no. dei record prima dell'iterazione, l'unico modo è creare due iteratori e utilizzare il primo per contare gli elementi e il secondo per leggere il file. BTW. Non usarlo len(list(iterable))caricherà tutti i dati in memoria. È possibile utilizzare: reduce(lambda x, _: x+1, iterable, 0). Modifica: anche il codice Zonda333 con la somma è buono.
Tomasz Wysocki,

1
@ user248237: perché dici che devi sapere quante voci sono disponibili per calcolare una determinata quantità? Potresti semplicemente leggerne un importo fisso e gestire il caso in cui è inferiore a tale importo fisso (davvero semplice da usare usando iterslice). C'è un altro motivo per cui devi leggere tutte le voci?
Kriss,

1
@Tomasz Nota che la riduzione è obsoleta e verrà eliminata in Python 3 e versioni successive.
Wilduck,

7
@Wilduck: non è sparito, è passato afunctools.reduce
Daenyth il

33

Non puoi (tranne il tipo di un particolare iteratore implementa alcuni metodi specifici che lo rendono possibile).

In genere, puoi contare gli elementi dell'iteratore solo consumando l'iteratore. Uno dei modi probabilmente più efficienti:

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)

(Per Python 3.x sostituire itertools.izipcon zip).


3
+1: in un confronto temporale con sum(1 for _ in iterator), questo era quasi il doppio più veloce.
agosto

1
È più preciso dire che consuma un iterabile leggendo ogni elemento in memoria e scartandolo immediatamente.
Rockallite,

È importante notare (che ho trascurato) che l' ordine degli argomenti è zipimportante : se passi zip(counter, iterable), otterrai effettivamente 1 in più rispetto al conteggio iterabile!
Kye W Shi,

risposta molto bella. darebbe grazia su di esso.
Reut Sharabani,

18

Tipo. Si potrebbe verificare il __length_hint__metodo, ma sappiate che (almeno fino a Python 3.4, come gsnedders punti utilmente out) si tratta di un dettaglio di implementazione non documentata ( seguente messaggio in filo ), che potrebbe benissimo scomparire o evocazione nasale demoni, invece.

Altrimenti no. Gli iteratori sono solo un oggetto che espone solo il next()metodo. Puoi chiamarlo tutte le volte che è necessario e potrebbero eventualmente rilanciare StopIteration. Fortunatamente, questo comportamento è per lo più trasparente per il programmatore. :)


5
Questo non è più il caso, a partire da PEP 424 e Python 3.4. __length_hint__è ora documentato, ma è un suggerimento e non garantisce l'accuratezza.
gsnedders,

12

Mi piace la cardinalità pacchetto per questo, è molto leggero e cerca di utilizzare l'implementazione più veloce possibile a seconda dell'iterabile.

Uso:

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

L' count()attuazione effettiva è la seguente:

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

Presumo che tu possa ancora iterare sull'iteratore se usi quella funzione, sì?
jcollum,

12

Quindi, per coloro che vorrebbero conoscere il riassunto di quella discussione. I punteggi migliori finali per il conteggio di un'espressione di un generatore di 50 milioni usando:

  • len(list(gen)),
  • len([_ for _ in gen]),
  • sum(1 for _ in gen),
  • ilen(gen)(da more_itertool ),
  • reduce(lambda c, i: c + 1, gen, 0),

ordinati in base all'esecuzione (incluso il consumo di memoria), ti sorprenderà:

`` `

1: test_list.py:8: 0.492 KiB

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

('list, sec', 1.9684218849870376)

2: test_list_compr.py:8: 0.867 KiB

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

('list_compr, sec', 2.5885991149989422)

3: test_sum.py:8: 0.859 KiB

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

('sum, sec', 3.441088170016883)

4: more_itertools / more.py: 413: 1.266 KiB

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 KiB

gen = (i for i in data*1000); t0 = monotonic(); reduce(lambda counter, i: counter + 1, gen, 0)

('ridurre, sec', 13.436614598002052) `` '

Quindi, len(list(gen))è il consumo di memoria più frequente e meno


Come hai misurato il consumo di memoria?
normanio

1
Puoi spiegare perché len(list(gen))consumare meno memoria dell'approccio basato sulla riduzione? Il primo crea un nuovo listche comporta l'allocazione di memoria mentre il secondo no. Quindi mi aspetto che quest'ultimo sia più efficiente in termini di memoria. Inoltre, il consumo di memoria dipenderà dal tipo di elemento.
normanio

Cordiali saluti: Posso riprodurre per Python 3.6.8 (su un MacBookPro) che il metodo 1 supera gli altri metodi in termini di runtime (ho saltato il metodo 4).
normanio

len(tuple(iterable))può essere ancora più efficiente: articolo di Nelson Minar
VMAtm

9

Un iteratore è solo un oggetto che ha un puntatore all'oggetto successivo che deve essere letto da un qualche tipo di buffer o stream, è come un LinkedList in cui non sai quante cose hai fino a quando non le esegui. Gli iteratori sono pensati per essere efficienti perché tutto ciò che fanno è dirti cosa sarà il prossimo tramite i riferimenti invece di usare l'indicizzazione (ma come hai visto perdi la possibilità di vedere quante voci sono le prossime).


2
Un iteratore non assomiglia a un elenco collegato. Un oggetto restituito da un iteratore non punta all'oggetto successivo e questi oggetti non sono (necessariamente) memorizzati. Piuttosto, può produrre oggetti uno dopo l'altro, in base a qualsiasi logica interna (che potrebbe essere, ma non deve essere, basata su un elenco memorizzato).
Tom,

1
@Tom stavo usando LinkedList come esempio soprattutto perché non sai quanto hai, dato che sai solo cosa c'è dopo in un certo senso (se c'è qualcosa). Mi scuso se la mia formulazione sembra un po 'fuori posto o se ho insinuato che sono una cosa sola.
Jesus Ramos,

8

Per quanto riguarda la tua domanda originale, la risposta è ancora che non c'è modo in generale di conoscere la lunghezza di un iteratore in Python.

Dato che la tua domanda è motivata da un'applicazione della libreria pysam, posso dare una risposta più specifica: sono un collaboratore di PySAM e la risposta definitiva è che i file SAM / BAM non forniscono un conteggio esatto di letture allineate. Né queste informazioni sono facilmente disponibili da un file indice BAM. La cosa migliore da fare è stimare il numero approssimativo di allineamenti utilizzando la posizione del puntatore del file dopo aver letto un numero di allineamenti ed estrapolato in base alla dimensione totale del file. Questo è sufficiente per implementare una barra di avanzamento, ma non un metodo per contare gli allineamenti in tempo costante.


6

Un rapido benchmark:

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

I risultati:

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

Cioè il semplice count_iter_items è la strada da percorrere.

Adattandolo per 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)

Nota: questo test si basa su python2
normanius

3

Esistono due modi per ottenere la lunghezza di "qualcosa" su un computer.

Il primo modo è quello di memorizzare un conteggio - questo richiede tutto ciò che tocca il file / i dati per modificarlo (o una classe che espone solo le interfacce - ma si riduce alla stessa cosa).

L'altro modo è iterare su di esso e contare quanto è grande.


0

È prassi comune inserire questo tipo di informazioni nell'intestazione del file e consentire a pysam di accedervi. Non conosco il formato, ma hai controllato l'API?

Come altri hanno già detto, non puoi conoscere la lunghezza dall'iteratore.


0

Questo è in contrasto con la definizione stessa di un iteratore, che è un puntatore a un oggetto, oltre a informazioni su come raggiungere l'oggetto successivo.

Un iteratore non sa quante altre volte sarà in grado di iterare fino al termine. Questo potrebbe essere infinito, quindi l'infinito potrebbe essere la tua risposta.


Non sta violando nulla e non c'è nulla di sbagliato nell'applicazione della conoscenza precedente quando si utilizza un iteratore. Ci sono miliardi di iteratori intorno, dove sai, che il numero di elementi è limitato. Pensa semplicemente al filtraggio di un elenco, puoi facilmente dare la massima lunghezza, non sai davvero quanti degli elementi si adattano effettivamente alla tua condizione di filtro. Volere conoscere il numero di elementi corrispondenti è un'applicazione valida, non violando alcuna idea misteriosa di un iteratore.
Michael,

0

Sebbene in generale non sia possibile eseguire ciò che è stato richiesto, è spesso utile avere un conteggio di quanti elementi sono stati ripetuti dopo averli ripetuti. Per questo, puoi usare jaraco.itertools.Counter o simili. Ecco un esempio usando Python 3 e rwt per caricare il pacchetto.

$ 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

-1
def count_iter(iter):
    sum = 0
    for _ in iter: sum += 1
    return sum

-1

Presumibilmente, vuoi contare il numero di elementi senza iterare, in modo che l'iteratore non sia esaurito e lo usi di nuovo in seguito. Questo è possibile con copyodeepcopy

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

L'output è "Finding the length did not exhaust the iterator! "

Facoltativamente (e sconsigliato), è possibile ombreggiare la lenfunzione integrata come segue:

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

1
Gli intervalli non sono iteratori. Esistono alcuni tipi di iteratori che possono essere copiati, ma altri causano il fallimento di questo codice con un TypeError (ad es. Generatori) e l'iterazione attraverso un iteratore copiato può causare effetti collaterali due volte o causare interruzioni arbitrarie nel codice che, ad esempio, ha restituito un mapiteratore in attesa che le chiamate di funzione risultanti si verifichino una sola volta.
user2357112 supporta Monica
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.