Comment gérer correctement les fonctionnalités optionnelles en python
Je travaille sur des packages python qui implémentent des modèles scientifiques et je me demande quelle est la meilleure façon de gérer les fonctionnalités optionnelles. Voici le comportement que j'aimerais: si certaines dépendances optionnelles ne peuvent pas être importées (module de traçage sur une machine sans tête par exemple), j'aimerais désactiver les fonctions utilisant ces modules dans mes classes, avertir l'utilisateur s'il essaie de utilisez-les et tout cela sans casser l'exécution. donc le script suivant fonctionnerait dans tous les cas:
mymodel.dostuff()
mymodel.plot() <= only plots if possible, else display log an error
mymodel.domorestuff() <= get executed regardless of the result of the previous statement
Jusqu'à présent, les options que je vois sont les suivantes:
- vérifiez dans le
__init __.py
pour les modules disponibles et gardez une liste d'entre eux (mais comment l'utiliser correctement dans le reste du paquet?) - pour chaque fonction reposant sur des dépendances optionnelles ont une
try import ... except ...
instruction - mettre les fonctions dépendant d'un module particulier dans un fichier séparé
Ces options devraient fonctionner, mais elles semblent toutes plutôt piratées et difficiles à maintenir. et si nous voulons supprimer complètement une dépendance? ou le rendre obligatoire?
La solution la plus simple, bien sûr, est d'importer simplement les dépendances optionnelles dans le corps de la fonction qui les nécessite. Mais le toujours-droit PEP 8
dit:
Les importations sont toujours placées en haut du fichier, juste après les commentaires et les docstrings du module, et avant les globales et les constantes du module.
Ne voulant pas aller à l'encontre des meilleurs voeux des maîtres python, j'adopte l'approche suivante, qui présente plusieurs avantages ...
Tout d'abord, importez avec un try-except
Disons que l'une de mes fonctions a foo
besoin numpy
et je veux en faire une dépendance facultative. En haut du module, je mets:
try:
import numpy as _numpy
except ImportError:
_has_numpy = False
else:
_has_numpy = True
Ici (dans le bloc sauf) serait l'endroit pour imprimer un avertissement, de préférence en utilisant le warnings
module.
Puis lancez l'exception dans la fonction
Que faire si l'utilisateur appelle foo
et n'a pas numpy? J'y lance l'exception et je documente ce comportement.
def foo(x):
"""Requires numpy."""
if not _has_numpy:
raise ImportError("numpy is required to do this.")
...
Vous pouvez également utiliser un décorateur et l'appliquer à toute fonction nécessitant cette dépendance:
@requires_numpy
def foo(x):
...
Cela a l'avantage d'éviter la duplication de code.
Et ajoutez-le en tant que dépendance facultative à votre script d'installation
Si vous distribuez du code, recherchez comment ajouter la dépendance supplémentaire à la configuration d'installation. Par exemple, avec setuptools
, je peux écrire:
install_requires = ["networkx"],
extras_require = {
"numpy": ["numpy"],
"sklearn": ["scikit-learn"]}
Cela spécifie que networkx
c'est absolument nécessaire au moment de l'installation, mais que les fonctionnalités supplémentaires de mon module nécessitent numpy
et sklearn
, qui sont facultatifs.
En utilisant cette approche, voici les réponses à vos questions spécifiques:
- Et si nous voulons rendre une dépendance obligatoire?
Nous pouvons simplement ajouter notre dépendance facultative à la liste des dépendances requises de notre outil de configuration. Dans l'exemple ci-dessus, nous passons numpy
à install_requires
. Tout le code vérifiant l'existence de numpy
peut alors être supprimé, mais le laisser en place ne provoquera pas la rupture de votre programme.
- Et si nous voulons supprimer complètement une dépendance?
Supprimez simplement la vérification de la dépendance dans toute fonction qui en avait précédemment besoin. Si vous avez implémenté la vérification des dépendances avec un décorateur, vous pouvez simplement le changer pour qu'il passe simplement la fonction d'origine sans changement.
Cette approche a l'avantage de placer toutes les importations en haut du module afin que je puisse voir d'un coup d'œil ce qui est requis et ce qui est facultatif.