Python
De ce o valoare implicită mutabilă este evaluată o singură dată în Python
Pe scurt
Expresiile din lista de parametri sunt evaluate o singură dată, la execuția instrucțiunii def — adică la import — nu la fiecare apel. Rezultatele sunt stocate în tuplul __defaults__ al obiectului funcție și reutilizate identic la fiecare apel care omite parametrul. Cu o valoare imuabilă asta nu se observă; cu o listă sau un dicționar, toate apelurile partajează același obiect, iar modificările se acumulează între ele. Remediul standard este None ca valoare implicită și construirea obiectului în corp, unde codul chiar rulează la fiecare apel.
Cum funcționează
def este o instrucțiune executabilă, nu o declarație. Când interpretorul o execută, evaluează expresiile pentru valorile implicite, evaluează adnotările, apoi construiește un obiect funcție și leagă rezultatele de el. Valorile implicite pozițional-sau-cheie ajung în tuplul __defaults__; cele exclusiv-cheie ajung în dicționarul __kwdefaults__. Corpul funcției nu se execută deloc în acest moment — este doar compilat într-un obiect cod.
La apel, mașina de legare a argumentelor completează parametrii lipsă citind din __defaults__. Citește, nu copiază. Parametrul devine încă un nume legat de exact același obiect, la fel ca o atribuire obișnuită. Dacă acel obiect este o listă, batch.append(sku) mută starea partajată vizibilă tuturor apelurilor viitoare.
Motivul pentru care limbajul funcționează așa este consistența cu restul modelului de execuție. Alternativa — reevaluarea expresiei la fiecare apel — ar face ca def f(x=expensive()) să apeleze expensive de fiecare dată, ar cere stocarea unui arbore sintactic în loc de o valoare și ar face imposibilă inspecția valorilor implicite prin __defaults__ sau inspect.signature. Python a ales evaluarea timpurie, la fel ca pentru orice altă expresie dintr-o instrucțiune executată.
Consecința nu este vizibilă pentru int, str, tuple sau None, pentru că nu ai cum să le modifici la loc. Este vizibilă pentru orice obiect mutabil, iar cazul cel mai insidios nu este lista, ci o valoare capturată la import: def open_ticket(opened_at=datetime.date.today()) îngheață ziua în care a pornit procesul, iar un serviciu care rulează săptămâni întregi raportează tăcut date greșite.
Invariantul de reținut: valoarea implicită este un obiect, nu o expresie. Dacă obiectul poate fi modificat sau devine învechit, valoarea implicită trebuie să fie None, iar construirea trebuie să se mute în corp.
Cum arată la rulare
Creează un fișier:
touch scan_batches.py
Rulează-l cu:
python3 scan_batches.py
Acumularea între apeluri independente
Cel mai mic caz: două apeluri fără nicio legătură aparentă, care ajung să vadă aceleași date.
def record_scan(sku, batch=[]):
batch.append(sku)
return batch
first = record_scan("SKU-4471")
second = record_scan("SKU-4472")
print("first :", first)
print("second:", second)
print("same object:", first is second)
print("default still holds:", record_scan.__defaults__)
first : ['SKU-4471', 'SKU-4472']
second: ['SKU-4471', 'SKU-4472']
same object: True
default still holds: (['SKU-4471', 'SKU-4472'],)
firstșisecondnu sunt doar egale, ci același obiect —isconfirmă. Ambele apeluri au returnat lista din__defaults__.- Ultima linie inspectează direct obiectul funcție: valoarea implicită a crescut. Nu există nicio „resetare” între apeluri, pentru că nimic nu o reconstruiește.
first conține și SKU-4472, deși a fost calculat înaintea celui de-al doilea apel. Numele first este legat de o listă care a fost modificată ulterior — un simptom tipic de aliasing, nu de recalculare.
Aceeași regulă, pentru orice tip de expresie
Următorul nivel arată că mecanismul nu are nimic de-a face cu listele: se aplică identic unui apel de funcție și unui dicționar. Înlocuiește fișierul cu:
import datetime
def open_ticket(subject, opened_at=datetime.date(2026, 3, 1), tags=[], meta={}):
tags.append("new")
meta["subject"] = subject
return opened_at, tags, meta
print("defaults at import:", open_ticket.__defaults__)
open_ticket("cold-chain breach")
open_ticket("late delivery")
print("defaults after two calls:", open_ticket.__defaults__)
defaults at import: (datetime.date(2026, 3, 1), [], {})
defaults after two calls: (datetime.date(2026, 3, 1), ['new', 'new'], {'subject': 'late delivery'})
- Tuplul
__defaults__există deja înainte de primul apel, cu toate cele trei valori construite. Instrucțiuneadefle-a evaluat. - Data rămâne neschimbată — nu pentru că ar fi tratată special, ci pentru că
datetime.dateeste imuabil și nimic din corp nu încearcă să o modifice. - Lista a acumulat două intrări, iar dicționarul păstrează doar ultimul subiect, suprascris de al doilea apel.
Dacă opened_at ar fi fost scris datetime.date.today(), tuplul ar fi conținut ziua importului și ar fi rămas acolo pentru toată viața procesului. Expresia nu se reevaluează niciodată, indiferent cât de dinamică arată.
Remediul și de ce funcționează
Al treilea nivel aplică tiparul standard și verifică toate cele trei căi de apel. Înlocuiește fișierul cu:
def record_scan(sku, batch=None):
if batch is None:
batch = []
batch.append(sku)
return batch
first = record_scan("SKU-4471")
second = record_scan("SKU-4472")
existing = record_scan("SKU-4473", ["SKU-0001"])
print("first :", first)
print("second :", second)
print("existing:", existing)
print("same object:", first is second)
print("default is:", record_scan.__defaults__)
first : ['SKU-4471']
second : ['SKU-4472']
existing: ['SKU-0001', 'SKU-4473']
same object: False
default is: (None,)
Noneeste un singleton imuabil, deci__defaults__nu are ce acumula.batch = []este în corp, iar corpul rulează la fiecare apel — de aici o listă nouă de fiecare dată, confirmată deiscare dăFalse.- Al treilea apel pasează o listă explicită; verificarea
is Nonenu se declanșează, deci funcția scrie în lista apelantului, așa cum era intenția.
Verificarea se scrie if batch is None, nu if not batch. A doua ar înlocui și o listă goală pasată intenționat de apelant cu una nouă, iar apelantul nu ar mai vedea rezultatele scrise în ea.
Aceeași capcană în alte forme
Ultimul nivel arată unde apare aceeași regulă în afara parametrilor obișnuiți. Înlocuiește fișierul cu:
import dataclasses
@dataclasses.dataclass
class Batch:
line: str
scans: list = dataclasses.field(default_factory=list)
a = Batch("line-3")
b = Batch("line-4")
a.scans.append("SKU-4471")
print("factory per instance:", a.scans, b.scans)
try:
@dataclasses.dataclass
class BrokenBatch:
line: str
scans: list = []
except ValueError as exc:
print("ValueError:", exc)
class Counter:
seen = []
def add(self, sku):
self.seen.append(sku)
c1, c2 = Counter(), Counter()
c1.add("SKU-4471")
print("class attribute is shared:", c2.seen)
factory per instance: ['SKU-4471'] []
ValueError: mutable default <class 'list'> for field scans is not allowed: use default_factory
class attribute is shared: ['SKU-4471']
default_factory=liststochează funcția, nu rezultatul ei, iar__init__generat o apelează la fiecare instanțiere — de aceeab.scansrămâne gol.dataclassesrefuză explicit lista ca valoare implicită. Este singurul loc din limbaj unde greșeala este prinsă la definire, tocmai pentru că apare atât de des.- Atributul de clasă
seeneste aceeași problemă fără nicio legătură cu parametrii: obiectul aparține clasei, iarself.seen.append(...)îl modifică pentru toate instanțele. Atributul de instanță se creează doar la atribuire, iarappendnu este o atribuire.
Ultimul caz este cel mai greu de observat, pentru că self.seen arată ca stare per instanță. Modificarea la loc nu creează niciodată un atribut de instanță; doar self.seen = [...] ar face-o.
Cazuri limită și capcane
- Valoarea implicită se poate modifica din exterior.
record_scan.__defaults__[0].append("SKU-9999")schimbă comportamentul funcției pentru toate apelurile viitoare — tuplul este imuabil, lista din el nu. - Tiparul are o utilizare legitimă: memoizarea.
def lookup(key, _cache={})este un cache deliberat legat de funcție. Scrie-l cu prefix underscore și un comentariu, altfel arată ca un bug. Nonenu este întotdeauna disponibil ca valoare santinelă. Dacă apelantul poate pasa legitimNone, folosește o santinelă proprie:_MISSING = object(), apoiif batch is _MISSING.- Argumentele exclusiv-cheie sunt în
__kwdefaults__, nu în__defaults__, deci codul de introspecție trebuie să le citească pe amândouă. - Adnotările se evaluează și ele la
def, cu același cost.from __future__ import annotationsle amână ca șiruri, dar nu afectează valorile implicite. - O valoare implicită imuabilă care încapsulează una mutabilă nu este sigură.
def f(config=({}, ))are un tuplu ca valoare implicită, dar dicționarul din el este tot partajat.
Rezumat
Expresiile valorilor implicite sunt evaluate o singură dată, la execuția instrucțiunii def, iar rezultatele trăiesc în __defaults__ pe toată durata obiectului funcție. Fiecare apel care omite parametrul îl leagă de exact același obiect, deci un obiect mutabil acumulează modificări între apeluri fără nicio legătură între ele.
defeste o instrucțiune executată la import; valorile implicite sunt obiecte gata construite, nu expresii reevaluate.- Legarea unui parametru la valoarea implicită este o atribuire de nume, nu o copiere.
- Folosește
Noneca valoare implicită și construiește obiectul în corp, cuif x is None, nu cuif not x. - Aceeași regulă îngheață și valorile aparent dinamice, precum
datetime.date.today(), la momentul importului. - În
dataclasses, foloseștefield(default_factory=list); lista directă este respinsă cuValueErrorla definirea clasei. - Un atribut mutabil de clasă este aceeași capcană:
self.x.append(...)modifică obiectul partajat, pentru că doar atribuirea creează un atribut de instanță.