When I am using PySide6, I pass a dictionary through a signal, but the dictionary data I receive in the slot function changes. The content itself does not change, but the order of the keys in the dictionary does. Aren’t dictionaries in newer versions of Python supposed to be ordered?
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton
class Btn(QWidget):
pressed = Signal(dict)
def __init__(self):
super().__init__()
self.setWindowTitle("Test Button")
self.setGeometry(100, 100, 200, 100)
layout = QVBoxLayout()
self.setLayout(layout)
btn = QPushButton("Press Me")
layout.addWidget(btn)
btn.clicked.connect(self.on_press)
def on_press(self):
d = {"R": [1, 2, 3], "G": [4, 5, 6], "B": [7, 8, 9]}
self.pressed.emit(d)
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Test Plugin Window")
self.setGeometry(100, 100, 400, 300)
layout = QVBoxLayout()
self.setLayout(layout)
btn = Btn()
layout.addWidget(btn)
btn.pressed.connect(self.on_click)
def on_click(self, d):
print(d)
if __name__ == '__main__':
from PySide6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
The result I expected was {"R": [1, 2, 3], "G": [4, 5, 6], "B": [7, 8, 9]}, but the final result was {'B': [7, 8, 9], 'G': [4, 5, 6], 'R': [1, 2, 3]}.
However, when I changed the slot (signal) to pressed = Signal(object), the behavior became exactly as expected. Why is this?
Replace the dict signal signature with object:
class Btn(QWidget):
pressed = Signal(object)
...
First of all, remember that PySide (like its older sibling PyQt) is a binding around the Qt library, which is written in C++.
This means that the underlying library doesn't know anything about being used by the "outside" language, and objects created in Python always need some form of conversion in order to be used correctly by the other library, even if we are under the impression that everything is directly managed in Python.
While most standard types used in Python have a corresponding similar counterpart in C++ (at least for standard usage), there is no exact equivalent for generic Python data containers as they are intended for common and "mixed" usage.
Most importantly, the flexibility of Python typing allows having any type in a container (lists, dicts, etc.), something that a static typing language such as C++ doesn't implicitly provide.
Even creating a simple list like ['A', 1] isn't immediate in C++ as it is in Python, because it's a dynamic array made of different types, requiring custom data structures in order to allow proper memory allocation and access.
Since Qt is intended as a flexible environment, it provides advanced types that partially allow more dynamic data usage. For instance, QVariant is a widely used object in Qt, also providing common automatic conversions, and those conversions are implicitly used in the Python bindings as well.
When a QVariant is provided by Qt and accessed in Python (for instance, from the data() function of an item model), the binding tries to convert it to the most reasonable Python type. The same happens when a Python object is "given" to a Qt function.
Note that objects emitted in signals always need to "pass through Qt", therefore a data conversion may normally happen in those cases: the object emitted by a signal could be equivalent to the one received by its connected slots, but there's no guarantee that it will be the same object; this is a case for which understanding the difference between equality and identity must be clear: even in Python [] == [] is different than [] is [].
Yet, there are even more complex types that are implicitly allowed in Python, but cannot be easily converted in C++.
For instance, this is a completely legal data structure in Python:
foo = ('whatever', {
'a': True,
25: ["cow", 0],
(lambda: None): "Hello"
})
In C++ the above is not immediately possible, and requires custom data structures in order to allow a "mapping" (a "dictionary") with keys of different types, and further array-like objects that contain different types.
Qt provides a "dictionary" object like QMap, but it still has some limitations.
Qt bindings like PySide and PyQt normally apply automatic conversions that usually work fine for most purposes and eventually decide to "send" the original object instead.
Unfortunately, PySide seems to force a QMap conversion for a dict, no matter what.
The result is that the dictionary you're emitting is first converted into a QMap, then converted back into a dict when "feeding" slots connected to that signal.
Since QMap is unsorted, the obvious result is what you're observing: the order is not maintained, because you're not getting the same object.
And that's not all!
PySide also has a bug causes a failed conversion in some cases: emitting a dictionary with different types as keys (eg: {0: 0, '0': 0}) will result in calling connected slots with an empty dictionary.
The common way to ensure that the emitted object is preserved as it is, is to change the signal signature to the generic object.
This ensures that the emitted object will always be the same that will be received by its connected slots.
That work-around is commonly used for some situations that may cause inconsistencies; for example, when dealing with numbers requiring more than 32 bits.
Besides the obvious benefits, using a generic object signature obviously creates some issues; most importantly:
[pyqt]Slot decorator, as there's no way to define the actual type for the target slot; also, PySide has always been quite inconsistent in the @Slot implementation;If you're not aiming for extreme/fringe cases, using the object signature will probably be the most appropriate and simple solution; as written at the beginning of this answer, replace the dict signal signature with object:
class Btn(QWidget):
pressed = Signal(object)
...
Another option is to use dummy subclasses, which should enforce PySide to respect the object type without conversions:
class MyDict(dict): pass
class Btn(QWidget):
pressed = Signal(MyDict)
...
Yet, it is of utmost importance to consider the warnings above, especially when dealing with threading, but you need to apply special care for complex types, as a simple subclassing like the above may not always work as expected, unless you properly implement all methods required by their subclasses.
In fact, using Python's OrderedDict from the collections module may be safer, as it's considered robust enough and also relies on a lower and more efficient implementation.