Importations circulaires (ou cycliques) en Python


353

Que se passera-t-il si deux modules s’importent?

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



1
également à titre de référence, il semble que les importations circulaires soient autorisées sur python 3.5 (et probablement au-delà) mais pas 3.4 (et probablement ci-dessous).
Charlie Parker

4
J'utilise python 3.7.2 et j'ai toujours une erreur d'exécution en raison de dépendances circulaires.
Richard Whitehead

Réponses:


282

L' an dernier, il y a eu une très bonne discussion à ce sujet sur comp.lang.python . Cela répond assez bien à votre question.

Les importations sont vraiment très simples. N'oubliez pas ce qui suit:

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

Si un module n'est 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 renvoie 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 renvoie simplement ce module, qu'il soit terminé ou non. C'est la raison pour laquelle les importations cycliques peuvent renvoyer des modules qui semblent en partie vides.

Enfin, le script 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 rapport avec __main__.

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


13
@meawoppl Pourriez-vous développer ce commentaire, s'il vous plaît? En quoi ont-ils changé spécifiquement?
Dan Schien

3
Pour l'instant, la seule référence aux importations circulaires en python3 "Quoi de neuf?" pages est dans le 3.5 . Il indique que "les importations circulaires impliquant des importations relatives sont désormais soutenues". @meawoppl avez-vous trouvé autre chose qui ne figure pas dans ces pages?
zezollo

4
Ils sont def. non pris en charge dans 3.0-3.4. Ou du moins la sémantique du succès est différente. Voici un synopsis que j'ai trouvé qui ne mentionne pas les changements de 3.5. gist.github.com/datagrok/40bf84d5870c41a77dc6
meawoppl

Veuillez développer ce point "Enfin, le script 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 rapport avec main .". Disons donc que le fichier est a.py et lorsqu'il s'exécute comme point d'entrée principal, il est maintenant principal s'il a du code comme lors de l'importation d'une variable. Le même fichier 'a.py' sera-t-il alors chargé dans la table des modules sys? Donc, cela signifie-t-il que s'il a dit l'instruction print, il s'exécutera deux fois? Une fois pour le fichier principal et à nouveau lorsque l'importation est rencontrée?
variable

Cette réponse a 10 ans et j'aimerais une mise à jour modernisée pour m'assurer qu'elle reste correcte dans différentes versions de Python, 2.x ou 3.x
Fallenreaper

296

Si vous faites à l' import foointérieur baret à l' import barintérieur foo, cela fonctionnera bien. Au moment où quelque chose s'exécute réellement, les deux modules seront entièrement chargés et auront des références l'un à l'autre.

Le problème est quand vous le faites from foo import abcet 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é.


27
Il semble que from foo import *et from bar import *sera également très bien.
Akavall

1
Vérifiez la modification du message ci-dessus à l'aide de a.py/b.py. Il n'utilise pas from x import y, mais obtient toujours l'erreur d'importation circulaire
Greg Ennis

2
Ce n'est pas tout à fait vrai. Tout comme import * de, si vous essayez d'accéder à un élément dans l'importation circulaire, au niveau supérieur, donc avant que le script ne termine son exécution, vous aurez le même problème. Par exemple, si vous définissez un package global dans un package d'un autre, et qu'ils s'incluent tous les deux. Je faisais cela pour créer une fabrique bâclée pour un objet dans la classe de base où cet objet pourrait être l'une d'un certain nombre de sous-classes et le code utilisant n'avait pas besoin d'être au courant de ce qu'il créait réellement.
AaronM

3
@Akavall Pas vraiment. Cela n'importera que les noms disponibles lors de l' importexécution de l' instruction. Il n'y aura donc pas d'erreur, mais vous n'obtiendrez peut-être pas toutes les variables que vous attendez.
août

3
Notez que si vous faites from foo import *et from bar import *, tout exécuté dans le fooest dans la phase d'initialisation de bar, et les fonctions réelles dans barn'ont pas encore été définies ...
Martian2049

100

Les importations cycliques se terminent, mais vous devez faire attention à ne pas utiliser les modules importés cycliquement 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 les éléments suivants:

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

Lors de la seconde importation de b.py (dans la seconde a in), l'interpréteur Python ne réimportera bpas, car il existe déjà dans le module dict.

Si vous essayez d'accéder à b.xpartir de alors de l'initialisation du module, vous obtiendrez un AttributeError.

Ajoutez la ligne suivante à a.py:

print b.x

Ensuite, la sortie 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'

En effet, les modules sont exécutés lors de l'importation et au moment de l' b.xaccès, la ligne x = 3n'a pas encore été exécutée, ce qui ne se produira qu'après b out.


14
cela explique grandement le problème, mais qu'en est-il de la solution? comment importer et imprimer correctement x? l'autre solution ci-dessus n'a pas fonctionné pour moi
mehmet

Je pense que cette réponse bénéficierait beaucoup si vous l'utilisiez à la __name__place de 'a'. Au début, je ne savais vraiment pas pourquoi un fichier serait exécuté deux fois.
Bergi

30

Comme d'autres réponses 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 import lorsque le fichier est importé par d'autres modules. Seulement s'il existe une dépendance circulaire logique, cela échouera.

La plupart des importations circulaires ne sont pas en fait des importations circulaires logiques, mais génèrent plutôt des ImportErrorerreurs, en raison de la façon dont import()les instructions de niveau supérieur du fichier entier sont appelées.

Ceux-ci ImportErrorspeuvent presque toujours être évités si vous voulez que vos importations soient au top :

Considérez 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 David Beazleys excellent talk Modules et packages: Live and Let Die! - PyCon 2015 , 1:54:00voici une façon 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 SimplifiedImageSerializeret s'il ImportErrorest levé, car il est déjà importé, il le tirera du cache d'importation.

PS: Vous devez lire l'intégralité de cet article dans la voix de David Beazley.


9
ImportError n'est pas déclenchée si le module a déjà été importé. Les modules peuvent être importés autant de fois que vous le souhaitez, c'est-à-dire "importer un; importer un;" est ok.
Yuras

9

J'ai ici un exemple 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

2
Comment avez-vous résolu ce problème? J'essaie de comprendre l'importation circulaire pour résoudre un problème qui ressemble beaucoup à ce que vous faites ...
c089

12
Euh ... Je pense que j'ai résolu mon problème avec ce hack incroyablement laid. {{{sinon 'foo.bar' dans sys.modules: à partir de la barre d'importation foo else: bar = sys.modules ['foo.bar']}}} Personnellement, je pense que les importations circulaires sont un énorme panneau d'avertissement sur un mauvais code design ...
c089

5
@ C089, ou vous pouvez simplement passer import barà foo.pyla fin
warvariuc

