Python. Проблемы импорта
версия для печати
![]() |
В Питоне отбитая система импорта, она может довести до истерики. И хотя я новичок в этой теме, я все же хочу зафискировать (в первую очередь для себя) решения, которые я нашел и у меня они работают. |
Вот есть такая иерархия скриптов:
|- reuse/ | ├- __init__.py | └- notificators.py | |- sandbox/ | ├- __init__.py | ├- connectors.py | └- wrapper.py | |- local_tests/ | └- some.py | |- main.py |- README.md
Не прикапывайтесь к организации каталогов, на практике она не существует. Примеры надуманные, только для демонстрации работы с импортами.
Сейчас неважно, как именно описаны __init__.py. Остановимся на том, что там как-то импорты объявлены. Питон видит каждый такой каталог, как пакет, из него можно что-то вызывать.
Импорт из соседнего каталога
Если запускаемый скрипт, как минимум, на уровень выше импортируемых - все работает:
# main.py
from reuse import notify_slack
notify_slack('This is a test')
$ python main.py # Так работает
Если импорты между соседними каталогами, но при этом запускаемый скрипт - выше, тоже будет работать, только импорты нужно относительные описывать:
# wrapper.py
# Тут относительный путь до импортируемого компонента, который лежит в текущем каталоге
# в соседнем скрипте
from .connectors import clickhouse_client
# А тут относительный путь до компонента в соседнем каталоге
from ..reuse import notify_slack
def query():
client = clickhouse_client()
...
def send(message):
notify_slack(message)
Запускаемый скрипт:
# main.py
from sandbox.wrapper import send
send('This is a test')
Но если запускаемый скрипт и импортируемый - в соседних каталогах, то уже ни хрена без бубна не работает. Поэтому делаем так:
# some.py
# Важно! Тут абсолютный путь, несмотря на то, что каталоги - соседи.
# С относительным не будет работать решение ниже.
from reuse import notify_slack
notify_slack('This is a test', True)
И вызываем как модуль:
$ python local_tests/some.py # Так не будет работать. $ python -m local_tests.some # А так - будет.
Решение нашел тут. Но по уму решение куда сложнее - SoF. В основе этих решений лежит мат.часть, как именно Питон рулит импортами, но там настолько сложно все, что одной цитатой не обойтись.
Циклический импорт
Ситуация #1: два скрипта в одном каталоге и нужно импортировать из одного во второй. Файловая организация из начала статьи, возьмем кусок:
|- sandbox/ | ├- __init__.py | ├- connectors.py | └- wrapper.py
Как не будет работать и как решается:
# wrapper.py
# Не используй короткую запись. Будет циклический импорт.
# from . import clickhouse_client
# Работает с прямым указанием скрипта, откуда импортировать.
from .connectors import clickhouse_client
Ситуация #2: реальный циклический импорт. Т.е. когда через последовательность импортов от пакета к пакету внезапно приходим в пакет, откуда начался импорт. Тут проще показать лог ошибки, чем описывать на абстрактном примере:
# python src/import_test.py Traceback (most recent call last): File "/usr/local/app/src/import_test.py", line 1, infrom packages.infrastructure import AppManager, Logger File "/usr/local/app/src/packages/infrastructure/__init__.py", line 1, in from .app_manager import * File "/usr/local/app/src/packages/infrastructure/app_manager.py", line 7, in from ..configurations import Target File "/usr/local/app/src/packages/configurations/__init__.py", line 1, in from .conf_builder import * File "/usr/local/app/src/packages/configurations/conf_builder.py", line 6, in from ..infrastructure import Logger, PathGuide ImportError: cannot import name 'Logger' from partially initialized module 'packages.infrastructure' (most likely due to a circular import) (/usr/local/app/src/packages/infrastructure/__init__.py)
Такие логи сложно раскуривать, но именно так Python сообщает о проблеме. Еще сложнее понять, а почему она вообще появилась. В коде у меня цепочка вызовов:
main -> infrastructure.AppManager -> configurations.Target
Казалось бы, причем тут configurations.conf_builder с его импортами?
А дело в том, что при импорте одного "объекта" из пакета Питон вычитывает вообще все импорты из глобальной области видимости каждого скрипта в пакете и немедленно их выполняет. Ну а че тянуть-то? И не помогают даже манипуляции c дописыванием пути в sys.path с последующим импортом "а-ля модуль", прием описан тут.
Короче, лечится это переносом проблемного импорта непосредственно в каждую функцию, где он нужен. Костыли.
Еще один способ избавиться от циклического импорта - убрать что-нибудь из цепочки в другой каталог. Можно в соседний, можно в подкаталог. Делается для того, чтобы через init-файлы нельзя было пойти по кругу. Эти файлы описывают содержимое только в своем каталоге. Даже если в нужном скрипте проблемный класс не имортируется, важно, чтобы во всех скриптах каталога его не было. Как-то так.
Должен заметить, что практика описывать все импорты в начале скрипта (в его глобальной области) применяется во многих языках. Это необязательное требование, просто код удобнее изучать, когда все зависимости наверху в куче. Но Питон - он особенный..
[1oo%, EoF]Понравилась статья? Расскажите о ней друзьям: