问题
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!
回答1:
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()))
回答2:
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)
回答3:
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).
回答4:
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)
来源:https://stackoverflow.com/questions/54928431/pickle-a-qpolygon-in-python-3-6-and-pyqt5