Obtener el número de elementos en un iterador en Python

Respuestas:

101

No, no es posible.

Ejemplo:

import random

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

iterator = gen(10)

La longitud de iteratores desconocida hasta que la repita.

Tomasz Wysocki
fuente
14
Alternativamente, def gen(): yield random.randint(0, 1)es infinito, por lo que nunca podrá encontrar una longitud iterando a través de ella.
tgray
1
Entonces, para validar lo obvio: la mejor manera de obtener el "tamaño" de un iterador es simplemente contar la cantidad de veces que ha pasado por la iteración, ¿verdad? En este caso, sería numIters = 0 ; while iterator: numIters +=1?
Mike Williamson
Interesante, así que es el problema de detención
Akababa
231

Este código debería funcionar:

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

Aunque itera por cada elemento y los cuenta, es la forma más rápida de hacerlo.

También funciona cuando el iterador no tiene ningún elemento:

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

Por supuesto, se ejecuta para siempre para una entrada infinita, así que recuerda que los iteradores pueden ser infinitos:

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

Además, tenga en cuenta que el iterador se agotará al hacer esto, y los intentos adicionales de usarlo no verán elementos . Esa es una consecuencia inevitable del diseño del iterador de Python. Si desea conservar los elementos, deberá almacenarlos en una lista o algo así.

John Howard
fuente
10
Me parece que esto hace exactamente lo que OP no quiere hacer: recorrer el iterador y contar.
Adam Crossland
36
Esta es una forma eficiente de contar los elementos en un iterable
Capitán Lepton el
9
Si bien esto no es lo que OP quiere, dado que su pregunta no tiene una respuesta, esta respuesta evita la creación de instancias de una lista, y es empíricamente más rápida por una constante que el método de reducción mencionado anteriormente.
Phillip Nordwall el
55
No puedo ayudar: ¿es la _referencia a Perl $_? :)
Alois Mahdal
17
@AloisMahdal No. En Python es convencional usar el nombre _de una variable ficticia cuyo valor no le interesa.
Taymon
67

No, cualquier método requerirá que resuelva cada resultado. Tu puedes hacer

iter_length = len(list(iterable))

pero ejecutar eso en un iterador infinito, por supuesto, nunca volverá. También consumirá el iterador y deberá restablecerse si desea utilizar los contenidos.

Decirnos qué problema real está tratando de resolver podría ayudarnos a encontrarle una mejor manera de lograr su objetivo real.

Editar: el uso list()leerá todo el iterable en la memoria a la vez, lo que puede ser indeseable. Otra forma es hacer

sum(1 for _ in iterable)

como otra persona publicó. Eso evitará mantenerlo en la memoria.

Daenyth
fuente
El problema es que estoy leyendo un archivo con "pysam" que tiene millones de entradas. Pysam devuelve un iterador. Para calcular una cierta cantidad, necesito saber cuántas lecturas hay en el archivo, pero no necesito leer cada una ... ese es el problema.
66
No soy usuario de pysam, pero probablemente esté leyendo el archivo "perezoso". Tiene sentido porque no desea tener un archivo grande en la memoria. Entonces si debes saber que no. de registros antes de la iteración, la única forma es crear dos iteradores, y usar el primero para contar elementos y el segundo para leer el archivo. Por cierto. No lo use len(list(iterable)), cargará todos los datos en la memoria. Se puede utilizar: reduce(lambda x, _: x+1, iterable, 0). Editar: el código Zonda333 con suma también es bueno.
Tomasz Wysocki
1
@ user248237: ¿por qué dice que necesita saber cuántas entradas están disponibles para calcular una determinada cantidad? Podrías leer una cantidad fija de ellos y administrar el caso cuando haya menos de esa cantidad fija (realmente simple de usar usando iterslice). ¿Hay alguna otra razón por la que tenga que leer todas las entradas?
kriss
1
@Tomasz Tenga en cuenta que reducir está en desuso y desaparecerá en Python 3 y versiones posteriores.
Wilduck
77
@Wilduck: No se ha ido, solo se mudó afunctools.reduce
Daenyth el
33

