DjangoなSignalについて

Python,TIPSDjango

Django Signalsは特定のイベントが発生した際に分離されたアプリケーションへ通知を受け取れるようにする機能です。

公式ドキュメントはこちら

例えば特定のModelインスタンスが更新されるたびにキャッシュされたページを無効にしつつ、コード側でこのModelを更新する箇所がいくつかあるとします。

この場合はSignalを使用する事でこの特定のModelのsave()メソッドが呼ばれるたびに実行されるコードの一部をフックできます。
またもう1つの一般的なユースケースとしては、1対1リレーションシップを使用してプロファイルを活用しDjangoのUserModelを拡張した場合です。

通常はシグナルディスパッチャを使用してユーザーのpost_saveイベントを購読し、Profileインスタンスも更新します。
UserModel拡張の概要については以下の記事を参照してください。

この投稿では具体的なSignalsの使用場面や実装方法などを紹介します。

どんな時に使用するのか

  • 多数のコードが同一のイベントを購読したいとき
  • 分離されているアプリケーションとやり取りする必要があるとき
  • サードパーティ製のModelを扱うとき

どのように動作するのか

Observerパターンについて知見がある場合はすんなりと理解できると思います。
Observerパターンの説明については@shoheiyokoyamaさんの記事がわかりやすくて参考になると思います。
デザインパターン「Observer」

DjangoでSignalsを活用する場合は基本的にObserverパターンで実装します。
Signalには、SenderとReceiverという2つの重要な要素があります。

これは名前が示すように送信者はSignalをディスパッチすし、受信側はこのSignalを受信して何かを行うコードになります。
Receiverは、Signalを受信する関数またはインスタンスメソッドである必要があります。
Senderは Pythonオブジェクトであるか、任意のSenderからイベントを受信する必要があります。

SenderとReceiver間の接続は、Connectメソッドを介してSignalのインスタンスである「シグナルディスパッチャー」を介して行います。

Django coreはSenderを app_label の文字列として怠惰に指定できるようにするSignalのサブクラスであるModelSignalも定義します。
app_label.ModelName の形式ですが、一般的にSignalクラスを使用してカスタムSignalを作成しておく必要があります。

したがって、Signalを受信するには、Signal.connect()メソッドを使用してSignalが送信されたときに呼び出される受信機能を登録しておく必要があるわけです。

使用方法

post_saveという組み込みSignalを見てみましょう。
このコードはdjango.db.models.signalsモジュールに実装されています。この特定のSignalは、Modelがsaveメソッドの実行を完了した直後に発火するようになっています。

from django.contrib.auth.models import User
from django.db.models.signals import post_save

def save_profile(sender, instance, **kwargs):
    instance.profile.save()

post_save.connect(save_profile, sender=User)

上記の例では、save_profile はReceiverであり、UserはSenderであり、post_saveはSignalです。 save_profile と post_saveUser インスタンスがUserModelのsaveメソッドの実行を購読するたびにsave_profile 関数が実行されます。 

Signalを登録する方法はもう一つあります。
それは @receiver というデコレータを使用することです。

def receiver(signal, **kwargs)

Signalのパラメータは、SignalインスタンスまたはSignalインスタンスのリスト/タプルのどちらかです。
まずは以下の例を参照してください。

from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def save_profile(sender, instance, **kwargs):
    instance.profile.save()

また、もし複数のタイミングでハンドラを発火させたい場合は次のようにするだけで出来ます。

@receiver([post_save, post_delete], sender=User)

どこに実装するか

アプリケーションのSignalを登録する場所によっては副作用が発生する場合があります。
従ってModels.pyやurls.pyといったModelモジュール、rootモジュールには入れないようにすることをオススメします。

ちなみにDjango公式Documentでは、App構成ファイルにSignalを配置することを推奨しています。
まずはProfileという名前のアプリで私が普段実装している内容を紹介します。

profile/signals.py

from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from cmdbox.profiles.models import Profile

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

profile/app.py

from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class ProfilesConfig(AppConfig):
    name = 'cmdbox.profiles'
    verbose_name = _('profiles')

    def ready(self):
        import cmdbox.profiles.signals 

profile/__init__.py:

default_app_config = 'cmdbox.profiles.apps.ProfilesConfig'

上記の例では、@receiver() デコレータを使用しているため、read()メソッドでsignals をインポートするだけで動作してくれます。

もしconnect()を使用してハンドラを発火させたい場合は次の例を参照してください。

profile/signals.py

from cmdbox.profiles.models import Profile

def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

profile/app.py:

from django.apps import AppConfig
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from cmdbox.profiles.signals import create_user_profile, save_user_profile

class ProfilesConfig(AppConfig):
    name = 'cmdbox.profiles'
    verbose_name = _('profiles')

    def ready(self):
        post_save.connect(create_user_profile, sender=User)
        post_save.connect(save_user_profile, sender=User)

profile/__init__.py

default_app_config = 'cmdbox.profiles.apps.ProfilesConfig'

注: profile/__init__.py は、settings.pyのINSTALLED_APPSで AppConfig を参照している場合は必要ありません。

ここでは、いくつかの有用でいて一般的に使用されている組み込みSignalを紹介します。

Django組み込みSignalsについて

Model Signals

django.db.models.signals.pre_init

receiver_function(sender, *args, **kwargs)

django.db.models.signals.post_init

receiver_function(sender, instance)

django.db.models.signals.pre_save

receiver_function(sender, instance, raw, using, update_fields)

django.db.models.signals.post_save

receiver_function(sender, instance, created, raw, using, update_fields)

django.db.models.signals.pre_delete

receiver_function(sender, instance, using)

django.db.models.signals.post_delete

receiver_function(sender, instance, using)

django.db.models.signals.m2m_changed:

receiver_function(sender, instance, action, reverse, model, pk_set, using)

Requesrt/Response Signals

django.core.signals.request_started

receiver_function(sender, environ)

django.core.signals.request_finished

receiver_function(sender, environ)

django.core.signals.got_request_exception

receiver_function(sender, request)

すべての組み込みSignalのリストはDjangoの公式ドキュメントから参照できます。

Python,TIPSDjango

Posted by Kenny