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:
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:
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()
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"))
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"))
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"
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...