web-dev-qa-db-fra.com

Importations circulaires (ou cycliques) dans Python

Que se passera-t-il si deux modules s'importent l'un l'autre?

Pour généraliser le problème, qu'en est-il des importations cycliques en Python?

302
Xolve

Il y a eu une très bonne discussion à ce sujet à comp.lang.python l'année dernière. Cela répond assez bien à votre question.

Les importations sont vraiment simples. Rappelez-vous juste ce qui suit:

'import' et 'from xxx import yyy' sont des instructions exécutables. Ils s'exécutent lorsque le programme en cours atteint cette ligne.

Si un module ne se trouve pas dans sys.modules, une importation crée la nouvelle entrée de module dans sys.modules puis exécute le code dans le module. Il ne rend pas le contrôle au module appelant tant que l'exécution n'est pas terminée.

Si un module existe dans sys.modules, une importation retourne simplement ce module, que son exécution soit terminée ou non. C'est la raison pour laquelle les importations cycliques peuvent renvoyer des modules qui semblent partiellement vides.

Enfin, le script en cours d'exécution s'exécute dans un module nommé __main__. L'importation du script sous son propre nom créera un nouveau module sans lien avec __main__.

Prenez cela ensemble et vous ne devriez pas avoir de surprises lors de l'importation de modules.

251
Shane C. Mason

Si vous faites import foo à l'intérieur bar et import bar à l'intérieur foo, cela fonctionnera correctement. Au moment où quelque chose fonctionne réellement, les deux modules seront complètement chargés et auront des références les uns aux autres.

Le problème est qu’à la place vous faites from foo import abc et from bar import xyz. Parce que maintenant chaque module nécessite que l'autre module soit déjà importé (pour que le nom que nous importons existe) avant de pouvoir être importé.

258
pythoneer

Les importations cycliques se terminent, mais vous devez veiller à ne pas utiliser les modules importés de manière cyclique lors de l'initialisation du module.

Considérez les fichiers suivants:

a.py:

print "a in"
import sys
print "b imported: %s" % ("b" in sys.modules, )
import b
print "a out"

b.py:

print "b in"
import a
print "b out"
x = 3

Si vous exécutez a.py, vous obtiendrez ce qui suit:

$ python a.py
a in
b imported: False
b in
a in
b imported: True
a out
b out
a out

Lors de la deuxième importation de b.py (dans la deuxième a in), l'interpréteur Python n'importe pas à nouveau b, car il existe déjà dans le module dict.

Si vous essayez d'accéder à b.x à partir de a pendant l'initialisation du module, vous obtiendrez un AttributeError.

Ajoutez la ligne suivante à a.py:

print b.x

Ensuite, le résultat est:

$ python a.py
a in                    
b imported: False
b in
a in
b imported: True
a out
Traceback (most recent call last):
  File "a.py", line 4, in <module>
    import b
  File "/home/shlomme/tmp/x/b.py", line 2, in <module>
    import a
 File "/home/shlomme/tmp/x/a.py", line 7, in <module>
    print b.x
AttributeError: 'module' object has no attribute 'x'

Cela est dû au fait que les modules sont exécutés lors de l'importation et qu'au moment de l'accès à b.x, la ligne x = 3 n'a pas encore été exécutée, ce qui ne se produira qu'après b out.

95
Torsten Marek

Comme d'autres réponses le décrivent, ce modèle est acceptable en python:

def dostuff(self):
     from foo import bar
     ...

Ce qui évitera l'exécution de l'instruction d'importation lorsque le fichier est importé par d'autres modules. Ce n'est que s'il existe une dépendance circulaire logique que cela échouera.

La plupart des importations circulaires ne sont pas réellement des importations circulaires logiques, mais plutôt des erreurs ImportError, en raison de la façon dont import() évalue les instructions de niveau supérieur du fichier entier lors de l'appel.

Ces ImportErrors peuvent presque toujours être évités si vous voulez que vos importations soient au top :

Considérons cette importation circulaire:

App A

# profiles/serializers.py

from images.serializers import SimplifiedImageSerializer

class SimplifiedProfileSerializer(serializers.Serializer):
    name = serializers.CharField()

class ProfileSerializer(SimplifiedProfileSerializer):
    recent_images = SimplifiedImageSerializer(many=True)

App B

# images/serializers.py

from profiles.serializers import SimplifiedProfileSerializer

class SimplifiedImageSerializer(serializers.Serializer):
    title = serializers.CharField()

class ImageSerializer(SimplifiedImageSerializer):
    profile = SimplifiedProfileSerializer()

De l'excellent discours de David Beazleys Modules et packages: vivre et laisser mourir - PyCon 2015 , 1:54:00, voici un moyen de traiter les importations circulaires en python:

try:
    from images.serializers import SimplifiedImageSerializer
except ImportError:
    import sys
    SimplifiedImageSerializer = sys.modules[__package__ + '.SimplifiedImageSerializer']

Cela essaie d'importer SimplifiedImageSerializer et si ImportError est levé, car il est déjà importé, il sera extrait de la tâche d'importation.

PS: Vous devez lire l'intégralité de ce message à la voix de David Beazley.

25
Sebastian Wozny

J'ai un exemple ici qui m'a frappé!

foo.py

import bar

class gX(object):
    g = 10

bar.py

from foo import gX

o = gX()

main.py

import foo
import bar

print "all done"

Sur la ligne de commande: $ python main.py

Traceback (most recent call last):
  File "m.py", line 1, in <module>
    import foo
  File "/home/xolve/foo.py", line 1, in <module>
    import bar
  File "/home/xolve/bar.py", line 1, in <module>
    from foo import gX
ImportError: cannot import name gX
8
Xolve

Je suis complètement d'accord avec la réponse de Pythoneer ici. Mais je suis tombé sur un code qui était défectueux avec les importations circulaires et qui posait problème en essayant d'ajouter des tests unitaires. Donc, pour corriger rapidement le problème sans tout changer, vous pouvez résoudre le problème en effectuant une importation dynamique.

# Hack to import something without circular import issue
def load_module(name):
    """Load module using imp.find_module"""
    names = name.split(".")
    path = None
    for name in names:
        f, path, info = imp.find_module(name, path)
        path = [path]
    return imp.load_module(name, f, path[0], info)
constants = load_module("app.constants")

Encore une fois, ce n'est pas un correctif permanent, mais peut aider quelqu'un qui veut corriger une erreur d'importation sans trop modifier le code.

À votre santé!

4
radtek

Module a.py:

import b
print("This is from module a")

Module b.py

import a
print("This is from module b")

Exécuter "Module a" produira:

>>> 
'This is from module a'
'This is from module b'
'This is from module a'
>>> 

Il a sorti ces 3 lignes alors qu'il était supposé sortir infinitival à cause de l'importation circulaire. Ce qui se passe ligne par ligne lorsque vous exécutez "Module a" est répertorié ici:

  1. La première ligne est import b. donc il visitera le module b
  2. La première ligne du module b est import a. donc il visitera un module
  3. La première ligne du module a est import b mais remarque que cette ligne ne sera plus exécutée , Parce que chaque fichier dans python exécute une ligne d'importation pour une fois, peu importe où et quand il est exécuté. il passera donc à la ligne suivante et imprimera "This is from module a".
  4. Après avoir visité tout le module a du module b, nous en sommes toujours au module b. donc la ligne suivante sera imprimée "This is from module b"
  5. Les lignes du module b sont exécutées complètement. nous allons donc revenir au module a où nous avons commencé le module b.
  6. les lignes d'importation b ont déjà été exécutées et ne seront pas exécutées à nouveau. la ligne suivante imprimera "This is from module a" et le programme sera terminé.
4
Mohsen Haddadi

J'ai résolu le problème de la manière suivante, et cela fonctionne bien sans erreur. Considérons deux fichiers a.py et b.py.

J'ai ajouté ceci à a.py et cela a fonctionné.

if __== "__main__":
        main ()

a.py:

import b
y = 2
def main():
    print ("a out")
    print (b.x)

if __== "__main__":
    main ()

b.py:

import a
print ("b out")
x = 3 + a.y

La sortie que je reçois est

>>> b out 
>>> a out 
>>> 5
1
Irfanuddin Shafi

Les importations circulaires peuvent prêter à confusion car l'importation a deux effets:

  1. il exécute le code du module importé
  2. ajoute un module importé à la table des symboles globaux du module importé

Le premier est effectué une seule fois, tandis que le dernier est indiqué à chaque déclaration d'importation. L'importation circulaire crée une situation lorsque le module d'importation utilise un module importé avec un code partiellement exécuté. En conséquence, il ne verra pas les objets créés après l’instruction d’importation. L'exemple de code ci-dessous le démontre.

Les importations circulaires ne sont pas le mal ultime à éviter à tout prix. Dans certains frameworks tels que Flask, ils sont assez naturels et modifier votre code pour les éliminer ne le rend pas meilleur.

main.py

print 'import b'
import b
print 'a in globals() {}'.format('a' in globals())
print 'import a'
import a
print 'a in globals() {}'.format('a' in globals())
if __== '__main__':
    print 'imports done'
    print 'b has y {}, a is b.a {}'.format(hasattr(b, 'y'), a is b.a)

b.by

print "b in, __= {}".format(__name__)
x = 3
print 'b imports a'
import a
y = 5
print "b out"

a.py

print 'a in, __= {}'.format(__name__)
print 'a imports b'
import b
print 'b has x {}'.format(hasattr(b, 'x'))
print 'b has y {}'.format(hasattr(b, 'y'))
print "a out"

sortie python main.py avec commentaires

import b
b in, __= b    # b code execution started
b imports a
a in, __= a    # a code execution started
a imports b           # b code execution is already in progress
b has x True
b has y False         # b defines y after a import,
a out
b out
a in globals() False  # import only adds a to main global symbol table 
import a
a in globals() True
imports done
b has y True, a is b.a True # all b objects are available
1
Jacek Błocki