iteradores - python funcion yield
¿Cuál es la forma más corta de contar la cantidad de elementos en un generador/iterador? (5)
Si quiero la cantidad de elementos en un iterable sin preocuparme por los elementos en sí, ¿cuál sería la manera pitónica de conseguirlo? En este momento, definiría
def ilen(it):
return sum(itertools.imap(lambda _: 1, it)) # or just map in Python 3
pero entiendo que lambda
está cerca de ser considerado dañino, y lambda _: 1
ciertamente no es bonito.
(El caso de uso de esto es contar el número de líneas en un archivo de texto que coincida con una expresión regular, es decir, grep -c
).
La forma habitual es
sum(1 for i in it)
Método que es significativamente más rápido que la sum(1 for i in it)
cuando el iterable puede ser largo (y no significativamente más lento cuando el iterable es corto), mientras mantiene el comportamiento fijo de memoria fija (a diferencia de len(list(it))
) para evitar swap sobrecarga de reasignación y reasignación para insumos más grandes:
# On Python 2 only, get zip that lazily generates results instead of returning list
from future_builtins import zip
from collections import deque
from itertools import count
def ilen(it):
# Make a stateful counting iterator
cnt = count()
# zip it with the input iterator, then drain until input exhausted at C level
deque(zip(it, cnt), 0) # cnt must be second zip arg to avoid advancing too far
# Since count 0 based, the next value is the count
return next(cnt)
Al igual que len(list(it))
realiza el ciclo en código C en CPython ( deque
, count
y zip
están todos implementados en C); evitar la ejecución del código de bytes por ciclo suele ser la clave del rendimiento en CPython.
Es sorprendentemente difícil encontrar casos de prueba justos para comparar el rendimiento ( list
trucos usando __length_hint__
que probablemente no estará disponible para los iterables de entrada arbitrarios, itertools
funciones de los itertools
que no proporcionan __length_hint__
menudo tienen modos de operación especiales que funcionan más rápido cuando el valor devuelto en cada ciclo se libera liberado antes de que se solicite el siguiente valor, lo que deque
con maxlen=0
hará). El caso de prueba que utilicé fue para crear una función de generador que tomaría una entrada y devolvería un generador de nivel C que carecía de itertools
retorno especiales optimizaciones de contenedor de retorno o __length_hint__
, utilizando el yield from
Python 3.3 yield from
:
def no_opt_iter(it):
yield from it
Luego, usando ipython
%timeit
magic (sustituyendo constantes diferentes por 100):
>>> %%timeit -r5 fakeinput = (0,) * 100
... ilen(no_opt_iter(fakeinput))
Cuando la entrada no es lo suficientemente grande como para que len(list(it))
cause problemas de memoria, en una caja de Linux que ejecute Python 3.5 x64, mi solución tarda aproximadamente un 50% más que def ilen(it): return len(list(it))
, independientemente de la longitud de entrada.
Para las entradas más pequeñas, los costos de configuración para llamar a deque
/ zip
/ count
/ next
significa que demora infinitesimalmente más tiempo que def ilen(it): sum(1 for x in it)
(aproximadamente 200 ns más en mi máquina para un entrada de longitud 0, que es un aumento del 33% sobre el enfoque de sum
simple), pero para entradas más largas, se ejecuta en aproximadamente la mitad del tiempo por elemento adicional; para las entradas de longitud 5, el costo es equivalente, y en algún lugar del rango de 50-100, la sobrecarga inicial es imperceptible en comparación con el trabajo real; el enfoque de sum
toma aproximadamente el doble de tiempo.
Básicamente, si el uso de la memoria es importante o las entradas no tienen un tamaño limitado y usted se preocupa por la velocidad más que por la brevedad, use esta solución. Si las entradas son limitadas y pequeñas, len(list(it))
es probablemente la mejor, y si son ilimitadas, pero el conteo de simplicidad / brevedad, usaría sum(1 for x in it)
.
Me gusta el paquete de cardinality para esto, es muy liviano y trata de 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
Un corto camino es:
def ilen(it):
return len(list(it))
Tenga en cuenta que si está generando muchos elementos (por ejemplo, decenas de miles o más), ponerlos en una lista puede convertirse en un problema de rendimiento. Sin embargo, esta es una expresión simple de la idea de que el rendimiento no va a importar en la mayoría de los casos.
more_itertools
es una biblioteca de terceros que implementa una herramienta ilen
. pip install more_itertools
import more_itertools as mit
mit.ilen(x for x in range(10))
# 10