15

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

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

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

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

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

那里已经有类似的东西了吗?

谢谢!

4

5 回答 5

4

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

于 2016-04-08T04:21:15.777 回答
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其他所有内容。

于 2013-08-09T17:19:43.183 回答
0

我尝试使用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])取回价值。

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

于 2013-08-07T12:47:45.473 回答
0

我绝对推荐像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 数组。

于 2013-08-24T03:13:39.060 回答
0

这不是一个直接的答案。无论如何,您可能也对 JSON 感兴趣。看看13.10。序列化 JSON 不支持的数据类型。它展示了如何扩展不受支持的类型的格式。

Mark Pilgrim 所著的“潜入 Python 3”的整章绝对是一本好书,至少要知道……

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

于 2013-08-24T20:44:19.623 回答