Python的序列化模块

什么是序列化

在程序运行的过程中,所有的变量存储的数据都是加载在内存中,一旦程序结束,数据所占用的内存就被操作系统全部回收。各种类型的数据从内存中变成可存储或传输的过程称之为序列化,序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来,把数据内容从序列化的对象重新读到内存里称之为反序列化。

序列化的目的:

  • 以某种存储形式使自定义对象持久化;
  • 将对象可以从一个地方传递到另一个地方;
  • 使程序更具有维护性。

在 Python 中,序列化其实就是从其他数据类型转向一个字符串数据类型的过程,而反序列化是从字符串数据类型反向转换成其他数据类型的过程,这里所说的序列其实可以理解为字符串。

在 Python 中为什么要使用序列化模块

当我们在 Python 代码中计算的一个数据需要给另外一段程序使用,那我们怎么给?现在我们能想到的方法就是存在文件里,然后另一个 Python 程序再从文件里读出来。但是我们都知道,对于文件来说是没有字典这个概念的,所以我们只能将数据转换成字典放到文件中。你一定会问,将字典转换成一个字符串很简单,用 str(dic) 就可以办到了,为什么我们还要学习序列化模块呢?

序列化的过程就是从 dic 变成 str(dic) 的过程。现在你可以通过 str(dic) 将一个名为 dic 的字典转换成一个字符串,但是你要怎么把一个字符串转换成字典呢?聪明的你肯定想到了 eval(),如果我们将一个字符串类型的字典 str_dic 传给 eval(),就会得到一个返回的字典类型了。eval() 函数十分强大,但是 eval() 的官方 demo 解释为:将字符串 str 当成有效的表达式来求值并返回计算结果。但是强大的函数有代价,安全性是其最大的缺点。想象一下,如果我们从文件中读出的不是一个数据结构,而是一句类似于 os.remove() 类似的破坏性语句,那么后果将不堪设想。所以,我们并不推荐用 eva() l方法来进行反序列化操作(将 str 转换成 python 中的数据结构)。

Python 中提供了 pickle、json 、 shelve 模块来实现序列化和反序列化。

pickle 模块

使用 pickle 模块可以对所有的 Python 中的数据类型进行序列化和反序列化操作,并且 pickle 序列化的内容只能使用 Python 才能操作,且反序列化依赖代码。

在写入文件时由于 pickle 写入的是二进制数据,所以文件对象打开方式需要用 wb 和 rb 的模式 。下面是 pickle 模块中常用的方法:

dump

1
pickle.dump(obj, file, protocol=None)

必备参数 obj 表示将要封装的对象,必备参数 file 表示 obj 要写入的文件对象,file 必须以二进制可写模式打开,即 wb。可选参数 protocol 表示告知 pickler 使用的协议,支持的协议有 0,1,2,3,默认的协议是添加在 Python 3 中的协议 3。例如:

1
2
3
4
5
6
7
8
9
10
import pickle

data = ['1a', '2b', '3c']

with open('test.pickle', 'wb') as fwt:
pickle.dump(data, fwt)

'''
将数据通过特殊的形式转换为只有 Python 语言认识的字符串,并写入文件
'''

load

1
pickle.load(file, *, fix_imports=True, encoding="utf-8", errors="strict")

必备参数 file 必须以二进制可读模式打开,即 rb,其他都为可选参数。例如:

1
2
3
4
5
6
7
8
9
import pickle

with open('test.pickle', 'rb') as frd:
data = pickle.load(frd, encoding='utf-8')

print(data)
'''
load 从数据文件中读取数据,并转换为 Python 的数据结构
'''

dumps

1
pickle.dumps(obj, protocol=None)

以字节对象形式返回封装的对象,不需要写入文件中。例如:

1
2
3
4
5
6
7
8
9
10
11
import pickle

data = ['1a', '2b', '3c']

pkle_str = pickle.dumps(data)
print(pkle_str)
'''
dumps 将数据通过特殊的形式转换为只有 Python 语言认识的字符串
输出:
b'\x80\x03]q\x00(X\x02\x00\x00\x001aq\x01X\x02\x00\x00\x002bq\x02X\x02\x00\x00\x003cq\x03e.'
'''

loads

1
pickle.loads(bytes_object, fix_imports=True, encoding="utf-8", errors="strict")

从字节对象中读取被封装的对象并返回。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

data = ['1a', '2b', '3c']
pkle_str = pickle.dumps(data)

mess = pickle.loads(pkle_str)
print(mess)

'''
loads 将 pickle 数据转换为 Python 的数据结构
输出:
['1a', '2b', '3c']
'''

json 模块

json 是一种轻量级的数据交换格式,JSON 数据的书写格式是:名称/值对。名称/值对包括字段名称(在双引号中),然后着是一个冒号(:),最后是值。比如 { "name" : "Python" },类似于Python中的字典。

JSON值可以是数字(整数或浮点数),字符串(在双引号中),逻辑值(True 或 False),数组(在中括号中,Python 中是列表),对象(在大括号中)和 null。例如 { "age": 21,"graduated ":true }。JSON值的基本格式如下图所示。

json 作为一种通用的序列化格式具有更好的可读性和跨平台性,但是在 Python 中只有很少一部分数据类型能够通过 json 转化成字符串,不过这已经能能够满足大多数的需求。

json 模块提供了四个方法: dumps、dump、loads、load
在 Python 中其实就是字典格式,里面可以包含方括号括起来的数组,也就是 Python 里面的列表。

dumps 和 loads

dumps 和 loads 都是对内存中的数据进行操作。注意,要用 json 的 loads 功能来处理的 字符串类型的 字典中的字符串必须由 "" 表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import json
>>> dic = {'k1':'v1','k2':'v2','k3':'v3'}
>>> str_dic = json.dumps(dic) # 序列化:将一个字典转换成一个字符串
>>> str_dic
'{"k1": "v1", "k2": "v2", "k3": "v3"}'
>>> print(type(str_dic))
<class 'str'>
>>>
>>> dic2 = json.loads(str_dic) # 反序列化:将一个字符串格式的字典转换成一个字典
>>> dic2
{'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}
>>> print(type(dic2))
<class 'dict'>

对于嵌套的数据类型也是同样适用的:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import json
>>> list_dic = [1, ['a', 'b', 'c'], 3, {'k1': 'v1', 'k2': 'v2'}]
>>> str_dic = json.dumps(list_dic)
>>> str_dic
'[1, ["a", "b", "c"], 3, {"k1": "v1", "k2": "v2"}]'
>>> print(type(str_dic))
<class 'str'>
>>> list_dic2 = json.loads(str_dic)
>>> list_dic2
[1, ['a', 'b', 'c'], 3, {'k1': 'v1', 'k2': 'v2'}]
>>> print(type(list_dic2))
<class 'list'>

dump 和 load

dump 和 load 是对文件对象中的数据进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json

dic = {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}

with open('json_file', 'w', encoding='utf-8') as fwt:
json.dump(dic, fwt)
# dump方法接收一个文件句柄,直接将字典转换成json字符串写入文件

with open('json_file', 'r', encoding='utf-8') as frd:
dic2 = json.load(frd)
# load方法接收一个文件句柄,直接将文件中的json字符串转换成数据结构返回

print(type(dic2), dic2)

'''
输出结果:
<class 'dict'> {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}
'''

其他参数

这里的其他参数是针对 dump 和 dumps 方法而言的。

ensure_ascii:当它为 True 的时候,所有非 ASCII 码字符显示为 \uXXXX 序列,只需在 dump 时将 ensure_ascii 设置为 False 即可,此时存入 json 的字符就会原样输出,包括中文字符。

indent:如果 indent 是一个非负整数或者字符串,那么 JSON 数组元素和对象成员会被美化输出为该值指定的缩进等级。如果缩进等级为零、负数或者 "",则只会添加换行符。如果不设置 indent 则使用默认值 None 以最紧凑的形式来显示。使用一个正整数会让每一层缩进同样数量的空格。如果 indent 的值是一个字符串(比如 "\t"),那么这个字符串会被用于每一层的缩进。

separators:分隔符,实际上是 (item_separator, dict_separator) 的一个元组。当 indent 为 None 时,默认值取 (', ', ': '),否则取 (',', ': ')。为了得到最紧凑的 json 格式的数据,应该指定其为 (',', ':') 以消除空白字符。

默认的就是 (',',':') ;这表示 dictionary 内 keys 之间用 , 隔开,而 key 和 value之间用 : 隔开。

sort_keys:默认为 False,如果设置其值是 true ,那么字典的输出会以键的顺序排序。

Skipkeys:默认值是 False,如果字典的键不是 Python 的基本类型(包括 str, int、float、bool、None),在设置为 Skipkeys=False 时,就会报 TypeError 的错误。此时设置成 True,则会跳过这类 key 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json

province_city = {
'山西': {
'太原': ['小店区', '迎泽区', '杏花岭区', '古交市', '其他'],
'大同': ['城区', '矿区', '南郊区', '灵丘县', '浑源县', '左云县', '大同县', '其他'],
'吕梁': ['离石区', '文水县', '岚县', '方山县', '孝义市', '汾阳市', '其他']
}
}

json_dic2 = json.dumps(province_city,
ensure_ascii=False, sort_keys=True, indent=4)

print(json_dic2)

输出格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"山西": {
"吕梁": [
"离石区",
"文水县",
"岚县",
"方山县",
"孝义市",
"汾阳市",
"其他"
],
"大同": [
"城区",
"矿区",
"南郊区",
"灵丘县",
"浑源县",
"左云县",
"大同县",
"其他"
],
"太原": [
"小店区",
"迎泽区",
"杏花岭区",
"古交市",
"其他"
]
}
}

注意事项

JSON 中的键-值对中的键永远是 str 类型的。当一个对象被转化为 JSON 时,字典中所有的键都会被强制转换为字符串。这所造成的结果是字典被转换为 JSON 然后转换回字典时可能和原来的不相等。换句话说,如果 x 具有非字符串的键,则有 loads(dumps(x)) != x 。

json 和 pickle 的区别

json 和 picle 模块都有 dumps、dump、loads、load 四种方法,而且用法一样。二者不同之处是 json 模块序列化出来的是通用格式,其它编程语言都认识,就是普通的字符串,而 pickle 模块序列化出来的只有 Python 可以认识,其他编程语言无法识别,表现为乱码。

shelve 模块

shelve 模块是 Python3 新出现的方式,特点是操作简单,使用序列化句柄直接操作,非常方便。缺点是目前不太常用。使用 shelve 模块时,通常使用一个 open 方法即可,shelve 以键值对的形式存储数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import shelve

with shelve.open('aa') as f:
f['person'] = {'age': 23, 'job': 'student'}
f['person']['age'] = 44 # 这里试图改变原来的年龄23
f['numbers'] = [i for i in range(10)]

with shelve.open('aa') as f:
person = f['person']
print(person) # {'age': 23, 'job': 'student'}
nums = f['numbers']
print(nums) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

使用 shelve 模块操作文件时 文件最好不要有后缀名,因为 shelve 会生成独有的三个文件。其中以 bak 和 dir 为后缀的文件是可以查看的。

而且 shelve 模块有一个特点就是允许写回(writeback)。在上面的代码中有个细节,我们读取键 person 时候,发现 age 还是 23 岁,f['person']['age'] = 44 后并没有变成 44 。默认情况下直接使用 f['person'] 改变其中的值之后,不会更新已存储的值,也就是没有把更新写回到文件,即使是文件被 close 后。

如果有改变值的需要,在 open 函数中添加一个参数 writeback=True ,再次运行下即可看到 age 的值已经发生了改变。

另外要注意,shelve 模块是有 close 函数的,因此如果不是用 with 方法打开的文件,关闭时需要主动 close 。

有钱任性,请我吃包辣条
0%