Todos sabemos que la forma común de ejecutar una declaración un cierto número de veces en Python es usar un bucle for.

La forma general de hacer esto es,

# I am assuming iterated list is redundant.
# Just the number of execution matters.
for _ in range(count):
    pass

Creo que nadie discutirá que el código anterior es la implementación común, sin embargo, hay otra opción. Usando la velocidad de creación de listas de Python multiplicando referencias.

# Uncommon way.
for _ in [0] * count:
    pass

También existe la antigua forma while.

i = 0
while i < count:
    i += 1

Probé los tiempos de ejecución de estos enfoques. Aquí está el código.

import timeit

repeat = 10
total = 10

setup = """
count = 100000
"""

test1 = """
for _ in range(count):
    pass
"""

test2 = """
for _ in [0] * count:
    pass
"""

test3 = """
i = 0
while i < count:
    i += 1
"""

print(min(timeit.Timer(test1, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test2, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test3, setup=setup).repeat(repeat, total)))

# Results
0.02238852552017738
0.011760978361696095
0.06971727824807639

No iniciaría el tema si hubiera una pequeña diferencia, sin embargo, se puede ver que la diferencia de velocidad es del 100%. ¿Por qué Python no fomenta ese uso si el segundo método es mucho más eficiente? ¿Hay una mejor manera?

La prueba se realiza con Windows 10 y Python 3.6 .

Siguiendo la sugerencia de @Tim Peters,

.
.
.
test4 = """
for _ in itertools.repeat(None, count):
    pass
"""
print(min(timeit.Timer(test1, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test2, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test3, setup=setup).repeat(repeat, total)))
print(min(timeit.Timer(test4, setup=setup).repeat(repeat, total)))

# Gives
0.02306803115612352
0.013021619340942758
0.06400113461638746
0.008105080015739174

Lo que ofrece una forma mucho mejor, y esto responde a mi pregunta.

¿Por qué es esto más rápido que range, ya que ambos son generadores? ¿Es porque el valor nunca cambia?

67
Max Paython 29 oct. 2017 a las 05:30

4 respuestas

La mejor respuesta

Usando

for _ in itertools.repeat(None, count)
    do something

Es la forma no obvia de obtener lo mejor de todos los mundos: un pequeño requisito de espacio constante y no se crean nuevos objetos por iteración. Debajo de las cubiertas, el código C para repeat usa un tipo entero C nativo (¡no un objeto entero Python!) Para realizar un seguimiento del recuento restante.

Por esa razón, la cuenta debe caber en el tipo de plataforma C ssize_t, que generalmente es a lo sumo 2**31 - 1 en un cuadro de 32 bits, y aquí en un cuadro de 64 bits:

>>> itertools.repeat(None, 2**63)
Traceback (most recent call last):
    ...
OverflowError: Python int too large to convert to C ssize_t

>>> itertools.repeat(None, 2**63-1)
repeat(None, 9223372036854775807)

Lo cual es bastante grande para mis bucles ;-)

92
Peter Mortensen 21 nov. 2017 a las 22:32

El primer método (en Python 3) crea un objeto de rango, que puede iterar a través del rango de valores. (Es como un objeto generador, pero puede recorrerlo varias veces). No ocupa mucha memoria porque no contiene todo el rango de valores, solo un valor actual y un valor máximo, donde sigue aumentando en tamaño de paso (predeterminado 1) hasta que alcanza o supera el máximo.

Compare el tamaño de range(0, 1000) con el tamaño de list(range(0, 1000)): ¡Pruébelo en línea!. El primero es muy eficiente en memoria; solo toma 48 bytes independientemente del tamaño, mientras que la lista completa aumenta linealmente en términos de tamaño.

El segundo método, aunque más rápido, ocupa ese recuerdo del que estaba hablando en el pasado. (Además, parece que aunque 0 ocupa 24 bytes y None toma 16, las matrices de 10000 de cada una tienen el mismo tamaño. Interesante. Probablemente porque son punteros)

Curiosamente, [0] * 10000 es más pequeño que list(range(10000)) en aproximadamente 10000, lo que tiene sentido porque en el primero, todo tiene el mismo valor primitivo para que pueda optimizarse.

El tercero también es bueno porque no requiere otro valor de pila (mientras que llamar range requiere otro lugar en la pila de llamadas), aunque dado que es 6 veces más lento, no vale la pena.

El último podría ser el más rápido solo porque itertools es genial de esa manera: P Creo que usa algunas optimizaciones de la biblioteca C, si no recuerdo mal.

11
HyperNeutrino 2 nov. 2017 a las 02:03

Esta respuesta proporciona una construcción de bucle por conveniencia. Para obtener más información sobre los bucles con itertools.repeat, consulte la respuesta de Tim Peters arriba, la respuesta de Alex Martelli aquí y la respuesta de Raymond Hettinger aquí.

# loop.py

"""
Faster for-looping in CPython for cases where intermediate integers
from `range(x)` are not needed.

Example Usage:
--------------

from loop import loop

for _ in loop(10000):
    do_something()

# or:

results = [calc_value() for _ in loop(10000)]
"""

from itertools import repeat
from functools import partial

loop = partial(repeat, None)
0
Darkonaut 19 feb. 2019 a las 21:56

Los dos primeros métodos deben asignar bloques de memoria para cada iteración, mientras que el tercero solo daría un paso para cada iteración.

El rango es una función lenta, y lo uso solo cuando tengo que ejecutar código pequeño que no requiere velocidad, por ejemplo, range(0,50). Creo que no puedes comparar los tres métodos; Son totalmente diferentes.

Según un comentario a continuación, el primer caso solo es válido para Python 2.7, en Python 3 funciona como xrange y no asigna un bloque para cada iteración. Lo probé y él tiene razón.

0
Peter Mortensen 21 nov. 2017 a las 22:34