Blog

Signals & Slots, QT's observer pattern.

The concept of signals and slots is a fundamental component of QT's architecture. It is an implementation of the observer pattern, which allows objects to communicate with each other in a loosely coupled manner (without requiring the objects to know anything about each other's implementation). This enables the creation of highly modular and extensible applications.

  • Signals act as messengers that send out a notification when an event occurs. A signal does not perform any action by itself; instead, it announces that something has occurred.

  • Slots are essentially observers. A slot is a function that will be called in response to a signal being emitted. Multiple slots can be connected to a single signal.

import weakref, collections
from inspect import ismethod
import collections.abc

class Signal:
    def __init__(self):
        self._lock = False
        self._slots = []

    def emit(self, *args, **kwargs):
        if self._lock:
            return

        for ref in self._slots:
            if (slot := ref()) is not None:
                slot(*args, **kwargs)

    def wrap_weakref(self, slot):
        return weakref.WeakMethod(slot) if ismethod(slot) else weakref.ref(slot)

    def connect(self, slot):
        if not isinstance(slot, collections.abc.Callable):
            raise ValueError("Argument must be callable")

        ref = self.wrap_weakref(slot)

        if ref not in self._slots:
            self._slots.append(ref)

    def disconnect(self, slot):
        self._slots = [ref for ref in self._slots if ref() != slot]

    def block(self):
        self._lock = True

    def unblock(self):
        self._lock = False

This pattern is particularly useful in graphical user interfaces (GUIs) where user interactions, such as button clicks or menu selections, need to trigger specific actions or updates in other parts of the application. It allows for clear separation of concerns and promotes a event-driven architecture.

A simple example of how to use the Signal & Slot pattern, where when button.click() is called, it emits the clicked signal, which triggers the connected save method of the document, resulting in the output "Document saved". This example demonstrates how this approach allows the button and the document to communicate without direct knowledge of each other. The button emits a signal when it is clicked, and the document responds to that signal by saving itself. This loose coupling makes the code more modular and easier to maintain.

class Button:
    clicked = Signal()

    def click(self):
        self.clicked.emit()

class Document:
    def save(self):
        print("Document saved")


button = Button()
document = Document()

button.clicked.connect(document.save)
button.click() # Output: Document saved.