将字典保存到文件(numpy 和 Python 2/3 友好)

Posted

技术标签:

【中文标题】将字典保存到文件(numpy 和 Python 2/3 友好)【英文标题】:Saving dictionaries to file (numpy and Python 2/3 friendly) 【发布时间】:2013-08-06 22:08:10 【问题描述】:

我想在 Python 中进行分层键值存储,这基本上归结为将字典存储到文件中。我的意思是任何类型的字典结构,它可能包含其他字典、numpy 数组、可序列化的 Python 对象等等。不仅如此,我还希望它能够存储空间优化的 numpy 数组,并在 Python 2 和 3 之间运行良好。

以下是我知道的方法。我的问题是此列表中缺少什么,是否有替代方案可以避开我所有的交易破坏者?

Python 的 pickle 模块(deal-breaker:大大增加了 numpy 数组的大小) Numpy 的 save/savez/load(交易破坏者:跨 Python 2/3 的格式不兼容) PyTables replacement for numpy.savez(交易破坏者:仅处理 numpy 数组) 手动使用 PyTables(交易破坏者:我希望它用于不断更改的研究代码,因此能够通过调用单个函数将字典转储到文件中非常方便)

numpy.savez 的 PyTables 替换是有希望的,因为我喜欢使用 hdf5 的想法,它可以非常有效地压缩 numpy 数组,这是一个很大的优势。但是,它不采用任何类型的字典结构。

最近,我一直在做的是使用类似于 PyTables 替换的东西,但增强了它以能够存储任何类型的条目。这实际上工作得很好,但我发现自己将原始数据类型存储在长度为 1 的 CArray 中,这有点尴尬(并且与实际长度为 1 的数组模棱两可),即使我将 chunksize 设置为 1 所以它不会占用这么多空间。

已经有类似的东西了吗?

谢谢!

【问题讨论】:

您是否考虑过使用像 MongoDB 这样的 NoSQL 数据库系统? @Xaranke 这是个好主意,但我怀疑它是否会提供高效的 numpy 数组存储......或者它可能会? 您可以将 numpy 数组保存为二进制对象,如下所示:***.com/questions/6367589/… @Xaranke 我看到了,但它依赖于 Python 酸洗,因此与酸洗相比,它不会提供任何空间改进。当然,我总是可以尝试以其他方式对它们进行二值化,但这基本上让我回到了原点。 我找到了这个链接**pypi.python.org/pypi/msgpack-python。似乎是 Redis 和 Pinterest 使用的一个非常高效的库。你可能想看看 【参考方案1】:

两年前问过这个问题后,我开始编写自己的基于 HDF5 的 pickle/np.save 替换代码。从那以后,它已经成熟为一个稳定的包,所以我想我最终会回答并接受我自己的问题,因为它的设计正是我所寻找的:

https://github.com/uchicago-cs/deepdish

【讨论】:

import deepdish as dd; dd.io.save(filename, 'dict1': dict1, 'dict2': dict2, compression=('blosc', 9)) 是我发现的好方法。 您可以将其添加到您的配置文件中,如果您始终喜欢这样的话:deepdish.readthedocs.io/en/latest/api_io.html 对,那看起来像 [io]newline and 4 个空格compression: (blosc, 9) 吗? 文档中没有说明如何在 conf 文件中指定压缩级别【参考方案2】:

我最近发现自己遇到了类似的问题,为此我编写了几个函数,用于将 dicts 的内容保存到 PyTables 文件中的组中,然后将它们加载回 dicts。

它们递归地处理嵌套的字典和组结构,并处理具有 PyTables 本身不支持的类型的对象,方法是腌制它们并将它们存储为字符串数组。它并不完美,但至少像 numpy 数组这样的东西会被有效地存储。还包括一项检查,以避免在将组内容读回字典时无意中将大量结构加载到内存中。

import tables
import cPickle

