Python 的 Pickle 模块

2025年1月11日 | 16分钟阅读

为了保存对象的内部状态以供后续使用,开发者有时可能希望通过网络传输复杂的对象指令。开发者可以利用Python标准库中的Pickle模块所支持的序列化过程来实现这一目标。

在本教程中,我们将讨论对象的序列化和反序列化,以及Python用户应该使用哪个包来序列化对象。Python的Pickle模块可以用来序列化各种类型的对象。我们还将讨论如何使用Pickle模块来序列化对象层次结构,以及开发者在从不可信来源反序列化对象时可能面临的风险。

Python序列化

在序列化过程中,数据结构被转换为可以存储或通过网络传输的线性形式。

Python的序列化功能允许程序员将一个复杂的对象结构转换为一个字节流,该字节流可以通过网络发送或存储在磁盘上。开发者可以将此技术称为编组(marshaling)。相反,反序列化是序列化的逆过程,涉及用户将字节流转换为数据结构。这个过程被称为解组(unmarshalling)。

序列化是开发者可以在许多不同场景中使用的工具。其中一个场景是,在完成训练阶段后保存神经网络的内部状态,以便以后可以再次使用,而无需重复训练。

Python标准库中有三个模块允许程序员序列化和反序列化对象:

  1. pickle模块
  2. marshal模块
  3. json模块

对于对象的序列化,开发者可以使用Python,它也支持XML。

在这三个模块中,json模块是最新颖的。它使开发者能够处理标准的JSON文件。JSON是交换数据的最佳和最常用的格式。

JSON格式之所以受欢迎,有多种原因,包括:

  • 人类可读
  • 它不受语言限制。
  • 比XML更轻量

开发者可以使用json模块序列化和反序列化多种常见的Python类型:

  • 列表
  • 字典(Dict)
  • String
  • int
  • 元组
  • Bool
  • Float

marshal模块是这三个模块中最古老的。它的主要功能是读写.py文件,当解释器导入Python模块时,开发者会收到这些文件,即Python模块的已编译字节码。因此,尽管开发者可以使用marshal模块来序列化对象,但并不建议这样做。

另一种序列化和反序列化Python对象的方法是使用pickle包。与json模块不同,它有其独特之处。对象以二进制格式序列化,这会产生人类无法理解的数据。它可以处理多种不同的Python类型,包括开发者定义的自定义对象,并且比其他方法更快。

因此,开发者有多种选择来序列化和反序列化Python对象。以下三个标准对于确定哪种方法适合开发者的情况至关重要:

  • 不应使用marshal模块,因为它的主要用户是解释器。根据官方文档,Python格式可能会以向后不兼容的方式更改。
  • 如果开发者需要与多种语言兼容并需要人类可读的格式,XML和JSON是很好的选择。
  • 对于所有其余场景,Python的pickle模块是理想选择。假设开发者更喜欢专有的、可互操作的格式,而不是标准的人类可读格式。

并且他们要求序列化自定义对象。那么pickle模块是下一个可用的选项。

什么是Pickle模块?

Python的pickle模块用于序列化和反序列化Python对象。序列化,也称为“pickling”,是将Python对象转换为字节流的过程,该字节流可以保存到文件或通过网络传输。反序列化,或“unpickling”,是相反的过程,即将字节流转换回Python对象。

pickle模块的主要特点

1. 序列化对象

  • pickle.dumps(obj): 将一个Python对象obj转换为字节流。
  • pickle.dump(obj, file): 将对象obj的字节流写入文件对象file。

2. 反序列化对象

  • pickle.loads(byte_stream): 将字节流转换回Python对象。
  • pickle.load(file): 从文件对象file中读取字节流并将其转换回Python对象。

pickling模块内部

