Python Python

Ce sunt referințele slabe(weakref) în Python și când sunt necesare?

Dima Aug 30, 2026

Pe scurt

O referință slabă arată către un obiect fără să conteze ca referință la el. Fiecare nume obișnuit, slot de listă și valoare de dicționar incrementează contorul de referințe al țintei și, prin urmare, o ține în viață; un weakref.ref nu, deci obiectul este eliberat imediat ce dispare ultima referință puternică, iar referința slabă se rezolvă atunci la None. Ele sunt necesare ori de câte ori ceva trebuie să observe sau să indexeze obiecte pe care nu are voie să le dețină: o memorie cache care nu trebuie să țină singură intrările vii, un registru de handlere active sau un copil care trebuie să ajungă la părintele lui fără ca perechea să se țină reciproc în viață pe veci.


Cum funcționează

Recuperarea în CPython este condusă de contorul de referințe din antetul fiecărui obiect, iar fiecare legare normală contribuie la el. Acesta este comportamentul pe care îl vrei aproape peste tot — un obiect rămâne viu exact cât timp ceva poate ajunge la el — și este exact greșit pentru structurile de evidență. O memorie cache indexată după identificatorul documentului ar trebui să accelereze accesul la documentele pe care programul deja le folosește, nu să garanteze că fiecare document analizat vreodată rămâne în memorie pe toată durata procesului.

O referință slabă rezolvă asta stocând pointerul fără să atingă contorul. weakref.ref(obj) returnează un obiect apelabil mic; apelarea lui returnează obiectul referit cât timp este viu și None după aceea. Referința slabă însăși nu devine invalidă și nu dispare — rămâne un obiect perfect valabil, care pur și simplu a încetat să se rezolve, motiv pentru care codul verifică întotdeauna rezultatul, în loc să presupună.

Suportul este per tip, iar regula este structurală, nu ține de mutabilitate. Un obiect poate fi referit slab doar dacă tipul lui oferă un slot __weakref__ pentru evidență. Majoritatea containerelor predefinite nu îl oferă: list, dict și tuple refuză toate, la fel și un simplu object(). Clasele obișnuite primesc slotul automat, motiv pentru care instanțele propriilor tale tipuri aproape întotdeauna funcționează — dacă nu cumva clasa declară __slots__ fără să includă __weakref__.

Biblioteca standard construiește trei structuri practice peste asta:
- WeakValueDictionary își ține valorile slab, deci o intrare dispare odată ce nimic altceva nu se mai referă la valoare — memoria cache care se curăță singură.
- WeakKeyDictionary își ține cheile slab, ceea ce atașează metadate obiectelor fără să le țină vii.
- weakref.ref(obj, callback) apelează callback când obiectul referit este eliberat, ceea ce permite registrelor să își evacueze propriile intrări.

Sincronizarea contează pentru callbackuri: obiectul referit a dispărut deja când rulează callbackul, deci callbackul primește referința slabă deja expirată și nu poate recupera sau inspecta obiectul. Orice îi trebuie callbackului trebuie capturat dinainte.


Cum arată la rulare

Creează un fișier:

touch manifest_cache.py

Rulează-l cu:

python3 manifest_cache.py

O referință slabă nu ține obiectul viu

Cel mai mic caz: creezi una, confirmi că nu a schimbat niciun contor, apoi urmărești cum încetează să se rezolve pe măsură ce dispar referințele puternice.

import sys
import weakref


class ParsedManifest:
    def __init__(self, document_id):
        self.document_id = document_id


manifest = ParsedManifest("DOC-4471")

before = sys.getrefcount(manifest) - 1
probe = weakref.ref(manifest)
after = sys.getrefcount(manifest) - 1

print("strong references before the weakref:", before)
print("strong references after the weakref: ", after)
print("the weakref resolves to the object:", probe().document_id)

alias = manifest
del manifest
print("still alive through the other name:", probe() is not None)

del alias
print("after the last strong reference goes:", probe())
print("the weakref object itself still exists:", type(probe).__name__)


strong references before the weakref: 1
strong references after the weakref:  1
the weakref resolves to the object: DOC-4471
still alive through the other name: True
after the last strong reference goes: None
the weakref object itself still exists: ReferenceType


  • Contorul este neschimbat de crearea referinței slabe — acel unic fapt este toată funcționalitatea.
  • Ștergerea lui manifest nu eliberează obiectul, pentru că alias este în continuare o referință puternică. Referințele slabe urmăresc viața obiectului, nu vreun nume anume.
  • Odată ce dispare ultimul nume puternic, obiectul referit este dealocat imediat, iar apelarea sondei returnează None.
  • Sonda este în continuare un obiect ReferenceType viu. Nu a devenit invalidă și nu a ridicat o excepție; pur și simplu a încetat să se rezolve, motiv pentru care convenția de apelare este întotdeauna „apeleaz-o și verifică None".

O memorie cache care nu își ține intrările vii

O dimensiune în plus: cazul practic. Două memorii cache primesc obiecte echivalente, iar doar una dintre ele dă drumul. Înlocuiește fișierul cu:

import weakref


class ParsedManifest:
    def __init__(self, document_id):
        self.document_id = document_id


weak_cache = weakref.WeakValueDictionary()
plain_cache = {}

weak_entry = ParsedManifest("DOC-4471")
plain_entry = ParsedManifest("DOC-4471")
weak_cache["DOC-4471"] = weak_entry
plain_cache["DOC-4471"] = plain_entry

print("while the parser holds them -> weak:", sorted(weak_cache), "| plain:", sorted(plain_cache))

del weak_entry
del plain_entry

print("after the parser releases them -> weak:", sorted(weak_cache), "| plain:", sorted(plain_cache))
print("the plain cache is still holding the object alive:", plain_cache["DOC-4471"].document_id)

still_open = ParsedManifest("DOC-8802")
weak_cache["DOC-8802"] = still_open
print("a document still in use stays cached:", sorted(weak_cache), "->", weak_cache["DOC-8802"].document_id)


while the parser holds them -> weak: ['DOC-4471'] | plain: ['DOC-4471']
after the parser releases them -> weak: [] | plain: ['DOC-4471']
the plain cache is still holding the object alive: DOC-4471
a document still in use stays cached: ['DOC-8802'] -> DOC-8802


Ambele memorii cache au acceptat o intrare și amândouă au raportat-o cât timp analizorul își ținea propria referință. Când acele referințe au dispărut, WeakValueDictionary s-a golit singur, iar dicționarul simplu nu — iar a treia linie dovedește că dicționarul simplu nu doar își amintește o cheie, ci ține tot obiectul viu. Aceea este diferența dintre o memorie cache și o scurgere: un dicționar simplu folosit ca memorie cache nu eliberează niciodată nimic dacă nu evacuează codul explicit, deci crește pe toată durata procesului. Linia finală arată că memoria cache slabă este în continuare utilă pentru ce servește o memorie cache — un document aflat cu adevărat în uz rămâne disponibil.

Notă: cele două memorii cache primesc obiecte separate în mod deliberat. Stocarea unui singur obiect în amândouă ar lăsa memoria cache simplă să îl țină viu, iar intrarea slabă nu ar dispărea niciodată.


Ce poate fi referit slab și ce se întâmplă la eliberare

Ultima treaptă acoperă constrângerea care surprinde lumea și cârligul de callback. Înlocuiește fișierul cu:

import weakref


class ParsedManifest:
    pass


def supports_weakref(value):
    try:
        weakref.ref(value)
        return True
    except TypeError:
        return False


for label, value in (
    ("list", [1]),
    ("dict", {1: 2}),
    ("tuple", (1,)),
    ("object()", object()),
    ("set", {1}),
    ("function", supports_weakref),
    ("class instance", ParsedManifest()),
):
    print(f"{label:15s} weak-referenceable: {supports_weakref(value)}")

evicted = []
tracked = ParsedManifest()
probe = weakref.ref(tracked, lambda dead_ref: evicted.append("callback fired"))

print("callback has not fired yet:", evicted == [])
del tracked
print("after the object died:", evicted)


list            weak-referenceable: False
dict            weak-referenceable: False
tuple           weak-referenceable: False
object()        weak-referenceable: False
set             weak-referenceable: True
function        weak-referenceable: True
class instance  weak-referenceable: True
callback has not fired yet: True
after the object died: ['callback fired']


Tiparul nu este mutabilitatea și nu este „containere față de valori" — list și dict refuză, în timp ce set acceptă, iar un simplu object() refuză, în timp ce o instanță a unei clase goale acceptă. Singura regulă este dacă tipul poartă un slot __weakref__, pe care containerele predefinite obișnuite îl omit ca să economisească pointerul de pe fiecare instanță și pe care clasele obișnuite îl primesc implicit. Consecința practică este că referirea slabă a datelor brute necesită de obicei învelirea lor mai întâi într-o clasă mică. Liniile cu callback completează tabloul: funcția rulează după ce obiectul referit a dispărut, primind referința acum expirată, deci este potrivită pentru evacuare și jurnalizare, nu pentru vreo ultimă privire asupra obiectului.


Cazuri limită și capcane

  • O referință slabă către o metodă legată este expirată din start. obj.method construiește un obiect metodă-legată proaspăt la fiecare acces, deci nimic nu îl ține, iar weakref.ref(obj.method)() este None imediat. Folosește weakref.WeakMethod, care urmărește în schimb instanța de dedesubt — capcana standard la construirea unui registru de callbackuri care nu trebuie să își rețină abonații.
  • WeakKeyDictionary este cealaltă direcție. Își ține cheile slab, ceea ce permite atașarea de metadate unor obiecte pe care maparea nu are voie să le țină vii; intrarea dispare odată cu obiectul-cheie.
  • __slots__ înlătură suportul pentru referințe slabe. O clasă care declară __slots__ fără '__weakref__' în listă ridică TypeError la weakref.ref, ceea ce se introduce ușor când optimizezi o clasă pe care altceva o urmărește slab.
  • Callbackul nu poate vedea obiectul. Până rulează el, obiectul referit este dealocat, deci capturează într-o închidere tot ce îi trebuie callbackului — cum ar fi cheia din memoria cache — chiar când creezi referința slabă.
  • Referințele slabe nu sunt o politică de evacuare a memoriei cache. Un WeakValueDictionary renunță la intrări doar când nimic altceva nu le folosește, ceea ce este o proprietate de corectitudine, nu o limită de dimensiune. Când scopul este plafonarea memoriei, nu evitarea reținerii, un LRU cu dimensiune maximă este instrumentul potrivit.

Rezumat

O referință slabă observă un obiect fără să îl țină viu, rezolvându-se la obiectul referit cât timp trăiește și la None odată ce a dispărut ultima referință puternică. Ele sunt necesare oriunde o structură trebuie să știe despre obiecte pe care nu are voie să le dețină — memorii cache, registre și pointeri înapoi către părinte — iar biblioteca standard împachetează formele obișnuite ca WeakValueDictionary, WeakKeyDictionary și callbackuri de eliberare.

  • Crearea unei referințe slabe nu schimbă contorul de referințe al țintei, deci nu întârzie niciodată dealocarea.
  • Apelarea referinței returnează obiectul sau None; obiectul-referință însuși rămâne valid în ambele cazuri, deci verifică întotdeauna rezultatul.
  • Doar tipurile care poartă un slot __weakref__ pot fi referite slab — clasele obișnuite pot, în timp ce list, dict, tuple și un simplu object() nu pot.
  • WeakValueDictionary oferă o memorie cache care eliberează intrările pe care nimic altceva nu le folosește; un dicționar simplu folosit ca memorie cache reține tot până la evacuarea explicită.
  • weakref.ref(obj, callback) se declanșează după ce obiectul referit a dispărut, deci callbackul poate evacua evidența, dar nu poate inspecta obiectul.
  • O referință slabă către o metodă legată expiră imediat; weakref.WeakMethod este echivalentul funcțional.

You need to be logged in to access the cloud lab.

Log in