Что такое питонический способ внедрения зависимостей?


84

Введение

Для Java внедрение зависимостей работает как чистое ООП, т. Е. Вы предоставляете интерфейс, который будет реализован, и в своем коде фреймворка принимаете экземпляр класса, реализующего определенный интерфейс.

Теперь для Python вы можете сделать то же самое, но я думаю, что в случае с Python этот метод был слишком накладным. Итак, как бы вы реализовали это питоническим способом?

Пример использования

Скажем, это код фреймворка:

class FrameworkClass():
    def __init__(self, ...):
        ...

    def do_the_job(self, ...):
        # some stuff
        # depending on some external function

Основной подход

Самый наивный (и, может быть, лучший?) Способ - потребовать, чтобы внешняя функция была передана в FrameworkClassконструктор, а затем вызывалась из do_the_jobметода.

Рамочный код:

class FrameworkClass():
    def __init__(self, func):
        self.func = func

    def do_the_job(self, ...):
        # some stuff
        self.func(...)

Код клиента:

def my_func():
    # my implementation

framework_instance = FrameworkClass(my_func)
framework_instance.do_the_job(...)

Вопрос

Вопрос короткий. Есть ли какой-нибудь лучший способ сделать это с помощью Pythonic? Или, может быть, какие-то библиотеки, поддерживающие такую ​​функциональность?

ОБНОВЛЕНИЕ: конкретная ситуация

Представьте, что я разрабатываю микро-веб-фреймворк, который обрабатывает аутентификацию с помощью токенов. Этому фреймворку нужна функция для предоставления некоторых, IDполученных из токена, и получения пользователя, соответствующего этомуID .

Очевидно, что фреймворк ничего не знает о пользователях или какой-либо другой логике приложения, поэтому клиентский код должен внедрить в фреймворк функциональность получателя пользователя, чтобы аутентификация работала.


2
Почему бы вам не «предоставить интерфейс, который будет реализован, и в коде вашего фреймворка не принять экземпляр класса, реализующего определенный интерфейс» ? В Python вы бы сделали это в стиле EAFP (т.е. предположим, что он соответствует этому интерфейсу, а в противном случае возникает AttributeErrorили TypeError), но в остальном это то же самое.
jonrsharpe

Легко сделать, используя abs«s ABCMetaметакласса с @abstractmethodдекоратором, и никакой ручной проверки. Просто хочу получить пару вариантов и предложений. Тот, который вы процитировали, является наиболее чистым, но я думаю, что он требует дополнительных затрат.
Баграт

Тогда я не знаю, какой вопрос вы пытаетесь задать.
jonrsharpe

Хорошо, попробую другими словами. Проблема ясна. Вопрос в том, как это сделать питоническим способом. Вариант 1 : Как вы процитировали, Вариант 2 : Базовый подход, который я описал в вопросе. Итак, вопрос в том, есть ли другие способы Pythonic для этого?
Баграт

Ответы:


66

См. Raymond Hettinger - Супер считается супер! - PyCon 2015 за аргумент о том, как использовать супер- и множественное наследование вместо DI. Если у вас нет времени на просмотр всего видео, перейдите к 15 минуте (но я бы рекомендовал посмотреть все это).

Вот пример того, как применить то, что описано в этом видео, к вашему примеру:

Рамочный код:

class TokenInterface():
    def getUserFromToken(self, token):
        raise NotImplementedError

class FrameworkClass(TokenInterface):
    def do_the_job(self, ...):
        # some stuff
        self.user = super().getUserFromToken(...)

Код клиента:

class SQLUserFromToken(TokenInterface):
    def getUserFromToken(self, token):      
        # load the user from the database
        return user

class ClientFrameworkClass(FrameworkClass, SQLUserFromToken):
    pass

framework_instance = ClientFrameworkClass()
framework_instance.do_the_job(...)

Это будет работать, потому что Python MRO гарантирует, что вызывается клиентский метод getUserFromToken (если используется super ()). Код придется изменить, если вы используете Python 2.x.

Дополнительным преимуществом здесь является то, что это вызовет исключение, если клиент не предоставит реализацию.

Конечно, это не совсем инъекция зависимостей, это множественное наследование и миксины, но это питонический способ решения вашей проблемы.


10
Этот ответ считается super():)
Баграт

2
Раймонд назвал его CI, а я подумал, что это чистый миксин. Но может ли быть так, что миксин и CI в Python практически одинаковы? Единственная разница - это уровень индексов. Mixin внедряет зависимость на уровень класса, в то время как CI внедряет зависимость в экземпляр.
nad2000

1
Я думаю, что внедрение на уровне конструктора в любом случае довольно просто сделать в Python, как это описал OP. однако этот питонический способ выглядит очень интересным. это просто требует немного больше проводки, чем простая инъекция конструктора IMO.
stucash 01

6
Хотя я считаю его очень элегантным, у меня есть две проблемы с этим подходом: 1. Что происходит, когда вам нужно добавить несколько элементов в ваш класс? 2. Наследование чаще всего используется в смысле "есть" / специализация. Использование его для DI противоречит этой идее (например, если я хочу внедрить службу в Presenter).
AljoSt

18

Внедрение зависимостей в нашем проекте выполняется с помощью inject lib. Ознакомьтесь с документацией . Я очень рекомендую использовать его для DI. Это не имеет смысла с одной функцией, но начинает иметь смысл, когда вам нужно управлять несколькими источниками данных и т. Д. И т. Д.

Следуя вашему примеру, это может быть что-то вроде:

# framework.py
class FrameworkClass():
    def __init__(self, func):
        self.func = func

    def do_the_job(self):
        # some stuff
        self.func()

Ваша пользовательская функция:

# my_stuff.py
def my_func():
    print('aww yiss')

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

# bootstrap.py
import inject
from .my_stuff import my_func

def configure_injection(binder):
    binder.bind(FrameworkClass, FrameworkClass(my_func))

inject.configure(configure_injection)

А затем вы можете использовать код следующим образом:

# some_module.py (has to be loaded with bootstrap.py already loaded somewhere in your app)
import inject
from .framework import FrameworkClass

framework_instance = inject.instance(FrameworkClass)
framework_instance.do_the_job()

Я боюсь, что это настолько питонический, насколько это возможно (модуль имеет некоторую сладость Python, например, декораторы для вставки по параметру и т. Д. - проверьте документацию), поскольку у python нет причудливых вещей, таких как интерфейсы или подсказки типов.

Так что ответить на ваш вопрос напрямую будет очень сложно. Я думаю, что истинный вопрос: есть ли в Python некоторая встроенная поддержка DI? К сожалению, ответ будет отрицательным.


Спасибо за ответ, кажется довольно интересным. Я проверю часть декораторов. А пока давайте подождем новых ответов.
Баграт

Спасибо за ссылку на библиотеку inject. Это самое близкое, что я нашел до сих пор, чтобы заполнить пробелы, которые я хотел заполнить с помощью DI - и бонус, он фактически поддерживается!
Энди Мортимер

14

Некоторое время назад я написал микрофреймворк для внедрения зависимостей с амбициями сделать его Pythonic - Dependency Injector . Вот как может выглядеть ваш код в случае его использования:

"""Example of dependency injection in Python."""

import logging
import sqlite3

import boto.s3.connection

import example.main
import example.services

import dependency_injector.containers as containers
import dependency_injector.providers as providers


class Platform(containers.DeclarativeContainer):
    """IoC container of platform service providers."""

    logger = providers.Singleton(logging.Logger, name='example')

    database = providers.Singleton(sqlite3.connect, ':memory:')

    s3 = providers.Singleton(boto.s3.connection.S3Connection,
                             aws_access_key_id='KEY',
                             aws_secret_access_key='SECRET')


class Services(containers.DeclarativeContainer):
    """IoC container of business service providers."""

    users = providers.Factory(example.services.UsersService,
                              logger=Platform.logger,
                              db=Platform.database)

    auth = providers.Factory(example.services.AuthService,
                             logger=Platform.logger,
                             db=Platform.database,
                             token_ttl=3600)

    photos = providers.Factory(example.services.PhotosService,
                               logger=Platform.logger,
                               db=Platform.database,
                               s3=Platform.s3)


class Application(containers.DeclarativeContainer):
    """IoC container of application component providers."""

    main = providers.Callable(example.main.main,
                              users_service=Services.users,
                              auth_service=Services.auth,
                              photos_service=Services.photos)

Вот ссылка на более подробное описание этого примера - http://python-dependency-injector.ets-labs.org/examples/services_miniapp.html

Надеюсь, это немного поможет. Для получения дополнительной информации, пожалуйста, посетите:


Спасибо @Roman Mogylatov. Мне любопытно узнать, как вы настраиваете / адаптируете эти контейнеры во время выполнения, скажем, из файла конфигурации. Похоже, что эти зависимости жестко запрограммированы в данный контейнер ( Platformи Services). Есть ли решение создать новый контейнер для каждой комбинации классов вводимых библиотек?
Билл ДеРоуз

2
Привет, @BillDeRose. Хотя мой ответ считался слишком длинным для того, чтобы быть комментарием SO, я создал проблему с github и разместил свой ответ там - github.com/ets-labs/python-dependency-injector/issues/197 :) Надеюсь, это поможет, Спасибо, Роман
Роман Могилатов

3

Внедрение зависимостей - это простой метод, который Python поддерживает напрямую. Никаких дополнительных библиотек не требуется. Использование подсказок типа может улучшить ясность и читаемость.

Рамочный код:

class UserStore():
    """
    The base class for accessing a user's information.
    The client must extend this class and implement its methods.
    """
    def get_name(self, token):
        raise NotImplementedError

