utiliza - Una mejor manera para un bucle ''for'' de Python
para que se utiliza while en python (4)
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 está el camino antiguo.
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 el
range
, ya que ambos son generadores?
¿Es porque el valor nunca cambia?
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 del paso (predeterminado 1) hasta que alcanza o supera el máximo.
Compare el tamaño del
range(0, 1000)
con el tamaño de la
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
ocupa 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 la
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 el
range
llamadas 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: PI creo que usa algunas optimizaciones de la biblioteca C, si no recuerdo mal.
Esta respuesta proporciona una construcción de bucle por conveniencia.
Para obtener más información sobre los bucles con
itertools.repeat
búsqueda de la respuesta de Tim Peters
above
, la respuesta de Alex Martelli
here
y la respuesta de Raymond Hettinger
here
.
# 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)
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 un 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 tiene razón.
Utilizando
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
utiliza un tipo entero C nativo (¡no un objeto entero Python!) Para realizar un seguimiento del recuento restante.
Por esa razón, el recuento debe caber en el tipo de plataforma C
ssize_t
, que generalmente es como máximo
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 ;-)