Кругова залежність від імпорту в Python


77

Скажімо, у мене є така структура каталогів:

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

У aпакеті років __init__.py, то cпакет імпортуються. Але c_file.pyімпортa.b.d .

Програма не працює, кажучи, bщо не існує при c_file.pyспробі імпортуватиa.b.d . (І насправді його не існує, тому що ми були в середині імпорту.)

Як можна усунути цю проблему?


1
Може, ви можете спробувати відносний імпорт? stackoverflow.com/questions/72852 / ...
eremzeit


також просто як посилання, здається, циркулярний імпорт дозволений на python 3.5 (і, можливо, і пізніше), але не 3.4 (і, можливо, нижче).
Чарлі Паркер,

1
Якщо ви виявите помилку імпорту, вона буде працювати нормально, якщо вам не потрібно буде використовувати щось в іншому модулі, перш ніж перший модуль завершить імпорт.
Gavin S. Yancey

Відповіді:


62

Якщо a залежить від c, а c залежить від a, чи насправді це не однакові одиниці?

Ви повинні реально вивчити, чому ви розділили a та c на два пакети, тому що або у вас є якийсь код, який ви повинні розділити на інший пакет (щоб вони обидва залежали від нового пакету, але не один від одного), або ви повинні об'єднати їх в один пакет.


112
Так, їх можна було б вважати одним пакетом. Але якщо це призводить до величезного файлу, то це недоцільно. Я згоден з тим, що часто кругові залежності означають, що дизайн слід ще раз продумати. Але Є кілька шаблонів дизайну, де це доцільно (і де об’єднання файлів разом призведе до величезного файлу), тому я вважаю догматичним стверджувати, що пакети слід або комбінувати, або дизайн слід переглядати.
Метью Лунд,

158

Ви можете відкласти імпорт, наприклад a/__init__.py:

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

тобто відкласти імпорт доти, поки це дійсно не знадобиться. Однак я б також уважно розглянув мої визначення / використання пакетів, оскільки циклічна залежність, подібна до вказаної, може вказувати на проблему проектування.


4
Іноді кругових посилань справді не уникнути. Це єдиний підхід, який працює для мене за таких обставин.
Джейсон Політес

1
Чи не додасть це багато накладних витрат під час кожного дзвінка foo?
Mr_and_Mrs_D

6
@Mr_and_Mrs_D - лише помірно. Python зберігає всі імпортовані модулі в глобальному кеші ( sys.modules), тому, як тільки модуль буде завантажений, він не буде завантажений знову. Код може включати пошук імені під час кожного дзвінка my_function, але також і код, який посилається на символи через кваліфіковані імена (наприклад, import foo; foo.frobnicate())
Дірк,

з усіх можливих рішень тут це єдине, що спрацювало для мене. Існують абсолютно обставини, коли кругове посилання є «найкращим» рішенням, особливо коли ви робите розділення набору об’єктів моделі на кілька файлів для обмеження розмірів файлів.
Richard J

14
Іноді кругові посилання є саме правильним способом моделювання проблеми. Думка про те, що кругові залежності якимось чином свідчать про поганий дизайн, здається, більше відображає Python як мову, а не легітимну точку дизайну.
Джулі в Остіні

29

Я дивувався цьому кілька разів (зазвичай, маючи справу з моделями, які повинні знати одне про одного). Найпростішим рішенням є лише імпорт цілого модуля, а потім посилання на те, що вам потрібно.

Тож замість того, щоб робити

from models import Student

в одному, і

from models import Classroom

в іншому - просто зробіть

import models

в одному з них, а потім зателефонуйте моделям. Класна кімната, коли це вам потрібно.


4

Кругові залежності завдяки підказкам типу

З натяками на тип є більше можливостей для створення кругового імпорту. До щастя, є рішення , використовуючи спеціальну константу: typing.TYPE_CHECKING.

Наступний приклад визначає Vertexклас і Edgeклас. Ребро визначається двома вершинами, а вершина підтримує список сусідніх ребер, до яких воно належить.

 

Без підказок типу, помилок немає

Файл: vertex.py

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

Файл: edge.py

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

 

Тип Підказки Причина ImportError

ImportError: неможливо імпортувати ім'я 'Edge' з частково ініціалізованого модуля 'edge' (швидше за все, через круговий імпорт)

Файл: 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] = []

Файл: edge.py

from vertex import Vertex


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

 

Рішення за допомогою TYPE_CHECKING

Файл: 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'] = []

Файл: 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

 

Процитовані проти підказок типу без котирувань

У версіях Python до 3.10 умовно імпортовані типи повинні міститися в лапках, роблячи їх "прямими посиланнями", що приховує їх від середовища виконання інтерпретатора.

У Python 3.7, 3.8 та 3.9 обхідним шляхом є використання такого спеціального імпорту.

from __future__ import annotations

Це дозволяє використовувати підказки типу без котирувань у поєднанні з умовним імпортом.

Python 3.10 (див. PEP 563 - Відкладена оцінка анотацій )

У Python 3.10 анотації функцій та змінних більше не оцінюватимуться під час визначення. Натомість форма рядка буде збережена у відповідному словнику анотацій . Статичні перевірки типу не бачать різниці в поведінці, тоді як інструменти, що використовують анотації під час виконання, повинні виконувати відкладене оцінювання.

Форма рядка отримується з AST на етапі компіляції, що означає, що форма рядка може не зберегти точне форматування джерела. Примітка: якщо анотація вже була рядковим літералом, вона все одно буде загорнута в рядок.


0

Проблема полягає в тому, що під час запуску з каталогу за замовчуванням лише пакунки, які є підкаталогами, видно як імпорт кандидатів, тому ви не можете імпортувати abd. Однак ви можете імпортувати bd, оскільки b є підпакетом a.

Якщо ви дійсно хочете імпортувати abd в, c/__init__.pyви можете зробити це, змінивши системний шлях на один каталог над a та змінивши імпорт уa/__init__.py на import abc

Ви a/__init__.pyповинні виглядати так:

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

Додаткова складність виникає, коли ви хочете запускати модулі на c як скрипти. Тут пакети a і b не існують. Ви можете зламати __int__.pyв каталозі гр , щоб вказати sys.path в каталог верхнього рівня , а потім імпортувати __init__в будь-яких модулях всередині з , щоб мати можливість використовувати повний шлях для імпорту Абда я сумніваюся , що це хороша практика для імпорту , __init__.pyале працював для моїх випадків використання.


0

Я пропоную наступний зразок. Його використання дозволить автозаповнення та підказку типу працювати належним чином.

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-----------------')

Ви не можете імпортувати класи A & B, використовуючи цей синтаксис

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

Ви не можете оголосити тип параметра a у методі класу __ init __ класу B, але ви можете "закинути" його таким чином:

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

-4

Іншим рішенням є використання проксі-сервера для файлу d_.

Наприклад, скажімо, що ви хочете поділитися класом blah з файлом c_file. Таким чином, файл d_ містить:

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

Ось те, що ви вводите в c_file.py:

# 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()

І в 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
зміна атрибутів глобального модуля в інший файл, наприклад, це швидко призведе до кошмару
Сурма
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.