シグナル

Django には "シグナルディスパッチャ" があり、フレームワークの他の場所でアクションが発生したときに、ほかのアプリケーションが通知を受けるのを助けてくれます。簡単に言うと、シグナルは特定の 送り手 が、あるアクションが発生したことを一連の 受け手 に通知できるようにします。特に、多くのコードが同じイベントに関連している場合に便利です。

例えば、サードパーティのアプリを登録して、設定変更の通知を受けることができます:

from django.apps import AppConfig
from django.core.signals import setting_changed


def my_callback(sender, **kwargs):
    print("Setting changed!")


class MyAppConfig(AppConfig):
    ...

    def ready(self):
        setting_changed.connect(my_callback)

Django の 組み込みのシグナル は、ユーザコードに特定のアクションを通知します。

また、独自のカスタムシグナルを定義して送信することもできます。以下の シグナルの定義と送信 を参照してください。

警告

シグナルは疎結合のように見えますが、すぐに理解や調整、デバッグが難しいコードにつながります。

可能であれば、シグナルでディスパッチするのではなく、処理コードを直接呼び出すことを選ぶべきです。

シグナルを待ち受ける

シグナルを受信するには、 Signal.connect() メソッドを使って receiver 関数を登録します。シグナルが送信されると、レシーバ関数が呼び出されます。シグナルのすべてのレシーバ関数は、登録された順番に1つずつ呼び出されます。

Signal.connect(receiver, sender=None, weak=True, dispatch_uid=None)
パラメータ:
  • receiver -- このシグナルに接続されるコールバック関数です。詳しくは レシーバ関数 を参照してください。
  • sender -- シグナルを受信する送信者を指定します。詳しくは 特定の送信者によって送られたシグナルに接続する を参照してください。
  • weak -- Django はデフォルトでシグナルハンドラを弱い参照として保存します。従って、レシーバがローカル関数の場合、ガベージコレクションされる可能性 があります。これを防ぐには、シグナルの connect() メソッドを呼び出すときに weak=False を渡してください。
  • dispatch_uid -- シグナルが重複して送信される可能性がある場合の、シグナル受信機の一意な識別子。詳しくは 重複したシグナルを防止する を参照してください。

HTTP リクエストが終了するたびに呼び出されるシグナルを登録することで、この仕組みを見てみましょう。ここでは request_finished シグナルに接続します。

レシーバ関数

まず、レシーバ関数を定義する必要があります。レシーバはPythonの関数やメソッドであれば何でもかまいません:

def my_callback(sender, **kwargs):
    print("Request finished!")

この関数は sender 引数とワイルドカードキーワード引数 (**kwargs) を取ることに注意してください。すべてのシグナルハンドラはこれらの引数を取らなければなりません。

送信者については もう少し後で 見るので、今は **kwargs 引数を見てください。すべてのシグナルはキーワード引数を送信し、いつでもそのキーワード引数を変更できます。 request_finished の場合、引数を送らないようにドキュメント化されているので、シグナル処理を my_callback(sender) と書きたくなるかもしれません。

これは間違いです。実際、そうすると Django はエラーを返します。というのも、シグナルに引数が追加される可能性があり、レシーバはその新しい引数を扱えなければならないからです。

レシーバーは同じシグネチャーを持ち、async def を使って宣言された非同期関数になることもできます:

async def my_callback(sender, **kwargs):
    await asyncio.sleep(5)
    print("Request finished!")

シグナルは同期でも非同期でも送ることができ、受信側は自動的に正しいコールスタイルに合わせられます。詳細は シグナルを送る を参照してください。

Changed in Django 5.0:

非同期レシーバのサポートが追加されました。

レシーバ関数に接続する

受信機を信号に接続する方法は2つあります。手動で接続する方法です:

from django.core.signals import request_finished

request_finished.connect(my_callback)

あるいは、 receiver() デコレータを使うこともできます:

receiver(signal, **kwargs)
パラメータ:
  • signal -- 関数を接続するシグナルまたはシグナルのリスト。
  • kwargs -- 関数 に渡すワイルドカードキーワード引数です。

下記がデコレーターとの繋げ方です:

from django.core.signals import request_finished
from django.dispatch import receiver


@receiver(request_finished)
def my_callback(sender, **kwargs):
    print("Request finished!")

これで、リクエストが終了するたびに my_callback 関数が呼ばれるようになります。

コードはどこに置くの?

厳密には、シグナル処理と登録のコードは好きな場所に置くことができますが、コードのインポートによる副作用を最小限にするために、アプリケーションのルートモジュールと models モジュールの置くのは避けることを推奨します。

実際には、シグナルハンドラは通常、関連するアプリケーションの signals サブモジュールで定義されます。シグナルレシーバーはアプリケーションの configuration クラスready() メソッドで接続されます。 receiver() デコレータを使っている場合は、 ready() の中にある signals サブモジュールをインポートしてください、これによりシグナルハンドラが暗黙的に接続されます:

from django.apps import AppConfig
from django.core.signals import request_finished


class MyAppConfig(AppConfig):
    ...

    def ready(self):
        # Implicitly connect signal handlers decorated with @receiver.
        from . import signals

        # Explicitly connect a signal handler.
        request_finished.connect(signals.my_callback)

注釈

テスト中に ready() メソッドが複数回実行されることがあるため、特にテスト内でシグナルを送信する予定がある場合は、シグナルの重複を防ぐ ためにシグナルを保護することを検討するとよいでしょう。

特定の送信者によって送られたシグナルに接続する

シグナルの中には何度も送信されるものがありますが、そのようなシグナルの特定のサブセットだけを受信したいと思うかも知れません。例えば、モデルが保存される前に送られるシグナル django.db.models.signals.pre_save を考えてみましょう。ほとんどの場合、 どの モデルが保存されるかを知る必要はありません。ある 特定の モデルが保存されたときだけ知る必要があります。

このような場合、特定の送信者のみが送信するシグナルを受信するように登録できます。 django.db.models.signals.pre_save の場合、送信者は保存されるモデルクラスになるので、あるモデルから送信されるシグナルだけが欲しいことを示すことができます:

from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel


@receiver(pre_save, sender=MyModel)
def my_handler(sender, **kwargs): ...

my_handler 関数は MyModel のインスタンスが保存されたときにだけ呼び出されます。

異なるシグナルは異なるオブジェクトを送信元として使用します。それぞれのシグナルの詳細については 組み込みシグナルのドキュメント を参照する必要があります。

重複したシグナルを防止する

状況によっては、レシーバーをシグナルに接続するコードが複数回実行されることがあります。そのため、レシーバ関数が複数回登録され、シグナルイベントに対して複数回呼び出される可能性があります。例えば、 ready() メソッドがテスト中に複数回実行されるかもしれません。より一般的には、シグナル登録はインポートされた回数だけ実行されるため、 シグナルを定義したモジュールをインポートしたプロジェクトではどこでも発生します。

この動作が問題となる場合 (シグナルを使用してモデルが保存されるたびにメールを送信する場合など)、受信関数を識別するために dispatch_uid 引数に一意な識別子を渡します。この識別子は通常文字列ですが、ハッシュ可能なオブジェクトであれば何でもかまいません。この結果、受信関数は一意な dispatch_uid の値ごとに一度だけシグナルにバインドされることになります:

from django.core.signals import request_finished

request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")

シグナルの定義と送信

アプリケーションは信号インフラを利用し、独自の信号を提供できます。

カスタムシグナルを使うべきタイミング

シグナルは暗黙の関数呼び出しなので、デバッグが難しくなります。カスタムシグナルの送信側と受信側の両方がプロジェクト内にある場合は、明示的な関数呼び出しを使ったほうがよいでしょう。

シグナルを定義する

class Signal

すべてのシグナルは django.dispatch.Signal インスタンスです。

例:

import django.dispatch

pizza_done = django.dispatch.Signal()

このコードは、pizza_done シグナルを宣言しています。

シグナルを送信する

Django でシグナルを同期的に送信する方法は2つあります。

Signal.send(sender, **kwargs)
Signal.send_robust(sender, **kwargs)

シグナルは非同期に送信することもできます。

Signal.asend(sender, **kwargs)
Signal.asend_robust(sender, **kwargs)

シグナルを送信するには、Signal.send()Signal.send_robust()await Signal.asend()await Signal.asend_robust() のどれかを呼び出します。引数として sender (ほとんどの場合クラス) を指定する必要があり、他のキーワード引数を好きなだけ指定できます。

たとえば、pizza_done シグナルを送信するには、次のようにします:

class PizzaStore:
    ...

    def send_pizza(self, toppings, size):
        pizza_done.send(sender=self.__class__, toppings=toppings, size=size)
        ...

4つのメソッドはすべて、呼び出されたレシーバー関数とそのレスポンス値のリストを表すタプルのペア [(receiver, response), ...] のリストを返します。

send()send_robust() と異なり、レシーバ関数が発生させた例外をどのようにハンド リングするかという点で異なります。 send() はレシーバが発生させた例外をキャッチしません。そのため、エラーが発生してもすべてのレシーバにシグナルが通知されるとは限りません。

send_robust() は Python の Exception クラスに由来するすべてのエラーをキャッチし、すべてのレシーバにシグナルが通知されるようにします。エラーが発生した場合、エラーを発生させたレシーバのタプルのペアにエラーインスタンスが返されます。

トレースバックは send_robust() を呼び出したときに返されるエラーの __traceback__ 属性に存在します。

asend() is similar to send(), but it is a coroutine that must be awaited:

async def asend_pizza(self, toppings, size):
    await pizza_done.asend(sender=self.__class__, toppings=toppings, size=size)
    ...

同期であっても非同期であっても、レシーバは send()asend() のどちらを使用しても正しく適応されます。asend() で呼び出された場合、同期レシーバは sync_to_async() を使って呼び出されます。非同期レシーバは、sync() によって呼び出された場合、 async_to_sync() を使って呼び出されます。ミドルウェア のケースと同様に、この方法でレシーバを適合させることには、わずかなパフォーマンスコストがあります。send() または asend() 呼び出しの中で同期/非同期の呼び出しスタイルの切り替えの回数を減らすために、呼び出す前に非同期かどうかでレシーバをグループ化していることに注意してください。これは、同期レシーバの前に登録された非同期レシーバが、同期レシーバの後に実行される可能性があることを意味します。さらに、非同期レシーバは asyncio.gather() を使って同時に実行されます。

非同期のリクエスト/レスポンス・サイクル以外のすべての組み込みシグナルは Signal.send() を使ってディスパッチされます。

Changed in Django 5.0:

非同期シグナルのサポートが追加されました。

シグナルを切断する

Signal.disconnect(receiver=None, sender=None, dispatch_uid=None)

シグナルからレシーバーを切断するには Signal.disconnect() を呼び出します。引数は Signal.connect() の説明と同じです。このメソッドは、レシーバが切断された場合は True を、切断されなかった場合は False を返します。sender<app label>.<model> への遅延参照として渡された場合、このメソッドは常に None を返します。

引数 receiver は、切断する登録済みのレシーバを指定します。レシーバを識別するために dispatch_uid を使用する場合は None を指定します。

Back to Top