Comment gérer correctement les fonctionnalités optionnelles en python


Jérémie

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 __.pypour 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?

jme

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 8dit:

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 foobesoin numpyet 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 warningsmodule.

Puis lancez l'exception dans la fonction

Que faire si l'utilisateur appelle fooet 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 networkxc'est absolument nécessaire au moment de l'installation, mais que les fonctionnalités supplémentaires de mon module nécessitent numpyet 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 numpypeut 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.

Articles connexes


Comment gérer les "interfaces optionnelles"?

Steve A «Interface facultative» n'est probablement pas un terme standard, alors laissez-moi vous donner un exemple. Supposons que j'ai: interface Car { start(); honk(); } Maintenant , je peux avoir comme HondaCar, PriusCar, etc., mises en œuvre. Yay! Mais

Comment tester les fonctionnalités optionnelles de Rust?

Dfhoughton J'ai un package auquel je souhaite ajouter une fonctionnalité facultative. J'ai ajouté une section appropriée à mon Cargo.toml: [features] foo = [] J'ai écrit un test expérimental pour les fonctionnalités de base de la cfg!macro: #[test] fn testing

Comment gérer correctement les erreurs en javascript?

Nicolas Stadler J'ai trois fonctions, chacune appelant une autre fonction. function one() { two(); } function two() { three(); } function three() { // here an error will occur, and then should be resolved try { // whatever } catch (e) {

Comment gérer correctement les fichiers en langage assembleur

Le dernier fromage de Roshan J'ai essayé quelques codes github sur Internet mais lorsque j'ai écrit le même code dans EXE templete, cela ne fonctionne pas. data segment selection db ? ;FILE I/O DATAS dir1 db "c:\test1", 0 dir2 db "test2", 0 dir3 db "newname"

Comment gérer correctement les mises à jour en utilisant signalr?

Kardon63 J'ai une application cliente Angularet un signalRhub, et aussi un service qui prend un horodatage comme paramètre. Je veux invoquer une méthode dans le hub lorsque j'appuie sur un bouton de démarrage dans le client, et lorsque la méthode est invoquée,