Was ist ein pythonischer Weg für die Abhängigkeitsinjektion?

84

Einführung

Für Java funktioniert Dependency Injection als reines OOP, dh Sie stellen eine zu implementierende Schnittstelle bereit und akzeptieren in Ihrem Framework-Code eine Instanz einer Klasse, die die definierte Schnittstelle implementiert.

Für Python können Sie jetzt auf die gleiche Weise vorgehen, aber ich denke, diese Methode war im Fall von Python zu viel Aufwand. Wie würden Sie es dann auf pythonische Weise implementieren?

Anwendungsfall

Angenommen, dies ist der Framework-Code:

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

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

Der grundlegende Ansatz

Der naivste (und vielleicht der beste?) Weg besteht darin, zu verlangen, dass die externe Funktion in den FrameworkClassKonstruktor eingegeben und dann von der do_the_jobMethode aufgerufen wird.

Rahmencode:

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

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

Kundencode:

def my_func():
    # my implementation

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

Frage

Die Frage ist kurz. Gibt es eine besser verwendete pythonische Methode, um dies zu tun? Oder vielleicht Bibliotheken, die solche Funktionen unterstützen?

UPDATE: Konkrete Situation

Stellen Sie sich vor, ich entwickle ein Micro-Web-Framework, das die Authentifizierung mithilfe von Token übernimmt. Dieses Framework benötigt eine Funktion, um einige IDvom Token erhaltene zu liefern und den Benutzer entsprechend zu erhalten ID.

Offensichtlich weiß das Framework nichts über Benutzer oder eine andere anwendungsspezifische Logik, daher muss der Client-Code die User-Getter-Funktionalität in das Framework einfügen, damit die Authentifizierung funktioniert.

Bagrat
quelle
2
Warum stellen Sie nicht "eine zu implementierende Schnittstelle bereit und akzeptieren in Ihrem Framework-Code eine Instanz einer Klasse, die die definierte Schnittstelle implementiert" ? In Python würden Sie dies in einem EAFP- Stil tun (dh annehmen, dass es diese Schnittstelle erfüllt und ein AttributeErroroder TypeErroranderweitig ausgelöst wird), aber ansonsten ist es dasselbe.
Jonrsharpe
Es ist einfach, dies mit absder ABCMetaMetaklasse mit @abstractmethodDekorateur und ohne manuelle Validierung zu tun . Ich möchte nur ein paar Optionen und Vorschläge erhalten. Der von Ihnen zitierte ist der sauberste, aber ich denke mit mehr Aufwand.
Bagrat
Dann weiß ich nicht, welche Frage Sie stellen wollen.
Jonrsharpe
Ok, ich werde es mit anderen Worten versuchen. Das Problem ist klar. Die Frage ist, wie man das auf pythonische Weise macht. Option 1 : Wie Sie zitiert haben, Option 2 : Der grundlegende Ansatz, den ich in der Frage beschrieben habe. Die Frage ist also, ob es andere pythonische Möglichkeiten gibt, dies zu tun.
Bagrat

Antworten:

66

Siehe Raymond Hettinger - Super als super! - PyCon 2015 für ein Argument zur Verwendung von Super- und Mehrfachvererbung anstelle von DI. Wenn Sie keine Zeit haben, das gesamte Video anzusehen, springen Sie zu Minute 15 (aber ich würde empfehlen, alles anzuschauen).

Hier ist ein Beispiel, wie Sie das, was in diesem Video beschrieben wird, auf Ihr Beispiel anwenden können:

Rahmencode:

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

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

Kundencode:

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

Dies funktioniert, da das Python-MRO garantiert, dass die Client-Methode getUserFromToken aufgerufen wird (wenn super () verwendet wird). Der Code muss geändert werden, wenn Sie Python 2.x verwenden.

Ein zusätzlicher Vorteil besteht darin, dass dies eine Ausnahme auslöst, wenn der Client keine Implementierung bereitstellt.

Natürlich ist dies keine wirkliche Abhängigkeitsinjektion, sondern eine Mehrfachvererbung und Mixins, aber es ist eine pythonische Methode, um Ihr Problem zu lösen.

Serban Teodorescu
quelle
10
Diese Antwort berücksichtigt super():)
Bagrat
2
Raymond nannte es CI, während ich dachte, dass es ein reines Mixin ist. Aber könnte es sein, dass in Python Mixin und CI praktisch gleich sind? Der einzige Unterschied ist der Grad der Unabhängigkeit. Mixin fügt Abhängigkeiten in eine Klassenebene ein, während CI Abhängigkeiten in eine Instanz einfügt.
Nad2000
1
Ich denke, die Injektion auf Konstruktorebene ist in Python sowieso ziemlich einfach, so wie es OP beschrieben hat. Diese pythonische Art scheint jedoch sehr interessant zu sein. Es erfordert nur etwas mehr Verkabelung als IMO mit einfacher Konstruktorinjektion.
Stucash
6
Obwohl ich es sehr elegant finde, habe ich zwei Probleme mit diesem Ansatz: 1. Was passiert, wenn mehrere Elemente in Ihre Klasse eingefügt werden müssen? 2. Vererbung wird am häufigsten in einem "Ist ein" / Spezialisierungssinn verwendet. Die Verwendung für DI widerspricht dieser Idee (zum Beispiel, wenn ich einen Service in einen Presenter einfügen möchte).
AljoSt
18

Die Art und Weise, wie wir in unserem Projekt Abhängigkeitsinjektionen durchführen, ist die Verwendung der Injektionsbibliothek . Lesen Sie die Dokumentation . Ich empfehle dringend, es für DI zu verwenden. Es macht irgendwie keinen Sinn mit nur einer Funktion, macht aber viel Sinn, wenn Sie mehrere Datenquellen usw. usw. verwalten müssen.

Nach Ihrem Beispiel könnte es etwas Ähnliches sein wie:

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

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

Ihre benutzerdefinierte Funktion:

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

Irgendwo in der Anwendung möchten Sie eine Bootstrap-Datei erstellen, die alle definierten Abhängigkeiten verfolgt:

# bootstrap.py
import inject
from .my_stuff import my_func

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

inject.configure(configure_injection)

Und dann könnten Sie den Code folgendermaßen verwenden:

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

Ich befürchte, dies ist so pythonisch wie es nur geht (das Modul hat eine gewisse Python-Süße wie Dekoratoren, die nach Parametern usw. eingefügt werden können - überprüfen Sie die Dokumente), da Python keine ausgefallenen Dinge wie Schnittstellen oder Typhinweise hat.

Es wäre also sehr schwierig, Ihre Frage direkt zu beantworten . Ich denke, die wahre Frage ist: Hat Python eine native Unterstützung für DI? Und die Antwort lautet leider: Nein.

Piotr Mazurek
quelle
Danke für deine Antwort, scheint ziemlich interessant zu sein. Ich werde den Teil der Dekorateure überprüfen. Warten wir in der Zwischenzeit auf weitere Antworten.
Bagrat
Vielen Dank für den Link zur 'Inject'-Bibliothek. Dies ist der nächste Punkt, den ich bisher gefunden habe, um die Lücken zu füllen, die ich von DI füllen wollte - und der Bonus wird tatsächlich beibehalten!
Andy Mortimer
13

Vor einiger Zeit schrieb ich ein Mikroframework zur Abhängigkeitsinjektion mit dem Ziel, es zu Pythonic - Dependency Injector zu machen . So kann Ihr Code im Falle seiner Verwendung aussehen:

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

Hier ist ein Link zu einer ausführlicheren Beschreibung dieses Beispiels - http://python-dependency-injector.ets-labs.org/examples/services_miniapp.html

Hoffe es kann ein bisschen helfen. Für weitere Informationen, besuchen Sie bitte:

Roman Mogylatov
quelle
Vielen Dank, dass Sie @Roman Mogylatov. Ich bin gespannt, wie Sie diese Container zur Laufzeit konfigurieren / anpassen, beispielsweise aus einer Konfigurationsdatei. Es scheint, dass diese Abhängigkeiten in den angegebenen Container ( Platformund Services) fest codiert sind . Ist die Lösung, einen neuen Container für jede Kombination injizierbarer Bibliotheksklassen zu erstellen?
Bill DeRose
2
Hallo @ BillDeRose. Obwohl meine Antwort als zu lang angesehen wurde, um ein SO-Kommentar zu sein, habe ich ein Github-Problem erstellt und meine Antwort dort veröffentlicht - github.com/ets-labs/python-dependency-injector/issues/197 :) Hoffe, es hilft, Vielen Dank, Roman
Roman Mogylatov
2

Ich denke, dass DI und möglicherweise AOP aufgrund der typischen Präferenzen der Python-Entwickler im Allgemeinen nicht als Pythonic angesehen werden, sondern aufgrund der Sprachfunktionen.

Tatsächlich können Sie ein grundlegendes DI-Framework in <100 Zeilen mithilfe von Metaklassen und Klassendekoratoren implementieren .

