2011-03-21 23 views
48

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, yo definiría¿Cuál es la forma más corta de contar la cantidad de elementos en un generador/iterador?

def ilen(it): 
    return sum(itertools.imap(lambda _: 1, it)) # or just map in Python 3 

pero entiendo lambda está cerca de ser considerada perjudicial, y lambda _: 1 ciertamente no es bastante.

(El caso de uso de este está contando el número de líneas en un archivo de texto que coincide con una expresión regular, es decir, grep -c.)

+4

Por favor, no use '_' como nombre de variable, debido a que (1) tiende a confundir a la gente, haciéndoles creer que esto es una especie de sintaxis especial, (2) choca con' _' en el intérprete interactivo y (3) colisiona con el alias gettext común. –

+4

@Sven: uso '_' todo el tiempo para las variables no utilizadas (un hábito de la programación de Prolog y Haskell). (1) es una razón para preguntar esto en primer lugar. No consideré (2) y (3), ¡gracias por señalarlos! –

+2

duplicado: http://stackoverflow.com/questions/390852/is-here-any-built-in-way-to-get-the-length-of-an-iterable-in-python – tokland

Respuesta

92

La forma habitual es

sum(1 for i in it) 
+1

puede usar 'len (list (it)) '- o si los elementos son únicos, entonces' len (set (it)) 'para guardar un caracter. – F1Rumors

+6

@ F1Rumors Usar 'len (list (it))' está bien en la mayoría de los casos. Sin embargo, cuando tiene un iterador perezoso que produce muchos elementos, no quiere almacenarlos todos en la memoria al mismo tiempo solo para contarlos, lo cual se evita utilizando el código en esta respuesta. –

+0

estuvo de acuerdo: como respuesta, se basaba en que el "código más corto" era más importante que la "memoria más baja". – F1Rumors

5

Una forma corta es:

def ilen(it): 
    return len(list(it)) 

Tenga en cuenta que si está generando un lote de elementos (digamos, decenas de miles o más), luego 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.

+0

Había pensado en esto, pero el rendimiento sí importa ya que a menudo proceso archivos de texto grandes. –

+6

Mientras no se quede sin memoria, esta solución es bastante buena en cuanto a rendimiento, ya que hará el ciclo en código C puro; todos los objetos tienen que generarse de todos modos. Incluso para iteradores grandes, esto es más rápido que 'sum (1 para i en él)' siempre y cuando todo encaje en la memoria. –

14

Método que es significativamente más rápido que sum(1 for i in it) cuando el iterable puede ser largo (y no significativamente más lenta cuando el iterable es corto), mientras se mantiene el comportamiento de sobrecarga de memoria fija (a diferencia de len(list(it))) para evitar thrashing de intercambio y los gastos generales reasignación para las entradas 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) 

Como len(list(it)) que realiza el bucle en el código C en CPython (deque, count y zip se implementan 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 llegar a casos de prueba justos para comparar el rendimiento (list tramposos que utilizan __length_hint__ que no es probable que esté disponible para iterables de entrada arbitrarias, itertools funciones que no proporcionan __length_hint__ menudo tienen modos de funcionamiento especiales que funcionan más rápido cuando se libera el valor devuelto en cada ciclo antes de que se solicite el siguiente valor, lo que hará deque con maxlen=0). El caso de prueba que utilicé fue crear una función de generador que tomaría una entrada y devolver un generador de nivel C que carecía especial itertools optimizaciones Devolución de envases o __length_hint__, usando Python 3.3 de yield from:

def no_opt_iter(it): 
    yield from it 

Luego, utilizando ipython%timeit magia (sustituyendo diferentes constantes de 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)) podría causar problemas de memoria, en un Linux ejecutando Python 3.5 x64, mi solución tarda aproximadamente un 50% más tha n def ilen(it): return len(list(it)), independientemente de la longitud de entrada.

Para la más pequeña de las entradas, los costos de instalación para llamar deque/zip/count/next significa que se necesita infinitesimalmente ya de esta manera que def ilen(it): sum(1 for x in it) (alrededor de 200 ns más en mi máquina para una entrada de longitud 0, que es un 33% aumentar sobre el enfoque simple sum), 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 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 la brevedad, utilice esta solución. Si las entradas son limitadas y más bien 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).

1

Me gusta el paquete 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 
1

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