Pythonでコードを書いていると、「あとから直しづらいな…」「テストを書くのがしんどいな…」と感じたことはありませんか?
その原因の多くは、クラス同士が強く結びつきすぎている設計にあります。 この状態をそのままにしていると、ちょっとした変更でも思わぬ場所が壊れたり、テストのたびに外部APIやデータベースに振り回されたりしてしまいます。
そこで登場するのが、依存関係注入(DI:Dependency Injection)という考え方です。 DIは「難しい上級者向けテクニック」に見えがちですが、実はコードをやさしく、長生きさせるための設計の工夫なんです。
この記事では、Python初心者〜中級者の方を想定して、
- なぜDIが必要になるのか
- DI・IoC・DIPの関係はどうなっているのか
- PythonではどうやってDIを実装するのか
- テストがなぜ一気に書きやすくなるのか
といったポイントを、できるだけ噛み砕いて解説していきます。
FastAPIで Depends() を見かけたことがある方や、 「モックって何のためにあるの?」とモヤっとしている方にも、きっとヒントになるはずです。
コードをガチガチに固める設計から、 必要に応じて入れ替えられる柔らかい設計へ。 その第一歩として、DIの世界を一緒にのぞいてみましょう ✨
1. なぜDIが必要なのか?密結合が生む3つの問題
まずは、DI(依存関係注入)がなぜ必要とされるのかから見ていきましょう。 多くの場合、問題は「動いてはいるけど、あとがつらいコード」から始まります。
Pythonでクラス設計をするとき、次のような書き方をしてしまうことはよくあります。
class UserService:
def __init__(self):
self.mailer = GmailSender()
def notify(self, user):
self.mailer.send(user.email)
一見するとシンプルで、何も問題がなさそうに見えますよね。 でもこの設計、じわじわと効いてくる落とし穴がいくつもあります。
問題① クラス同士が強く結びついてしまう(密結合)
この UserService は、GmailSender という具体的な実装に直接依存しています。 これはつまり、
- メール送信方法を変更したい
- 別の送信サービスを使いたい
といった要望が出たときに、UserService自体を修正しなければならないということです。
クラス同士が「溶接」されたような状態になり、 一部分を変えたいだけなのに、周囲まで影響が広がってしまいます。
問題② テストがとにかく書きづらい
さらに厄介なのがテストです。
このままでは、テスト時にも本物の GmailSender が使われてしまいます。 すると、
- テスト実行で実際にメールが飛ぶ
- 外部サービスの状態にテスト結果が左右される
- テストが遅く、不安定になる
といった問題が起きがちです。
「本当はメール送信なんてしたくない。呼ばれたかどうかだけ確認したい」 そんな当たり前のテストが、書けなくなってしまうんですね。
問題③ 将来の変更コストがどんどん膨らむ
最初は小さなクラスでも、機能追加や仕様変更を重ねるうちに、
- あちこちに同じ修正が必要
- どこを直せばいいのかわからない
- 触るのが怖くて変更を避ける
という状態に陥りがちです。
このような「変更に弱いコード」の正体は、 依存関係がコードの中に埋め込まれてしまっていることにあります。
DIは、この依存関係を外に出すことで、
- クラス同士の結びつきを弱め
- テストしやすくし
- 変更に強い設計にする
ための考え方です。

