I've used PyQt4 quite a lot - sometimes I like to overload some of the objects to allow me to add some functionality. This works fine in PyQt4, eg:
from PyQt4 import QtGui
button = QtGui.QPushButton()
class MyPushButton(QtGui.QPushButton): pass
button.__class__ = MyPushButton
However, I'm trying to adapt some of my code so that it uses PySide instead of PyQt4. My understanding is that they should have the same functionality. PySide will not allow me to do the same thing.
from PySide import QtGui
button = QtGui.QPushButton()
class MyPushButton(QtGui.QPushButton): pass
button.__class__ = MyPushButton
This errors with:
TypeError: __class__ assignment: only for heap types
Is there another way I can change the class of my object to avoid this error? I'm not really sure what's causing it.
NOTE: I need to change the class of the object after it is created as the object is created in a library which is compiled by pyuic. Also my PySide installation does not have the uic module.
PySide uses Shiboken to generate the CPython bindings for Qt from
its C++ classes. All of the Qt python classes such as QPushButton
are
implemented in C++ which is why you cannot overwrite __class__
.
>>> button = QPushButton()
>>> button.__class__ = MyButton
TypeError: __class__ assignment: only for heap types
According to the Shiboken's documentation, you can monkey patch (or duck punch)
the methods on an already instantiated object so long as the methods are
virtual (overridable):
import types
def override_text(self):
return 'overridden'
# Bind override_text() to button.
button.text = types.MethodType(override_text, button, QPushButton)
Taking this further you can sub-class QPushButton
as MyButton
, and
dynamically inject the methods from MyButton
into the QPushButton
instance. Making MyButton
a sub-class of QPushButton
is purely
optional, but would allow you to make your own instances of MyButton
in addition to the modified QPushButton
instances.
Let's define MyButton
as a sub-class of QPushButton
.
class MyButton(QPushButton):
def text(self):
# This will override QPushButton's text() method.
print("inside MyButton.text()")
return QPushButton.text(self)
- NOTE: You have to use the old-style of calling parent class methods.
super()
fails with a TypeError
because self is actually a QPushButton
and not a MyButton
when the method is injected.
Or if you wanted to take more of a mixin approach, let's define MyButtonOverrides
:
class MyButtonOverrides(object):
def text(self):
# This will override QPushButton's text() method.
print("inside MyButtonOverrides.text()")
return self.__class__.text(self)
- NOTE: You can call the
QPushButton.text()
directly through self.__class__
because you won't be using MyButtonOverrides
directly.
Now let's define extend_instance()
which will inject your override
methods from MyButton
(or MyButtonOverrides
) into the QPushButton
instance:
import inspect
def extend_instance(obj, cls):
for name, attr in vars(cls).items():
if inspect.isroutine(attr):
# Bind instance, class and static methods to *obj*.
setattr(obj, name, attr.__get__(obj, obj.__class__))
If you'd like your class methods to remain bound to their originating class
(e.g., MyButton
) then use the following:
def extend_instance(obj, cls):
for name, attr in vars(cls).items():
if inspect.isroutine(attr):
if isinstance(attr, classmethod):
# Bind class methods to *cls*.
setattr(obj, name, attr.__get__(cls, cls))
else:
# Bind instance and static methods to *obj*.
setattr(obj, name, attr.__get__(obj, obj.__class__))
Through my testing this works in Python 2.7 and 3.3, but should
work on 2.6+ and 3+.
Finally to modify the button, use extend_instance()
.
>>> button = QPushButton()
>>> extend_instance(button, MyButton)
>>> button.text()
inside MyButton.text()
u''
Monkey-patching __class__
is completely unnecessary. Instead, you should promote the relevant widgets in Qt Designer so that they automatically use your subclass.
To do this, in Qt Designer, right-click the widget and select "Promote to...". In the dialog, set "Promoted class name" to your subclass (e.g. "MyPushButton"), and set "Header file" to the python import path for the module containing the subclass (e.g. "myapp", or "myapp.gui", or whatever).
Now click "Add", and then "Promote", and you will see the class change from "QPushButton" to "MyPushButton" in the Object Inspector pane.
When you re-generate your ui module with pyuic
or pyside-uic
, it will contain code like this:
class Ui_Window(object):
def setupUi(self, Window):
...
self.button = MyPushButton(Window)
self.button.setObjectName("button")
...
from myapp import MyPushButton
So the promoted button will be created as an instance of MyPushButton
, and there is no longer any need to hack __class__
.
This technique of promoting widgets will work for both PyQt and PySide.