Structurer votre projet

Par “structurer” nous entendons les décisions que vous faites concernant comment votre projet atteint au mieux son objectif. Nous avons besoin de considérer comment exploiter au mieux les fonctionnalités de Python pour créer un code propre et efficace. En termes pratiques, “structurer” signifie produire du code propre dont la logique et les dépendances sont claires ainsi que la façon dont les fichiers et dossiers sont organisés dans le système de fichiers.

Quelle fonctions doivent aller dans quels modules? Comment circule la donnée dans le projet? Quelles fonctionnalités et fonctions peuvent être groupées ensemble et isolées? En répondant à des questions comme cela, vous pouvez commencer à planifier, au sens large, ce à quoi votre produit fini ressemblera.

Dans cette section, nous allons jeter un œil de plus près sur les systèmes de module et d’import de Python comme ils sont les éléments centraux pour faire respecter une structure dans votre projet. Nous discuterons ensuite des diverses perspectives sur comment construire du code qui peut être étendu et testé de manière fiable.

Structure du dépôt

C’est important

Juste comme le style de codage, le design d’API, et l’automatisation sont essentiels à un cycle de développement sain, la structure d’un dépôt est une part cruciale de l’architecture de votre projet.

Quand un utilisateur potentiel ou un contributeur arrive sur la page d’un dépôt, ils voient certaines choses:

  • Le nom du projet

  • La description du projet

  • Un tas de fichiers

C’est seulement quand ils font défiler la page que les utilisateurs verront le README de votre projet.

Si votre dépôt est un amas massif de fichiers ou une pagaille imbriquée de répertoires, ils risquent de regarder ailleurs avant même de lire votre belle documentation.

Habillez vous pour le job que vous voulez, pas pour le job que vous avez.

Bien sûr, les premières impressions ne sont pas tout. Vous et vos collègues allez passer un nombre d’heures incalculable à travailler sur ce dépôt, finalement devenir intimement familier avec tous les coins et recoins. Son organisation est importante.

Dépôt exemple

tl;dr (acronyme de “Too Long, I Didn’t Read it”): C’est ce que Kenneth Reitz recommande.

Ce dépôt est disponible sur GitHub.

README.rst
LICENSE
setup.py
requirements.txt
sample/__init__.py
sample/core.py
sample/helpers.py
docs/conf.py
docs/index.rst
tests/test_basic.py
tests/test_advanced.py

Entrons dans quelques détails.

Le module actuel

Emplacement

./sample/ ou ./sample.py

But

Le code qui nous intéresse

Votre paquet de module est le point central du dépôt. Il ne devrait pas être mis à l’écart:

./sample/

Si votre module consiste en un seul fichier, vous pouvez le placer directement à la racine de votre répertoire:

./sample.py

Votre bibliothèque n’appartient pas à un sous-répertoire ambigu comme src ou python.

Licence

Emplacement

./LICENSE

But

Se couvrir juridiquement.

Ceci est sans doute la partie la plus importante de votre dépôt, en dehors du code source lui-même. Les revendications de copyright et le texte de la licence complet devraient être dans ce fichier.

Si vous n’êtes pas sûr de la licence que vous souhaitez utiliser pour votre projet, consultez choosealicense.com.

Bien sûr, vous êtes aussi libre de publier votre code sans une licence, mais cela risque potentiellement d’empêcher de nombreuses personnes d’utiliser votre code.

Setup.py

Emplacement

./setup.py

But

Gestion de la distribution et de la création de paquets

Si votre paquet de module est à la racine de votre dépôt, ceci devrait évidemment être aussi à la racine.

Fichier requirements

Emplacement

./requirements.txt

But

Dépendances de développement.

Un fichier requirements de pip devrait être placé à la racine du dépôt. Il devrait spécifier les dépendances requises pour contribuer au projet: les tests, les builds et la génération de la documentation.

Si votre projet n’a pas de dépendances de développement ou vous préférez la configuration de l’environnement de développement via setup.py, ce fichier peut s’avérer non nécessaire.

Documentation

Emplacement

./docs/

But

Documentation de référence des paquets.

Il y a très peu de raison pour cela qu’il existe ailleurs.

Suite de tests

Emplacement

./test_sample.py ou ./tests

But

Intégration de paquets et tests unitaires

En débutant, une petite suite de tests existera souvent dans un seul fichier:

./test_sample.py

Une fois que la suite de tests grossit, vous pouvez déplacer vos tests dans un répertoire, comme ceci:

tests/test_basic.py
tests/test_advanced.py

Évidemment, ces modules de test doivent importer votre module empaqueté pour le tester. Vous pouvez le faire de plusieurs façons:

  • Attendre que le paquet soit installé dans site-packages.

  • Utiliser un modification de chemin simple (mais explicite) pour résoudre le paquet correctement.

Je recommande fortement ce dernier. Demander à un développeur de lancer setup.py develop pour tester une base de code qui change activement lui demande aussi d’avoir une configuration d’environnement isolé pour chaque instance de la base de code.

Pour donner un contexte d’importation aux tests individuels, créez un fichier tests/config.py.

import os
import sys
sys.path.insert(0, os.path.abspath('..'))

import sample

Ensuite, dans les modules de test individuels, importez le module comme ceci:

from .context import sample

Cela fonctionnera toujours comme prévu quelle que soit la méthode d’installation.

Certains personnes avanceront que vous devriez distribuer vos tests à l’intérieur du module lui-même – Je ne suis pas d’accord. Cela augmente souvent la complexité pour vos utilisateurs; de nombreuses suites de test nécessitent souvent des dépendances et des contextes d’exécution supplémentaires.

Makefile

Emplacement

./Makefile

But

Tâches de gestion génériques.

Si vous jetez un œil à la plupart de mes projets ou n’importe quel projet de Pocoo, vous remarquerez un Makefile qui traîne autour. Pourquoi? Ces projets ne sont pas écrits en C... En bref, make est un outil incroyablement utile pour définir des tâches génériques pour votre projet.

Makefile exemple:

init:
    pip install -r requirements.txt

test:
    py.test tests

D’autres scripts de gestion génériques (comme manage.py ou fabfile.py) appartiennent aussi à la racine du dépôt.

En ce qui concerne les applications Django

J’ai noté une nouvelle tendance dans les applications Django depuis la sortie de Django 1.4. De nombreux développeurs structurent leurs dépôts de manière médiocre à cause des nouveaux modèles d’applications mis à disposition.

Comment? Bien, ils vont dans leur nouveau dépôt encore nu et frais et exécutent ce qui suit, comme ils l’ont toujours fait:

$ django-admin.py start-project samplesite

La structure du dépôt résultant ressemble à ceci:

README.rst
samplesite/manage.py
samplesite/samplesite/settings.py
samplesite/samplesite/wsgi.py
samplesite/samplesite/sampleapp/models.py

Ne faites pas ça.

Des chemins répétitifs sont sources de confusion à la fois pour vos outils et vos développeurs. Une imbrication inutile n’aide personne (à moins d’être nostalgique des dépôts SVN monolithiques)

Faisons-le proprement:

$ django-admin.py start-project samplesite .

Notez le “.”.

La structure résultante:

README.rst
manage.py
samplesite/settings.py
samplesite/wsgi.py
samplesite/sampleapp/models.py

La structure du code est la clé

Grâce à la façon dont les imports et les modules sont traités en Python, il est relativement facile de structurer un projet Python. Facile, ici, signifie que vous n’avez pas beaucoup de contraintes et que le modèle qui fait l’import du module est facile à comprendre. Par conséquent, vous vous retrouvez avec la tâche architecturale pure de concevoir les différentes parties de votre projet et de leurs interactions.