次のセクションでは、 DI・IoC・DIP という、よく一緒に語られる言葉の関係を整理しながら、 DIの正体をもう少しクリアにしていきます。
2. DI・IoC・DIPの関係を整理しよう
DIの話を調べていると、ほぼ確実に一緒に出てくる言葉があります。
- IoC(Inversion of Control:制御の反転)
- DIP(Dependency Inversion Principle:依存関係逆転の原則)
どれも似た文脈で使われるので混乱しがちですが、 それぞれの役割と立ち位置を整理すると、DIの理解が一気に楽になります。
DIは「実装テクニック」
まず、DI(依存関係注入)はとてもシンプルです。
「必要なものを、自分で作らず、外から渡してもらう」
これがDIの本質です。
先ほどの例で言えば、
- GmailSenderをクラス内部でnewしない
- 外から渡されたメール送信オブジェクトを使う
という形に変えることがDIです。
DIはコードとして実装できる具体的な手法であり、 Pythonではコンストラクタ引数や関数引数で自然に表現できます。
IoCは「考え方・設計の方向性」
次に IoC(制御の反転) です。
これは少し抽象的で、
「何を使うか」「どう作るか」という主導権を、 クラス自身から外部に移す考え方
を指します。
DIは、IoCを実現する代表的な手段のひとつです。
つまり、
- IoC:設計思想
- DI:それをコードで実現する方法
という関係になります。
DIPは「守るべき設計原則」
最後に DIP(依存関係逆転の原則) です。
DIPでは、次の2点が重要になります。
- 上位モジュールは下位モジュールに依存してはいけない
- どちらも「抽象」に依存すべきである
ここで言う「抽象」とは、Pythonであれば
- 抽象クラス(abc)
- プロトコル(typing.Protocol)
などを指します。
DIを使って依存関係を外から注入し、 その依存先を「抽象」にしておくことで、 DIPを自然に満たす設計になります。
3つの関係を一言でまとめると
- IoC:設計の考え方
- DIP:守るべき原則
- DI:それを実装するための手段
この関係が見えてくると、 「DIを使うかどうか」で悩むよりも、
「変更しやすく、テストしやすい構造になっているか?」
を基準に設計を考えられるようになります。
設計思想を深く理解したい人へ
ここまで読んで、
- なぜ「抽象」に依存するのか
- 依存の向きをどう設計すべきか
をもっと体系的に理解したくなった方には、次の一冊がとても相性がいいです。
Clean Architecture 達人に学ぶソフトウェアの構造と設計
✅ Amazonでチェックする | ✅ 楽天でチェックする
DIやDIPが「テクニック」ではなく、 ソフトウェアを長く保つための考え方だと実感できる一冊です。

次のセクションでは、 いよいよPythonでDIをどう書くのかを、 シンプルなコード例で見ていきます。
3. PythonでDIを実装する基本パターン
ここからは、Pythonで依存関係注入(DI)をどう書くのかを見ていきます。 難しい仕組みは不要で、まずは素のPythonで理解するのがいちばんです。
手順① 依存先を「抽象」で表現する
DIの第一歩は、具体的なクラスではなく、役割(インターフェース)に依存することです。
Pythonでは abc モジュールを使って、抽象クラスを定義できます。
from abc import ABC, abstractmethod
class MailSender(ABC):
@abstractmethod
def send(self, email: str) -> None:
pass
ここでは、
- 「メールを送る」という役割だけを定義
- どのサービスを使うかは決めない
という状態を作っています。
手順② 具体的な実装クラスを用意する
次に、この抽象を実装するクラスを作ります。
class GmailSender(MailSender):
def send(self, email: str) -> None:
print(f"Gmailで {email} にメールを送信しました")
将来、別のサービスに切り替えたくなった場合でも、
- YahooSender
- MockMailSender(テスト用)
などを追加するだけで対応できます。
手順③ コンストラクタ注入(いちばんおすすめ)
ここで、依存関係を外から渡すようにします。
class UserService:
def __init__(self, mail_sender: MailSender):
self.mail_sender = mail_sender
def notify(self, email: str) -> None:
self.mail_sender.send(email)
ポイントは、
GmailSenderを直接生成していないMailSenderという抽象に依存している
という点です。
実際に使うときは、外側で組み立てます。
mail_sender = GmailSender()
service = UserService(mail_sender)
service.notify("user@example.com")
これがコンストラクタ注入です。 オブジェクト生成時に必要な依存がすべて揃うので、 安全で読みやすいのが大きなメリットです。
メソッド注入という選択肢
場合によっては、依存関係をメソッド引数で渡すこともあります。
class UserService:
def notify(self, email: str, mail_sender: MailSender) -> None:
mail_sender.send(email)
この方法は、
- その処理でしか使わない依存
- 一時的な振る舞いの切り替え
に向いています。
ただし、クラス全体で使う依存は コンストラクタ注入の方が意図が明確なので、基本はこちらを選びましょう。
「DIっぽく書く」だけでも効果はある
最初から抽象クラスを用意しなくても、
- 外からオブジェクトを渡す
- 内部でnewしない
これだけでも、設計はかなり改善されます。
DIは「完璧にやること」よりも、 依存を外に出す意識を持つことが何より大切です。

