Jaki jest Pythonic sposób na wstrzykiwanie zależności?

84

Wprowadzenie

W przypadku języka Java Dependency Injection działa jako czysty OOP, tj. Udostępniasz interfejs do zaimplementowania, aw kodzie struktury akceptujesz instancję klasy, która implementuje zdefiniowany interfejs.

Teraz w przypadku Pythona możesz zrobić to samo, ale myślę, że ta metoda była zbyt dużym narzutem w przypadku Pythona. Jak więc zaimplementowałbyś to w Pythonie?

Przypadek użycia

Powiedz, że to jest kod ramowy:

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

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

Podejście podstawowe

Najbardziej naiwnym (a może najlepszym?) Sposobem jest wymaganie, aby funkcja zewnętrzna została dostarczona do FrameworkClasskonstruktora, a następnie wywoływana z do_the_jobmetody.

Kod ramowy:

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

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

Kod klienta:

def my_func():
    # my implementation

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

Pytanie

Pytanie jest krótkie. Czy istnieje lepszy, powszechnie używany sposób w Pythonie, aby to zrobić? A może biblioteki obsługujące taką funkcjonalność?

AKTUALIZACJA: konkretna sytuacja

Wyobraź sobie, że tworzę mikro framework sieciowy, który obsługuje uwierzytelnianie za pomocą tokenów. Ta struktura wymaga funkcji, która będzie dostarczać część IDuzyskaną z tokenu i pobierać użytkownika odpowiadającego temu ID.

Oczywiście framework nie wie nic o użytkownikach ani żadnej innej logice specyficznej dla aplikacji, więc kod klienta musi wstrzyknąć do frameworka funkcję pobierającą użytkownika, aby uwierzytelnianie działało.

bagrat
źródło
2
Dlaczego nie „udostępnisz interfejsu do zaimplementowania i nie zaakceptujesz instancji klasy, która implementuje zdefiniowany interfejs w kodzie frameworka” ? W Pythonie zrobiłbyś to w stylu EAFP (tj. Załóżmy, że spotyka się z tym interfejsem i AttributeErrorinaczej TypeErrorzostanie podniesiony lub), ale poza tym jest tak samo.
jonrsharpe
Łatwo jest to zrobić używając abs„s ABCMetametaklasa z @abstractmethoddekoratora i bez ręcznego sprawdzania poprawności. Chcę tylko uzyskać kilka opcji i sugestii. Ten, który zacytowałeś, jest najczystszy, ale myślę, że jest większy.
bagrat
Więc nie wiem, jakie pytanie próbujesz zadać.
jonrsharpe
Ok, spróbuję innymi słowy. Problem jest jasny. Pytanie brzmi, jak to zrobić w Pythonie. Opcja 1 : sposób cytowania, opcja 2 : podstawowe podejście, które opisałem w pytaniu. Pytanie brzmi, czy są jakieś inne sposoby na to w Pythonie ?
bagrat

Odpowiedzi:

66

Zobacz Raymond Hettinger - Super uważany za super! - PyCon 2015 w celu omówienia sposobu korzystania z dziedziczenia super i wielokrotnego zamiast DI. Jeśli nie masz czasu na obejrzenie całego filmu, przejdź do minuty 15 (ale polecam obejrzenie całości).

Oto przykład, jak zastosować to, co opisano w tym filmie, w swoim przykładzie:

Kod ramowy:

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

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

Kod klienta:

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(...)

To zadziała, ponieważ Python MRO zagwarantuje, że zostanie wywołana metoda klienta getUserFromToken (jeśli używana jest super ()). Kod będzie musiał się zmienić, jeśli korzystasz z Pythona 2.x.

Dodatkową korzyścią jest to, że spowoduje to wyjątek, jeśli klient nie zapewni implementacji.

Oczywiście nie jest to tak naprawdę wstrzykiwanie zależności, jest to wielokrotne dziedziczenie i miksowanie, ale jest to Pythonowy sposób rozwiązania problemu.

