保存一个对象(数据持久化)

368

我已经创建了这样一个对象:

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

我想保存这个对象。我该如何做到呢?


3
请参考示例以获取有关如何使用pickle的简单示例。 - Martin Thoma
@MartinThoma:为什么你(看起来)更喜欢那个答案而不是被接受的那个答案(来自链接问题)? - martineau
当我进行链接时,被接受的答案没有protocol=pickle.HIGHEST_PROTOCOL。我的答案还提供了pickle的替代方案。 - Martin Thoma
6个回答

663
你可以使用标准库中的 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?。简而言之,您可以使用以下内容来确保您的代码始终在可用时使用C版本,无论是在Python 2还是Python 3中:
try:
    import cPickle as pickle
except ModuleNotFoundError:
    import pickle

数据流格式(协议)

pickle可以读和写几种不同的Python特定格式的文件,称为协议,如文档中所述,“协议版本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对象以供多个pickle操作使用,只需指定一次协议即可:

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

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

多个对象

虽然pickle文件可以包含任意数量的pickled对象,如上例所示,但当存在未知数量的对象时,通常更容易将它们全部存储在某种可变大小的容器中,例如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)
主要的优点是,在后期加载对象实例时,你无需知道有多少个对象实例被保存(虽然在没有这些信息的情况下,仍然可以通过一些稍微专业的代码来实现)。查看相关问题的答案(在pickle文件中保存和加载多个对象)获取不同的解决方法的详细信息。在个人看来,我最喜欢@Lutz Prechelt的答案,因此下面的示例代码使用该方法:
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))

2
这对我来说很罕见,因为我想象中应该有一种更简单的方法来保存一个对象...比如 'saveobject(company1,c:\mypythonobjects)' - Peterstone
8
如果你只想要存储一个对象,你只需要用我示例中大约一半的代码即可--我特意将代码编写成这样,以展示如何将多个对象保存到同一个文件中(并稍后从其中读回)。 - martineau
4
@martinaeau,这是对perstones评论的回应,他认为只需要一个函数将对象保存到磁盘中。pickle的责任仅仅是将对象转换为可处理的数据块。将数据写入文件的责任属于文件对象。通过保持不同职责的分离,可以实现更高的重用性,例如能够通过网络连接发送pickled数据或将其存储在数据库中,这些都与实际数据<->对象转换分离开来。 - Harald Scheirich
1
@Mike:主要是因为这样做与问题本身没有太大关系,也不是大多数读者可能想要做的事情。但另外一个原因是我强烈怀疑提问的主要动机只是为了宣传你编写的“dill”模块——在某些情况下,这个模块肯定会很有用。 - martineau
2
@Mike:抱歉,我认为这个问题不是正确的解决方法。就我所知,更有效地推广“dill”的方法是在其下载页面上更清楚地说明它可以做什么,而不是在各种SO帖子中提出其用途来解决与手头问题无关的问题。如果有共识认为它足以解决人们在尝试使用“pickle”时普遍遇到的严重缺陷,也许它应该成为标准库的一部分。 - martineau
显示剩余15条评论

71

我认为假设这个对象是一个class是相当强的假设。如果不是class呢?还有一种假设是这个对象没有在解释器中定义,但如果它是在解释器中定义的呢?此外,如果属性是动态添加的呢?当某些 Python 对象在创建后在其__dict__中添加属性时,pickle 不会尊重这些属性的添加(即它会“忘记”它们被添加了,因为pickle按照对对象定义的引用进行序列化)。

在所有这些情况下,picklecPickle 都可能出现严重问题。

如果您想要保存一个任意创建的object,其中包含属性(无论是在对象定义中添加的还是后来添加的),您最好使用dill,它几乎可以序列化 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 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__ 并且可以序列化类定义。 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包,您就可以使用以下命令获取最新的dillgit+https://github.com/uqfoundation/dill.git@master#egg=dill。您也可以使用以下命令获取最新发布版本:pip install dill


当我尝试使用dill(看起来很有前途)处理一个包括音频文件的相当复杂的对象时,出现了TypeError: __new__() takes at least 2 arguments (1 given) - MikeiLL
1
@MikeiLL:当你做什么时,确切地说,你遇到了TypeError?这通常是在实例化类实例时参数数量错误的迹象。如果这不是上面问题的一部分,你可以将其作为另一个问题发布,通过电子邮件提交给我,或将其添加为“dill”github页面上的问题。 - Mike McKerns
3
对于任何跟进此问题的人,这是@MikeLL发表的相关问题 -- 根据答案,显然不是 dill 的问题。 - martineau
dill 给出了 MemoryError 的错误提示,而 cPicklepicklehickle 也是如此。 - Farid Alijani
在读取操作期间,我遇到了关于dill的以下错误:“RecursionError:超过最大递归深度”,是否有可能克服这个问题? - alper
显示剩余3条评论

18

以下是使用 Python3 处理您问题中的 company1 的快速示例。

import pickle

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

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

然而,正如这个答案中提到的,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"))

7
您可以使用 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目录下。在以后的调用中,序列化的对象将会被加载。如果保留了cachedir,则可以在之后的Python运行中继续使用序列化的对象。详情请查看文档

如何使用 anycache 来保存多个实例,比如一个 class 或容器(如 list),而不是调用函数的结果? - martineau

4
新版本的pandas功能也具备保存pickle文件的能力。
我发现这更加便捷。例如:
pd.to_pickle(object_to_save,'/temp/saved_pkl.pickle' )

2

虽然 pickle 是序列化对象最广泛使用的选项,但它并不是没有问题,特别是涉及安全性方面。有可能构造一个 Python 对象,在反序列化时会执行任意代码。下面附有一个示例。

import pickle

class Example:
    def __reduce__(self):
        print("Serialised")
        return print, ("Deserialised",)

example = Example()

serialised = pickle.dumps(example) # prints "Serialised"
pickle.loads(serialised) # prints "Deserialised"

上面的例子完全无害,但是很容易将其中的print替换为execeval等更加恶意的代码。
没有一个好的解决方案,但人们提到了dill库作为pickle的可能替代品。然而,还有另一种选择,即marshal。它是一个内置的序列化库,但它的缺点是它不能处理某些类型的对象(尽管与pickle不同,它可以序列化CodeObjects,这就是为什么在编译CodeObjects成.pyc文件时内部使用它进行序列化)。它也更快,因为是用C语言实现的(代码在这里),但还没有经过严格的审计。然而,目前没有已知的安全漏洞。 marshal实际上是pickle的替代品。它的loadsloaddumpsdump方法都完全相同。

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