Vì sao list mặc định trong Python nhớ lần gọi trước?
Cái list trong default argument "nhớ" lần gọi trước — vì sao?
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ẽ 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. Sự thật:
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ế
Khi Python đọc tới def them_mon(...), nó tính ngay các giá trị mặc định và gắn vào object hàm (xem được qua them_mon.__defaults__ — sẽ thấy cái list chung nằm đó). Mọi lần gọi sau đều xài lại đúng object list ấy.
Vì sao chỉ list/dict/set dính mà int/str/None không? Vì mutable (sửa tại chỗ được) thì trạng thái tích lại qua các lần gọi; còn immutable (số, chuỗi, None) thì không sửa tại chỗ được, nên không ai thấy vấn đề (chi tiết mutable vs immutable ở bài 2).
Hệ quả: bug âm thầm, cực nguy trong backend
Cái này 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", và lúc 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 nghen
(Để ý is None chứ không == None — lý do ở bài 3.)
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.) - [ ] 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 list mới bên trong hàm.) - [ ] Xem default đang lưu ở đâu? (
ten_ham.__defaults__.)
Bài tới: "Gán không copy — vì sao b = a rồi sửa b mà a cũng đổi."
Bản gốc đăng tại Substack: https://quakebaynghe.substack.com/p/python-mutable-default-argument
All rights reserved