5
Si baret les foodeux doivent utiliser gX, la solution la plus «propre» consiste à mettre gXdans un autre module et à avoir les deux fooet à barimporter ce module. (le plus propre dans le sens où il n'y a pas de dépendances sémantiques cachées.)
Tim Wilder

2
Tim a un bon point. Fondamentalement, c'est parce que je barne trouve même pas gXdans le foo. l'importation circulaire est bonne en soi, mais c'est juste que ce gXn'est pas défini quand il est importé.
Martian2049

9

Module a.py:

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

Module b.py

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

L'exécution du "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 lors de l'exécution du "Module a" est répertorié ici:

  1. La première ligne est import b. il visitera donc le module b
  2. La première ligne du module b est import a. il visitera donc le module a
  3. La première ligne du module a est import bmais notez que cette ligne ne sera plus exécutée , car chaque fichier en python exécute une ligne d'importation juste pour une fois, peu importe où ou quand elle est exécutée. il passera donc à la ligne suivante et s'imprimera "This is from module a".
  4. Après avoir fini de visiter le module entier a du module b, nous sommes toujours au module b. de sorte que la ligne suivante s'imprime"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. la ligne d'importation b a déjà été exécutée et ne sera plus exécutée. la ligne suivante s'imprimera "This is from module a"et le programme sera terminé.

4

Je suis entièrement d'accord avec la réponse de pythoneer ici. Mais je suis tombé sur un code qui était imparfait avec les importations circulaires et a causé des problèmes lors de l'ajout de tests unitaires. Donc, pour le corriger rapidement 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 souhaite corriger une erreur d'importation sans trop modifier le code.

À votre santé!


3

Il y a beaucoup de bonnes réponses ici. Bien qu'il existe généralement des solutions rapides au problème, dont certaines semblent plus pythoniques que d'autres, si vous avez le luxe de refactoriser, une autre approche consiste à analyser l'organisation de votre code et à supprimer la dépendance circulaire. Vous pouvez par exemple constater que vous avez:

Fichier a.py

from b import B

class A:
    @staticmethod
    def save_result(result):
        print('save the result')

    @staticmethod
    def do_something_a_ish(param):
        A.save_result(A.use_param_like_a_would(param))

    @staticmethod
    def do_something_related_to_b(param):
        B.do_something_b_ish(param)

Fichier b.py

from a import A

class B:
    @staticmethod
    def do_something_b_ish(param):
        A.save_result(B.use_param_like_b_would(param))

Dans ce cas, il suffit de déplacer une méthode statique vers un fichier distinct, par exemple c.py:

Fichier c.py

def save_result(result):
    print('save the result')

permettra de supprimer la save_resultméthode de A, et donc de supprimer l'importation de A de a dans b:

Fichier refacturé a.py

from b import B
from c import save_result

class A:
    @staticmethod
    def do_something_a_ish(param):
        A.save_result(A.use_param_like_a_would(param))

    @staticmethod
    def do_something_related_to_b(param):
        B.do_something_b_ish(param)

Fichier refacturé b.py

from c import save_result

class B:
    @staticmethod
    def do_something_b_ish(param):
        save_result(B.use_param_like_b_would(param))

En résumé, si vous avez un outil (par exemple pylint ou PyCharm) qui rend compte des méthodes qui peuvent être statiques, il suffit de jeter un staticmethoddécorateur dessus pour ne pas être le meilleur moyen de faire taire l'avertissement. Même si la méthode semble liée à la classe, il pourrait être préférable de la séparer, surtout si vous avez plusieurs modules étroitement liés qui pourraient avoir besoin de la même fonctionnalité et que vous avez l'intention de pratiquer les principes DRY.


2

Les importations circulaires peuvent prêter à confusion car l'importation signifie deux choses:

  1. il exécute le code du module importé
  2. ajoute le module importé à la table de symboles globale du module importateur

Le premier est effectué une seule fois, tandis que le second à chaque déclaration d'importation. L'importation circulaire crée une situation lorsque le module d'importation utilise un module importé avec du 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 montre.

Les importations circulaires ne sont pas le mal ultime à éviter à tout prix. Dans certains frameworks comme Flask, ils sont assez naturels et peaufiner 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 __name__ == '__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, __name__ = {}".format(__name__)
x = 3
print 'b imports a'
import a
y = 5
print "b out"

a.py

print 'a in, __name__ = {}'.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, __name__ = b    # b code execution started
b imports a
a in, __name__ = 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

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

J'ai ajouté cela à a.pyet cela a fonctionné.

if __name__ == "__main__":
        main ()

a.py:

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

if __name__ == "__main__":
    main ()

b.py:

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

La sortie que j'obtiens est

>>> b out 
>>> a out 
>>> 5

0

Ok, je pense que j'ai une solution assez cool. Disons que vous avez un fichier aet un fichier b. Vous avez un defou classdans le fichier bque vous souhaitez utiliser dans le module a, mais vous avez quelque chose d' autre, que ce soit un def, classou une variable de fichier aque vous avez besoin dans votre définition ou de la classe dans le fichier b. Ce que vous pouvez faire est, au bas du fichier a, après avoir appelé la fonction ou la classe dans le fichier aqui est nécessaire dans le fichier b, mais avant d'appeler la fonction ou la classe à partir du fichier bdont vous avez besoin pour le fichier a, dites import b ensuite, et voici la partie clé , dans toutes les définitions ou classes du fichier bqui nécessitent le defou à classpartir du fichiera(appelons-le CLASS), vous ditesfrom a import CLASS

Cela fonctionne car vous pouvez importer un fichier bsans que Python n'exécute aucune des instructions d'importation dans le fichier b, et vous échappez ainsi à toute importation circulaire.

Par exemple:

Déposer un:

class A(object):

     def __init__(self, name):

         self.name = name

CLASS = A("me")

import b

go = B(6)

go.dostuff

Fichier b:

class B(object):

     def __init__(self, number):

         self.number = number

     def dostuff(self):

         from a import CLASS

         print "Hello " + CLASS.name + ", " + str(number) + " is an interesting number."

Voila.


from a import CLASSne saute pas réellement l'exécution de tout le code dans a.py. Voici ce qui se passe réellement: (1) Tout le code dans a.py est exécuté comme un module spécial "__main__". (2) À import b, le code de niveau supérieur dans b.py est exécuté (définissant la classe B), puis le contrôle retourne à "__main__". (3) "__main__" passe éventuellement le contrôle à go.dostuff(). (4) lorsque doStuff () vient import a, il exécute tout le code dans a.py à nouveau , cette fois que le module « a »; puis il importe l'objet CLASS du nouveau module "a". Donc, en fait, cela fonctionnerait aussi bien si vous l' import autilisiez n'importe où dans b.py.
Matthias Fripp
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.