LoginSignup
163
128

More than 5 years have passed since last update.

PythonでのDependency Injection 依存性の注入

Last updated at Posted at 2018-07-07

想定環境

  • Python 3.6.5

依存とは

プログラミングの文脈で語られる依存とは、大抵の場合、特定のクラスの内部で使用される値が、外部の何か(変数、定数、クラスのインスタンスなど)に依存している状態を指します。

オブジェクト指向の説明でよく使われがちな車の説明で書くと、

test.py
# 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

人がハンドルを操作し、車はハンドルから回転角の入力を受け、適正なタイヤの回転角に変換して、タイヤに出力します。
車は、タイヤとハンドルが無ければクラスとして成り立つことが出来ず、タイヤとハンドルに依存しています。

a1

データの流れから言えば、human_handle_controlはhandleクラスが受け取って内部で値を保持し、rotate関数ではその値を出力のみするべき(引数でのangle受け取りは正しくない)ですが、本題ではないのでこのままいきます。

さらに、データの流れから言えば、本来はタイヤが車に依存し、車がハンドルに依存し、ハンドルは人に依存するべきですが、これも本題ではないのでこのままいきます。

DI 依存性の注入 とは

「依存性の注入」という日本語がジッサイ良くないという話もちらほら聞きます。
実態としては、「必要なものを外部からの注入にする」ことが依存性の注入と呼ばれています。

一番簡単な例では、

test2.py
# 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) を利用します。

test3.py
# 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 デコレータはメソッドを実装すべき抽象メソッドとして扱うようにします。

抽象クラスとして abstractTireabstractHandle を追加し、抽象メソッド rotate を持たせました。
car クラスではこの抽象クラスを受け入れることを明示することで、rotate メソッドを持っていることを要求します。

ここで、同じ名前のメソッドを持っていればいいのなら抽象クラスも同じものでいいのでは、となるのは良くないです。機能や動作が似通っていても、本来の役割が別の物であれば、そこはやはり分離されていなければなりません。

次に、試しに handle クラスから rotate メソッドを削除してみました。

test3.py
# 前略

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 クラスをインスタンス化出来なかった、というエラーが出ます。

これで abstractTireabstractHandle を継承したクラスは rotate メソッドを持つことを強制できます。

ただ、Pythonのタイプヒントで利用できる型アノテーションは、実行時チェックが無いので、例えば rotate メソッドを持っている抽象クラスを継承 していない gear クラスなんかを実装してタイヤの代わりに渡すと、これは動いてしまいます。

test3.py
# 前略

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 関数を利用します。

test3.py
# 前略

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 クラスのコンストラクタに渡される引数は abstractTireabstractHandle を継承していることが保証され、abstractTireabstractHandle を継承しているということは rotate メソッドを持っていることが保証されることになりました。

依存関係をおさらいしてみると、car クラスは、具象クラスの tirehandleに依存していたのを離れて、抽象クラス abstractTireabstractHandle への依存に切り替えました。
具象クラス tirehandle から見ると、どこにも依存しておらず、依存される側でしたが、こちらも抽象クラス abstractTireabstractHandle の存在に依存するように切り替わっています。

抽象クラス abstractTireabstractHandle は、car クラス "が" 内部で利用するための定義なので、car クラスと同じ名前空間にするのが自然です。
tirehandle はそれぞれ別の名前空間であるべきなので、これを図にしてみると、cartirehandle に依存していたのが、tirehandlecar に依存するようになっている、依存の方向が逆転しているのが見て取れます。

a2

依存性の逆転は、正しくは「依存関係逆転の原則」と言います。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

injector_test.py
# 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

inject_test.py
# 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は別の利用法もあります。

inject_test.py
# 前略

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を利用するならこれが本命かもと一瞬思ったのですが、

inject_test.py
# 前略

def inject_config(binder):
    binder.bind(abstractTire, handle())
    binder.bind(abstractHandle, handle())

# 後略
結果
10

isinstance のチェックが出来ないため、わざと間違った型を渡してもそれだけではエラーになりません。
実用は避けたほうが無難なようです。

その他

他にも、Google先生のPinjectpython-dependency-injector
di-pysiringapy3njection 等々、GitHubをpython dependency injectionで検索すると105も出てくる2018/07/08時点)のですが、メンテされていないものや抽象クラスを考慮していないものも多いようで、そういうものは引数の名前と具象クラスを直接紐付けたり、型に指定された具象クラスのインスタンスを取得して注入することを主機能としています。
引数の型には抽象クラスを指定し、抽象クラスと具象クラスの紐付けを行い、抽象クラスを見て具象クラスのインスタンスを注入、が出来ないと、依存性の逆転には対応出来ていないことになります。
依存性の注入自体は出来ているのでDIツールの動作としては正しいのですが、テストに強くメンテナンス性の高いより良いコードを書くには、残念ながら少々機能不足感は否めないです。

参考資料

Python Tips: Python でインタフェースを使いたい - Life with Python

なぜDependency Injectionは、それほど悪いものではないのか - Qiita

163
128
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
163
128