5 conseils pour écrire de meilleures fonctions Python

py-func

bala-pyfunc 5 conseils pour écrire de meilleures fonctions Python NEWS
Image par auteur

Nous écrivons tous des fonctions lors du codage en Python. Mais écrivons-nous nécessairement bien les fonctions? Eh bien, découvrons-le.

Les fonctions en Python vous permettent d’écrire du code modulaire. Lorsque vous devez effectuer une tâche à plusieurs endroits, vous pouvez intégrer la logique de la tâche dans une fonction Python. Et vous pouvez appeler la fonction chaque fois que vous devez effectuer cette tâche spécifique. Aussi simple que cela puisse paraître pour démarrer avec les fonctions Python, écrire des fonctions maintenables et performantes n’est pas si simple.

Et c’est pourquoi nous allons explorer quelques pratiques qui vous aideront à écrire des fonctions Python plus propres et faciles à maintenir. Commençons…

1. Écrivez des fonctions qui ne font qu’une seule chose

Lors de l’écriture de fonctions en Python, il est souvent tentant de regrouper toutes les tâches associées dans une seule fonction. Bien que cela puisse vous aider à coder les choses rapidement, cela ne fera que rendre votre code difficile à maintenir dans un avenir proche. Non seulement cela rendra plus difficile la compréhension de ce que fait une fonction, mais cela entraînera également d’autres problèmes tels qu’un trop grand nombre de paramètres (nous en reparlerons plus tard !).

En tant que bonne pratique, vous devriez toujours essayer de faire en sorte que votre fonction ne fasse qu’une seule chose (une seule tâche) et qu’elle soit bien exécutée. Mais parfois, pour une seule tâche, vous devrez peut-être exécuter une série de sous-tâches. Alors, comment décidez-vous si et comment la fonction doit être refactorisée ?

Selon quoi la fonction essaie de faire et quelle que soit la complexité de la tâche, vous pouvez déterminer la séparation des préoccupations entre les sous-tâches. Ensuite, identifiez un niveau approprié auquel vous pouvez refactoriser la fonction en plusieurs fonctions, chacune se concentrant sur une sous-tâche spécifique.

bala-refactor-func 5 conseils pour écrire de meilleures fonctions Python NEWS bala-refactor-func 5 conseils pour écrire de meilleures fonctions Python NEWS
Fonctions de refactorisation | Image par auteur

Voici un exemple. Regardez la fonction analyze_and_report_sales:

# fn. to analyze sales data, calculate sales metrics, and write it to a file
def analyze_and_report_sales(data, report_filename):
	total_sales = sum(item['price'] * item['quantity'] for item in data)
	average_sales = total_sales / len(data)
    
	with open(report_filename, 'w') as report_file:
    	    report_file.write(f"Total Sales: {total_sales}n")
    	    report_file.write(f"Average Sales: {average_sales}n")
    
	return total_sales, average_sales

Il est assez facile de voir qu’il peut être refactorisé en deux fonctions : une pour calculer les mesures de ventes et une autre pour écrire les mesures de ventes dans un fichier comme celui-ci :

# refactored into two funcs: one to calculate metrics and another to write sales report
def calculate_sales_metrics(data):
	total_sales = sum(item['price'] * item['quantity'] for item in data)
	average_sales = total_sales / len(data)
	return total_sales, average_sales

def write_sales_report(report_filename, total_sales, average_sales):
	with open(report_filename, 'w') as report_file:
    	    report_file.write(f"Total Sales: {total_sales}n")
    	    report_file.write(f"Average Sales: {average_sales}n")

Il est désormais plus facile de déboguer séparément tout problème lié au calcul des mesures de ventes et aux opérations de fichiers. Et voici un exemple d’appel de fonction :

data = [{'price': 100, 'quantity': 2}, {'price': 200, 'quantity': 1}]
total_sales, average_sales = calculate_sales_metrics(data)
write_sales_report('sales_report.txt', total_sales, average_sales)

