IoC Container in Python using meta programming

Often while working with python I miss having a DI framework (sadly this is not very common in the python world). There are multiple blog posts on the benefits of DI, so I won’t go into that here.

Either way, I was on the lookout for a DI framework for python and this one caught my eye: Dependency Injector. Mainly because of this listed feature:

“Performance. Fast. Written in Cython.”

Which peaked my interest, how much overhead does a DI framework add to your code? So I wrote a small benchmark to test this.

For the benchmark we are requesting the containers to give us an instance of FooService, with the following dependency tree, with Cache, HTTPClient and Config being singletons and the rest being transient:

FooService
- Config
- Database
- Config
- Cache
- Config
- HttpClient
- Config
- BarService
- Database
- Config
- Cache
- Config
- HTTPClient
- Config
FooService
    - Config
    - Database
        - Config
    - Cache
        - Config
    - HttpClient
        - Config
    - BarService
        - Database
            - Config
        - Cache
            - Config
        - HTTPClient
            - Config
FooService - Config - Database - Config - Cache - Config - HttpClient - Config - BarService - Database - Config - Cache - Config - HTTPClient - Config

Enter fullscreen mode Exit fullscreen mode

And these were the results:

$ python3.11 benchmark/run_benchmark.py
Direct (No framework) 563.216755 ns
Dependency Injector Container (Cython) 1183.705037 ns
Rodi Container (Pure Python) 1983.972198 ns
$ python3.11 benchmark/run_benchmark.py 
Direct (No framework) 563.216755 ns
Dependency Injector Container (Cython) 1183.705037 ns
Rodi Container (Pure Python) 1983.972198 ns
$ python3.11 benchmark/run_benchmark.py Direct (No framework) 563.216755 ns Dependency Injector Container (Cython) 1183.705037 ns Rodi Container (Pure Python) 1983.972198 ns

Enter fullscreen mode Exit fullscreen mode

Ok, so compared to directly instantiating the objects ourselves it seems a pure python container is ~4x slower and the cython implementation is ~2x slower.

Which makes sense, since a normal container has some overhead to get a service:

  • Check if service is a singleton or transient
  • If its a singleton check if its already instantiated
  • Find the dependencies for the service and get them (recursively)

Meta programming to the rescue

So how can we make this faster?

After a few failed attempts using closures and meta classes I had the idea to use meta programming and generate the code for the container at runtime. If we already know all services beforehand, we can generate a class with specialized methods for each service we need to instantiate.

Here’s the optimizations we can perform on each methods:

  • No need to check if the service is a singleton or transient
  • No need to find the dependencies for the service at runtime
  • Inlining the code to instantiate the dependencies
    • This saves us the overhead of recursively calling the container to get the dependencies
    • Preloading singletons also avoids the need to check if they are already instantiated

Here’s a snippet of the generated code for the FooService method with all optimizations mentioned above:

<span>def</span> <span>get</span><span>(</span><span>self</span><span>,</span> <span>service_id</span><span>):</span> <span># to provide a clean interface we still have this method as overhead </span> <span>return</span> <span>self</span><span>.</span><span>_service_getter_map</span><span>[</span><span>service_id</span><span>]()</span>
<span>def</span> <span>get_services_FooService</span><span>(</span><span>self</span><span>):</span>
<span>return</span> <span>services</span><span>.</span><span>FooService</span><span>(</span>
<span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span> <span># singleton instance is preloaded and inlined </span> <span>database</span><span>=</span><span>services</span><span>.</span><span>Database</span><span>(</span> <span># transient service is inlined </span> <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span>
<span>),</span>
<span>cache</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Cache</span><span>],</span>
<span>http_client</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>HTTPClient</span><span>],</span>
<span>bar_service</span><span>=</span><span>services</span><span>.</span><span>BarService</span><span>(</span>
<span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span>
<span>database</span><span>=</span><span>services</span><span>.</span><span>Database</span><span>(</span>
<span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span>
<span>),</span>
<span>cache</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Cache</span><span>],</span>
<span>http_client</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>HTTPClient</span><span>],</span>
<span>),</span>
<span>)</span>
    <span>def</span> <span>get</span><span>(</span><span>self</span><span>,</span> <span>service_id</span><span>):</span> <span># to provide a clean interface we still have this method as overhead </span>        <span>return</span> <span>self</span><span>.</span><span>_service_getter_map</span><span>[</span><span>service_id</span><span>]()</span>

    <span>def</span> <span>get_services_FooService</span><span>(</span><span>self</span><span>):</span>
        <span>return</span> <span>services</span><span>.</span><span>FooService</span><span>(</span>
            <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span> <span># singleton instance is preloaded and inlined </span>            <span>database</span><span>=</span><span>services</span><span>.</span><span>Database</span><span>(</span> <span># transient service is inlined </span>                <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span>
            <span>),</span>
            <span>cache</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Cache</span><span>],</span>
            <span>http_client</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>HTTPClient</span><span>],</span>
            <span>bar_service</span><span>=</span><span>services</span><span>.</span><span>BarService</span><span>(</span>
                <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span>
                <span>database</span><span>=</span><span>services</span><span>.</span><span>Database</span><span>(</span>
                    <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span>
                <span>),</span>
                <span>cache</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Cache</span><span>],</span>
                <span>http_client</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>HTTPClient</span><span>],</span>
            <span>),</span>
        <span>)</span>
def get(self, service_id): # to provide a clean interface we still have this method as overhead return self._service_getter_map[service_id]() def get_services_FooService(self): return services.FooService( config=self._singleton_instances[services.Config], # singleton instance is preloaded and inlined database=services.Database( # transient service is inlined config=self._singleton_instances[services.Config], ), cache=self._singleton_instances[services.Cache], http_client=self._singleton_instances[services.HTTPClient], bar_service=services.BarService( config=self._singleton_instances[services.Config], database=services.Database( config=self._singleton_instances[services.Config], ), cache=self._singleton_instances[services.Cache], http_client=self._singleton_instances[services.HTTPClient], ), )

Enter fullscreen mode Exit fullscreen mode

Results

So, did it work? Here’s the results:

$ python3.11 benchmark/run_benchmark.py
Direct 563.216755 ns
MetaDI Container 792.128003 ns
Dependency Injector Container (Cython) 1183.705037 ns
Rodi Container 1983.972198 ns
$ python3.11 benchmark/run_benchmark.py 
Direct 563.216755 ns
MetaDI Container 792.128003 ns
Dependency Injector Container (Cython) 1183.705037 ns
Rodi Container 1983.972198 ns
$ python3.11 benchmark/run_benchmark.py Direct 563.216755 ns MetaDI Container 792.128003 ns Dependency Injector Container (Cython) 1183.705037 ns Rodi Container 1983.972198 ns

Enter fullscreen mode Exit fullscreen mode

Which are pretty good! We managed to be faster than the cython implementation while using pure python.

Conclusion

Even though I believe this performance gain is irrelevant for most applications, it was a fun experiment and I learned a lot about meta programming. I also think this is a good example of how meta programming can be used to optimize code, and I will definitely keep this in mind for future projects.

Source code can be found on Github

原文链接:IoC Container in Python using meta programming

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
Nobody looks down on you because everybody is too busy to look at you.
没谁瞧不起你,因为别人根本就没瞧你,大家都很忙的
评论 抢沙发

请登录后发表评论

    暂无评论内容