6

Django源码阅读(二)-Signal

 2 years ago
source link: https://dreamgoing.github.io/Django%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB(%E4%BA%8C)-Signal.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Django源码阅读-Signal

介绍和学习Django中面向对象的Signal的一种实现

在linux系统中,signal即一个进程可以向另一个进程发送挂起(SIGHUP 1), 杀死(SIGKILL 9)来控制进程的行为。在Django中也有很类似的实现,是更加高级语言的实现,一种Signal dispatcher的机制,即当一个发生时,被解耦的应用可以接收到信号并作出相应的处理。

Signals 在Django中有很多使用,其目的主要是当某个信号发生时,发送信号告知接受者集合,接收者集合可以进行相应的处理。例如: pre_save, post_save可以在model存数据库之前进行一些操作。

  • model.save() 触发django.db.models.signals.pre_save|post_save
  • model.delete() 触发django.db.models.signals.pre_delete|post_delete
  • ManyToManyField 这种关联字段发送改变时,触发django.db.models.signals.m2m2_changed
  • HTTP requset Django在开始结束一个http请求,触发django.core.signals.request_started | request_finished

下面即是一个简单的例子

xxxxxxxxxx
from django.db import models
from django.db.models import signals
def create_customer(sender, instance, created, **kwargs):
    print "Save is called"
class Customer(models.Model):
    name = models.CharField(max_length=16)
    description = models.CharField(max_length=32)
signals.post_save.connect(receiver=create_customer, sender=Customer)
# connect函数可以使用receiver装饰器代替,即表明该函数是,pre_save的一个接受者
@receiver(signals.pre_save, sender=Customer)
def create_customer(sender, instance, created, **kwargs):
    print "customer created"
x
In [1]: obj = Customer(name='foo', description='foo in detail')
In [2]: obj.save()
Save is called

在Django框架中有很多地方利用了Signal来解耦各个模块,例如在用户登录模块:

首先定义了用户登录的信号

user_logged_in = Signal(providing_args=['request', 'user'])

在Auth模块的AppConfig中

x
class AuthConfig(AppConfig):
    name = 'django.contrib.auth'
    verbose_name = _("Authentication and Authorization")
    def ready(self):
        # Django 仅App启动时加载
        post_migrate.connect(
            create_permissions,
            dispatch_uid="django.contrib.auth.management.create_permissions"
        )
        if hasattr(get_user_model(), 'last_login'):
            from .models import update_last_login
            # 将更新最后一次登录时间的函数,与用户登录信号绑定在一起
            user_logged_in.connect(update_last_login, dispatch_uid='update_last_login')
        checks.register(check_user_model, checks.Tags.models)
        checks.register(check_models_permissions, checks.Tags.models)

在通用的login函数中最后发送用户登录的信号,并触发更新用户最后一次登录时间的函数

xxxxxxxxxx
def login(request, user, backend=None):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request. Note that data set during
    the anonymous session is retained when the user logs in.
    """
    # 用户密码的HMAC
    session_auth_hash = ''
    # 获取当前用户
    if user is None:
        user = request.user
    if hasattr(user, 'get_session_auth_hash'):
        session_auth_hash = user.get_session_auth_hash()
    if SESSION_KEY in request.session:
        # 如果request 的session key的key与session base存储的不一致
        if _get_user_session_key(request) != user.pk or (
                session_auth_hash and
                not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)):
            # To avoid reusing another user's session, create a new, empty
            # session if the existing session corresponds to a different
            # authenticated user.
            # 清楚已有数据,并重新生成一个session key
            request.session.flush()
    else:
        # 存储一致则以新的key存储已有数据
        request.session.cycle_key()
    try:
        backend = backend or user.backend
    except AttributeError:
        backends = _get_backends(return_tuples=True)
        if len(backends) == 1:
            _, backend = backends[0]
        else:
            raise ValueError(
                'You have multiple authentication backends configured and '
                'therefore must provide the `backend` argument or set the '
                '`backend` attribute on the user.'
            )
    # 在session中更新登录状态的一些信息,SESSION KEY, auth_hash等等
    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    request.session[BACKEND_SESSION_KEY] = backend
    request.session[HASH_SESSION_KEY] = session_auth_hash
    if hasattr(request, 'user'):
        request.user = user
    # 创建CSRF的cookie
    rotate_token(request)
    # 发送登录的信号
    user_logged_in.send(sender=user.__class__, request=request, user=user)

上述是Django用户登录的信号user_logger_in, 相应的还有其他的信号例如user_login_failed,user_logged_out.

弱引用weakref

在介绍源码之前,先了解一下弱引用的概念。插一句Python中的weakref于C++中的weak_ptr基本一样,弱引用会引用一个对象,但是不会保证被引用的对象的状态,也不会增加引用计数。即当gc回收了对象时,弱引用将会拿不到被回收对象。得益于弱引用上述特定,弱引用主要的用途是实现大对象的mapping或者cache,而使用弱引用存储的对象有如下特定,不会仅仅单独出现在mapping或cache中。

Python弱引用最常用的场景是维护接收(信号)Notify的对象,且整个Notification系统不会阻止GC,当某个接收信号的对象被回收时,相应的Notify也不会发送给它,这样看来合情合理。

xxxxxxxxxx
class Signal:
    """
    Base class for all signals
    Internal attributes:
    Signal 初始化内部存储结构如下:
        receivers
            { receiverkey (id) : weakref(receiver) }
    """
    def __init__(self, providing_args=None, use_caching=False):
        """
        Create a new signal.
        providing_args
            A list of the arguments this signal can pass along in a send() call.
        """
        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):
        """
        Connect receiver to sender for signal.
        Arguments:
            receiver
                A function or an instance method which is to receive signals.
                Receivers must be hashable objects.
                If weak is True, then receiver must be weak referenceable.
                Receivers must be able to accept keyword arguments.
                If a receiver is connected with a dispatch_uid argument, it
                will not be added if another receiver was already connected
                with that dispatch_uid.
            sender
                The sender to which the receiver should respond. Must either be
                a Python object, or None to receive events from any sender.
            weak
                Whether to use weak references to the receiver. By default, the
                module will attempt to use weak references to the receiver
                objects. If this parameter is false, then strong references will
                be used.
            dispatch_uid
                An identifier used to uniquely identify a particular instance of
                a receiver. This will usually be a string, though it may be
                anything hashable.
        """
        from django.conf import settings
        # If DEBUG is on, check that we got a good receiver
        if settings.configured and settings.DEBUG:
            assert callable(receiver), "Signal receivers must be callable."
            # Check for **kwargs
            if not func_accepts_kwargs(receiver):
                raise ValueError("Signal receivers must accept keyword arguments (**kwargs).")
        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
            if hasattr(receiver, '__self__') and hasattr(receiver, '__func__'):
                ref = weakref.WeakMethod
                receiver_object = receiver.__self__
            receiver = ref(receiver)
            weakref.finalize(receiver_object, self._remove_receiver)
        with self.lock:
            # 清理已经被GC回收的receiver
            self._clear_dead_receivers()
            for r_key, _ in self.receivers:
                # receiver已经在当前信号中
                if r_key == lookup_key:
                    break
            else:
                # 将receiver添加到Signal内部
                self.receivers.append((lookup_key, receiver))
            self.sender_receivers_cache.clear()
     def send(self, sender, **named):
        """
        Send signal from sender to all connected receivers.
        If any receiver raises an error, the error propagates back through send,
        terminating the dispatch loop. So it's possible that all receivers
        won't be called if an error is raised.
        Arguments:
            sender
                The sender of the signal. Either a specific object or None.
            named
                Named arguments which will be passed to receivers.
        Return a list of tuple pairs [(receiver, response), ... ].
        """
        if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
            return []
        return [
            # 调用发送的信号
            (receiver, receiver(signal=self, sender=sender, **named))
            for receiver in self._live_receivers(sender)
        ]

Reference


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK