while utiliza que para for ejemplos con ciclo bucle anidado python python-3.x performance time-complexity

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 ;-)