写pandas到HDF5时是否可以指定pickle协议?

Is it possible to specify the pickle protocol when writing pandas to HDF5?

有没有办法告诉 Pandas 使用特定的 pickle 协议(例如 4)在写入 HDF5 文件时

情况如下(已简化):

请注意,纯数字 DataFrame 不会发生这种情况(例如 pd.DataFrame(np.random.normal(size=(10,10)))。所以这是一个可重现的示例:

(base) $ conda activate py38
(py38) $ python
Python 3.8.1 (default, Jan  8 2020, 22:29:32)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas as pd
>>> df = pd.DataFrame(['hello', 'world']))
>>> df.to_hdf('foo', 'x')
>>> exit()
(py38) $ conda deactivate
(base) $ python
Python 3.7.4 (default, Aug 13 2019, 20:35:49)
[GCC 7.3.0] :: Anaconda, Inc. on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas as pd
>>> df = pd.read_hdf('foo', 'x')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/anaconda3/lib/python3.7/site-packages/pandas/io/pytables.py", line 407, in read_hdf
    return store.select(key, auto_close=auto_close, **kwargs)
  File "/opt/anaconda3/lib/python3.7/site-packages/pandas/io/pytables.py", line 782, in select
    return it.get_result()
  File "/opt/anaconda3/lib/python3.7/site-packages/pandas/io/pytables.py", line 1639, in get_result
    results = self.func(self.start, self.stop, where)
  File "/opt/anaconda3/lib/python3.7/site-packages/pandas/io/pytables.py", line 766, in func
    return s.read(start=_start, stop=_stop, where=_where, columns=columns)
  File "/opt/anaconda3/lib/python3.7/site-packages/pandas/io/pytables.py", line 3206, in read
    "block{idx}_values".format(idx=i), start=_start, stop=_stop
  File "/opt/anaconda3/lib/python3.7/site-packages/pandas/io/pytables.py", line 2737, in read_array
    ret = node[0][start:stop]
  File "/opt/anaconda3/lib/python3.7/site-packages/tables/vlarray.py", line 681, in __getitem__
    return self.read(start, stop, step)[0]
  File "/opt/anaconda3/lib/python3.7/site-packages/tables/vlarray.py", line 825, in read
    outlistarr = [atom.fromarray(arr) for arr in listarr]
  File "/opt/anaconda3/lib/python3.7/site-packages/tables/vlarray.py", line 825, in <listcomp>
    outlistarr = [atom.fromarray(arr) for arr in listarr]
  File "/opt/anaconda3/lib/python3.7/site-packages/tables/atom.py", line 1227, in fromarray
    return six.moves.cPickle.loads(array.tostring())
ValueError: unsupported pickle protocol: 5
>>>

注意:我也尝试在 python=3.7.4 中使用 pandas=1.0.0(和 pytables=3.6.1)阅读。这也失败了,所以我相信这只是导致问题的 Python 版本(3.8 writer vs 3.7 reader)。这是有道理的,因为 pickle 协议 5 被引入为 PEP-574 for Python 3.8.

我(曾经)面临同样的问题...我 "know" 如何解决它,我想你也是... 解决方案是将整个数据重新处理为 pickle(或 csv),并在 python3.7 中将其重新转换为 hdf5(只知道协议 4)。

流程是这样的: python3.8 -> hdf5 -> python3.8 -> csv/pickle -> python3.7 -> hdf5(兼容两个版本)

我避免了这条路线,因为我导出了数据框的大块数据,创建了大量文件。

您真的只能使用 python3.7 吗?我受到 tensorflow 的限制,它目前最多只支持 3.7(官方),但你可以安装 tensorflow-nightly-build 并且它适用于 python 3.8

检查您是否可以升级到 3.8,这一定会解决您的问题。 :)

更新:我错误地认为这是不可能的。事实上,根据@PiotrJurkiewicz 的出色 "monkey-patch" 建议,这里有一个简单的上下文管理器,可以让我们临时 更改最高 pickle 协议。它:

  1. 隐藏猴子补丁,
  2. 在上下文之外没有副作用;它可以在任何时候使用,无论之前是否导入过 pickle,在 pandas 之前或之后,都没有关系。

这是代码(例如在文件 pickle_prot.py 中):

import importlib
import pickle


class PickleProtocol:
    def __init__(self, level):
        self.previous = pickle.HIGHEST_PROTOCOL
        self.level = level

    def __enter__(self):
        importlib.reload(pickle)
        pickle.HIGHEST_PROTOCOL = self.level

    def __exit__(self, *exc):
        importlib.reload(pickle)
        pickle.HIGHEST_PROTOCOL = self.previous


def pickle_protocol(level):
    return PickleProtocol(level)

writer中的用法示例:

import pandas as pd
from pickle_prot import pickle_protocol


pd.DataFrame(['hello', 'world']).to_hdf('foo_0.h5', 'x')

with pickle_protocol(4):
    pd.DataFrame(['hello', 'world']).to_hdf('foo_1.h5', 'x')

pd.DataFrame(['hello', 'world']).to_hdf('foo_2.h5', 'x')

并且,使用一个简单的测试reader:

import pandas as pd
from glob import glob

for filename in sorted(glob('foo_*.h5')):
    try:
        df = pd.read_hdf(filename, 'x')
        print(f'could read {filename}')
    except Exception as e:
        print(f'failed on {filename}: {e}')

现在,在 py38 中写入后尝试在 py37 中读取,我们得到:

failed on foo_0.h5: unsupported pickle protocol: 5
could read foo_1.h5
failed on foo_2.h5: unsupported pickle protocol: 5

但是,使用同一个版本(37或38)来读写,当然也不例外。

注意:issue 33087 仍在 Pandas 问题跟踪器上。

PyTable默认使用最高协议,这里硬编码:https://github.com/PyTables/PyTables/blob/50dc721ab50b56e494a5657e9c8da71776e9f358/tables/atom.py#L1216

作为解决方法,您可以在编写 HDF 文件的客户端 A 上对 pickle 模块进行猴子修补。您应该在导入 pandas:

之前执行此操作
import pickle
pickle.HIGHEST_PROTOCOL = 4
import pandas

df.to_hdf(file, key)

现在已经使用 pickle 协议版本 4 而不是版本 5 创建了 HDF 文件。