6

After reading the post https://www.toptal.com/python/python-class-attributes-an-overly-thorough-guide I think I grasp the difference between class attributes and instance attributes. But there is still a weird phenomenon that I cannot explain: the behaviour of the pyqtSignal() in PyQt.

 

1. About class attributes

I've learned that class attributes exist in the namespace of the class. If you access such variable - for example class_var - through an instance, Python will first look in the namespace of the instance. Not finding it there, Python looks at the namespace of the class.

1.1 Primitive class attributes

Let's look at a class attribute which is a simple integer. You should access it through the class itself, but you can also do it through one of its instances:

class MyClass(object):
    class_var = 1

    def __init__(self):
        pass

if __name__ == '__main__':
    obj_01 = MyClass()
    obj_02 = MyClass()

    # Access the 'class_var' attribute
    print("obj_01.class_var  -> " + str(obj_01.class_var))   # prints 1
    print("obj_02.class_var  -> " + str(obj_02.class_var))   # prints 1
    print("MyClass.class_var -> " + str(MyClass.class_var))  # prints 1

However, if you assign a new value to class_var through an instance, you actually create a new attribute named class_var in the namespace of that particular instance. The actual class variable remains untouched:

    obj_01.class_var = 3
    print("obj_01.class_var  -> " + str(obj_01.class_var))   # prints 3
    print("obj_02.class_var  -> " + str(obj_02.class_var))   # prints 1
    print("MyClass.class_var -> " + str(MyClass.class_var))  # prints 1

The following figure illustrates what happens:

enter image description here

1.2 Mutable class attributes (non-primitive)

In the previous example, the assignment obj_01.class_var = 3 didn't touch the actual class variable - it just created an new instance variable for the particular object. The story gets more complicated for mutable (non-primitive) class attributes. Consider this:

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

    def get_str_value(self):
        myStr = "(" + str(self.a) + "," + str(self.b) + ")"
        return myStr

class MyClass(object):
    class_var = Foo(1, 1)

    def __init__(self):
        pass

if __name__ == '__main__':
    obj_01 = MyClass()
    obj_02 = MyClass()

    # Access the 'class_var' attribute
    print("obj_01.class_var  -> " + obj_01.class_var.get_str_value())   # prints (1,1)
    print("obj_02.class_var  -> " + obj_02.class_var.get_str_value())   # prints (1,1)
    print("MyClass.class_var -> " + MyClass.class_var.get_str_value())  # prints (1,1)

    # Change it
    obj_01.class_var.a = 3
    print("obj_01.class_var  -> " + obj_01.class_var.get_str_value())   # prints (3,1)
    print("obj_02.class_var  -> " + obj_02.class_var.get_str_value())   # prints (3,1)
    print("MyClass.class_var -> " + MyClass.class_var.get_str_value())  # prints (3,1)

I don't reassign a new Foo()-object to the variable, I merely mutate it. I do the mutation through instance obj_01, but the mutation happens on the actual class variable, wich is common to all instances of that class: enter image description here

 

2. The weird behaviour of pyqtSignal()

Consider the following code sample. Class EmitterTester has one class attribute named signal.

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *

class EmitterTester(QObject):
    signal = pyqtSignal()   # class attribute 'signal'
    def __init__(self):
        super(EmitterTester, self).__init__()
        pass

class MainWindow(QMainWindow):
    def __init__(self):
        '''
        Just create a window with a "START TEST" button.
        '''
        super(MainWindow, self).__init__()
        self.setGeometry(200, 200, 200, 200)
        self.setWindowTitle("Emitter tester")

        self.frame = QFrame()
        self.layout = QHBoxLayout()
        self.frame.setLayout(self.layout)
        self.setCentralWidget(self.frame)

        self.button = QPushButton()
        self.button.setText("START TEST")
        self.button.setFixedHeight(50)
        self.button.setFixedWidth(100)
        self.button.clicked.connect(self.start_test)
        self.layout.addWidget(self.button)

        self.show()

    def start_test(self):
        '''
        This is the actual test code!
        '''
        obj_01 = EmitterTester()
        obj_02 = EmitterTester()
        obj_01.signal.connect(lambda: print("one"))
        obj_02.signal.connect(lambda: print("two"))

        obj_01.signal.emit()            # prints "one"
        obj_02.signal.emit()            # prints "two"
        EmitterTester.signal.emit()     # throws error

if __name__ == '__main__':
    app = QApplication(sys.argv)
    myGUI = MainWindow()
    sys.exit(app.exec_())

So let us look at the problematic code snippet step-by-step.

STEP 1
First we create two instances of the EmitterTest class. The class variable named signal is not yet connected to anything.

obj_01 = EmitterTester()
obj_02 = EmitterTester()

enter image description here

STEP 2
Next, we connect the signal class variable to a lambda function that prints "one". We access the signal class variable through obj_01, but that shouldn't matter, because we're not assigning a new value but merely mutating it:

obj_01.signal.connect(lambda: print("one"))

enter image description here

STEP 3
At last, we connect the signal class variable to a lambda function that prints "two". This time we access the signal class variable through obj_02, but again, that shouldn't matter:

obj_02.signal.connect(lambda: print("one"))

enter image description here

STEP 4
Finally we call the emit() method on signal. First we reach signal through obj_01, next through obj_02. The emit() method behaves differently, depending on how we reach the signal attribute!

obj_01.signal.emit()            # prints "one"
obj_02.signal.emit()            # prints "two"

enter image description here

3. My questions

My first question to you is: why does the emit() method behave differently, depending on how we reach the signal attribute?

One possible explanation could be that obj_01 and obj_02 each made their own copy of the class variable (so they actually created each their own "instance variable"). To verify this theory, I checked the namespaces of the objects and the class:

print(obj_01.__dict__)         # prints "{}"
print(obj_02.__dict__)         # prints "{}"
print(EmitterTester.__dict__)  # prints "{'__module__' : '__main__',
                               #              'signal' : <unbound PYQT_SIGNAL signal()>,
                               #            '__init__' : <function EmitterTester.__init__ at 0x000001DE592098C8>,
                               #             '__doc__' : None}

As you can see, the signal attribute is only in the namespace of the class. The objects don't have their own copy.

My second question to you is: why does EmitterTester.signal.emit() throw an error? I'm merely accessing the class variable as it should happen...

K.Mulier
  • 8,069
  • 15
  • 79
  • 141

0 Answers0