在Python 3.6+和PyQt5中如何将QPolygon对象进行序列化?

5

我尝试使用pickle保存QPolygon并在之后重新加载,但是出现了错误。我曾经用Python2和PyQt4做过这个操作,但现在我想在Python3和PyQt5上实现它。

我不想读取/加载由Python 2生成的数据!pickle文件仅用于在Python3之间临时存储Qt元素,如QPolygons。

我已经尝试过pickle.dump()中1-4不同的协议选项,并尝试使用"fix_imports=True"选项,但在Python3中不应产生任何区别。

以下是我的简化代码:

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)

我收到以下错误信息,但无法对其进行任何操作:
elem = pickle.load(f, encoding='bytes')  # , fix_imports=True)
TypeError: index 0 has type 'int' but 'QPoint' is expected

有没有pickle的替代方案呢?提前谢谢!

4个回答

3

您可以使用QDataStream来序列化/反序列化Qt对象:

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()))

看起来在使用pickle和QPolygon时存在错误。我喜欢你的答案,因为它似乎是用QDataStream代替pickle的快速简单方法。但我可能过于简化了我的例子。我还需要在输出文件/流中存储例如字符串。使用QDataStream也可以做到吗?我的第一个测试未能将字符串和几个QPolygon存储在一个输出文件中。 - HoWil
嗨,是的,你可以存储任何你想要的东西,但你需要使用Qt对象,我会更新我的示例来展示给你。 - Isma
在读取QDataStream时,有没有可能检查下一个元素的类型?我必须检查下一个是字符串还是QPolygon。为了确定类型,我必须将数据读入例如QVariant中,但这会破坏QPolygons。 - HoWil
我认为默认情况下不可能实现,但你应该知道添加项目的顺序,对吧?也许你可以创建一个继承自QObject的类,其中包含类型列表,然后将第一个传递给该类并使用它来确定反序列化时需要哪些类型的项目。 - Isma

2
在文档中进行一些搜索,您可以通过__reduce__方法实现自定义类的自定义pickle。基本上,您需要返回一个包含新对象构造函数(在取消pickle时创建)以及将传递给构造函数的参数元组的元组。随意地,您可以传递一个state对象(请参见__getstate____setstate__)和一些迭代器以添加位置数据和键值数据。

通过子类化QPolygon,我们可以添加picklability,如下所示:(这可能需要清理/重构,但这是我第一次能够工作的实现)

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)

1
这一定是pyqt5中的一个bug,因为文档说明支持pickling QPolygon。请使用您的例子在pyqt邮件列表上发布bug报告。
与此同时,迄今为止最简单的替代方案是使用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))

输出:

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

这可以与PyQt支持的任何类型一起使用进行pickling,以及PyQt可以转换为/从QVariant转换的任何其他内容。 PyQt还支持type参数来QSettings.value(),可用于明确声明所需的类型。由于QSettings设计用于存储应用程序配置数据,因此可以在同一文件中存储任意数量的对象(有点像Python的shelve模块)。

我也在文档中找到了相关信息,但不确定是否有做错什么。我已经写信给PyQt邮件列表,并会随时更新您。 - HoWil
原因是我必须为我放入QSettings的每个条目命名。在我用来展示问题的简化示例中,只有一个QPolygon。实际上有数百个多边形和字符串。这使得处理所有变量名称有点困难。最终,我接受了Isma使用QDataStream的解决方案,它最相当于pickle,只需将其一个接一个地写入并以相同的方式读取即可。尽管如此,感谢您的帮助和讨论。 - HoWil
这种方法的缺点是数据始终作为配置/设置文件存储在Qt配置文件夹中。在Linux上,这是您主目录下的.config文件夹,但不在我的Python文件旁边或我指定的路径中。 - HoWil
@HoWil 这不是真的 - 你可以指定特定路径。我会更新我的答案来使用这种方法(这实际上是我最初想要的)。这将使用标准的ini文件格式。 - ekhumoro
谢谢。不足之处在于,与pickle(二进制)导出相比,导出的文件要大几倍。最后,即使它不能完全回答我的原始问题,我仍然在我的当前项目中使用这个解决方案。 - HoWil
显示剩余2条评论

0
受之前答案的启发,我创建了一个函数,它接受支持QDataSteam的Qt类型,并返回一个从该类继承且可被pickle的类,在以下示例中,我将使用QPoygon和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)

使用OP代码:

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)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接