Django包含了一个“信号发射器”,可以帮助解耦后的应用在框架其他地方发生某些动作的时候得到通知。总结为一句话:信号机制允许特定的发送者(sender)去通知多个接收者(receiver)某些动作(action)发生了。当许多分散的代码对某个相同的事件感兴趣时,信号机制就变得特别有用了。
Django提供了一系列内置信号让用户代码在某些动作发生后得到通知。其中包含了许多有用的信号:
- django.db.models.signals.pre_save & django.db.models.signals.post_save
当一个模型的save函数被调用前或被调用后时发送的信号。
- django.db.models.signals.pre_delete & django.db.models.signals.post_delete
当一个模型的delete函数或一个查询集的delete函数被调用前或被调用后发送的信号。
- django.db.models.signals.m2m_changedz
当一个ManyToMany字段被修改时发送的信号。
- django.core.signals.request_started & django.core.signals.request_finished
当Django开始或结束一次HTTP请求时发送的信号。
3.22.1监听信号
为了接收信号,需要使用Signal.connect()函数注册一个接收者函数(receiver function),当信号被发送时,这个接收者函数将会被执行,所有的接收者函数会一次一个地执行,且执行顺序就是它们被注册的顺序。
Signal.connect(receiver,sender=None,weak=True,dispatch_uid=None)
参数解释:
- receiver-与信号绑定的回调函数。
- sender-指定一个特定的发送者来发送信号。
- weak-Django默认把信号处理函数存储为弱引用,因此,如果你的receiver回调函数是一个本地函数,那么它可能会被垃圾回收。
- dispatch_uid-一个信号对应的一个独一无二的标识符,以应对多个信号被发送的情况。
让我们通过注册一个在每次HTTP请求结束后会调用的接收者函数来领会一下信号机制是如何工作的,这个接收者函数将会被绑定到request_finished信号。
接收者函数
首先,我们需要定义一个接收者函数。一个接收者函数可以是任何python函数或方法:
def my_callback(sender, **kwargs):
print("Request finished!")
注意这个函数需要一个sender参数和关键字参数(**kwargs);所有的信号处理函数都必须包含这些参数。
我们之后再研究sender参数,现在来看下**kwargs参数。所有的信号发送关键字参数,并且有可能在某个时刻改变这些关键字参数。在request_finished这个信号的文档中,写明了这个信号发送时没有参数,这意味着我们可能被诱导去写下这样的信号处理函数:my_callback(sender)。
然而这种写法是错误的,实际上,如果你这么写了,Django会抛出一个错误。这是因为参数可以新增,而你的接收者函数必须有能力处理这些新增的参数。
绑定接收者函数
有两种方法可以将一个接收者函数与一个信号绑定,第一种:
from django.core.signals import request_finished
request_finished.connect(my_callback)
另一种方法则是使用receiver()装饰器,先看下这个装饰器的介绍:
receiver(signal)
参数 sginal-与某个函数绑定的信号(单个或者多个,多个的话则以列表形式组织)
下面是如何使用这个装饰器的例子:
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函数就会在每次HTTP请求结束后执行啦。
这些代码应该放在哪里?
严格来讲,信号处理函数和注册代码可以写在你喜欢的任何地方。但是我们建议您避免在应用的根目录和models模块里写这些代码,以最小化新代码的副作用。
在实际操作中,信号处理函数通常在关联的应用的signals子模块里定义,在应用配置类的reday函数里面与信号绑定。如果你使用的是receiver()装饰器,请在ready函数里面导入signals子模块。
与特定发送者发送的信号绑定
一种信号会被许多不同的发送者发送许多次,不过你可能只对其中一部分信号感兴趣。举一个例子,django.db.models.signals.pre_save这个信号会在一个model被保存之前发送。大多数时候,你不需要知道所有的model被保存的时间点,只需要知道某个特定的model被保存的时间点。
在这种情况下,你可以注册一个接收者函数,只接受由特定发送者发送的信号。对于django.db.models.signals.pre_save来说,这个发送者是某个model类,所以你需要写明你需要哪些类发送这个信号。
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()函数在测试期间可能会执行多次(上文提过了,通常会把绑定接收者函数和信号的代码放到ready函数里)。更常见的,当你的项目导入了某个定义了信号的模块,上述情况就会发生,因为信号注册代码每一次被导入都会运行一次。
如果这种行为造成了问题(比如当一个模型被保存时发送一封邮件),你可以使用一个独一无二的标识符-dispatch_uid参数去标识你的接收者函数。这个标识符通常是一个字符串。最终结果就是对于每个标识符,你的接收者函数将只会被绑定到信号一次,例子如下:
from django.core.signals import request_finished
request_finished.connect(my_callback, dispatch_uid="my_unique_identifier")
3.22.2 定义并发送信号
你的应用程序可以利用信号机制的优势,同时也可以提供自定义的信号。
什么时候使用自定义的信号?
信号是一种隐式的函数调用,它会让调试变得更困难。如果发送者和接收者都在你的项目里,你最好是使用一个显式的函数调用。
定义信号
class Signal(providing_args = list)
所有的信号都是django.dispatch.Signal的实例。参数providing_args是一个参数名的列表,由信号提供给监听者。不过,没有任何机制检测信号是否提供了这些参数给监听者,所以这个参数纯粹是让文档变得更漂亮而已。
举个例子:
import django.dispatch
pizza_done = django.dispatch.Signal(providing_args=["toppings","size"])
这里声明了一个pizza_done信号,而且会向它的接收者提供toppings和size这两个参数。
请记住你可以随时改变这个参数列表,所以让API在第一次尝试运行时就正确是不必要的。
发送信号
在Django中,由两种方法可以发送信号。
Signal.send(sender, **kwargs)-所有内置信号都使用这个方法来发送
Signal.send_robust(sender, **kwargs)
为了发送一个信号,你需要调用以上两个方法中的某一个。你必须提供sender参数(通常是一个类),你还可以按照自己的喜好提供其他的关键字参数。
举个例子,这里是如何发送我们上面定义的pizza_done信号的代码:
class PizzaStore:
...
def send_pizza(self,toppings,size):
pizza_done.send(sender=self.__class__,toppings=toppings,size=size)
send()和send_robust()都会返回一个元组对的列表[(receiver,response),...],表示所有接收者函数和它们的返回值。
send()与send_robust()的不同之处在于当接收者函数抛出异常时如何处理一场。send()不会捕捉任何异常,它允许错误增加。因此当有错误时,不是所有的接收者都会得到信号的通知。
send_robust()捕捉所有的错误(这里指由python的Exception类派生出来的错误),并且保证所有的接收者函数都能接到这个信号的通知。如果一个错误出现,这个错误实例就会被包含在元组对里被返回给抛出这个错误的接收者。
3.22.3 与信号解绑
Signal.disconnect(receiver=None,sender=None,dispatch_uid=None)
本篇文章只是基于官方文档的翻译,当作讲稿就有些本本主义了,之后有了实践经验会再完善,目前就让诸君勉为其难一读吧。