pickle e cPickle - Serializzazione di oggetti Python

Scopo Serializzazione di oggetti Python.
Versione Python pickle almeno dalla 1.4, cPickle dalla 1.5

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.

Il modulo cPickle implementa lo stesso algoritmo, in C invece che in Python. E' molte volte più veloce dell'implementazione Python, ma non consente all'utente di derivare classi da Pickle. Se la derivazione di classi non ò importante per le proprie esigenze, ò consigliabile l'uso di cPickle.

La documentazione di pickle mette in chiaro che non offre alcuna garanzia di sicurezza. Prestare attenzione se si usa pickle per comunicazioni tra processi o conservazione di dati. Non fidarsi di dati che non possono essere verificati come sicuri.

Importazione

E' prassi comune tentare di importare cPickle per primo, assegnandogli l'alias di "pickle". Se per una qualche ragione l'importazione fallisce si può successivamente ripiegare verso l'implementazione nativa di Python nel modulo pickle. Questo consente di ottenere l'implementazione più veloce, se disponibile, e l'implementazione portabile altrimenti.

try:
   import cPickle as pickle
except:
   import pickle

Codificare e Decodificare Dati in Stringhe

Il primo esempio di pickle codifica una struttura dati come stringa, quindi stampa la stringa verso la console. Definisce una struttura dati fatta intermente di tipi nativi. Le istanze di qualsiasi classe possono essere serializzate con pickle, come verrà in seguito illustrato con un esempio. Si usa pickle.dumps() per creare una rappresentazione stringa del valore del'oggetto.

try:
    import cPickle as pickle
except:
    import pickle
import pprint

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

data_string = pickle.dumps(data)
print 'PICKLE:', data_string

In modalità predefinita, pickle contiene solo caratteri ASCII. E' anche disponibile un formato binario più efficiente, ma per questi esempi si manterrà la versione ASCII, in quanto più semplice da comprendere nella stampa.

$ python pickle_string.py DATI:[{'a': 'A', 'b': 2, 'c': 3.0}]
PICKLE: (lp1
(dp2
S'a'
S'A'
sS'c'
F3
sS'b'
I2
sa.

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

try:
    import cPickle as pickle
except:
    import pickle
import pprint

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

data1_string = pickle.dumps(data1)

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

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

Come si vede il nuovo oggetto costruito ò uguale ma non ò lo stesso oggetto originale. Nessuna sorpresa qui.

$ python 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 comode funzioni 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.

try:
    import cPickle as pickle
except:
    import pickle
import pprint
from StringIO import StringIO

class SimpleObject(object):

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

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

# Simula un file con StringIO
out_s = StringIO()

# Scrive allo stream
for o in data:
    print 'SCRITTURA: %s (%s)' % (o.name, o.name_backwards)
    pickle.dump(o, out_s)
    out_s.flush()

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

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

L'esempio simula dei flussi usando i buffer di StringIO, quindi occorre usare qualche trucco per stabilire il flusso leggibile. Anche una semplice forma di database potrebbe usare pickle per conservare gli oggetti, anche se usando il modulo shelve potrebbe essere più facile lavorarci.

$ python pickle_stream.py
SCRITTURA: pickle (elkcip)
SCRITTURA: cPickle (elkciPc)
SCRITTURA: ultimo (omitlu)
LETTURA: pickle (elkcip)
LETTURA: cPickle (elkciPc)
LETTURA: ultimo (omitlu)

Oltre alla conservazione di dati, gli oggetti serializzati da pickle sono molto comodi per comunicazioni tra processi. Ad esempio usando os.fork() ed os.pipe() si possono stabilire dei worker process (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 worker e per l'invio di job e la ricezione delle risposte può essere riusato, visto che gli oggetti per il job e la 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 di ogni oggetto, per spingere i dati attraverso la connessione verso l'altro estremo. Vedere multiprocessing se non si vuole scrivere il proprio gestore del gruppo di worker.

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 da pickle, non la definzione della classe. Il nome della classe viene usato per trovare il costruttore per creare il nuovo oggetto quando viene ripristinato. Vedere questo esempio, che scrive delle istanze di una classe verso un file.

try:
    import cPickle as pickle
except:
    import pickle
import sys

class SimpleObject(object):

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

if __name__ == '__main__':
    data = []
    data.append(SimpleObject('pickle'))
    data.append(SimpleObject('cPickle'))
    data.append(SimpleObject('ultimo'))

    try:
        filename = sys.argv[1]
    except IndexError:
        raise RuntimeError('Prego specificare un nome di file come parametro di %s' % sys.argv[0])

    out_s = open(filename, 'wb')
    try:
        # Scrive verso lo stream
        for o in data:
            print 'SCRITTURA: %s (%s)' % (o.name, o.name_backwards)
            pickle.dump(o, out_s)
    finally:
        out_s.close()

Quando viene eseguito lo script, verrà creato un file il cui nome ò quello passato come parametro alla riga comandi:

$ python pickle_load_from_file_1.py test.dat
SCRITTURA: pickle (elkcip)
SCRITTURA: cPickle (elkciPc)
SCRITTURA: ultimo (omitlu)

Un tentativo semplicistico di caricare gli oggetti serializzati da pickle potrebbe essere:

try:
    import cPickle as pickle
except:
    import pickle
import pprint
from StringIO import StringIO
import sys


try:
    filename = sys.argv[1]
except IndexError:
    raise RuntimeError('Prego specificare un nome di file come parametro di %s' % sys.argv[0])

in_s = open(filename, 'rb')
try:
    # Legge i dati
    while True:
        try:
            o = pickle.load(in_s)
        except EOFError:
            break
        else:
            print 'LETTURA: %s (%s)' % (o.name, o.name_backwards)
finally:
    in_s.close()

Questa versione non funziona perchò non ò disponibile alcuna classe SimpleObject:

$ python pickle_load_from_file_1.py test.dat
Traceback (most recent call last):
  File "pickle_load_from_file_1.py", line 23, in 
    o = pickle.load(in_s)
AttributeError: 'module' object has no attribute 'SimpleObject'

Una versione riveduta e corretta, che importa SimpleObject dallo script originale, avrà successo.

Si aggiunge:

from pickle_dump_to_file_1 import SimpleObject

alla fine dell'elenco degli import, poi rieseguendo lo script:

$ python pickle_dump_to_file_2.py test.dat
LETTURA: pickle (elkcip)
LETTURA: cPickle (elkciPc)
LETTURA: ultimo (omitlu)

Ci sono alcune speciali considerazioni quando si serializzano tipi di dati con valori che pickle non può elaborare (socket, handle di file, connessioni a database ecc.). Le classi che usano valori che non possono essere elaborati da pickle possono definire __getstate__() e __setstate__() per restituire un sottoinsieme dello stato dell'istanza da elaborare con pickle. Le classi di nuovo stile possono anche definire __getnewargs__(), che dovrebbe restituire i parametri da passare all'allocatore di memoria della classe (C.__new__())). L'uso di queste caratteristiche ò trattato più dettagliatamente nella documentazione della libreria standard.

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 digraph:

esempio di digraph

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

import pickle

class Node(object):
    """A simple digraph where each node knows about the other nodes
    it leads to.
    """
    def __init__(self, name):
        self.name = name
        self.connections = []
        return

    def add_edge(self, node):
        "Create an edge between this node and the other."
        self.connections.append(node)
        return

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

def preorder_traversal(root, seen=None, parent=None):
    """Generator function to yield the edges via a preorder traversal."""
    if seen is None:
        seen = set()
    yield (parent, root)
    if root in seen:
        return
    seen.add(root)
    for node in root:
        for (parent, subnode) in preorder_traversal(node, seen, root):
            yield (parent, subnode)
    return

def show_edges(root):
    "Print all of the edges in the graph."
    for parent, child in preorder_traversal(root):
        if not parent:
            continue
        print '%5s -> %2s (%s)' % (parent.name, child.name, id(child))

# Set up the nodes.
root = Node('root')
a = Node('a')
b = Node('b')
c = Node('c')

# Add edges between them.
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 'GRAPH ORIGINALE:'
show_edges(root)

# Pickle and unpickle the graph to create
# a new set of nodes.
dumped = pickle.dumps(root)
reloaded = pickle.loads(dumped)

print
print 'GRAPH 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 le affermazioni possono essere verificate esaminando i valori di id() dei nodi, prima e dopo il passaggio tramite pickle.

$ python pickle_cycle.py
GRAPH ORIGINALE:
 root ->  a (3078455148)
    a ->  b (3078466476)
    b ->  a (3078455148)
    b ->  c (3078466508)
    a ->  a (3078455148)
 root ->  b (3078466476)

GRAPH RICARICATO:
 root ->  a (3078466604)
    a ->  b (168491500)
    b ->  a (3078466604)
    b ->  c (168491660)
    a ->  a (3078466604)
 root ->  b (168491500)

Vedere anche:

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