Quantcast
Channel: 初心者タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 21085

Pythonで DI Container を作る

$
0
0

Pythonで DI Container を作る

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

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

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

DEMO

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

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

classAnimal:def__init__(self,kind:str,name:str):self.kind=kindself.name=namedef__str__(self):return"I am {kind}. My name is {name}".format(kind=self.kind,name=self.name)classCat(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.
classPerson:def__init__(self,name:str,role:str):self.name=nameself.role=roledef__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
classDIContainer:concrete_table={}singleton_table={}defregister(self,base_cls:type,concrete_cls:type):ifnotissubclass(concrete_cls,base_cls):raiseTypeError('Concrete class is required {} not {}.'.format(base_cls,concrete_cls))self.concrete_table[base_cls]=concrete_clsdefregister_singleton(self,t:type,instance:object):ifnotisinstance(instance,t):raiseTypeError('Instance type is required {} not {}.'.format(t,type(instance)))self.singleton_table[t]=instancedefget(self,t:type):ifself.is_registered_concrete(t):returnself.concrete_table[t]returntdefget_singleton(self,t:type):ifself.is_registered_singleton(t):returnself.singleton_table[t]raiseKeyError('{} is not registered as singleton.'.format(t))defis_registered_singleton(self,t:type):returntinself.singleton_table.keys()defis_registered_concrete(self,t:type):returntinself.concrete_table.keys()defis_registered(self,t:type):returnself.is_registered_concrete(t)orself.is_registered_singleton(t)defclear(self):self.concrete_table.clear()self.singleton_table.clear()

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

Resolver

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

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

2つだけです。

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

resolver.py
fromcontainerimportDIContainerclassResolver:def__init__(self,container:DIContainer):self.container=containerdefresolve(self,cls:type):ifself.container.is_registered_singleton(cls):returnself.container.get_singleton(cls)cls=self.container.get(cls)init_args=self.resolve_init_args(cls)returncls(**init_args)defresolve_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)forkey,tininit_args_annotations.items():ifself.container.is_registered(t)orlen(defaults)<args_count:result[key]=self.resolve(t)else:result[key]=defaults[len(defaults)-args_count]args_count-=1returnresultdefget_init_args_annotations(cls:type):ifhasattr(cls.__init__,'__annotations__'):returncls.__init__.__annotations__return{}defget_init_default_values(cls:type):ifhasattr(cls.__init__,'__defaults__'):result=cls.__init__.__defaults__return[]ifresultisNoneelseresultreturn[]

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

classPerson:def__init__(self,name:str,age:int):passprint(Person.__init__.__annotaions__)# {'name': <class 'str'> , 'age': <class 'int'>}

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

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

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

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

di.py
fromcontainerimportDIContainerfromresolverimportResolverclassDI:def__init__(self):self.container=DIContainer()self.resolver=Resolver(self.container)defregister_singleton(self,t:type,instance:object):self.container.register_singleton(t,instance)defregister(self,base_cls:type,concrete_cls:type):self.container.register(base_cls,concrete_cls)defresolve(self,t:type):returnself.resolver.resolve(t)defclear(self):self.container.clear()

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

まとめ

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

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

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

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

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


Viewing all articles
Browse latest Browse all 21085

Trending Articles