def dict2group(f, parent, groupname, dictin, force=False, recursive=True):
    """
    Take a dict, shove it into a PyTables HDF5 file as a group. Each item in
    the dict must have a type and shape compatible with PyTables Array.

    If 'force == True', any existing child group of the parent node with the
    same name as the new group will be overwritten.

    If 'recursive == True' (default), new groups will be created recursively
    for any items in the dict that are also dicts.
    """
    try:
        g = f.create_group(parent, groupname)
    except tables.NodeError as ne:
        if force:
            pathstr = parent._v_pathname + '/' + groupname
            f.removeNode(pathstr, recursive=True)
            g = f.create_group(parent, groupname)
        else:
            raise ne
    for key, item in dictin.iteritems():
        if isinstance(item, dict):
            if recursive:
                dict2group(f, g, key, item, recursive=True)
        else:
            if item is None:
                item = '_None'
            f.create_array(g, key, item)
    return g


def group2dict(f, g, recursive=True, warn=True, warn_if_bigger_than_nbytes=100E6):
    """
    Traverse a group, pull the contents of its children and return them as
    a Python dictionary, with the node names as the dictionary keys.

    If 'recursive == True' (default), we will recursively traverse child
    groups and put their children into sub-dictionaries, otherwise sub-
    groups will be skipped.

    Since this might potentially result in huge arrays being loaded into
    system memory, the 'warn' option will prompt the user to confirm before
    loading any individual array that is bigger than some threshold (default
    is 100MB)
    """

    def memtest(child, threshold=warn_if_bigger_than_nbytes):
        mem = child.size_in_memory
        if mem > threshold:
            print '[!] "%s" is %iMB in size [!]' % (child._v_pathname, mem / 1E6)
            confirm = raw_input('Load it anyway? [y/N] >>')
            if confirm.lower() == 'y':
                return True
            else:
                print "Skipping item \"%s\"..." % g._v_pathname
        else:
            return True
    outdict = 
    for child in g:
        try:
            if isinstance(child, tables.group.Group):
                if recursive:
                    item = group2dict(f, child)
                else:
                    continue
            else:
                if memtest(child):
                    item = child.read()
                    if isinstance(item, str):
                        if item == '_None':
                            item = None
                else:
                    continue
            outdict.update(child._v_name: item)
        except tables.NoSuchNodeError:
            warnings.warn('No such node: "%s", skipping...' % repr(child))
            pass
    return outdict

还值得一提的是joblib.dumpjoblib.load,除了 Python 2/3 交叉兼容性之外,它们都勾选了所有选项。在后台,他们使用 np.save 来处理 numpy 数组,cPickle 来处理其他所有内容。

【讨论】:

谢谢,这与我开始做的非常相似。除了我的全部内容是我使用Atom.from_dtype(np.dtype(type(obj)) 创建的大小为 1 的 CArray。由于 PyTables 本身支持 numpy object 类型,因此类将获得该类型并正常工作。当然,PyTables 会在底层处理它,但它对我来说隐藏了这一点,所以我使用的代码非常短。虽然它与合法的 (,1) 大小的数组模棱两可,但这是一个小问题,如果它成为问题,我可以解决。 不错。这可能是我第一次看到np.object 类型的实际用途。【参考方案3】:

这不是一个直接的答案。无论如何,您可能也对 JSON 感兴趣。看看13.10. Serializing Datatypes Unsupported by JSON。它展示了如何扩展不受支持的类型的格式。

Mark Pilgrim 的“Dive into Python 3”的整章绝对是一本好书,至少知道...

更新: 可能是一个不相关的想法,但是......我在某处读到,XML 最终被用于异构环境中的数据交换的原因之一是一些比较专门的二进制格式的研究使用压缩的 XML。您的结论可能是使用可能不太节省空间的解决方案并通过 zip 或其他众所周知的算法对其进行压缩。当您需要调试(解压缩然后通过肉眼查看文本文件)时,使用已知算法会有所帮助。

【讨论】:

如果主要关注的是用简单数据保存字典结构,我认为这是一个很好的解决方案(与 ZODB 等类似)。但是,我的重点是当叶子通常是非常大的 numpy 数组时,因此它们将完全破坏具有大块字节码的文件的人类可读性。如果没有人类可读的方面,恐怕使用 JSON/XML 的解决方案在优化存储空间方面无法满足要求,尽管增加了压缩(至少与 PyTables 相比)。【参考方案4】:

我绝对推荐像ZODB 这样的python 对象数据库。考虑到您将对象(实际上是您喜欢的任何东西)存储到字典中,它似乎非常适合您的情况 - 这意味着您可以将字典存储在字典中。我已经在各种问题中使用了它,而且好处是您可以将数据库文件(扩展名为 .fs 的文件)交给某人。有了这个,他们将能够读入它,执行他们希望的任何查询,并修改他们自己的本地副本。如果您希望多个程序同时访问同一个数据库,我会确保查看ZEO。

只是一个如何开始的愚蠢示例:

from ZODB import DB
from ZODB.FileStorage import FileStorage
from ZODB.PersistentMapping import PersistentMapping
import transaction
from persistent import Persistent
from persistent.dict import PersistentDict
from persistent.list import PersistentList

# Defining database type and creating connection.
storage = FileStorage('/path/to/database/zodbname.fs') 
db = DB(storage)
connection = db.open()
root = connection.root()

# Define and populate the structure.
root['Vehicle'] = PersistentDict() # Upper-most dictionary
root['Vehicle']['Tesla Model S'] = PersistentDict() # Object 1 - also a dictionary
root['Vehicle']['Tesla Model S']['range'] = "208 miles"
root['Vehicle']['Tesla Model S']['acceleration'] = 5.9
root['Vehicle']['Tesla Model S']['base_price'] = "$71,070"
root['Vehicle']['Tesla Model S']['battery_options'] = ["60kWh","85kWh","85kWh Performance"]
# more attributes here

root['Vehicle']['Mercedes-Benz SLS AMG E-Cell'] = PersistentDict() # Object 2 - also a dictionary
# more attributes here

# add as many objects with as many characteristics as you like.

# commiting changes; up until this point things can be rolled back
transaction.get().commit()
transaction.get().abort()
connection.close()
db.close()
storage.close()

一旦创建了数据库,它就非常易于使用。由于它是一个对象数据库(字典),因此您可以非常轻松地访问对象:

#after it's opened (lines from the very beginning, up to and including root = connection.root() )
>> root['Vehicles']['Tesla Model S']['range'] 
'208 miles'

您还可以显示所有键(并执行您可能想做的所有其他标准字典操作):

>> root['Vehicles']['Tesla Model S'].keys()
['acceleration', 'range', 'battery_options', 'base_price']

最后我想说的是,可以更改密钥:Changing the key value in python dictionary。值也可以更改 - 因此,如果您的研究结果因更改方法或某些原因而更改,则不必从头开始启动整个数据库(尤其是在其他一切都还可以的情况下)。做这两个都要小心。我在我的数据库代码中加入了安全措施,以确保我知道我试图覆盖键或值。

** 已添加 **

# added imports
import numpy as np
from tempfile import TemporaryFile
outfile = TemporaryFile()

# insert into definition/population section
np.save(outfile,np.linspace(-1,1,10000))
root['Vehicle']['Tesla Model S']['arraydata'] = outfile

# check to see if it worked
>>> root['Vehicle']['Tesla Model S']['arraydata']
<open file '<fdopen>', mode 'w+b' at 0x2693db0>

outfile.seek(0)# simulate closing and re-opening
A = np.load(root['Vehicle']['Tesla Model S']['arraydata'])

>>> print A
array([-1.        , -0.99979998, -0.99959996, ...,  0.99959996,
    0.99979998,  1.        ])

您还可以使用 numpy.savez() 以完全相同的方式压缩保存多个 numpy 数组。

【讨论】:

这似乎很好地存储了字典。但是,我对存储 numpy 数组的看法并不多。由于我的许多字典树叶都是 numpy 数组,因此以最佳方式存储它们很重要。 Pickling 会膨胀大小,numpy.save 会按原样存储二进制数据,而 PyTables 可以压缩低熵数据。如果 ZODB 没有明确的支持,它充其量只能退回到对数据进行腌制,这恐怕是不合格的。 两件事:1)为什么你认为它无法存储numpy数组?作为我的键的参数,我可以很容易地存储一个 numpy 数组。 2)我刚刚测试了 numpy.save() ,它似乎在我的 python-3.3.1 版本中工作得很好。你是说语法不一样吗?如果是这样,有工具 (docs.python.org/2/library/2to3.html) 可以在两者之间进行转换。如果您仍然有问题,为什么不检查它默认的python版本(导入平台\n平台.python_version()),并在条件语句中调整语法? 您甚至可以将文件存储为键的参数,从而允许您使用 numpy.save() 压缩您的 numpy 数组。 1) 我在他们的文档中找不到关于它的提及,这可能意味着它最多只会腌制数组,这是一个交易破坏者 2) 我使用 numpy.load/save a很多,你甚至可以直接保存字典,完全符合我想要的规范。问题是保存在 Python 2 中的文件不能在 Python 3 中打开,反之亦然。这不是语法问题,而是因为 numpy.load/save 依赖于标准库代码来编写字节数组,这在 python 之间是不同的。 嗯,我想我开始看到你的困境了。即使您使用此对象数据库存储文件,您仍然无法在 2.x 和 3.x 版本之间打开。就像您在第一篇文章中提到的那样,如果有一个功能可以很好地与 2 和 3 一起使用。如果存在这样的工具,您可以使用 ZODB 以您想要的方式组织您的词典(或将您的词典直接保存到文件中) .我会留意这样的事情。【参考方案5】:

