Djangoには、フレームワーク内での各種アクションの発生を他の機能へ通知する、シグナル(Signals)という機能があります。
Signals | Django ドキュメント | Django
シグナルの機能を使うと、Djangoアプリケーション間のモジュールの依存関係を緩くできたり、拡張性のあるアプリを作れたりします。
試した環境は Python 3.11, Django 5.0
シグナルの使い方
シグナルの使い方は以下の通りです。
- シグナルを定義する ...
my_signal = Signal()
- (受信側)シグナルにレシーバーを接続する ...
Signal.connect()
- (送信側)シグナルを送信する ...
Signal.send()
- (受信側)レシーバーの処理が実行される
自分で定義したシグナルを使う場合は、1のシグナル定義から。既定のシグナルを利用する場合は、2のレシーバーの接続からになります。
シグナルを使わない例
base、app1、app2 のように、3つのDjangoアプリケーションがある例です。
baseアプリは最初に作成され、後から追加されたapp1とapp2は、baseアプリから処理が呼び出される、といったモジュール構成を想定します。
base/spam.py:
from app1.egg import receiver as app1_receiver from app2.egg import receiver as app2_receiver def main(): print("base/spam") app1_receiver("by base") app2_receiver("by base")
app1/egg.py:
def receiver(param): print(f"app1 {param}")
app2/egg.py:
def receiver(param): print(f"app2 {param}")
この場合、Django shellから base/spam.py:main()
を実行すると、以下のようになります。
>>> from base.spam import main >>> main() base/spam app1 by base app2 by base
このとき、base、app1、app2の依存関係について考えてみます。
- baseアプリからは、app1とapp2の関数をimportしているので、Pythonモジュールとして直接依存している
- app1とapp2の間には依存関係はない
また、「app1とapp2は後から追加される」という想定だと、app1やapp2を実装するときに、呼び出し元のbaseのアプリも編集が必要となります。
base/spam.py:main()
の処理は、密結合のapp1とapp2のreceiverの処理を含むため、このような構成だと main()
のユニットテストは複雑になっていきます。
シグナルを使うと、このような密結合のモジュールの依存を変更し、緩い結合にできます。
モジュール、関数の依存と呼び出しの関係は、以下の図のようになります。
シグナルを利用する形に書き換えた例
base/signals.py:
from django.dispatch import Signal spam_main = Signal()
base/spam.py:
from .signals import spam_main def main(): print("base/spam") spam_main.send(main, "by base")
app1/egg.py:
def receiver(sender, **kwargs): print(f"app1 {kwargs['param']}")
app1/apps.py:
from django.apps import AppConfig from base.signals import spam_main from .egg import receiver class App1Config(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'app1' def ready(self): spam_main.connect(receiver)
app2/egg.py:
def receiver(sender, **kwargs): print(f"app2 {kwargs['param']}")
app2/apps.py:
from django.apps import AppConfig from base.signals import spam_main from .egg import receiver class App2Config(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'app2' def ready(self): spam_main.connect(receiver)
実行結果は、シグナルを使わない場合と同じです。
ポイントは以下の通りです。
- app1とapp2を後から追加するときに、baseを変更しなくてよい
- base側からapp1とapp2へのimportをしなくてよい
- 記述するコード量は増える
- シグナルへの接続は、AppConfig.readyでやっている
モジュール、関数の依存と呼び出しの関係は、以下の図のようになります。
図ではAppConfig部分を省略していますが、シグナルを使わない例よりも複雑になっています。
base/main.py、app1/egg.py、app2/egg.pyでそれぞれのモジュールが独立している構成になるため、ユニットテストを書いたりするときに楽になります。
まとめ
- Djangoのシグナル機能は機能間でアクションを通知できる
- シグナルを使うとアプリ間の依存を緩くできる
検証に使ったコードはGitHub上にあります。
https://github.com/tokibito/sample_nullpobug/tree/main/django/django-signal