Une structuration facile de projet signifie que c’est aussi facile de mal le faire. Certains signes d’un projet mal structuré incluent:

  • Des dépendances circulaires multiples et désordonnées: si vos classes Table et Chair dans furn.py ont besoin d’importer Carpenter depuis workers.py pour répondre à une question comme table.isdoneby(), et si au contraire la classe Carpenter a besoin d’importer Table et Chair, pour répondre à la question carpenter.whatdo(), alors vous avez une dépendance circulaire. Dans ce cas, vous devrez recourir à des hacks fragiles telles que l’utilisation de déclarations d’importation à l’intérieur de méthodes ou de fonctions.

  • Couplage caché: chaque changement dans l’implémentation de Table casse 20 tests dans des cas de tests non liés parce qu’il casse le code de Carpenter, qui nécessite une intervention ciblée très prudente pour adapter le changement. Cela signifie que vous avez trop d’hypothèses à propos de Table dans le code de Carpenter ou l’inverse.

  • Un usage intensif d’un état ou d’un contexte global: au lieu de passer explicitement (height, width, type, wood) de l’un à l’autre, Table et Carpenter s’appuient sur des variables globales qui peuvent être modifiées et sont modifiées à la volée par différents agents. Vous devez examiner tous les accès à ces variables globales pour comprendre pourquoi une table rectangulaire est devenu un carré, et découvrir que le code du modèle distant est aussi en train de modifier ce contexte, mettant le désordre dans les dimensions de la table.

  • Code Spaghetti: plusieurs pages de clauses if imbriquée et de boucles for avec beaucoup de code procédural copie-collé et aucune segmentation adéquate sont connus comme du code spaghetti. L’indentation significative de Python (une de ses caractéristiques les plus controversées) rend très difficile de maintenir ce genre de code. Donc, la bonne nouvelle est que vous pourriez ne pas en voir de trop.

  • Le code Ravioli est plus probable en Python: il se compose de centaines de petits morceaux semblables de logique, souvent des classes ou des objets, sans structure appropriée. Si vous ne pouvez vous rappeler si vous avez à utiliser FurnitureTable, AssetTable ou Table, ou même TableNew pour votre tâche, vous pourriez être en train de nager dans du code ravioli.

Modules

Les modules Python sont l’une des principales couches d’abstraction disponible et probablement la plus naturelle. Les couches d’abstraction permettent la séparation du code dans des parties contenant des données et des fonctionnalités connexes.

Par exemple, une couche d’un projet peut gérer l’interface avec les actions utilisateurs, tandis qu’un autre gérerait la manipulation de bas-niveau des données. La façon la plus naturelle de séparer ces deux couches est de regrouper toutes les fonctionnalités d’interface dans un seul fichier, et toutes les opérations de bas-niveau dans un autre fichier. Dans ce cas, le fichier d’interface doit importer le fichier de bas-niveau. Cela se fait avec les déclarations import et from ... import.

Dès que vous utilisez les déclarations import, vous utilisez des modules. Ceux-ci peuvent être soit des modules intégrés comme os et sys, soit des modules tiers que vous avez installé dans votre environnement, ou dans les modules internes de votre projet.

Pour rester aligné avec le guide de style, garder les noms de modules courts, en minuscules, et assurez-vous d’éviter d’utiliser des symboles spéciaux comme le point (.) ou le point d’interrogation (?). Donc, un nom de fichier comme my.spam.py est l’un de ceux que vous devriez éviter! Nommer de cette façon interfère avec la manière dont Python cherche pour les modules.

Dans le cas de my.spam.py`, Python attend de trouver un fichier spam.py dans un dossier nommé my, ce qui n’est pas le cas. Il y a un exemple de la comment la notation par point devrait être utilisée dans la documentation Python.

Si vous souhaitez, vous pouvez nommer votre module my_spam.py, mais même notre ami le underscore (tiret bas) ne devrait pas être vu souvent dans les noms de modules.

Mis à part quelques restrictions de nommage, rien de spécial n’est nécessaire pour qu’un fichier Python puisse être un module, mais vous avez besoin de comprendre le mécanisme d’import afin d’utiliser ce concept correctement et éviter certains problèmes.

Concrètement, la déclaration import modu va chercher le fichier approprié, qui est modu.py dans le même répertoire que l’appelant si il existe. Si il n’est pas trouvé, l’interpréteur Python va rechercher modu.py dans le “path” récursivement et lever une exception ImportError si il n’est pas trouvé.

Une fois que modu.py est trouvé, l’interpréteur Python va exécuter le module dans une portée isolée. N’importe quelle déclaration à la racine de modu.py sera exécutée, incluant les autres imports le cas échéant. Les définitions de fonction et classe sont stockées dans le dictionnaire du module.

Ensuite, les variables, les fonctions et les classes du module seront mises à disposition de l’appelant via l’espace de nom du module, un concept central dans la programmation qui est particulièrement utile et puissant en Python.

Dans de nombreuses langages, une directive include file est utilisée par le préprocesseur pour prendre tout le code trouvé dans le fichier et le ‘copier’ dans le code de l’appelant. C’est différent en Python: le code inclus est isolé dans un espace de nom de module, ce qui signifie que vous n’avez pas généralement à vous soucier que le code inclus puisse avoir des effets indésirables, par exemple remplacer une fonction existante avec le même nom.

Il est possible de simuler le comportement plus standard en utilisant une syntaxe particulière de la déclaration d’import: from modu import *. Ceci est généralement considéré comme une mauvaise pratique. L’utilisation d’import * rend le code plus difficile à lire et rend les dépendances moins cloisonnées.

L’utilisation de from modu import func est un moyen de cibler la fonction que vous souhaitez importer et la mettre dans l’espace de nom global. Bien que beaucoup moins néfaste que import * parce que cela montre explicitement ce qui est importé dans l’espace de nom global, son seul avantage par rapport à un import modu, plus simple est qu’il permettra d’économiser un peu de frappe clavier.

Très mauvais

[...]
from modu import *
[...]
x = sqrt(4)  # Is sqrt part of modu? A builtin? Defined above?

Mieux

from modu import sqrt
[...]
x = sqrt(4)  # sqrt may be part of modu, if not redefined in between

Le mieux

import modu
[...]
x = modu.sqrt(4)  # sqrt is visibly part of modu's namespace

Comme mentionné dans la section Style de code, la lisibilité est l’une des principales caractéristiques de Python. La lisibilité signifie éviter du texte standard inutile et le désordre, donc des efforts sont consacrés pour essayer de parvenir à un certain niveau de concision. Mais le laconisme et l’obscur sont les limites où la brièveté doit cesser. Être en mesure de dire immédiatement d’où une classe ou une fonction provient, comme dans l’idiome modu.func, améliore grandement la lisibilité du code et la compréhensibilité dans tous les projets même ceux avec le plus simple fichier unique.

Paquets

Python fournit un système de packaging très simple, qui est simplement une extension du mécanisme de module à un répertoire.

Tout répertoire avec un fichier __init__.py est considéré comme un paquet Python. Les différents modules dans le paquet sont importés d’une manière similaire comme des modules simples, mais avec un comportement spécial pour le fichier __init__.py, qui est utilisé pour rassembler toutes les définitions à l’échelle des paquets.

Un fichier modu.py dans le répertoire pack/ est importé avec la déclaration import pack.modu. Cette déclaration va chercher un fichier __init__.py dans pack, exécuter toutes ses déclarations de premier niveau. Puis elle va chercher un fichier nommé pack/modu.py et exécuter tous ses déclarations de premier niveau. Après ces opérations, n’importe quelle variable, fonction ou classe définie dans modu.py est disponible dans l’espace de nom pack.modu.

Un problème couramment vu est d’ajouter trop de code aux fichiers __init__.py. Lorsque la complexité du projet grossit, il peut y avoir des sous-paquets et sous-sous-paquets dans une structure de répertoire profonde. Dans ce cas, importer un seul élément à partir d’un sous-sous-paquet nécessitera d’exécuter tous les fichiers __init__.py rencontrés en traversant l’arbre.

Laisser un fichier __init__.py vide est considéré comme normal et même une bonne pratique, si les modules du paquet et des sous-paquets n’ont pas besoin de partager aucun code.

Enfin, une syntaxe pratique est disponible pour importer des paquets imbriquées profondément: import very.deep.module as mod. Cela vous permet d’utiliser mod à la place de la répétition verbeuse de very.deep.module.

Programmation orientée objet

Python est parfois décrit comme un langage de programmation orienté objet. Cela peut être quelque peu trompeur et doit être clarifié.

En Python, tout est un objet, et peut être manipulé en tant que tel. Voilà ce que l’on entend quand nous disons, par exemple, que les fonctions sont des objets de première classe. Les fonctions, les classes, les chaînes et même les types sont des objets en Python: comme tout objet, ils ont un type, ils peuvent être passés comme arguments de fonction, et ils peuvent avoir des méthodes et propriétés. Sur ce point, Python est un langage orienté objet.

Cependant, contrairement à Java, Python n’impose pas la programmation orientée objet comme paradigme de programmation principal. Il est parfaitement viable pour un projet de Python de ne pas être orienté objet, à savoir de ne pas utiliser ou très peu de définitions de classes, d’héritage de classe, ou d’autres mécanismes qui sont spécifiques à la programmation orientée objet.

En outre, comme on le voit dans la section modules, la façon dont Python gère les modules et les espaces de nom donne au développeur un moyen naturel pour assurer l’encapsulation et la séparation des couches d’abstraction, les deux étant les raisons les plus courantes d’utiliser l’orientation objet. Par conséquent, les programmeurs Python ont plus de latitude pour ne pas utiliser l’orientation objet, quand elle n’est pas requise par le modèle métier.

Il y a quelques raisons pour éviter inutilement l’orientation objet. Définir des classes personnalisées est utile lorsque l’on veut coller ensemble un état et certaines fonctionnalités. Le problème, comme l’a souligné les discussions sur la programmation fonctionnelle, vient de la partie “state” de l’équation.

Dans certaines architectures, typiquement des applications Web, plusieurs instances de processus Python sont lancées pour répondre aux demandes externes qui peuvent se produire en même temps. Dans ce cas, tenir quelques états dans des objets instanciés, ce qui signifie garder des informations statiques sur le monde, est sujet à des problèmes de concurrence ou de race-conditions. Parfois, entre l’initialisation de l’état d’un objet (généralement fait avec la méthode __init__()) et l’utilisation réelle de l’état de l’objet à travers l’une de ses méthodes, le monde peut avoir changé, et l’état retenu peut ne plus être à jour. Par exemple, une requête peut charger un élément en mémoire et le marquer comme lu par un utilisateur. Si une autre requête nécessite la suppression de cet article dans le même temps, il peut arriver que la suppression se produise pour de vrai après que le premier processus ait chargé l’élément, et ensuite nous devons marquer comme lu un objet supprimé.

Ceci et d’autres problèmes a conduit à l’idée que l’utilisation des fonctions sans état est un meilleur paradigme de programmation.

Une autre façon de dire la même chose est de suggérer l’utilisation des fonctions et procédures avec le moins de contextes implicites et d’effets de bord possibles. Le contexte d’une fonction implicite est composée de n’importe quelles variables ou objets globaux dans la couche de persistance qui sont accessibles depuis l’intérieur de la fonction. Les effets de bord sont les changements qu’une fonction fait à son contexte implicite. Si une fonction sauve ou supprime la donnée dans une variable globale ou dans la couche de persistance, elle est dite comme ayant un effet de bord.

Isoler soigneusement les fonctions avec un contexte et des effets de bord depuis des fonctions avec une logique (appelé fonctions pures) permet les avantages suivants:

  • Les fonctions pures sont déterministes: pour une entrée donnée fixe, la sortie sera toujours la même.

  • Les fonctions pures sont beaucoup plus faciles à changer ou remplacer si elles doivent être refactorisées ou optimisées.

  • Les fonctions pures sont plus faciles à tester avec des tests unitaires: il y a moins besoin d’une configuration du contexte complexe et d’un nettoyage des données après.

  • Les fonctions pures sont plus faciles à manipuler, décorer, et déplacer.

En résumé, les fonctions pures sont des blocs de construction plus efficaces que les classes et les objets pour certaines architectures parce qu’elles n’ont pas de contexte ou d’effets de bord.

Évidemment, l’orientation objet est utile et même nécessaire dans de nombreux cas, par exemple lors du développement d’applications graphiques de bureau ou des jeux, où les choses qui sont manipulés (fenêtres, boutons, avatars, véhicules) ont une vie relativement longue par elle-même dans la mémoire de l’ordinateur.

Décorateurs

Le langage Python fournit une syntaxe simple mais puissante appelée ‘décorateurs’. Un décorateur est une fonction ou une classe qui enveloppe (ou décore) une fonction ou une méthode. La fonction ou méthode ‘décorée” remplacera la fonction ou méthode originale ‘non décorée’ . Parce que les fonctions sont des objets de première classe en Python, cela peut être fait ‘manuellement’, mais utiliser la syntaxe @decorator est plus clair et donc préféré.

def foo():
    # do something

def decorator(func):
    # manipulate func
    return func

foo = decorator(foo)  # Manually decorate

@decorator
def bar():
    # Do something
# bar() is decorated

Ce mécanisme est utile pour la “Separation of concerns” et évite à une logique externe non liée de “polluer” la logique de base de la fonction ou de la méthode. Un bon exemple de morceau de fonctionnalité qui est mieux géré avec la décoration est la mémorisation ou la mise en cache: vous voulez stocker les résultats d’une fonction coûteuse dans une table et les utiliser directement au lieu de les recalculer quand ils ont déjà été calculés. Ceci ne fait clairement pas partie de la logique de la fonction.

Gestionnaires de contexte

Un gestionnaire de contexte est un objet Python qui fournit une information contextuelle supplémentaire à une action. Cette information supplémentaire prend la forme de l’exécution d’un callable lors de l’initialisation d’un contexte à l’aide de la déclaration with, ainsi que l’exécution d’un callable après avoir terminé tout le code à l’intérieur du bloc with. L’exemple le plus connu d’utilisation d’un gestionnaire de contexte est montré ici, à l’ouverture d’un fichier:

with open('file.txt') as f:
    contents = f.read()

Quiconque est familier avec ce pattern sait que l’invocation open de cette façon s’assure que la fonction close``de ``f sera appelée à un moment donné. Cela réduit la charge cognitive d’un développeur et rend le code plus facile à lire.

Il existe deux moyens faciles d’implémenter cette fonctionnalité vous-même: en utilisant une classe ou à l’aide d’un générateur. Implémentons la fonctionnalité ci-dessus nous-mêmes, en commençant par l’approche par classe:

class CustomOpen(object):
    def __init__(self, filename):
      self.file = open(filename)

    def __enter__(self):
        return self.file

    def __exit__(self, ctx_type, ctx_value, ctx_traceback):
        self.file.close()

with CustomOpen('file') as f:
    contents = f.read()

C’est juste un objet Python régulier avec deux méthodes supplémentaires qui sont utilisés par la déclaration with. CustomOpen est d’abord instancié puis sa méthode __enter__ est appelée et tout ce que __enter__ retourne est assigné à f dans la partie as _f de la déclaration. Lorsque le contenu du bloc with a fini de s’exécuter, la méthode __exit__ est alors appelée.

Et maintenant, l’approche par générateur en utilisant la bibliothèque contextlib de Python:

from contextlib import contextmanager

@contextmanager
def custom_open(filename):
    f = open(filename)
    try:
        yield f
    finally:
        f.close()

with custom_open('file') as f:
    contents = f.read()

Cela fonctionne exactement de la même manière que l’exemple de classe ci-dessus, mais il est plus laconique. La fonction custom_open s’exécute jusqu’à la déclaration yield. Il rend alors le contrôle à la déclaration with, qui assigne tout ce qui était yield‘é à f`dans la portion `as f. La clause finally s’assure que close() est appelé s’il y avait ou non une exception à l’intérieur du with.

Étant donné que les deux approches semblent similaires, nous devrions suivre le Zen de Python pour décider quand utiliser laquelle. L’approche classe pourrait être mieux s’il y a une quantité considérable de logique à encapsuler. L’approche fonction pourrait être préférable pour des situations où nous avons affaire à une action simple.

Typage dynamique

Python est typé dynamiquement, ce qui signifie que les variables n’ont pas un type fixe. En fait, en Python, les variables sont très différentes de ce qu’elles sont dans de nombreux autres langages, en particulier les langages typés statiquement. Les variables ne sont pas un segment de la mémoire de l’ordinateur où une certaine valeur est écrite, elles sont des ‘tags’ ou des ‘noms’ pointant vers des objets. Il est donc possible pour la variable ‘a’ d’être définie à la valeur 1, puis à la valeur ‘a string’, puis à une fonction.

Le typage dynamique de Python est souvent considéré comme une faiblesse, et en effet, elle peut conduire à la complexité et à du code difficile à débugguer. Quelque chose nommée ‘a’ peut être assigné à de nombreuses choses différentes, et le développeur ou le mainteneur doit suivre ce nom dans le code pour s’assurer qu’il n’a pas été assigné à un objet complètement différent.

Quelques lignes directrices pour éviter ce problème:

  • Évitez d’utiliser le même nom de variable pour des choses différentes.

Mauvais

a = 1
a = 'a string'
def a():
    pass  # Do something

Bon

count = 1
msg = 'a string'
def func():
    pass  # Do something

Utiliser des fonctions ou des méthodes courtes permet de réduire le risque d’utiliser le même nom pour deux choses indépendantes.

Il est préférable d’utiliser des noms différents, même pour des choses qui sont liées, quand elles ont un type différent:

Mauvais

items = 'a b c d'  # This is a string...
items = items.split(' ')  # ...becoming a list
items = set(items)  # ...and then a set

Il n’y a pas de gain d’efficacité lors de la réutilisation de noms: les affectations devront créer de nouveaux objets de toute façon. Toutefois, lorsque la complexité augmente et chaque affectation est séparée par d’autres lignes de code, incluant des branches ‘si’ et des boucles, il devient plus difficile d’établir quel est le type d’une variable donnée.

Certaines pratiques de codage, comme la programmation fonctionnelle, recommandent de ne jamais réaffecter une variable. En Java cela se fait avec le mot-clé final. Python n’a pas de mot-clé final et cela irait à l’encontre de sa philosophie de toute façon. Cependant, cela peut être une bonne discipline pour éviter d’assigner à une variable plus d’une fois, et cela aide à comprendre le concept de types mutables et immutables.

Types mutables et immutables

Python a deux sortes de types intégrés/définis par l’utilisateur.

Les types mutables sont ceux qui permettent la modification sur place du contenu. Des mutables typiques sont les listes et les dictionnaires: toutes les listes ont des méthodes mutables, comme list.append() ou list.pop(), et peuvent être modifiées sur place. La même chose vaut pour les dictionnaires.

Les types immuables fournissent aucune méthode pour modifier leur contenu. Par exemple, la variable x définie à l’entier 6 n’a pas de méthode “increment”. Si vous voulez calculer x + 1, vous devez créer un autre entier et lui donner un nom.

my_list = [1, 2, 3]
my_list[0] = 4
print my_list  # [4, 2, 3] <- The same list as changed

x = 6
x = x + 1  # The new x is another object

Une conséquence de cette différence de comportement est que les types mutables ne sont pas “stables”, et ne peuvent donc être utilisées comme clés du dictionnaire.

L’utilisation correcte des types mutables pour des choses qui sont mutables par nature et des types immutables pour des choses qui sont fixes par nature aide à clarifier l’intention du code.

Par exemple, l’équivalent immutable d’une liste est le tuple, créé avec (1, 2). Ce tuple est une paire qui ne peut pas être changé sur place, et qui peut être utilisée comme clé pour un dictionnaire.

Une particularité de Python qui peut surprendre les débutants est que les chaînes sont immutables. Cela signifie que lors de la construction d’une chaîne à partir de ses parties, il est beaucoup plus efficace d’accumuler les parties dans une liste, qui est mutable, puis coller (‘join’) les morceaux ensemble lorsque la chaîne complète est nécessaire. Une chose à remarquer, cependant, est que les compréhensions de liste sont mieux et plus rapides que la construction d’une liste dans une boucle avec des appels à append().

Mauvais

# create a concatenated string from 0 to 19 (e.g. "012..1819")
nums = ""
for n in range(20):
  nums += str(n)   # slow and inefficient
print nums

Bon

# create a concatenated string from 0 to 19 (e.g. "012..1819")
nums = []
for n in range(20):
  nums.append(str(n))
print "".join(nums)  # much more efficient

Le mieux

# create a concatenated string from 0 to 19 (e.g. "012..1819")
nums = [str(n) for n in range(20)]
print "".join(nums)

Une dernière chose à mentionner sur les chaînes est que l’utilisation join() n’est est pas toujours ce qu’il y a de mieux. Dans les cas où vous créez une nouvelle chaîne depuis un nombre prédéterminé de chaînes, utiliser l’opérateur d’addition est vraiment plus rapide, mais dans des cas comme ci-dessus ou dans des cas où vous ajoutez à une chaîne existante, utiliser join() devrait être votre méthode de préférence.

foo = 'foo'
bar = 'bar'

foobar = foo + bar  # This is good
foo += 'ooo'  # This is bad, instead you should do:
foo = ''.join([foo, 'ooo'])

Note

Vous pouvez également utiliser l’opérateur de formatage % pour concaténer un nombre prédéterminé de chaînes en plus de str.join() et +. Cependant, la PEP 3101, décourage l’utilisation de l’opérateur % en faveur de la méthode str.format().

foo = 'foo'
bar = 'bar'

foobar = '%s%s' % (foo, bar) # It is OK
foobar = '{0}{1}'.format(foo, bar) # It is better
foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # It is best

Inclure les dépendances dans l’arbre de source de votre dépôt de code

Runners