次のセクションでは、 このDIがリファクタリングの現場でどう役立つのかを、 実務視点で見ていきます。
4. リファクタリング視点で見るDI導入の効果
DIは「最初からきれいに設計できる人のためのもの」と思われがちですが、 実際の現場ではリファクタリングの途中で導入されるケースの方が圧倒的に多いです。
ここでは、DIがあることで何がどう楽になるのかを、 リファクタリングの視点から見ていきましょう。
「変更したいのに、触るのが怖い」コードの正体
長く使われているコードほど、こんな状態になりがちです。
- クラスの中で依存オブジェクトを直接生成している
- 同じような処理があちこちに散らばっている
- テストがなく、動作確認は手動のみ
この状態で仕様変更が入ると、
- どこを直せばいいかわからない
- 直したつもりが別の場所で壊れる
という負のループに陥ります。
DIは「安全に切り出すための足場」
DIを導入すると、依存関係が外に見える形になります。
すると、
- このクラスは何に依存しているのか
- どこまでが責務なのか
が明確になり、 安心してクラスを分割・整理できるようになります。
いきなり完璧な設計を目指す必要はありません。
- まずは new している部分を外に出す
- 引数で受け取れる形にする
これだけでも、コードは一段階「柔らかく」なります。
DIは「あとから効いてくる」設計投資
DIの効果は、導入した瞬間よりも、
- 仕様変更が入ったとき
- テストを書き始めたとき
- 別の人がコードを触るとき
に強く実感できます。
「最初は少し面倒だったけど、今は助かっている」 DIは、そんなタイプの設計です。
既存コードを改善したい人へ
すでに動いているコードにDIを入れたい場合、 どう直せば安全なのかで悩むことが多いと思います。
そんなときにとても参考になるのが、次の一冊です。
リファクタリング(第2版)
✅ Amazonでチェックする | ✅ 楽天でチェックする
DIを直接解説する本ではありませんが、
- 変更を小さく、安全に進める考え方
- 「直していいコード」と「触る前に準備が必要なコード」の見極め
が非常にわかりやすく整理されています。

次のセクションでは、 DIとテストがなぜ相性抜群なのかを、 具体的なテストの流れと一緒に見ていきます。
5. テストとDIの相性が最強な理由
DIの価値がいちばんわかりやすく表れるのが、テストを書くときです。 ここでは、「なぜDIがあるとテストが一気に楽になるのか」を具体的に見ていきましょう。
DIがないと、テストはなぜつらいのか
DIを使っていないコードでは、テスト中でも
- 実際のデータベースに接続してしまう
- 外部APIを本当に呼んでしまう
- ファイルやネットワークに依存してしまう
といった問題が起きがちです。
すると、
- テストが遅い
- 環境によって結果が変わる
- 失敗した理由がわかりにくい
という、あまりうれしくない状況になります。
DIがあると「差し替え」が自然にできる
DIを使っていれば、テスト時に本物の依存先を使う必要がありません。
たとえば、先ほどの MailSender を、 テスト用のクラスに差し替えることができます。
class FakeMailSender(MailSender):
def __init__(self):
self.called = False
def send(self, email: str) -> None:
self.called = True
これをテスト対象に注入すると、
def test_notify_calls_send():
fake_sender = FakeMailSender()
service = UserService(fake_sender)
service.notify("test@example.com")
assert fake_sender.called
外部環境に一切依存しない、 高速で再現性のあるテストが書けるようになります。
「呼ばれたかどうか」だけを検証できる安心感
テストで本当に確認したいのは、
- メールが送信されたか
- DBに保存されたか
ではなく、
「その責務を持つ処理が、正しく呼ばれたか」
であることがほとんどです。
DIを使えば、 テスト対象のクラスは自分の仕事だけに集中できます。
DIとテストはセットで考えると強い
DIだけを導入しても、テストを書かなければ恩恵は半分です。 逆に、テストを書こうとすると、DIのありがたみをすぐに実感します。
この2つは、お互いを支え合う関係なんですね。
テスト駆動の視点で理解したい人へ
「そもそも、なぜテストしやすい設計が大事なのか」 「設計とテストはどうつながっているのか」
を体系的に理解したい方には、次の一冊がとても参考になります。
テスト駆動開発
✅ Amazonでチェックする | ✅ 楽天でチェックする
DIを「テクニック」ではなく、 設計と品質を守るための考え方として理解できるようになります。