我尝试使用np.memmap 来保存字典数组。假设我们有字典:

a = np.array([str('a':1, 'b':2, 'c':[1,2,3,'d':4]])

首先我尝试将其直接保存到memmap

f = np.memmap('stack.array', dtype=dict, mode='w+', shape=(100,))
f[0] = d
# CRASHES when reopening since it looses the memory pointer

f = np.memmap('stack.array', dtype=object, mode='w+', shape=(100,))
f[0] = d
# CRASHES when reopening for the same reason

它的工作方式是将字典转换为字符串:

f = np.memmap('stack.array', dtype='|S1000', mode='w+', shape=(100,))
f[0] = str(a)

这可行,之后您可以eval(f[0]) 取回价值。

我不知道这种方法相对于其他方法的优势,但值得仔细研究。

【讨论】:

谢谢,我很感激,但我不得不坦率地说,这在很多方面都是一个糟糕的想法:使用 str/eval 作为序列化的手段根本不可靠,它在您的程序中引入了一个主要的安全漏洞。默认情况下,大型 numpy 数组将使用... 打印,它无法被评估,类实例通常不会被重构,等等。此外,即使它们是,将所有内容存储为字符串表示形式比任何内容都占用更多空间到目前为止讨论的其他方法。

以上是关于将字典保存到文件(numpy 和 Python 2/3 友好)的主要内容,如果未能解决你的问题,请参考以下文章

如何将字典和数组保存在同一个存档中(使用 numpy.savez)

如何在 Python 中将对象数组保存到文件中

如何将不同大小的numpy数组列表保存到磁盘?

Python | Python保存高维数组array,Python用pandas将numpy保存csv文件,Python保存3维数组

从python字典如何将键和值保存到* .txt文件[关闭]

python 如何将字典对象保存到json文件的示例