No puede (excepto el tipo de un iterador particular implementa algunos métodos específicos que lo hacen posible).

En general, puede contar los elementos del iterador solo consumiendo el iterador. Probablemente una de las formas más eficientes:

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)

(Para Python 3.x reemplace itertools.izipcon zip).

zuo
fuente
3
+1: en una comparación de tiempo sum(1 for _ in iterator), esto fue casi el doble de rápido.
agosto
1
Es más exacto decir que consume un iterable al leer cada elemento en la memoria y descartarlo de inmediato.
Rockallite
Es importante tener en cuenta (lo que pasé por alto) que el orden de los argumentos es zipimportante : si apruebas zip(counter, iterable), ¡en realidad obtendrás 1 más que el recuento iterable!
Kye W Shi
Muy buena respuesta. daría recompensa por ello.
Reut Sharabani
18

Un poco Usted podría comprobar el __length_hint__método, pero se advirtió que (al menos hasta Python 3.4, como gsnedders señala amablemente a cabo) que es un detalle de implementación indocumentado ( siguiente mensaje en el hilo ), que muy bien podría desaparecer o convocar nasal demonios en su lugar.

De otra manera no. Los iteradores son solo un objeto que solo expone el next()método. Puede llamarlo tantas veces como sea necesario y pueden o no aumentar eventualmente StopIteration. Afortunadamente, este comportamiento es casi siempre transparente para el codificador. :)

badp
fuente
55
Este ya no es el caso, a partir de PEP 424 y Python 3.4. __length_hint__ahora está documentado, pero es una pista y no garantiza la precisión.
gsnedders
12

Me gusta el paquete de cardinalidad para esto, es muy liviano e intenta usar la implementación más rápida posible según el iterable.

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

La count()implementación real es la siguiente:

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
fuente
Supongo que aún puede iterar sobre el iterador si usa esa función, ¿sí?
jcollum
12

Entonces, para aquellos que desean conocer el resumen de esa discusión. Los puntajes máximos finales para contar una expresión generadora de 50 millones de longitud usando:

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

ordenado por el rendimiento de la ejecución (incluido el consumo de memoria), te sorprenderá:

`` `

1: test_list.py:8: 0.492 KiB

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

('lista, seg', 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()

('suma, seg', 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)

('reducir, seg', 13.436614598002052) `` `

Entonces, len(list(gen))es el consumible más frecuente y con menos memoria

Alex-Bogdanov
fuente
¿Cómo midió el consumo de memoria?
normanius
1
¿Puede explicar por qué len(list(gen))debería consumir menos memoria que el enfoque basado en reducir? El primero crea un nuevo listque implica la asignación de memoria, mientras que el segundo no debería. Por lo tanto, esperaría que este último sea más eficiente en memoria. Además, el consumo de memoria dependerá del tipo de elemento.
normanius
FYI: puedo reproducir para Python 3.6.8 (en un MacBookPro) que el método 1 supera a los otros métodos en términos de tiempo de ejecución (omití el método 4).
normanius
len(tuple(iterable))puede ser aún más eficiente: artículo de Nelson Minar
VMAtm
9

Un iterador es solo un objeto que tiene un puntero al siguiente objeto para ser leído por algún tipo de búfer o flujo, es como un LinkedList donde no sabes cuántas cosas tienes hasta que iteras a través de ellas. Los iteradores están destinados a ser eficientes porque todo lo que hacen es decirle lo que sigue por referencias en lugar de usar indexación (pero como vieron, pierden la capacidad de ver cuántas entradas son las siguientes).

