🐍

PythonのDI(Dependency Injection)ライブラリInjectorとSpringの比較

2025/01/02に公開

背景

最近、Pythonを触る機会があり、オニオンアーキテクチャを意識してコードを書いているのですが、DI(Dependency Injection)を適用したくなってきました。普段仕事では、Java+Springで開発しているので、DIコンテナは意識せずとも利用しています。PythonでもDIコンテナのライブラリがあるのか調べてみたところ、Injectorというライブラリを紹介している人が多そうに思ったので、今回はこれを使ってみることにしました。使い方について、理解するまで少々戸惑ってしまったのですが、普段Springを触っている私にとってはSpringと対応させて考えると理解しやすいと思ったのでメモとして残しておきます。

https://github.com/python-injector/injector

@Autowired

Springではインジェクションする対象に @Autowired というアノテーションを付与します。Springではインジェクションの方法は以下の3種類があります。

  • コンストラクタインジェクション
  • フィールドインジェクション
  • セッターインジェクション

例えば、コンストラクタインジェクションではコンストラクタに @Autowired というアノテーションをつけます。

class MyService {
    private final MyRepository myRepository;

    @Autowired
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }
}

Injectorでは、コンストラクタインジェクションを行う場合は __init__ メソッドに @inject というデコレータをつけます。

from injector import inject

class MyService:
    @inject
    def __init__(self, my_repository: MyRepository):
        self.my_repository = my_repository

セッターインジェクション

コード例をどこで見たか分からなくなってしまいましたが、Injectorではメソッドに対しても @inject というデコレータをつけることでインジェクションすることができるようです。これが正しいとすると、これを利用してSpringでいうセッターインジェクションと同等のことができそうです。ただ、基本的にはコンストラクタインジェクションで事足りるのではないかと思います。

Injectorでフィールドインジェクションが可能かは分かりませんでした。

Beanの登録

Springでは、DIコンテナが管理しているオブジェクトのことをBeanと呼びます。Beanに登録する方法は以下の3種類があります。

  • ステレオタイプアノテーション
  • @Beanメソッド
  • <bean>タグ(ここでは割愛)

ステレオタイプアノテーション

ステレオタイプアノテーションとは、 @Component などのアノテーションをクラスに付与することで、コンポーネントスキャンの対象となり、DIコンテナが管理するオブジェクトとして扱われるというものです。

from injector import Injector, inject
class Inner:
    def __init__(self):
        self.forty_two = 42

class Outer:
    @inject
    def __init__(self, inner: Inner):
        self.inner = inner

injector = Injector()
outer = injector.get(Outer)
outer.inner.forty_two

Injectの場合、公式のREADMEに記載されている上記のサンプルを見ても、Innerクラスには @inject@provider などのInject固有の特別なことはしていませんが、Outerクラスの __init__ から必要とされる際に、Innerクラスのインスタンスが生成されることが分かります。Springと異なりステレオタイプアノテーションや@Beanメソッドで明示的にBean登録せずともインジェクションが行われるようです。

@Beanメソッド

@Bean
public MyService myService(MyRepository myRepository) {
    return new MyService(myRepository);
}

@BeanメソッドはJavaConfigクラス(@Configurationを付与したクラス)内で定義され、このメソッドをDIコンテナが自動的に呼び出し、返したオブジェクトがBeanとして管理されるというものです。

Injectの場合、ModuleがJavaConfigクラスに対応するものであり、 @provider@Bean に対応するものであると理解しています。

class RequestHandler:
  @inject
  def __init__(self, db: sqlite3.Connection):
    self._db = db

  def get(self):
    cursor = self._db.cursor()
    cursor.execute('SELECT key, value FROM data ORDER by key')
    return cursor.fetchall()

class DatabaseModule(Module):
  @singleton
  @provider
  def provide_sqlite_connection(self, configuration: Configuration) -> sqlite3.Connection:
    conn = sqlite3.connect(configuration.connection_string)
    cursor = conn.cursor()
    cursor.execute('CREATE TABLE IF NOT EXISTS data (key PRIMARY KEY, value)')
    cursor.execute('INSERT OR REPLACE INTO data VALUES ("hello", "world")')
    return conn

injector = Injector([configure_for_testing, DatabaseModule()]) # DatabaseModuleを利用
handler = injector.get(RequestHandler)
tuple(map(str, handler.get()[0]))

RequestHandlerは __init__@inject が付与されており、sqlite3.Connection に依存していることが示されているため、DIコンテナはこれを注入しようとします。この際、sqlite3.Connection を返す @provider が付与されたメソッドが存在するため、DIコンテナはこれを利用してオブジェクトを生成します。このとき、DIコンテナは @provider が付与されたメソッドの戻り値の型を見て判断していると思われます。また、Springの@Beanメソッドの場合は、デフォルトではメソッド名がBean名になりますが、 @provider の場合はメソッド名は特に意味をなさずプログラマが任意につけることができるのではないかと思っています。

さらに、@singleton が付与されているため、injector.get(RequestHandler) が複数回呼び出されても複数回オブジェクトを生成することはせず、同一のオブジェクトが返されます。

injector.get(sqlite3.Connection) is injector.get(sqlite3.Connection) # -> True

インターフェースと実装クラスの紐付け

Springでは、インターフェースに対してBean登録されている実装クラスが1つしかない場合、自動的にインターフェースと実装クラスの紐付けを行います。

public interface MyRepository {
    void save(String value);
}
@Repository
public class MyRepositoryImpl implements MyRepository {
    @Override
    public void save(String value) {
        // 実装
    }
}
@Service
public class MyService {
    private final MyRepository myRepository;

    // Bean登録されているMyRepositoryの実装クラスがMyRepositoryImplしか存在しないため、
    // 設定などは書かなくてもmyRepositoryにはMyRepositoryImplのインスタンスが注入される
    // @Autowired コンストラクタが1つの場合は省略可能
    public MyService(MyRepository myRepository) {
        this.myRepository = myRepository;
    }
}

Injectorでは、このような紐付けは自動では行われないため、Binder を利用して紐付けの設定を行う必要があるようです。

from abc import abstractmethod, ABC

from injector import Binder, Module, Injector


class MyRepository(ABC):
    @abstractmethod
    def save(self, value: str):
        pass

class MyRepositoryImpl(MyRepository):
    def save(self, value: str):
        print(f"Saving {value}")

class MyModule(Module):
    def configure(self, binder: Binder):
        binder.bind(MyRepository, to=MyRepositoryImpl)

injector = Injector([MyModule()])
my_repository = injector.get(MyRepository)
my_repository.save("some value") # -> "Saving some value"

以下のようにModuleを渡さずに実行するとエラーが発生する

injector = Injector()
my_repository = injector.get(MyRepository)
injector.CallError: Call to ABCMeta.__new__() failed: Can't instantiate abstract class MyRepository without an implementation for abstract method 'save' (injection stack: [])

まとめ

自分の頭の整理のために、Springの知識を元にして、PythonのInjectorの挙動を理解してみました。Injectorについて、まだ理解が足りないところも多々ありそうですが、本エントリを書くことで頭の整理にはなったかなと思います。

参考サイト

https://github.com/python-injector/injector/blob/0.21.0/README.md
https://qiita.com/mkgask/items/d984f7f4d94cc39d8e3c
https://zenn.dev/ktnyt/articles/cc5056ce81e9d3
https://blog.mmmcorp.co.jp/2024/06/10/python-dependency-injection/

Discussion