pickle - Serializzazione di oggetti

Scopo: Serializzazione di oggetti

Il modulo pickle implementa un algoritmo per trasformare un oggetto arbitrario Python in un una serie di byte. Questo processo viene anche detto serializzazione dell'oggetto). Il flusso di byte che rappresenta l'oggetto può essere trasmesso o conservato, e successivamente ricostruito per creare un nuovo oggetto con le stesse caratteristiche (deserializzazione) .

La documentazione per pickle specifica chiaramente che viene offerto senza alcuna garanzia di sicurezza. In effetti l'operazione di deserializzazione può eseguire codice arbitrario. Si raccomanda prudenza nell'utilizzo di pickle per comunicazione o conservazione di dati tra processi, e non ci si fidi di dati che non possono essere verificati come sicuri. Si veda il modulo hmac per un esempio di un modo sicuro di verificare la fonte di un dato in formato pickle

Codificare e Decodificare Dati in Stringhe

Il primo esempio usa dumps() per codificare una struttura dati come stringa, quindi stampa la stringa verso la console. Usa una struttura dati composta internamente di tipi built-in. Le istanze di qualsiasi classe possono essere serializzate con pickle, come verrà in seguito illustrato con un esempio.

# pickle_string.py

import pickle
import pprint

data = [{'a': 'A', 'b': 2, 'c': 3.0}]
print('DATI:', end=' ')
pprint.pprint(data)

data_string = pickle.dumps(data)
print('PICKLE: {!r}'.format(data_string))

In modalità predefinita, i dati verrano scritti nel formato binario maggiormente compatibile quando occorre condividerli tra programmi Python 3.

$ python3 pickle_string.py

DATI: [{'a': 'A', 'b': 2, 'c': 3.0}]
PICKLE: b'\x80\x04\x95#\x00\x00\x00\x00\x00\x00\x00]\x94}\x94(\x8c\x01a\x94\x8c\x01A\x94\x8c\x01b\x94K\x02\x8c\x01c\x94G@\x08\x00\x00\x00\x00\x00\x00ua.'

Una volta che i datai sono serializzati, possono essere scritti in un file, socket , pipe ecc. Successivamente si può leggere il file e recuperare i dati per costruire un nuovo oggetto con gli stessi valori.

# pickle_unpickle.py

import pickle
import pprint

data1 = [{'a': 'A', 'b': 2, 'c': 3.0}]
print('PRIMA  : ', end=' ')
pprint.pprint(data1)

data1_string = pickle.dumps(data1)

data2 = pickle.loads(data1_string)
print('DOPO   : ', end=' ')
pprint.pprint(data2)

print('STESSI?:', (data1 is data2))
print('UGUALI?:', (data1 == data2))

il nuovo oggetto costruito è uguale ma non è lo stesso oggetto originale.

$ python3 pickle_unpickle.py

PRIMA  :  [{'a': 'A', 'b': 2, 'c': 3.0}]
DOPO   :  [{'a': 'A', 'b': 2, 'c': 3.0}]
STESSI?: False
UGUALI?: True

Lavorare con Flussi

Oltre a dumps() e loads(), pickle fornisce un paio di funzioni di convenienza per lavorare con flussi tipo file. E' possibile scrivere oggetti multipli verso un flusso, quindi leggerli da esso senza sapere in anticipo quanti oggetti sono stati scritti o quanto grandi essi siano.

# pickle_stream.py

import io
import pickle
import pprint


class SimpleObject:

    def __init__(self, name):
        self.name = name
        self.name_backwards = name[::-1]
        return


data = []
data.append(SimpleObject('pickle'))
data.append(SimpleObject('preserva'))
data.append(SimpleObject('ultimo'))

# Simula a file.
out_s = io.BytesIO()

# Scrive verso lo stream
for o in data:
    print('IN SCRITTURA : {} ({})'.format(o.name, o.name_backwards))
    pickle.dump(o, out_s)
    out_s.flush()

# Imposta uno stream leggibile
in_s = io.BytesIO(out_s.getvalue())

# Legge i dati
while True:
    try:
        o = pickle.load(in_s)
    except EOFError:
        break
    else:
        print('LETTURA      : {} ({})'.format(
            o.name, o.name_backwards))

L'esempio simula dei flussi usando due buffer BytesIO. Il primo riceve gli oggetti serializzati e il suo valore viene passato al secondo dal quale legge load(). Anche un semplice formato di database potrebbe usare questo sistema per conservare i dati. Il modulo shelve rappresenta questo tipo di implementazione.

$ python3 pickle_stream.py

IN SCRITTURA : pickle (elkcip)
IN SCRITTURA : preserva (avreserp)
IN SCRITTURA : ultimo (omitlu)
LETTURA      : pickle (elkcip)
LETTURA      : preserva (avreserp)
LETTURA      : ultimo (omitlu)