Python的pickle模块包含以下四种方法:

  • dump( obj, file, protocol = None, * , fix_imports = True, buffer_callback = None)
  • dumps( obj, protocol = None, * , fix_imports = True, buffer_callback = None)
  • load( file, * , fix_imports = True, encoding = " ASCII ", errors = "strict ", buffers = None)
  • loads( bytes_object, * , fix_imports = True, encoding = " ASCII ", errors = " strict ", buffers = None)

前两种方法用于执行pickling过程,后两种方法用于执行unpickling过程。

在dump()和dumps()之间,前者创建一个包含序列化结果的文件,而后者返回一个字符串。

开发者可以记住dumps()函数中的“s”代表字符串(string),以便将其与dump()区分开来。

load()和loads()函数也可以类似地使用。loads()函数处理字符串,而load()函数则读取文件以进行unpickling过程。

假设用户开发了一个名为forexample_class的自定义类,它具有多种不同类型的特性:

  • The_number
  • The_string
  • The_list
  • The_dictionary
  • The_tuple

下面的示例演示了如何创建该类的一个实例并将其pickle,以获得一个可供用户使用的普通字符串。如果用户在类被pickle后更改其属性的值,pickle后的字符串不会受到影响。然后,用户可以恢复pickle后的类的副本,并将之前pickle过的字符串在另一个变量中unpickle。

示例

输出

This is user's pickled object: 
 b' \x80 \x04 \x95$ \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x8c \x08__main__ \x94 \x8c \x10forexample_class \x94 \x93 \x94) \x81 \x94. ' 
 
 This is the_dict of the unpickled object: 
 {' first ': ' a ', ' second ': 2, ' third ': [ 1, 2, 3 ] } 

说明

在这里,pickling过程已正确结束,它将用户的整个实例存储在字符串中:b' \x80 \x04 \x95$ \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x8c \x08__main__ \x94 \x8c \x10forexample_class \x94 \x93 \x94) \x81 \x94. ' 完成pickling过程后,用户可以更改其原始对象,将 a_dict 属性等于 None。

用户现在可以unpickle该字符串并创建一个全新的实例,此时用户会得到一个与pickling过程开始时的对象原始结构完全相同的副本。

Python的Pickle模块协议格式

pickle模块是Python特有的;只有另一个程序可以读取其输出。开发者应该知道,即使他们可能正在使用Python,pickle模块目前也在不断发展。

因此,如果开发者使用特定版本的Python对对象进行了pickle,他们可能无法使用更早的版本来unpickle它。

Python的Pickle模块支持六种不同的协议。协议版本越高,就越需要更新的Python解释器来unpickle。

  • 协议版本0是最初的版本。与其他协议不同,它是人类可读的。
  • 协议版本1是第一个使用二进制格式的。
  • 协议版本2在Python 2.3中引入。
  • 协议版本3包含在Python 3.0中。Python 2.x无法unpickle它。
  • 协议版本4在Python 3.4中引入。从Python 3.8开始,它是默认协议,并支持各种对象大小和类型。
  • 协议版本5在Python 3.8中引入。

可Pickle和不可Pickle的类型

虽然我们已经讨论过Python的pickle模块可以比json模块序列化更多类型的对象,但并非所有类型都可以被pickle。

不可pickle的对象列表包括数据库连接、活动线程、打开的网络套接字等等。

如果用户发现自己被不可pickle的对象困住,可用的选项并不多。他们的第一个选择是使用第三方库,如Dill。

dill库可以增强pickle的功能。借助这个库,用户可以序列化一些不常见的类型,包括嵌套函数、lambdas、带yield的函数等等。

用户可以尝试pickle lambda函数来测试这个模块。

例如

Python的pickle模块无法序列化lambda函数,所以如果用户尝试运行这段代码,他们会收到一个异常。

输出

PicklingError                             Traceback (most recent call last)
<ipython-input-9-1141f36c69b9> in <module>
      3 
      4 squaring = lambda x : x * x
----> 5 user_pickle = pickle.dumps(squaring)

