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()
На этом этапе это стоило бы сделать шаг назад и изучить на высоком уровне, что происходит за кулисами:
- Вызов
Инжектор. Вставьте
добавляет нашуЗависитностьинжекторFinder
наsys.meta_path
Список, поэтому Python будет использовать его для будущего импорта. Линия
Импортировать 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
Отказ - Вымойте, ополаскивайте и повторите для каждого поиска последовательного модуля.
- Python Combs через
- После всего этого,
внешний интерфейс. Всплывающее окно
функционально то же самое, что иFrontendModule. Всплывающее окно
, А в остальном, как говорится, уже история.
Как я уже упоминал ранее в статье, важно не злоупотреблять языковыми функциями, такими – с большой силой приходит большая ответственность, в конце концов – и это особенно звонит истина, когда речь идет о метапрограмме. Тем не менее, есть время и место для всего, включая импортные крюки; Но, пожалуйста, используйте их ответственно.
Наконец, полный пример кода подходит как Гист на Github Если вы хотите что-то, что вы можете скачать и пореваться. Счастливое зацепление!
Оригинал: “https://dev.to/dangerontheranger/dependency-injection-with-import-hooks-in-python-3-5hap”