0

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 ba cũng đổi."


Bản gốc đăng tại Substack: https://quakebaynghe.substack.com/p/python-mutable-default-argument


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí