Zirkuläre Importabhängigkeit in Python


77

Angenommen, ich habe die folgende Verzeichnisstruktur:

a\
    __init__.py
    b\
        __init__.py
        c\
            __init__.py
            c_file.py
        d\
            __init__.py
            d_file.py

In den aPaketen __init__.pywird das cPaket importiert. Aber c_file.pyImporte a.b.d.

Das Programm schlägt fehl und sagt, dass bes beim c_file.pyImportieren nicht vorhanden ist a.b.d. (Und es existiert wirklich nicht, weil wir gerade dabei waren, es zu importieren.)

Wie kann dieses Problem behoben werden?


1
Vielleicht könnten Sie relative Importe versuchen? stackoverflow.com/questions/72852/…
eremzeit


auch nur als Referenz scheint es, dass zirkuläre Importe auf Python 3.5 (und wahrscheinlich darüber hinaus) erlaubt sind, aber nicht auf 3.4 (und wahrscheinlich unten).
Charlie Parker

1
Wenn Sie den Importfehler bemerken, funktioniert er einwandfrei, solange Sie im anderen Modul nichts verwenden müssen, bevor das erste Modul den Import beendet.
Gavin S. Yancey

Antworten:


63

Wenn a von c und c von a abhängt, sind sie dann nicht tatsächlich dieselbe Einheit?

Sie sollten wirklich untersuchen, warum Sie a und c in zwei Pakete aufgeteilt haben, da Sie entweder Code haben, den Sie in ein anderes Paket aufteilen sollten (damit beide von diesem neuen Paket abhängen, aber nicht voneinander), oder Sie sollten sie zusammenführen in ein Paket.


111
Ja, sie könnten als dasselbe Paket betrachtet werden. Wenn dies jedoch zu einer riesigen Datei führt, ist dies unpraktisch. Ich stimme zu, dass kreisförmige Abhängigkeiten häufig bedeuten, dass das Design erneut durchdacht werden sollte. Es gibt jedoch einige Entwurfsmuster, bei denen dies angemessen ist (und bei denen das Zusammenführen der Dateien zu einer großen Datei führen würde). Daher halte ich es für dogmatisch zu sagen, dass die Pakete entweder kombiniert oder das Design neu bewertet werden sollte.
Matthew Lund

158

Sie können den Import verschieben, zum Beispiel in a/__init__.py:

def my_function():
    from a.b.c import Blah
    return Blah()

Verschieben Sie den Import, bis er wirklich benötigt wird. Ich würde mir jedoch auch meine Paketdefinitionen / -verwendungen genauer ansehen, da eine zyklische Abhängigkeit wie die oben genannte auf ein Entwurfsproblem hinweisen könnte.


4
Manchmal sind Zirkelverweise wirklich unvermeidlich. Dies ist der einzige Ansatz, der unter diesen Umständen für mich funktioniert.
Jason Polites

1
Würde dies nicht bei jedem Anruf von foo viel Aufwand verursachen?
Mr_and_Mrs_D

6
@Mr_and_Mrs_D - nur mäßig. Python speichert alle importierten Module in einem globalen Cache ( sys.modules). Sobald ein Modul geladen wurde, wird es nicht mehr geladen. Der Code kann eine Namenssuche bei jedem Aufruf von beinhalten my_function, aber auch Code, der Symbole über qualifizierte Namen (z. B. import foo; foo.frobnicate()) referenziert
Dirk

Von allen möglichen Lösungen hier ist dies die einzige, die für mich funktioniert hat. Es gibt absolut Umstände, in denen ein Zirkelverweis "die beste" Lösung ist - insbesondere, wenn Sie eine Reihe von Modellobjekten auf mehrere Dateien aufteilen, um die Dateigröße zu beschränken.
Richard J

14
Manchmal sind Zirkelverweise genau der richtige Weg, um das Problem zu modellieren. Die Vorstellung, dass zirkuläre Abhängigkeiten irgendwie ein Hinweis auf schlechtes Design sind, scheint eher eine Reflexion über Python als Sprache als ein legitimer Entwurfspunkt zu sein.
Julie in Austin

29

Ich habe mich das ein paar Mal gefragt (normalerweise beim Umgang mit Modellen, die voneinander wissen müssen). Die einfache Lösung besteht darin, nur das gesamte Modul zu importieren und dann auf das zu verweisen, was Sie benötigen.

Also anstatt zu tun

from models import Student

in einem und

from models import Classroom

in der anderen einfach tun

import models

Rufen Sie in einem von ihnen models.Classroom an, wenn Sie es brauchen.


3

Zirkuläre Abhängigkeiten aufgrund von Typhinweisen

Mit Typhinweisen gibt es mehr Möglichkeiten, zirkuläre Importe zu erstellen. Glücklicherweise gibt es eine Lösung mit der speziellen Konstante : typing.TYPE_CHECKING.

Das folgende Beispiel definiert eine VertexKlasse und eine EdgeKlasse. Eine Kante wird durch zwei Scheitelpunkte definiert, und ein Scheitelpunkt verwaltet eine Liste der benachbarten Kanten, zu denen er gehört.

 

Ohne Typhinweise kein Fehler

Datei: vertex.py

class Vertex:
    def __init__(self, label):
        self.label = label
        self.adjacency_list = []

Datei: edge.py

class Edge:
    def __init__(self, v1, v2):
        self.v1 = v1
        self.v2 = v2

 

Typ Hinweise Ursache ImportError

ImportError: Der Name 'Edge' kann nicht aus dem teilweise initialisierten Modul 'edge' importiert werden (höchstwahrscheinlich aufgrund eines zirkulären Imports).

Datei: vertex.py

from typing import List
from edge import Edge


class Vertex:
    def __init__(self, label: str):
        self.label = label
        self.adjacency_list: List[Edge] = []

Datei: edge.py

from vertex import Vertex


class Edge:
    def __init__(self, v1: Vertex, v2: Vertex):
        self.v1 = v1
        self.v2 = v2

 

Lösung mit TYPE_CHECKING

Datei: vertex.py

from typing import List, TYPE_CHECKING

if TYPE_CHECKING:
    from edge import Edge


class Vertex:
    def __init__(self, label: str):
        self.label = label
        self.adjacency_list: List['Edge'] = []

Datei: edge.py

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from vertex import Vertex


class Edge:
    def __init__(self, v1: 'Vertex', v2: 'Vertex'):
        self.v1 = v1
        self.v2 = v2

 

Zitierte oder nicht zitierte Typhinweise

In Versionen von Python vor 3.10 müssen bedingt importierte Typen in Anführungszeichen gesetzt werden, sodass sie "Weiterleitungsreferenzen" sind, wodurch sie vor der Interpreter-Laufzeit verborgen werden.

In Python 3.7, 3.8 und 3.9 besteht eine Problemumgehung darin, den folgenden speziellen Import zu verwenden.

from __future__ import annotations

Dies ermöglicht die Verwendung von nicht zitierten Typhinweisen in Kombination mit bedingten Importen.

Python 3.10 (Siehe PEP 563 - Verschiebung der Auswertung von Anmerkungen )

In Python 3.10 werden Funktions- und Variablenanmerkungen zur Definitionszeit nicht mehr ausgewertet. Stattdessen wird eine Zeichenfolgenform im jeweiligen Anmerkungswörterbuch beibehalten . Statische Typprüfungen sehen keinen Unterschied im Verhalten, während Tools, die zur Laufzeit Anmerkungen verwenden, eine verzögerte Auswertung durchführen müssen.

Das Zeichenfolgenformular wird während des Kompilierungsschritts vom AST abgerufen. Dies bedeutet, dass das Zeichenfolgenformular möglicherweise nicht die genaue Formatierung der Quelle beibehält. Hinweis: Wenn eine Anmerkung bereits ein Zeichenfolgenliteral war, wird sie weiterhin in eine Zeichenfolge eingeschlossen.


0

Das Problem ist, dass beim Ausführen aus einem Verzeichnis standardmäßig nur die Pakete als Unterverzeichnisse als Kandidatenimporte sichtbar sind, sodass Sie abd nicht importieren können. Sie können jedoch bd importieren, da b ein Unterpaket von a ist.

Wenn Sie abd wirklich importieren möchten, c/__init__.pykönnen Sie dies erreichen, indem Sie den Systempfad in ein Verzeichnis über a ändern und den Import in a/__init__.pyabc importieren

Du a/__init__.pysolltest so aussehen:

import sys
import os
# set sytem path to be directory above so that a can be a 
# package namespace
DIRECTORY_SCRIPT = os.path.dirname(os.path.realpath(__file__)) 
sys.path.insert(0,DIRECTORY_SCRIPT+"/..")
import a.b.c

Eine zusätzliche Schwierigkeit tritt auf, wenn Sie Module in c als Skripte ausführen möchten. Hier existieren die Pakete a und b nicht. Sie können das __int__.pyim c-Verzeichnis hacken, um den sys.path auf das Verzeichnis der obersten Ebene zu verweisen, und dann __init__in alle Module in c importieren , um den vollständigen Pfad zum Importieren verwenden zu können. Ich bezweifle, dass es eine gute Praxis ist, es zu importieren, __init__.pyaber es hat für meine Anwendungsfälle gearbeitet.


0

Ich schlage das folgende Muster vor. Wenn Sie es verwenden, funktionieren die automatische Vervollständigung und die Eingabe von Hinweisen ordnungsgemäß.

cyclic_import_a.py

import playground.cyclic_import_b

class A(object):
    def __init__(self):
        pass

    def print_a(self):
        print('a')

if __name__ == '__main__':
    a = A()
    a.print_a()

    b = playground.cyclic_import_b.B(a)
    b.print_b()

cyclic_import_b.py

import playground.cyclic_import_a

class B(object):
    def __init__(self, a):
        self.a: playground.cyclic_import_a.A = a

    def print_b(self):
        print('b1-----------------')
        self.a.print_a()
        print('b2-----------------')

Mit dieser Syntax können Sie die Klassen A und B nicht importieren

from playgroud.cyclic_import_a import A
from playground.cyclic_import_b import B

Sie können den Typ des Parameters a in der Methode Klasse B __ init __ nicht deklarieren, aber Sie können ihn folgendermaßen "umwandeln":

def __init__(self, a):
    self.a: playground.cyclic_import_a.A = a

-4

Eine andere Lösung besteht darin, einen Proxy für die Datei d_file zu verwenden.

Angenommen, Sie möchten die bla-Klasse mit der Datei c_file teilen. Die d_file enthält also:

class blah:
    def __init__(self):
        print("blah")

Folgendes geben Sie in c_file.py ein:

# do not import the d_file ! 
# instead, use a place holder for the proxy of d_file
# it will be set by a's __init__.py after imports are done
d_file = None 

def c_blah(): # a function that calls d_file's blah
    d_file.blah()

Und in a's init .py:

from b.c import c_file
from b.d import d_file

class Proxy(object): # module proxy
    pass
d_file_proxy = Proxy()
# now you need to explicitly list the class(es) exposed by d_file
d_file_proxy.blah = d_file.blah 
# finally, share the proxy with c_file
c_file.d_file = d_file_proxy

# c_file is now able to call d_file.blah
c_file.c_blah() 

11
Das Ändern globaler Modulattribute in einer anderen Datei wie dieser führt schnell zu einem Albtraum
Antimon
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.