15
18

More than 3 years have passed since last update.

Pythonで DI Container を作る

Posted at

Pythonで DI Container を作る

DI Containerとは、クラスのインスタンス生成時に、コンストラクターの引数に自動で適切な型のインスタンスを渡して生成をしてくれるシステムです。
DIパターンの実装を容易にして、より疎結合なコードを表現できます。

詳しくDIパターンについて詳しく知りたい方は別の記事を参照してみてください。
実はとっても簡単なつくりなので頑張ります。

完成したレポジトリーのリンクを張っておきます。
レポジトリー

DEMO

DIクラスに実装クラスはこれですよとか、このクラスはシングルトンで使いますよ、とかを登録します。
その後、DIクラスにこのクラスのインスタンス作ってーと頼むという流れです。

いかのDEMOを見てください。

class Animal:
    def __init__(self, kind: str, name: str):
        self.kind = kind
        self.name = name

    def __str__(self):
        return "I am {kind}. My name is {name}".format(kind=self.kind, name=self.name)


class Cat(Animal):
    def __init__(self, name: str = "Cathy"):
        super().__init__('Cat', name)


di = DI()
di.register(Animal, Cat)
print(di.resolve(Animal))
# > I am Cat. My name is Cathy.


class Person:
    def __init__(self, name: str, role: str):
        self.name = name
        self.role = role

    def __str__(self):
        return "I am {name}. My role is {role}.".format(name=self.name, role=self.role)

nukumizu = Person('Nukumizu', 'Actor')
di.register_singleton(Person, nukumizu)
print(di.resolve(Person))
# > I am Nukumizu. My role is Actor.

全体像

DI Containerに最低限必要なのは以下の3つです。

  • 対応するクラスやシングルトンを登録するためのContainerクラス
  • 登録されているクラス等を生成するResolverクラス
  • 上の二つのファサードクラスのDIクラス

では実際のコードを順番に見ていきましょう。

Container

ではまずContainerクラスから見ていきましょう。
このクラスは以下の機能を提供します。

  • ベースクラスに対して実装クラスを登録する機能
  • シングルトンインスタンスを登録する機能
  • ベースクラスから実装クラスを取得する機能
  • 登録されているシングルトンインスタンスを取得する機能
  • 登録されているか確認する機能
  • 登録されているデータをClearする機能

では実際のコードを見ていきましょう。

container.py
class DIContainer:
    concrete_table = {}
    singleton_table = {}

    def register(self, base_cls: type, concrete_cls: type):
        if not issubclass(concrete_cls, base_cls):
            raise TypeError('Concrete class is required {} not {}.'.format(base_cls, concrete_cls))
        self.concrete_table[base_cls] = concrete_cls

    def register_singleton(self, t: type, instance: object):
        if not isinstance(instance, t):
            raise TypeError('Instance type is required {} not {}.'.format(t, type(instance)))
        self.singleton_table[t] = instance

    def get(self, t: type):
        if self.is_registered_concrete(t):
            return self.concrete_table[t]
        return t

    def get_singleton(self, t: type):
        if self.is_registered_singleton(t):
            return self.singleton_table[t]

        raise KeyError('{} is not registered as singleton.'.format(t))

    def is_registered_singleton(self, t: type):
        return t in self.singleton_table.keys()

    def is_registered_concrete(self, t: type):
        return t in self.concrete_table.keys()

    def is_registered(self, t: type):
        return self.is_registered_concrete(t) or self.is_registered_singleton(t)

    def clear(self):
        self.concrete_table.clear()
        self.singleton_table.clear()

正直登録するだけなので特筆して説明できる場所がないかなと思います。
コードが読みにくければすいません。

Resolver

次はResolverクラスを見ていきましょう。
このクラスは以下の機能を提供します。

  • 登録されているクラスを生成する機能
  • クラスのinitの引数のインスタンスを生成する機能

2つだけです。

では実際のコードを見ていきましょう。

resolver.py
from container import DIContainer


class Resolver:
    def __init__(self, container: DIContainer):
        self.container = container

    def resolve(self, cls: type):
        if self.container.is_registered_singleton(cls):
            return self.container.get_singleton(cls)
        cls = self.container.get(cls)
        init_args = self.resolve_init_args(cls)
        return cls(**init_args)

    def resolve_init_args(self, cls: type):
        init_args_annotations = get_init_args_annotations(cls)
        defaults = get_init_default_values(cls)
        result = {}
        args_count = len(init_args_annotations)
        for key, t in init_args_annotations.items():
            if self.container.is_registered(t) or len(defaults) < args_count:
                result[key] = self.resolve(t)
            else:
                result[key] = defaults[len(defaults) - args_count]
            args_count -= 1

        return result


def get_init_args_annotations(cls: type):
    if hasattr(cls.__init__, '__annotations__'):
        return cls.__init__.__annotations__
    return {}


def get_init_default_values(cls: type):
    if hasattr(cls.__init__, '__defaults__'):
        result = cls.__init__.__defaults__
        return [] if result is None else result
    return []

クラス外にあるget_init_args_annotationsの説明をします。
DIContainerを作成する際に必要なのがコンストラクターの引数の情報です。
これがないと始まりません。
pythonでコンストラクタの情報を取得する方法がannotationsです。
これで引数名がKeyでクラスタイプがValueの連想配列を取得できます。
以下に例を示します。

class Person:
    def __init__(self, name: str, age: int):
        pass

print(Person.__init__.__annotaions__)
# {'name': <class 'str'> , 'age': <class 'int'>}

_annotations_で取得した情報をもとにクラスのインスタンスを生成します。

では生成の処理回りのフローを確認しましょう。

  1. まず対象のクラスがシングルトンに登録されていればそのインスタンスをそのまま返すだけです。
  2. 次にContainerに対象のクラスの実装クラスが登録されているか確認します。
  3. 登録されている場合は実装クラスのインスタンスを生成するようにします。
  4. 生成するクラスのコンストラクタに引数が必要な場合は引数のクラスのインスタンスを先に生成します。(つまり、引数のクラスをresolve関数で生成してもらうということです。)
  5. 深さ優先で引数のクラスのインスタンスをすべての生成したら、対象のクラスの引数に渡し生成します。

再帰的にクラスを生成していく感じですね。
これでDI Containerとしての機能はそろいましたので、ファサードを作れば利用できます。

di.py
from container import DIContainer
from resolver import Resolver


class DI:
    def __init__(self):
        self.container = DIContainer()
        self.resolver = Resolver(self.container)

    def register_singleton(self, t: type, instance: object):
        self.container.register_singleton(t, instance)

    def register(self, base_cls: type, concrete_cls: type):
        self.container.register(base_cls, concrete_cls)

    def resolve(self, t: type):
        return self.resolver.resolve(t)

    def clear(self):
        self.container.clear()

ここまでくればDEMOのようにDI Containerを使うことができます。

まとめ

一度理解してしまえば、使い方を忘れることが少なくなると思います。
自分はDIパターンを知って、Interface関係の取扱いに関する疑問がいくつか解決しました。

前の職場で自社ライブラリーにDI Containerを導入したことでいちいちクラスの生成方法を確認しなくて済むのはめちゃくちゃ便利でした。
もちろんデメリットもありますが便利なのには変わりないので使えるときは使っていこうかと思います。

完成したレポジトリーのリンクを張っておきます。
レポジトリー

何か不備等あれば教えていただければ幸いです。

最後までお付き合いいただきありがとうございました。

15
18
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
15
18