Jesus Ramos
fuente
2
Un iterador no se parece en nada a una lista vinculada. Un objeto devuelto por un iterador no apunta al siguiente objeto, y estos objetos no (necesariamente) se almacenan en la memoria. Más bien, puede producir objetos uno tras otro, en función de la lógica interna (que podría ser, pero no tiene que ser, según una lista almacenada).
Tom
1
@Tom Estaba usando LinkedList como ejemplo principalmente porque no sabes cuánto tienes, ya que solo sabes lo que sigue en un sentido (si hay algo). Pido disculpas si mi redacción parece un poco desagradable o si implico que son una en la misma.
Jesús Ramos
8

Con respecto a su pregunta original, la respuesta sigue siendo que, en general, no hay forma de saber la longitud de un iterador en Python.

Dado que su pregunta está motivada por una aplicación de la biblioteca pysam, puedo darle una respuesta más específica: contribuyo a PySAM y la respuesta definitiva es que los archivos SAM / BAM no proporcionan un recuento exacto de lecturas alineadas. Tampoco esta información está fácilmente disponible desde un archivo de índice BAM. Lo mejor que puede hacer es estimar el número aproximado de alineaciones mediante el uso de la ubicación del puntero del archivo después de leer un número de alineaciones y extrapolar en función del tamaño total del archivo. Esto es suficiente para implementar una barra de progreso, pero no un método para contar alineaciones en tiempo constante.

Kevin Jacobs
fuente
6

Un punto de referencia rápido:

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

Los resultados:

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

Es decir, el simple count_iter_items es el camino a seguir.

Ajustando esto para 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)
Miguel
fuente
Nota: esta prueba se basa en python2
normanius el
3

Hay dos formas de obtener la longitud de "algo" en una computadora.

La primera forma es almacenar un recuento; esto requiere cualquier cosa que toque el archivo / datos para modificarlo (o una clase que solo expone interfaces, pero se reduce a lo mismo).

La otra forma es iterar sobre él y contar qué tan grande es.

Wayne Werner
fuente
0

Es una práctica común poner este tipo de información en el encabezado del archivo, y para que pysam le dé acceso a esto. No sé el formato, pero ¿has verificado la API?

Como han dicho otros, no se puede saber la longitud del iterador.

tom10
fuente
0

Esto va en contra de la definición misma de un iterador, que es un puntero a un objeto, más información sobre cómo llegar al siguiente objeto.

Un iterador no sabe cuántas veces más podrá iterar hasta que finalice. Esto podría ser infinito, por lo que el infinito podría ser su respuesta.

FCAlive
fuente
No está violando nada, y no hay nada de malo en aplicar el conocimiento previo al usar un iterador. Hay millones de iteradores alrededor, donde sabes, que el número de elementos es limitado. Piense simplemente en filtrar una lista, puede dar fácilmente la longitud máxima, simplemente no sabe cuántos de los elementos realmente se ajustan a su condición de filtro. Querer saber el número de elementos coincidentes es una aplicación válida, que no viola ninguna idea misteriosa de un iterador.
Michael
0

Aunque en general no es posible hacer lo que se le ha pedido, a menudo es útil contar cuántos elementos se repitieron después de iterar sobre ellos. Para eso, puede usar jaraco.itertools.Counter o similar. Aquí hay un ejemplo usando Python 3 y rwt para cargar el paquete.

$ 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
fuente
-1
def count_iter(iter):
    sum = 0
    for _ in iter: sum += 1
    return sum
Hasen
fuente
-1

Presumiblemente, desea contar el número de elementos sin iterar, para que el iterador no se agote y lo use nuevamente más tarde. Esto es posible 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")

El resultado es " Finding the length did not exhaust the iterator!"

Opcionalmente (y desaconsejado), puede lenseguir la función incorporada de la siguiente manera:

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
Palillo de anémona
fuente
1
Los rangos no son iteradores. Hay algunos tipos de iteradores que se pueden copiar, pero otros harán que este código falle con un error de tipo (por ejemplo, generadores), y la iteración a través de un iterador copiado puede causar efectos secundarios dos veces o causar una rotura arbitraria en el código que, por ejemplo, devolvió un mapiterador esperando que las llamadas de función resultantes sucedan solo una vez.
user2357112 es compatible con Monica el