python
January 31

Динамическая загрузка модулей в Python и как она спасает при работе с pyspark

"Приобретение знаний - это как путешествие в неизведанные земли: чем больше вы исследуете, тем больше открытий вы делаете".

Библиотека importlib в Python предоставляет инструменты для динамической загрузки модулей. То есть она будет происходить не на этапе анализа кода интерпретатором, а во время выполнения программы. Это полезно, когда некоторые модули не известны до старта программы, например, как при работе с pyspark до инициализации переменных окружения с нужными путями.

Импорт модуля

Самый простой способ динамически загрузить модуль - использовать функцию import_module. Например, встроенный модуль math можно загрузить и использовать так:

import importlib
import sys

math_mod = importlib.import_module('math')
math_mod.log(2)

Как указывал выше, этот способ спасет при работе с pyspark, вот как импортируются наиболее часто используемые модули:

F = importlib.import_module('pyspark.sql.functions')
T = importlib.import_module('pyspark.sql.types')

Теперь рассмотрим пользовательский модуль следующего содержания:

def print_(s):
    print(s)
    
a = 1
b = 2

Для его загрузки понадобится такой код:

func_mod = importlib.import_module('pkg.func')

func_mod.a, func_mod.b
func_mod.print_('hi')

Так можно извлечь нужные нам переменные по некоторому правилу:

[k for k,v in vars(func_mod).items() if isinstance(v, int)]
vars(func_mod)['a']

Часто такое получение переменных применяется при тесте дагов Airflow (подробнее о vars можете прочитать тут):

from airflow import DAG
[k for k,v in vars(func_mod).items() if isinstance(v, DAG)]

Перезагрузка модуля

Если в нашем пользовательском модуле инициализировать дополнительную переменную (c), то потребуется перезагрузка модуля:

def print_(s):
    print(s)
    
a = 1
b = 2
c = 3

Иначе мы не сможем обратиться к переменной:

func_mod.c

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

func_mod = importlib.import_module('pkg.func')
func_mod.c

Это происходит из-за кеширования результатов загрузки модуля (подробнее смотри тут). Помочь может очистка специального словаря.

Так, для хранения информации о загруженных модулях используется словарь sys.modules. Когда модуль загружается, его имя добавляется в словарь. Это позволяет системе быстро определить, был ли модуль уже загружен или нет, чтобы избежать повторной загрузки и улучшить производительность.

Словарь sys.modules также может быть использован для управления процессом импорта, например, вы можете удалить оттуда запись о модуле, чтобы система его перезагрузила при очередном импорте:

sys.modules['pkg.func']

del sys.modules['pkg.func']

func_mod = importlib.import_module('pkg.func')
func_mod.c

С функцией reload можно произвести перезагрузку модуля без манипуляций с sys.modules. Добавим еще одну переменную в наш модуль:

def print_(s):
    print(s)
    
a = 1
b = 2
c = 3
d = 4

Как и ожидалось, сначала она не доступна:

func_mod.d

Однако после вызова reload все встает на свои места:

importlib.reload(func_mod)

func_mod.d

Спецификация модуля

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

Для получения объекта ModuleSpec, можно воспользоваться функцией find_spec модуля importlib.util:

spec = importlib.util.find_spec('pkg.func')
# spec = importlib.util.spec_from_file_location('func', 'pkg/func.py')
print(spec)
if spec is not None:
    print(f"Module name: {spec.name}")
    print(f"File location: {spec.origin}")
    print(f"Loader: {spec.loader}")

Так можно загрузить сам модуль по объекту спецификации:

func_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(func_mod)

Следует отметить, что этот способ не добавляет модуль в sys.modules:

sys.modules['pkg.func']

Однако ничто не мешает сделать это самим, если надо:

sys.modules['pkg.func'] = func_mod

sys.modules['pkg.func']