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

 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()et read_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:

  1. Noir – Formateur de code
  2. Fraise – Linter rapide
  3. mypy – vérificateur de type statique
  4. 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:

  1. Installer pré-engagement: pip install pre-commit
  2. Créer un .pre-commit-config.yaml fichier avec les outils configurés
  3. 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.





Source link

Related post