1. Code
  2. Python

Serialização e Desserialização de Objetos do Python: Parte 1

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.
Scroll to top

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.