Python Python

Când ar trebui să folosești pickle în loc de JSON în Python

Dima Aug 30, 2026

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 schimb TypeError: 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.dumps primește un cârlig default=, iar json.loads un object_hook. Ele centralizează conversia pentru un tip în toată baza de cod, în loc să o repete în fiecare to_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.loads rulează 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 și from_json_dict, ca cititorul să decidă ce clasă construiește.

Trebuie să fii autentificat pentru a accesa laboratorul cloud.

Autentifică-te