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”