Late binding closures trong vòng lặp
Hiện tượng lạ
Phần tiêu đề “Hiện tượng lạ”# Tạo list các functionsfunctions = []for i in range(5): functions.append(lambda: i)
# Gọi từng functionfor func in functions: print(func())Bạn nghĩ sẽ in: 0 1 2 3 4
Kết quả thực tế: 4 4 4 4 4 ❌
What the Python?! Tất cả functions đều return 4?! 😱
Giải thích
Phần tiêu đề “Giải thích”Late Binding
Phần tiêu đề “Late Binding”Python closures sử dụng late binding: giá trị của biến được lookup lúc function ĐƯỢC GỌI, không phải lúc được TẠO!
functions = []for i in range(5): # Lambda KHÔNG capture giá trị của i # Lambda chỉ REFERENCE đến biến i functions.append(lambda: i)
# Sau loop, i = 4# Tất cả lambdas đều reference đến CÙNG biến i# Khi gọi, i đã = 4 → Tất cả return 4Minh họa
Phần tiêu đề “Minh họa”# Sau khi loop kết thúcprint(i) # 4
# Mọi lambda đều reference biến i nàyfunctions[0]() # Lookup i → 4functions[1]() # Lookup i → 4functions[2]() # Lookup i → 4Giải pháp
Phần tiêu đề “Giải pháp”✅ Solution 1: Default argument (Khuyến khích!)
Phần tiêu đề “✅ Solution 1: Default argument (Khuyến khích!)”functions = []for i in range(5): # Default argument được evaluate LÚC TẠO functions.append(lambda x=i: x) # Capture giá trị của i
# In ra đúngfor func in functions: print(func()) # 0 1 2 3 4 ✅✅ Solution 2: functools.partial
Phần tiêu đề “✅ Solution 2: functools.partial”from functools import partial
def return_value(x): return x
functions = []for i in range(5): functions.append(partial(return_value, i))
for func in functions: print(func()) # 0 1 2 3 4 ✅✅ Solution 3: Function factory
Phần tiêu đề “✅ Solution 3: Function factory”def make_func(x): return lambda: x # x là local variable của make_func
functions = []for i in range(5): functions.append(make_func(i))
for func in functions: print(func()) # 0 1 2 3 4 ✅✅ Solution 4: List comprehension
Phần tiêu đề “✅ Solution 4: List comprehension”# List comprehension tạo scope riêngfunctions = [lambda x=i: x for i in range(5)]
for func in functions: print(func()) # 0 1 2 3 4 ✅Ví dụ thực tế
Phần tiêu đề “Ví dụ thực tế”Bug 1: Event handlers
Phần tiêu đề “Bug 1: Event handlers”# SAIbuttons = []for i in range(5): # Tất cả buttons đều gọi callback với i=4! button = Button(onclick=lambda: print(f"Button {i}")) # ❌ buttons.append(button)
# ĐÚNGbuttons = []for i in range(5): button = Button(onclick=lambda x=i: print(f"Button {x}")) # ✅ buttons.append(button)Bug 2: Tkinter callbacks
Phần tiêu đề “Bug 2: Tkinter callbacks”import tkinter as tk
# SAIfor i in range(5): btn = tk.Button( root, text=str(i), command=lambda: print(i) # ❌ Tất cả print 4 ) btn.pack()
# ĐÚNGfor i in range(5): btn = tk.Button( root, text=str(i), command=lambda x=i: print(x) # ✅ ) btn.pack()Bug 3: Async callbacks
Phần tiêu đề “Bug 3: Async callbacks”import asyncio
# SAIasync def main(): tasks = [] for i in range(5): # Tất cả tasks đều dùng i=4 tasks.append(asyncio.create_task(process(lambda: i))) # ❌
# ĐÚNGasync def main(): tasks = [] for i in range(5): tasks.append(asyncio.create_task(process(lambda x=i: x))) # ✅Nested loops
Phần tiêu đề “Nested loops”# SAIfunctions = []for i in range(3): for j in range(3): functions.append(lambda: (i, j)) # ❌
for func in functions: print(func()) # Tất cả print (2, 2)
# ĐÚNGfunctions = []for i in range(3): for j in range(3): functions.append(lambda x=i, y=j: (x, y)) # ✅
for func in functions: print(func()) # (0,0), (0,1), (0,2), (1,0), ...List comprehension vs for loop
Phần tiêu đề “List comprehension vs for loop”For loop - Late binding
Phần tiêu đề “For loop - Late binding”# For loop - SHARED scopefuncs = []for i in range(3): funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2] ❌List comprehension - Early binding (Python 3)
Phần tiêu đề “List comprehension - Early binding (Python 3)”# Python 3: List comp có scope riêngfuncs = [lambda: i for i in range(3)]print([f() for f in funcs]) # [2, 2, 2] ❌ Vẫn late binding!
# Phải dùng default argfuncs = [lambda x=i: x for i in range(3)]print([f() for f in funcs]) # [0, 1, 2] ✅Tóm tắt
Phần tiêu đề “Tóm tắt”Closures trong Python dùng LATE BINDING
- ❌
lambda: i- Reference biến i- ✅
lambda x=i: x- Capture giá trị i- 🐛 Bug phổ biến với loops + lambdas
- 🔧 Luôn dùng default argument để capture values
Pattern an toàn:
for i in range(n): func = lambda x=i: do_something(x) # Capture i