Meaning of using DI framework in Python

Introduction

The documentation for the Python DI framework Dependency Injector was so suggestive that I translated some of it to let many people know the goodness of DI in Python.

Dependency Injector Documentation (https://python-dependency-injector.ets-labs.org/introduction/di_in_python.html)

DI (Dependency Injection) and Inversion of Control in Python

Originally, DI patterns were popular in statically typed languages ​​like Java. DI is a principle that helps achieve inversion of control. DI frameworks can use static typing to greatly increase language flexibility. Implementing DI Ramwork for languages ​​that use static typing is not immediate. It will be quite complicated to do well. And it takes time.

Python is an interpreted language that uses dynamic typing. There is an opinion that DI in Python does not work as well as Java. It has a lot of flexibility already built in. Others argue that DI frameworks are rarely needed by Python developers. Python developers say DI is easy to implement using language foundations.

This page describes the benefits of using DI in Python. Here is a Python example showing how to implement DI. This section describes how to use the DI framework Dependency Injector and its containers, Factory, Singleton, and Configuration providers. This example shows how to use the Dependency Injector provider's override feature to test or configure a project in different environments and explains why it is better than a monkey patch.

What is Dependency Injection (DI)?

Let's look at what Dependency Injection (DI) is.

DI is a principle that helps reduce coupling and increase cohesion.

image.png

What is binding and agglutination?

Coupling and cohesion represent how tightly the parts are tied together.

-** High bond ** If the bond is high, it's like using superglue or welding. There is no easy way to disassemble.

-** High cohesion ** High cohesion is like using a screw. It's very easy to disassemble and reassemble, or otherwise assemble. This is the opposite of high binding.

The higher the cohesion, the lower the binding.

Low coupling provides flexibility. It makes it easy to modify and test your code.

How to implement DI?

Prevents objects from spawning each other. Instead, it provides a way to inject dependencies.

Before:

import os


class ApiClient:

    def __init__(self):
        self.api_key = os.getenv('API_KEY')  # <--Dependence
        self.timeout = os.getenv('TIMEOUT')  # <--Dependence


class Service:

    def __init__(self):
        self.api_client = ApiClient()  # <--Dependence


def main() -> None:
    service = Service()  # <--Dependence
    ...


if __name__ == '__main__':
    main()

After:

import os


class ApiClient:

    def __init__(self, api_key: str, timeout: int):
        self.api_key = api_key  # <--Dependency was injected
        self.timeout = timeout  # <--Dependency was injected


class Service:

    def __init__(self, api_client: ApiClient):
        self.api_client = api_client  # <--Dependency was injected


def main(service: Service):  # <--Dependency was injected
    ...


if __name__ == '__main__':
    main(
        service=Service(
            api_client=ApiClient(
                api_key=os.getenv('API_KEY'),
                timeout=os.getenv('TIMEOUT'),
            ),
        ),
    )

ApiClient is decoupled from knowing where the option came from. You can also read the API key and timeout time (not limited to environment variables) from a file or get it from the database.

Service is detached from ApiClient. Service no longer creates it internally. You can put stubs and other compatible objects.

Flexibility comes at a price.

I needed to assemble and inject the object as follows:

main(
    service=Service(
        api_client=ApiClient(
            api_key=os.getenv('API_KEY'),
            timeout=os.getenv('TIMEOUT'),
        ),
    ),
)

This assembly code can be duplicated, making it difficult to change the structure of your application.

This is where the Dependency Injector comes into play.

What does Dependency Injector do?

In the DI pattern, the object relinquishes responsibility for building dependencies. The Dependency Injector takes responsibility for it.

The Dependency Injector helps you build and inject dependencies.

Dependency Injector provides containers and providers to help you assemble objects. If you need an object, place the Provide marker as the default value for the function argument. When you call this function, the framework builds and injects dependencies.

from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide


class Container(containers.DeclarativeContainer):

    config = providers.Configuration()

    api_client = providers.Singleton(
        ApiClient,
        api_key=config.api_key,
        timeout=config.timeout.as_int(),
    )

    service = providers.Factory(
        Service,
        api_client=api_client,
    )


@inject
def main(service: Service = Provide[Container.service]):
    ...


if __name__ == '__main__':
    container = Container()
    container.config.api_key.from_env('API_KEY')
    container.config.timeout.from_env('TIMEOUT')
    #Specify the module to inject the container
    container.wire(modules=[sys.modules[__name__]])

    main()  # <--Dependencies are automatically injected

    with container.api_client.override(mock.Mock()):
        main()  # <--Dependencies replaced by mock are automatically injected

Calling the main () function automatically constructs and injects the Service dependency.

When testing, call container.api_client.override () to replace the actual API client with a mock. Calling main () injects a mock.

You can replace any provider with another provider.

It also helps you configure your project for different environments. In this example, the API client is replaced with a stub in the development environment or staging environment.

Object assembly is integrated into the container. Dependency injection is explicitly defined. This makes it easier to understand and modify the behavior of your application.

Test, monkey patch, DI

The advantage of testability is in contrast to monkey patches.

With Python, you can apply monkey patches to anything at any time. The problem with monkey patches is that they are fragile. The reason is that when you apply a monkey patch, you do something unintended. Suppose you've applied a monkey patch to the details of your implementation. If that implementation changes, the monkey patch will break.

Dependency injection patches the interface, not the implementation. This is a more stable approach.

Also, monkey patches are a way too dirty to use outside of test code to reconfigure a project for different environments.

Conclusion

DI has three advantages.

-** Flexibility ** Components are loosely coupled. You can easily extend or change the functionality of your system by combining components in different ways. You can do it on the fly too.

-** Testability ** Testing is easy because you can easily inject mock instead of real objects that use APIs, databases, etc.

-** Clarity and maintainability ** DI helps clarify dependencies. What was implicit becomes explicit. "Explicit is better than implicit," PEP 20 --Zen of Python also states. All components and dependencies are explicitly defined within the container. This provides an overview and control of the application structure. Easy to understand and change.

Is it worth using DI in Python?

It depends on what you build. When using Python as a scripting language, the above advantages are less important. The situation is different when you create an application using Python. The larger the application, the greater the benefits.

Is it worth using the DI framework?

The complexity of implementing DI patterns in Python is lower than in other languages, but it still works. It doesn't mean that you need to use a framework, but using it can be beneficial in the following ways:

--Already implemented --Tested on all platforms and versions --There is a document --There is support --Other engineers also know

Finally a little advice

-** Let's try ** DI is counterintuitive. The first thing that usually comes to mind when we need something is to get it. DI tells us, "Wait, don't get something right now, but write first why you need it." It's like a small investment that will pay off later. The advice is to give it a try for two weeks. That time will be enough to see the effect. If you don't like it, at least you have nothing to lose.

-** Think intuitively ** Use your intuition to adapt DI. This is a good principle, but not a silver bullet. Overdoing it will expose the implementation details. Experience involves practice and time. (DDD and onion architecture may be modified during this period)

Recommended Posts

Meaning of using DI framework in Python
(Bad) practice of using this in Python
Framework development in Python
Summary of Excel operations using OpenPyXL in Python
Development and deployment of REST API in Python using Falcon Web Framework
Basics of I / O screen using tkinter in python3
Equivalence of objects in Python
python: Basics of using scikit-learn ①
Implementation of quicksort in Python
Translate using googletrans in Python
Using Python mode in Processing
DI (Dependency Injection) in Python
Ssh connection memo using ProxyCommand of ssh_config in Python
A memo of writing a basic function in Python using recursion
Pixel manipulation of images in Python
Image capture of firefox using python
Precautions when using pit in Python
Create a DI Container in Python
Division of timedelta in Python 2.7 series
Removal of haze using Python detailEnhanceFilter
MySQL-automatic escape of parameters in python
Try using LevelDB in Python (plyvel)
Implementation of life game in Python
Waveform display of audio in Python
Using global variables in python functions
Let's see using input in python
Infinite product in Python (using functools)
Implementation of desktop notifications using Python
Edit videos in Python using MoviePy
The meaning of ".object" in Django
Law of large numbers in python
Install Python framework django using pip
Implementation of original sorting in Python
Reversible scrambling of integers in Python
[python] -1 meaning of numpy's reshape method
Handwriting recognition using KNN in Python
When using regular expressions in Python
GUI creation in python using tkinter 2
[Question] How to get data of textarea data in real time using Python web framework bottle
Write various forms of phylogenetic tree in Python using ETE Toolkit
Implementation of CRUD using REST API with Python + Django Rest framework + igGrid
Python: Basics of image recognition using CNN
Mouse operation using Windows API in Python
Conversion of string <-> date (date, datetime) in Python
Notes using cChardet and python3-chardet in Python 3.3.1.
Automatic collection of stock prices using python
About building GUI using TKinter of Python
Try using the Wunderlist API in Python
GUI creation in python using tkinter part 1
Check the behavior of destructor in Python
Get Suica balance in Python (using libpafe)
Slowly hash passwords using bcrypt in Python
General Theory of Relativity in Python: Introduction
Try using the Kraken API in Python
Using venv in Windows + Docker environment [Python]
Display a list of alphabets in Python 3
Comparison of Japanese conversion module in Python3
[FX] Hit oanda-API in Python using Docker
Python: Application of image recognition using CNN
Summary of various for statements in Python
Tweet using the Twitter API in Python