次のセクションでは、 DIフレームワークやFastAPIの Depends() を いつ・どこで使うべきかを整理していきます。
6. DIフレームワークはいつ使うべきか
ここまでで、素のPythonだけでもDIは十分に実装できることが分かりました。 では、DIフレームワークはいつ必要になるのでしょうか?
結論から言うと、 「依存関係の組み立てがつらくなったとき」が導入のタイミングです。
手動DIで十分なケース
次のような場合は、フレームワークを使わなくても問題ありません。
- 依存関係の数が少ない
- オブジェクト生成の流れが単純
- 小規模なスクリプトやツール
むしろ、最初からDIフレームワークを入れてしまうと、
- 設定が多くて理解しづらい
- コードの追跡が難しくなる
といった逆効果になることもあります。
DIフレームワークが欲しくなる瞬間
一方で、次のようなサインが出てきたら要注意です。
- オブジェクト生成コードがあちこちに散らばっている
- 依存関係の入れ替えが頻繁に起こる
- 初期化順序を間違えてバグが出る
この段階になると、
「何をどこで生成しているのか分からない」
という状態になりがちです。
DIフレームワークは、 この組み立ての複雑さを一箇所に集約するための道具です。
代表的なDIフレームワーク
dependency-injector
- 高速で柔軟
- コンテナで依存関係を明示的に定義
- 大規模アプリ向き
依存関係が増えても、 「設計図」として一覧できるのが大きな強みです。
injector
- アノテーション中心のシンプルな記述
- Guice風の書き味
Java経験者には馴染みやすいですが、 Pythonでは好みが分かれることもあります。
FastAPIのDepends()はDIの入門に最適
FastAPIを使っている方なら、 Depends() をすでに見たことがあるかもしれません。
これは、フレームワーク組み込みのDI機構です。
- 依存関係を関数として定義できる
- テスト時に簡単に差し替え可能
- 非同期処理とも相性が良い
「DIをちゃんと使うのは初めて」という方でも、 FastAPIなら自然にDIの考え方に触れられます。
最初の判断基準はこれでOK
- まずは手動DIで書いてみる
- つらくなったらフレームワークを検討する

DIフレームワークは魔法の道具ではありません。 あくまで、複雑さが増えたときに助けてくれる整理係です。
まとめ
今回は、Pythonにおける依存関係注入(DI)について、 「なぜ必要なのか」という背景から、実装方法、テストやリファクタリングとの関係までを順番に見てきました。
- DIはコードを複雑にするためのものではない
- 変更しやすく、テストしやすい状態を保つための工夫
- IoCやDIPとセットで考えると理解しやすい
という点が、この記事の大きなポイントです。
私自身も、最初は「DIって回りくどいな」と感じていました。 でも、仕様変更やテスト対応が増えるにつれて、
「DIが入っているコードは、触るのが怖くない」
と実感するようになりました。
すべてのコードにDIが必要なわけではありません。 ただ、少しでも
- テストが書きづらい
- 変更のたびに修正箇所が増える
と感じているなら、 DIはとても頼れる選択肢になります。
まずは小さなクラスひとつから、 「newしない設計」を意識してみてください。 それだけでも、コードの見通しはかなり変わりますよ 😊
あわせて読みたい
- Pythonでテストコードを書く方法|初心者向けpytest入門ガイド
- 初心者でもすぐわかる!Pythonのクラス入門ガイド
- 【リファクタリングとは?】初心者向けにコード改善の基本をやさしく解説
- 読みやすく変更しやすい「綺麗なコード」の書き方
- 【初心者向け】FastAPIの基本の使い方をやさしく解説
参考文献
- DataCamp|Python Dependency Injection Tutorial
- ArjanCodes|Python Dependency Injection Best Practices
- dependency-injector公式ドキュメント|Dependency Injection in Python
- Zenn|Pythonにおける依存性注入メモ
- Classmethod Developers Blog|Python injector入門
- Pytest with Eric|Python Unit Testing Best Practices
- Stack Overflow|How to do basic Dependency Injection in Python for testing
- dependency-injector|公式サイト
- Qiita|Pythonの依存性注入パターンまとめ
- note|Python依存性注入(DI)を初心者向けに整理
よくある質問(Q&A)
- Q小規模なPythonスクリプトでもDIは使うべきですか?
- A
無理に使う必要はありません。 ファイル1〜2本で完結するスクリプトなら、 直接依存させた方が読みやすいことも多いです。
ただし、「あとから機能追加するかも」「テストを書く予定がある」なら、 DIを意識しておくと将来が楽になります。
- Qabcを使わずにDIっぽく書いても問題ありませんか?
- A
問題ありません。 Pythonでは、
- ダックタイピング
- typing.Protocol
なども立派な選択肢です。
大事なのは抽象に依存する意識であって、 必ずabcを使うことではありません。
- QFastAPIのDepends()はDIフレームワークと同じものですか?
- A
役割は似ていますが、スコープは少し違います。
FastAPIのDepends()は、
- リクエスト単位の依存解決
- Webアプリ向けに最適化
されています。
一方、DIフレームワークはアプリ全体の依存関係を管理します。 FastAPIを使っている場合は、まずDepends()から慣れていくのがおすすめです。







※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。
※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。