LoginSignup
2
1

More than 1 year has passed since last update.

PythonでDIするコードを書いてみました。

Last updated at Posted at 2022-05-24

はじめに

DI研究の一環です。
Pythonの標準機能のみでDIを目指します。

コード

先に全体を乗せ、後で詳細に説明していきます。

実装

class DependencyInjection:
  def __init__(self, declarations):
    self.di_container = dict()
    for c in declarations:
      init_param = c.__init__.__annotations__
      correspond = {
        k: self.di_container[v]
        for k, v in init_param.items()
      }
      self.di_container[c] = c(**correspond)

class A:
  def __init__(self):
    self.iam = 'class A'
    self.di = None

class B:
  def __init__(self, cls_a: A):
    self.iam = 'class B'
    self.di = cls_a

class C:
  def __init__(self, cls_b: B):
    self.iam = 'class C'
    self.di = cls_b

class D:
  def __init__(self, cls_b: B, cls_c: C):
    self.iam = 'class D'
    self.di = [cls_b, cls_c]

di = DependencyInjection([A, B, C, D])
print(di.di_container)
print()
for k, v in di.di_container.items():
  print(v.iam)
  print(v.di)

DependencyInjection がDI機能を担うクラスで、クラスA, B, C, Dがそれぞれ依存関係にあるクラスの想定です。
DependencyInjection に依存関係にあるクラス群を依存関係順にリストで渡すことでDIされるようになっています。
このコードを実行すると以下の出力結果になります。

出力結果

{
  <class '__main__.A'>: <__main__.A object at 0x000001FA6FF0BD00>,
  <class '__main__.B'>: <__main__.B object at 0x000001FA6FF0BC70>,
  <class '__main__.C'>: <__main__.C object at 0x000001FA6FF0BC10>,
  <class '__main__.D'>: <__main__.D object at 0x000001FA6FF0BBB0>
}

class A
None
class B
<__main__.A object at 0x000001FA6FF0BD00>
class C
<__main__.B object at 0x000001FA6FF0BC70>
class D
[<__main__.B object at 0x000001FA6FF0BC70>, <__main__.C object at 0x000001FA6FF0BC10>]

メモリIDは実行ごとに異なるかと思いますが、IDを見ていただけばDIされていることがわかるかと思います。

説明

DI処理のメイン部分となる、 DependencyInjection について説明します。

class DependencyInjection:
  def __init__(self, declarations):
    self.di_container = dict()
    for c in declarations:
      init_param = c.__init__.__annotations__
      correspond = {
        k: self.di_container[v]
        for k, v in init_param.items()
      }
      self.di_container[c] = c(**correspond)

受け取ったクラスのリスト declarations

correspond = {
  k: self.di_container[v]
  for k, v in init_param.items()
}

で引数名と引数の型のインスタンスの辞書を作成。( init_param については後述しますので、ここはそういうものとして一旦流してください。)

self.di_container[c] = c(**correspond)

辞書を展開しつつインスタンスを作成し、そのインスタンスを self.di_container にクラスと対応付けてマッピングします。
これのため、 依存関係にあるクラス群を依存関係順にリストで渡す 必要があります。
試していないのでできるかわかりませんが、リストの順番に依存しないDIを解決する一案として両端キューを用います。
リストの先頭を取り出し、すでに self.di_container にクラスがあれば注入、なければリストの末尾に戻す、01-BFSのような方法を草案としてあります。
とはいえ、これをやるならやるで、ループ検知もやる必要があると考えています。難しい。

次、

init_param = c.__init__.__annotations__

について、Pythonのinspect__annotations__を用いています。
これは以下の振る舞いをします。

from typing import (List, Dict)

def f (n: int, str_list: List[str], d: Dict[str, bool]):
  pass

f.__annotations__  # {'n': <class 'int'>, 'str_list': typing.List[str], 'd': typing.Dict[str, bool]}

このように、引数名とその型を辞書形式で取得できます。
これを知ったときPythonでDIをやってみようと思い立ち、本記事につながった次第です。
従って、

correspond = {
  k: self.di_container[v]
  for k, v in init_param.items()
}

が引数名とその型の辞書となるわけです。

おわりに

DIに関しての進捗報告でした。
このDI部分をクラスにしようと考えたのは、このDependencyInjectionを受け取る親DIクラスを作れたらと考えたからです。イメージとしてはAngularのNgModuleです。今回はやりません。また別の機会に回します。
最後に、Pythonのinspectや特殊メソッドが面白いです。終わり。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1