Serban Teodorescu
źródło
10
Ta odpowiedź została uwzględniona super():)
bagrat
2
Raymond nazwał to CI, a myślałem, że to czysty mixin. Ale czy to możliwe, że w Pythonie mixin i CI są praktycznie takie same? Jedyną różnicą jest poziom indekcji. Mixin wstrzykuje zależność do poziomu klasy, podczas gdy CI wstrzykuje zależność do instancji.
nad2000
1
Myślę, że i tak wstrzyknięcie poziomu konstruktora jest dość łatwe do wykonania w Pythonie, tak jak opisał to OP. ten pytoniczny sposób wygląda jednak na bardzo interesujący. wymaga tylko trochę więcej okablowania niż prosty konstruktor wtrysku IMO.
stucash
6
Chociaż uważam to za bardzo eleganckie, mam dwa problemy z tym podejściem: 1. Co się dzieje, gdy potrzebujesz wstrzyknąć kilka przedmiotów do swojej klasy? 2. Dziedziczenie jest najczęściej używane w sensie „jest” / specjalizacją. Używanie go dla DI jest sprzeczne z tym pomysłem (na przykład jeśli chcę wstrzyknąć usługę prezenterowi).
AljoSt,
18

Sposób, w jaki wykonujemy wstrzyknięcie zależności w naszym projekcie, polega na użyciu pliku inject lib. Sprawdź dokumentację . Bardzo polecam używanie go do DI. To trochę nie ma sensu z tylko jedną funkcją, ale zaczyna mieć dużo sensu, gdy musisz zarządzać wieloma źródłami danych itp.

Idąc za twoim przykładem, może to być coś podobnego do:

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

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

Twoja funkcja niestandardowa:

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

Gdzieś w aplikacji chcesz utworzyć plik bootstrap, który śledzi wszystkie zdefiniowane zależności:

# bootstrap.py
import inject
from .my_stuff import my_func

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

inject.configure(configure_injection)

Następnie możesz konsumować kod w ten sposób:

# 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()

Obawiam się, że jest to tak Pythonowe, jak to tylko możliwe (moduł ma trochę słodyczy Pythona, takich jak dekoratory do wstrzykiwania przez parametr itp. - sprawdź dokumentację), ponieważ Python nie ma wymyślnych rzeczy, takich jak interfejsy lub podpowiedzi typu.

Dlatego udzielenie bezpośredniej odpowiedzi na pytanie byłoby bardzo trudne. Myślę, że prawdziwe pytanie brzmi: czy Python ma natywne wsparcie dla DI? Niestety, odpowiedź brzmi: nie.

Piotr Mazurek
źródło
Dzięki za odpowiedź, wydaje się całkiem interesująca. Sprawdzę część dekoratorów. W międzyczasie poczekajmy na więcej odpowiedzi.
bagrat
Dzięki za link do biblioteki „inject”. Jest to najbliższe wypełnienie luk, które chciałem wypełnić DI - i bonus, tak naprawdę jest utrzymywane!
Andy Mortimer
14

Jakiś czas temu napisałem microframework typu dependency injection z ambicją uczynienia go Pythonic - Dependency Injector . Tak może wyglądać Twój kod w przypadku jego użycia:

"""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)

Oto link do bardziej szczegółowego opisu tego przykładu - http://python-dependency-injector.ets-labs.org/examples/services_miniapp.html

Mam nadzieję, że to może trochę pomóc. Aby uzyskać więcej informacji prosimy odwiedzić:

Roman Mogylatov
źródło
Dziękuję @Roman Mogylatov. Jestem ciekawy, jak konfigurujesz / dostosowujesz te kontenery w czasie wykonywania, powiedzmy z pliku konfiguracyjnego. Wygląda na to, że te zależności są zakodowane na stałe w danym kontenerze ( Platformi Services). Czy rozwiązaniem jest utworzenie nowego kontenera dla każdej kombinacji wstrzykiwanych klas bibliotecznych?
Bill DeRose
2
Cześć @BillDeRose. Chociaż moja odpowiedź została uznana za zbyt długą, aby być komentarzem SO, utworzyłem problem na github i opublikowałem tam swoją odpowiedź - github.com/ets-labs/python-dependency-injector/issues/197 :) Mam nadzieję, że to pomoże, Dzięki, Roman
Roman Mogylatov
3

Wstrzykiwanie zależności to prosta technika, którą Python obsługuje bezpośrednio. Nie są wymagane żadne dodatkowe biblioteki. Korzystanie ze wskazówek dotyczących tekstu może poprawić przejrzystość i czytelność.

Kod ramowy:

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}!')

Kod klienta:

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')

UserStoreKlasa i rodzaj podpowiedzi nie są wymagane dla wykonania iniekcji zależność. Ich głównym celem jest udzielenie wskazówek deweloperowi klienta. Jeśli usuniesz UserStoreklasę i wszystkie odniesienia do niej, kod nadal działa.

Bryan Roach
źródło
2

Myślę, że DI i prawdopodobnie AOP nie są ogólnie uważane za Pythonic ze względu na typowe preferencje programistów Pythona, a raczej cechy języka.

W rzeczywistości możesz zaimplementować podstawowy framework DI w <100 liniach , używając metaklas i dekoratorów klas.

Aby uzyskać mniej inwazyjne rozwiązanie, te konstrukcje mogą być używane do podłączania niestandardowych implementacji do ogólnej struktury.

Andrea Ratto
źródło
2

Istnieje również Pinject, wstrzykiwacz zależności Pythona typu open source od Google.

Oto przykład

>>> 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

A oto kod źródłowy

Nasser Abdou
źródło
1

Bardzo prostym i Pythonowym sposobem na wstrzykiwanie zależności jest importlib.

Możesz zdefiniować małą funkcję użytkową

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

A potem możesz go użyć:

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

W mypackage / mymodule.py definiujesz moją funkcję

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

Możesz oczywiście użyć klasy MyClass iso. funkcja myfunction. Jeśli zdefiniujesz wartości nazwy metody w pliku settings.py, możesz załadować różne wersje nazwy metody w zależności od wartości pliku ustawień. Django używa takiego schematu do definiowania połączenia z bazą danych.

Ruben Decrop
źródło
1

Ze względu na implementację Python OOP, IoC i wstrzykiwanie zależności nie są standardowymi praktykami w świecie Pythona. Ale podejście wydaje się obiecujące nawet dla Pythona.

  • Używanie zależności jako argumentów jest podejściem innym niż Python. Python to język OOP z pięknym i eleganckim modelem OOP, który zapewnia prostsze sposoby utrzymywania zależności.
  • Definiowanie klas pełnych abstrakcyjnych metod tylko po to, by imitować typ interfejsu, też jest dziwne.
  • Ogromne obejścia typu wrapper-on-wrapper tworzą narzut kodu.
  • Nie lubię też korzystać z bibliotek, gdy potrzebuję tylko małego wzoru.

Więc moje rozwiązanie to:

# 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         

EDYTOWAĆ:

Uważaj na wzór. Użyłem go w prawdziwym projekcie i pokazał się niezbyt dobrze. Mój post na Medium o moich doświadczeniach z wzorem.

I159
źródło
Oczywiście MKOl i DI są powszechnie stosowane, co nie jest powszechnie stosowane są DI ram , na lepsze lub gorsze.
juanpa.arrivillaga
1

Po zabawie z niektórymi frameworkami DI w Pythonie stwierdziłem, że ich użycie jest trochę niezgrabne, porównując, jak proste jest to w innych dziedzinach, takich jak .NET Core. Wynika to głównie z łączenia za pomocą elementów takich jak dekoratory, które zaśmiecają kod i utrudniają po prostu dodanie go do projektu lub usunięcie go z projektu lub łączenie na podstawie nazw zmiennych.

Niedawno pracowałem nad strukturą iniekcji zależności, która zamiast tego używa adnotacji do pisania, aby wykonać wstrzyknięcie o nazwie Simple-Injection. Poniżej znajduje się prosty przykład

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!

Ta biblioteka obsługuje okresy istnienia usług i usługi powiązania z implementacjami.

Jednym z celów tej biblioteki jest to, że łatwo jest dodać ją do istniejącej aplikacji i zobaczyć, jak Ci się podoba, zanim się do niej zdecydujesz, ponieważ wszystko, czego wymaga, to aplikacja, która ma odpowiednie typy, a następnie zbudujesz wykres zależności w punkt wejścia i uruchom go.

Mam nadzieję że to pomoże. Aby uzyskać więcej informacji, zobacz

bradlewis
źródło