Рубрики
Без рубрики

Инъекция зависимости с импортными крючками в Python 3

Узнайте, как использовать `sys.meta_path`, чтобы написать простую в использовании структуру впрыска зависимостей. Теги с Python, MetaProgramming, зависимости.

sys.meta_path Является ли одним из самых продолжительных секретов в стандартной библиотеке Python, несмотря на то, что его прямо расположены в модуле, каждый программист Python знаком. Некоторые могут сказать, что секретность хорошо оправдана; Обычно не очень хорошая идея возиться с инструментами и функциями, которые могут подорвать ожидания программиста, как этот странно называемый список потенциально может. Но не «не» лучше, чем «не знаю?» Поэтому до быстрой грунтовки на зависимости впрыскиваем, давайте рассмотрим некоторые из них и ауты sys.meta_path Отказ

Всякий раз, когда вы используете Импорт Заявление, скажем, Импорт ОС Это функционально эквивалентно следующему коду:

os = __import__("os")

Это мягкое упрощение, поскольку вещи, такие как подмодулы, потребуют немного больше работы (как нам придется бороться с позже, на самом деле), но главный момент, который нужно сделать вот то, что __import__ Встроенный ли тяжелый подъем под капотом для импорта модуля в Python, и теперь мы знаем, что он существует, мы можем изучить его контрольный поток. Во-первых, это проверяет Sys.modules , Dict, содержащий уже загруженные модули, на случай, если мы уже импортировали запрошенный модуль до. Далее это проверяет sys.path – список, содержащий пути файловых систем для Python для поиска для потенциальных модулей; Вы, вероятно, знаете об этом списке уже. Однако, если sys.meta_path Список не пусто, чем до Глядя через sys.path каждая мета-импортер крюк в sys.meta_path Запрошено до проверки файловой системы, давая нам простой способ перехватить импорт модуля и делать все виды причудливых чудесных вещей, таких как инъекция зависимости.

Так что такое впрыск зависимости? Это примерно прикладная версия Принцип инверсии зависимости , часто для вещей размером модулей. Скажем, ваше приложение имеет несколько различных передних концов, может быть нацелена на разные платформы или наборы пользовательских интернет-пользователей. Пока у вас есть общий интерфейс, инъекция зависимости позволит вам сделать что-то вроде:

user_info = info_getter.get_user_info()
frontend = dependency_injector.get("frontend")
frontend.display_user_info(user_info)

“И что? Я могу сделать это уже с фасадом или что-то в этом роде, может быть, конфигурационный коммутатор: «Я слышу, как вы говорите. Для очень простых случаев использования, которые на самом деле могут быть лучше – просто лучше, чем сложный, помните? Однако с инъекцией зависимости мы можем легко обеспечить повторное использование уровня модуля, а также значительно сократить на котельной. Что, если я сказал вам, к концу этой статьи вы могли бы написать что-то вроде этого:

import myapp.dependency.frontend as frontend
popup = frontend.Popup(popup_style=frontend.ERROR)
popup.display()

Один импорт, который работает независимо от того, используете ли мы использование GTK +, QT, NCurses или независимо от вашей программы. Вы можете думать о некоторых очень темных мыслях в этот момент. Это правда, всевозможные темные магии могут быть выполнены в зависимости от деталей наших импортных крючков. Но читаемость считается, поэтому важно, чтобы мы не были слишком далеко от проторенного пути. Таким образом, давайте устанавливаем некоторые требования к нашей структуре инъекции зависимости перед написанием нашей реализации:

  • Все наличие импортных путей введения зависимостей начинаются с myapp.virtual.
  • Предоставленные зависимости известны заранее и зарегистрированы в A обеспечить метод

Все волшебство – и там не так много – полагается почти полностью на создание Finder Meta Path и добавлением его в sys.meta_path Отказ Finder Meta Path – это очень простой объект с одним публичным методом, find_spec :

import importlib.abc
import importlib.machinery

class DependencyInjectorFinder(importlib.abc.MetaPathFinder):
    def __init__(self, loader):
        # we'll write the loader in a minute, hang tight
        self._loader = loader
    def find_spec(self, fullname, path, target=None):
        """Attempt to locate the requested module
        fullname is the fully-qualified name of the module,
        path is set to __path__ for sub-modules/packages, or None otherwise.
        target can be a module object, but is unused in this example.
        """
        if self._loader.provides(fullname):
            return self._gen_spec(fullname)
    def _gen_spec(self, fullname):
        spec = importlib.machinery.ModuleSpec(fullname, self._loader)
        return spec