Oltre alla conservazione di dati, gli oggetti serializzati con pickle sono molto comodi per comunicazioni tra processi. Ad esempio usando os.fork() ed os.pipe() si possono stabilire degli elaboratori di richieste che leggono delle istruzioni da elaborare da una pipe e scrivono i risultati in un'altra pipe. Il codice base per la gestione del gruppo di elaboratori di richieste e per l'invio delle istruzioni e la ricezione delle risposte può essere riusato, visto che gli oggetti delle istruzioni e risposta non devono essere di una classe particolare. Se si stanno usando pipe o socket, non ci si deve dimenticare di eseguire uno svuotamento dopo avere disposto ogni oggetto, per spingere i dati attraverso la connessione verso l'altro estremo. Si veda il modulo multiprocessing se non si vuole scrivere il proprio gestore del gruppo di elaboratori di richieste.

Problemi nella Ricostruzione degli Oggetti

Quando si lavora con le proprie classi, ci si deve assicurare che la classe che si vuole serializzare appaia nello spazio dei nomi del processo che sta leggendo il pickle. Solo i dati per quell'istanza vengono trattati, non la definizione della classe. Il nome della classe viene usato per trovare il costruttore per creare il nuovo oggetto quando viene deserializzato. L'esempio seguente scrive delle istanze di una classe verso un file.

# pickle_dump_to_file_1.py

import pickle
import sys


class SimpleObject:

    def __init__(self, name):
        self.name = name
        l = list(name)
        l.reverse()
        self.name_backwards = ''.join(l)


if __name__ == '__main__':
    data = []
    data.append(SimpleObject('pickle'))
    data.append(SimpleObject('preserva'))
    data.append(SimpleObject('ultimko'))

    filename = sys.argv[1]

    with open(filename, 'wb') as out_s:
        for o in data:
            print('IN SCRITTURA: {} ({})'.format(
                o.name, o.name_backwards))
            pickle.dump(o, out_s)

Quando viene eseguito, lo script crea un file il cui nome ò quello passato come argomento da riga di comando.

$ python3 pickle_load_from_file_1.py test.dat

Traceback (most recent call last):
  File "pickle_load_from_file_1.py", line 12, in <module>
    o = pickle.load(in_s)
AttributeError: Can't get attribute 'SimpleObject' on <module '__main__' from 'pickle_load_from_file_1.py'>

Un tentativo semplicistico di caricare gli oggetti serializzati risultanti fallirebbe.

# pickle_load_from_file_1.py

import pickle
import pprint
import sys

filename = sys.argv[1]

with open(filename, 'rb') as in_s:
    while True:
        try:
            o = pickle.load(in_s)
        except EOFError:
            break
        else:
            print('LETTI: {} ({})'.format(
                o.name, o.name_backwards))

Questa versione fallisce perchò non ò disponibile alcuna classe SimpleObject.

$ python3 pickle_load_from_file_1.py test.dat

Traceback (most recent call last):
  File "pickle_load_from_file_1.py", line 12, in <module>
    o = pickle.load(in_s)
AttributeError: Can't get attribute 'SimpleObject' on <module '__main__' from 'pickle_load_from_file_1.py'>

La versione corretta, che importa SimpleObject dallo script originale, ha successo. L'aggiunta dell'istruzione di importazione alla file dell'elenco delle risorse importate consente allo script di trovare la classe e costruire l'oggetto.

from pickle_dump_to_file_1 import SimpleObject

L'esecuzione dello script modificato ora produce il risultato atteso.

$ python3 pickle_dump_to_file_2.py test.dat

  File "pickle_dump_to_file_2.py", line 29
    print 'LETTURA: %s (%s)' % (o.name, o.name_backwards)
          ^
SyntaxError: invalid syntax

Oggetti non Serializzabili

Non tutti gli oggetti possono essere serializzati da pickle socket, handle di file, connessioni a database e altri oggetti con uno stato a livello di esecuzione che dipende dal sistema operativo o da un altro processo potrebbero essere impossibili da salvare in un modo efficace. Gli oggetti che hanno attributi che non possono essere elaborati da pickle possono definire __getstate__() e __setstate__() per restituire un sottoinsieme dello stato dell'istanza da serializzare.

Il metodo __getstate__() deve ritornare un oggetto che contenga lo stato interno dell'oggetto. Un comodo metodo per rappresentare questo stato è con un dizionario, ma il valore può essere un qualunque oggetto che possa essere serializzato. Lo stato viene conservato, quindi passato a __setstate()__ quando l'oggetto viene caricato per la deserializzazione.

# pickle_state.py

import pickle


class State:

    def __init__(self, name):
        self.name = name

    def __repr__(self):
        return 'State({!r})'.format(self.__dict__)


class MyClass:

    def __init__(self, name):
        print('MyClass.__init__({})'.format(name))
        self._set_name(name)

    def _set_name(self, name):
        self.name = name
        self.computed = name[::-1]

    def __repr__(self):
        return 'MyClass({!r}) (calcolato={!r})'.format(
            self.name, self.computed)

    def __getstate__(self):
        state = State(self.name)
        print('__getstate__ -> {!r}'.format(state))
        return state

    def __setstate__(self, state):
        print('__setstate__({!r})'.format(state))
        self._set_name(state.name)


inst = MyClass('qui il nome')
print('Prima:', inst)

dumped = pickle.dumps(inst)

reloaded = pickle.loads(dumped)
print('Dopo :', reloaded)

Questo esempio usa un oggetto State separato per mantenere lo stato interno di MyClass. Quando un'istanza di MyClass viene caricata da un elemento serializzato da pickle, __setstate__() viene passato all'istanza di State che lo usa per inizializzare l'oggetto.

$ python3  pickle_state.py

MyClass.__init__(qui il nome)
Prima: MyClass('qui il nome') (calcolato='emon li iuq')
__getstate__ -> State({'name': 'qui il nome'})
__setstate__(State({'name': 'qui il nome'}))
Dopo : MyClass('qui il nome') (calcolato='emon li iuq')
Se il valore restituito è False, __setstate__() non viene chiamato quando l'oggetto viene deserializzato.

Riferimenti Circolari

Il protocollo di pickle gestisce automaticamente i riferimenti circolari tra gli oggetti, quindi non ci si deve preoccupare di fare qualcosa di speciale con complesse strutture di dati. Si consideri il digrafo seguente. Include parecchi cicli, tuttavia la struttura corretta può essere serializzata, quindi deserializzata.

esempio di digraph
Serializzazione di una struttura dati con cicli
.

Sebbene il grafico includa diversi cicli, la struttura corretta può essere serializzata e successivamente ricaricata.

# pickle_cycle.py

import pickle


class Node:
    """Un semplice digrafo
    """
    def __init__(self, name):
        self.name = name
        self.connections = []

    def add_edge(self, node):
        "Crea un collegamento tra questo nodo e gli altri."
        self.connections.append(node)

    def __iter__(self):
        return iter(self.connections)


def preorder_traversal(root, seen=None, parent=None):
    """Generatore che fornisce i collegamenti in un grafo.
    """
    if seen is None:
        seen = set()
    yield (parent, root)
    if root in seen:
        return
    seen.add(root)
    for node in root:
        recurse = preorder_traversal(node, seen, root)
        for parent, subnode in recurse:
            yield (parent, subnode)


def show_edges(root):
    "Stampa tutti i collegamenti nel grafo."
    for parent, child in preorder_traversal(root):
        if not parent:
            continue
        print('{:>5} -> {:>2} ({})'.format(
            parent.name, child.name, id(child)))


# Imposta i nodi.
root = Node('root')
a = Node('a')
b = Node('b')
c = Node('c')

# Aggiunge i collegamenti tra i nodi.
root.add_edge(a)
root.add_edge(b)
a.add_edge(b)
b.add_edge(a)
b.add_edge(c)
a.add_edge(a)

print('GRAFO ORIGINALE :')
show_edges(root)

# Serializza e deserializza il grafo per creare
# un nuovo insieme di nodi.
dumped = pickle.dumps(root)
reloaded = pickle.loads(dumped)

print('\nGRAFO RICARICATO:')
show_edges(reloaded)

I nodi ricaricati non sono lo stesso oggetto, ma la relazione tra i nodi ò mantenuta e viene ricaricata solo una copia dell'oggetto con riferimenti multipli. Entrambe queste affermazioni possono essere verificate esaminando i valori di id() dei nodi, prima e dopo il passaggio di serializzazione e deserializzazione.

$ python3  pickle_cycle.py

GRAFO ORIGINALE :
 root ->  a (140450602566416)
    a ->  b (140450601802720)
    b ->  a (140450602566416)
    b ->  c (140450601633584)
    a ->  a (140450602566416)
 root ->  b (140450601802720)

GRAFO RICARICATO:
 root ->  a (140450601127072)
    a ->  b (140450600563760)
    b ->  a (140450601127072)
    b ->  c (140450600565056)
    a ->  a (140450601127072)
 root ->  b (140450600563760)

Vedere anche:

pickle
La documentazione della libreria standard per questo modulo.
PEP 3154
Il protocollo pickle versione 4
shelve
Il modulo shelve
Pickle: An Interesting stack language
di Alexandre Vassalotti