PicklingError: Can't pickle <function <lambda> at 0x000001F1581DEE50>: attribute lookup <lambda> on __main__ failed

如果用户将pickle模块换成dill库,他们现在就可以看到区别了。

例如

输出

b' \x80 \x04 \x95 \xb2 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x8c \ndill._dill \x94 \x8c \x10_create_function \x94 \x93 \x94 ( h \x00 \x8c \x0c_create_code \x94 \x93 \x94 ( K \x01K \x00K \x00K \x01K \x02KCC \x08| \x00| \x00 \x14 \x00S \x00 \x94N \x85 \x94 ) \x8c \x01x \x94 \x85 \x94 \x8c \x1f< ipython-input-11-30f1c8d0e50d > \x94 \x8c \x08< lambda > \x94K \x04C \x00 \x94 ) )t \x94R \x94c__builtin__ \n__main__ \nh \nNN } \x94Nt \x94R \x94. '

Dill库还有一个有趣的特性:能够序列化整个解释器会话。

例如

在上面的示例中,用户启动了解释器,导入了模块,然后定义了lambda函数以及一些其他变量。然后他们导入了dill库并调用了dump_session()函数来序列化整个会话。

如果用户正确运行了代码,他们将在当前目录中得到testing.pkl文件。

输出

$ ls testing.pkl
4 -rw-r--r--@ 1 dave  staff  493 Feb  12 09:52 testing.pkl

现在,用户可以启动解释器的一个新实例并加载testing.pkl文件以恢复他们的上一个会话。

例如

输出

dict_items( [ ( ' __name__ ' , ' __main__ ' ) , ( ' __doc__ ' , ' Automatically created module for IPython interactive environment ' ) , ( ' __package__ ' , None ) , ( ' __loader__ ' , None ) , ( ' __spec__ ' , None ) , ( ' __builtin__ ' , < module ' builtins ' ( built-in ) > ) , ( ' __builtins__ ' , < module ' builtins ' ( built-in ) > ) , ( ' _ih ' , [ ' ' , ' globals().items() ' ] ) , ( ' _oh ' , {} ) , ( ' _dh ' , [ ' C:\\Users \\User Name \\AppData \\Local \\Programs \\Python \\Python39 \\Scripts ' ] ) , ( ' In ' , [ ' ' , ' globals().items() ' ] ) , ( ' Out ' , {} ) , ( ' get_ipython ' , < bound method InteractiveShell.get_ipython of < ipykernel.zmqshell.ZMQInteractiveShell object at 0x000001E1CDD8DDC0 > > ) , ( ' exit ' , < IPython.core.autocall.ZMQExitAutocall object at 0x000001E1CDD9FC70 > ) , ( ' quit ' , < IPython.core.autocall.ZMQExitAutocall object at 0x000001E1CDD9FC70 > ) , ( ' _ ' , ' ' ) , ( ' __ ' , ' ' ) , ( ' ___ ' , ' ' ) , ( ' _i ' , ' ' ) , ( ' _ii ' , ' ' ) , ( ' _iii ' , ' ' ) , ( ' _i1 ' , ' globals().items() ' ) ] )

用户启动了解释器,导入了模块,然后定义了示例的lambda函数和一些其他变量。导入dill库后,他们运行了dump_session()函数来序列化整个会话。

==

如果代码已正确执行,testing.pkl文件应该在用户的当前目录中。

输出

dict_items( [ ( ' __name__ ' , ' __main__ ' ) , ( ' __doc__ ' , ' Automatically created module for IPython interactive environment ' ) , ( ' __package__ ' , None ) , ( ' __loader__ ' , None ) , ( ' __spec__ ' , None ) , ( ' __builtin__ ' , < module ' builtins ' ( built-in ) > ) , ( ' __builtins__ ' , < module ' builtins ' ( built-in ) > ) , ( ' _ih ' , [ ' ' , " squaring = lambda x : x * x \na = squaring( 25 ) \nimport math \nq = math.sqrt ( 139 ) \nimport dill \ndill.dump_session( ' testing.pkl ' ) \nexit() " ] ) , ( ' _oh ' , {} ) , ( ' _dh ' , [ ' C:\\ Users\\ User Name \\AppData \\Local \\Programs \\Python \\Python39 \\Scripts ' ] ) , ( ' In ' , [ ' ' , " squaring = lambda x : x * x \np = squaring( 25 ) \nimport math\nq = math.sqrt ( 139 ) \nimport dill \ndill.dump_session( ' testing.pkl ' ) \nexit() " ] ) , ( ' Out ' , {} ) , ( ' get_ipython ' , < bound method InteractiveShell.get_ipython of < ipykernel.zmqshell.ZMQInteractiveShell object at 0x000001E1CDD8DDC0 > > ) , ( ' exit ' , < IPython.core.autocall.ZMQExitAutocall object at 0x000001E1CDD9FC70 > ) , ( ' quit ' , < IPython.core.autocall.ZMQExitAutocall object at 0x000001E1CDD9FC70 > ) , ( ' _ ' , ' ' ) , ( ' __ ' , ' ' ) , ( ' ___ ' , ' ' ) , ( ' _i ' , ' ' ) , ( ' _ii ' , ' ' ) , ( ' _iii ' , ' ' ) , ( ' _i1 ' , " squaring = lambda x : x * x \np = squaring( 25 ) \nimport math \nq = math.sqrt ( 139 ) \nimport dill \ndill.dump_session( ' testing.pkl ' ) \nexit() " ) , ( ' _1 ' , dict_items( [ ( ' __name__ ' , ' __main__ ' ) , ( ' __doc__ ' , ' Automatically created module for IPython interactive environment ' ) , ( ' __package__ ' , None ) , ( ' __loader__ ' , None ) , ( ' __spec__ ' , None ) , ( ' __builtin__ ' , < module ' builtins ' ( built-in ) > ) , ( ' __builtins__ ' , < module ' builtins ' ( built-in ) > ) 

输出

625

输出

22.0

输出

(x) >

这里是初始的globals()。item()语句表明解释器处于起始状态,开发者必须导入DILL库并调用load_session()来恢复他们序列化的解释器会话。

彼得处于起始状态。

开发者应该记住,如果他们使用dill库而不是pickle模块,那么dill库不是标准库的一部分。与pickle模块相比,它更慢。

尽管Dill库可以比Pickle模块序列化更多的对象,但它不能解决开发者可能遇到的所有序列化问题。如果开发者想要序列化一个包含数据库连接的对象,他们不能使用Dill库。dill库有一个名为unserialized object的未序列化对象。

这个问题的解决方案是在反序列化后不重新初始化连接的情况下序列化对象。

开发者可以使用 _getstate_() 方法指定哪些对象应该被包含在pickling过程中以及其他细节。开发者可以使用这种技术来指明他们想要pickle什么。如果他们不覆盖 _getstate_(),将会使用 _dict_(),这是一个默认实例。

在下面的示例中,用户定义了一个带有一些属性的类,然后使用 _getstate_() 将其中一个属性从序列化过程中排除。

例如

在上面的示例中,用户生成了一个具有三个属性的对象,其中一个是lambda,对于pickle模块来说是不可pickle的对象。为了解决这个问题,他们在 _getstate_() 函数中定义了要pickle的属性。用户复制了整个实例的 _dict_ 来定义类的所有属性,然后删除了不可pickle的属性 'r'。

运行这段代码并反序列化对象后,用户可以观察到新实例缺少 'r' 属性。

输出

{'p': 25, 'q': ' testing '}

Pickle对象压缩

Python中的Pickle对象压缩涉及使用pickle模块序列化对象,然后压缩序列化的数据以减小其大小。这通常通过将pickle与zlib、gzip或bz2等压缩库结合使用来实现。例如,您可以pickle一个对象,使用zlib.compress()压缩字节流,然后使用zlib.decompress()解压和unpickle。这种方法对于节省存储空间和减少网络传输时间很有用。然而,重要的是要平衡压缩效率和处理开销,特别是对于大型或频繁访问的数据。

示例

输出

Serialized Data (byte stream): b'\x80\x04\x95>\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c\x05Alice\x94\x8c\x03age\x94K\x1e\x8c\x04city\x94\x8c\x08New York\x94u.'
Compressed Data: b'x\x9c\xabV*IM-.QH\xcc-\xc8IUH,ITE\x04\x00\xa2x\x11\xa0'
Decompressed Data (byte stream): b'\x80\x04\x95>\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x04name\x94\x8c\x05Alice\x94\x8c\x03age\x94K\x1e\x8c\x04city\x94\x8c\x08New York\x94u.'
Original Data: {'name': 'Alice', 'age': 30, 'city': 'New York'}

用户需要记住,文件越小意味着过程越慢。

关于Pickle模块安全性的担忧

到目前为止,我们已经讨论了使用Python的pickle包来序列化和反序列化对象。当开发者希望将对象的状态保存到磁盘或通过网络发送时,序列化是一种便捷的方法。

Python的pickle模块不是很安全,这是开发者需要注意的另一点。我们已经讨论了使用 _set state_() 函数。这个方法最适合用于unpickling过程和额外的初始化。

开发者降低风险的选项很少。一般的准则是,开发者永远不应该unpickle从不可信来源获得或通过不安全网络发送的数据。用户可以使用hmac等工具对数据进行签名,以确保它没有被篡改,从而挫败攻击。

例如

观察unpickle一个修改过的pickle如何将用户的系统暴露给攻击者。

例如

在上面的示例中,unpickling过程调用的 _set state_() 函数将运行一个Bash命令,在8080端口上向192.168.1.10系统打开一个远程shell。

用户可以在他们的Mac或Linux机器上以这种方式安全地测试脚本。要列出到8080端口的连接,他们必须首先打开终端,然后使用nc命令。

例如

攻击者将使用这个终端。

然后,在同一台计算机系统上,用户必须打开另一个终端并运行Python代码来移除恶意代码。

用户必须确保将代码中的IP地址更改为与他们正在攻击的终端的IP地址相匹配。运行代码后,shell就对攻击者可用了。

攻击控制台现在将显示一个bash shell。此时,被攻击的系统可以直接操作这个控制台。

例如

输出

bash: no job control in this shell

The default interactive shell is now zsh.
To update your account to use zsh, please run ` chsh -s /bin /zsh`.
For more details, please visit https://support.apple.com /kb /HT208060.
bash-3.1$

安全注意事项

  • 不可信来源: 避免对从不可信或未经身份验证的来源接收的数据进行unpickling。从不可信来源unpickling数据可能导致执行任意代码,构成重大安全风险。
  • 验证: 确保正在unpickling的数据来自可信且经过验证的来源。

优点

  • 多功能性: 可以序列化大多数Python对象,包括列表、字典、类和实例等复杂数据结构。
  • 便利性: 简化了为持久存储或数据交换而保存和加载对象的过程。

局限性

  • 安全性: 从不可信来源unpickling数据可能导致安全漏洞。
  • 跨语言兼容性: Pickled数据是Python特有的,可能不容易与其他编程语言一起使用。
  • 版本兼容性: 使用一个版本的Python创建的Pickled数据可能与其他版本不兼容。

结论

总之,Python的pickle模块是用于序列化和反序列化Python对象的强大工具,能够将对象转换为字节流,反之亦然。此功能对于将复杂数据结构保存到文件或通过网络传输它们特别有用。pickle模块简化了数据持久化和交换,为处理Python对象提供了一种便捷的方式。

s