Search code examples
pythonpyqt5signals-slots

Good practices on where to define the connection of custom Signals and Slots


When I emit a signal in script A and catch it script B to execute a slot there. Where should the mysignalA.connect(myslotB) go? In script A as: self.mysignalA.connect(B.myslotB) or in script B as: A.mysignalA.connnect(self.myslotB)

My Code is working properly and realiably, but: I have a big project with multiple Qthreads split up over several files and kind of lose track over all the signals/slots. Also multiple people work on it so that doesn't help either.

In this specific example, I have a thread handling the UDP communication to send status updates to an external system, a thread handling the serial communication to the 3D printer and another thread handling some temperature modelling. Now both the UDPsend and the temperatureDtThread require the temperature reading of the 3D printer at different rates/times, but I also can't flood the serial communication with the same request twice so I decided on triggering a SLOT_request_temperature_reading that does some serial communication and translation whenever someone somewhere needs the temperature and then emitting that temperature reading out again through the SIGNAL_temperature_reading to be caught by whoever needs it at this point.

UDPComms.py

class UDPsend(QThread):
    SIGNAL_temperature_req_UDP = pyqtSignal()
    
    def __init__(self, printer):
        self.SIGNAL_temperature_req_UDP.connect(printer.SLOT_request_temperature_reading)
        printer.SIGNAL_temperature_reading.connect(self.SLOT_get_temperature)

    def run(self):
        while True
            #DoSomethingSomething
            if required
                self.SIGNAL_temperature_req_UDP.emit()

    @pyqtslot(float)
    def SLOT_get_temperature(temperature_reading):
        self.temperature=temperature_reading #So i can use the temperature in this class now

printer.py

class PrinterThread(QThred):
    SIGNAL_temperature_reading = pyqtSignal(float)

    def run(self):

        if self.Temp_reading_requested = True
            #DoSerialCommunicationAndStuff
            self.SIGNAL_temperature_reading.emit(self.mytemperature)

    @pyqtSlot()
    def SLOT_request_temperature_reading():
        self.Temp_reading_requested = True

tempDT.py

class temperatureDtThread(QThread):
    SIGNAL_temperature_req_DT = pyqtSignal()
    
    def __init__(self, printer)
    self.SIGNAL_temperature_req_DT.connect(printer.SLOT_request_temperature_reading)
    printer.SIGNAL_temperature_reading.connect(self.SLOT_temperature_read_out)

    def run(self):
        while True
            #DoSomethingSomething
            if required
                SIGNAL_temperature_req_DT.emit
        
    @pyqtSlot(float)
    def SLOT_temperature_read_out(temperature_reading):
        self.temperature=temperature_reading #So i can use the temperature in this class now

main.py

if __name__ == "__main__":
    printer_thread = printer.Printerthread()
    udp_thread = UDPComms.UDPSend(printer_thread)
    tempdt_thread = tempDT.temperatureDtThread(printer_thread)

    printer_thread.start()
    udp_thread.start()
    tempdt_thread.start()

example

So right now in this case I try to do the connections from outside the printer.py, but without much reasoning behind it. Is there a common good practice to where the connection of signals and slots should be defined?


Solution

  • Theoretically speaking, objects should normally be self contained and be able to work on their own, no matter the context in which they are placed. This is obviously not always possible, and sometimes it's also not advisable.

    In case of Qt, a QObject should normally address only itself or its child objects, despite the fact that that object is actually used from/by something else, or even in case there is nothing else but that object.

    In the hierarchy of objects and where they're defined, it's preferable to create those connections "from the outside", or, so to speak, from an ideal "controller", which in your case is what happens in "main".
    Creating a connection from within an object to an external one is not wrong per se, as, normally, the context in which a connection is made is irrelevant. The exception is when the thread context of the signal and slot may not be automatically resolved, but this isn't an issue as long as you're using proper QObjects and their methods/slots. This obviously can change if using non-QObjects, standard functions or lambdas.

    In your case, the printer_thread you're providing in other objects constructors is actually used only for the signal connection; while it makes sense to do that (and theoretically simplifies things in the "main" code), it's not completely appropriate in a more strict sense. A more correct solution would not use the printer object in those thread constructors, and the connection is instead created from where those objects are created:

    # in main
    printer_thread = printer.Printerthread()
    udp_thread = UDPComms.UDPSend()
    tempdt_thread = tempDT.temperatureDtThread()
    
    printer_thread.Signal_temperature_reading.connect(
        udp_thread.SLOT_get_temperature)
    printer_thread.SIGNAL_temperature_reading.connect(
        tempdt_thread.SLOT_temperature_read_out)
    udp_thread.SIGNAL_temperature_req_UDP.connect(
        printer.SLOT_request_temperature_reading)
    tempdt_thread.SIGNAL_temperature_req_DT.connect(
        printer.SLOT_request_temperature_reading)
    

    One of the benefits of this approach is that all connections are done in the same place, making the code flow more consistent.

    This can also be beneficial in case many similar objects need to connect to similar signals or slots. Supposing that you have many objects that need to connect to the printer, and they all have consistent naming (which is not your case, and I suggest you to do so instead), that would be even easier:

    printer_thread = printer.Printerthread()
    some_threads = [Whatever() for _ in range(10)]
    for thread in some_threads:
        printer_thread.temperatureRead.connect(thread.temperatureReceived)
        thread.requestTemperature.connect(printer_thread.requestTemperature)
    

    Note that there is actually nothing wrong in your approach: it works, and it doesn't create any real problem.

    As usual, good or preferred practices are mostly guidelines, not absolute rules. Therefore, they should be applied only as long as they make sense, including code readability/portability, object structure clarity and correlation between objects that do not have direct relation between their hierarchy.