Comment optimiser votre code Python même si vous êtes un débutant

Image de l’auteur | Idéogramme
Soyons honnêtes. Lorsque vous apprenez Python, vous ne pensez probablement pas à la performance. Vous essayez juste de faire fonctionner votre code! Mais voici la chose: rendre votre code Python plus rapide ne vous oblige pas à devenir un programmeur expert du jour au lendemain.
Avec quelques techniques simples que je vais vous montrer aujourd’hui, vous pouvez considérablement améliorer la vitesse et l’utilisation de la mémoire de votre code.
Dans cet article, nous parcourons ensemble cinq techniques pratiques d’optimisation adaptées aux débutants. Pour chacun, je vais vous montrer le code « avant » (la façon dont de nombreux débutants l’écrivent), le code « After » (la version optimisée), et expliquer exactement pourquoi l’amélioration fonctionne et combien il devient rapide.
🔗 Lien vers le code sur github
1. Remplacez les boucles par des compréhensions de liste
Commençons par quelque chose que vous faites probablement tout le temps: créer de nouvelles listes en transformant celles existantes. La plupart des débutants atteignent une boucle pour une boucle, mais Python a un moyen beaucoup plus rapide de le faire.
Avant l’optimisation
Voici comment la plupart des débutants auraient une liste de chiffres:
import time
def square_numbers_loop(numbers):
result = ()
for num in numbers:
result.append(num ** 2)
return result
# Let's test this with 1000000 numbers to see the performance
test_numbers = list(range(1000000))
start_time = time.time()
squared_loop = square_numbers_loop(test_numbers)
loop_time = time.time() - start_time
print(f"Loop time: {loop_time:.4f} seconds")
Ce code crée une liste vide appelée Résultat, puis les boucles via chaque numéro dans notre liste d’entrée, le fixent et le consiste à la liste de résultats. Assez simple, non?
Après optimisation
Maintenant, réécrivons cela en utilisant une compréhension de la liste:
def square_numbers_comprehension(numbers):
return (num ** 2 for num in numbers) # Create the entire list in one line
start_time = time.time()
squared_comprehension = square_numbers_comprehension(test_numbers)
comprehension_time = time.time() - start_time
print(f"Comprehension time: {comprehension_time:.4f} seconds")
print(f"Improvement: {loop_time / comprehension_time:.2f}x faster")
Cette seule ligne (num ** 2 for num in numbers) Fait exactement la même chose que notre boucle, mais il indique à Python « Créer une liste où chaque élément est le carré de l’élément correspondant en nombres ».
Sortir:
Loop time: 0.0840 seconds
Comprehension time: 0.0736 seconds
Improvement: 1.14x faster
Amélioration des performances: Les compréhensions de la liste sont généralement plus rapides de 30 à 50% que les boucles équivalentes. L’amélioration est plus visible lorsque vous travaillez avec de très grandes itérables.
Pourquoi cela fonctionne-t-il? Les compréhensions de la liste sont implémentées en C sous le capot, donc elles évitent beaucoup de frais généraux qui sont livrés avec des boucles Python, des choses comme des recherches variables et des appels de fonction qui se produisent dans les coulisses.
2. Choisissez la bonne structure de données pour le travail
Celui-ci est énorme, et c’est quelque chose qui peut rendre votre code des centaines de fois plus vite avec juste un petit changement. La clé est de comprendre quand utiliser les listes par rapport aux ensembles par rapport aux dictionnaires.
Avant l’optimisation
Disons que vous souhaitez trouver des éléments communs entre deux listes. Voici l’approche intuitive:
def find_common_elements_list(list1, list2):
common = ()
for item in list1: # Go through each item in the first list
if item in list2: # Check if it exists in the second list
common.append(item) # If yes, add it to our common list
return common
# Test with reasonably large lists
large_list1 = list(range(10000))
large_list2 = list(range(5000, 15000))
start_time = time.time()
common_list = find_common_elements_list(large_list1, large_list2)
list_time = time.time() - start_time
print(f"List approach time: {list_time:.4f} seconds")
Ce code boucle via la première liste, et pour chaque élément, il vérifie si cet élément existe dans la deuxième liste en utilisant si l’élément en list2. Le problème? Lorsque vous faites un article dans list2Python doit rechercher dans toute la deuxième liste jusqu’à ce qu’il trouve l’élément. C’est lent!
Après optimisation
Voici la même logique, mais en utilisant un ensemble pour des recherches plus rapides:
def find_common_elements_set(list1, list2):
set2 = set(list2) # Convert list to a set (one-time cost)
return (item for item in list1 if item in set2) # Check membership in set
start_time = time.time()
common_set = find_common_elements_set(large_list1, large_list2)
set_time = time.time() - start_time
print(f"Set approach time: {set_time:.4f} seconds")
print(f"Improvement: {list_time / set_time:.2f}x faster")
Tout d’abord, nous convertissons la liste en un ensemble. Ensuite, au lieu de vérifier si l’article list2nous vérifions si l’article dans set2. Ce petit changement rend les tests d’adhésion presque instantanés.
Sortir:
List approach time: 0.8478 seconds
Set approach time: 0.0010 seconds
Improvement: 863.53x faster
Amélioration des performances: Cela peut être de l’ordre de 100x plus rapidement pour les grands ensembles de données.
Pourquoi cela fonctionne-t-il? Les ensembles utilisent des tables de hachage sous le capot. Lorsque vous vérifiez si un élément est dans un ensemble, Python ne recherche pas dans chaque élément; Il utilise le hachage pour sauter directement vers où l’élément devrait être. C’est comme avoir un index d’un livre au lieu de lire chaque page pour trouver ce que vous voulez.
3. Utilisez les fonctions intégrées de Python dans la mesure du possible
Python est livré avec des tonnes de fonctions intégrées qui sont fortement optimisées. Avant d’écrire votre propre boucle ou une fonction personnalisée pour faire quelque chose, vérifiez si Python a déjà une fonction pour cela.
Avant l’optimisation
Voici comment vous pouvez calculer la somme et le maximum d’une liste si vous ne connaissiez pas les intégrés:
def calculate_sum_manual(numbers):
total = 0
for num in numbers:
total += num
return total
def find_max_manual(numbers):
max_val = numbers(0)
for num in numbers(1:):
if num > max_val:
max_val = num
return max_val
test_numbers = list(range(1000000))
start_time = time.time()
manual_sum = calculate_sum_manual(test_numbers)
manual_max = find_max_manual(test_numbers)
manual_time = time.time() - start_time
print(f"Manual approach time: {manual_time:.4f} seconds")
Le sum La fonction commence par un total de 0, puis ajoute chaque nombre à ce total. Le max La fonction commence par supposer que le premier nombre est le maximum, puis compare tous les autres numéros pour voir s’il est plus grand.
Après optimisation
Voici la même chose en utilisant les fonctions intégrées de Python:
start_time = time.time()
builtin_sum = sum(test_numbers)
builtin_max = max(test_numbers)
builtin_time = time.time() - start_time
print(f"Built-in approach time: {builtin_time:.4f} seconds")
print(f"Improvement: {manual_time / builtin_time:.2f}x faster")
C’est ça! sum() donne le total de tous les nombres de la liste, et max() Renvoie le plus grand nombre. Même résultat, beaucoup plus rapide.
Sortir:
Manual approach time: 0.0805 seconds
Built-in approach time: 0.0413 seconds
Improvement: 1.95x faster
Amélioration des performances: Les fonctions intégrées sont généralement plus rapides que les implémentations manuelles.
Pourquoi cela fonctionne-t-il? Les fonctions intégrées de Python sont écrites en C et fortement optimisées.
4. Effectuer des opérations de chaîne efficaces avec join
La concaténation des cordes est quelque chose que chaque programmeur fait, mais la plupart des débutants le font d’une manière qui devient exponentiellement plus lente à mesure que les cordes s’allongent.
Avant l’optimisation
Voici comment vous pourriez construire une chaîne CSV en concaténant avec l’opérateur +:
def create_csv_plus(data):
result = "" # Start with an empty string
for row in data: # Go through each row of data
for i, item in enumerate(row): # Go through each item in the row
result += str(item) # Add the item to our result string
if i < len(row) - 1: # If it's not the last item
result += "," # Add a comma
result += "n" # Add a newline after each row
return result
# Test data: 1000 rows with 10 columns each
test_data = ((f"item_{i}_{j}" for j in range(10)) for i in range(1000))
start_time = time.time()
csv_plus = create_csv_plus(test_data)
plus_time = time.time() - start_time
print(f"String concatenation time: {plus_time:.4f} seconds")
Ce code construit notre chaîne CSV pièce par pièce. Pour chaque ligne, il passe par chaque élément, le convertit en une chaîne et l’ajoute à notre résultat. Il ajoute des virgules entre les articles et les nouvelles lignes entre les lignes.
Après optimisation
Voici le même code en utilisant la méthode de jointure:
def create_csv_join(data):
# For each row, join the items with commas, then join all rows with newlines
return "n".join(",".join(str(item) for item in row) for row in data)
start_time = time.time()
csv_join = create_csv_join(test_data)
join_time = time.time() - start_time
print(f"Join method time: {join_time:.4f} seconds")
print(f"Improvement: {plus_time / join_time:.2f}x faster")
Cette ligne unique fait beaucoup! La partie intérieure ",".join(str(item) for item in row) Prend chaque ligne et rejoint tous les articles avec des virgules. La partie extérieure "n".join(...) Prend toutes ces lignes séparées par des virgules et les rejoint avec Newlines.
Sortir:
String concatenation time: 0.0043 seconds
Join method time: 0.0022 seconds
Improvement: 1.94x faster
Amélioration des performances: La jonction des cordes est beaucoup plus rapide que la concaténation pour les grandes chaînes.
Pourquoi cela fonctionne-t-il? Lorsque vous utilisez + = pour concaténer les chaînes, Python crée un nouvel objet de chaîne à chaque fois car les chaînes sont immuables. Avec de grandes cordes, cela devient incroyablement un gaspillage. Le join La méthode détermine exactement la quantité de mémoire dont il a besoin à l’avance et construit la chaîne une fois.
5. Utiliser des générateurs pour un traitement économe en mémoire
Parfois, vous n’avez pas besoin de stocker toutes vos données en mémoire à la fois. Les générateurs vous permettent de créer des données à la demande, ce qui peut économiser des quantités massives de mémoire.
Avant l’optimisation
Voici comment vous pourriez traiter un grand ensemble de données en stockant tout dans une liste:
import sys
def process_large_dataset_list(n):
processed_data = ()
for i in range(n):
# Simulate some data processing
processed_value = i ** 2 + i * 3 + 42
processed_data.append(processed_value) # Store each processed value
return processed_data
# Test with 100,000 items
n = 100000
list_result = process_large_dataset_list(n)
list_memory = sys.getsizeof(list_result)
print(f"List memory usage: {list_memory:,} bytes")
Cette fonction traite les numéros de 0 à N-1, applique un certain calcul à chacun (le quadratrice, en multipliant par 3 et en ajoutant 42), et stocke tous les résultats dans une liste. Le problème est que nous gardons les 100 000 valeurs traitées en mémoire à la fois.
Après optimisation
Voici le même traitement à l’aide d’un générateur:
def process_large_dataset_generator(n):
for i in range(n):
# Simulate some data processing
processed_value = i ** 2 + i * 3 + 42
yield processed_value # Yield each value instead of storing it
# Create the generator (this doesn't process anything yet!)
gen_result = process_large_dataset_generator(n)
gen_memory = sys.getsizeof(gen_result)
print(f"Generator memory usage: {gen_memory:,} bytes")
print(f"Memory improvement: {list_memory / gen_memory:.0f}x less memory")
# Now we can process items one at a time
total = 0
for value in process_large_dataset_generator(n):
total += value
# Each value is processed on-demand and can be garbage collected
La différence clé est yield au lieu de append. Le yield Le mot-clé en fait une fonction de générateur – il produit des valeurs une à la fois au lieu de les créer tous en même temps.
Sortir:
List memory usage: 800,984 bytes
Generator memory usage: 224 bytes
Memory improvement: 3576x less memory
Amélioration des performances: Les générateurs peuvent utiliser «beaucoup» moins de mémoire pour les grands ensembles de données.
Pourquoi cela fonctionne-t-il? Les générateurs utilisent une évaluation paresseuse, ils ne calculent les valeurs que lorsque vous les demandez. L’objet générateur lui-même est minuscule; Il se souvient juste où il se trouve dans le calcul.
Conclusion
L’optimisation du code Python ne doit pas être intimidant. Comme nous l’avons vu, de petits changements dans la façon dont vous abordez les tâches de programmation communes peuvent donner des améliorations spectaculaires à la fois de la vitesse et de l’utilisation de la mémoire. La clé consiste à développer une intuition pour choisir le bon outil pour chaque travail.
N’oubliez pas ces principes principaux: utilisez des fonctions intégrées lorsqu’elles existent, choisissez des structures de données appropriées pour votre cas d’utilisation, évitez les travaux répétés inutiles et soyez conscient de la façon dont Python gère la mémoire. List Comprehensions, les ensembles de tests d’adhésion, la jonction des chaînes, les générateurs de grands ensembles de données sont tous des outils qui devraient être dans la boîte à outils du programmeur Python débutant. Continuez à apprendre, continuez à coder!
Bala Priya C est développeur et écrivain technique d’Inde. Elle aime travailler à l’intersection des mathématiques, de la programmation, de la science des données et de la création de contenu. Ses domaines d’intérêt et d’expertise incluent DevOps, la science des données et le traitement du langage naturel. Elle aime lire, écrire, coder et café! Actuellement, elle travaille sur l’apprentissage et le partage de ses connaissances avec la communauté des développeurs en créant des tutoriels, des guides pratiques, des pièces d’opinion, etc. Bala crée également des aperçus de ressources engageants et des tutoriels de codage.
