Djangoのシグナル機能(django.dispatch.Signal)

Djangoには、フレームワーク内での各種アクションの発生を他の機能へ通知する、シグナル(Signals)という機能があります。

Signals | Django ドキュメント | Django

シグナルの機能を使うと、Djangoアプリケーション間のモジュールの依存関係を緩くできたり、拡張性のあるアプリを作れたりします。

試した環境は Python 3.11, Django 5.0

シグナルの使い方

シグナルの使い方は以下の通りです。

  1. シグナルを定義する ... my_signal = Signal()
  2. (受信側)シグナルにレシーバーを接続する ... Signal.connect()
  3. (送信側)シグナルを送信する ... Signal.send()
  4. (受信側)レシーバーの処理が実行される

自分で定義したシグナルを使う場合は、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