Nous soaps tous que la manière courante d’exécuter une instruction un certain nombre de fois en Python est d’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 la mise en œuvre commune, mais il y a une autre option. Utiliser la vitesse de création de liste Python en multipliant les références.
# Uncommon way. for _ in [0] * count: pass
Il y a aussi l’ancien while
chemin.
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 voudrais pas initier 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 une telle utilisation si la seconde méthode est beaucoup plus efficace? Y a-t-il une meilleure façon?
Le test est effectué avec Windows 10 et Python 3.6 .
Suite à 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 solution, et cela répond à ma question.
Pourquoi est-ce plus rapide que la range
, puisque les deux sont des générateurs. Est-ce parce que la valeur ne change jamais?
En utilisant
for _ in itertools.repeat(None, count) do something
est la manière non évidente de tirer le meilleur parti de tous les mondes: un besoin d’espace constant minuscule et aucun nouvel object créé par itération. Sous les couvertures, le code C pour la repeat
utilise un type entier C natif (et non un object entier Python!) Pour suivre le compte restant.
Pour cette raison, le nombre doit correspondre au type de plate-forme C ssize_t
, qui est généralement d’au plus 2**31 - 1
sur une zone 32 bits, et ici sur une zone 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 bien gros pour mes boucles 😉
La première méthode (dans Python 3) crée un object de plage qui peut parcourir la plage de valeurs. (C’est comme un object générateur mais vous pouvez le parcourir plusieurs fois.) Il ne prend pas beaucoup de mémoire car il ne contient pas toute la plage de valeurs, juste une valeur actuelle et maximale, où il augmente sans cesse. taille du pas (par défaut 1) jusqu’à ce qu’il atteigne ou dépasse le maximum.
Comparez la taille de la range(0, 1000)
à la taille de la 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 seconde méthode, bien que plus rapide, reprend la mémoire dont je parlais dans le passé. (En outre, il semble que bien que 0
prenne 24 octets et que None
prenne 16 octets, les tableaux de 10000
de chacun 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 est identique pour pouvoir être optimisé.
Le troisième est également intéressant car il ne nécessite pas une autre valeur de stack (alors que la range
appels nécessite un autre emplacement sur la stack d’appels).
Le dernier pourrait être le plus rapide car itertools
est cool de cette façon: PI pense qu’il utilise des optimisations de la bibliothèque C, si je me souviens bien.
Les deux premières méthodes ont besoin d’allouer des blocs de mémoire pour chaque itération, tandis que la troisième ne ferait qu’une étape pour chaque itération.
La scope est une fonction lente et je l’utilise uniquement lorsque je dois exécuter un petit code qui ne nécessite pas de vitesse, par exemple, la 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 valide que pour Python 2.7, dans Python 3, il fonctionne comme xrange et n’alloue pas de bloc à chaque itération. Je l’ai testé et il a raison.