简体   繁体   中英

Pickle a QPolygon in python 3.6+ and PyQt5

I try to pickle a QPolygon and load it afterwards, but I get an error. I have done this on Python2 with PyQt4 but want to use it now on Python3 with PyQt5.

I do not want to read/load data generated with Python 2! The pickle file is simply used to temporarily store Qt-elements like QPolygons from Python3 to Python3.

I have tested different protocol options from 1-4 for pickle.dump() and tried to use the "fix_imports=True" option which should not make a difference in Python3.

Here is my simplified code

from PyQt5.QtGui import QPolygon
from PyQt5.QtCore import QPoint
import pickle

file_name = "test_pickle.chip"

with open(file_name, 'wb') as f:
    poly = QPolygon((QPoint(1, 1), QPoint(2, 2))) 
    pickle.dump(poly, f, protocol=2)  # , fix_imports=True)

# loading the data again
with open(file_name, 'rb') as f:
    elem = pickle.load(f, encoding='bytes')  # , fix_imports=True)

I get the following error message but can't do anything with it:

 elem = pickle.load(f, encoding='bytes') # , fix_imports=True) TypeError: index 0 has type 'int' but 'QPoint' is expected 

Is there maybe any alternative to pickle? Thanks in advance!

You can use QDataStream to serialize / deserialize Qt objects:

from PyQt5 import QtCore
from PyQt5.QtGui import QPolygon
from PyQt5.QtCore import QPoint, QFile, QIODevice, QDataStream, QVariant

file_name = "test.dat"

output_file = QFile(file_name)
output_file.open(QIODevice.WriteOnly)
stream_out = QDataStream(output_file)
output_poly = QPolygon((QPoint(1, 6), QPoint(2, 6)))
output_str = QVariant('foo')  # Use QVariant for QString
stream_out << output_poly << output_str
output_file.close()

input_file = QFile(file_name)
input_file.open(QIODevice.ReadOnly)
stream_in = QDataStream(input_file)
input_poly = QPolygon()
input_str = QVariant()
stream_in >> input_poly >> input_str
input_file.close()

print(str(output_str.value()))
print(str(input_str.value()))

A bit of searching on the docs tells you how you can implement custom pickling for custom classes via the __reduce__ method . Basically you return a tuple including the constructor for the new object to be created when un-pickling as well as a tuple of arguments that will be passed to the constructor. Optionally you then can then pass a state object (see __getstate__ and __setstate__ ) and some iterators to add positional data and key-value data.

by subclassing QPolygon we can add picklability as such: (this could probably be cleaned up / restructured, but this is the first implementation I got to that worked)

from PyQt5.QtGui import QPolygon
from PyQt5.QtCore import QPoint
import pickle

class Picklable_QPolygon(QPolygon):

    def __reduce__(self):
        # constructor, (initargs), state object passed to __setstate__
        return type(self), (), self.__getstate__()

    #I'm not sure where this gets called other than manually in __reduce__
    #  but it seemed like the semantically correct way to do it...
    def __getstate__(self):
        state = [0]
        for point in self:
            state.append(point.x())
            state.append(point.y())
        return state

    #putPoints seems to omit the `nPoints` arg compared to c++ version.
    #  this may be a version dependent thing for PyQt. Definitely test that
    #  what you're getting out is the same as what you put in..
    def __setstate__(self, state):
        self.putPoints(*state)

poly = Picklable_QPolygon((QPoint(1, 1), QPoint(2, 2)))
s = pickle.dumps(poly)

elem = pickle.loads(s)

This must be a bug in pyqt5, since the documentation states that pickling QPolygon is supported. Please therefore post a bug report to the pyqt mailing list using your example.

In the meantime, by far the simplest alternative is to use QSettings :

from PyQt5.QtGui import QPolygon
from PyQt5.QtCore import QSettings, QPoint

file_name = "test_pickle.chip"

poly = QPolygon((QPoint(1, 1), QPoint(2, 2)))

# write object
s = QSettings(file_name, QSettings.IniFormat)
s.setValue('poly', poly)

del s, poly

# read object
s = QSettings(file_name, QSettings.IniFormat)
poly = s.value('poly')

print(poly)
print(poly.point(0), poly.point(1))

Output:

<PyQt5.QtGui.QPolygon object at 0x7f871f1f8828>
PyQt5.QtCore.QPoint(1, 1) PyQt5.QtCore.QPoint(2, 2)

This can be used with any type PyQt supports for pickling, plus anything else that PyQt can convert to/from a QVariant . PyQt also supports a type argument to QSettings.value() that can be used to explicitly state the required type. And since QSettings is designed for storing application config data, any number of objects can be stored in the same file (somewhat like python's shelve module ).

Inspired by the previous answers I created a function that takes some kind of Qt that supports QDataSteam and returns a class that inherits from that class and is picklable, in the following example I show with QPoygon and QPainterPath:

import pickle
from PyQt5 import QtCore, QtGui

def picklable_reduce(self):
    return type(self), (), self.__getstate__()

def picklable_getstate(self):
    ba = QtCore.QByteArray()
    stream = QtCore.QDataStream(ba, QtCore.QIODevice.WriteOnly)
    stream << self
    return ba

def picklable_setstate(self, ba):
    stream = QtCore.QDataStream(ba, QtCore.QIODevice.ReadOnly)
    stream >> self

def create_qt_picklable(t):
    return type("Picklable_{}".format(t.__name__), (t,),
        {
            '__reduce__': picklable_reduce,
            '__getstate__': picklable_getstate,
            '__setstate__': picklable_setstate
        }
    ) 

if __name__ == '__main__':
    # QPolygon picklable
    Picklable_QPolygon = create_qt_picklable(QtGui.QPolygon)
    old_poly = Picklable_QPolygon((QtCore.QPoint(1, 1), QtCore.QPoint(2, 2)))
    s = pickle.dumps(old_poly)
    new_poly = pickle.loads(s)
    assert(old_poly == new_poly)
    # QPainterPath picklable
    Picklable_QPainterPath = create_qt_picklable(QtGui.QPainterPath)
    old_painterpath = Picklable_QPainterPath()
    old_painterpath.addRect(20, 20, 60, 60)
    old_painterpath.moveTo(0, 0)
    old_painterpath.cubicTo(99, 0,  50, 50,  99, 99)
    old_painterpath.cubicTo(0, 99,  50, 50,  0, 0);
    s = pickle.dumps(old_painterpath)
    new_painterpath= pickle.loads(s)
    assert(old_painterpath == new_painterpath)

Using the OP code:

if __name__ == '__main__':
    Picklable_QPolygon = create_qt_picklable(QtGui.QPolygon)

    file_name = "test_pickle.chip"
    poly = Picklable_QPolygon((QtCore.QPoint(1, 1), QtCore.QPoint(2, 2))) 
    with open(file_name, 'wb') as f:
        pickle.dump(poly, f, protocol=2)  # , fix_imports=True)

    elem = None
    with open(file_name, 'rb') as f:
        elem = pickle.load(f, encoding='bytes')  # , fix_imports=True)

    assert(poly == elem)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM