Unable to change __class__ of PySide.QtGui objects

2019-04-11 17:00发布

问题:

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.

回答1:

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''


回答2:

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.