Относительный импорт и перезагрузка модуля - PullRequest
0 голосов
/ 22 января 2020

У меня есть проект машинного обучения, и я должен провести мл эксперименты. Каждый эксперимент проводится с модулем, который сильно зависит от других файлов в хранилище. Ради воспроизводимости я рассматриваю каждый эксперимент как снимок хранилища. Позже я хочу воспроизвести эксперимент и сравнить его с другим экспериментом в одном сценарии. Для этого я восстанавливаю состояние репозитория на момент эксперимента в специальной папке reproduce/experiment_name и организую следующую структуру:

├── launcher.py
├── package
│   ├── dependency.py
│   └── subpackage
│       └── module.py
└── reproduce
    ├── experiment_23
    │   └── package
    │       ├── dependency.py
    │       └── subpackage
    │           ├── module.py
    └── experiment_24
        └── package
            ├── dependency.py
            └── subpackage
                └── module.py

Итак, в launcher.py я хочу импортировать содержимое experiment_23 указан путь к нужному модулю с importlib.utils. Но мне нужно сохранить его зависимости, чтобы старый код работал в своей собственной области. Мне пришла в голову идея изменить sys.path и добавить путь непосредственно перед импортом, а затем удалить его. Вот содержимое launcher.py.

import sys
import importlib.util


path_to_module = "/package/subpackage/module.py"
experiment_path = "/reproduce/exp23"
project_path = "/home/user/PycharmProjects/PathTest"

# Here I add path to experiment directory
sys.path.insert(0, project_path + experiment_path)

spec = importlib.util.spec_from_file_location("", project_path + experiment_path + path_to_module, )
exp23 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(exp23)

# Clear path
sys.path = sys.path[1:]
# This runs successfully
exp23.run()


# So now i want to import my newest version of the module.py
# And this fails, meaning it executes code from exp23
import package.subpackage.module as exp
exp.run()

Содержимое module.py

from package.dependency import function


def run():
    function()

и dependency.py - маркер успеха:

def function():
    print("23")

Таким образом, проблема в том, что после импорта и загрузки зависимостей from package.dependency import function, python больше не будет искать его в sys.path и использовать какой-то кэш. У меня вопрос: как я могу избежать использования кеша в этом случае или, по крайней мере, как я могу добиться аналогичной функциональности разными методами?

1 Ответ

2 голосов
/ 22 января 2020

Итак, прежде всего, я предполагаю, что у вас есть __init__.py внутри каждого каталога package и subpackage, чтобы сделать их настоящими "пакетами"; как указано здесь .

Теперь вы должны заметить, как subpackage, содержащий module.py, является фактическим подпакетом package. Поэтому, когда вы делаете from package.dependency import ..., вы пытаетесь импортировать из родительского пакета. Это не только плохая практика, но и путь, который вы используете в launcher.py, предназначенный только для прямого и непосредственного нацеливания на module.py, который не знает, что такое package или даже subpackage в этом отношении.

То, на что вы должны ориентироваться, это package, который (опять же, при условии, что у вас есть __init__.py там) будет загружаться как фактический «пакет». Делая все внутри него доступным ... после нескольких настроек.

В ваших __init__.py вы должны выставить все, что вы хотите, чтобы этот модуль содержал. Например, в вашем package/__init__.py должна быть строка from . import subpackage. Это не только позволит вам делать такие вещи, как package.subpackage, но и жизненно важно при загрузке содержимого subpackage.

Следуя той же логике c, в вашем subpackage/__init__.py должно быть from . import module, загружая и выставляя module.py, что позволяет вам делать package.subpackage.module.

Это должно быть все, что вам нужно для правильной загрузки и доступа ко всему желаемому содержимому ... за исключением того, что вы также оборачиваете все что в experiment_n папках. Здесь у вас есть два варианта: вы можете сделать experiment_n пакетом с __init__.py, выставить его package и нацелиться на него с importlib, или вы можете переименовать package в experiment_n и удалить верхнюю папку. Исходя из этого:

experiment_23
    └── package
        ├── __init__.py
        ├── dependency.py
        └── subpackage
            ├── __init__.py
            └── module.py

К этому:

experiment_23
    ├── __init__.py
    ├── dependency.py
    └── subpackage
        ├── __init__.py
        └── module.py

Давайте сделаем перерыв в редактировании папки и посмотрим, как вы будете использовать нашу текущую итерацию в коде. Рассмотрим следующее launcher.py:

import sys, os
import importlib.util

current_directory = os.path.dirname(os.path.realpath(__file__))

path_to_experiment = os.path.join(current_directory, 'reproduce/experiment_23')

path_to_experiment = os.path.join(path_to_experiment, '__init__.py') # package!

spec = importlib.util.spec_from_file_location('exp23', path_to_experiment, submodule_search_locations = [])

exp23 = importlib.util.module_from_spec(spec)

sys.modules[exp23.__name__] = exp23

# old one
spec.loader.exec_module(exp23)

exp23.subpackage.module.run()

# new one
import package.subpackage.module as exp

exp.run()

С основной функцией печати main и воспроизведением одной печати exp23.

Сначала мы составим наш path_to_experiment, указывая на наш пакет __init__.py. module_from_file_location нужен файл , а не папка, поэтому мы не нацеливаемся на /expriment_23 напрямую. После, с фактическим именем (exp23), мы получаем spec с spec_from_file_location, устанавливая submodule_search_locations в пустой список, чтобы указать, что это пакет, как указано здесь . Наконец, загрузите наш модуль с помощью module_from_spec.

Важно, чтобы мы добавили наш новый модуль в sys.modules, чтобы относительный импорт (который мы используем) мог найти их родительский модуль.

Мы разрешаем наш модуль выполняет и вызывает его и функцию нашего нового модуля run:

main
main

Кажется, что это не сработало, мы хотели, чтобы первый был exp23

Преступник эта from package.dependency import function строка в нашем experiment_23/subpackage/module.py. Здесь импортируется absolute, а не relative, что означает, что он будет искать / или импортировать "package" in / to sys.modules и использовать его. В этом случае это указывает непосредственно на наш основной пакет. Изменение его на from ..dependency import function и повторное выполнение launcher.py дает нам:

exp23
main

Success:)

Система импорта может быть довольно запутанной, тратя некоторое время на import system docs и importlib docs избавит вас от головной боли при возникновении подобной ситуации.

В качестве дополнительного предложения вы можете переместить module.py в experiment_23 и из subpackage и удалить subpackage. Теперь вы можете сделать from .dependency import function. Некоторые утверждают, что супер относительный импорт (с использованием более одного .) является плохой практикой или, по крайней мере, демонстрирует недостаточное понимание структурирования пакетов.

Редактировать: Все изменения, примененные к experiment_23, относятся к основному пакет эксперимента и все остальные experiment_n ...

...