Django Signal

Django的信号机制中,并没有用到比较高深的东西,其实仅仅只是对方法的调用而已。中间使用了一些手段来保证线程安全以及对象的正确回收。

1. 调用信号

首先来看我们使用信号所编写的函数:

1
2
3
4
5
6
7
8
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver

receiver(post_save, sender=User)
def create_tkuser(sender, instance, created, **kwargs):
if created:
# Do something......

编写一个发送方为User、信号为post_save的接收函数create_tkuser。该函数使用了receiver函数进行装饰,那么首先分析receiver函数所做的事情。

1
2
3
4
5
6
7
8
9
def receiver(signal, **kwargs):
def _decorator(func):
if isinstance(signal, (list, tuple)):
for s in signal:
s.connect(func, **kwargs)
else:
signal.connect(func, **kwargs)
return func
return _decorator

在我们自己编写的代码中,传入receiversignal参数为post_save, **kwargssender=User
那么审视闭包后可以得到最终可以得到实际调用的为:

1
post_save.connect(create_tkuser, sender=User)

2. post_save

post_save其实是一个类的实例,这个实例在Django的全局进行使用。那么因为模块是Python中最方便的单例模式的实现方式,故在Django的运行环境中,只有这一个实例。

1
post_save = ModelSignal(providing_args=["instance", "raw", "created", "using", "update_fields"], use_caching=True)

ModelSignal继承自Signal,在我们编写的代码中,主要所用到的一些方法均来自于父类Signal。故对Signal进行仔细梳理。

3. Signal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Signal(object):
def __init__(self, providing_args=None, use_caching=False):
# post_save实例化ModelSignal并进行了初始化参数的传递,那么providing_args就是上面的list
self.receivers = []
if providing_args is None:
providing_args = []
# 对传入的参数list进行去重
self.providing_args = set(providing_args)
# 线程锁,保证线程安全
self.lock = threading.Lock()
# 此处self.use_caching=True
self.use_caching = use_caching
self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {}
self._dead_receivers = False

Signal__init__方法中定义了一些常规的实例参数以及对传入的数据进行部分处理,值得注意的是weakref.WeakKeyDictionary()。这里对weakref模块做一个简单的介绍。
Python的垃圾回收机制中,主要的算法为引用计数,即当一个对象的引用计数为0时,该对象就会被回收。weakref模块其实是对对象的一个弱引用,相对于强引用而言。弱引用并不会增加一个对象的引用计数,也就是说当一个对象只存在弱引用时,随时都会被GC模块进行回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
import weakref

class Foo(object):
def __init__(self, name):
self.name = name

obj1 = Foo("Smart") # obj1 <__main__.Foo at 0x7f4bc5587590>
ref = weakref.ref(obj1) # 创建一个obj1对象的弱引用
ref
<weakref at 0x7f4bc5506f70; to 'Foo' at 0x7f4bc5587590>
obj1 = None # 此时<__main__.Foo at 0x7f4bc5587590>的引用计数为0,仅有一个弱引用
ref # 那么当然的被收回收掉了
<weakref at 0x7f4bc5506f70; dead>

此外weakref.ref模块可以接受一个callback函数,用于当弱引用的对象将被销毁回收时调用,该回调函数必须接受所传递的对象。

1
2
3
4
5
6
7
8
def callback(instance):
print instance
print "Calling func callback"
obj2 = Foo("bar")
ref = weakref.ref(obj2, callback)
obj1 = None
<weakref at 0x7f4bc5506890; dead>
Calling func callback

weakref.WeakKeyDictionary()其实本质上是一个字典,只不过字典key为对象的弱引用而已。类似的还有weakref.WeakValueDictionary(),字典的value为对象的弱引用。
post_save这个实例建立完毕以后,调用.connect方法,并将create_tkuser, sender=User作为参数传入.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def _make_id(target):
# __func__方法其实是一个类函数才会拥有的属性,一般的函数并没有
if hasattr(target, '__func__'):
return (id(target.__self__), id(target.__func__))
return id(target)

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:
# 执行该流程下的语句,得到一个二元tuple
lookup_key = (_make_id(receiver), _make_id(sender))
if weak:
ref = weakref.ref
receiver_object = receiver
# Check for bound methods
if hasattr(receiver, '__self__') and hasattr(receiver, '__func__'):
ref = WeakMethod
receiver_object = receiver.__self__
if six.PY3:
receiver = ref(receiver)
weakref.finalize(receiver_object, self._remove_receiver)
else:
# 在python2中将会执行下面的语句, receiver是一个弱引用对象
receiver = ref(receiver, self._remove_receiver)
with self.lock:
self._clear_dead_receivers()
# 在__init__方法中 self.receivers = []
for r_key, _ in self.receivers:
if r_key == lookup_key:
break
else:
self.receivers.append((lookup_key, receiver))
# 那么此时self.receivers = [(receiver_id, sender_id), create_user函数地址]
self.sender_receivers_cache.clear()

单独审视这个函数其实所执行的语句并不是很多,实际调用为:
post_save.connect(create_tkuser, sender=User, weak=True, dispatch_uid=None)
在该函数执行完毕后,实例属性self.receivers结果为:

1
2
self.receivers = [((139895460012952, 53111536),
<weakref at 0x7f3bfe78e680; to 'function' at 0x7f3bf3332398 (create_tkuser)>)]

那么connect方法结束后,装饰器也就结束了自己的装饰,需要保存的信息均位于post_save这个方法之中。
准备工作做完,就等着小伙伴儿上砧板开搞了。
post_save是指在某一个model将数据写入数据库之后所要做的事情,那么触发机制一定会写在Model中。
果不其然在Model类中的save_base方法中调用了post_save.send方法

1
2
3
4
5
6
7
8
9
10
11
class Model(six.with_metaclass(ModelBase)):
......
def save_base(self, raw=False, force_insert=False,
force_update=False, using=None, update_fields=None):
if not meta.auto_created:
signals.pre_save.send(sender=origin, instance=self, raw=raw, using=using,
update_fields=update_fields)
...... # 这里对数据进行了入库操作,代码略过
if not meta.auto_created:
signals.post_save.send(sender=origin, instance=self, created=(not updated),
update_fields=update_fields, raw=raw, using=using)

核心代码为:

1
2
signals.post_save.send(sender=origin, instance=self, created=(not updated),
update_fields=update_fields, raw=raw, using=using)

那么传递的参数origincls = origin = self.__class__,那么对于User来讲就是User,然后来看.send方法,该方法其实非常简单

1
2
3
4
5
6
7
8
9
10
def send(self, sender, **named):
responses = []
# self.receivers并不为空,在上面的代码中已经给出的具体的例子
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return responses
# 在这里调用的self._live_receivers这个方法
for receiver in self._live_receivers(sender):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
return responses

self._live_receivers
这个方法比较长,进行逐行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    def _live_receivers(self, sender):
receivers = None
# self.use_caching == True, self._dead_receivers == False, 所以进入下面的判断
if self.use_caching and not self._dead_receivers:
# 此时self.sender_receivers_cache是一个key为弱引用的字典,而且里面啥都没有
# receivers将会得到None值,不满足 receivers is NO_RECEIVERS 该条件
receivers = self.sender_receivers_cache.get(sender)
if receivers is NO_RECEIVERS:
return []
if receivers is None:
with self.lock:
self._clear_dead_receivers()
# 此时sender=User, 那么senderkey得到的值和装饰器调用connect方法时得到的值相同
senderkey = _make_id(sender)
receivers = []
# 对self.receivers进行迭代,这里注释将最开始得到的self.receivers贴出
# self.receivers = [((139895460012952, 53111536),
# <weakref at 0x7f3bfe78e680; to 'function' at 0x7f3bf3332398 (create_tkuser)>)]
for (receiverkey, r_senderkey), receiver in self.receivers:
# r_senderkey == senderkey 条件成立
if r_senderkey == NONE_ID or r_senderkey == senderkey:
# 那么receivers列表将会添加对函数create_tkuser的弱引用
receivers.append(receiver)
if self.use_caching:
if not receivers:
self.sender_receivers_cache[sender] = NO_RECEIVERS
else:
# 这里对弱引用字典进行赋值操作,key为User的弱引用,值其实也是弱引用
self.sender_receivers_cache[sender] = receivers
non_weak_receivers = []
for receiver in receivers:
if isinstance(receiver, weakref.ReferenceType):
# 这里并不是真正的调用函数,而是获取到弱引用的对象,一定要分清楚
receiver = receiver()
if receiver is not None:
non_weak_receivers.append(receiver)
else:
non_weak_receivers.append(receiver)
# 最后将会返回一个函数list
return non_weak_receivers

回头再来看刚才的send方法

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

那么self._live_receivers(sender)将会返回一个跟sender参数相关的函数list,也就是要执行的函数列表。response = receiver(signal=self, sender=sender, **named),也就是在这里正式的执行create_tkuser方法,并将执行的函数和结果丢到responses列表中。
那么到这里,信号的显式调用也就结束了,达到了我们在调用Usersave方法之后并调用create_tkuser方法的效果。
回顾整个代码流程,最重要的就是利用了模块天然的单例模式以及弱引用机制。解除了代码的耦合以及避免硬编码等。


Alt text

2. 信号量在Django工程文件位置

在日常的开发之中,使用较为频繁的信息量为post_save,pre_save等,那么这些信号量都与model相关,所在位置应位于model所在的app中:

1
2
3
4
5
6
7
8
9
--ProjectName
|-- apps
|--my_app
|--__init__.py
|--apps.py
|--views.py
|--models.py
|--tests.py
|--signals.py

signals.py中编写信号逻辑:

1
2
3
4
5
6
7
8
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver

receiver(post_save, sender=User)
def create_tkuser(sender, instance, created, **kwargs):
if created:
# Do something......

此时signals.py中的信号量并不会被django环境所读取,需要将其添加至django的启动环境中.

1. 重写AppConfig
1
2
3
4
5
6
7
8
9
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.apps import AppConfig

class MyappConfig(AppConfig):
name = 'my_app'

def ready(self):
import my_app.signals
2. init文件中指定Config

__init__.py:

1
default_app_config = 'my_app.apps.MyappConfig'

3. settings.INSTALL_APPS重新指定app配置文件
1
2
3
4
5
INSTALLED_APPS = [
....
'my_app.apps.MyappConfig',
...
]

在上面的配置过程中,出现了一个坑……
在我的signals.py文件中,导入了一个位于项目根目录下面的package,该package中用到了django.core.cache, 那么在测试的过程中添加了这样的文件头:

1
2
3
4
5
6
import os
import django
os.environ.update({"DJANGO_SETTINGS_MODULE": "project.settings"})
django.setop()

# 一堆代码......

导致了在DEBUG Django的时候无法启动,注释后代码运行正常.