Importowanie cykliczne w Pythonie?


102

Więc otrzymuję ten błąd

Traceback (most recent call last):
  File "/Users/alex/dev/runswift/utils/sim2014/simulator.py", line 3, in <module>
    from world import World
  File "/Users/alex/dev/runswift/utils/sim2014/world.py", line 2, in <module>
    from entities.field import Field
  File "/Users/alex/dev/runswift/utils/sim2014/entities/field.py", line 2, in <module>
    from entities.goal import Goal
  File "/Users/alex/dev/runswift/utils/sim2014/entities/goal.py", line 2, in <module>
    from entities.post import Post
  File "/Users/alex/dev/runswift/utils/sim2014/entities/post.py", line 4, in <module>
    from physics import PostBody
  File "/Users/alex/dev/runswift/utils/sim2014/physics.py", line 21, in <module>
    from entities.post import Post
ImportError: cannot import name Post

i widzisz, że używam tej samej instrukcji importu w dalszej części i działa? Czy jest jakaś niepisana zasada dotycząca importowania cyklicznego? Jak używać tej samej klasy niżej w stosie wywołań?

Odpowiedzi:


167

Myślę, że odpowiedź z jpmc26, choć w żadnym wypadku nie jest błędna , zbyt mocno dotyczy importu kołowego. Mogą działać dobrze, jeśli skonfigurujesz je poprawnie.

Najłatwiej to zrobić, używając import my_moduleskładni zamiast from my_module import some_object. Ten pierwszy prawie zawsze będzie działał, nawet jeśli zostanie my_moduleuwzględniony, importuje nas z powrotem. Ta ostatnia działa tylko wtedy, gdy my_objectjest już zdefiniowana wmy_module , co może nie mieć miejsca w przypadku importu cyklicznego.

Aby być konkretnym dla twojego przypadku: spróbuj zmienić, entities/post.pyaby zrobić, import physicsa następnie odnosić się do niego, physics.PostBodya nie tylko PostBodybezpośrednio. Podobnie, zmień, physics.pyaby zrobić, import entities.posta następnie użyj, entities.post.Posta nie tylko Post.


5
Czy ta odpowiedź jest zgodna z importem względnym?
Joe

19
Dlaczego to się dzieje?
Juan Pablo Santos

5
Błędem jest twierdzenie, że nieskładnia fromzawsze będzie działać. Jeśli mam class A(object): pass; class C(b.B): passw module a iw class B(a.A): passmodule b, import cykliczny nadal stanowi problem i to nie zadziała.
CrazyCasta

1
Masz rację, wszelkie zależności cykliczne w kodzie najwyższego poziomu modułów (takie jak klasy bazowe deklaracji klas w twoim przykładzie) będą problemem. Jest to sytuacja, w której odpowiedź jpmc, że należy zmienić organizację modułu, jest prawdopodobnie w 100% poprawna. Przenieś klasę Bdo modułu alub przenieś klasę Cdo modułu, baby przerwać cykl. Warto również zauważyć, że nawet jeśli tylko jeden kierunek okręgu ma kod najwyższego poziomu zaangażowania (np jeśli klasa Cnie istnieje), to może się błąd, w zależności od modułu został importowany pierwszy przez inny kod.
Blckknght

2
@TylerCrompton: Nie jestem pewien, co masz na myśli mówiąc „import modułu musi być bezwzględny”. Importy względne cykliczne mogą działać, o ile importujesz moduły, a nie ich zawartość (np. from . import sibling_moduleNie from .sibling_module import SomeClass). Jest trochę więcej subtelności, gdy plik pakietu __init__.pyjest zaangażowany w import cykliczny, ale problem jest zarówno rzadki, jak i prawdopodobnie błąd importimplementacji. Zobacz błąd w Pythonie 23447 , dla którego przesłałem poprawkę (która niestety nie działa).
Blckknght

51

Kiedy importujesz moduł (lub jego element członkowski) po raz pierwszy, kod wewnątrz modułu jest wykonywany sekwencyjnie, jak każdy inny kod; np. nie jest traktowany inaczej niż ciało funkcji. importJest tylko komenda jak każdy inny (cesji, wywołania funkcji, def, class). Zakładając, że importowanie odbywa się u góry skryptu, oto co się dzieje:

  • Przy próbie importu Worldz worldTheworld skrypt zostaje wykonany.
  • Na worldimport skryptów Field, co powodujeentities.field skrypt zostanie wykonany.
  • Ten proces trwa, dopóki nie osiągniesz entities.post skryptu, ponieważ próbowałeś zaimportowaćPost
  • Te entities.postprzyczyny scriptphysics moduł ma zostać wykonany, ponieważ stara się importemPostBody
  • Wreszcie physicspróbuje importować Postzentities.post
  • Nie jestem pewien, czy entities.postmoduł istnieje w pamięci, ale to naprawdę nie ma znaczenia. Albo modułu nie ma w pamięci, albo moduł nie ma jeszcze Postczłonka, ponieważ nie zakończył wykonywania definiowaniaPost
  • Tak czy inaczej, wystąpi błąd, ponieważ Postnie ma go do zaimportowania

Więc nie, to nie jest „praca wyżej w stosie wywołań”. To jest ślad stosu wskazujący, gdzie wystąpił błąd, co oznacza, że ​​wystąpił błąd podczas próby importu Postw tej klasie. Nie należy używać importu cyklicznego. W najlepszym przypadku przynosi znikome korzyści (zazwyczaj brak korzyści) i powoduje takie problemy. Obciąża to każdego dewelopera w utrzymaniu go, zmuszając go do chodzenia po skorupkach jaj, aby ich nie rozbić. Zmień organizację modułów.


1
Powinien być isinstance(userData, Post). Niezależnie od tego nie masz wyboru. Import cykliczny nie zadziała. Fakt, że masz import okrężny, to dla mnie zapach kodu. Sugeruje, że masz pewne funkcje, które powinny zostać przeniesione do trzeciego modułu. Nie mogłem powiedzieć czego, nie patrząc na obie klasy.
jpmc26

3
@CpILL Po jakimś czasie przyszła mi do głowy bardzo hakerska opcja. Jeśli nie można obejść w ten sposób do teraz (ze względu na ograniczenia czasowe lub co ty), to mógłby zrobić import lokalnie wewnątrz metody gdzie używasz go. Treść funkcji wewnątrz defnie jest wykonywana, dopóki funkcja nie zostanie wywołana, więc import nie nastąpi, dopóki nie wywołasz funkcji. Do tego czasu imports powinny działać, ponieważ jeden z modułów zostałby całkowicie zaimportowany przed wywołaniem. To absolutnie obrzydliwy hack i nie powinien pozostawać w bazie twojego kodu przez dłuższy czas.
jpmc26

15
Myślę, że twoja odpowiedź jest zbyt trudna w przypadku importu kołowego. Import cykliczny zwykle działa, jeśli robisz to tylko import foozamiast from foo import Bar. Dzieje się tak, ponieważ większość modułów po prostu definiuje rzeczy (takie jak funkcje i klasy), które będą uruchamiane później. Moduły, które wykonują ważne rzeczy podczas ich importowania (takie jak skrypt niechroniony przez if __name__ == "__main__"), mogą nadal powodować problemy, ale nie jest to zbyt częste.
Blckknght

6
@Blckknght Myślę, że przygotowujesz się do spędzania czasu na dziwnych problemach, które inni ludzie będą musieli zbadać i być zdezorientowani, jeśli używasz importu cyklicznego. Zmuszają cię do spędzania czasu uważając, aby się o nie nie potknąć, a do tego dochodzi zapach kodu, który twój projekt wymaga refaktoryzacji. Mogłem się mylić co do tego, czy są one technicznie wykonalne, ale są to okropny wybór projektowy, który prędzej czy później spowoduje problemy. Przejrzystość i prostota to święty Graal w programowaniu, a import okrężny narusza oba w mojej książce.
jpmc26

