Що таке пітонічний спосіб введення залежності?


84

Вступ

Для Java Dependency Injection працює як чистий ООП, тобто ви надаєте інтерфейс, який має бути реалізований, і у своєму коді фреймворку приймаєте екземпляр класу, який реалізує визначений інтерфейс.

Що стосується 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(...)

Питання

Питання коротке. Чи існує кращий, загальновживаний пітонічний спосіб зробити це? А може, якісь бібліотеки, що підтримують таку функціональність?

ОНОВЛЕННЯ: Конкретна ситуація

Уявіть, я розробляю мікро-веб-фреймворк, який обробляє аутентифікацію за допомогою маркерів. Цей фреймворк потребує функції, щоб забезпечити частину IDотриманого з маркера та отримати відповідного йому користувачаID .

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


2
Чому б вам не "надати інтерфейс, який має бути реалізований, і у своєму коді фреймворку не прийняти екземпляр класу, який реалізує визначений інтерфейс" ? У Python ви робите це в стилі EAFP (тобто припустимо, що він відповідає цьому інтерфейсу і AttributeErrorабо TypeErrorпіднімається інакше), але в іншому випадку це те саме.
jonrsharpe

Це легко зробити, використовуючи метаклас abs'' ABCMetaз @abstractmethodдекоратором, і відсутність ручної перевірки. Просто хочу отримати кілька варіантів та пропозицій. Той, який ви цитували, є найбільш чистим, але я думаю, що з більшими витратами.
bagrat

Тоді я не знаю, яке питання ви намагаєтеся задати.
jonrsharpe

Гаразд, я спробую іншими словами. Проблема зрозуміла. Питання в тому, як це зробити пітонічним способом. Варіант 1 : Те, як ви цитували, Варіант 2 : Основний підхід, який я описав у питанні. Тож питання в тому, чи існують інші пітонічні способи зробити це?
bagrat

Відповіді:


66

Дивіться Реймонда Хеттінгера - супер вважається супер! - 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():)
bagrat

2
Реймонд назвав це CI, хоча, я думав, це чистий міксин. Але чи може бути, що в Python міксин і CI практично однакові? Різниця лише в рівні індексації. Mixin вводить залежність у рівень класу, тоді як CI вводить залежність в екземпляр.
nad2000

1
Я думаю, що ін'єкцію рівня конструктора в Python зробити досить просто, як, наприклад, як це описав OP. цей пітонічний спосіб виглядає дуже цікавим. для цього просто потрібно трохи більше проводки, ніж просте введення в конструктор IMO.
stucash

6
Хоча я вважаю це дуже елегантним, у мене є дві проблеми з таким підходом: 1. Що трапляється, коли вам потрібно вводити серверні предмети у ваш клас? 2. Спадщина найчастіше використовується у сенсі "є" / спеціалізація. Використання його для DI заперечує цю ідею (наприклад, якщо я хочу ввести Службу у Ведучого).
AljoSt

18

У нашому проекті ми виконуємо ін’єкцію залежностей за допомогою ін’єкційної бібліотеки. Перевірте документацію . Я настійно рекомендую використовувати його для 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 певну підтримку DI? І відповідь така, на жаль: ні.


Дякую за відповідь, здається досить цікавим. Я огляну частину декораторів. Тим часом, давайте чекатимемо більше відповідей.
баграт

Дякуємо за посилання на бібліотеку 'inject'. Це найближче, що я знайшов до цього часу, щоб заповнити прогалини, які я хотів заповнити DI - і бонус, це насправді підтримується!
Енді Мортімер,

13

Деякий час тому я написав мікрофреймворк із введенням залежностей, маючи на меті зробити його 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

Сподіваюся, це може трохи допомогти. Для отримання додаткової інформації відвідайте:


Дякую @ Роман Могилатов. Мені цікаво дізнатись, як ви налаштовуєте / адаптуєте ці контейнери під час виконання, скажімо з конфігураційного файлу. Здається, ці залежності жорстко закодовані в даному контейнері ( Platformі Services). Чи є рішення створити новий контейнер для кожної комбінації класів бібліотеки для ін’єкцій?
Bill DeRose

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

2

Я думаю, що DI та, можливо, AOP зазвичай не вважаються 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

І ось вихідний код


2

Введення залежності - це проста техніка, яку 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клас і всі посилання на нього, код все одно працює.


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 ви визначаєте мою функцію

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

Звичайно, ви також можете використовувати клас MyClass iso. функція. Якщо ви визначаєте значення імені методу у файлі settings.py, ви можете завантажувати різні версії імені методу залежно від значення файлу налаштувань. Django використовує таку схему для визначення з'єднання з базою даних.


1

Завдяки реалізації Python OOP, 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

0

Погравшись з деякими фреймворками 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.