Django describes signal as a “signal dispatcher” that triggers multiple applications, mainly in the form of signals. This article will explain how signals work in Django and how to use them from the perspective of source code analysis.
The most common scenario for signal is notifications. For example, if your blog has comments, the system will have a notification mechanism to push the comments to you. If you use signal, you only need to trigger the signal notification when the comment is posted, instead of putting the notification logic after the comment is posted, which greatly reduces the program coupling and facilitates the maintenance of the system later.
Django implements a Signal class, which is used to implement the function of “signal dispatcher”. The working mechanism is shown in the figure below, which is divided into two parts: first, each callback function that needs to be dispatched is registered to signal, and second, the event triggers
sender to send the signal.
The signal maintains a list
receiver of callback functions and their
ids that connect to the signal. Each
receiver must be a callback function and accepts the keyword argument
**kwarg, and the signal’s
connect method is used to connect the callback function to the signal.
Let’s take a look at the source code of
connect, as follows.
The code is very clean and is divided into four parts: checking parameters, getting the ID of
receiver weak references, and locking. Here we mainly look at the last two parts.
receiver weak reference
Weak references: Python’s approach to garbage collection is to mark references, and the role of weak references is to avoid memory leaks caused by circular references. The main principle is that when a weak reference is made to an object, the number of references is not added to the reference mark, so when the strong reference to the object is 0, the system still reclaims it, and the weak reference fails.
method and function: Python’s functions are the same as those of other languages, including function names and function bodies, and supporting formal parameters; compared to functions, methods have an additional layer of class relationships, that is, they are functions defined in classes. This function is used to configure
method step by step, see an excerpt of the source code.
In addition to the function attribute
im_func, there is an
im_self attribute table
self and an
im_class attribute table
Bound Method and Unbound Method: Methods can be divided into
bound methods and
unbound methods. The difference is that
bound methods have an additional layer of instance binding, i.e.,
bound method invokes a method through an instance, while
unbound method invokes a method directly through a class.
Weak references in signal:
Anyone familiar with Python garbage collection should know that Python only garbage collects an object when its reference count is 0. Therefore, all references to callback functions in signal are weakly referenced by default to avoid memory leaks.
weak argument to
connect indicates whether to use a weak reference, and the default is
receiver can be either a function or a method, and the reference to
bound method is short-lived, in line with the lifetime of the instance, so the standard weak reference is not enough to keep it, and
weakref. WeakMethod to emulate the weak reference of
bound method; finally the
weakref.finalize method returns a callable terminator object that is called when
receiver is garbage collected, unlike normal weak references, the terminator will always be alive before it is called and die after it is called, thus greatly simplifying life cycle management.
Locks exist to achieve thread safety, which means that when multiple threads are present at the same time, the result still meets expectations. Obviously, the
receiver registration process in signal is not inherently thread-safe; the thread-safe method in signal is locking to achieve atomic operation of the
Locks are defined in the signal’s
__init__ method, using
Lock from the standard library.
signal encapsulates the three operations of cleaning up weakly referenced objects in the
receiver list, adding elements to the
receiver list, and cleaning up the global cache dictionary into atomic operations using thread locks, as follows.
To be precise, the
sender in signal is an identifier to record “who” triggered the signal, what really works is the
send method, which is used in the
event to trigger the signal to all
event. Here is the source code for
It is easy to see that the process of triggering all the recorded callback functions is synchronous, so signal is not suitable for handling large batches of tasks, but we can rewrite it as asynchronous tasks.
How to use signal
The use of signal requires only two configurations, one is the registration of the callback function, and the other is the event triggering.
There are two ways to register callback functions, one is the regular
signal.connect(); the other is that Django signal provides a decorator
receiver, which can be decorated by passing in which signal it is, or you can specify
sender, and if you don’t specify it, you receive all the information sent by
sender. Event triggering is done by calling
<your_signal>.send() where it can be triggered. A demo is given below.
Django’s built-in signals
Django has a number of built-in signals that we can use directly.