個人開発中、依存先が多いクラスをインスタンス化する場面がありました。
その時は依存先を全部インスタンス化→依存元をインスタンス化する時に引数で渡して実装しました。
1度だけなら問題になりませんが毎回この手順で実装する場合、実装の手間が増える上に可読性も下がります。 この問題を解消するためにDIコンテナを採用します。
DIコンテナをざっくりと説明すると、「インスタンス生成時に依存関係をいい感じに解決してくれる仕組み」です。
今回は injector
モジュールを利用してPythonでDIコンテナを実現します。
環境
- Python3.8.10
- injector 0.19.0
injectorの採用理由
- 使用例を理解しやすかった
- 採用事例が多い(体感ですが)
- Pure Pythonだけで完結するため特定FWに依存しない
- 直近のコミットが2021年10月と比較的新しく、今後もメンテナンスが続くと見込まれる
PythonのDI用ライブラリとしてinjectorの他には python-injector・siringa・di-py がありますがいずれもStar数が少なくコミットも止まっているため不採用としました。
インストール手順
下記コマンドでインストールします。
$ pip install injector
コマンドが終了したら正常にインストールできているか確認します。
$ pip show injector
Name: injector
Version: 0.19.0
Summary: Injector - Python dependency injection framework, inspired by Guice
Home-page: https://github.com/alecthomas/injector
Author: Alec Thomas
Author-email: alec@swapoff.org
License: BSD
Location: /usr/local/lib/python3.8/dist-packages
Requires: typing-extensions
Required-by:
上のように表示されれば成功です。
使用例
DIコンテナを実装するには以下の4要素が出てきます。
- 依存元クラス
- 依存対象のインターフェースおよびその実装クラス 1
- インターフェースと具象クラスの紐づけを記述するクラス
- 依存元クラスの呼び出し用モジュール
各モジュールのサンプルコードが以下になります。
1. 依存元クラス
# ① injectorモジュールからinjectデコレータをインポート
from injector import inject
from Animal import Animal
class UseCase():
# 依存先オブジェクトの注入対象であることをinjectデコレータで宣言
@inject
def __init__(self, animal: Animal) -> None: # ③型アノテーションは必須
self.animal = animal
def action(self) -> None:
print(f'{self.animal.cry()}')
① injectorモジュールからinjectデコレータをインポート
特に言及することはありません。
② 依存先オブジェクトの注入対象であることをinjectデコレータで宣言
依存先が注入されることを明記します。
③ 型アノテーション
型アノテーションで依存先クラスを指定しないと動作しないので注意です。
依存対象のインターフェースおよびその実装クラス
from abc import ABCMeta, abstractmethod
# 抽象クラス
class Animal(metaclass=ABCMeta):
@abstractmethod
def cry(self) -> None:
raise NotImplementedError
# 抽象クラスを実装した具象クラス
class Cat(Animal):
def __init__(self):
pass
def cry(self) -> str:
return 'cry meow meow.'
依存対象のインターフェースとその実装クラスは特に特筆する点はありません。
Pythonにはインターフェースがないので抽象クラスで代用しています。
インターフェースと具象クラスの紐づけを記述するクラス
DIコンテナの実装において最重要なクラスです。
from injector import Injector
from Animal import Animal, Cat
from sample2 import A
class Dependency():
def __init__(self) -> None:
# 依存関係を設定する関数を読み込む
self.injector = Injector(self.__class__.config)
# 依存関係を設定するメソッド
@classmethod
def config(cls, binder):
# 抽象クラスをインスタンス化する際にCatクラスを使うよう登録する
binder.bind(Animal, Cat)
# injector.get()に引数を渡すと依存関係を解決してインスタンスを生成する
def resolve(self, cls):
return self.injector.get(cls)
ポイントはbinder.bind()
で「この抽象クラスをインスタンス化する時はこの具象クラスを使う」と設定している箇所です。2
依存元クラスの呼び出し用モジュール
# importは省略
if __name__ == "__main__":
# Dependency クラスをインスタンス化
injector = Dependency()
# Aクラスのインスタンスを生成
a = injector.resolve(A)
print(vars(a))
実行結果は以下の通りです。
$ python3 injector_sample/main.py
{'animal': <Animal.Cat object at 0x7f28157be460>}
Catクラスインスタンスを初期化することなくAクラスインスタンスを生成することができました。
AクラスインスタンスのプロパティとしてCatクラスインスタンスも保持されています。
injectorクラスを使えばAクラスインスタンスを利用できる状態になっています。
これにより、利用する側はAクラスの知識を持たずに済みます。
まとめ
DIコンテナを使うことでプログラムをより可読性と保守性の高い状態に保つことができます。
参考資料
Injector API reference
Pythonライブラリinjectorの使い方
Python の DI コンテナ実装の紹介と活用例