Vous devriez pouvoir voir le fichier « sales_report.txt » dans votre répertoire de travail avec les statistiques de ventes. Il s’agit d’un exemple simple pour commencer, mais il s’avère particulièrement utile lorsque vous travaillez sur des fonctions plus complexes.

2. Ajoutez des indices de type pour améliorer la maintenabilité

Python est un langage typé dynamiquement. Vous n’avez donc pas besoin de déclarer des types pour les variables que vous créez. Mais vous pouvez ajouter des indications de type pour spécifier le type de données attendu pour les variables. Lorsque vous définissez la fonction, vous pouvez ajouter les types de données attendus pour les paramètres et les valeurs de retour.

Étant donné que Python n’applique pas les types au moment de l’exécution, l’ajout d’indicateurs de type n’a aucun effet au moment de l’exécution. Mais l’utilisation des indices de type présente toujours des avantages, notamment en termes de maintenabilité :

  • L’ajout d’indices de type aux fonctions Python sert de documentation en ligne et donne une meilleure idée de ce que fait la fonction et des valeurs qu’elle consomme et renvoie.
  • Lorsque vous ajoutez des indicateurs de type à vos fonctions, vous pouvez configurer votre IDE pour exploiter ces indicateurs de type. Ainsi, vous recevrez des avertissements utiles si vous essayez de transmettre un argument de type non valide dans un ou plusieurs appels de fonction, si vous implémentez des fonctions dont les valeurs de retour ne correspondent pas au type attendu, etc. Vous pouvez ainsi minimiser les erreurs dès le départ.
  • Vous pouvez éventuellement utiliser des vérificateurs de type statique comme somnolent pour détecter les erreurs plus tôt plutôt que de laisser les incompatibilités de types introduire des bogues subtils difficiles à déboguer.

Voici une fonction qui traite les détails de la commande :

# fn. to process orders
def process_orders(orders):
	total_quantity = sum(order['quantity'] for order in orders)
	total_value = sum(order['quantity'] * order['price'] for order in orders)
	return {
    	'total_quantity': total_quantity,
    	'total_value': total_value
	}

Ajoutons maintenant des indices de type à la fonction comme ceci :

# modified with type hints
from typing import List, Dict

def process_orders(orders: List[Dict[str, float | int]]) -> Dict[str, float | int]:
	total_quantity = sum(order['quantity'] for order in orders)
	total_value = sum(order['quantity'] * order['price'] for order in orders)
	return {
    	'total_quantity': total_quantity,
    	'total_value': total_value
	}

Avec la version modifiée, vous apprenez que la fonction prend en charge une liste de dictionnaires. Les clés du dictionnaire doivent toutes être des chaînes et les valeurs peuvent être des entiers ou des valeurs à virgule flottante. La fonction renvoie également un dictionnaire. Prenons un exemple d’appel de fonction :

# Sample data
orders = [
	{'price': 100.0, 'quantity': 2},
	{'price': 50.0, 'quantity': 5},
	{'price': 150.0, 'quantity': 1}
]

# Sample function call
result = process_orders(orders)
print(result)

Voici le résultat :

{'total_quantity': 8, 'total_value': 600.0}

Dans cet exemple, les indications de type nous aident à avoir une meilleure idée du fonctionnement de la fonction. À l’avenir, nous ajouterons des astuces de type pour toutes les meilleures versions des fonctions Python que nous écrivons.

3. Acceptez uniquement les arguments dont vous avez réellement besoin

Si vous êtes débutant ou si vous venez de commencer votre premier rôle de développeur, il est important de penser aux différents paramètres lors de la définition de la signature de fonction. Il est assez courant d’introduire des paramètres supplémentaires dans la signature de fonction que la fonction ne traite jamais réellement.

