Django中的Signal代码详解


Posted in Python onFebruary 05, 2018

本文研究的主要是Django开发中的signal 的相关内容,具体如下。

前言

在web开发中, 你可能会遇到下面这种场景:

在用户完成某个操作后, 自动去执行一些后续的操作. 譬如用户完成修改密码后,
你要发送一份确认邮件.

当然可以把逻辑写在一起,但是有个问题是,触发操作一般不止一种(如用户更改了其它信息的确认邮件),这时候这个逻辑会需要写多次,所以你可能会想着DRY(Don't repeat yourself),于是你把它写到了一个函数中,每次调用。当然这是没问题的.

但是, 如果你换个思路你会发现另一个完全不同的方案, 即:

  • 类似于daemon的程序监听着特定的事件
  • 前置操作来触发相应的事件
  • 监听程序执行对应的操作

这样的好处是什么呢?

  • 松耦合(不用把后续操作写在主逻辑中)
  • 便于复用(这也是为什么django本身,及第三方应用如pinax大量使用此技术的原因),在各种高级语言中都会有类似的特性,如java,javascript等,而在django中我们使用signal。

观察者模式

Siganl是Django框架中提供的一个 “信号分发器”。它是设计模式中经常提到的观察者模式的一个实现应用。

在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。

观察者模式的使用场景

  • 关联行为场景,需要注意的是,关联行为是可拆分的,而不是“组合”关系。
  • 事件多级触发场景。
  • 跨系统的消息交换场景,如消息队列、事件总线的处理机制。

优点

1.解除耦合,让耦合的双方都依赖于抽象,从而使得各自的变换都不会影响另一边的变换。

它在被观察者和观察者之间建立一个抽象的耦合。被观察者角色所知道的只是一个具体观察者列表,每一个具体观察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体观察者,它只知道它们都有一个共同的接口。

由于被观察者和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。这种耦合性使得代码的可读性、维护性大大提高。

2.观察者模式实现了动态联动;

由于观察者模式对观察者注册实行管理,那就可以在运行期间,通过动态的控制注册的观察者来控制某个动作的联动范围,从而实现动态联动。

3.观察者模式支持广播通信。

目标发送通知给观察者是面向所有注册的观察者,所以目标每次通知的信息就要对所有注册的观察者进行广播,也可以在目标上添加新的方法来限制广播的范围。

Django 中Siganl 机制的典型应用是,框架为 Models 创建了 pre_save、post_save等与Model的某些方法调用相关联的信号,如pre_save 和 post_save 分别会在 Modle的save()方法的调用之前和之后通知观察者,从而让观察者进行一系列操作。

django signal的处理是同步的,勿用于处理大批量任务。
django signal对程序的解耦、代码的复用及维护性有很大的帮助。

Signal 机制的实现方式

Siganl的源码位于django dispatch包下,主要的代码位于 dispatcher.py中。

在dispatcher中定义了Signal类,以及一个用于使用Python装饰器的方式来连接信号以及信号接受者的方法receiver(signal,**kwargs)。

class Signal(object):
  """
  Base class for all signals

  Internal attributes:

    receivers
      { receiverkey (id) : weakref(receiver) }
  """
  def __init__(self, providing_args=None, use_caching=False):
    """
    创建一个新的Signal
    providing_args 参数,指定这个Siganl 在发出事件(调用send方法)时,可以提供给观察者的信息参数
    比如 post_save()会带上 对应的instance对象,以及update_fields等
    """
    self.receivers = []
    if providing_args is None:
      providing_args = []
    self.providing_args = set(providing_args)
    self.lock = threading.Lock()
    self.use_caching = use_caching
    # For convenience we create empty caches even if they are not used.
    # A note about caching: if use_caching is defined, then for each
    # distinct sender we cache the receivers that sender has in
    # 'sender_receivers_cache'. The cache is cleaned when .connect() or
    # .disconnect() is called and populated on send().
    self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {}
    self._dead_receivers = False

  def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):

    from django.conf import settings

    if dispatch_uid:
      lookup_key = (dispatch_uid, _make_id(sender))
    else:
      lookup_key = (_make_id(receiver), _make_id(sender))

    if weak:
      ref = weakref.ref
      receiver_object = receiver
      # Check for bound methods
      # 构造弱引用的的receiver
      if hasattr(receiver, '__self__') and hasattr(receiver, '__func__'):
        ref = WeakMethod
        receiver_object = receiver.__self__
      if sys.version_info >= (3, 4):
        receiver = ref(receiver)
        weakref.finalize(receiver_object, self._remove_receiver)
      else:
        receiver = ref(receiver, self._remove_receiver)

    with self.lock:
      #clear掉 由于弱引用 已被垃圾回收期回收的receivers
      self._clear_dead_receivers()
      for r_key, _ in self.receivers:
        if r_key == lookup_key:
          break
      else:
        self.receivers.append((lookup_key, receiver))
      self.sender_receivers_cache.clear()

  def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None):

    if dispatch_uid:
      lookup_key = (dispatch_uid, _make_id(sender))
    else:
      lookup_key = (_make_id(receiver), _make_id(sender))

    disconnected = False
    with self.lock:
      self._clear_dead_receivers()
      for index in range(len(self.receivers)):
        (r_key, _) = self.receivers[index]
        if r_key == lookup_key:
          disconnected = True
          del self.receivers[index]
          break
      self.sender_receivers_cache.clear()
    return disconnected

  def has_listeners(self, sender=None):
    return bool(self._live_receivers(sender))

  def send(self, sender, **named):

    responses = []
    if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
      return responses

    for receiver in self._live_receivers(sender):
      response = receiver(signal=self, sender=sender, **named)
      responses.append((receiver, response))
    return responses

  def send_robust(self, sender, **named):

    responses = []
    if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
      return responses

    # Call each receiver with whatever arguments it can accept.
    # Return a list of tuple pairs [(receiver, response), ... ].
    for receiver in self._live_receivers(sender):
      try:
        response = receiver(signal=self, sender=sender, **named)
      except Exception as err:
        if not hasattr(err, '__traceback__'):
          err.__traceback__ = sys.exc_info()[2]
        responses.append((receiver, err))
      else:
        responses.append((receiver, response))
    return responses

  def _clear_dead_receivers(self):
    # Note: caller is assumed to hold self.lock.
    if self._dead_receivers:
      self._dead_receivers = False
      new_receivers = []
      for r in self.receivers:
        if isinstance(r[1], weakref.ReferenceType) and r[1]() is None:
          continue
        new_receivers.append(r)
      self.receivers = new_receivers

  def _live_receivers(self, sender):
    """
    过滤掉 已经被 垃圾回收的receiver
    """
    receivers = None
    # 如果使用了cache , 并且没有调用过_remove_receiver 函数 则去 sender_receivers_cache中查找
    if self.use_caching and not self._dead_receivers:
      receivers = self.sender_receivers_cache.get(sender)
      # We could end up here with NO_RECEIVERS even if we do check this case in
      # .send() prior to calling _live_receivers() due to concurrent .send() call.
      if receivers is NO_RECEIVERS:
        return []
    if receivers is None:
      with self.lock:
        self._clear_dead_receivers()
        senderkey = _make_id(sender)
        receivers = []
        for (receiverkey, r_senderkey), receiver in self.receivers:
          if r_senderkey == NONE_ID or r_senderkey == senderkey:
            receivers.append(receiver)
        if self.use_caching:
          if not receivers:
            self.sender_receivers_cache[sender] = NO_RECEIVERS
          else:
            # Note, we must cache the weakref versions.
            self.sender_receivers_cache[sender] = receivers
    non_weak_receivers = []
    for receiver in receivers:
      if isinstance(receiver, weakref.ReferenceType):
        # Dereference the weak reference.
        receiver = receiver()
        if receiver is not None:
          non_weak_receivers.append(receiver)
      else:
        non_weak_receivers.append(receiver)
    return non_weak_receivers

  def _remove_receiver(self, receiver=None):

    self._dead_receivers = True

connect方法

connect方法用于连接信号和信号处理函数,类似的概念相当于为某个事件(信号发出表示一个事件)注册观察者(处理函数),函数参数中receiver就是信号处理函数(函数也是对象,这太方便了),sender表示信号的发送者,比如Django框架中的post_save()这个信号,任何一个模型在save()函数调用之后都会发出这个信号,但是我们只想关注某一个模型 save()方法调用的事件发生,就可以指定sender为我们需要关注的模型类。

weak参数表示是否将receiver转换成弱引用对象,Siganl中默认会将所有的receiver转成弱引用,所以如果你的receiver是个局部对象的话,那么receiver可能会被垃圾回收期回收,receiver也就变成一个dead_receiver了,Siganl会在connect和disconnect方法调用的时候,清除dead_receiver。

dispatch_uid,这个参数用于唯一标识这个receiver函数,主要的作用是防止receiver函数被注册多次,这样会导致receiver函数会执行多次,这可能是我们不想要的一个结果。

disconnect方法

disconnect方法用于断开信号的接收器,函数内首先会生成根据sender和receiver对象构造出的一个标识lookup_key,在遍历receiver数组时,根据lookup_key找到需要disconnect的receiver然后从数组中删除这个receiver。

send和send_robust

send和send_robust方法都是用于发送事件的函数,不同点在于send_robust函数中会捕获信号接收函数发生的异常,添加到返回的responses数组中。

Siganl类的使用

Django signal的处理过程如下图所示:

Django中的Signal代码详解

内建signal的使用

模型相关:

  • pre_save 对象save前触发
  • post_save 对象save后触发
  • pre_delete 对象delete前触发
  • post_delete 对象delete后触发
  • m2m_changed ManyToManyField 字段更新后触发

请求相关:

  • request_started 一个request请求前触发
  • request_finished request请求后触发

针对django自带的signal,我们只需要编写receiver 即可,使用如下。

第一步,编写receiver并绑定到signal

# myapp/signals/handlers.py

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

## decorators 方式绑定
@receiver(request_finished, dispatch_uid="request_finished")
def my_signal_handler(sender, **kwargs):
  print("Request finished!================================")

# 普通绑定方式
def my_signal_handler(sender, **kwargs):
  print("Request finished!================================")

request_finished.connect(my_signal_handler)

#####################################################
# 针对model 的signal 
from django.dispatch import receiver
from django.db.models.signals import post_save

from polls.models import MyModel


@receiver(post_save, sender=MyModel, dispatch_uid="mymodel_post_save")
def my_model_handler(sender, **kwargs):
 print('Saved: {}'.format(kwargs['instance'].__dict__))

dispatch_uid确保此receiver只调用一次

第二步,加载signal

# myapp/__init__py

default_app_config = 'myapp.apps.MySendingAppConfig'
# myapp/apps.py

from django.apps import AppConfig


class MyAppConfig(AppConfig):
  name = 'myapp'

  def ready(self):
    # signals are imported, so that they are defined and can be used
    import myapp.signals.handlers

到此,当系统受到request 请求完成后,便会执行receiver。

其他内建的signal,参考官方文档:

https://docs.djangoproject.com/en/1.9/topics/signals/

自定义signal的使用

自定义signal,需要我们编写signal和receiver。

第一步,编写signal

myapp.signals.signals.py

importdjango.dispatch

my_signal = django.dispatch.Signal(providing_args=["my_signal_arg1", "my_signal_arg_2"])

第二步,加载signal

# myapp/__init__py

default_app_config = 'myapp.apps.MySendingAppConfig'
myapp/apps.py

from django.apps import AppConfig


class MyAppConfig(AppConfig):
  name = 'myapp'

  def ready(self):
    # signals are imported, so that they are defined and can be used
    import myapp.signals.handlers

第三步,事件触发时,发送signal

# myapp/views.py

from .signals.signals import my_signal

my_signal.send(sender="some function or class",
        my_signal_arg1="something", my_signal_arg_2="something else"])

自定义的signal,django已经为我们编写了此处的事件监听。

第四步,收到signal,执行receiver

# myapp/signals/handlers.py

from django.dispatch import receiver
from myapp.signals.signals import my_signal


@receiver(my_signal, dispatch_uid="my_signal_receiver")
def my_signal_handler(sender, **kwargs):
  print('my_signal received')

此时,我们自定义的signal 便开发完成了。

总结

以上就是本文关于Django中的Signal代码详解的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!

Python 相关文章推荐
Python struct.unpack
Sep 06 Python
python实现同时给多个变量赋值的方法
Apr 30 Python
如何利用Fabric自动化你的任务
Oct 20 Python
查看django版本的方法分享
May 14 Python
Python使用min、max函数查找二维数据矩阵中最小、最大值的方法
May 15 Python
python的pip安装以及使用教程
Sep 18 Python
ubuntu 16.04下python版本切换的方法
Jun 14 Python
python3+PyQt5 实现Rich文本的行编辑方法
Jun 17 Python
Django中create和save方法的不同
Aug 13 Python
numpy库ndarray多维数组的维度变换方法(reshape、resize、swapaxes、flatten)
Apr 28 Python
Python+uiautomator2实现自动刷抖音视频功能
Apr 29 Python
Pygame如何使用精灵和碰撞检测
Nov 17 Python
Python实现XML文件解析的示例代码
Feb 05 #Python
Python下载网络文本数据到本地内存的四种实现方法示例
Feb 05 #Python
Python实现屏幕截图的两种方式
Feb 05 #Python
Python实现连接两个无规则列表后删除重复元素并升序排序的方法
Feb 05 #Python
用python实现对比两张图片的不同
Feb 05 #Python
使用pygame模块编写贪吃蛇的实例讲解
Feb 05 #Python
Python安装模块的常见问题及解决方法
Feb 05 #Python
You might like
数据库中排序的对比及使用条件详解
2012/02/23 PHP
探讨php中header的用法详解
2013/06/07 PHP
PHP用mb_string函数库处理与windows相关中文字符及Win环境下开启PHP Mb_String方法
2015/11/11 PHP
Referer原理与图片防盗链实现方法详解
2019/07/03 PHP
准确获得页面、窗口高度及宽度的JS
2006/11/26 Javascript
js下通过getList函数实现分页效果的代码
2010/09/17 Javascript
js实现特定位取反原理及示例
2014/06/30 Javascript
JS制作手机端自适应缩放显示
2015/06/11 Javascript
基于javascript html5实现多文件上传
2016/03/03 Javascript
jquery.multiselect多选下拉框实现代码
2016/11/11 Javascript
JavaScript数据结构中串的表示与应用实例
2017/04/12 Javascript
Node.js简单入门前传
2017/08/21 Javascript
jquery实现回车键触发事件(实例讲解)
2017/11/21 jQuery
Vue.js中的computed工作原理
2018/03/22 Javascript
vue中实现图片压缩 file文件的方法
2020/05/28 Javascript
python解析xml模块封装代码
2014/02/07 Python
Python Tkinter GUI编程入门介绍
2015/03/10 Python
常见python正则用法的简单实例
2016/06/21 Python
python中星号变量的几种特殊用法
2016/09/07 Python
python运行其他程序的实现方法
2017/07/14 Python
python2.7+selenium2实现淘宝滑块自动认证功能
2018/02/24 Python
Python使用Selenium模块实现模拟浏览器抓取淘宝商品美食信息功能示例
2018/07/18 Python
python+mysql实现学生信息查询系统
2019/02/21 Python
PyQt5的安装配置过程,将ui文件转为py文件后显示窗口的实例
2019/06/19 Python
如何通过Python3和ssl实现加密通信功能
2020/05/09 Python
实习生体会的自我评价范文
2013/11/28 职场文书
财务会计实习报告体会
2013/12/20 职场文书
党员2014两会学习心得体会
2014/03/17 职场文书
工作失误检讨书(3篇)
2014/10/11 职场文书
捐助倡议书
2015/01/19 职场文书
2016保送生自荐信范文
2016/01/29 职场文书
《童年的发现》教学反思
2016/02/18 职场文书
2016年基层党组织公开承诺书
2016/03/25 职场文书
2019年国庆祝福语(70句)
2019/09/19 职场文书
在HTML5 localStorage中存储对象的示例代码
2021/04/21 Javascript
MySQL 常见的数据表设计误区汇总
2021/06/07 MySQL