Vì sao list mặc định trong Python nhớ lần gọi trước?
Nền tảng để lên level · Python · Bài 1 - Bẫy mutable default argument & cách diệt bug rò rỉ state
Bài 1 - series “Python cho dân backend: nền tảng để lên level”. Mở màn bằng câu đố kinh điển nhứt Python, mà qua cá là bây từng dính:
def them_mon(mon, gio_hang=[]):
gio_hang.append(mon)
return gio_hang
print(them_mon("táo")) # ['táo']
print(them_mon("cam")) # tưởng ['cam']... thực ra ['táo', 'cam'] (!)Lần gọi thứ hai bây không truyền gio_hang, vậy mà nó “nhớ” luôn quả táo của lần trước. Cái default [] đáng lẽ phải là list rỗng mới mỗi lần chớ? Trật. Và hiểu sai chỗ này là rải bug khắp backend.
Đa số nghĩ: mỗi lần gọi hàm, [] tạo list rỗng mới
Sai từ gốc rồi bây. Sự thật là đây nha:
Default argument chỉ được tính MỘT LẦN - lúc Python định nghĩa hàm (đọc dòng
def), không phải mỗi lần gọi.
Cái list [] được tạo một lần duy nhứt lúc định nghĩa, rồi dùng chung cho mọi lần gọi không truyền tham số. Mỗi lần append, bây đang nhét vào cùng một cái list chung đó - nên nó tích lũy mãi đó.
Cơ chế: default nằm ở đâu?
Khi Python đọc tới def them_mon(...), nó tính ngay các giá trị mặc định rồi gắn thẳng vào object hàm. Soi thử nghen:
print(them_mon.__defaults__) # ([...],) - thấy đúng cái list chung nằm trỏngMọi lần gọi không truyền gio_hang đều xài lại đúng cái list trong __defaults__ đó. Nó không sinh ra cái mới - nên append cứ chồng lên hoài.
Vì sao chỉ list/dict/set dính, còn int/str/None thì không?
Vì list, dict, set là mutable - sửa-tại-chỗ được, nên trạng thái tích lại qua từng lần gọi. Còn int, str, None... là immutable, không sửa-tại-chỗ được, nên dù cũng “xài chung” thì chẳng có gì để tích - không ai thấy vấn đề.
Mutable với immutable là gì, vì sao immutable “không sửa được”, và chuyện “gán không copy” - qua mổ kỹ ở bài 2. Ở đây bây chỉ cần nằm lòng một câu: default mà là mutable thì nguy.
Hệ quả: bug âm thầm, cực nguy trong backend
Không phải chuyện học thuật. Trong web backend, một hàm có default mutable mà dùng qua nhiều request → state rò rỉ giữa các request, giữa các user. Người A đặt món, người B mở giỏ thấy luôn món của người A. Bug kiểu này khó lần vì code “nhìn đúng”, mà test một phát thì chưa lộ.
Cách sửa: dùng None làm sentinel
Quy tắc vàng: đừng bao giờ để giá trị mặc định là mutable. Dùng None, rồi tạo list mới bên trong hàm - chỗ này mới chạy mỗi lần gọi:
def them_mon(mon, gio_hang=None):
if gio_hang is None:
gio_hang = [] # tạo MỚI mỗi lần gọi
gio_hang.append(mon)
return gio_hang
print(them_mon("táo")) # ['táo']
print(them_mon("cam")) # ['cam'] - đúng rồi nghengio_hang={} hay set() làm default cũng dính y chang - cứ sửa hết thành None rồi tạo mới bên trong. (Để ý is None chứ không == None - lý do để dành bài sau.)
Checklist: bây nắm chưa?
Default argument được tính lúc nào? (Một lần, lúc định nghĩa hàm - không phải mỗi lần gọi.)
Vì sao
gio_hang=[]“nhớ” lần trước? (Cùng một list được dùng chung cho mọi lần gọi.)Xem default đang lưu ở đâu? (
ten_ham.__defaults__.)Vì sao int/str/None làm default thì không dính? (Immutable - không sửa-tại-chỗ được.)
Hệ quả nguy hiểm trong web backend? (State rò rỉ giữa các request/user.)
Cách sửa đúng? (Default =
None, tạo mới bên trong hàm.)
Bài tới (bài 2): “Gán không copy - vì sao b = a rồi sửa b mà a cũng đổi.” Ở đó qua mổ kỹ mô hình nhãn → object với mutable vs immutable.


