Un meilleur moyen pour une boucle Python ‘for’

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.