Für eine weniger invasive Lösung können diese Konstrukte verwendet werden, um benutzerdefinierte Implementierungen in ein generisches Framework einzufügen.

Andrea Ratto
quelle
2

Es gibt auch Pinject, einen Open-Source-Python-Abhängigkeitsinjektor von Google.

Hier ist ein Beispiel

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

Und hier ist der Quellcode

Nasser Abdou
quelle
2

Die Abhängigkeitsinjektion ist eine einfache Technik, die Python direkt unterstützt. Es sind keine zusätzlichen Bibliotheken erforderlich. Die Verwendung von Typhinweisen kann die Klarheit und Lesbarkeit verbessern.

Rahmencode:

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

Kundencode:

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

Die UserStoreKlassen- und Typhinweise sind für die Implementierung der Abhängigkeitsinjektion nicht erforderlich. Ihr Hauptzweck ist es, dem Kundenentwickler eine Anleitung zu geben. Wenn Sie die UserStoreKlasse und alle Verweise darauf entfernen , funktioniert der Code weiterhin.

Bryan Roach
quelle
1

Eine sehr einfache und pythonische Methode zur Durchführung der Abhängigkeitsinjektion ist importlib.

Sie können eine kleine Dienstprogrammfunktion definieren

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

Und dann können Sie es verwenden:

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

In mypackage / mymodule.py definieren Sie myfunction

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

Sie können natürlich auch eine Klasse MyClass iso verwenden. die Funktion myfunction. Wenn Sie die Werte von Methodenname in einer Datei settings.py definieren, können Sie abhängig vom Wert der Einstellungsdatei verschiedene Versionen des Methodennamens laden. Django verwendet ein solches Schema, um seine Datenbankverbindung zu definieren.

Ruben Decrop
quelle
1

Aufgrund der Python-OOP-Implementierung sind IoC und Abhängigkeitsinjektion in der Python-Welt keine Standardpraktiken. Aber der Ansatz scheint auch für Python vielversprechend.

  • Abhängigkeiten als Argumente zu verwenden, ist ein nicht-pythonischer Ansatz. Python ist eine OOP-Sprache mit einem schönen und eleganten OOP-Modell, das einfachere Möglichkeiten zum Verwalten von Abhängigkeiten bietet.
  • Es ist auch seltsam, Klassen voller abstrakter Methoden zu definieren, um nur den Schnittstellentyp zu imitieren.
  • Riesige Wrapper-on-Wrapper-Problemumgehungen verursachen Code-Overhead.
  • Ich benutze auch keine Bibliotheken, wenn ich nur ein kleines Muster brauche.

Meine Lösung lautet also:

# 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         

BEARBEITEN:

Sei vorsichtig mit dem Muster. Ich habe es in einem echten Projekt verwendet und es hat sich als nicht so gut erwiesen. Mein Beitrag auf Medium über meine Erfahrungen mit dem Muster.

I159
quelle
Natürlich werden häufig IOC und DI verwendet, was nicht häufig verwendet wird, sind DI- Frameworks , zum Guten oder zum Schlechten.
juanpa.arrivillaga
0

Nachdem ich mit einigen DI-Frameworks in Python herumgespielt habe, habe ich festgestellt, dass sie sich etwas umständlich anfühlen, wenn man vergleicht, wie einfach es in anderen Bereichen wie .NET Core ist. Dies ist hauptsächlich auf die Verknüpfung über Dinge wie Dekorateure zurückzuführen, die den Code überladen und es schwierig machen, ihn einfach in ein Projekt einzufügen oder aus einem Projekt zu entfernen, oder auf die Verknüpfung basierend auf Variablennamen.

Ich habe kürzlich an einem Abhängigkeitsinjektions-Framework gearbeitet, das stattdessen Typanmerkungen verwendet, um die Injektion namens Simple-Injection durchzuführen. Unten ist ein einfaches Beispiel

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!

Diese Bibliothek unterstützt die Lebensdauer und die Bindung von Diensten an Implementierungen.

Eines der Ziele dieser Bibliothek ist, dass es auch einfach ist, sie zu einer vorhandenen Anwendung hinzuzufügen und zu sehen, wie sie Ihnen gefällt, bevor Sie sie festschreiben, da Ihre Anwendung lediglich über geeignete Typisierungen verfügt. Anschließend erstellen Sie das Abhängigkeitsdiagramm unter den Einstiegspunkt und führen Sie es aus.

Hoffe das hilft. Weitere Informationen finden Sie unter

Bradlewis
quelle