【问题标题】:Saving an Object (Data persistence)保存对象(数据持久性)
【发布时间】:2023-03-18 13:57:01
【问题描述】:

我创建了一个这样的对象:

company1.name = 'banana' 
company1.value = 40

我想保存这个对象。我该怎么做?

【问题讨论】:

  • 请参阅example 了解如何使用泡菜的简单示例。
  • @MartinThoma:为什么你(似乎)更喜欢这个答案而不是接受的答案(linked question)?
  • 在我链接的时候,接受的答案没有protocol=pickle.HIGHEST_PROTOCOL。我的回答也提供了泡菜的替代品。

标签: python serialization save persistence pickle


【解决方案1】:

您可以使用标准库中的pickle 模块。 这是它在您的示例中的基本应用:

import pickle

class Company(object):
    def __init__(self, name, value):
        self.name = name
        self.value = value

with open('company_data.pkl', 'wb') as outp:
    company1 = Company('banana', 40)
    pickle.dump(company1, outp, pickle.HIGHEST_PROTOCOL)

    company2 = Company('spam', 42)
    pickle.dump(company2, outp, pickle.HIGHEST_PROTOCOL)

del company1
del company2

with open('company_data.pkl', 'rb') as inp:
    company1 = pickle.load(inp)
    print(company1.name)  # -> banana
    print(company1.value)  # -> 40

    company2 = pickle.load(inp)
    print(company2.name) # -> spam
    print(company2.value)  # -> 42

您还可以定义自己的简单实用程序,如下所示,它打开一个文件并向其中写入一个对象:

def save_object(obj, filename):
    with open(filename, 'wb') as outp:  # Overwrites any existing file.
        pickle.dump(obj, outp, pickle.HIGHEST_PROTOCOL)

# sample usage
save_object(company1, 'company1.pkl')

更新

由于这是一个如此受欢迎的答案,我想谈谈一些稍微高级的使用主题。

cPickle(或_pickle)与pickle

实际上使用cPickle 模块而不是pickle 几乎总是更可取,因为前者是用C 编写的,而且速度更快。它们之间有一些细微的差别,但在大多数情况下它们是等价的,C 版本将提供非常出色的性能。切换到它再简单不过了,只需将 import 语句更改为:

import cPickle as pickle

在 Python 3 中,cPickle 重命名为 _pickle,但不再需要这样做,因为 pickle 模块现在会自动执行此操作 - 请参阅 What difference between pickle and _pickle in python 3?

简而言之,您可以使用以下内容来确保您的代码在 Python 2 和 3 都可用时始终使用 C 版本:

try:
    import cPickle as pickle
except ModuleNotFoundError:
    import pickle

数据流格式(协议)

pickle 可以读取和写入几种不同的、特定于 Python 的格式的文件,称为 protocols,如documentation 中所述,“协议版本 0”是 ASCII,因此“人类-可读”。版本 > 0 是二进制的,可用的最高版本取决于使用的 Python 版本。默认值还取决于 Python 版本。在 Python 2 中,默认是协议版本 0,但在 Python 3.8.1 中,它是协议版本 4。在 Python 3.x 中,该模块添加了 pickle.DEFAULT_PROTOCOL,但在 Python 2 中不存在。

幸运的是,在每个调用中都有写 pickle.HIGHEST_PROTOCOL 的简写(假设这是您想要的,并且您通常会这样做),只需使用文字数字 -1 — 类似于通过负索引引用序列的最后一个元素。 所以,不要写:

pickle.dump(obj, outp, pickle.HIGHEST_PROTOCOL)

你可以写:

pickle.dump(obj, outp, -1)

无论哪种方式,如果您创建了一个用于多个泡菜操作的Pickler 对象,您只需指定一次协议:

pickler = pickle.Pickler(outp, -1)
pickler.dump(obj1)
pickler.dump(obj2)
   etc...

注意:如果您在运行不同版本 Python 的环境中,那么您可能希望显式使用(即硬编码)所有人都可以读取的特定协议号(稍后版本通常可以读取早期版本生成的文件)。

多个对象

虽然泡菜文件可以包含任意数量的泡菜对象,如上面的示例所示,但当它们的数量未知时,通常更容易将它们全部存储在某种变量中 -大小的容器,例如 listtupledict,并在一次调用中将它们全部写入文件:

tech_companies = [
    Company('Apple', 114.18), Company('Google', 908.60), Company('Microsoft', 69.18)
]
save_object(tech_companies, 'tech_companies.pkl')

稍后恢复列表和其中的所有内容:

