Thursday, August 25, 2016

Serialization and Deserialization of Python Objects: Part 1

Serialization and Deserialization of Python Objects: Part 1

Python object serialization and deserialization is an important aspect of any non-trivial program. If in Python you save something to a file, if you read a configuration file, or if you respond to an HTTP request, you do object serialization and deserialization. 

In one sense, serialization and deserialization are the most boring things in the world. Who cares about all the formats and protocols? You just want to persist or stream some Python objects and get them back later intact. 

This is a very healthy way to look at the world at the conceptual level. But, at the pragmatic level, which serialization scheme, format or protocol you choose may determine how fast your program runs, how secure it is, how much freedom you have to maintain your state, and how well you're going to interoperate with other systems. 

The reason there are so many options is that different circumstances call for different solutions. There is no "one size fits all". In this two-part tutorial I'll go over the pros and cons of the most successful serialization and deserialization schemes, show how to use them, and provide guidelines for choosing between them when faced with a specific use case.

Running Example

In the following sections I'll serialize and deserialize the same Python object graphs using different serializers. To avoid repetition, I'll define these object graphs here.

Simple Object Graph

The simple object graph is a dictionary that contains a list of integers, a string, a float, a boolean, and a None.

Complex Object Graph

The complex object graph is also a dictionary, but it contains a datetime object and user-defined class instance that has a self.simple attribute, which is set to the simple object graph.

Pickle

Pickle is a staple. It is a native Python object serialization format. The pickle interface provides four methods: dump, dumps, load, and loads. The dump() method serializes to an open file (file-like object). The dumps() method serializes to a string. The load() method deserializes from an open file-like object. The loads() method deserializes from a string.

Pickle supports by default a textual protocol, but has also a binary protocol, which is more efficient, but not human-readable (helpful when debugging).

Here is how you pickle a Python object graph to a string and to a file using both protocols.

The binary representation may seem larger, but this is an illusion due to its presentation. When dumping to a file, the textual protocol is 130 bytes, while the binary protocol is only 85 bytes.

Unpickling from a string is as simple as:

Note that pickle can figure out the protocol automatically. There is no need to specify a protocol even for the binary one.

Unpickling from a file is just as easy. You just need to provide an open file.

According to the documentation, you're supposed to open binary pickles using the 'rb' mode, but as you can see it works either way.

Let's see how pickle deals with the complex object graph.

The efficiency of the binary protocol is even greater with complex object graphs.

JSON

JSON (JavaScript Object Notation) has been part of the Python standard library since Python 2.5. I'll consider it a native format at this point. It is a text-based format and is the unofficial king of the web as far as object serialization goes. Its type system naturally models JavaScript, so it is pretty limited. 

Let's serialize and deserialize the simple and complex objects graphs and see what happens. The interface is almost identical to the pickle interface. You have dump(), dumps(), load(), and loads() functions. But, there are no protocols to select, and there are many optional arguments to control the process. Let's start simple by dumping the simple object graph without any special arguments:

The output looks pretty readable, but there is no indentation. For a larger object graph, this can be a problem. Let's indent the output:

That looks much better. Let's move on to the complex object graph.

Whoa! That doesn't look good at all. What happened? The error message is that the A object is not JSON serializable. Remember that JSON has a very limited type system and it can't serialize user defined classes automatically. The way to address it is to subclass the JSONEncoder class used by the json module and implement the default() that is called whenever the JSON encoder runs into an object it can't serialize. 

The job of the custom encoder is to convert it to a Python object graph that the JSON encoder is able to encode. In this case we have two objects that require special encoding: the datetime object and the A class. The following encoder does the job. Each special object is converted to a dict where the key is the name of the type surrounded by dunders (double underscores). This will be important for decoding. 

Let's try again with our custom encoder:

This is beautiful. The complex object graph was serialized properly, and the original type information of the components was retained via the keys: "__A__" and "__datetime__". If you use dunders for your names, then you need to come up with a different convention to denote special types.

Let's decode the complex object graph.

Hmmm, the deserialization worked (no errors), but it is different than the original complex object graph we serialized. Something is wrong. Let's take a look at the deserialized object graph. I'll use the pprint function of the pprint module for pretty printing.

Ok. The problem is that the json module doesn't know anything about the A class or even the standard datetime object. It just deserializes everything by default to the Python object that matches its type system. In order to get back to a rich Python object graph, you need custom decoding. 

There is no need for a custom decoder subclass. The load() and loads() functions provide the "object_hook" parameter that lets you provide a custom function that converts dicts to objects. 

Let's decode using the decode_object() function as a parameter to the loads() object_hook parameter.

Conclusion

In part one of this tutorial, you've learned about the general concept of serialization and deserialization of Python objects and explored the ins and out of serializing Python objects using Pickle and JSON. 

In part two, you'll learn about YAML, performance and security concerns, and a quick review of additional serialization schemes.


No comments:

Post a Comment