Исправление атрибутов модуля при импорте из расположения файла - PullRequest
0 голосов
/ 28 мая 2020

Рассмотрим три модуля a.py, b.py и main.py. Каждый из них может находиться в произвольном месте, но для примера все они будут в одном каталоге.

Модуль a загружает b, указав его путь с помощью importlib.util.spec_from_file_location и main делает то же самое с a.

# b.py
eggs = 42
# a.py
import importlib.util
import os

B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')  # could be an arbitrary path
spec = importlib.util.spec_from_file_location('b', B_PATH)
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)


spam = "ham"
print(f"{__name__} says {b.eggs=}")
# main.py
import importlib.util
import os

A_PATH = os.path.join(os.path.dirname(__file__), 'a.py')  # could be an arbitrary path
spec = importlib.util.spec_from_file_location('a', A_PATH)
a = importlib.util.module_from_spec(spec)
spec.loader.exec_module(a)


print(f"{__name__} says {a.spam=}")

Когда мы запускаем main.py:

$ python main.py
a says b.eggs=42
__main__ says a.spam='ham'

Как я могу исправить b.eggs из main.py, чтобы a.py видел исправленное значение? Загрузка b из местоположения файла и исправление b.eggs перед загрузкой a не работает:

# main.py v2
import importlib.util
import os

B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')
spec = importlib.util.spec_from_file_location('b', B_PATH)
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)

b.eggs = "patched"

A_PATH = os.path.join(os.path.dirname(__file__), 'a.py')
spec = importlib.util.spec_from_file_location('a', A_PATH)
a = importlib.util.module_from_spec(spec)
spec.loader.exec_module(a)


print(f"{__name__} says {a.spam=}")
print(f"{__name__} says {b.eggs=}")
$ python main.py
a says b.eggs=42
__main__ says a.spam='ham'
__main__ says b.eggs='patched'

Как я могу сделать это, не исправляя фактический исходный код b ?


(Обратите внимание, что в этом примере, поскольку все модули находятся в одном каталоге, импорт из пути к файлу глуп, так как это может быть выполнено с помощью оператора import, но это просто чтобы сделать минимальный рабочий пример. В моем реальном случае мои модули находятся в разных каталогах, и я не могу изменить переменную среды PYTHONPATH, поэтому я делаю это так.)

1 Ответ

0 голосов
/ 10 июня 2020

Импорт модулей из источника, как показано, не проверяет и не обновляет sys.modules, так как не использует __import__. В справочнике по языку говорится: :

Может произойти прямой вызов __import__() [...] определенных побочных эффектов, таких как импорт родительских пакетов и обновление различных кешей ( включая sys.modules ), [...]

Таким образом, чтобы a и main видели один и тот же объект модуля, нам нужно для проверки и обновления вручную sys.modules. Таким образом, для загрузки модулей с их пути мы могли бы использовать эту функцию:

import sys, types

def load_from_path(name: str, path: str) -> types.ModuleType:
    if name not in sys.modules:
        spec = importlib.util.spec_from_file_location(name, path)
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        sys.modules[name] = mod
    return sys.modules[name]

Таким образом, приведенный выше пример становится (предположим, что load_from_path определен во всех модулях):

# b.py
eggs = 42
# a.py
import importlib.util
import os

B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')
b = load_from_path('b', B_PATH)


spam = "ham"
print(f"{__name__} says {b.eggs=}")
# main.py
import importlib.util
import os

A_PATH = os.path.join(os.path.dirname(__file__), 'a.py')
B_PATH = os.path.join(os.path.dirname(__file__), 'b.py')

b = load_from_path('b', B_PATH)
b.eggs = "patched"
a = load_from_path('a', A_PATH)


print(f"{__name__} says {a.spam=}")
print(f"{__name__} says {b.eggs=}")

Результат:

a says b.eggs='patched'
__main__ says a.spam='ham'
__main__ says b.eggs='patched'
...