Serialização e Desserialização de Objetos do Python: Parte 1
Portuguese (Português) translation by Erick Patrick (you can also view the original English article)
Serialização de deserialização de objetos em Python é um aspecto importante de qualquer programa não trivial. Quando em Python se salvar algo em arquivo, ler um arquivo de configuração ou se responder a uma requisição HTTP, realizamos serialização e deserialização de objetos.
Para alguns, serialização e deserialização são as coisas mais chatas do mundo. Quem se importa com todos esses formatos e protocolos? Só queremos persistir ou transmitir alguns objetos Python e obtê-los de volta intactos depois.
É uma maneira bem saudável de ver o mundo em um nível conceitual. Mas, a nível pragmático, o esquema, formato ou protocolo de serialização que escolhermos determinará o quão rápido os programas executam, quão seguros são, quanta liberdade para manter seu estado e o quão bem interoperará com outros sistemas.
O motivo de tantas opção é que diferentes circunstâncias clamam por soluçõe diferentes. Não existe "solução de tamanho único". Nesse tutorial de duas partes, mostraremos os prós e contras dos esquemas de serialização e deserialização, assim como seus usos, e proveremos guias para a escolha certa para um determinado caso.
Exemplos Executáveis
A seguir, serializaremos e deserializaremos os mesmos grafos de objetos Python usando serializadores diferentes. Para evitar repetição, definiremos os grafos de objeto aqui.
Grafo de Objetos Simples
Grafo de objeto simples é um dicionário contendo: lista de inteiros, cadeia de caracters, ponto flutuante, booleano e None.
1 |
simple = dict(int_list=[1, 2, 3], |
2 |
|
3 |
|
4 |
|
5 |
text='string', |
6 |
|
7 |
|
8 |
|
9 |
number=3.44, |
10 |
|
11 |
|
12 |
|
13 |
boolean=True, |
14 |
|
15 |
|
16 |
|
17 |
none=None) |
Grafo de Objetos Complexos
Gráfo de Objeto Complexo é um dicionário com: objeto datetime, uma instância de classe definida por usuário com um atributo self.simple, que é um gráfo de objeto simples.
1 |
from datetime import datetime |
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
class A(object): |
10 |
|
11 |
|
12 |
|
13 |
def __init__(self, simple): |
14 |
|
15 |
|
16 |
|
17 |
self.simple = simple |
18 |
|
19 |
|
20 |
|
21 |
def __eq__(self, other): |
22 |
|
23 |
|
24 |
|
25 |
if not hasattr(other, 'simple'): |
26 |
|
27 |
|
28 |
|
29 |
return False |
30 |
|
31 |
|
32 |
|
33 |
return self.simple == other.simple |
34 |
|
35 |
|
36 |
|
37 |
def __ne__(self, other): |
38 |
|
39 |
|
40 |
|
41 |
if not hasattr(other, 'simple'): |
42 |
|
43 |
|
44 |
|
45 |
return True |
46 |
|
47 |
|
48 |
|
49 |
return self.simple != other.simple |
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
complex = dict(a=A(simple), when=datetime(2016, 3, 7)) |
Pickle
Pickle é o primeiro. É o formato de serialização de objetos nativo do Python. Sua interface provê quatro métodos: dump, dumps, load e loads. O dump() serializa em um arquivo aberto (objeto do tipo file). O dumps() serializa para uma cadeia de caracteres. O load() deserializa de um objeto aberto do tipo file. O loads() deserializa de uma cadeia de caracteres.
Pickle suporta, por padrão, um protocolo textual, mas também um protocolo binário, mais eficiente, mas não legível (que é útil em depuração).
Eis como serializa um objeto de grafo Python para uma cadeia de caracteres e para um arquivo, ambos com protoclos.
1 |
import cPickle as pickle |
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
pickle.dumps(simple) |
10 |
|
11 |
|
12 |
|
13 |
"(dp1\nS'text'\np2\nS'string'\np3\nsS'none'\np4\nNsS'boolean'\np5\nI01\nsS'number'\np6\nF3.4399999999999999\nsS'int_list'\np7\n(lp8\nI1\naI2\naI3\nas." |
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
pickle.dumps(simple, protocol=pickle.HIGHEST_PROTOCOL) |
22 |
|
23 |
|
24 |
|
25 |
'\x80\x02}q\x01(U\x04textq\x02U\x06stringq\x03U\x04noneq\x04NU\x07boolean\x88U\x06numberq\x05G@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x06(K\x01K\x02K\x03eu.' |
A representação binária parece maior, mas é uma ilusão devido sua apresentação. Ao salvar em um arquivo, o protocolo textual é 130 bytes, enquanto o binário só 85 bytes.
1 |
pickle.dump(simple, open('simple1.pkl', 'w')) |
2 |
|
3 |
|
4 |
|
5 |
pickle.dump(simple, open('simple2.pkl', 'wb'), protocol=pickle.HIGHEST_PROTOCOL) |
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
ls -la sim*.* |
14 |
|
15 |
|
16 |
|
17 |
-rw-r--r-- 1 gigi staff 130 Mar 9 02:42 simple1.pkl |
18 |
|
19 |
|
20 |
|
21 |
-rw-r--r-- 1 gigi staff 85 Mar 9 02:43 simple2.pkl |
Deserialização é tão fácil quanto:
1 |
x = pickle.loads("(dp1\nS'text'\np2\nS'string'\np3\nsS'none'\np4\nNsS'boolean'\np5\nI01\nsS'number'\np6\nF3.4399999999999999\nsS'int_list'\np7\n(lp8\nI1\naI2\naI3\nas.") |
2 |
|
3 |
|
4 |
|
5 |
assert x == simple |
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
x = pickle.loads('\x80\x02}q\x01(U\x04textq\x02U\x06stringq\x03U\x04noneq\x04NU\x07boolean\x88U\x06numberq\x05G@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x06(K\x01K\x02K\x03eu.') |
14 |
|
15 |
|
16 |
|
17 |
assert x == simple |
Note que o pickle descobre automaticamente o protocolo. Não é preciso especificá-lo, nem mesmo para o binário.
Deserializar de um arquivo é tão fácil quanto. Basta prover um arquivo aberto.
1 |
x = pickle.load(open('simple1.pkl')) |
2 |
|
3 |
|
4 |
|
5 |
assert x == simple |
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
x = pickle.load(open('simple2.pkl')) |
14 |
|
15 |
|
16 |
|
17 |
assert x == simple |
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
x = pickle.load(open('simple2.pkl', 'rb')) |
26 |
|
27 |
|
28 |
|
29 |
assert x == simple |
De acordo com a documentação, é preciso abrir arquivos binários do pickle no modo 'rb', mas, como se vê, funciona do mesmo jeito.
Vejamos como o pickle lida com grafos de objeto complexos.
1 |
pickle.dumps(complex) |
2 |
|
3 |
|
4 |
|
5 |
"(dp1\nS'a'\nccopy_reg\n_reconstructor\np2\n(c__main__\nA\np3\nc__builtin__\nobject\np4\nNtRp5\n(dp6\nS'simple'\np7\n(dp8\nS'text'\np9\nS'string'\np10\nsS'none'\np11\nNsS'boolean'\np12\nI01\nsS'number'\np13\nF3.4399999999999999\nsS'int_list'\np14\n(lp15\nI1\naI2\naI3\nassbsS'when'\np16\ncdatetime\ndatetime\np17\n(S'\\x07\\xe0\\x03\\x07\\x00\\x00\\x00\\x00\\x00\\x00'\ntRp18\ns." |
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
pickle.dumps(complex, protocol=pickle.HIGHEST_PROTOCOL) |
14 |
|
15 |
|
16 |
|
17 |
'\x80\x02}q\x01(U\x01ac__main__\nA\nq\x02)\x81q\x03}q\x04U\x06simpleq\x05}q\x06(U\x04textq\x07U\x06stringq\x08U\x04noneq\tNU\x07boolean\x88U\x06numberq\nG@\x0b\x85\x1e\xb8Q\xeb\x85U\x08int_list]q\x0b(K\x01K\x02K\x03eusbU\x04whenq\x0ccdatetime\ndatetime\nq\rU\n\x07\xe0\x03\x07\x00\x00\x00\x00\x00\x00\x85Rq\x0eu.' |
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
pickle.dump(complex, open('complex1.pkl', 'w')) |
26 |
|
27 |
|
28 |
|
29 |
pickle.dump(complex, open('complex2.pkl', 'wb'), protocol=pickle.HIGHEST_PROTOCOL) |
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
ls -la comp*.* |
38 |
|
39 |
|
40 |
|
41 |
-rw-r--r-- 1 gigi staff 327 Mar 9 02:58 complex1.pkl |
42 |
|
43 |
|
44 |
|
45 |
-rw-r--r-- 1 gigi staff 171 Mar 9 02:58 complex2.pkl |
A eficiência do protocolo binário é ainda maior com grafos de objetos complexos.
JSON
JSON (JavaScript Object Notation) faz parte da biblioteca padrão do Python desde a versão 2.5. Consideramo-no um formato nativo, agora. É um formato baseado em texto e é o rei não oficial da web, em termos de serialização de objetos. Seu sistema de tipos imita o do JavaScript, logo é bem limitado.
Serializemos e deserializemos um grafo de objeto simples e um complexo e vejamos o que acontece. A interface é quase idêntica à interface do pickle. Temos dump(), dumps(), load() e loads(). Mas, não há protocolos a se selecionar, e há muitos argumentos opcionais para controlar o processo. Comecemos devagar, salvando um grafo de objeto simples sem quaisquer argumentos especiais:
1 |
import json |
2 |
|
3 |
|
4 |
|
5 |
print json.dumps(simple) |
6 |
|
7 |
|
8 |
|
9 |
{"text": "string", "none": null, "boolean": true, "number": 3.44, "int_list": [1, 2, 3]} |
O resultado parece bem legível, mas não há identação. Em um grande grafo de objeto, pode ser problemático. Identemos o retorno:
1 |
print json.dumps(simple, indent=4) |
2 |
|
3 |
|
4 |
|
5 |
{
|
6 |
|
7 |
|
8 |
|
9 |
"text": "string", |
10 |
|
11 |
|
12 |
|
13 |
"none": null, |
14 |
|
15 |
|
16 |
|
17 |
"boolean": true, |
18 |
|
19 |
|
20 |
|
21 |
"number": 3.44, |
22 |
|
23 |
|
24 |
|
25 |
"int_list": [ |
26 |
|
27 |
|
28 |
|
29 |
1, |
30 |
|
31 |
|
32 |
|
33 |
2, |
34 |
|
35 |
|
36 |
|
37 |
3
|
38 |
|
39 |
|
40 |
|
41 |
]
|
42 |
|
43 |
|
44 |
|
45 |
}
|
Muito melhor. Sigamos para o grafo de objeto complexo.
1 |
json.dumps(complex) |
2 |
|
3 |
|
4 |
|
5 |
---------------------------------------------------------------------------
|
6 |
|
7 |
|
8 |
|
9 |
TypeError Traceback (most recent call last) |
10 |
|
11 |
|
12 |
|
13 |
<ipython-input-19-1be2d89d5d0d> in <module>() |
14 |
|
15 |
|
16 |
|
17 |
----> 1 json.dumps(complex) |
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
/usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.pyc in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, encoding, default, sort_keys, **kw) |
26 |
|
27 |
|
28 |
|
29 |
241 cls is None and indent is None and separators is None and |
30 |
|
31 |
|
32 |
|
33 |
242 encoding == 'utf-8' and default is None and not sort_keys and not kw): |
34 |
|
35 |
|
36 |
|
37 |
--> 243 return _default_encoder.encode(obj) |
38 |
|
39 |
|
40 |
|
41 |
244 if cls is None: |
42 |
|
43 |
|
44 |
|
45 |
245 cls = JSONEncoder |
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
/usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in encode(self, o) |
54 |
|
55 |
|
56 |
|
57 |
205 # exceptions aren't as detailed. The list call should be roughly |
58 |
|
59 |
|
60 |
|
61 |
206 # equivalent to the PySequence_Fast that ''.join() would do. |
62 |
|
63 |
|
64 |
|
65 |
--> 207 chunks = self.iterencode(o, _one_shot=True) |
66 |
|
67 |
|
68 |
|
69 |
208 if not isinstance(chunks, (list, tuple)): |
70 |
|
71 |
|
72 |
|
73 |
209 chunks = list(chunks) |
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 |
|
81 |
/usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in iterencode(self, o, _one_shot) |
82 |
|
83 |
|
84 |
|
85 |
268 self.key_separator, self.item_separator, self.sort_keys, |
86 |
|
87 |
|
88 |
|
89 |
269 self.skipkeys, _one_shot) |
90 |
|
91 |
|
92 |
|
93 |
--> 270 return _iterencode(o, 0) |
94 |
|
95 |
|
96 |
|
97 |
271
|
98 |
|
99 |
|
100 |
|
101 |
272 def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, |
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
/usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc in default(self, o) |
110 |
|
111 |
|
112 |
|
113 |
182
|
114 |
|
115 |
|
116 |
|
117 |
183 """ |
118 |
|
119 |
|
120 |
|
121 |
--> 184 raise TypeError(repr(o) + " is not JSON serializable") |
122 |
|
123 |
|
124 |
|
125 |
185
|
126 |
|
127 |
|
128 |
|
129 |
186 def encode(self, o):
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
TypeError: <__main__.A object at 0x10f367cd0> is not JSON serializable
|
Oh! Isso não é bom! O que aconteceu? O erro diz que o objeto A não é serialização via JSON. Lembre que JSON tem um sistema de tipos bem simples e não serializa classes criadas por usuários automaticamente. Para resolver, é preciso uma subclasse de JSONEncoder, usada pelo módulo json, e implementar default(), que é chamado sempre que um codificador se depara com um objeto que não consegue serializar.
O codificador customizado converte em um grafo de objeto Python possível de codificar pelo codificador JSON. Nesse caso, temos dois objetos que requerem codificação especial: datetime e A. O codificador a seguir resolve. Cada objeto especial é converto em um dict onde a chave é o nome do tipo envolto em dunders (sublinhados duplos). Isso será importante para decodificação.
1 |
from datetime import datetime |
2 |
|
3 |
|
4 |
|
5 |
import json |
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
class CustomEncoder(json.JSONEncoder): |
18 |
|
19 |
|
20 |
|
21 |
def default(self, o): |
22 |
|
23 |
|
24 |
|
25 |
if isinstance(o, datetime): |
26 |
|
27 |
|
28 |
|
29 |
return {'__datetime__': o.replace(microsecond=0).isoformat()}
|
30 |
|
31 |
|
32 |
|
33 |
return {'__{}__'.format(o.__class__.__name__): o.__dict__}
|
Tentemos novamente nosso codificador customizado:
1 |
serialized = json.dumps(complex, indent=4, cls=CustomEncoder) |
2 |
|
3 |
|
4 |
|
5 |
print serialized |
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
{
|
14 |
|
15 |
|
16 |
|
17 |
"a": { |
18 |
|
19 |
|
20 |
|
21 |
"__A__": { |
22 |
|
23 |
|
24 |
|
25 |
"simple": { |
26 |
|
27 |
|
28 |
|
29 |
"text": "string", |
30 |
|
31 |
|
32 |
|
33 |
"none": null, |
34 |
|
35 |
|
36 |
|
37 |
"boolean": true, |
38 |
|
39 |
|
40 |
|
41 |
"number": 3.44, |
42 |
|
43 |
|
44 |
|
45 |
"int_list": [ |
46 |
|
47 |
|
48 |
|
49 |
1, |
50 |
|
51 |
|
52 |
|
53 |
2, |
54 |
|
55 |
|
56 |
|
57 |
3
|
58 |
|
59 |
|
60 |
|
61 |
]
|
62 |
|
63 |
|
64 |
|
65 |
}
|
66 |
|
67 |
|
68 |
|
69 |
}
|
70 |
|
71 |
|
72 |
|
73 |
},
|
74 |
|
75 |
|
76 |
|
77 |
"when": { |
78 |
|
79 |
|
80 |
|
81 |
"__datetime__": "2016-03-07T00:00:00" |
82 |
|
83 |
|
84 |
|
85 |
}
|
86 |
|
87 |
|
88 |
|
89 |
}
|
Agora, sim. O grafo de objeto complexo foi serializado apropriadamente e a informação original de tipo dos componentes foi mantida nas chaves: "__A__" e "__datetime__". Se usarmos dunders nos nossos nomes, é preciso encontrar uma outra convenção para denotar tipos especiais.
Decodifiquemos o grafo de objeto complexo.
1 |
> deserialized = json.loads(serialized) |
2 |
|
3 |
|
4 |
|
5 |
> deserialized == complex |
6 |
|
7 |
|
8 |
|
9 |
False
|
Humm, a deserialização funcionou (sem erros), mas está diferente do grafo de objeto complexo que serializamos. Algo está errado. Vejamos o grafo de objeto deserializado. Usaremos a função pprint do modulo pprint para visualizr melhor.
1 |
> from pprint import pprint |
2 |
|
3 |
|
4 |
|
5 |
> pprint(deserialized) |
6 |
|
7 |
|
8 |
|
9 |
{u'a': {u'__A__': {u'simple': {u'boolean': True, |
10 |
|
11 |
|
12 |
|
13 |
u'int_list': [1, 2, 3], |
14 |
|
15 |
|
16 |
|
17 |
u'none': None, |
18 |
|
19 |
|
20 |
|
21 |
u'number': 3.44, |
22 |
|
23 |
|
24 |
|
25 |
u'text': u'string'}}}, |
26 |
|
27 |
|
28 |
|
29 |
u'when': {u'__datetime__': u'2016-03-07T00:00:00'}} |
Ok. O problema é que o módulo JSON disconhece a classe A ou mesmo o objeto padrão datetime. Simplesmente deserializa tudo por padrão no objeto Python que casa com seu sistema de tipo. Para obter o grafo de objeto Python original, precisamos de decodificação customizada.
Não é necessária sublasse decodificador customizada. Os load() e load() provêem o parâmetro "object_hook" que permite passar uma função customizada que converte os dicionários em objetos.
1 |
def decode_object(o): |
2 |
|
3 |
|
4 |
|
5 |
if '__A__' in o: |
6 |
|
7 |
|
8 |
|
9 |
a = A() |
10 |
|
11 |
|
12 |
|
13 |
a.__dict__.update(o['__A__']) |
14 |
|
15 |
|
16 |
|
17 |
return a |
18 |
|
19 |
|
20 |
|
21 |
elif '__datetime__' in o: |
22 |
|
23 |
|
24 |
|
25 |
return datetime.strptime(o['__datetime__'], '%Y-%m-%dT%H:%M:%S') |
26 |
|
27 |
|
28 |
|
29 |
return o |
Decodifiquemos usando a função decode_object() como o parâmetro 'object_hook' de loads().
1 |
> deserialized = json.loads(serialized, object_hook=decode_object) |
2 |
|
3 |
|
4 |
|
5 |
> print deserialized |
6 |
|
7 |
|
8 |
|
9 |
{u'a': <__main__.A object at 0x10d984790>, u'when': datetime.datetime(2016, 3, 7, 0, 0)} |
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
> deserialized == complex |
18 |
|
19 |
|
20 |
|
21 |
True
|
Conclusão
Nessa primeira parte, aprendemos sobre o conceito geral de serialização e deserialização de objetos Python e exploramos tudo da serialiação com Pickle e JSON.
Na parte dois, veremos YAML, performance e segurança, além de resenha dos esquemas adicionais de serialização.



