Pickle序列化的优点和缺点
图片来源:RitaE from Pixabay
除非你知道所有这些要点,否则不要使用Python Pickle
Pickle序列化的优点和缺点,以及我们应该何时使用它
与其他大多数流行的编程语言相比,Python可能拥有最灵活的对象序列化。在Python中,所有的东西都是一个对象,所以我们可以说,几乎所有的东西都可以被序列化。是的,我说的这个模块就是 Pickle。
然而,与其他 "常规 "的序列化方法如JSON相比,Pickle有更多的方面需要我们在使用时注意。这就是标题所说的,除非你知道这些事实,否则不要使用Pickle。
在这篇文章中,我将组织一些关于Pickle的重要注意事项,希望它们能对你有所帮助。
- 基本用法
图片:Photo Mix from Pixabay
通过使用Python的Pickle模块,我们可以轻松地将几乎所有类型的对象序列化到一个文件中。在我们使用它之前,需要先导入它。
导入 pickle
让我们以一个字典为例。
import pickle
Let’s take a dictionary as an example.
my_dict = {
'name': 'Chris',
'age': 33
}
我们可以使用 pickle.dump() 方法来序列化字典并将其写入文件。
with open('my_dict.pickle', 'wb') as f:
pickle.dump(my_dict, f)
然后,我们可以读取该文件并将其加载到一个变量中。之后,我们就有了准确的字典回来。它们在内容上是 100% 相同的。
with open('my_dict.pickle', 'rb') as f:
my_dict_unpickled = pickle.load(f)
my_dict == my_dict_unpickled
- 为什么是Pickle?有什么优点和缺点?
图片来自Pixabay的Christine Sponchia
的确,如果我们在上面的例子中使用JSON来序列化Python字典,会有更多的好处。一般来说,Pickle序列化有三个主要缺点。
缺点-1:Pickle是不安全的
不像JSON只是一个字符串,有可能构建恶意的Pickle数据,在解压过程中执行任意代码。
大喵补充:pickle是一串二进制的数字!因此,我们绝对不应该解开那些可能来自不信任的源头或可能被篡改的数据。
缺点2:Pickle是不可读的
将Python字典序列化为JSON字符串的最大意义在于,其结果是人类可读的。然而,对于Pickle文件来说,这并不是真的。这里是我们刚刚腌制的字典的 pickle 文件。如果我们试图把它作为一个文本文件打开,我们会得到这样的结果。
缺点 3:Python 中的 Pickle 是有限的
一个 pickle 对象只能用 Python 加载。其他语言可能可以这样做,但需要第三方库的参与,而且可能仍然没有得到完美的支持。
相比之下,JSON字符串在编程领域非常常用,并且被大多数编程语言所支持。
Pickle的优点
Pickle通过调用任意的函数来构造任意的Python对象,这就是为什么它不安全。然而,这使得它能够序列化几乎所有的Python对象,而JSON和其他序列化方法是做不到的。
解开一个对象通常不需要 "模板"。所以,它非常适用于快速和简单的序列化。例如,你可以把所有的变量转储到pickle文件中,然后终止你的程序。稍后,你可以启动另一个Python会话,并从序列化的文件中恢复一切。所以,这使我们能够以一种更灵活的方式运行一段程序。
另一个例子将是多线程。当我们使用多进程模块在多线程中运行一个程序时,我们可以很容易地将任意的Python对象发送到其他进程或计算节点。
在这些情况下,安全问题通常并不适用,人类也不会去读这些对象。我们只需要快速、简单和兼容性。在这些情况下,Pickle可以完美地被利用。
- 还有什么可以被腌制 pickle?
好吧,我一直在谈论几乎所有可以被Pickle序列化的东西。现在,让我给你看一些例子。
Do Not Use Python Pickle Unless You Know All These Points2.jpegpickle 一个函数
第一个例子将是一个函数。是的,我们可以在 Python 中序列化一个函数,因为函数在 Python 中也是一个对象。
def my_func(num):
print(f'my function will add 1 to the number {num}')
return num + 1
只是为演示目的定义了一个简单的函数。现在,让我们把它提取出来并载入一个新的变量中。
with open('my_func.pickle', 'wb') as f:
pickle.dump(my_func, f)
with open('my_func.pickle', 'rb') as f:
my_func_unpickled = pickle.load(f)
my_func_unpickled(10)
新的变量可以作为一个函数使用,而且这个函数将与原来的函数完全相同。
Pickle一个Pandas数据框
另一个例子是一个Pandas数据框。让我们来定义一个Pandas数据框。
import pandas as pd
my_df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Chris'],
'age': [25, 29, 33]
})
现在,我们可以腌制它并将其解压到一个新的变量中,新的DataFrame将是相同的。
with open('my_df.pickle', 'wb') as f:
pickle.dump(my_df, f)
with open('my_df.pickle', 'rb') as f:
my_df_unpickled = pickle.load(f)
请注意,Pandas有内置的方法,可以对数据帧进行pickle和unpickle处理
请注意,Pandas有内置的方法,可以对数据帧进行纠错和解错。它们会做与上面相同的工作,但代码会更干净。其性能也是相同的。
那么,可能会有一个问题,为什么我们要对数据框使用Pickle而不是CSV?
第一个答案是速度。CSV是人类可读的,但它几乎是存储Pandas数据帧的最慢的方式。
对序列化Pandas数据框架的不同方式的性能进行了基准测试。
Reference benchmark test:
What is the fastest way to upload a big csv file in notebook to work with python pandas? - Stack Overflow
Pickle pandas dataframe的第二个好处是数据类型。当我们把数据框架写到CSV文件时,所有的东西都要转换成文本。有时,当我们把它加载回来时,这将造成一些不便或麻烦。例如,如果我们把一个日期时间列写到CSV中,我们很可能需要在加载回来时指定格式字符串。
然而,这个问题对于一个pickle对象来说并不存在。你腌制的东西,当你加载它时,你保证会有完全相同的东西回来。不需要做任何其他事情。
- 腌制协议版本
像我在前面的例子中所做的那样使用Pickle是很常见的。他们没有错,但如果我们能指定Pickle的协议版本(通常是最高的),那就更好了。简单地说,Pickle的序列化有不同的版本。随着Python版本的不断迭代,Pickle模块也在不断发展。
如果你对现有的版本和改进的内容感兴趣,这里有一个官方文档的列表。
协议版本0是原始的 "人类可读 "协议,并且向后兼容早期的Python版本。
协议版本1是一种旧的二进制格式,也与早期的Python版本兼容。
协议版本 2 是在 Python 2.3 中引入的。它提供了一个更有效的新式类的腌制方法。
协议版本 3 是在 Python 3.0 中加入的。它对字节对象有明确的支持,并且不能被Python 2.x解开。这是Python 3.0-3.7的默认协议。
协议版本 4 是在 Python 3.4 中添加的。它增加了对非常大的对象的支持,腌制了更多种类的对象,以及一些数据格式的优化。从Python 3.8开始,它是默认的协议。
协议版本5是在Python 3.8中添加的。它增加了对带外数据的支持和对带内数据的加速。
一般来说,在以下方面,高版本总是比低版本好一些
腌制对象的大小
解除pickling的性能
如果我们使用不同的版本对Pandas数据帧进行pickle,我们可以看到其大小上的差异。
with open('my_df_p4.pickle', 'wb') as f:
pickle.dump(my_df, f, protocol=4)
with open('my_df_p3.pickle', 'wb') as f:
pickle.dump(my_df, f, protocol=3)
with open('my_df_p2.pickle', 'wb') as f:
pickle.dump(my_df, f, protocol=2)
with open('my_df_p1.pickle', 'wb') as f:
pickle.dump(my_df, f, protocol=1)
path.getsize
import os
print('P4:', os.path.getsize('my_df_p4.pickle'))
print('P3:', os.path.getsize('my_df_p3.pickle'))
print('P2:', os.path.getsize('my_df_p2.pickle'))
print('P1:', os.path.getsize('my_df_p1.pickle'))
为什么 Python 仍然保留旧版本而新版本总是更好?这是因为协议并不总是向后兼容。这意味着,如果我们想要更好的兼容性,就必须选择一个较低的版本。
然而,如果我们使用pickle对象而不需要向后兼容,我们可以使用枚举来保证我们的程序使用最新的版本(最好的版本)。例子如下。
pickle.dump(my_df, f, protocol=pickle.HIGHEST_PROTOCOL)
- 拾取一个自定义类
尽管Pickle支持Python中几乎所有的对象,但当我们拾取一个从自定义类中实例化出来的对象时,我们仍然需要小心。简而言之,当我们加载被拾取的对象时,该类需要是存在的。
例如,让我们定义一个有两个属性和一个方法的简单类 "Person"。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def self_introduce(self):
print(f'My name is {self.name} and my age is {self.age}')
p = Person('Chris', 33)
p.self_introduce()
现在,让我们用Pickle来序列化对象 "p"。
with open('person.pickle', 'wb') as f:
pickle.dump(p, f)
如果这个类不存在,问题就会发生。如果我们试图在一个新的会话中加载pickle对象,而这个类并没有被定义,这就会发生。我们可以通过删除类的定义来模拟这种情况。
del Person
然后,如果我们试图重新加载腌制对象,就会出现一个异常。
with open('person.pickle', 'rb') as f:
p_unpickled = pickle.load(f)
image.png
因此,我们需要确保当我们加载对象回来时,这个类是存在的。然而,如果类的定义稍有不同,可能不会造成问题,但对象的行为可能会根据新的类定义而改变。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def self_introduce(self):
print(f'(Modified) My name is {self.name} and my age is {self.age}')
在新的类定义中,我修改了自我介绍方法的打印信息。然后,如果我们把腌制好的对象装回去,就不会有任何错误,但自我介绍方法会与原来的方法不同。
with open('person.pickle', 'rb') as f:
p_unpickled = pickle.load(f)
p_unpickled.self_introduce()
image.png
- 不是所有的对象都可以被pickled
在最后一节中,我不得不回到我最初的声明 "几乎所有的 Python 对象都可以被腌制"。我使用 "几乎所有 "是因为仍然有一些类型的对象不能被Pickle序列化。
一个典型的不能被腌制的类型是实时连接对象,比如网络或数据库连接。这是有道理的,因为Pickle在关闭后将无法建立连接。这些对象只能用适当的凭证和其他资源重新创建。
另一个需要提到的类型将是一个模块。一个重要的模块也不能被腌制。请看下面的例子。
import datetime
with open('datetime.pickle', 'wb') as f:
pickle.dump(datetime, f)
这一点很重要,因为这意味着我们将不能在global()中腌制所有的东西,因为导入的模块会在里面。
总结
在这篇文章中,我介绍了Python中内置的序列化方法 - Pickle。它可以用于快速而简单的序列化。它几乎支持所有类型的Python对象,如函数,甚至Pandas数据帧。当对不同版本的Python使用Pickle时,我们还需要记住,Pickle的版本也可能不同。