web-dev-qa-db-fra.com

Une meilleure façon pour une boucle Python 'pour'

Nous savons tous que la manière courante d’exécuter une instruction un certain nombre de fois dans Python consiste à utiliser une boucle for.

La manière générale de le faire est,

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

Je crois que personne ne dira que le code ci-dessus est l'implémentation commune, mais il existe une autre option. Utilisation de la vitesse de Python création de liste en multipliant les références.

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

Il y a aussi l'ancienne manière while.

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

J'ai testé les temps d'exécution de ces approches. Voici le code.

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

Je ne commencerais pas le sujet s'il y avait une petite différence, mais on peut voir que la différence de vitesse est de 100%. Pourquoi Python n'encourage-t-il pas un tel usage si la deuxième méthode est beaucoup plus efficace? Existe-t-il un meilleur moyen?

Le test est effectué avec Windows 1 et Python 3.6.

Suivant la suggestion 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

Ce qui offre une bien meilleure façon, et cela répond assez bien à ma question.

Pourquoi est-ce plus rapide que range, puisque les deux sont des générateurs. Est-ce parce que la valeur ne change jamais?

65
Max Paython

En utilisant

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

est le moyen non évident d’obtenir le meilleur de tous les mondes: un minimum d’espace constant et aucun nouvel objet créé par itération. Sous les couvertures, le code C de repeat utilise un type entier C natif (pas un Python!) Pour suivre le nombre restant.

Pour cette raison, le nombre doit tenir dans la plate-forme C ssize_t type, qui est généralement au plus 2**31 - 1 sur une boîte 32 bits, et ici sur une boîte 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)

Ce qui est très gros pour mes boucles ;-)

91
Tim Peters

La première méthode (in Python 3) crée un objet range, qui peut parcourir la plage de valeurs. (C'est comme un objet générateur mais vous pouvez le parcourir plusieurs fois.) Cela ne fonctionne pas. t occupe beaucoup de mémoire car il ne contient pas toute la plage de valeurs, mais seulement une valeur actuelle et une valeur maximale, où il continue d’augmenter de la taille du pas (valeur par défaut 1) jusqu’à atteindre ou dépasser le maximum.

Comparez la taille de range(0, 1000) à la taille de list(range(0, 1000)): essayez-le en ligne! . Le premier est très efficace en mémoire; cela ne prend que 48 octets quelle que soit la taille, alors que la liste entière augmente linéairement en termes de taille.

La deuxième méthode, bien que plus rapide, utilise la mémoire dont je parlais auparavant. (En outre, il semble que bien que 0 Occupe 24 octets et que None en prenne 16, les tableaux de 10000 Ont la même taille. Intéressant. Probablement parce qu'ils sont des pointeurs )

Il est intéressant de noter que [0] * 10000 Est plus petit que list(range(10000)) d'environ 10000, ce qui a du sens, car dans le premier, tout a la même valeur primitive et peut donc être optimisé.

Le troisième est également Nice car il n’exige pas de valeur de pile supplémentaire (alors que l’appel range nécessite un autre emplacement sur la pile d’appels), bien qu’il soit 6 fois plus lent, cela ne vaut pas la peine.

Le dernier pourrait être le plus rapide simplement parce que itertools est cool comme ça: P Je pense qu'il utilise certaines optimisations de la bibliothèque C, si je me souviens bien.

11
HyperNeutrino

Cette réponse fournit une construction de boucle pour plus de commodité. Pour plus d'informations sur la mise en boucle avec itertools.repeat cherchez la réponse de Tim Peters ci-dessus , la réponse d'Alex Martelli ici et la réponse de Raymond Hettinger ici .

# 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

Les deux premières méthodes doivent allouer des blocs de mémoire pour chaque itération, tandis que la troisième ne fait qu'un pas pour chaque itération.

Range est une fonction lente, et je ne l'utilise que lorsque je dois exécuter un petit code qui ne nécessite pas de vitesse, par exemple, range(0,50). Je pense que vous ne pouvez pas comparer les trois méthodes; ils sont totalement différents.

Selon un commentaire ci-dessous, le premier cas n'est valable que pour Python 2.7, dans Python 3, il fonctionne comme xrange et n'attribue pas de bloc pour chaque itération. Je l'ai testé et il a raison.

0
Mr. bug