# we'll also add it to sys.meta_path later

Если Finder Meta Path предоставляет запрошенный модуль, то он должен вернуть экземпляр ImportLib.machinery. Модульсфункция Класс, который является довольно простым делом с небольшой горсткой атрибутов, которые позволяют импортировать машины Python знать, что нужно знать, чтобы принять следующие шаги в импорте модуля, который пользователь запрашивается. Для наших целей мы заинтересованы в двух атрибутах (только необходимые): ModuleSpec.name , что название запрошенного модуля и ModuleSpec.loader , который является объектом погрузчика, который Python должен использовать для фактического загрузки модуля – вы заметите Self._Loader Линии выше, которые ссылаются на объект погрузчика, а также. Объект Loader – это очень простой класс с двумя необходимыми методами в современном Python (3.4 внутрь): create_module , который занимает Модульсфункция В качестве своего единственного аргумента и возвращает объект, который Python будет рассмотрен новым модулем, а exec_module , который принимает новый модуль в качестве единственного аргумента и выполняет его. Так что не-OP, Barebones Loader выглядит что-то подобное:

class Loader(importlib.abc.Loader):
    def create_module(self, spec):
        raise NotImplementedError
    def exec_module(self, module):
        raise NotImplementedError

В дополнение к этим двум методам, мы должны реализовать обеспечить Метод, о котором мы говорили ранее, чтобы сигнализировать, что определенная зависимость предоставляется, в дополнение к Обеспечивает Метод, который наш Finder ссылается ранее, что указывает, является ли запрашиваемый модуль частью нашей структуры инъекций зависимостей. Вот реализация:

import importlib.abc
import sys
import types


class DependencyInjectorLoader(importlib.abc.Loader):
    _COMMON_PREFIX = "myapp.virtual."
    def __init__(self):
        self._services = {}
        # create a dummy module to return when Python attempts to import
        # myapp and myapp.virtual, the :-1 removes the last "." for
        # aesthetic reasons :) 
        self._dummy_module = types.ModuleType(self._COMMON_PREFIX[:-1])
        # set __path__ so Python believes our dummy module is a package
        # this is important, since otherwise Python will believe our
        # dummy module can have no submodules
        self._dummy_module.__path__ = []
    def provide(self, service_name, module):
        """Register a service as provided via the given module
        A service is any Python object in this context - an imported module,
        a class, etc."""
        self._services[service_name] = module
    def provides(self, fullname):
        if self._truncate_name(fullname) in self._services:
            return True
        else:
            # this checks if we should return the dummy module,
            # since this evaluates to True when importing myapp and
            # myapp.virtual
            return self._COMMON_PREFIX.startswith(fullname)
    def create_module(self, spec):
        """Create the given module from the supplied module spec
        Under the hood, this module returns a service or a dummy module,
        depending on whether Python is still importing one of the names listed
        in _COMMON_PREFIX.
        """
        service_name = self._truncate_name(spec.name)
        if service_name not in self._services:
            # return our dummy module since at this point we're loading
            # *something* along the lines of "myapp.virtual" that's not
            # a service
            return self._dummy_module
        module = self._services[service_name]
        return module
    def exec_module(self, module):
        """Execute the given module in its own namespace
        This method is required to be present by importlib.abc.Loader,
        but since we know our module object is already fully-formed,
        this method merely no-ops.
        """
        pass
    def _truncate_name(self, fullname):
        """Strip off _COMMON_PREFIX from the given module name
        Convenience method when checking if a service is provided.
        """
        return fullname[len(self._COMMON_PREFIX):]

Здесь, по-видимому, здесь много кода. В первую очередь есть обеспечить и Обеспечивает методы, которые мы говорили ранее; Там нет много магии, присутствующего в одном из них. create_module Возвращает так называемый макетный модуль, если мы пытаемся импортировать либо MyApp или myapp.virtual И есть хорошая причина для этого. Скажем, у нас есть следующая строка кода:

import myapp.virtual.frontend

Под капотом это генерирует три различных поиска в импортной машине Python: Один для MyApp , другой для myapp.virtual и, наконец, один для myapp.virtual.frontend . Так как явно MyApp и myapp.virtual На самом деле не существует нигде в системе, но Python будет жаловаться, если они не будут загружены, мы претендуем, мы предоставляем обоих – обратите внимание, как Обеспечивает вернется Правда При запросе для обоих MyApp и myapp.virtual И верните пустой манекет, чтобы успокоить импортное оборудование Python. Только когда мы сталкиваемся myapp.virtual.frontend Проверим ли мы к списку предоставленных услуг в зависимости от инъекций.

На данный момент мы фактически написали все сантехники, которые нам нужно, так что давайте окупаемся и посмотрим, как мы использовали эти классы на практике:

import sys


class DependencyInjector:
    """
    Convenience wrapper for DependencyInjectorLoader and DependencyInjectorFinder.
    """
    def __init__(self):
        self._loader = DependencyInjectorLoader()
        self._finder = DependencyInjectorFinder(self._loader)
    def install(self):
        sys.meta_path.append(self._finder)
    def provide(self, service_name, module):
        self._loader.provide(service_name, module)


class FrontendModule:
    class Popup:
        def __init__(self, message):
            self._message = message
        def display(self):
            print("Popup:", self._message)


if __name__ == "__main__":
    injector = DependencyInjector()
    # we could install() and then provide() if we wanted, order isn't
    # important for the below two calls
    injector.provide("frontend", FrontendModule())
    injector.install()
    # note that these last three lines could exist in any other source file,
    # as long as injector.install() was called somewhere first
    import myapp.virtual.frontend as frontend
    popup = frontend.Popup("Hello World!")
    popup.display()

На этом этапе это стоило бы сделать шаг назад и изучить на высоком уровне, что происходит за кулисами:

  1. Вызов Инжектор. Вставьте добавляет нашу ЗависитностьинжекторFinder на sys.meta_path Список, поэтому Python будет использовать его для будущего импорта.
  2. Линия Импортировать myapp.virtual.frontend как frontend Триггеры три поиска модуля, как упоминалось ранее – один для MyApp , тогда myapp.virtual Тогда myapp.virtual.frontend :

    • Python Combs через sys.meta_path Ищете Meta Path Finders. Если ваша система Python является чем-то вроде моей, наш ЗависитностьинжекторFinder будет единственный, кто не определяется _ Frozen_importLib , что является частью самой питона.
    • Для каждого обнаружили Meta Path Finder, Python запросит Finder’s find_spec Метод, видя, если Finder предоставляет данный модуль.
    • Очевидно myapp.virtual.frontend Не существует на файловой системе, поэтому он падает на наш Finder Meta Path для обработки его. Во всех трех случаях мы возвращаем Модульсфункция экземпляр с Имя Установите к тому же названию, который Питон спросил, если мы сможем найти, и погрузчик Установить на наш пользовательский ЗависитностьНинжекторный загрузчик Отказ
    • Далее Python позвонит create_module Метод на нашем погрузчике для Модульсфункция обсуждаемый. Для MyApp и myapp.virtual Наш загрузчик распознает, что это фиктивные модули и возвращает один и тот же модуль фиктивного модуля для обоих (обманных питонов в веру в модуль был загружен), но возвращает экземпляр FrontendModule Мы дали это с Инжектор. Производитель () После того, как попросили загрузить myapp.virtual.frontend Отказ Python позволяет любому действительному объекту функционировать в качестве модуля, поэтому простой старый класс совершенно нормально, чтобы вернуться.
    • Python наконец-то позвонит exec_module Метод на нашем погрузчике, передавая его объект, который мы вернулись из create_module Отказ Python требует exec_module метод присутствовать, но на самом деле не заботится о его поведении; Обычно метод будет выполнять код модуля в новом пространстве имен, но поскольку у нас уже есть полностью сформированные модули, готовые к работе – либо наш предварительно сделанный модуль для фирма или нашего FrontendModule Экземпляр – мы просто ничего не делаем внутри exec_module Отказ
    • Вымойте, ополаскивайте и повторите для каждого поиска последовательного модуля.
  3. После всего этого, внешний интерфейс. Всплывающее окно функционально то же самое, что и FrontendModule. Всплывающее окно , А в остальном, как говорится, уже история.

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

Наконец, полный пример кода подходит как Гист на Github Если вы хотите что-то, что вы можете скачать и пореваться. Счастливое зацепление!

Оригинал: “https://dev.to/dangerontheranger/dependency-injection-with-import-hooks-in-python-3-5hap”