| Scopo | Serializzazione di oggetti Python. |
| Versione Python | pickle almeno dalla 1.4, cPickle dalla 1.5 |
A partire dal 1 gennaio 2021 le versioni 2.x di Python non sono piu' supportate. Ti invito a consultare la corrispondente versione 3.x dell'articolo per le versioni 3.x di Python
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.
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
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
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.
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, ino = 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.
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 :
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: