¿Qué es una forma pitónica para la inyección de dependencia?

84

Introducción

Para Java, Dependency Injection funciona como POO puro, es decir, usted proporciona una interfaz para ser implementada y en su código marco acepta una instancia de una clase que implementa la interfaz definida.

Ahora, para Python, puedes hacer lo mismo, pero creo que ese método era demasiado general en el caso de Python. Entonces, ¿cómo lo implementaría de la manera Pythonic?

Caso de uso

Digamos que este es el código del marco:

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

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

El enfoque básico

La forma más ingenua (¿y quizás la mejor?) Es exigir que la función externa se proporcione en el FrameworkClassconstructor y luego se invoque desde el do_the_jobmétodo.

Código marco:

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

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

Codigo del cliente:

def my_func():
    # my implementation

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

Pregunta

La pregunta es breve. ¿Existe alguna forma Pythonic mejor utilizada para hacer esto? ¿O tal vez alguna biblioteca que admita dicha funcionalidad?

ACTUALIZACIÓN: Situación concreta

Imagine que desarrollo un marco de micro web, que maneja la autenticación mediante tokens. Este marco necesita una función para proporcionar algunos IDobtenidos del token y obtener el usuario correspondiente ID.

Obviamente, el marco no sabe nada sobre los usuarios ni sobre ninguna otra lógica específica de la aplicación, por lo que el código del cliente debe inyectar la funcionalidad del getter del usuario en el marco para que la autenticación funcione.

bagrat
fuente
2
¿Por qué no "proporciona una interfaz para ser implementada y en su código marco acepta una instancia de una clase que implementa la interfaz definida" ? En Python, haría esto en un estilo EAFP (es decir, suponga que cumple con esa interfaz y se eleva AttributeErroro de TypeErrorotra manera), pero por lo demás es lo mismo.
jonrsharpe
Es fácil hacer eso usando absla ABCMetametaclase de 'con @abstractmethoddecorador y sin validación manual. Solo quiero obtener un par de opciones y sugerencias. El que citó es el más limpio, pero creo que con más gastos generales.
bagrat
Entonces no sé qué pregunta estás tratando de hacer.
jonrsharpe
Ok, lo intentaré en otras palabras. El problema es claro. La pregunta es cómo hacer eso en forma Pythonic. Opción 1 : La forma en que citó, Opción 2 : El enfoque básico que describí en la pregunta. Entonces, la pregunta es, ¿alguna otra forma Pythonic de hacer eso?
bagrat

Respuestas:

66

Mira a Raymond Hettinger - ¡Súper considerado súper! - PyCon 2015 para un argumento sobre cómo usar la herencia super y múltiple en lugar de DI. Si no tiene tiempo para ver el video completo, salte al minuto 15 (pero recomiendo verlo todo).

Aquí hay un ejemplo de cómo aplicar lo que se describe en este video a su ejemplo:

Código marco:

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

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

Codigo del cliente:

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

Esto funcionará porque Python MRO garantizará que se llame al método de cliente getUserFromToken (si se usa super ()). El código tendrá que cambiar si está en Python 2.x.

Un beneficio adicional aquí es que esto generará una excepción si el cliente no proporciona una implementación.

Por supuesto, esto no es realmente una inyección de dependencia, es herencia múltiple y mixins, pero es una forma Pythonic de resolver su problema.

Serban Teodorescu
fuente
10
Esta respuesta considerada super():)
bagrat
2
Raymond lo llamó CI mientras que, pensé, es un mixin puro. Pero, ¿podría ser que en Python mixin y CI sean prácticamente lo mismo? La única diferencia es el nivel de indección. Mixin inyecta dependencia en un nivel de clase, mientras que CI inyecta dependencia en una instancia.
nad2000
1
Creo que la inyección a nivel de constructor es bastante fácil de hacer en Python de todos modos, como lo describió OP. Sin embargo, esta forma pitónica parece ser muy interesante. solo requiere un poco más de cableado que la simple inyección del constructor IMO.
stucash
6
Si bien lo encuentro muy elegante, tengo dos problemas con este enfoque: 1. ¿Qué sucede cuando necesita que se inyecten varios elementos en su clase? 2. La herencia se usa con mayor frecuencia en un sentido "es un" / especialización. Usarlo para DI desafía esa idea (por ejemplo, si quiero inyectar un Servicio en un Presentador).
AljoSt
18

La forma en que hacemos la inyección de dependencias en nuestro proyecto es mediante el uso de inject lib. Consulte la documentación . Recomiendo encarecidamente usarlo para DI. No tiene sentido con una sola función, pero comienza a tener mucho sentido cuando tiene que administrar múltiples fuentes de datos, etc., etc.

Siguiendo tu ejemplo, podría ser algo similar a:

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

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

Tu función personalizada:

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

En algún lugar de la aplicación, desea crear un archivo de arranque que realiza un seguimiento de todas las dependencias definidas:

# bootstrap.py
import inject
from .my_stuff import my_func

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

inject.configure(configure_injection)

Y luego podrías consumir el código de esta manera:

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

Me temo que esto es lo más pitónico posible (el módulo tiene algo de dulzura de Python como decoradores para inyectar por parámetro, etc.), ya que Python no tiene cosas sofisticadas como interfaces o sugerencias de tipo.

Entonces, responder directamente a su pregunta sería muy difícil. Creo que la verdadera pregunta es: ¿Python tiene algún soporte nativo para DI? Y la respuesta es, lamentablemente: no.

Piotr Mazurek
fuente
Gracias por tu respuesta, parece bastante interesante. Revisaré la parte de los decoradores. Mientras tanto, esperemos más respuestas.
bagrat
Gracias por el enlace a la biblioteca 'inyectar'. Esto es lo más cerca que he encontrado hasta ahora de llenar los vacíos que quería llenar con DI, y además, ¡en realidad se está manteniendo!
Andy Mortimer
13

Hace algún tiempo escribí un microframework de inyección de dependencia con la ambición de convertirlo en Pythonic - Dependency Injector . Así es como puede verse su código en caso de su uso:

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

Aquí hay un enlace a una descripción más extensa de este ejemplo: http://python-dependency-injector.ets-labs.org/examples/services_miniapp.html

Espero que pueda ayudar un poco. Para mayor información por favor visite:

Roman Mogylatov
fuente
Gracias @Roman Mogylatov. Tengo curiosidad por saber cómo configura / adapta estos contenedores en tiempo de ejecución, por ejemplo, desde un archivo de configuración. Parece que estas dependencias están codificadas en el contenedor dado ( Platformy Services). ¿La solución es crear un nuevo contenedor para cada combinación de clases de biblioteca inyectables?
Bill DeRose
2
Hola @BillDeRose. Si bien mi respuesta se consideró demasiado larga para ser un comentario SO, creé un problema de github y publiqué mi respuesta allí: github.com/ets-labs/python-dependency-injector/issues/197 :) Espero que ayude, Gracias, Roman
Roman Mogylatov
2

Creo que DI y posiblemente AOP generalmente no se consideran Pythonic debido a las preferencias típicas de los desarrolladores de Python, sino a las características del lenguaje.

De hecho, puede implementar un marco DI básico en <100 líneas , utilizando metaclases y decoradores de clases.

Para una solución menos invasiva, estas construcciones se pueden utilizar para incorporar implementaciones personalizadas en un marco genérico.

Andrea Ratto
fuente
2

También está Pinject, un inyector de dependencia de Python de código abierto de Google.

Aquí hay un ejemplo

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

Y aquí está el código fuente

Nasser Abdou
fuente
2

La inyección de dependencia es una técnica simple que Python admite directamente. No se requieren bibliotecas adicionales. El uso de sugerencias de tipo puede mejorar la claridad y la legibilidad.

Código marco:

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

Codigo del cliente:

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

Las UserStoresugerencias de clase y tipo no son necesarias para implementar la inyección de dependencia. Su propósito principal es brindar orientación al desarrollador del cliente. Si elimina la UserStoreclase y todas las referencias a ella, el código aún funciona.

Bryan Roach
fuente
1

Una forma muy fácil y Pythonic de hacer la inyección de dependencias es importlib.

Podrías definir una pequeña función de utilidad

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

Y luego puedes usarlo:

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

En mypackage / mymodule.py define myfunction

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

Por supuesto, también podría utilizar una clase iso MyClass. la función myfunction. Si define los valores del nombre del método en un archivo settings.py, puede cargar diferentes versiones del nombre del método según el valor del archivo de configuración. Django está usando un esquema de este tipo para definir su conexión a la base de datos.

Ruben Decrop
fuente
1

Debido a la implementación de Python OOP, IoC y la inyección de dependencias no son prácticas estándar en el mundo de Python. Pero el enfoque parece prometedor incluso para Python.

  • Usar dependencias como argumentos es un enfoque no pitónico. Python es un lenguaje OOP con un modelo OOP hermoso y elegante, que proporciona formas más sencillas de mantener las dependencias.
  • Definir clases llenas de métodos abstractos solo para imitar el tipo de interfaz también es extraño.
  • Las enormes soluciones alternativas de envoltorio sobre envoltorio crean una sobrecarga de código.
  • Tampoco me gusta usar bibliotecas cuando todo lo que necesito es un patrón pequeño.

Entonces mi solución es:

# 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         

EDITAR:

Tenga cuidado con el patrón. Lo usé en un proyecto real y se mostró de una manera no tan buena. Mi publicación en Medium sobre mi experiencia con el patrón.

I159
fuente
Por supuesto, IOC y DI se usan comúnmente, lo que no se usa comúnmente son los marcos DI , para bien o para mal.
juanpa.arrivillaga
0

Después de jugar con algunos de los marcos de DI en Python, descubrí que se han sentido un poco torpes de usar al comparar lo simple que es en otros ámbitos, como con .NET Core. Esto se debe principalmente a la unión a través de elementos como decoradores que desordenan el código y dificultan simplemente agregarlo o eliminarlo de un proyecto, o unirse en función de nombres de variables.

Recientemente he estado trabajando en un marco de inyección de dependencia que en su lugar usa anotaciones de escritura para hacer la inyección llamada Simple-Injection. A continuación se muestra un ejemplo simple

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!

Esta biblioteca admite la vida útil de los servicios y los servicios vinculados a las implementaciones.

Uno de los objetivos de esta biblioteca es que también es fácil agregarla a una aplicación existente y ver cómo le gusta antes de comprometerse con ella, ya que todo lo que requiere es que su aplicación tenga los tipos adecuados, y luego construya el gráfico de dependencia en el punto de entrada y ejecútelo.

Espero que esto ayude. Para obtener más información, consulte

bradlewis
fuente