Est-ce pythonique d'importer des fonctions internes?

126

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.

À l'occasion, je viole PEP 8. Parfois, j'importe des trucs à l'intérieur de fonctions. En règle générale, je fais cela s'il y a une importation qui n'est utilisée que dans une seule fonction.

Des opinions?

EDIT (la raison pour laquelle j'ai l'impression d'importer des fonctions peut être une bonne idée):

Raison principale: cela peut rendre le code plus clair.

  • En regardant le code d'une fonction, je pourrais me demander: "Qu'est-ce qu'une fonction / classe xxx?" (xxx étant utilisé dans la fonction). Si j'ai toutes mes importations en haut du module, je dois y aller pour déterminer ce qu'est xxx. C'est plus un problème lors de l'utilisation from m import xxx. Voir m.xxxdans la fonction m'en dit probablement plus. Selon ce qui mest: S'agit-il d'un module / package de premier niveau bien connu ( import m)? Ou est-ce un sous-module / package ( from a.b.c import m)?
  • Dans certains cas, avoir ces informations supplémentaires ("Qu'est-ce que xxx?") À proximité de l'endroit où xxx est utilisé peut rendre la fonction plus facile à comprendre.
codeape
la source
2
et tu fais ça pour la performance?
Macarse
4
Je pense que cela rend le code plus clair dans certains cas. Je suppose que les performances brutes diminuent lors de l'importation dans une fonction (puisque l'instruction d'importation s'exécutera à chaque fois que la fonction est appelée).
codeape
Vous pouvez répondre "Qu'est-ce que la fonction / classe xxx?" en utilisant la syntaxe import xyz plutôt que la syntaxe from xyz import abc
Tom Leys
1
Si la clarté est le seul facteur, U pourrait tout aussi bien inclure un commentaire pertinent à cet effet. ;)
Lakshman Prasad
5
@becomingGuru: Bien sûr, mais les commentaires peuvent ne pas être synchronisés avec la réalité ...
codeape

Réponses:

88

À long terme, je pense que vous apprécierez d'avoir la plupart de vos importations en haut du fichier, de cette façon, vous pouvez voir d'un coup d'œil à quel point votre module est compliqué par ce qu'il doit importer.

Si j'ajoute un nouveau code à un fichier existant, je fais généralement l'importation là où c'est nécessaire, puis si le code reste, je rendrai les choses plus permanentes en déplaçant la ligne d'importation en haut du fichier.

Un autre point, je préfère obtenir une ImportErrorexception avant l'exécution de tout code - comme vérification de l'intégrité, c'est donc une autre raison d'importer en haut.

J'utilise pyCheckerpour vérifier les modules inutilisés.

Peter Ericson
la source
47

Il y a deux occasions où je viole la PEP 8 à cet égard:

  • Importations circulaires: le module A importe le module B, mais quelque chose dans le module B nécessite le module A (bien que ce soit souvent un signe que je dois refactoriser les modules pour éliminer la dépendance circulaire)
  • Insérer un point d'arrêt pdb: import pdb; pdb.set_trace()C'est pratique b / c je ne veux pas mettre import pdben haut de chaque module que je pourrais vouloir déboguer, et il est facile de se souvenir de supprimer l'importation lorsque je supprime le point d'arrêt.

En dehors de ces deux cas, c'est une bonne idée de tout mettre en haut. Cela rend les dépendances plus claires.

Rick Copeland
la source
7
Je suis d'accord que cela rend les dépendances plus claires en ce qui concerne le module dans son ensemble. Mais je pense que cela peut rendre le code moins clair au niveau de la fonction pour tout importer en haut. Lorsque vous regardez le code d'une fonction, vous pouvez vous demander: "Qu'est-ce que la fonction / classe xxx?" (xxx est utilisé dans la fonction). Et vous devez regarder tout en haut du fichier pour voir d'où vient xxx. C'est plus un problème lors de l'utilisation de m import xxx. Voir m.xxx vous en dit plus - du moins s'il n'y a aucun doute sur ce qu'est m.
codeape
20

Voici les quatre cas d'utilisation d'importation que nous utilisons

  1. import(et from x import yet import x as y) en haut

  2. Choix pour l'importation. Au sommet.

    import settings
    if setting.something:
        import this as foo
    else:
        import that as foo
    
  3. Importation conditionnelle. Utilisé avec les bibliothèques JSON, XML et autres. Au sommet.

    try:
        import this as foo
    except ImportError:
        import that as foo
    
  4. Importation dynamique. Jusqu'à présent, nous n'avons qu'un seul exemple de cela.

    import settings
    module_stuff = {}
    module= __import__( settings.some_module, module_stuff )
    x = module_stuff['x']
    

    Notez que cette importation dynamique n'apporte pas de code, mais apporte des structures de données complexes écrites en Python. C'est un peu comme une donnée décapée, sauf que nous l'avons décapée à la main.

    C'est aussi, plus ou moins, en haut d'un module


Voici ce que nous faisons pour rendre le code plus clair:

  • Gardez les modules courts.

  • Si j'ai toutes mes importations en haut du module, je dois aller y chercher pour déterminer ce qu'est un nom. Si le module est court, c'est facile à faire.

  • Dans certains cas, avoir ces informations supplémentaires à proximité de l'endroit où un nom est utilisé peut rendre la fonction plus facile à comprendre. Si le module est court, c'est facile à faire.

S.Lott
la source
Garder les modules courts est bien sûr une très bonne idée. Mais pour avoir l'avantage d'avoir toujours des "informations d'importation" pour les fonctions disponibles, la longueur maximale du module devrait être d'un écran (probablement 100 lignes maximum). Et ce serait probablement trop court pour être pratique dans la plupart des cas.
codeape
Je suppose que vous pourriez amener cela à un extrême logique. Je pense qu'il pourrait y avoir un point d'équilibre où votre module est "suffisamment petit" pour que vous n'ayez pas besoin de techniques d'importation sophistiquées pour gérer la complexité. La taille moyenne de nos modules est - par coïncidence - d'environ 100 lignes.
S.Lott
8

Une chose à garder à l'esprit: les importations inutiles peuvent entraîner des problèmes de performances. Donc, si c'est une fonction qui sera appelée fréquemment, vous feriez mieux de simplement mettre l'importation en haut. Bien sûr, il s'agit d' une optimisation, donc s'il y a un cas valable à faire pour que l'importation à l'intérieur d'une fonction soit plus claire que l'importation en haut d'un fichier, cela l'emporte sur les performances dans la plupart des cas.

Si vous utilisez IronPython, on me dit qu'il est préférable d'importer des fonctions internes (car la compilation de code dans IronPython peut être lente). Ainsi, vous pourrez peut-être obtenir un moyen d'importer des fonctions internes. Mais à part cela, je dirais que cela ne vaut tout simplement pas la peine de lutter contre les conventions.

En règle générale, je fais cela s'il y a une importation qui n'est utilisée que dans une seule fonction.

Un autre point que je voudrais faire est que cela peut être un problème de maintenance potentiel. Que se passe-t-il si vous ajoutez une fonction qui utilise un module qui était auparavant utilisé par une seule fonction? Allez-vous vous rappeler d'ajouter l'importation en haut du fichier? Ou allez-vous analyser chaque fonction pour les importations?

FWIW, il y a des cas où il est logique d'importer à l'intérieur d'une fonction. Par exemple, si vous souhaitez définir la langue dans cx_Oracle, vous devez définir une _variable d'environnement NLS LANG avant son importation. Ainsi, vous pouvez voir un code comme celui-ci:

import os

oracle = None

def InitializeOracle(lang):
    global oracle
    os.environ['NLS_LANG'] = lang
    import cx_Oracle
    oracle = cx_Oracle
Jason Baker
la source
2
Je suis d'accord avec votre problème de maintenance. Le refactoring du code peut être un peu problématique. Si j'ajoute une deuxième fonction qui utilise un module précédemment utilisé par une seule fonction - soit je déplace l'importation vers le haut, soit je brise ma propre règle générale en important le module dans la deuxième fonction également.
codeape
2
Je pense que l'argument de la performance peut également aller dans l'autre sens. L'importation d'un module peut prendre du temps. Sur les systèmes de fichiers distribués comme ceux des supercalculateurs, l'importation d'un gros module comme numpy peut prendre plusieurs secondes. Si un module n'est nécessaire que pour une seule fonction rarement utilisée, l'importation dans la fonction accélérera considérablement le cas courant.
amaurea
6

J'ai déjà enfreint cette règle pour les modules qui s'auto-testent. Autrement dit, ils ne sont normalement utilisés que pour le support, mais je définis un principal pour eux afin que si vous les exécutez seuls, vous puissiez tester leurs fonctionnalités. Dans ce cas, j'importe parfois getoptet cmdjuste en général, parce que je veux qu'il soit clair pour quelqu'un qui lit le code que ces modules n'ont rien à voir avec le fonctionnement normal du module et ne sont inclus que pour les tests.

Dan Lew
la source
5

Venant de la question sur le chargement du module deux fois - Pourquoi pas les deux?

Une importation en haut du script indiquera les dépendances et une autre importation dans la fonction rendra cette fonction plus atomique, tout en ne causant apparemment aucun inconvénient en termes de performances, car une importation consécutive est bon marché.

IljaBek
la source
3

Tant que ce n'est importpas le cas from x import *, vous devriez les mettre en haut. Il ajoute juste un nom à l'espace de noms global, et vous vous en tenez à PEP 8. De plus, si vous en avez besoin plus tard ailleurs, vous n'avez rien à déplacer.

Ce n'est pas grave, mais comme il n'y a presque aucune différence, je suggérerais de faire ce que PEP 8 dit.

Javier
la source
3
En fait, mettre from x import *une fonction à l'intérieur générera un SyntaxWarning, au moins en 2.5.
Rick Copeland
3

Jetez un œil à l'approche alternative utilisée dans sqlalchemy: injection de dépendances:

@util.dependencies("sqlalchemy.orm.query")
def merge_result(query, *args):
    #...
    query.Query(...)

Remarquez comment la bibliothèque importée est déclarée dans un décorateur, et passée en argument à la fonction !

Cette approche rend le code plus propre et fonctionne également 4,5 fois plus rapidement qu'une importinstruction!

Benchmark: https://gist.github.com/kolypto/589e84fbcfb6312532658df2fabdb796

kolypto
la source
2

Dans les modules qui sont à la fois des modules «normaux» et qui peuvent être exécutés (c'est-à-dire qui ont une section if __name__ == '__main__':), j'importe généralement des modules qui ne sont utilisés que lors de l'exécution du module dans la section principale.

Exemple:

def really_useful_function(data):
    ...


def main():
    from pathlib import Path
    from argparse import ArgumentParser
    from dataloader import load_data_from_directory

    parser = ArgumentParser()
    parser.add_argument('directory')
    args = parser.parse_args()
    data = load_data_from_directory(Path(args.directory))
    print(really_useful_function(data)


if __name__ == '__main__':
    main()
codeape
la source
1

Il y a un autre cas (probablement "dans le coin") où il peut être bénéfique à l' importintérieur de fonctions rarement utilisées: raccourcir le temps de démarrage.

J'ai frappé ce mur une fois avec un programme plutôt complexe s'exécutant sur un petit serveur IoT acceptant les commandes d'une ligne série et effectuant des opérations, peut-être des opérations très complexes.

Placer des importinstructions en haut des fichiers destinés à traiter toutes les importations avant le démarrage du serveur; depuis la importliste inclus jinja2, lxml, signxmlet d' autres « poids lourds » de (et SoC n'a pas été très puissant) , cela signifiait minutes avant la première instruction a été effectivement exécuté.

OTOH plaçant la plupart des importations dans des fonctions, j'ai pu avoir le serveur "vivant" sur la ligne série en quelques secondes. Bien sûr, lorsque les modules étaient réellement nécessaires, je devais en payer le prix (Remarque: cela peut également être atténué en créant une tâche d'arrière-plan importen temps d'inactivité).

ZioByte
la source