Wat is een Pythonische manier voor Dependency Injection?

Voor Java werkt Dependency Injection als pure OOP, d.w.z. u levert een te implementeren interface en accepteert in uw frameworkcode een instantie van een klasse die de gedefinieerde interface implementeert.

Voor Python kun je nu op dezelfde manier doen, maar ik denk dat die methode te veel overhead was in het geval van Python. Dus hoe zou je het dan op de Pythonische manier implementeren?

Gebruiksvoorbeeld

Zeg dat dit de raamcode is:

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

De basisaanpak

De meest naïeve (en misschien wel de beste?) manier is om te eisen dat de externe functie wordt geleverd in de FrameworkClass-constructor en vervolgens wordt aangeroepen vanuit de do_the_job-methode .

Kadercode:

class FrameworkClass():
    def __init__(self, func):
        self.func = func
    def do_the_job(self, ...):
        # some stuff
        self.func(...)

Klantcode:

def my_func():
    # my implementation
framework_instance = FrameworkClass(my_func)
framework_instance.do_the_job(...)

Vraag

De vraag is kort. Is er een betere, veelgebruikte Python-manier om dit te doen? Of misschien bibliotheken die dergelijke functionaliteit ondersteunen?

UPDATE: Concrete situatie

Stel je voor dat ik een micro-webframework ontwikkel dat authenticatie met tokens afhandelt. Dit framework heeft een functie nodig om een ​​IDverkregen uit het token te leveren en de gebruiker te krijgen die overeenkomt met die ID.

Het is duidelijk dat het framework niets weet over gebruikers of enige andere applicatiespecifieke logica, dus de clientcode moet de user getter-functionaliteit in het framework injecteren om de authenticatie te laten werken.


Antwoord 1, autoriteit 100%

Zie Raymond Hettinger – Super beschouwd als super! – PyCon 2015voor een argument over het gebruik van super en meervoudige overerving in plaats van DI. Als je geen tijd hebt om de hele video te bekijken, ga dan naar minuut 15 (maar ik raad je aan om de hele video te bekijken).

Hier is een voorbeeld van hoe u kunt toepassen wat in deze video wordt beschreven op uw voorbeeld:

Kadercode:

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

Klantcode:

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

Dit werkt omdat de Python MRO garandeert dat de getUserFromToken-clientmethode wordt aangeroepen (als super() wordt gebruikt). De code zal moeten veranderen als je Python 2.x gebruikt.

Een bijkomend voordeel is dat er een uitzondering ontstaat als de klant geen implementatie levert.

Natuurlijk is dit niet echt afhankelijkheidsinjectie, het is meervoudige overerving en mixins, maar het is een Pythonische manier om je probleem op te lossen.


Antwoord 2, autoriteit 26%

De manier waarop we afhankelijkheidsinjectie in ons project doen, is door de injectlib te gebruiken. Bekijk de documentatie. Ik raad het ten zeerste aan om het voor DI te gebruiken. Het heeft eigenlijk geen zin met slechts één functie, maar het begint heel logisch te worden wanneer je meerdere gegevensbronnen moet beheren, enz.

Als u uw voorbeeld volgt, kan het iets zijn dat lijkt op:

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

Uw aangepaste functie:

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

Ergens in de applicatie wil je een bootstrap-bestand maken dat alle gedefinieerde afhankelijkheden bijhoudt:

# bootstrap.py
import inject
from .my_stuff import my_func
def configure_injection(binder):
    binder.bind(FrameworkClass, FrameworkClass(my_func))
inject.configure(configure_injection)

En dan zou je de code op deze manier kunnen gebruiken:

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

Ik ben bang dat dit zo pythonisch is als het maar kan krijgen (de module heeft wat python-zoetheid zoals decorateurs om per parameter te injecteren enz. – controleer de documenten), aangezien python geen fancy dingen heeft zoals interfaces of typehints.

p>

Dus om uw vraag direct te beantwoordenzou erg moeilijk zijn. Ik denk dat de echte vraag is: heeft python enige native ondersteuning voor DI? En het antwoord is helaas: nee.


Antwoord 3, autoriteit 20%

Enige tijd geleden schreef ik afhankelijkheidsinjectie-microframework met de ambitie om het Pythonic te maken – Dependency Injector . Zo kan uw code eruitzien in het geval van gebruik:

"""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 is een link naar een uitgebreidere beschrijving van dit voorbeeld – http: //python-dependency-inogror.ets-labs.org/examples/services_miniappp.html

Ik hoop dat het een beetje kan helpen. Ga voor meer informatie naar:


4, Autoriteit 3%

Ik denk dat DI en MOGELIJK AOP niet over het algemeen worden beschouwd als Pythonic vanwege de typische voorkeuren van Python-ontwikkelaars, eerder die taalfuncties.

In feite kun je een basic di framework in & lt; 100 Lijnen , met behulp van metaclasses en klassecorateurs.

Voor een minder invasieve oplossing kunnen deze constructen worden gebruikt om aangepaste implementaties in een generiek framework in te pluggen.


Antwoord 5

Vanwege de Python OOP-implementatie zijn IoC en afhankelijkheidsinjectie geen standaardpraktijken in de Python-wereld. Maar de aanpak lijkt zelfs voor Python veelbelovend.

  • Het gebruik van afhankelijkheden als argumenten is een niet-pythonische benadering. Python is een OOP-taal met een mooi en elegant OOP-model, dat eenvoudigere manieren biedt om afhankelijkheden te behouden.
  • Het is ook raar om klassen vol abstracte methoden te definiëren om het interfacetype te imiteren.
  • Enorme wrapper-on-wrapper-oplossingen zorgen voor code-overhead.
  • Ik hou er ook niet van om bibliotheken te gebruiken als ik alleen maar een klein patroon nodig heb.

Dus mijn oplossingis:

# 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         

BEWERKEN:

Wees voorzichtig met het patroon. Ik gebruikte het in een echt project en het toonde zich niet zo goed. Mijn bericht op Medium over mijn ervaring met het patroon.


Antwoord 6

Na wat spelen met enkele van de DI-frameworks in python, merkte ik dat ze een beetje onhandig aanvoelden om te gebruiken bij het vergelijken van hoe eenvoudig het is in andere gebieden, zoals met .NET Core. Dit komt voornamelijk door het samenvoegen via dingen zoals decorateurs die de code onoverzichtelijk maken en het moeilijk maken om het eenvoudig toe te voegen aan of te verwijderen uit een project, of deelnemen op basis van variabelenamen.

Ik heb onlangs gewerkt aan een afhankelijkheidsinjectieraamwerk dat in plaats daarvan annotaties gebruikt om de injectie uit te voeren, genaamd Simple-Injection. Hieronder staat een eenvoudig voorbeeld

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!

Deze bibliotheek ondersteunt de levensduur van services en het binden van services aan implementaties.

Een van de doelen van deze bibliotheek is dat het ook gemakkelijk is om het toe te voegen aan een bestaande applicatie en te zien hoe je het bevalt voordat je je eraan vastlegt, aangezien het enige dat je applicatie nodig heeft om de juiste typeringen te hebben, en dan bouw je de afhankelijkheidsgrafiek bij het beginpunt en voer het uit.

Ik hoop dat dit helpt. Voor meer informatie, zie

Other episodes