想定環境
- Python 3.6.5
依存とは
プログラミングの文脈で語られる依存とは、大抵の場合、特定のクラスの内部で使用される値が、外部の何か(変数、定数、クラスのインスタンスなど)に依存している状態を指します。
オブジェクト指向の説明でよく使われがちな車の説明で書くと、
# conding: utf-8
class tire():
def rotate(self, angle: int) -> int:
return angle
class handle():
def rotate(self, angle: int) -> int:
return angle
class car():
t = tire()
h = handle()
def drive(self, angle: int) -> int:
return self.t.rotate(self.h.rotate(angle))
if __name__ == '__main__':
human_handle_control = 10
c = car()
print(c.drive(human_handle_control))
10
人がハンドルを操作し、車はハンドルから回転角の入力を受け、適正なタイヤの回転角に変換して、タイヤに出力します。
車は、タイヤとハンドルが無ければクラスとして成り立つことが出来ず、タイヤとハンドルに依存しています。
* データの流れから言えば、human_handle_controlはhandleクラスが受け取って内部で値を保持し、rotate関数ではその値を出力のみするべき(引数でのangle受け取りは正しくない)ですが、本題ではないのでこのままいきます。
* さらに、データの流れから言えば、本来はタイヤが車に依存し、車がハンドルに依存し、ハンドルは人に依存するべきですが、これも本題ではないのでこのままいきます。
DI 依存性の注入 とは
「依存性の注入」という日本語がジッサイ良くないという話もちらほら聞きます。
実態としては、「必要なものを外部からの注入にする」ことが依存性の注入と呼ばれています。
一番簡単な例では、
# conding: utf-8
class tire:
def rotate(self, angle: int) -> int:
return angle
class handle:
def rotate(self, angle: int) -> int:
return angle
class car:
def __init__(self, t: tire, h: handle):
self.t = t
self.h = h
def drive(self, angle: int) -> int:
return self.t.rotate(self.h.rotate(angle))
if __name__ == '__main__':
human_handle_control = 10
t = tire()
h = handle()
c = car(t, h)
print(c.drive(human_handle_control))
10
内部で生成していたインスタンスを、外部から受け取る形に変更しています。
つまり依存している先を外部から注入出来るようになっています。
これで何が嬉しいかと言うと、同じメソッドを持つ別のインスタンスでもこのクラスで使うことが出来るようになり、依存していた互いのクラスの結合度が下がり、より汎用性の高いクラスにすることが出来ました。
タイヤやハンドルは車に使うパーツの種類ですが、実際の製品は同じタイヤやハンドルでもたくさんのものが売られています。
履き古したタイヤやパンクしたタイヤを新品に交換したり、レースに使う車だと走行の踏破性や安定性をテストするのにいくつものタイヤを使ったりするでしょう。
こうやってパーツを交換出来るようにしておくことは、開発時テスト時を問わず大変素晴らしいメリットになります。
Pythonのinterface代替モジュール abc と 依存性の逆転
依存性の注入が出来るようになると、依存先を変更することが出来るようになります。
依存先を変更出来るということは、依存先を抽象化することが出来ます。
抽象化した依存先を新たに作成し、依存していた側も依存されていた側も抽象に依存するようにすることが依存性の逆転です。
依存されていた側が依存する側に回ることが出来るようになり、依存性の注入で弱めた結合を更に弱めることが出来ます。
他言語での依存性の逆転は、interfaceを使ってメソッドの実装を強制し、クラスに直接依存していたのをinterfaceへの依存にすることで依存性の逆転を実現しますが、Pythonにはinterfaceが無いので、類似の機能を実現できる標準モジュール abc(Abstract Base Class) を利用します。
# conding: utf-8
from abc import ABCMeta, abstractmethod
class abstractTire(metaclass=ABCMeta):
@abstractmethod
def rotate(self, angle: int) -> int:
return angle
class abstractHandle(metaclass=ABCMeta):
@abstractmethod
def rotate(self, angle: int) -> int:
return angle
class tire(abstractTire):
def rotate(self, angle: int) -> int:
return angle
class handle(abstractHandle):
def rotate(self, angle: int) -> int:
return super(handle, self).rotate(angle)
class car:
def __init__(self, t: abstractTire, h: abstractHandle):
self.t = t
self.h = h
def drive(self, angle: int) -> int:
return self.t.rotate(self.h.rotate(angle))
if __name__ == '__main__':
human_handle_control = 10
t = tire()
h = handle()
c = car(t, h)
print(c.drive(human_handle_control))
10
ABCMeta
はクラスを抽象クラスとして扱うようにし、@abstractmethod
デコレータはメソッドを実装すべき抽象メソッドとして扱うようにします。
抽象クラスとして abstractTire
、abstractHandle
を追加し、抽象メソッド rotate
を持たせました。
car
クラスではこの抽象クラスを受け入れることを明示することで、rotate
メソッドを持っていることを要求します。
* ここで、同じ名前のメソッドを持っていればいいのなら抽象クラスも同じものでいいのでは、となるのは良くないです。機能や動作が似通っていても、本来の役割が別の物であれば、そこはやはり分離されていなければなりません。
次に、試しに handle
クラスから rotate
メソッドを削除してみました。
# 前略
class handle(abstractHandle):
pass
# 後略
Traceback (most recent call last):
File "main.py", line 30, in <module>
h = handle()
TypeError: Can't instantiate abstract class handle with abstract methods rotate
抽象メソッド rotate
を持っている handle
クラスをインスタンス化出来なかった、というエラーが出ます。
これで abstractTire
、abstractHandle
を継承したクラスは rotate
メソッドを持つことを強制できます。
ただ、Pythonのタイプヒントで利用できる型アノテーションは、実行時チェックが無いので、例えば rotate
メソッドを持っている抽象クラスを継承 していない gear クラスなんかを実装してタイヤの代わりに渡すと、これは動いてしまいます。
# 前略
class gear:
def rotate(self, angle: int):
return angle
if __name__ == '__main__':
human_handle_control = 10
t = gear()
h = handle()
c = car(t, h)
print(c.drive(human_handle_control))
10
これをちゃんと抽象クラスを継承していないと通らないようにするために isinstance
関数を利用します。
# 前略
class car:
def __init__(self, t: abstractTire, h: abstructHandle):
if not isinstance(t, abstractTire):
raise Exception("t is not abstractTire.")
if not isinstance(h, abstructHandle):
raise Exception("h is not abstructHandle.")
self.t = t
self.h = h
def drive(self, angle: int) -> int:
return self.t.rotate(self.h.rotate(angle))
if __name__ == '__main__':
human_handle_control = 10
t = gear()
h = handle()
c = car(t, h)
print(c.drive(human_handle_control))
# 後略
Traceback (most recent call last):
File "main.py", line 41, in <module>
c = car(t, h)
File "main.py", line 29, in __init__
raise Exception("t is not abstractTire.")
Exception: t is not abstractTire.
gear
クラスのインスタンスを渡すと、abstractTire
を継承していないインスタンスだとして例外エラーになりました。
これで、car
クラスのコンストラクタに渡される引数は abstractTire
、abstractHandle
を継承していることが保証され、abstractTire
、abstractHandle
を継承しているということは rotate
メソッドを持っていることが保証されることになりました。
依存関係をおさらいしてみると、car
クラスは、具象クラスの tire
、handle
に依存していたのを離れて、抽象クラス abstractTire
、abstractHandle
への依存に切り替えました。
具象クラス tire
、handle
から見ると、どこにも依存しておらず、依存される側でしたが、こちらも抽象クラス abstractTire
、abstractHandle
の存在に依存するように切り替わっています。
抽象クラス abstractTire
、abstractHandle
は、car
クラス "が" 内部で利用するための定義なので、car
クラスと同じ名前空間にするのが自然です。
tire
、handle
はそれぞれ別の名前空間であるべきなので、これを図にしてみると、car
がtire
、handle
に依存していたのが、tire
、handle
が car
に依存するようになっている、依存の方向が逆転しているのが見て取れます。
* 依存性の逆転は、正しくは「依存関係逆転の原則」と言います。SOLID原則の一つで、依存性の注入とは出自が違うのですが、依存について知っておくべきものとして並べて書いています。(2018/07/13追記)
PythonのDIライブラリ
ここまでで、依存性を外部から注入するようにし、依存性の逆転を行うようにしました。
そうすることで密結合を疎結合に切り離し、依存の関係を外部から管理することが可能になりました。
次は、依存関係の管理を自動化します。
QiitaではPythonのDIについてあまり議題として挙がっていないようですが、ライブラリはいくつもあるので今回は筆者が気になったPythonのDIライブラリを紹介します。
Injector
alecthomas/injector: Python dependency injection framework, inspired by Guice
Qiita記事
PythonでDI(Dependency Injection) - Qiita
# conding: utf-8
from abc import ABCMeta, abstractmethod
from injector import Injector, inject, Module
class abstractTire(metaclass=ABCMeta):
@abstractmethod
def rotate(self, angle: int) -> int:
return angle
class abstractHandle(metaclass=ABCMeta):
@abstractmethod
def rotate(self, angle: int) -> int:
return angle
class tire(abstractTire):
def rotate(self, angle: int) -> int:
return angle
class handle(abstractHandle):
def rotate(self, angle: int) -> int:
return super(handle, self).rotate(angle)
class gear():
def rotate(self, angle: int) -> int:
return angle
class car:
@inject
def __init__(self, t: abstractTire, h: abstractHandle):
if not isinstance(t, abstractTire):
raise Exception("t is not abstractTire.")
if not isinstance(h, abstractHandle):
raise Exception("h is not abstructHandle.")
self.t = t
self.h = h
def drive(self, angle: int) -> int:
return self.t.rotate(self.h.rotate(angle))
class carDIModule(Module):
def configure(self, binder):
binder.bind(abstractTire, to=tire)
binder.bind(abstractHandle, to=handle)
if __name__ == '__main__':
injector = Injector([carDIModule()])
c = injector.get(car)
human_handle_control = 10
print(c.drive(human_handle_control))
10
Injectorのインスタンス化で依存関係の設定の呼び出し、Module
クラスを継承したクラスで実際の依存関係の設定を行い、コンストラクタに @inejct
デコレータを付与したクラスをinjector.get
で取得することで、実際の依存関係の解決を行っています。
依存関係の更新の際には、基本的には Module
クラスの継承クラスだけを触っていけば良いので、ミスも少なく分かりやすい更新が行なえます。
python-inject
ivankorobkov/python-inject: Python dependency injection
Qiita記事
Python で DDD するなら Inject がオススメ - Qiita
# conding: utf-8
from abc import ABCMeta, abstractmethod
import inject
class abstractTire(metaclass=ABCMeta):
@abstractmethod
def rotate(self, angle: int) -> int:
return angle
class abstractHandle(metaclass=ABCMeta):
@abstractmethod
def rotate(self, angle: int) -> int:
return angle
class tire(abstractTire):
def rotate(self, angle: int) -> int:
return angle
class handle(abstractHandle):
def rotate(self, angle: int) -> int:
return super(handle, self).rotate(angle)
class gear():
def rotate(self, angle: int) -> int:
return angle
class car:
@inject.params(t=abstractTire, h=abstractHandle)
def __init__(self, t: abstractTire, h: abstractHandle):
if not isinstance(t, abstractTire):
raise Exception("t is not abstractTire.")
if not isinstance(h, abstractHandle):
raise Exception("h is not abstructHandle.")
self.t = t
self.h = h
def drive(self, angle: int) -> int:
return self.t.rotate(self.h.rotate(angle))
def inject_config(binder):
binder.bind(abstractTire, tire())
binder.bind(abstractHandle, handle())
if __name__ == '__main__':
inject.configure(inject_config)
human_handle_control = 10
c = car()
print(c.drive(human_handle_control))
10
こちらは inject.configure
で依存関係を設定し、@inject.params
という関数デコレータで依存関係の解決を行っています。
コンストラクタの引数と関数デコレータで同じことを二度書いているのが少々気になります。
また、Injectは別の利用法もあります。
# 前略
class car:
t = inject.attr(abstractTire)
h = inject.attr(abstractHandle)
def drive(self, angle: int) -> int:
return self.t.rotate(self.h.rotate(angle))
# 後略
10
inject.configureで依存関係の設定後、コンストラクタの関数デコレータではなく、クラス変数で直接依存関係の解決を行っています。
一見すると依存性の注入に反しているように見えるかもしれませんが、inject自体がクラス外のものなので、外部からの値の注入(注入するものの管理はinjectが行えている)が出来ています。
Injectを利用するならこれが本命かもと一瞬思ったのですが、
# 前略
def inject_config(binder):
binder.bind(abstractTire, handle())
binder.bind(abstractHandle, handle())
# 後略
10
isinstance
のチェックが出来ないため、わざと間違った型を渡してもそれだけではエラーになりません。
実用は避けたほうが無難なようです。
その他
他にも、Google先生のPinject、python-dependency-injector、
di-py、siringa、py3njection 等々、GitHubをpython dependency injectionで検索すると105も出てくる(2018/07/08時点)のですが、メンテされていないものや抽象クラスを考慮していないものも多いようで、そういうものは引数の名前と具象クラスを直接紐付けたり、型に指定された具象クラスのインスタンスを取得して注入することを主機能としています。
引数の型には抽象クラスを指定し、抽象クラスと具象クラスの紐付けを行い、抽象クラスを見て具象クラスのインスタンスを注入、が出来ないと、依存性の逆転には対応出来ていないことになります。
依存性の注入自体は出来ているのでDIツールの動作としては正しいのですが、テストに強くメンテナンス性の高いより良いコードを書くには、残念ながら少々機能不足感は否めないです。