Comment réduire l'utilisation de la mémoire et accélérer le code Python à l'aide de générateurs

Bonjour à tous. Aujourd'hui, nous voulons partager une traduction utile, préparée avant le lancement du cours "Développeur Web en Python" . L'écriture de code efficace en temps et en mémoire en Python est particulièrement importante lors de la création d'une application Web, d'un modèle d'apprentissage automatique ou de tests.



Quand j'ai commencé à apprendre les générateurs en Python, je ne savais pas à quel point ils étaient importants. Cependant, ils m'ont constamment aidé à écrire des fonctions tout au long de mon parcours à travers l'apprentissage automatique.


Les fonctions de générateur vous permettent de déclarer une fonction qui se comportera comme un itérateur. Ils permettent aux programmeurs de créer des itérateurs rapides, simples et propres. Un itérateur est un objet qui peut être répété (bouclé). Il est utilisé pour abstraire le conteneur de données et le faire se comporter comme un objet itérable. Par exemple, un exemple d'objet itérable peut être des chaînes, des listes et des dictionnaires.


Le générateur ressemble à une fonction, mais utilise le mot-clé yield au lieu de return. Regardons un exemple pour le rendre plus clair.


def generate_numbers(): n = 0 while n < 3: yield n n += 1 

Il s'agit d'une fonction de générateur. Lorsque vous l'appelez, il renvoie un objet générateur.


 >>> numbers = generate_numbers() >>> type(numbers) <class 'generator'> 

Il est important de faire attention à la façon dont l'état est encapsulé dans le corps de la fonction de générateur. Vous pouvez parcourir une à la fois en utilisant la fonction intégrée next ():


 >>> next_number = generate_numbers() >>> next(next_number) 0 >>> next(next_number) 1 >>> next(next_number) 2 

Que se passe-t-il si vous appelez next () après la fin de l'exécution?


StopIteration est un type d'exception intégré qui se produit automatiquement dès que le générateur cesse de renvoyer un résultat. Il s'agit d'un signal d'arrêt pour la boucle for.


Déclaration de rendement


Sa tâche principale est de contrôler le flux de la fonction de générateur afin qu'elle ressemble à une instruction de retour. Lorsqu'une fonction de générateur est appelée ou qu'une expression de générateur est utilisée, elle renvoie un itérateur spécial appelé générateur. Pour utiliser un générateur, affectez-le à une variable. Lors de l'appel de méthodes spéciales dans le générateur, comme next (), le code de fonction sera exécuté jusqu'à ce que yield.


Lorsqu'il entre dans l'instruction yield, le programme interrompt la fonction et renvoie la valeur à l'objet qui a lancé l'exécution. (Alors que return arrête complètement l'exécution de la fonction.) Lorsque la fonction est suspendue, son état est conservé.


Maintenant que nous connaissons les générateurs en Python, comparons l'approche habituelle avec l'approche qui utilise des générateurs en termes de mémoire et de temps consacré à l'exécution de code.


Énoncé du problème


Supposons que nous devons parcourir une grande liste de nombres (par exemple, 1 000 000 000) et enregistrer les carrés de tous les nombres qui doivent être stockés séparément dans une autre liste.


Approche habituelle


 import memory_profiler import time def check_even(numbers): even = [] for num in numbers: if num % 2 == 0: even.append(num*num) return even if __name__ == '__main__': m1 = memory_profiler.memory_usage() t1 = time.clock() cubes = check_even(range(100000000)) t2 = time.clock() m2 = memory_profiler.memory_usage() time_diff = t2 - t1 mem_diff = m2[0] - m1[0] print(f"It took {time_diff} Secs and {mem_diff} Mb to execute this method") 

Après avoir exécuté le code ci-dessus, nous obtenons ce qui suit:


 It took 21.876470000000005 Secs and 1929.703125 Mb to execute this method 

Utilisation de générateurs


 import memory_profiler import time def check_even(numbers): for num in numbers: if num % 2 == 0: yield num * num if __name__ == '__main__': m1 = memory_profiler.memory_usage() t1 = time.clock() cubes = check_even(range(100000000)) t2 = time.clock() m2 = memory_profiler.memory_usage() time_diff = t2 - t1 mem_diff = m2[0] - m1[0] print(f"It took {time_diff} Secs and {mem_diff} Mb to execute this method") 

Après avoir exécuté le code ci-dessus, nous obtenons ce qui suit:


 It took 2.9999999995311555e-05 Secs and 0.02656277 Mb to execute this method 

Comme nous pouvons le voir, le temps d'exécution et la mémoire utilisés sont considérablement réduits. Les générateurs fonctionnent selon un principe connu sous le nom de «calcul paresseux». Cela signifie qu'ils peuvent économiser le processeur, la mémoire et d'autres ressources informatiques.


Conclusion


J'espère que dans cet article, j'ai pu montrer comment les générateurs en Python peuvent être utilisés pour économiser des ressources telles que la mémoire et le temps. Cet avantage apparaît du fait que les générateurs ne stockent pas tous les résultats en mémoire, mais les calculent à la volée, et la mémoire n'est utilisée que si nous demandons le résultat des calculs. Les générateurs vous permettent également d'abstraire une grande quantité de code passe-partout, ce qui est nécessaire pour écrire des itérateurs, de sorte qu'ils aident également à réduire la quantité de code.

Source: https://habr.com/ru/post/fr477926/


All Articles