¿Hay una manera eficiente de saber cuántos elementos hay en un iterador en Python, en general, sin iterar a través de cada uno y contar?
¿Hay una manera eficiente de saber cuántos elementos hay en un iterador en Python, en general, sin iterar a través de cada uno y contar?
Respuestas:
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 iterator
es desconocida hasta que la repita.
def gen(): yield random.randint(0, 1)
es infinito, por lo que nunca podrá encontrar una longitud iterando a través de ella.
numIters = 0 ; while iterator: numIters +=1
?
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í.
_
referencia a Perl $_
? :)
_
de una variable ficticia cuyo valor no le interesa.
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.
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.
functools.reduce
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.izip
con zip
).
sum(1 for _ in iterator)
, esto fue casi el doble de rápido.
zip
importante : si apruebas zip(counter, iterable)
, ¡en realidad obtendrás 1 más que el recuento iterable!
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. :)
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
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á:
`` `
gen = (i for i in data*1000); t0 = monotonic(); len(list(gen))
('lista, seg', 1.9684218849870376)
gen = (i for i in data*1000); t0 = monotonic(); len([i for i in gen])
('list_compr, sec', 2.5885991149989422)
gen = (i for i in data*1000); t0 = monotonic(); sum(1 for i in gen); t1 = monotonic()
('suma, seg', 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)
('reducir, seg', 13.436614598002052) `` `
Entonces, len(list(gen))
es el consumible más frecuente y con menos memoria
len(list(gen))
debería consumir menos memoria que el enfoque basado en reducir? El primero crea un nuevo list
que 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.
len(tuple(iterable))
puede ser aún más eficiente: artículo de Nelson Minar
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).
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.
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)
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.
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.
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
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 copy
odeepcopy
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 len
seguir 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
map
iterador esperando que las llamadas de función resultantes sucedan solo una vez.