Python
Când ar trebui să folosești pickle în loc de JSON în Python
Pe scurt
Folosește pickle doar pentru date produse de propriul tău program, care rămân în interiorul unei granițe pe care o controlezi — o memorie cache locală, argumente predate unui proces de lucru. El restaurează exact obiecte Python: clase proprii, mulțimi, tupluri, datetime-uri, grafuri de obiecte cu cicluri, dintre care niciunul nu supraviețuiește unui dus-întors prin JSON. Prețul este că pickle.loads nu analizează octeții, ci îi execută ca instrucțiuni, deci încărcarea unei încărcături dintr-o sursă neîncrezătoare rulează orice spune acea încărcătură să ruleze. Pentru orice traversează o graniță de încredere, de limbaj sau de versiune, folosește JSON cu o conversie explicită, scrisă de tine.
Cum funcționează
JSON este un format de date cu șase tipuri: obiect, tablou, șir, număr, boolean, null. Serializarea către el înseamnă proiectarea Python-ului pe acea mulțime mică, iar orice nu se potrivește trebuie convertit înainte sau respins. Încărcarea returnează dicționare, liste și șiruri simple — clasele au dispărut. Două distincții din Python dispar tăcut în proces: un tuplu se întoarce ca listă, iar o cheie de dicționar care nu este șir se întoarce ca șir.
pickle nu este un format de date. Octeții sunt instrucțiuni pentru o mașină cu stivă mică, ce rulează în interiorul lui pickle.loads: pune această valoare pe stivă, caută această clasă după nume, apeleaz-o cu aceste argumente, setează aceste atribute. Încărcarea unui pickle înseamnă rularea acelui program. Exact de aceea poate reconstrui orice obiect — nu descrie obiectul, ci reia apelurile care îl construiesc.
Acel mecanism este și toată problema de securitate. „Apelează acest obiect apelabil cu aceste argumente" este o singură instrucțiune și nu îi pasă dacă obiectul apelabil este clasa ta sau altceva de pe mașină. Orice clasă poate dicta ce spune acea instrucțiune, definind __reduce__. Deci pickle.loads pe octeți pe care nu i-ai creat tu rulează cod ales de cine a scris octeții — motiv pentru care regula este absolută, nu o chestiune de grijă: nu deserializa niciodată date dintr-o sursă neîncrezătoare și tratează „neîncrezătoare" ca incluzând orice a traversat o rețea sau un disc pe care nu îl controlezi în exclusivitate.
Există o a doua cuplare, mai discretă. Un pickle stochează o referință către o clasă — calea ei de modul și numele — nu clasa însăși. Deserializarea importă acel modul și caută numele, deci programul care citește are nevoie de același cod pe care îl avea programul care a scris. Redenumește clasa, mut-o în alt modul sau schimbă-i constructorul, iar pickle-urile de ieri încetează să se mai încarce. JSON nu are o astfel de dependență, ceea ce îl face exact portabil între limbaje și între versiuni ale propriului tău cod.
Decizia decurge din aceste două proprietăți. În interiorul unui singur program de încredere, cu o singură bază de cod — un calcul memorat pe disc, obiecte trimise unui proces de lucru multiprocessing — pickle este instrumentul potrivit, iar costurile lui nu se aplică. În clipa în care datele sunt stocate pentru mai târziu, trimise altui serviciu sau citite de ceva ce nu ai scris tu, răspunsul este JSON plus o mapare explicită.
Cum arată la rulare
Creează un fișier:
touch manifest_store.py
Rulează-l cu:
python3 manifest_store.py
Fiecare pas este un program complet; înlocuiește fișierul de fiecare dată.
Ce pierde JSON și ce păstrează pickle
Un manifest care conține tipurile Python pentru care JSON nu are nicio reprezentare.
import json
import pickle
from datetime import datetime, timezone
manifest = {
"legs": (1, 2),
"docks": {"dock-7", "dock-9"},
"received_at": datetime(2026, 3, 14, 9, 30, tzinfo=timezone.utc),
}
try:
json.dumps(manifest)
except TypeError as exc:
print("json.dumps ->", exc)
restored = pickle.loads(pickle.dumps(manifest))
print("pickle round trip equal:", restored == manifest)
print(" legs stayed a", type(restored["legs"]).__name__)
print(" docks stayed a", type(restored["docks"]).__name__)
plain = {"legs": (1, 2), 7: "dock-7"}
print("json round trip:", json.loads(json.dumps(plain)))
json.dumps -> Object of type set is not JSON serializable
pickle round trip equal: True
legs stayed a tuple
docks stayed a set
json round trip: {'legs': [1, 2], '7': 'dock-7'}
JSON refuză mulțimea din start — un eșec zgomotos și cinstit. Pickle restaurează exact fiecare tip, ceea ce este capacitatea pentru care se plătește. Ultima linie este cea de reținut: dicționarul pe care JSON l-a acceptat s-a întors schimbat. Tuplul este acum o listă, iar cheia întreagă 7 este acum șirul '7', fără nicio eroare în niciun punct. Un dus-întors cu pierderi care nu ridică nimic este mai greu de prins decât unul care ridică.
Încărcarea unui pickle îl rulează
Același apel loads, primind o clasă care definește ce ar trebui să se întâmple când este reconstruită.
import pickle
class Manifest:
def __init__(self, shipment_id):
self.shipment_id = shipment_id
def __reduce__(self):
return (print, (" >>> this ran while pickle.loads() was reading the bytes",))
blob = pickle.dumps(Manifest("SHP-4471"))
print("payload is:", type(blob).__name__)
print("calling pickle.loads()...")
result = pickle.loads(blob)
print("loads returned:", result)
payload is: bytes
calling pickle.loads()...
>>> this ran while pickle.loads() was reading the bytes
loads returned: None
__reduce__ îi spune lui pickle cum să reconstruiască obiectul, numind un obiect apelabil și argumentele lui, iar aici îl numește pe print. Nimic din loads nu inspectează și nu aprobă acea alegere — este calea obișnuită de reconstrucție, folosită de fiecare clasă care are nevoie de restaurare personalizată. Ieșirea dovedește secvența: mesajul apare între linia „calling" și rezultat, iar loads returnează None, pentru că asta a returnat print. O încărcătură construită astfel nu se deosebește de una legitimă până când nu a rulat deja, motiv pentru care nicio validare a octeților dinainte nu face sigură deserializarea din surse neîncrezătoare.
Alternativa explicită
Scrie maparea în ambele direcții. Costă câteva linii și înlătură deodată ambele probleme.
import json
from dataclasses import dataclass
from datetime import datetime, timezone
@dataclass
class ScanEvent:
sku: str
dock: str
received_at: datetime
def to_json_dict(self):
return {
"sku": self.sku,
"dock": self.dock,
"received_at": self.received_at.isoformat(),
}
@classmethod
def from_json_dict(cls, raw):
return cls(
sku=raw["sku"],
dock=raw["dock"],
received_at=datetime.fromisoformat(raw["received_at"]),
)
event = ScanEvent("SKU-9921", "dock-7", datetime(2026, 3, 14, 9, 30, tzinfo=timezone.utc))
wire = json.dumps(event.to_json_dict())
print("on the wire :", wire)
restored = ScanEvent.from_json_dict(json.loads(wire))
print("restored type:", type(restored).__name__)
print("round trip ok:", restored == event)
on the wire : {"sku": "SKU-9921", "dock": "dock-7", "received_at": "2026-03-14T09:30:00+00:00"}
restored type: ScanEvent
round trip ok: True
Formatul de transmisie este text pe care îl poate citi orice limbaj, iar încărcarea lui nu construiește nimic în afară de un dicționar, până când from_json_dict decide ce să construiască — cititorul alege clasa, nu încărcătura. datetime supraviețuiește pentru că isoformat și fromisoformat sunt o pereche explicită, ceea ce este forma generală: pentru fiecare tip pe care JSON nu îl poate purta, alege o reprezentare pe care o poate purta și scrie ambele direcții. @dataclass furnizează __eq__-ul care face comparația finală semnificativă.
Cazuri limită și capcane
- Semnarea unui pickle nu îl face sigur. Un HMAC dovedește că octeții au venit de la cineva care deține cheia; nu spune nimic despre ce fac ei. Dacă se scurge cheia, încărcătura tot se execută. Alege un format care nu poate executa, în loc să autentifici unul care poate.
- Nici pickle nu poate trata orice și pică în două feluri diferite. Un lambda ridică
PicklingError, iar o clasă definită local ridicăAttributeError, pentru că pickle stochează o cale de căutare, iar niciuna nu are una. Fișierele deschise, socketurile, generatoarele și lacătele ridică în schimbTypeError: ele învelesc stare vie, care nu poate fi deloc reconstruită din octeți. - Versiunea protocolului se schimbă. Protocolul implicit urcă odată cu versiunile de Python, iar pickle-urile mai noi sunt ilizibile pentru interpretoare mai vechi, deci fixarea lui
protocol=contează ori de câte ori scriitorul și cititorul nu sunt actualizați împreună. json.dumpsprimește un cârligdefault=, iarjson.loadsunobject_hook. Ele centralizează conversia pentru un tip în toată baza de cod, în loc să o repete în fiecareto_json_dict.- Redenumirile de clase strică pickle-urile stocate. Calea clasei este coaptă în încărcătură, deci refactorizarea unui modul invalidează date aflate deja pe disc — o problemă de migrare pe care JSON pur și simplu nu o are.
Rezumat
Pickle restaurează exact obiecte Python, iar JSON nu, dar pickle realizează asta executând instrucțiuni din încărcătură și codificând fix căile claselor tale în datele stocate. Ambele costuri sunt irelevante în interiorul unui singur program de încredere și descalificante oriunde altundeva, ceea ce face din graniță — nu din comoditate — lucrul care decide.
- Folosește pickle doar pentru date scrise de propriul tău cod și pe care le va citi înapoi cu același cod disponibil: memorii cache locale, argumente pentru
multiprocessing. pickle.loadsrulează un program de mașină cu stivă din octeți, iar__reduce__permite oricărei clase să aleagă ce se apelează, deci intrarea neîncrezătoare înseamnă execuție arbitrară.- Un pickle stochează calea de modul și numele clasei, nu definiția ei, deci redenumirea sau mutarea unei clase strică fiecare pickle deja scris.
- Dus-întorsurile prin JSON pierd informație fără să ridice excepții: tuplurile se întorc ca liste, iar cheile de dicționar care nu sunt șiruri se întorc ca șiruri.
- Pentru orice traversează o graniță de încredere, de limbaj sau de versiune, scrie perechi explicite
to_json_dictșifrom_json_dict, ca cititorul să decidă ce clasă construiește.