Refactorisation des applications Python pour plus de simplicité

Refactorisation des applications Python pour plus de simplicité

Voulez-vous du code Python plus simple? Vous démarrez toujours un projet avec les meilleures intentions, une base de code propre et une belle structure. Mais au fil du temps, vos applications ont changé et les choses peuvent devenir un peu désordonnées.

Si vous pouvez écrire et maintenir du code Python simple et propre, cela vous fera gagner beaucoup de temps à long terme. Vous pouvez passer moins de temps à tester, à trouver des bogues et à apporter des modifications lorsque votre code est bien présenté et simple à suivre.

Dans ce didacticiel, vous apprendrez:

  • Comment mesurer la complexité du code Python et de vos applications

  • Comment changer votre code sans le casser

  • Quels sont les problèmes courants dans le code Python qui causent une complexité supplémentaire et comment les résoudre

Tout au long de ce tutoriel, je vais utiliser le thème des réseaux ferroviaires souterrains pour expliquer la complexité car naviguer dans un métro dans une grande ville peut être compliqué! Certains sont bien conçus et d'autres semblent trop complexes.

Free Bonus:5 Thoughts On Python Mastery, un cours gratuit pour les développeurs Python qui vous montre la feuille de route et l'état d'esprit dont vous aurez besoin pour faire passer vos compétences Python au niveau supérieur.

Complexité du code en Python

La complexité d'une application et de sa base de code dépend de la tâche qu'elle effectue. Si vous écrivez du code pour le laboratoire de propulsion à réaction de la NASA (littéralementrocket science), cela va être compliqué.

La question n'est pas tant: "Mon code est-il compliqué?" comme: "Mon code est-il plus compliqué qu'il ne devrait l'être?"

Le réseau ferroviaire de Tokyo est l'un des plus étendus et compliqués au monde. C'est en partie parce que Tokyo est une métropole de plus de 30 millions d'habitants, mais c'est aussi parce qu'il y a 3 réseaux qui se chevauchent.

Il y a les réseaux de transport rapide Toei et Tokyo Metro ainsi que les trains Japan Rail East qui traversent le centre de Tokyo. Même pour le voyageur le plus expérimenté, naviguer dans le centre de Tokyo peut être incroyablement compliqué.

Voici une carte du réseau ferroviaire de Tokyo pour vous donner un aperçu:

Si votre code commence à ressembler un peu à cette carte, alors c'est le tutoriel pour vous.

Tout d'abord, nous allons passer en revue 4 mesures de complexité qui peuvent vous donner une échelle pour mesurer vos progrès relatifs dans la mission pour rendre votre code plus simple:

Code Complexity Metrics Table of Contents

Après avoir exploré les métriques, vous découvrirez un outil appeléwily pour automatiser le calcul de ces métriques.

Métriques pour mesurer la complexité

Beaucoup de temps et de recherche ont été consacrés à l'analyse de la complexité des logiciels informatiques. Des applications trop complexes et non gérables peuvent avoir un coût très réel.

La complexité du logiciel est liée à la qualité. Un code facile à lire et à comprendre sera probablement mis à jour par les développeurs à l'avenir.

Voici quelques métriques pour les langages de programmation. Ils s'appliquent à de nombreux langages, pas seulement à Python.

Lignes de code

LOC, ou Lines of Code, est la mesure la plus grossière de la complexité. On peut se demander s'il existe une corrélation directe entre les lignes de code et la complexité d'une application, mais la corrélation indirecte est claire. Après tout, un programme à 5 lignes est probablement plus simple qu'un programme à 5 millions.

Lorsque nous examinons les métriques Python, nous essayons d'ignorer les lignes vides et les lignes contenant des commentaires.

Les lignes de code peuvent être calculées à l'aide de la commandewc sous Linux et Mac OS, oùfile.py est le nom du fichier que vous souhaitez mesurer:

$ wc -l file.py

Si vous souhaitez ajouter les lignes combinées dans un dossier en recherchant récursivement tous les fichiers.py, vous pouvez combinerwc avec la commandefind:

$ find . -name \*.py | xargs wc -l

Pour Windows, PowerShell propose une commande de comptage de mots dansMeasure-Object et une recherche de fichier récursive dansGet-ChildItem:

$ Get-ChildItem -Path *.py -Recurse | Measure-Object –Line

Dans la réponse, vous verrez le nombre total de lignes.

Pourquoi des lignes de code sont-elles utilisées pour quantifier la quantité de code dans votre application? L'hypothèse est qu'une ligne de code équivaut à peu près à une instruction. Les lignes sont une meilleure mesure que les caractères, ce qui inclurait des espaces.

En Python, nous sommes encouragés à mettre une seule instruction sur chaque ligne. Cet exemple est composé de 9 lignes de code:

 1 x = 5
 2 value = input("Enter a number: ")
 3 y = int(value)
 4 if x < y:
 5     print(f"{x} is less than {y}")
 6 elif x == y:
 7     print(f"{x} is equal to {y}")
 8 else:
 9     print(f"{x} is more than {y}")

Si vous n'utilisez que des lignes de code comme mesure de la complexité, cela pourrait encourager les mauvais comportements.

Le code Python doit être facile à lire et à comprendre. En prenant ce dernier exemple, vous pouvez réduire le nombre de lignes de code à 3:

 1 x = 5; y = int(input("Enter a number:"))
 2 equality = "is equal to" if x == y else "is less than" if x < y else "is more than"
 3 print(f"{x} {equality} {y}")

Mais le résultat est difficile à lire, et PEP 8 a des directives concernant la longueur de ligne maximale et la rupture de ligne. Vous pouvez consulterHow to Write Beautiful Python Code With PEP 8 pour en savoir plus sur PEP 8.

Ce bloc de code utilise 2 fonctionnalités du langage Python pour raccourcir le code:

  • Compound statements: en utilisant;

  • Déclarations conditionnelles ou ternaires chaînées: name = value if condition else value if condition2 else value2

Nous avons réduit le nombre de lignes de code mais avons violé l'une des lois fondamentales de Python:

«La lisibilité compte»

- Tim Peters, Zen de Python

Ce code abrégé est potentiellement plus difficile à maintenir car les responsables du code sont des humains, et ce code court est plus difficile à lire. Nous explorerons des métriques plus avancées et utiles pour la complexité.

Complexité cyclomatique

La complexité cyclomatique est la mesure du nombre de chemins de code indépendants à travers votre application. Un chemin est une séquence d'instructions que l'interprète peut suivre pour arriver à la fin de l'application.

Une façon de penser à la complexité cyclomatique et aux chemins de code est d'imaginer que votre code est comme un réseau ferroviaire.

Pour un voyage, vous devrez peut-être changer de train pour atteindre votre destination. Le système ferroviaire métropolitain de Lisbonne au Portugal est simple et facile à naviguer. La complexité cyclomatique pour tout voyage est égale au nombre de lignes sur lesquelles vous devez voyager:

Si vous aviez besoin d'aller deAlvalade àAnjos, vous feriez alors 5 arrêts sur lelinha verde (ligne verte):

Ce voyage a une complexité cyclomatique de 1 car vous ne prenez qu'un train. C’est un voyage facile. Ce train est équivalent dans cette analogie à une branche de code.

Si vous deviez voyager depuis leAeroporto (aéroport) pour échantillonner lesfood in the district of Belém, le trajet est plus compliqué. Vous devrez changer de train àAlameda etCais do Sodré:

Ce voyage a une complexité cyclomatique de 3, car vous prenez 3 trains. Vous feriez mieux de prendre un taxi!

Vu que vous ne naviguez pas dans Lisbonne, mais que vous écrivez du code, les changements de ligne de train deviennent une branche en cours d’exécution, comme une instructionif. Explorons cet exemple:

x = 1

Il n'y a qu'une seule façon d'exécuter ce code, il a donc une complexité cyclomatique de 1.

Si nous ajoutons une décision, ou une branche au code comme une instructionif, cela augmente la complexité:

x = 1
if x < 2:
    x += 1

Même s'il n'y a qu'une seule façon dont ce code peut être exécuté, commex est une constante, cela a une complexité cyclomatique de 2. Tous les analyseurs de complexité cyclomatique traiteront une instructionif comme une branche.

C'est également un exemple de code trop complexe. L'instructionif est inutile carx a une valeur fixe. Vous pouvez simplement refactoriser cet exemple comme suit:

x = 2

C'était un exemple de jouet, alors explorons quelque chose d'un peu plus réel.

main() a une complexité cyclomatique de 5. Je commenterai chaque branche du code afin que vous puissiez voir où elles se trouvent:

# cyclomatic_example.py
import sys

def main():
    if len(sys.argv) > 1:  # 1
        filepath = sys.argv[1]
    else:
        print("Provide a file path")
        exit(1)
    if filepath:  # 2
        with open(filepath) as fp:  # 3
            for line in fp.readlines():  # 4
                if line != "\n":  # 5
                    print(line, end="")

if __name__ == "__main__":  # Ignored.
    main()

Il existe certainement des façons de transformer le code en une alternative beaucoup plus simple. Nous y reviendrons plus tard.

Note: La mesure de la complexité cyclomatique était dedeveloped by Thomas J. McCabe, Sr en 1976. Vous pouvez le voir appelé lesMcCabe metric ouMcCabe number.

Dans les exemples suivants, nous utiliserons la bibliothèqueradonfrom PyPi pour calculer les métriques. Vous pouvez l'installer maintenant:

$ pip install radon

Pour calculer la complexité cyclomatique à l'aide deradon, vous pouvez enregistrer l'exemple dans un fichier appelécyclomatic_example.py et utiliserradon à partir de la ligne de commande.

La commanderadon prend 2 arguments principaux:

  1. Le type d'analyse (cc pour la complexité cyclomatique)

  2. Un chemin vers le fichier ou le dossier à analyser

Exécutez la commanderadon avec l'analysecc par rapport au fichiercyclomatic_example.py. L'ajout de-s donnera la complexité cyclomatique de la sortie:

$ radon cc cyclomatic_example.py -s
cyclomatic_example.py
    F 4:0 main - B (6)

La sortie est un peu cryptique. Voici ce que signifie chaque partie:

  • F signifie fonction,M signifie méthode etC signifie classe.

  • main est le nom de la fonction.

  • 4 est la ligne sur laquelle la fonction démarre.

  • B est la note de A à F. A est la meilleure note, c'est-à-dire la moindre complexité.

  • Le nombre entre parenthèses,6, est la complexité cyclomatique du code.

Métriques Halstead

Les mesures de complexité Halstead se rapportent à la taille de la base de code d'un programme. Ils ont été développés par Maurice H. Halstead en 1977. Il y a 4 mesures dans les équations de Halstead:

  • Operands sont des valeurs et des noms de variables.

  • Operators sont tous les mots-clés intégrés, tels queif,else,for ouwhile.

  • Length (N) est le nombre d'opérateurs plus le nombre d'opérandes dans votre programme.

  • Vocabulary (h) est le nombre d'opérateursunique plus le nombre d'opérandesunique dans votre programme a.

Il y a ensuite 3 mesures supplémentaires avec ces mesures:

  • Volume (V) représente un produit deslength et desvocabulary.

  • Difficulty (D) représente un produit de la moitié des opérandes uniques et de la réutilisation des opérandes.

  • Effort (E) est la métrique globale qui est un produit devolume etdifficulty.

Tout cela est très abstrait, alors disons-le en termes relatifs:

  • L'effort de votre application est plus élevé si vous utilisez beaucoup d'opérateurs et d'opérandes uniques.

  • L'effort de votre application est moindre si vous utilisez quelques opérateurs et moins de variables.

Pour l'exemplecyclomatic_complexity.py, les opérateurs et les opérandes apparaissent tous deux sur la première ligne:

import sys  # import (operator), sys (operand)

import est un opérateur etsys est le nom du module, c'est donc un opérande.

Dans un exemple légèrement plus complexe, il existe un certain nombre d'opérateurs et d'opérandes:

if len(sys.argv) > 1:
    ...

Il y a 5 opérateurs dans cet exemple:

  1. if

  2. (

  3. )

  4. >

  5. :

De plus, il existe 2 opérandes:

  1. sys.argv

  2. 1

Sachez queradon ne compte qu'un sous-ensemble d'opérateurs. Par exemple, les parenthèses sont exclues dans tous les calculs.

Pour calculer les mesures Halstead enradon, vous pouvez exécuter la commande suivante:

$ radon hal cyclomatic_example.py
cyclomatic_example.py:
    h1: 3
    h2: 6
    N1: 3
    N2: 6
    vocabulary: 9
    length: 9
    calculated_length: 20.264662506490406
    volume: 28.529325012980813
    difficulty: 1.5
    effort: 42.793987519471216
    time: 2.377443751081734
    bugs: 0.009509775004326938

Pourquoiradon donne-t-il une métrique pour le temps et les bogues?

Halstead a émis l'hypothèse que vous pouviez estimer le temps nécessaire en secondes pour coder en divisant l'effort (E) par 18.

Halstead a également déclaré que le nombre prévu de bogues pourrait être estimé en divisant le volume (V) par 3000. Gardez à l'esprit que cela a été écrit en 1977, avant même que Python ne soit inventé! Alors ne paniquez pas et commencez tout de suite à chercher des bugs.

Indice de maintenabilité

L'indice de maintenabilité apporte les mesures de complexité cyclomatique McCabe et de volume Halstead dans une échelle à peu près comprise entre zéro et cent.

Si vous êtes intéressé, l'équation originale est la suivante:

MI Equation

Dans l'équation,V est la métrique de volume de Halstead,C est la complexité cyclomatique etL est le nombre de lignes de code.

Si vous êtes aussi déconcerté que moi lorsque j'ai vu cette équation pour la première fois, cela signifie: il calcule une échelle qui inclut le nombre de variables, d'opérations, de chemins de décision et de lignes de code.

Il est utilisé dans de nombreux outils et langues, c'est donc l'une des mesures les plus standard. Cependant, il y a de nombreuses révisions de l'équation, donc le nombre exact ne doit pas être considéré comme un fait. radon,wily et Visual Studio limitent le nombre entre 0 et 100.

Sur l'échelle de l'indice de maintenabilité, tout ce dont vous devez faire attention, c'est quand votre code devient significativement plus bas (vers 0). L'échelle considère tout ce qui est inférieur à 25 commehard to maintain, et tout ce qui dépasse 75 commeeasy to maintain. L'indice de maintenabilité est également appeléMI.

L'indice de maintenabilité peut être utilisé comme mesure pour obtenir la maintenabilité actuelle de votre application et voir si vous progressez en la refactorisant.

Pour calculer l'indice de maintenabilité à partir deradon, exécutez la commande suivante:

$ radon mi cyclomatic_example.py -s
cyclomatic_example.py - A (87.42)

Dans ce résultat,A est la note queradon a appliquée au nombre87.42 sur une échelle. Sur cette échelle,A est le plus maintenable etF le moins.

Utilisation dewily pour capturer et suivre la complexité de vos projets

wily is an open-source software project pour collecter les métriques de complexité du code, y compris celles que nous avons couvertes jusqu'à présent comme Halstead, Cyclomatic et LOC. wily s'intègre à Git et peut automatiser la collecte de métriques entre les branches et les révisions Git.

Le but dewily est de vous donner la possibilité de voir les tendances et les changements dans la complexité de votre code au fil du temps. Si vous tentiez d’affiner une voiture ou d’améliorer votre condition physique, commencez par mesurer une ligne de base et suivre les améliorations au fil du temps.

Installation dewily

wily est disponibleon PyPi et peut être installé à l'aide de pip:

$ pip install wily

Une fois quewily est installé, certaines commandes sont disponibles dans votre ligne de commande:

  • wily build: parcourt l'historique Git et analyse les métriques pour chaque fichier

  • wily report: voit la tendance historique dans les métriques pour un fichier ou un dossier donné

  • wily graph: graphique un ensemble de métriques dans un fichier HTML

Construire un cache

Avant de pouvoir utiliserwily, vous devez analyser votre projet. Cela se fait à l'aide de la commandewily build.

Pour cette section du tutoriel, nous analyserons le très populaire packagerequests, utilisé pour parler aux API HTTP. Parce que ce projet est open-source et disponible sur GitHub, nous pouvons facilement accéder et télécharger une copie du code source:

$ git clone https://github.com/requests/requests
$ cd requests
$ ls
AUTHORS.rst        CONTRIBUTING.md    LICENSE            Makefile
Pipfile.lock       _appveyor          docs               pytest.ini
setup.cfg          tests              CODE_OF_CONDUCT.md HISTORY.md
MANIFEST.in        Pipfile            README.md          appveyor.yml
ext                requests           setup.py           tox.ini

Les utilisateurs Windows deNote: doivent utiliser l'invite de commande PowerShell pour les exemples suivants au lieu de la ligne de commande MS-DOS traditionnelle. Pour démarrer l'interface de ligne de commande PowerShell, appuyez surWin[.kbd .key-r]#R## and type `+powershell` then [.keys] [.kbd .key-enter]Enter #.

Vous verrez un certain nombre de dossiers ici, pour les tests, la documentation et la configuration. Nous ne sommes intéressés que par le code source du package Pythonrequests, qui se trouve dans un dossier appelérequests.

Appelez la commandewily build à partir du code source cloné et fournissez le nom du dossier de code source comme premier argument:

$ wily build requests

Cela prendra quelques minutes à analyser, en fonction de la puissance CPU de votre ordinateur:

Screenshot capture of Wily build command

Collecte de données sur votre projet

Une fois que vous avez analysé le code source derequests, vous pouvez interroger n'importe quel fichier ou dossier pour voir les métriques clés. Plus tôt dans le didacticiel, nous avons discuté des éléments suivants:

  • Lignes de code

  • Indice de maintenabilité

  • Complexité cyclomatique

Ce sont les 3 métriques par défaut enwily. Pour afficher ces métriques pour un fichier spécifique (tel querequests/api.py), exécutez la commande suivante:

$ wily report requests/api.py

wily imprimera un rapport tabulaire sur les métriques par défaut pour chaque commit Git dans l'ordre des dates inversé. Vous verrez le commit le plus récent en haut et le plus ancien en bas:

Révision Auteur Date MI Lignes de code Complexité cyclomatique

f37daf2

Nate Prewitt

13/01/2019

100 (0,0)

158 (0)

9 (0)

6dd410f

Ofek Lev

13/01/2019

100 (0,0)

158 (0)

9 (0)

5c1f72e

Nate Prewitt

14/12/2018

100 (0,0)

158 (0)

9 (0)

c4d7680

Matthieu Moy

14/12/2018

100 (0,0)

158 (0)

9 (0)

c452e3b

Nate Prewitt

11/12/2018

100 (0,0)

158 (0)

9 (0)

5a1e738

Nate Prewitt

10/12/2018

100 (0,0)

158 (0)

9 (0)

Cela nous indique que le fichierrequests/api.py a:

  • 158 lignes de code

  • Un indice de maintenabilité parfait de 100

  • Une complexité cyclomatique de 9

Pour voir d'autres mesures, vous devez d'abord connaître leurs noms. Vous pouvez le voir en exécutant la commande suivante:

$ wily list-metrics

Vous verrez une liste d'opérateurs, de modules qui analysent le code et les métriques qu'ils fournissent.

Pour interroger d'autres mesures sur la commande de rapport, ajoutez leurs noms après le nom de fichier. Vous pouvez ajouter autant de métriques que vous le souhaitez. Voici un exemple avec le classement de maintenabilité et les lignes de code source:

$ wily report requests/api.py maintainability.rank raw.sloc

Vous verrez que le tableau a maintenant 2 colonnes différentes avec les métriques alternatives.

Représentation graphique des mesures

Maintenant que vous connaissez les noms des métriques et comment les interroger sur la ligne de commande, vous pouvez également les visualiser dans des graphiques. wily prend en charge les graphiques HTML et interactifs avec une interface similaire à la commande de rapport:

$ wily graph requests/sessions.py maintainability.mi

Votre navigateur par défaut s'ouvrira avec un graphique interactif comme celui-ci:

Screenshot capture of Wily graph command

Vous pouvez survoler des points de données spécifiques et il affichera le message de validation Git ainsi que les données.

Si vous souhaitez enregistrer le fichier HTML dans un dossier ou un référentiel, vous pouvez ajouter l'indicateur-o avec le chemin vers un fichier:

$ wily graph requests/sessions.py maintainability.mi -o my_report.html

Il y aura maintenant un fichier appelémy_report.html que vous pouvez partager avec d'autres. Cette commande est idéale pour les tableaux de bord d'équipe.

wily en tant que crochetpre-commit

wily peut être configuré pour qu'avant de valider les modifications de votre projet, il puisse vous alerter des améliorations ou des dégradations de complexité.

wily a une commandewily diff, qui compare les dernières données indexées avec la copie de travail actuelle d'un fichier.

Pour exécuter une commandewily diff, indiquez les noms des fichiers que vous avez modifiés. Par exemple, si j'ai apporté des modifications àrequests/api.py, vous verrez l'impact sur les métriques en exécutantwily diff avec le chemin du fichier:

$ wily diff requests/api.py

Dans la réponse, vous verrez toutes les métriques modifiées, ainsi que les fonctions ou classes qui ont changé pour la complexité cyclomatique:

Screenshot of the wily diff command

La commandediff peut être associée à un outil appelépre-commit. pre-commit insère un hook dans votre configuration Git qui appelle un script à chaque fois que vous exécutez la commandegit commit.

Pour installerpre-commit, vous pouvez installer à partir de PyPI:

$ pip install pre-commit

Ajoutez ce qui suit à un.pre-commit-config.yaml dans le répertoire racine de vos projets:

repos:
-   repo: local
    hooks:
    -   id: wily
        name: wily
        entry: wily diff
        verbose: true
        language: python
        additional_dependencies: [wily]

Une fois cela défini, vous exécutez la commandepre-commit install pour finaliser les choses:

$ pre-commit install

Chaque fois que vous exécutez la commandegit commit, elle appellerawily diff avec la liste des fichiers que vous avez ajoutés à vos modifications par étapes.

wily est un utilitaire utile pour baser la complexité de votre code et mesurer les améliorations que vous apportez lorsque vous commencez à refactoriser.

Refactoring en Python

Le refactoring est la technique de modification d'une application (soit le code, soit l'architecture) afin qu'elle se comporte de la même manière à l'extérieur, mais en interne s'est améliorée. Ces améliorations peuvent être la stabilité, les performances ou la réduction de la complexité.

L'un des plus anciens chemins de fer souterrains du monde, le métro de Londres, a commencé en 1863 avec l'ouverture de la ligne métropolitaine. Il avait des voitures en bois allumées au gaz tirées par des locomotives à vapeur. À l'ouverture du chemin de fer, il était adapté à l'usage. 1900 a apporté l'invention des chemins de fer électriques.

En 1908, le métro de Londres s'était étendu à 8 voies ferrées. Pendant la Seconde Guerre mondiale, les stations de métro de Londres ont été fermées aux trains et utilisées comme abris anti-aériens. Le métro londonien moderne transporte des millions de passagers par jour avec plus de 270 stations:

Il est presque impossible d’écrire du code parfait la première fois et les exigences changent fréquemment. Si vous aviez demandé aux concepteurs originaux du chemin de fer de concevoir un réseau adapté à 10 millions de passagers par jour en 2020, ils n'auraient pas conçu le réseau qui existe aujourd'hui.

Au lieu de cela, le chemin de fer a subi une série de changements continus pour optimiser son fonctionnement, sa conception et son aménagement en fonction des changements dans la ville. Il a été refactorisé.

Dans cette section, vous découvrirez comment refactoriser en toute sécurité en tirant parti des tests et des outils. Vous verrez également comment utiliser la fonctionnalité de refactorisation dansVisual Studio Code etPyCharm:

Refactoring Section Table of Contents

Éviter les risques avec la refactorisation: tirer parti des outils et passer des tests

Si le point de refactoring est d'améliorer les internes d'une application sans impact sur les externes, comment vous assurez-vous que les externes n'ont pas changé?

Avant de vous lancer dans un projet de refactoring majeur, vous devez vous assurer que vous disposez d'une suite de tests solide pour votre application. Idéalement, cette suite de tests devrait être principalement automatisée, de sorte que lorsque vous apportez des modifications, vous voyez l'impact sur l'utilisateur et vous y parvenez rapidement.

Si vous souhaitez en savoir plus sur les tests en Python,Getting Started With Testing in Python est un excellent point de départ.

Il n'y a pas de nombre parfait de tests à effectuer sur votre application. Mais, plus la suite de tests est robuste et complète, plus vous pouvez refactoriser votre code de manière agressive.

Les deux tâches les plus courantes que vous effectuerez lors de la refactorisation sont:

  • Renommer des modules, des fonctions, des classes et des méthodes

  • Recherche des utilisations des fonctions, des classes et des méthodes pour voir où elles sont appelées

Vous pouvez simplement le faire manuellement en utilisantsearch and replace, mais cela prend du temps et est risqué. Au lieu de cela, il existe d'excellents outils pour effectuer ces tâches.

Utilisation derope pour la refactorisation

rope est un utilitaire Python gratuit pour refactoriser le code Python. Il est livré avec un ensemble d'APIextensive pour refactoriser et renommer les composants dans votre base de code Python.

rope peut être utilisé de deux manières:

  1. En utilisant un plugin d'éditeur, pourVisual Studio Code,Emacs ouVim

  2. Directement en écrivant des scripts pour refactoriser votre application

Pour utiliser corde comme bibliothèque, installez d'abordrope en exécutantpip:

$ pip install rope

Il est utile de travailler avecrope sur le REPL pour pouvoir explorer le projet et voir les changements en temps réel. Pour commencer, importez le typeProject et instanciez-le avec le chemin d'accès au projet:

>>>

>>> from rope.base.project import Project

>>> proj = Project('requests')

La variableproj peut désormais exécuter une série de commandes, commeget_files etget_file, pour obtenir un fichier spécifique. Récupérez le fichierapi.py et affectez-le à une variable appeléeapi:

>>>

>>> [f.name for f in proj.get_files()]
['structures.py', 'status_codes.py', ...,'api.py', 'cookies.py']

>>> api = proj.get_file('api.py')

Si vous souhaitez renommer ce fichier, vous pouvez simplement le renommer sur le système de fichiers. Cependant, tous les autres fichiers Python de votre projet qui ont importé l'ancien nom seraient désormais rompus. Renommons lesapi.py ennew_api.py:

>>>

>>> from rope.refactor.rename import Rename

>>> change = Rename(proj, api).get_changes('new_api')

>>> proj.do(change)

En exécutantgit status, vous verrez querope a apporté quelques modifications au référentiel:

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes not staged for commit:
  (use "git add/rm ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   requests/__init__.py
    deleted:    requests/api.py

Untracked files:
  (use "git add ..." to include in what will be committed)

   requests/.ropeproject/
   requests/new_api.py

no changes added to commit (use "git add" and/or "git commit -a")

Les trois modifications apportées parrope sont les suivantes:

  1. requests/api.py supprimé et créérequests/new_api.py

  2. requests/__init__.py modifié pour importer denew_api au lieu deapi

  3. Création d'un dossier de projet nommé.ropeproject

Pour réinitialiser la modification, exécutezgit reset.

Il y ahundreds of other refactorings qui peut être fait avecrope.

Utilisation du code Visual Studio pour la refactorisation

Visual Studio Code ouvre un petit sous-ensemble des commandes de refactoring disponibles dansrope via sa propre interface utilisateur.

Vous pouvez:

  1. Extraire des variables d'une instruction

  2. Extraire des méthodes d'un bloc de code

  3. Trier les importations dans un ordre logique

Voici un exemple d'utilisation de la commandeExtract methods de la palette de commandes:

Screenshot of Visual Studio Code refactoring

Utilisation de PyCharm pour le refactoring

Si vous utilisez ou envisagez d'utiliser PyCharm en tant qu'éditeur Python, il convient de noter les puissantes capacités de refactorisation dont il dispose.

Vous pouvez accéder à tous les raccourcis de refactoring avec leCtrl[.kbd .key-t]#T## command on Windows and macOS. The shortcut to access refactoring in Linux is [.keys]#[.kbd .key-control]##Ctrl##Shift[.kbd .key-alt]##Alt##[.kbd .key-t]#T #.

Recherche des appelants et utilisations des fonctions et des classes

Avant de supprimer une méthode ou une classe ou de modifier son comportement, vous devez savoir quel code en dépend. PyCharm peut rechercher toutes les utilisations d'une méthode, d'une fonction ou d'une classe dans votre projet.

Pour accéder à cette fonctionnalité, sélectionnez une méthode, une classe ou une variable en cliquant avec le bouton droit de la souris et sélectionnezFind Usages:

Finding usages in PyCharm

Tout le code qui utilise vos critères de recherche est affiché dans un panneau en bas. Vous pouvez double-cliquer sur n'importe quel élément pour accéder directement à la ligne en question.

Utilisation des outils de refactorisation PyCharm

Certaines des autres commandes de refactoring incluent la possibilité de:

  • Extraire des méthodes, des variables et des constantes du code existant

  • Extraire les classes abstraites des signatures de classe existantes, y compris la possibilité de spécifier des méthodes abstraites

  • Renommer pratiquement n'importe quoi, d'une variable à une méthode, un fichier, une classe ou un module

Voici un exemple de changement de nom du même moduleapi.py que vous avez renommé précédemment à l'aide du modulerope ennew_api.py:

How to rename methods in pycharm

La commande rename est contextualisée dans l'interface utilisateur, ce qui rend le refactoring rapide et simple. Il a mis à jour automatiquement les importations dans__init__.py avec le nouveau nom de module.

Un autre refactor utile est la commandeChange Signature. Cela peut être utilisé pour ajouter, supprimer ou renommer des arguments à une fonction ou une méthode. Il recherchera les usages et les mettra à jour pour vous:

Changing method signatures in PyCharm

Vous pouvez définir des valeurs par défaut et également décider comment le refactoring doit gérer les nouveaux arguments.

Sommaire

Le refactoring est une compétence importante pour tout développeur. Comme vous l’avez appris dans ce chapitre, vous n’êtes pas seul. Les outils et les IDE sont déjà livrés avec de puissantes fonctionnalités de refactoring pour pouvoir apporter des modifications rapidement.

Anti-Patterns de complexité

Maintenant que vous savez comment mesurer la complexité, comment la mesurer et comment refactoriser votre code, il est temps d'apprendre 5 anti-modèles courants qui rendent le code plus complexe qu'il ne devrait l'être:

Anti-Patterns Table of Contents

Si vous pouvez maîtriser ces modèles et savoir comment les refactoriser, vous serez bientôt sur la bonne voie (jeu de mots) vers une application Python plus maintenable.

1. Fonctions qui devraient être des objets

Python prend en chargeprocedural programming en utilisant des fonctions et aussiinheritable classes. Les deux sont très puissants et doivent être appliqués à différents problèmes.

Prenons cet exemple de module pour travailler avec des images. La logique des fonctions a été supprimée par souci de concision:

# imagelib.py

def load_image(path):
    with open(path, "rb") as file:
        fb = file.load()
    image = img_lib.parse(fb)
    return image

def crop_image(image, width, height):
    ...
    return image

def get_image_thumbnail(image, resolution=100):
    ...
    return image

Il y a quelques problèmes avec cette conception:

  1. Il n’est pas clair sicrop_image() etget_image_thumbnail() modifient la variableimage d’origine ou créent de nouvelles images. Si vous vouliez charger une image puis créer à la fois une image recadrée et une vignette, devriez-vous d'abord copier l'instance? Vous pouvez lire le code source dans les fonctions, mais vous ne pouvez pas compter sur tous les développeurs pour le faire.

  2. Vous devez passer la variable image comme argument dans chaque appel aux fonctions image.

Voici à quoi pourrait ressembler le code appelant:

from imagelib import load_image, crop_image, get_image_thumbnail

image = load_image('~/face.jpg')
image = crop_image(image, 400, 500)
thumb = get_image_thumbnail(image)

Voici quelques symptômes de code utilisant des fonctions qui pourraient être refactorisées en classes:

  • Arguments similaires entre les fonctions

  • Nombre plus élevé de Halsteadh2unique operands

  • Mélange de fonctions mutables et immuables

  • Fonctions réparties sur plusieurs fichiers Python

Voici une version refactorisée de ces 3 fonctions, où se produit ce qui suit:

  • .__init__() remplaceload_image().

  • crop() devient une méthode de classe.

  • get_image_thumbnail() devient une propriété.

La résolution des vignettes est devenue une propriété de classe, elle peut donc être modifiée globalement ou sur cette instance particulière:

# imagelib.py

class Image(object):
    thumbnail_resolution = 100
    def __init__(self, path):
        ...

    def crop(self, width, height):
        ...

    @property
    def thumbnail(self):
        ...
        return thumb

S'il y avait beaucoup plus de fonctions liées à l'image dans ce code, le refactoring vers une classe pourrait faire un changement radical. La considération suivante serait la complexité du code consommateur.

Voici à quoi ressemblerait l'exemple refactorisé:

from imagelib import Image

image = Image('~/face.jpg')
image.crop(400, 500)
thumb = image.thumbnail

Dans le code résultant, nous avons résolu les problèmes d'origine:

  • Il est clair quethumbnail renvoie une vignette puisqu'il s'agit d'une propriété, et qu'il ne modifie pas l'instance.

  • Le code ne nécessite plus de créer de nouvelles variables pour l'opération de recadrage.

2. Objets devant être des fonctions

Parfois, l'inverse est vrai. Il existe un code orienté objet qui conviendrait mieux à une ou deux fonctions simples.

Voici quelques signes révélateurs d'une mauvaise utilisation des classes:

  • Classes avec 1 méthode (autre que.__init__())

  • Classes contenant uniquement des méthodes statiques

Prenons cet exemple de classe d'authentification:

# authenticate.py

class Authenticator(object):
    def __init__(self, username, password):
        self.username = username
        self.password = password

    def authenticate(self):
        ...
        return result

Il serait plus logique d'avoir juste une fonction simple nomméeauthenticate() qui prendusername etpassword comme arguments:

# authenticate.py

def authenticate(username, password):
    ...
    return result

Vous n’avez pas à vous asseoir et à rechercher manuellement les classes qui correspondent à ces critères:pylint est livré avec une règle selon laquelle les classes doivent avoir au moins 2 méthodes publiques. Pour plus d'informations sur PyLint et d'autres outils de qualité de code, vous pouvez consulterPython Code Quality.

Pour installerpylint, exécutez la commande suivante dans votre console:

$ pip install pylint

pylint prend un certain nombre d'arguments facultatifs, puis le chemin vers un ou plusieurs fichiers et dossiers. Si vous exécutezpylint avec ses paramètres par défaut, il va donner beaucoup de sortie carpylint a un grand nombre de règles. Au lieu de cela, vous pouvez exécuter des règles spécifiques. L'ID de règletoo-few-public-methods estR0903. Vous pouvez rechercher ceci sur lesdocumentation website:

$ pylint --disable=all --enable=R0903 requests
************* Module requests.auth
requests/auth.py:72:0: R0903: Too few public methods (1/2) (too-few-public-methods)
requests/auth.py:100:0: R0903: Too few public methods (1/2) (too-few-public-methods)
************* Module requests.models
requests/models.py:60:0: R0903: Too few public methods (1/2) (too-few-public-methods)

-----------------------------------
Your code has been rated at 9.99/10

Cette sortie nous indique queauth.py contient 2 classes qui n'ont qu'une seule méthode publique. Ces classes se trouvent aux lignes 72 et 100. Il existe également une classe à la ligne 60 demodels.py avec une seule méthode publique.

3. Conversion du code «triangulaire» en code plat

Si vous deviez faire un zoom arrière sur votre code source et incliner votre tête de 90 degrés vers la droite, est-ce que les espaces blancs semblent plats comme la Hollande ou montagneux comme l'Himalaya? Le code montagneux est un signe que votre code contient beaucoup d'imbrication.

Voici l'un des principes desZen of Python:

"L'appartement est mieux que imbriqué"

- Tim Peters, Zen de Python

Pourquoi le code plat serait-il meilleur que le code imbriqué? Parce que le code imbriqué rend plus difficile la lecture et la compréhension de ce qui se passe. Le lecteur doit comprendre et mémoriser les conditions au fil des branches.

Ce sont les symptômes d'un code hautement imbriqué:

  • Une complexité cyclomatique élevée en raison du nombre de branches de code

  • Un indice de maintenabilité faible en raison de la complexité cyclomatique élevée par rapport au nombre de lignes de code

Prenez cet exemple qui examine l'argumentdata pour les chaînes qui correspondent au moterror. Il vérifie d'abord si l'argumentdata est une liste. Ensuite, il itère sur chacun et vérifie si l'élément est une chaîne. S'il s'agit d'une chaîne et que la valeur est"error", alors il renvoieTrue. Sinon, il renvoieFalse:

def contains_errors(data):
    if isinstance(data, list):
        for item in data:
            if isinstance(item, str):
                if item == "error":
                    return True
    return False

Cette fonction aurait un faible indice de maintenabilité car elle est petite, mais elle a une complexité cyclomatique élevée.

Au lieu de cela, nous pouvons refactoriser cette fonction en «retournant tôt» pour supprimer un niveau d'imbrication et en retournantFalse si la valeur dedata n'est pas list. Puis en utilisant.count() sur l'objet de liste pour compter les instances de"error". La valeur de retour est alors une évaluation que le.count() est supérieur à zéro:

def contains_errors(data):
    if not isinstance(data, list):
        return False
    return data.count("error") > 0

Une autre technique pour réduire l'imbrication consiste à tirer parti des compréhensions de liste. Ce modèle courant de création d'une nouvelle liste, en parcourant chaque élément d'une liste pour voir s'il correspond à un critère, puis en ajoutant toutes les correspondances à la nouvelle liste:

results = []
for item in iterable:
    if item == match:
        results.append(item)

Ce code peut être remplacé par une compréhension de liste plus rapide et plus efficace.

Refactorisez le dernier exemple en une compréhension de liste et une instructionif:

results = [item for item in iterable if item == match]

Ce nouvel exemple est plus petit, moins complexe et plus performant.

Si vos données ne sont pas une liste à dimension unique, vous pouvez utiliser le packageitertools dans la bibliothèque standard, qui contient des fonctions de création d'itérateurs à partir de structures de données. Vous pouvez l'utiliser pour chaîner ensemble des itérables, mapper des structures, faire du vélo ou répéter sur des itérables existants.

Itertools contient également des fonctions de filtrage des données, commefilterfalse(). Pour plus d'informations sur Itertools, consultezItertools in Python 3, By Example.

4. Gestion de dictionnaires complexes avec des outils de requête

L'un des types de noyau les plus puissants et les plus utilisés de Python est le dictionnaire. Il est rapide, efficace, évolutif et hautement flexible.

Si vous êtes nouveau dans les dictionnaires ou pensez pouvoir en tirer davantage parti, vous pouvez lireDictionaries in Python pour plus d'informations.

Cela a un effet secondaire majeur: lorsque les dictionnaires sont fortement imbriqués, le code qui les interroge devient également imbriqué.

Prenez cet exemple de données, un échantillon des lignes du métro de Tokyo que vous avez vues plus tôt:

data = {
 "network": {
  "lines": [
    {
     "name.en": "Ginza",
     "name.jp": "銀座線",
     "color": "orange",
     "number": 3,
     "sign": "G"
    },
    {
     "name.en": "Marunouchi",
     "name.jp": "丸ノ内線",
     "color": "red",
     "number": 4,
     "sign": "M"
    }
  ]
 }
}

Si vous vouliez obtenir la ligne qui correspondait à un certain nombre, cela pourrait être réalisé dans une petite fonction:

def find_line_by_number(data, number):
    matches = [line for line in data if line['number'] == number]
    if len(matches) > 0:
        return matches[0]
    else:
        raise ValueError(f"Line {number} does not exist.")

Même si la fonction elle-même est petite, l'appel de la fonction est inutilement compliqué car les données sont tellement imbriquées:

>>>

>>> find_line_by_number(data["network"]["lines"], 3)

Il existe des outils tiers pour interroger les dictionnaires en Python. Certains des plus populaires sontJMESPath,glom,asq etflupy.

JMESPath peut vous aider avec notre réseau ferroviaire. JMESPath est un langage de requête conçu pour JSON, avec un plugin disponible pour Python qui fonctionne avec les dictionnaires Python. Pour installer JMESPath, procédez comme suit:

$ pip install jmespath

Ouvrez ensuite une REPL Python pour explorer l'API JMESPath, en copiant dans le dictionnairedata. Pour commencer, importezjmespath et appelezsearch() avec une chaîne de requête comme premier argument et les données comme second. La chaîne de requête"network.lines" signifie retournerdata['network']['lines']:

>>>

>>> import jmespath

>>> jmespath.search("network.lines", data)
[{'name.en': 'Ginza', 'name.jp': '銀座線',
  'color': 'orange', 'number': 3, 'sign': 'G'},
 {'name.en': 'Marunouchi', 'name.jp': '丸ノ内線',
  'color': 'red', 'number': 4, 'sign': 'M'}]

Lorsque vous travaillez avec des listes, vous pouvez utiliser des crochets et fournir une requête à l'intérieur. La requête «tout» est simplement*. Vous pouvez ensuite ajouter le nom de l'attribut à l'intérieur de chaque élément correspondant à renvoyer. Si vous souhaitez obtenir le numéro de ligne pour chaque ligne, vous pouvez le faire:

>>>

>>> jmespath.search("network.lines[*].number", data)
[3, 4]

Vous pouvez fournir des requêtes plus complexes, comme un== ou<. La syntaxe est un peu inhabituelle pour les développeurs Python, alors gardez lesdocumentation à portée de main pour référence.

Si nous voulions trouver la ligne avec le nombre3, cela peut être fait en une seule requête:

>>>

>>> jmespath.search("network.lines[?number==`3`]", data)
[{'name.en': 'Ginza', 'name.jp': '銀座線', 'color': 'orange', 'number': 3, 'sign': 'G'}]

Si nous voulions obtenir la couleur de cette ligne, vous pouvez ajouter l'attribut à la fin de la requête:

>>>

>>> jmespath.search("network.lines[?number==`3`].color", data)
['orange']

JMESPath peut être utilisé pour réduire et simplifier le code qui interroge et recherche dans des dictionnaires complexes.

5. Utilisation deattrs etdataclasses pour réduire le code

Un autre objectif lors de la refactorisation est de simplement réduire la quantité de code dans la base de code tout en obtenant les mêmes comportements. Les techniques présentées jusqu'à présent peuvent contribuer considérablement à la refactorisation du code en modules plus petits et plus simples.

Certaines autres techniques nécessitent une connaissance de la bibliothèque standard et de certaines bibliothèques tierces.

Qu'est-ce que la chaudière?

Le code de la chaudière est un code qui doit être utilisé dans de nombreux endroits avec peu ou pas de modifications.

En prenant notre réseau de trains comme exemple, si nous devions le convertir en types à l'aide de classes Python et d'indices de type Python 3, cela pourrait ressembler à ceci:

from typing import List

class Line(object):
    def __init__(self, name_en: str, name_jp: str, color: str, number: int, sign: str):
        self.name_en = name_en
        self.name_jp = name_jp
        self.color = color
        self.number = number
        self.sign = sign

    def __repr__(self):
        return f""

    def __str__(self):
        return f"The {self.name_en} line"

class Network(object):
    def __init__(self, lines: List[Line]):
        self._lines = lines

    @property
    def lines(self) -> List[Line]:
        return self._lines

Maintenant, vous voudrez peut-être aussi ajouter d'autres méthodes magiques, comme.__eq__(). Ce code est passe-partout. Il n'y a pas de logique métier ni aucune autre fonctionnalité ici: nous copions simplement des données d'un endroit à un autre.

Un cas pourdataclasses

Introduit dans la bibliothèque standard de Python 3.7, avec un package de rétroportage pour Python 3.6 sur PyPI, le module dataclasses peut aider à supprimer beaucoup de passe-partout pour ces types de classes où vous ne faites que stocker des données.

Pour convertir la classeLine ci-dessus en une classe de données, convertissez tous les champs en attributs de classe et assurez-vous qu'ils ont des annotations de type:

from dataclasses import dataclass

@dataclass
class Line(object):
    name_en: str
    name_jp: str
    color: str
    number: int
    sign: str

Vous pouvez alors créer une instance de typeLine avec les mêmes arguments que précédemment, avec les mêmes champs, et même.__str__(),.__repr__() et.__eq__() sont implémentés:

>>>

>>> line = Line('Marunouchi', "丸ノ内線", "red", 4, "M")

>>> line.color
red

>>> line2 = Line('Marunouchi', "丸ノ内線", "red", 4, "M")

>>> line == line2
True

Les classes de données sont un excellent moyen de réduire le code avec une seule importation qui est déjà disponible dans la bibliothèque standard. Pour une procédure pas à pas complète, vous pouvez vérifierThe Ultimate Guide to Data Classes in Python 3.7.

Quelques cas d'utilisation deattrs

attrs est un package tiers qui existe depuis beaucoup plus longtemps que les classes de données. attrs a beaucoup plus de fonctionnalités, et il est disponible sur Python 2.7 et 3.4+.

Si vous utilisez Python 3.5 ou une version antérieure,attrs est une excellente alternative àdataclasses. En outre, il offre de nombreuses fonctionnalités supplémentaires.

L'exemple de classes de données équivalentes dansattrs serait similaire. Au lieu d'utiliser des annotations de type, les attributs de classe sont affectés avec une valeur deattrib(). Cela peut prendre des arguments supplémentaires, tels que des valeurs par défaut et des rappels pour valider l'entrée:

from attr import attrs, attrib

@attrs
class Line(object):
    name_en = attrib()
    name_jp = attrib()
    color = attrib()
    number = attrib()
    sign = attrib()

attrs peut être un package utile pour supprimer le code standard et la validation d'entrée sur les classes de données.

Conclusion

Maintenant que vous avez appris à identifier et à gérer le code compliqué, repensez aux étapes que vous pouvez désormais suivre pour faciliter la modification et la gestion de votre application:

  • Commencez par créer une ligne de base de votre projet à l'aide d'un outil tel quewily.

  • Examinez certaines des mesures et commencez avec le module qui a l'indice de maintenabilité le plus bas.

  • Refactoriser ce module en utilisant la sécurité fournie dans les tests et la connaissance d'outils comme PyCharm etrope.

Une fois que vous avez suivi ces étapes et les meilleures pratiques de cet article, vous pouvez effectuer d'autres tâches intéressantes dans votre application, comme l'ajout de nouvelles fonctionnalités et l'amélioration des performances.