Как вызвать поздний импорт другого модуля, только если из моего модуля требуется определенный объект? - PullRequest
0 голосов
/ 31 мая 2019

Ситуация

Я хочу иметь модуль, который примерно работает следующим образом:

# my_module.py

my_number = 17

from other_module import foo
my_object = foo(23)

Однако существует проблема: установка other_module вызывает проблемы для некоторых пользователей и требуется только для тех, кто хочет использовать my_object - что, в свою очередь, составляет лишь небольшую часть пользователей. Я хочу избавить тех пользователей, которым не нужно my_object от установки other_module.

Поэтому я хочу, чтобы импорт other_module происходил только в том случае, если my_object импортируется из my_module. Другими словами, пользователь должен иметь возможность запускать следующее без установки other_module:

from my_module import my_number

Мое лучшее решение на данный момент

Я мог бы предоставить my_object через функцию, которая содержит импорт:

# in my_module.py

def get_my_object():
    from other_module import foo
    my_object = foo(23)
    return my_object

Пользователь должен будет сделать что-то вроде:

from my_module import get_my_object
my_object = get_my_object()

Вопрос

Есть ли лучший способ условно инициировать импорт other_module? Я в основном заинтересован в том, чтобы все было максимально просто для пользователей.

Ответы [ 3 ]

2 голосов
/ 31 мая 2019

Я бы предпочел подход get_my_object(), но в Python 3.7 то, что вы просите, возможно, определив уровень модуля __getattr__ функция :

# my_module.py

my_number = 17

def __getattr__(name):
    if name != 'my_object':
        raise AttributeError
    global my_object
    from other_module import foo
    my_object = foo(23)
    return my_object

Этобудет пытаться импортировать other_module и вызывать foo только после получения доступа к my_object.Несколько предостережений:

  • Будет не инициировать попытки доступа к my_object с помощью поиска глобальной переменной в my_module.Он будет срабатывать только при доступе к атрибуту my_module.my_object или при импорте from my_module import my_object (который осуществляет доступ к атрибутам изнутри).
  • Если вы забудете назначить глобальное имя my_object в __getattr__,my_object будет пересчитываться при каждом доступе.
  • Уровень модуля __getattr__ ничего не делает до Python 3.7, поэтому вы можете захотеть выполнить проверку версии и сделать что-то еще для Python 3.6 и ниже:

    import sys
    if sys.version_info >= (3, 7):
        def __getattr__(name):
            ...
    else:
        # Do something appropriate. Maybe raise an error. Maybe unconditionally
        # import other_module and compute my_object up front.
    
1 голос
/ 31 мая 2019

Это обычный хак, который нужно сделать:

import sys

class MockModule:
    def __init__(self, module):
        self.module = module

    def __getattr__(self, attr):
        if attr == 'dependency_required_var':
            try:
                import foo
                return self.module.dependency_required_var
            except ImportError:
                raise Exception('foo library is required to use dependency_required_var')
        else:
            return getattr(self.module, attr)


MockModule.__name__ = __name__
sys.modules[__name__] = MockModule(sys.modules[__name__])

dependency_required_var = 0

С этим PEP мы можем просто сделать (мы должны быть в состоянии, но я не мог заставить его работать) следующее в Python 3.7 и выше:

def __getattr__(attr):
    if attr == 'dependency_required_var':
        try:
            import foo
            return dependency_required_var
        except ImportError:
            raise Exception('foo library is required to use dependency_required_var')
    else:
        return globals()[attr]

Кажется, что PEP принят, но соответствующий запрос извлечения из PEP закрыт, я на самом деле не уверен, был ли он реализован илине.

1 голос
/ 31 мая 2019

Подход A - чистый раствор

Создайте новый отдельный модуль и попросите пользователя импортировать объект из другого модуля. Например

from my_module import my_number # regular use
from my_module.extras import my_object # for a small part of the user base

Это означает, что в вашем коде вы создаете папку модуля my_module с __init__.py, куда вы импортируете обычные вещи и не импортируете субмодуль extras.

Если вы не хотите помещать extras в my_module (проще), просто создайте my_object в отдельном модуле extras.py.

Подход B - сигнализирует о плохой архитектуре 99% раз

Вы можете использовать importlib.import_module для динамического импорта модуля внутри get_my_object, не загрязняя глобальное пространство, и это на чище, чем импорт внутри функции, которая создает побочные эффекты, такие как переопределение вашей глобальной переменной с этим именем импорта (см. на этот вопрос и ответы ), однако это обычно признак неправильных шаблонов кодирования в другой части кода.

Подход C - просто и эффективно

Я обычно склоняюсь к этому простому шаблону, когда есть пользователи, у которых, возможно, нет библиотеки, поскольку Python 3 не поддерживает импорт, который не находится на верхнем уровне:

try:
    import other_module
    _HAS_OTHER_MODULE_ = True
except:
    _HAS_OTHER_MODULE_ = False


def get_my_object():
    assert _HAS_OTHER_MODULE_, "Please install other module"
    return other_module.foo(23)
...