class WebFramework():
    def __init__(self, user_store: UserStore):
        self.user_store = user_store

    def greet_user(self, token):
        user_name = self.user_store.get_name(token)
        print(f'Good day to you, {user_name}!')

Код клиента:

class AlwaysMaryUser(UserStore):
    def get_name(self, token):      
        return 'Mary'

class SQLUserStore(UserStore):
    def __init__(self, db_params):
        self.db_params = db_params

    def get_name(self, token):
        # TODO: Implement the database lookup
        raise NotImplementedError

client = WebFramework(AlwaysMaryUser())
client.greet_user('user_token')

Подсказка UserStoreкласса и типа не требуется для реализации внедрения зависимостей. Их основная цель - дать рекомендации разработчику клиента. Если вы удалите UserStoreкласс и все ссылки на него, код все равно будет работать.


2

Я думаю, что DI и, возможно, АОП обычно не считаются Pythonic из-за типичных предпочтений разработчиков Python, а не из-за особенностей языка.

Фактически вы можете реализовать базовую структуру DI в <100 строк , используя метаклассы и декораторы классов.

Для менее инвазивного решения эти конструкции можно использовать для встраивания пользовательских реализаций в общую структуру.


2

Существует также Pinject, инжектор зависимостей Python с открытым исходным кодом от Google.

Вот пример

>>> class OuterClass(object):
...     def __init__(self, inner_class):
...         self.inner_class = inner_class
...
>>> class InnerClass(object):
...     def __init__(self):
...         self.forty_two = 42
...
>>> obj_graph = pinject.new_object_graph()
>>> outer_class = obj_graph.provide(OuterClass)
>>> print outer_class.inner_class.forty_two
42

А вот исходный код


1

Очень простой и питонический способ внедрения зависимостей - importlib.

Вы можете определить небольшую функцию полезности

def inject_method_from_module(modulename, methodname):
    """
    injects dynamically a method in a module
    """
    mod = importlib.import_module(modulename)
    return getattr(mod, methodname, None)

И тогда вы можете использовать это:

myfunction = inject_method_from_module("mypackage.mymodule", "myfunction")
myfunction("a")

В mypackage / mymodule.py вы определяете myfunction

def myfunction(s):
    print("myfunction in mypackage.mymodule called with parameter:", s)

Конечно, вы также можете использовать класс MyClass iso. функция myfunction. Если вы определяете значения имени метода в файле settings.py, вы можете загружать разные версии имени метода в зависимости от значения файла настроек. Django использует такую ​​схему для определения подключения к базе данных.


1

Из-за реализации ООП Python IoC и внедрение зависимостей не являются стандартной практикой в ​​мире Python. Но этот подход кажется многообещающим даже для Python.

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

Итак, мое решение :

# Framework internal
def MetaIoC(name, bases, namespace):
    cls = type("IoC{}".format(name), tuple(), namespace)
    return type(name, bases + (cls,), {})


# Entities level                                        
class Entity:
    def _lower_level_meth(self):
        raise NotImplementedError

    @property
    def entity_prop(self):
        return super(Entity, self)._lower_level_meth()


# Adapters level
class ImplementedEntity(Entity, metaclass=MetaIoC):          
    __private = 'private attribute value'                    

    def __init__(self, pub_attr):                            
        self.pub_attr = pub_attr                             

    def _lower_level_meth(self):                             
        print('{}\n{}'.format(self.pub_attr, self.__private))


# Infrastructure level                                       
if __name__ == '__main__':                                   
    ENTITY = ImplementedEntity('public attribute value')     
    ENTITY.entity_prop         

РЕДАКТИРОВАТЬ:

Будьте осторожны с выкройкой. Я использовал его в реальном проекте, и он показал себя не очень хорошо. Мой пост на Medium о моем опыте работы с узором.


Конечно, обычно используются IOC и DI, но обычно не используются фреймворки DI , к лучшему или к худшему.
juanpa.arrivillaga

1

Поигравшись с некоторыми фреймворками DI в python, я обнаружил, что их использование было немного неуклюжим при сравнении того, насколько это просто в других сферах, таких как .NET Core. В основном это происходит из-за объединения с помощью таких вещей, как декораторы, которые загромождают код и затрудняют его простое добавление или удаление из проекта, или объединение на основе имен переменных.

Недавно я работал над структурой внедрения зависимостей, которая вместо этого использует аннотации ввода для выполнения инъекции, называемой Simple-Injection. Ниже простой пример

from simple_injection import ServiceCollection


class Dependency:
    def hello(self):
        print("Hello from Dependency!")

class Service:
    def __init__(self, dependency: Dependency):
        self._dependency = dependency

    def hello(self):
        self._dependency.hello()

collection = ServiceCollection()
collection.add_transient(Dependency)
collection.add_transient(Service)

collection.resolve(Service).hello()
# Outputs: Hello from Dependency!

Эта библиотека поддерживает время жизни службы и привязку служб к реализациям.

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

Надеюсь это поможет. Для получения дополнительной информации см.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.