S’assurer que la fonction n’accepte que les arguments réellement nécessaires permet de garder les appels de fonction plus propres et plus maintenables en général. Dans le même ordre d’idées, trop de paramètres dans la signature de fonction rendent également la maintenance difficile. Alors, comment définir des fonctions faciles à maintenir avec le bon nombre de paramètres ?

Si vous vous retrouvez à écrire une signature de fonction avec un nombre croissant de paramètres, la première étape consiste à supprimer tous les paramètres inutilisés de la signature. S’il y a trop de paramètres même après cette étape, revenez au conseil n°1 : divisez la tâche en plusieurs sous-tâches et refactorisez la fonction en plusieurs fonctions plus petites. Cela aidera à contrôler le nombre de paramètres.

bala-num-params 5 conseils pour écrire de meilleures fonctions Python NEWS bala-num-params 5 conseils pour écrire de meilleures fonctions Python NEWS
Gardez num_params sous contrôle | Image par auteur

Il est temps de donner un exemple simple. Ici, la définition de la fonction pour calculer les notes des étudiants contient le instructor paramètre qui n’est jamais utilisé :

# takes in an arg that's never used!
def process_student_grades(student_id, grades, course_name, instructor'):
	average_grade = sum(grades) / len(grades)
	return f"Student {student_id} achieved an average grade of {average_grade:.2f} in {course_name}."


Vous pouvez réécrire la fonction sans le instructor paramètre comme ceci :

# better version!
def process_student_grades(student_id: int, grades: list, course_name: str) -> str:
	average_grade = sum(grades) / len(grades)
	return f"Student {student_id} achieved an average grade of {average_grade:.2f} in {course_name}."

# Usage
student_id = 12345
grades = [85, 90, 75, 88, 92]
course_name = "Mathematics"
result = process_student_grades(student_id, grades, course_name)
print(result)

Voici le résultat de l’appel de fonction :

Student 12345 achieved an average grade of 86.00 in Mathematics.

4. Appliquez des arguments composés uniquement de mots clés pour minimiser les erreurs

En pratique, la plupart des fonctions Python acceptent plusieurs arguments. Vous pouvez transmettre des arguments aux fonctions Python sous forme d’arguments de position, d’arguments de mot-clé ou un mélange des deux. Lire Arguments de fonction Python : un guide définitif pour un aperçu rapide des arguments de la fonction.

Certains arguments sont naturellement positionnels. Mais parfois, avoir des appels de fonction contenant uniquement des arguments de position peut prêter à confusion. Cela est particulièrement vrai lorsque la fonction prend en compte plusieurs arguments du même type de données, certains obligatoires et d’autres facultatifs.

Si vous vous souvenez, avec les arguments de position, les arguments sont passés aux paramètres dans la signature de fonction dans le même ordre dans lequel ils apparaissent dans l’appel de fonction. Ainsi, un changement dans l’ordre des arguments peut introduire des erreurs subtiles de type bugs.

Il est souvent utile de faire facultatif arguments mot-clé uniquement. Cela rend également l’ajout de paramètres facultatifs beaucoup plus facile, sans interrompre les appels existants.

Voici un exemple. Le process_payment la fonction prend en option un description chaîne:

# example fn. for processing transaction
def process_payment(transaction_id: int, amount: float, currency: str, description: str = None):
	print(f"Processing transaction {transaction_id}...")
	print(f"Amount: {amount} {currency}")
	if description:
    		print(f"Description: {description}")

Dites que vous souhaitez rendre l’option facultative description un argument de mot-clé uniquement. Voici comment procéder :

# enforce keyword-only arguments to minimize errors
# make the optional `description` arg keyword-only
def process_payment(transaction_id: int, amount: float, currency: str, *, description: str = None):
	print(f"Processing transaction {transaction_id}:")
	print(f"Amount: {amount} {currency}")
	if description:
    		print(f"Description: {description}")

Prenons un exemple d’appel de fonction :

process_payment(1234, 100.0, 'USD', description='Payment for services')

Cela produit :

Processing transaction 1234...
Amount: 100.0 USD
Description: Payment for services

Essayez maintenant de transmettre tous les arguments comme positionnels :

# throws error as we try to pass in more positional args than allowed!
process_payment(5678, 150.0, 'EUR', 'Invoice payment') 

Vous obtiendrez une erreur comme indiqué :

Traceback (most recent call last):
  File "/home/balapriya/better-fns/tip4.py", line 9, in 
	process_payment(1234, 150.0, 'EUR', 'Invoice payment')
TypeError: process_payment() takes 3 positional arguments but 4 were given

5. Ne renvoie pas les listes des fonctions ; Utilisez plutôt des générateurs

Il est assez courant d’écrire des fonctions Python qui génèrent des séquences telles qu’une liste de valeurs. Mais autant que possible, vous devez éviter de renvoyer des listes à partir de fonctions Python. Au lieu de cela, vous pouvez les réécrire en tant que fonctions génératrices. Les générateurs utilisent une évaluation paresseuse ; ils produisent donc des éléments de la séquence à la demande plutôt que de calculer toutes les valeurs à l’avance. Lire Premiers pas avec les générateurs Python pour une introduction au fonctionnement des générateurs en Python.

A titre d’exemple, prenons la fonction suivante qui génère la séquence de Fibonacci jusqu’à une certaine limite supérieure :

# returns a list of Fibonacci numbers
def generate_fibonacci_numbers_list(limit):
	fibonacci_numbers = [0, 1]
	while fibonacci_numbers[-1] + fibonacci_numbers[-2] <= limit:
    		fibonacci_numbers.append(fibonacci_numbers[-1] + fibonacci_numbers[-2])
	return fibonacci_numbers

Il s’agit d’une implémentation récursive qui nécessite des calculs coûteux et qui remplit la liste et la renvoie semble plus verbeuse que nécessaire. Voici une version améliorée de la fonction qui utilise des générateurs :

# use generators instead
from typing import Generator

def generate_fibonacci_numbers(limit: int) -> Generator[int, None, None]:
	a, b = 0, 1
	while a <= limit:
    		yield a
    	a, b = b, a + b

Dans ce cas, la fonction renvoie un objet générateur que vous pouvez ensuite parcourir pour obtenir les éléments de la séquence :

limit = 100
fibonacci_numbers_generator = generate_fibonacci_numbers(limit)
for num in fibonacci_numbers_generator:
	print(num)

Voici le résultat :

0
1
1
2
3
5
8
13
21
34
55
89

Comme vous pouvez le constater, l’utilisation de générateurs peut être beaucoup plus efficace, en particulier pour les entrées de grande taille. En outre, vous pouvez chaîner plusieurs générateurs ensemble, afin de pouvoir créer des pipelines de traitement de données efficaces avec des générateurs.

Emballer

Et c’est terminé. Vous pouvez trouver tout le code sur GitHub. Voici un aperçu des différents conseils que nous avons passés en revue :

  • Écrire des fonctions qui ne font qu’une seule chose
  • Ajouter des indices de type pour améliorer la maintenabilité
  • Acceptez uniquement les arguments dont vous avez réellement besoin
  • Appliquer des arguments de mots clés uniquement pour minimiser les erreurs
  • Ne renvoie pas de listes à partir de fonctions ; utilisez plutôt des générateurs

J’espère que vous les avez trouvés utiles ! Si ce n’est pas déjà fait, essayez ces pratiques lors de l’écriture de fonctions Python. Bon codage !

Bala Priya C est un développeur et rédacteur technique indien. 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 le DevOps, la science des données et le traitement du langage naturel. Elle aime lire, écrire, coder et prendre le café ! Actuellement, elle travaille à l’apprentissage et au partage de ses connaissances avec la communauté des développeurs en créant des didacticiels, des guides pratiques, des articles d’opinion, etc. Bala crée également des aperçus de ressources attrayants et des didacticiels de codage.

Source