with open('tech_companies.pkl', 'rb') as inp:
    tech_companies = pickle.load(inp)

主要优点是您不需要知道保存了多少对象实例以便稍后加载它们(尽管这样做可能没有这些信息可能的,它需要一些稍微专门的代码)。有关执行此操作的不同方法的详细信息,请参阅相关问题 Saving and loading multiple objects in pickle file? 的答案。就我个人而言,我最喜欢@Lutz Prechelt 的answer,这就是下面示例代码中使用的方法:

class Company:
    def __init__(self, name, value):
        self.name = name
        self.value = value

def pickle_loader(filename):
    """ Deserialize a file of pickled objects. """
    with open(filename, "rb") as f:
        while True:
            try:
                yield pickle.load(f)
            except EOFError:
                break

print('Companies in pickle file:')
for company in pickle_loader('company_data.pkl'):
    print('  name: {}, value: {}'.format(company.name, company.value))

【讨论】:

  • 这对我来说很少见,因为我想会有一种更简单的方法来保存对象...类似 'saveobject(company1,c:\mypythonobjects)
  • @Peterstone:如果你只想存储一个对象,你只需要我示例中一半的代码——我故意按照我的方式编写它,以展示多个对象如何可以保存到同一个文件中(然后再读回)。
  • @Peterstone,职责分离有一个很好的理由。这样,酸洗过程中的数据的使用方式就没有限制。您可以将其存储到光盘中,也可以通过网络连接发送。
  • @martinaeau,这是对 perstones 评论的回应,即一个人应该只有一个功能来将对象保存到磁盘。 pickles 的职责是将对象转换为可以作为块处理的数据。将内容写入文件是文件对象的职责。通过将事物分开,可以实现更高的重用性,例如能够通过网络连接发送腌制数据或将其存储在数据库中,所有职责与实际数据分开对象转换
  • 您删除了company1company2。你为什么不也删除Company 并展示会发生什么?
【解决方案2】:

我认为假设对象是class 是一个非常强的假设。如果不是class 怎么办?还有一个假设是对象没有在解释器中定义。如果它是在解释器中定义的呢?另外,如果属性是动态添加的呢?当某些 python 对象在创建后将属性添加到其__dict__ 时,pickle 不尊重这些属性的添加(即它“忘记”它们被添加——因为pickle 通过引用对象定义进行序列化) .

在所有这些情况下,picklecPickle 可能会让你非常失望。

如果你想保存一个object(任意创建),你有属性(在对象定义中添加,或者在之后添加)......你最好的选择是使用dill,它可以序列化几乎任何东西蟒蛇。

我们从一门课开始……

Python 2.7.8 (default, Jul 13 2014, 02:29:54) 
[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pickle
>>> class Company:
...     pass
... 
>>> company1 = Company()
>>> company1.name = 'banana'
>>> company1.value = 40
>>> with open('company.pkl', 'wb') as f:
...     pickle.dump(company1, f, pickle.HIGHEST_PROTOCOL)
... 
>>> 

现在关闭,然后重新启动...

Python 2.7.8 (default, Jul 13 2014, 02:29:54) 
[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pickle
>>> with open('company.pkl', 'rb') as f:
...     company1 = pickle.load(f)
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/pickle.py", line 1378, in load
    return Unpickler(file).load()
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/pickle.py", line 858, in load
dispatch[key](self)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/pickle.py", line 1090, in load_global
    klass = self.find_class(module, name)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/pickle.py", line 1126, in find_class
    klass = getattr(mod, name)
AttributeError: 'module' object has no attribute 'Company'
>>> 

糟糕……pickle 无法处理。让我们试试dill。我们将添加另一种对象类型(lambda)以作好衡量。

Python 2.7.8 (default, Jul 13 2014, 02:29:54) 
[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dill       
>>> class Company:
...     pass
... 
>>> company1 = Company()
>>> company1.name = 'banana'
>>> company1.value = 40
>>> 
>>> company2 = lambda x:x
>>> company2.name = 'rhubarb'
>>> company2.value = 42
>>> 
>>> with open('company_dill.pkl', 'wb') as f:
...     dill.dump(company1, f)
...     dill.dump(company2, f)
... 
>>> 

现在读取文件。

Python 2.7.8 (default, Jul 13 2014, 02:29:54) 
[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dill
>>> with open('company_dill.pkl', 'rb') as f:
...     company1 = dill.load(f)
...     company2 = dill.load(f)
... 
>>> company1 
<__main__.Company instance at 0x107909128>
>>> company1.name
'banana'
>>> company1.value
40
>>> company2.name
'rhubarb'
>>> company2.value
42
>>>    

它有效。 pickle 失败而 dill 没有失败的原因是 dill__main__ 视为模块(在大多数情况下),并且还可以腌制类定义而不是通过引用腌制(如 pickle做)。 dill 可以腌制 lambda 的原因是它给了它一个名字……然后腌制魔法就会发生。

实际上,有一种更简单的方法可以保存所有这些对象,尤其是当您创建了很多对象时。只需转储整个 python 会话,稍后再返回。

Python 2.7.8 (default, Jul 13 2014, 02:29:54) 
[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dill
>>> class Company:
...     pass
... 
>>> company1 = Company()
>>> company1.name = 'banana'
>>> company1.value = 40
>>> 
>>> company2 = lambda x:x
>>> company2.name = 'rhubarb'
>>> company2.value = 42
>>> 
>>> dill.dump_session('dill.pkl')
>>> 

现在关掉你的电脑,去喝杯浓缩咖啡或其他什么,稍后再回来......

Python 2.7.8 (default, Jul 13 2014, 02:29:54) 
[GCC 4.2.1 Compatible Apple Clang 4.1 ((tags/Apple/clang-421.11.66))] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dill
>>> dill.load_session('dill.pkl')
>>> company1.name
'banana'
>>> company1.value
40
>>> company2.name
'rhubarb'
>>> company2.value
42
>>> company2
<function <lambda> at 0x1065f2938>

唯一的主要缺点是dill 不是python 标准库的一部分。所以如果你不能在你的服务器上安装一个python包,那么你就不能使用它。

但是,如果您能够在系统上安装 python 包,您可以通过git+https://github.com/uqfoundation/dill.git@master#egg=dill 获得最新的dill。您可以通过pip install dill获取最新发布的版本。

【讨论】:

  • 当我尝试将dill(看起来很有希望)与包含音频文件的相当复杂的对象一起使用时,我收到了TypeError: __new__() takes at least 2 arguments (1 given)
  • @MikeiLL:当你做什么时,你会得到一个TypeError,究竟是什么?这通常是实例化类实例时参数数量错误的标志。如果这不属于上述问题的工作流程,您能否将其作为另一个问题发布,通过电子邮件提交给我,或者将其作为问题添加到dill github 页面上?
  • 对于任何关注的人,这是发布的related question @MikeLL -- 从答案来看,这显然不是dill 问题。
  • dill 给了我MemoryErrorcPicklepicklehickle 也是如此。
  • 在读取操作期间,我收到 dill RecursionError: maximum recursion depth exceeded 的以下错误,是否有可能克服这个问题?
【解决方案3】:

使用您的问题中的company1 和python3 的快速示例。

import pickle

# Save the file
pickle.dump(company1, file = open("company1.pickle", "wb"))

# Reload the file
company1_reloaded = pickle.load(open("company1.pickle", "rb"))

然而,正如answer 所说,pickle 经常失败。所以你真的应该使用dill

import dill

# Save the file
dill.dump(company1, file = open("company1.pickle", "wb"))

# Reload the file
company1_reloaded = dill.load(open("company1.pickle", "rb"))

【讨论】:

    【解决方案4】:

    您可以使用anycache 为您完成这项工作。它考虑了所有细节:

    • 它使用dill作为后端, 它扩展了 python pickle 模块以处理 lambda 和所有不错的 python 功能。
    • 它将不同的对象存储到不同的文件中并正确地重新加载它们。
    • 限制缓存大小
    • 允许清除缓存
    • 允许在多次运行之间共享对象
    • 允许尊重影响结果的输入文件

    假设您有一个创建实例的函数myfunc

    from anycache import anycache
    
    class Company(object):
        def __init__(self, name, value):
            self.name = name
            self.value = value
    
    @anycache(cachedir='/path/to/your/cache')    
    def myfunc(name, value)
        return Company(name, value)
    

    Anycache 第一次调用myfunc 并将结果腌制到一个 cachedir 中的文件,使用唯一标识符(取决于函数名及其参数)作为文件名。 在任何连续运行中,都会加载腌制对象。 如果在 python 运行之间保留了cachedir,则腌制对象取自上一次 python 运行。

    如需了解更多详情,请参阅documentation

    【讨论】:

    • 如何使用anycache 保存多个实例,例如classlist 等容器(这不是调用函数的结果)?
    【解决方案5】:

    新版本的 pandas 还具有保存泡菜的功能。

    我发现它更容易。例如

    pd.to_pickle(object_to_save,'/temp/saved_pkl.pickle' )
    

    【讨论】: