Arrêter d’écrire un python désordonné: un cours accidentel du code propre

Image de l’auteur | Idéogramme
Si vous codiez dans Python depuis un certain temps, vous avez probablement maîtrisé les bases, construit quelques projets. Et maintenant, vous regardez votre code en pensant: « Cela fonctionne, mais … ce n’est pas exactement quelque chose que je montrerais fièrement dans une revue de code. » Nous avons tous été là.
Mais à mesure que vous continuez à coder, l’écriture de code propre devient aussi importante que la rédaction du code fonctionnel. Dans cet article, j’ai compilé des techniques pratiques qui peuvent vous aider à passer de « Il fonctionne, ne le touchez pas » à « Ceci est réellement maintenable ».
🔗 Lien vers le code sur github
1. Modèle de données explicitement. Ne passez pas les dicts
Les dictionnaires sont super flexibles à Python et c’est précisément le problème. Lorsque vous transmettez les dictionnaires bruts tout au long de votre code, vous invitez les fautes de frappe, les erreurs clés et la confusion sur les données qui devraient réellement être présentes.
Au lieu de ceci:
def process_user(user_dict):
if user_dict('status') == 'active': # What if 'status' is missing?
send_email(user_dict('email')) # What if it's 'mail' in some places?
# Is it 'name', 'full_name', or 'username'? Who knows!
log_activity(f"Processed {user_dict('name')}")
Ce code n’est pas robuste car il suppose que les clés de dictionnaire existent sans validation. Il n’offre aucune protection contre les fautes de frappe ou les clés manquantes, ce qui provoquera KeyError
Exceptions au moment de l’exécution. Il n’y a pas non plus de documentation sur les domaines attendus.
Faites ceci:
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
id: int
email: str
full_name: str
status: str
last_login: Optional(datetime) = None
def process_user(user: User):
if user.status == 'active':
send_email(user.email)
log_activity(f"Processed {user.full_name}")
Python @dataclass
Le décorateur vous donne une structure propre et explicite avec un minimum de passe-partout. Votre IDE peut désormais fournir une saisie semi-automatique pour les attributs, et vous obtiendrez des erreurs immédiates si les champs requis sont manquants.
Pour une validation plus complexe, considérez Pydontic:
from pydantic import BaseModel, EmailStr, validator
class User(BaseModel):
id: int
email: EmailStr # Validates email format
full_name: str
status: str
@validator('status')
def status_must_be_valid(cls, v):
if v not in {'active', 'inactive', 'pending'}:
raise ValueError('Must be active, inactive or pending')
return v
Maintenant, vos données se valident, attrape les erreurs tôt et documentent clairement les attentes.
2. Utilisez des énumérations pour des choix connus
Les littéraux de cordes sont sujets aux fautes de frappe et ne fournissent pas d’adhérence IDE. La validation ne se produit qu’à l’exécution.
Au lieu de ceci:
def process_order(order, status):
if status == 'pending':
# process logic
elif status == 'shipped':
# different logic
elif status == 'delivered':
# more logic
else:
raise ValueError(f"Invalid status: {status}")
# Later in your code...
process_order(order, 'shiped') # Typo! But no IDE warning
Faites ceci:
from enum import Enum, auto
class OrderStatus(Enum):
PENDING = 'pending'
SHIPPED = 'shipped'
DELIVERED = 'delivered'
def process_order(order, status: OrderStatus):
if status == OrderStatus.PENDING:
# process logic
elif status == OrderStatus.SHIPPED:
# different logic
elif status == OrderStatus.DELIVERED:
# more logic
# Later in your code...
process_order(order, OrderStatus.SHIPPED) # IDE autocomplete helps!
Lorsque vous avez affaire à un ensemble fixe d’options, une énumération rend votre code plus robuste et auto-documentaire.
Avec enums:
- Votre IDE fournit des suggestions de saisie semi-automatique
- Les fautes de frappe deviennent (presque) impossibles
- Vous pouvez itérer à travers toutes les valeurs possibles en cas de besoin
Enum crée un ensemble de constantes nommées. L’indice de type status: OrderStatus
documente le type de paramètre attendu. En utilisant OrderStatus.SHIPPED
Au lieu d’un littéral de chaîne, permet d’obtenir l’auto-automatique et attrape les fautes de frappe au moment du développement.
3. Utilisez des arguments de mots clés uniquement pour plus de clarté
Le système d’arguments flexible de Python est puissant, mais il peut entraîner une confusion lorsque les appels de fonction ont plusieurs paramètres facultatifs.
Au lieu de ceci:
def create_user(name, email, admin=False, notify=True, temporary=False):
# Implementation
# Later in code...
create_user("John Smith", "john@example.com", True, False)
Attendez, que signifient ces booléens?
Lorsqu’il est appelé avec des arguments positionnels, il n’est pas clair ce que les valeurs booléennes représentent sans vérifier la définition de la fonction. Est vrai pour l’administrateur, le notification ou autre chose?
Faites ceci:
def create_user(name, email, *, admin=False, notify=True, temporary=False):
# Implementation
# Now you must use keywords for optional args
create_user("John Smith", "john@example.com", admin=True, notify=False)
La syntaxe *, la syntaxe oblige tous les arguments après avoir spécifié par mot-clé. Cela fait que votre fonction appelle l’auto-documentation et empêche le problème « mystère booléen » où les lecteurs ne peuvent pas dire à quoi se réfère vrai ou faux sans lire la définition de la fonction.
Ce modèle est particulièrement utile dans les appels API et similaires, où vous souhaitez assurer la clarté du site d’appel.
4. Utilisez Pathlib sur OS.Path
Le module OS.Path de Python est fonctionnel mais maladroit. Le module Pathlib plus récent fournit une approche orientée objet plus intuitive et moins sujet aux erreurs.
Au lieu de ceci:
import os
data_dir = os.path.join('data', 'processed')
if not os.path.exists(data_dir):
os.makedirs(data_dir)
filepath = os.path.join(data_dir, 'output.csv')
with open(filepath, 'w') as f:
f.write('resultsn')
# Check if we have a JSON file with the same name
json_path = os.path.splitext(filepath)(0) + '.json'
if os.path.exists(json_path):
with open(json_path) as f:
data = json.load(f)
Cela utilise la manipulation de la chaîne avec os.path.join()
et os.path.splitext()
pour la manipulation du chemin. Les opérations de chemin sont dispersées sur différentes fonctions. Le code est verbeux et moins intuitif.
Faites ceci:
from pathlib import Path
data_dir = Path('data') / 'processed'
data_dir.mkdir(parents=True, exist_ok=True)
filepath = data_dir / 'output.csv'
filepath.write_text('resultsn')
# Check if we have a JSON file with the same name
json_path = filepath.with_suffix('.json')
if json_path.exists():
data = json.loads(json_path.read_text())
Pourquoi Pathlib est meilleur:
- Le chemin de la joie avec / est plus intuitif
- Des méthodes comme
mkdir()
,exists()
etread_text()
sont attachés à l’objet Path - Les opérations comme changer les extensions (avec_suffix) sont plus sémantiques
Pathlib gère les subtilités de la manipulation du chemin à travers différents systèmes d’exploitation. Cela rend votre code plus portable et robuste.
5. Échouez rapidement avec les clauses de garde
Les stades si imbriquées profondément sont souvent difficiles à comprendre et à entretenir. L’utilisation des rendements précoces – Clauses de garde – conduit à un code plus lisible.
Au lieu de ceci:
def process_payment(order, user):
if order.is_valid:
if user.has_payment_method:
payment_method = user.get_payment_method()
if payment_method.has_sufficient_funds(order.total):
try:
payment_method.charge(order.total)
order.mark_as_paid()
send_receipt(user, order)
return True
except PaymentError as e:
log_error(e)
return False
else:
log_error("Insufficient funds")
return False
else:
log_error("No payment method")
return False
else:
log_error("Invalid order")
return False
La nidification profonde est difficile à suivre. Chaque bloc conditionnel nécessite de suivre plusieurs branches simultanément.
Faites ceci:
def process_payment(order, user):
# Guard clauses: check preconditions first
if not order.is_valid:
log_error("Invalid order")
return False
if not user.has_payment_method:
log_error("No payment method")
return False
payment_method = user.get_payment_method()
if not payment_method.has_sufficient_funds(order.total):
log_error("Insufficient funds")
return False
# Main logic comes after all validations
try:
payment_method.charge(order.total)
order.mark_as_paid()
send_receipt(user, order)
return True
except PaymentError as e:
log_error(e)
return False
Les clauses de garde gérent les cas d’erreur à l’avant, en réduisant les niveaux d’indentation. Chaque condition est vérifiée séquentiellement, ce qui rend le flux plus facile à suivre. La logique principale vient à la fin, clairement séparée de la gestion des erreurs.
Cette approche évolue beaucoup mieux à mesure que votre logique se développe en complexité.
6. Ne pas utiliser la liste des compréhensions de la liste
Les compréhensions de la liste sont l’une des caractéristiques les plus élégantes de Python, mais elles deviennent illisibles lorsqu’ils sont surchargés de conditions ou de transformations complexes.
Au lieu de ceci:
# Hard to parse at a glance
active_premium_emails = (user('email') for user in users_list
if user('status') == 'active' and
user('subscription') == 'premium' and
user('email_verified') and
not user('email') in blacklisted_domains)
Cette compréhension de la liste contient trop de logique en une seule ligne. Il est difficile de lire et de déboguer. Plusieurs conditions sont enchaînées, ce qui rend difficile de comprendre les critères du filtre.
Faites ceci:
Voici de meilleures alternatives.
Option 1: fonction avec un nom descriptif
Extrait la condition complexe dans une fonction nommée avec un nom descriptif. La compréhension de la liste est désormais beaucoup plus claire, en se concentrant sur ce qu’elle fait (extraire des e-mails) plutôt que sur la façon dont il filtre.
def is_valid_premium_user(user):
return (user('status') == 'active' and
user('subscription') == 'premium' and
user('email_verified') and
not user('email') in blacklisted_domains)
active_premium_emails = (user('email') for user in users_list if is_valid_premium_user(user))
Option 2: Boucle traditionnelle lorsque la logique est complexe
Utilise une boucle traditionnelle avec des continues précoces pour plus de clarté. Chaque condition est vérifiée séparément, ce qui permet de déboguer facilement quelle condition pourrait échouer. La logique de transformation est également clairement séparée.
active_premium_emails = ()
for user in users_list:
# Complex filtering logic
if user('status') != 'active':
continue
if user('subscription') != 'premium':
continue
if not user('email_verified'):
continue
if user('email') in blacklisted_domains:
continue
# Complex transformation logic
email = user('email').lower().strip()
active_premium_emails.append(email)
Les compréhensions de la liste devraient rendre votre code plus lisible, pas moins. Lorsque la logique devient complexe:
- Briser les conditions complexes en fonctions nommées
- Envisagez d’utiliser une boucle régulière avec les premières continues
- Diviser les opérations complexes en plusieurs étapes
N’oubliez pas que l’objectif est la lisibilité.
7. Écrivez des fonctions pures réutilisables
Une fonction est une fonction pure si elle produit toujours la même sortie pour les mêmes entrées. De plus, il n’a aucun effet secondaire.
Au lieu de ceci:
total_price = 0 # Global state
def add_item_price(item_name, quantity):
global total_price
# Look up price from global inventory
price = inventory.get_item_price(item_name)
# Apply discount
if settings.discount_enabled:
price *= 0.9
# Update global state
total_price += price * quantity
# Later in code...
add_item_price('widget', 5)
add_item_price('gadget', 3)
print(f"Total: ${total_price:.2f}")
Cela utilise l’état mondial (total_price
) Ce qui rend les tests difficiles.
La fonction a des effets secondaires (modification de l’état global) et dépend de l’état externe (inventaire et paramètres). Cela le rend imprévisible et difficile à réutiliser.
Faites ceci:
def calculate_item_price(item, price, quantity, discount=0):
"""Calculate final price for a quantity of items with optional discount.
Args:
item: Item identifier (for logging)
price: Base unit price
quantity: Number of items
discount: Discount as decimal
Returns:
Final price after discounts
"""
discounted_price = price * (1 - discount)
return discounted_price * quantity
def calculate_order_total(items, discount=0):
"""Calculate total price for a collection of items.
Args:
items: List of (item_name, price, quantity) tuples
discount: Order-level discount
Returns:
Total price after all discounts
"""
return sum(
calculate_item_price(item, price, quantity, discount)
for item, price, quantity in items
)
# Later in code...
order_items = (
('widget', inventory.get_item_price('widget'), 5),
('gadget', inventory.get_item_price('gadget'), 3),
)
total = calculate_order_total(order_items,
discount=0.1 if settings.discount_enabled else 0)
print(f"Total: ${total:.2f}")
La version suivante utilise des fonctions pures qui prennent toutes les dépendances comme paramètres.
8. Écrivez les docstrings pour les fonctions publiques et les classes
La documentation n’est pas (et ne devrait pas être) une réflexion après coup. C’est une partie centrale du code maintenable. Les bonnes docstrings expliquent non seulement ce que font les fonctions, mais pourquoi elles existent et comment les utiliser correctement.
Au lieu de ceci:
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return celsius * 9/5 + 32
Il s’agit d’un docstring minimal qui ne répète que le nom de la fonction. Ne fournit aucune information sur les paramètres, les valeurs de retour ou les cas de bord.
Faites ceci:
def celsius_to_fahrenheit(celsius):
"""
Convert temperature from Celsius to Fahrenheit.
The formula used is: F = C × (9/5) + 32
Args:
celsius: Temperature in degrees Celsius (can be float or int)
Returns:
Temperature converted to degrees Fahrenheit
Example:
>>> celsius_to_fahrenheit(0)
32.0
>>> celsius_to_fahrenheit(100)
212.0
>>> celsius_to_fahrenheit(-40)
-40.0
"""
return celsius * 9/5 + 32
Un bon docstring:
- Documents Paramètres et valeurs de retour
- Note toutes les exceptions qui pourraient être soulevées
- Fournit des exemples d’utilisation
Vos docstrings servent de documentation exécutable qui reste en synchronisation avec votre code.
9. Automatiser la lignée et le formatage
Ne comptez pas sur l’inspection manuelle pour attraper des problèmes de style et des bugs communs. Les outils automatisés peuvent gérer le travail fastidieux d’assurer la qualité et la cohérence du code.
Vous pouvez essayer de configurer ces outils de liaison et de formatage:
- Noir – Formateur de code
- Fraise – Linter rapide
- mypy – vérificateur de type statique
- isorter – Organisateur d’importation
Intégrez-les à l’aide de crochets de pré-engagement pour vérifier automatiquement et formater le code avant chaque validation:
- Installer pré-engagement:
pip install pre-commit
- Créer un
.pre-commit-config.yaml
fichier avec les outils configurés - Courir
pre-commit install
pour activer
Cette configuration garantit un style de code cohérent et capture les erreurs tôt sans effort manuel.
Vous pouvez vérifier 7 outils pour aider à écrire un meilleur code Python pour en savoir plus à ce sujet.
10. Évitez le fourre-tout sauf
Les gestionnaires d’exceptions génériques masquent les bogues et rendent le débogage difficile. Ils attrapent tout, y compris les erreurs de syntaxe, les erreurs de mémoire et les interruptions du clavier.
Au lieu de ceci:
try:
user_data = get_user_from_api(user_id)
process_user_data(user_data)
save_to_database(user_data)
except:
# What failed? We'll never know!
logger.error("Something went wrong")
Cela utilise une exception nue pour gérer:
- Erreurs de programmation (comme les erreurs de syntaxe)
- Erreurs du système (comme MemoryError)
- Interruptions du clavier (Ctrl + C)
- Erreurs attendues (comme les délais d’expiration du réseau)
Cela rend le débogage extrêmement difficile, car toutes les erreurs sont traitées de la même manière.
Faites ceci:
try:
user_data = get_user_from_api(user_id)
process_user_data(user_data)
save_to_database(user_data)
except ConnectionError as e:
logger.error(f"API connection failed: {e}")
# Handle API connection issues
except ValueError as e:
logger.error(f"Invalid user data received: {e}")
# Handle validation issues
except DatabaseError as e:
logger.error(f"Database error: {e}")
# Handle database issues
except Exception as e:
# Last resort for unexpected errors
logger.critical(f"Unexpected error processing user {user_id}: {e}",
exc_info=True)
# Possibly re-raise or handle generically
raise
Attrape des exceptions spécifiques qui peuvent être attendues et gérées de manière appropriée. Chaque type d’exception a son propre message d’erreur et sa stratégie de gestion.
L’exception finale sauf capture des erreurs inattendues, les enregistre avec la trace complète (exc_info=True
), Et les réévalue pour éviter d’ignorer silencieusement de graves problèmes.
Si vous avez besoin d’un gestionnaire de fourre-tout pour une raison quelconque, utilisez except Exception as e:
plutôt qu’un nu except:
et enregistrez toujours les détails de l’exception complète avec exc_info=True
.
Emballage
J’espère que vous pourrez utiliser au moins certaines de ces pratiques dans votre code. Commencez à les mettre en œuvre dans vos projets.
Vous constaterez que votre code devient plus maintenable, plus testable et plus facile à raisonner.
La prochaine fois que vous serez tenté de prendre un raccourci, rappelez-vous: Le code est lu plusieurs fois plus qu’il n’est écrit. Codage propre?
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.