1. 程式人生 > >淺談django的訊號機制 Signals

淺談django的訊號機制 Signals

一、概述

Django includes a “signal dispatcher” which helps allow decoupled applications get notified when actions occur elsewhere in the framework. In a nutshell, signals allow certain senders to notify a set of receivers that some action has taken place. They’re especially useful when many pieces of code may be interested in the same events.

Django內部包含了一位“訊號排程員”:當某事件在框架內發生時,它可以通知到我們的應用程式。 簡而言之,當event(事件)發生時,signals(訊號)允許若干 senders(寄件人)通知一組receivers(接收者)。這在我們多個獨立的應用程式碼對同一事件的發生都感興趣時,特別有用。

二、signal、receiver、sender

2.1 signal

所有signal都是django.dispatch.Signal類的例項。 Django提供了一組內建的訊號集,只要使用者程式碼事先繫結到這些訊號上,當框架內某些事件發生時,使用者程式碼可以從中獲取到通知,從而執行使用者程式碼。 以下是一些Django內預定義的signals:

django.db.models.signals.pre_save & django.db.models.signals.post_save

Sent before or after a model’s save() method is called.

django.db.models.signals.pre_delete & django.db.models.signals.post_delete

Sent before or after a model’s delete() method or queryset’s delete() method is called.

django.db.models.signals.m2m_changed

Sent when a ManyToManyField on a model is changed.

django.core.signals.request_started & django.core.signals.request_finished

Sent when Django starts or finishes an HTTP request.

當然,使用者也可以自定義signal。

For example:

import django.dispatch
pizza_done = django.dispatch.Signal(providing_args=["toppings", "size"])

This declares a pizza_done signal that will provide receivers with toppings and size arguments.

定義一個名叫pizza_done的Signal例項,它提供 toppings和size字典引數給 receivers。 (疑問:檢視Signal類的原始碼,個人覺得這個providing_args就是個‘花瓶’,貌似整個類就沒有對providing_args屬性有過利用,就像個口頭約定,receivers能得到什麼引數,還得看send()當時是怎麼傳的~,有明白的少俠請指點下)

2.2 receiver以及如何繫結到signal

receiver 相當於一個回撥函式,相關事件發生了就會觸發它被執行。

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

Notice that the function takes a sender argument, along with wildcard keyword arguments (**kwargs); all signal handlers must take these arguments.

We'll look at senders a bit later, but right now look at the **kwargs argument. All signals send keyword arguments, and may change those keyword arguments at any time. In the case of request_finished, it's documented as sending no arguments, which means we might be tempted to write our signal handling as my_callback (sender).

This would be wrong -- in fact, Django will throw an error if you do so. That's because at any point arguments could get added to the signal and your receiver must be able to handle those new arguments.

上面的話我是這麼理解的:為了讓我們的回撥函式能夠支援千變萬化的引數,用sender+kwargs字典作參得了。

可是每次取值都得從kwargs中取,看起來可讀性也變差了。個人用法是函式中經常會用到的引數直接列出來,比如def my_callback(sender,app,**kwargs):pass。

繫結receivers到signal

有兩種方式可以實現這點。手動方式:

from django.core.signals import request_finished
request_finished.connect(my_callback)

或者,你可以使用receiver裝飾器來定義你的receiver:

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

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

現在,每次request_finished訊號有通知,我們就執行一次my_callback。

當你這麼設定好了後,就會收到各種signals發來的通知。可是,如果我的回撥哈數只對特定sender感興趣的話怎麼辦呢?可以通過指定 connect 函式的 sender 引數來過濾,用裝飾器方式過濾的話,像這樣:

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例項被儲存時,才會被呼叫。

此外,connect函式還有個引數 dispatch_uid(通常是一個字串)用來唯一標識你的回撥函式,目的是為了避免重複 繫結或註冊 回撥函式到同一signal上,如果那樣的話,一個事件發生了會進好幾次回撥哦。

2.3 sender

其實sender只是個引數,不只是上面說到的connect函式中的可選引數,傳送訊號時則必須要指定sender引數,它告訴receiver函式這個signal到底來自哪裡。舉個例子:

class PizzaStore(object):
    ...

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

這裡,pizza_done訊號被髮送,sender引數是個PizzaStore例項。

除了用Signal.send(sender, kwargs)來發送訊號,你還可以使用Signal.send_robust(sender,kwargs)。區別是後者會捕捉receiver回撥函式中的異常,並將錯誤資訊新增進函式返回值中。

2.4 其他

方法 Signal.disconnect([receiver=None, sender=None, weak=True, dispatch_uid=None])用於解除signal與某receiver的繫結關係。

三、舉個Django中現成的例子

在你的settings.INSTALLED_APPS中新增進應用'django.contrib.contenttypes',然後執行 "manage.py syncdb",這時候你會看到資料庫多一張表 “django_content_type”,而且裡面會有一些紀 錄呢,當然,這並沒有什麼好奇怪的,表django_content_type中的紀錄代表著整個專案已安裝上的應 用。接著,你再往settings.INSTALLED_APPS新增一些其他應用,比如我們自定義的,news應用,那麼這個news的紀錄要如何新增進表django_content_type中呢?答案還是"manage.py syncdb"!而這實現之中就利用到了Django的signal機制。

首先是signal定義:

#file:django\db\models\signals.py

from django.dispatch import Signal
post_syncdb = Signal(providing_args=["class", "app", "created_models", 
"verbosity", "interactive"])

post_syncdb就是下面多次使用到的訊號了。

其次是receiver繫結到signal:

#file:django\contrib\contenttypes\management.py(有木有覺得所有signal的connect方法都是放
在應用的management.py檔案或者management目錄下__init__.py檔案中執行的)

from django.db.models import signals
signals.post_syncdb.connect(update_contenttypes)

update_contenttypes函式 根據新增的應用在django_content_type表中生成相應的記錄。

最後是訊號的觸發:

#file:django\core\management\sql.py

def emit_post_sync_signal(created_models, verbosity, interactive, db):
    # Emit the post_sync signal for every application.
    for app in models.get_apps():
        app_name = app.__name__.split('.')[-2]
        if verbosity >= 2:
            print("Running post-sync handlers for application %s" % app_name)
        models.signals.post_syncdb.send(sender=app, app=app,
            created_models=created_models, verbosity=verbosity,
            interactive=interactive, db=db)

這個函式emit_post_sync_signal正是被manage.py的兩大命令syncdb和flush所呼叫的,從而每次manage.py syncdb總是這麼見效。

以上總結,是敝人所思所想,若有不對的地方,請果斷拍磚~

嗨,就到這裡,就到這裡吧!