Let’s think about dependency injection. In comparison with the java
ecosystem, dependency injection is not commonly used in the python
world.
Dependency injection is a cool technique allowing developers to not care about how the service or component get’s created — they just ask for it and DI
framework will give it to them.
Good DI
Let’s illustrate this principal using Kotlin
language and slightly enhanced google-juice
library. Here is how we get an instance of HttpClient
interface, which is returned by injector
container.
interface HttpClient {
fun get(url: String, query_params_map: Map<String, String>)
}
// how to get an http client
val httpClient = injector.getInstance<HttpClient>()
DI
container knows where the implementation is stored and how to created it. Here is the real implementation:
// real implementation
class HttpClientImpl(val awsCredentials: AwsCredentials): HttpClient {
....
}
We have two components — http client and was credentials, thus we need to declare how they should be created:
class HttpModule: AbstractModule() {
override fun configure() {
binder().bindTo<HttpClient, HttpClientImpl>()
binder().bindToProvider<AwsCredentials, EnvAwsCredentialsFactory>()
}
}
We are telling here that we are going to use HttpClientImple
as HttpClient
interface implementation andAwsCredentials
will be provided by EnvAwsCredentialsFactory
(which is going to read AWS credentials from the env variables).
With kotlin
, java
or any other statically typed language that looks good, feels good and works good. Which is not the case for dynamically typed languages, since we rely on types to inject the proper values.
Let’s take a look at how it can be implemented in python
My Python DI
Let’s play a game and create a by-name injector, where services will be injected by parameter name. Not very useful for any real world project, but a good learning experience.
Here we are going to build a weather forecast service. WeatherForecast
service expects aTemperatureSensor
and a WindSensor
services in order to be instantiated.
class TemperatureSensor:
def get_temperature(self):
return 100.0
class WindSensor:
def get_wind_speed(self):
return 30.0
class WeatherForecast:
def __init__(self, temperature_sensor, wind_sensor):
self.temperature_sensor = temperature_sensor
self.wind_sensor = wind_sensor
def get_forecast(self):
result = []
if self.wind_sensor.get_wind_speed() < 10:
result.append("wind good")
else:
result.append("wind bad")
if self.temperature_sensor.get_temperature() > 30:
result.append("it's hot")
else:
result.append("it's not hot")
return result
Let’s create a DI
container, consisting of two things:
- container with the dependencies
- service initialization function
container = {
"temperature_sensor": TemperatureSensor(),
"wind_sensor": WindSensor()
}
def get_instance(container, clazz):
param_names = {p for p in inspect.signature(clazz.__init__).parameters}.difference({"self"})
args = {}
for param_name in param_names:
args[param_name] = container.get(param_name)
return clazz(**args)
forecast = get_instance(container, WeatherForecast)
print(forecast.get_forecast())
In the given example WeatherForecast
service was initiated and get_forecast
function worked.
Conclusion
When DI
is used everywhere in the project it may come very handy, implementation of a particular class can be replaced in a second, special configurations for tests and different environments can be customized, dependencies become structured and configurable.
Here we explored a DI
concept a little bit by creating a toy container, not suitable for any production application. That is the way!
Do you use DI
for your python projects?