6
Alternatywnie; za bardzo podzieliłeś swoją funkcjonalność i to jest przyczyną cyklicznego importu. Jeśli masz dwie rzeczy, które przez cały czas na sobie polegają ; najlepiej umieścić je w jednym pliku. Python to nie Java; nie ma powodu, aby nie grupować funkcji / klas w jednym pliku, aby zapobiec dziwnej logice importu. :-)
Mark Ribau

41

Aby zrozumieć zależności cykliczne, należy pamiętać, że Python jest zasadniczo językiem skryptowym. Wykonywanie instrukcji poza metodami odbywa się w czasie kompilacji. Instrukcje importu są wykonywane tak jak wywołania metod i aby je zrozumieć, należy myśleć o nich jak o wywołaniach metod.

Podczas importowania to, co się stanie, zależy od tego, czy importowany plik już istnieje w tabeli modułu. Jeśli tak, Python używa tego, co aktualnie znajduje się w tablicy symboli. Jeśli nie, Python zaczyna czytać plik modułu, kompilując / wykonując / importując wszystko, co tam znajdzie. Symbole, do których odwołuje się w czasie kompilacji, zostały znalezione lub nie, w zależności od tego, czy zostały one zauważone, czy nie zostały jeszcze zauważone przez kompilator.

Wyobraź sobie, że masz dwa pliki źródłowe:

Plik X.py

def X1:
    return "x1"

from Y import Y2

def X2:
    return "x2"

Plik Y.py

def Y1:
    return "y1"

from X import X1

def Y2:
    return "y2"

Teraz załóżmy, że kompilujesz plik X.py. Kompilator rozpoczyna od zdefiniowania metody X1, a następnie trafia na instrukcję import w X.py. Powoduje to, że kompilator wstrzymuje kompilację X.py i rozpoczyna kompilację Y.py. Wkrótce potem kompilator trafia na instrukcję import w Y.py. Ponieważ X.py jest już w tabeli modułów, Python używa istniejącej niekompletnej tabeli symboli X.py, aby spełnić wszelkie wymagane odniesienia. Wszelkie symbole pojawiające się przed instrukcją import w X.py znajdują się teraz w tablicy symboli, ale żadne symbole po nich już nie. Ponieważ X1 pojawia się teraz przed instrukcją importu, zostało pomyślnie zaimportowane. Python wznawia kompilację Y.py. Robiąc to, definiuje Y2 i kończy kompilację Y.py. Następnie wznawia kompilację X.py i znajduje Y2 w tabeli symboli Y.py. Kompilacja ostatecznie kończy się bez błędu.

Coś zupełnie innego dzieje się, jeśli spróbujesz skompilować Y.py z wiersza poleceń. Podczas kompilowania Y.py kompilator trafia w instrukcję import, zanim zdefiniuje Y2. Następnie zaczyna kompilować X.py. Wkrótce trafia na instrukcję import w X.py, która wymaga Y2. Ale Y2 jest niezdefiniowane, więc kompilacja kończy się niepowodzeniem.

Zwróć uwagę, że jeśli zmodyfikujesz X.py, aby zaimportować Y1, kompilacja zawsze się powiedzie, bez względu na to, który plik kompilujesz. Jednak jeśli zmodyfikujesz plik Y.py, aby zaimportować symbol X2, żaden plik nie zostanie skompilowany.

Za każdym razem, gdy moduł X lub jakikolwiek moduł importowany przez X może zaimportować bieżący moduł, NIE używaj:

from X import Y

Za każdym razem, gdy myślisz, że może nastąpić cykliczny import, powinieneś również unikać kompilacji odniesień do zmiennych w innych modułach. Rozważ niewinnie wyglądający kod:

import X
z = X.Y

Załóżmy, że moduł X importuje ten moduł, zanim ten moduł zaimportuje X. Ponadto przypuśćmy, że Y jest zdefiniowane w X po instrukcji importu. Wtedy Y nie zostanie zdefiniowane podczas importowania tego modułu i pojawi się błąd kompilacji. Jeśli ten moduł zaimportuje Y jako pierwszy, możesz sobie z tym poradzić. Ale kiedy jeden z twoich współpracowników niewinnie zmieni kolejność definicji w trzecim module, kod się zepsuje.

W niektórych przypadkach można rozwiązać zależności cykliczne, przenosząc instrukcję importu w dół poniżej definicji symboli wymaganych przez inne moduły. W powyższych przykładach definicje przed instrukcją import nigdy nie zawodzą. Definicje po instrukcji import czasami zawodzą, w zależności od kolejności kompilacji. Możesz nawet umieścić instrukcje importu na końcu pliku, o ile żaden z importowanych symboli nie jest potrzebny w czasie kompilacji.

Zwróć uwagę, że przeniesienie instrukcji importu w dół w module przesłania to, co robisz. Skompensuj to komentarzem u góry modułu, podobnym do następującego:

#import X   (actual import moved down to avoid circular dependency)

Ogólnie jest to zła praktyka, ale czasami trudno jej uniknąć.


2
Nie sądzę, istnieje kompilator lub kompilacji w Pythonie w ogóle
Jerie Wang

6
Python nie posiada kompilator, i jest kompilowane @pkqxdd, kompilacja jest tylko zwykle ukryte przed użytkownikiem. Może to być trochę zagmatwane, ale autorowi trudno byłoby podać ten niezwykle jasny opis tego, co się dzieje, bez odniesienia do, nieco zaciemnionego, „czasu kompilacji” Pythona.
Hank


Poszedłem do przodu, aby wypróbować to na mojej maszynie i uzyskałem inny wynik. Uruchomiono X.py, ale pojawił się błąd „nie można zaimportować nazwy 'Y2' z 'Y'”. Ran Y.py bez problemu. Jestem na Pythonie 3.7.5. Czy mógłbyś pomóc wyjaśnić, na czym polega problem?
xuefeng huang

18

Dla tych z Was, którzy, tak jak ja, przychodzą do tego numeru z Django, powinniście wiedzieć, że dokumentacja zapewnia rozwiązanie: https://docs.djangoproject.com/en/1.10/ref/models/fields/#foreignkey

„... Aby odwołać się do modeli zdefiniowanych w innej aplikacji, możesz jawnie określić model z pełną etykietą aplikacji. Na przykład, jeśli powyższy model producenta jest zdefiniowany w innej aplikacji zwanej produkcją, musisz użyć:

class Car(models.Model):
    manufacturer = models.ForeignKey(
        'production.Manufacturer',
        on_delete=models.CASCADE,
)

Ten rodzaj odwołania może być przydatny podczas rozwiązywania zależności między importami cyklicznymi między dwiema aplikacjami.… ”


6
Wiem, że nie powinienem używać komentarza, aby powiedzieć „dziękuję”, ale dręczy mnie to od kilku godzin. Dziekuję Dziekuję Dziękuję!!!
MikeyE,

Zgadzam się z @MikeyE. Przeczytałem kilka blogów i Stackoverflows próbując temu zaradzić dzięki PonyORM. Tam, gdzie inni mówią, że to zła praktyka lub dlaczego miałbyś zakodować swoje klasy tak, aby były okrągłe, dobrze, że ORM są dokładnie tam, gdzie to się dzieje. Ponieważ wiele przykładów umieszcza wszystkie modele w tym samym pliku, a my podążamy za tymi przykładami, z wyjątkiem tego, że używamy modelu na plik, problem nie jest jasny, gdy Python nie może się skompilować. Jednak odpowiedź jest taka prosta. Jak wskazał Mike, bardzo dziękuję.
trash80

4

Udało mi się zaimportować moduł w ramach funkcji (tylko), który wymagałby obiektów z tego modułu:

def my_func():
    import Foo
    foo_instance = Foo()

jak elegancko z Pythona
Yaro

2

Jeśli napotkasz ten problem w dość złożonej aplikacji, refaktoryzacja wszystkich importów może być uciążliwa. PyCharm oferuje szybką naprawę, która automatycznie zmieni również użycie importowanych symboli.

wprowadź opis obrazu tutaj


0

Używałem następujących:

from module import Foo

foo_instance = Foo()

ale żeby się go pozbyć circular referencezrobiłem co następuje i zadziałało:

import module.foo

foo_instance = foo.Foo()
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.