Câu 1Code này in ra gì?stock = 100
def f():
stock = 50
f()
print(stock)
Hàm lồng và Closure — Làm chủ phạm vi với global và nonlocal
Học phạm vi, hàm lồng và closure trong Python để làm chủ biến global, local và viết hàm nhớ trạng thái bên ngoài.
Trong bài trước bạn đã thấy cách đối số và giá trị trả về hoạt động. Lần này bạn sẽ đi sâu hơn vào một vài chủ đề liên quan đến hàm: cách Python tách biệt biến trong và ngoài hàm (phạm vi), cách định nghĩa hàm bên trong hàm (hàm lồng), và cách trả về một hàm nhớ các giá trị bên ngoài (closure). Trên đường đi, bạn sẽ thấy khi nào dùng global và nonlocal để ghi đè một biến bao quanh từ bên trong hàm.
Biến global và local — Phạm vi tách biệt
Biến được khai báo ngoài mọi hàm là biến global; biến khai báo bên trong hàm là biến local. Bạn có thể đọc một biến global từ bên trong hàm, nhưng nếu gán cho cùng tên đó bằng = value bên trong hàm, Python tạo ra một biến local mới — một thứ tách biệt khỏi biến bên ngoài.
Nếu kiểm tra địa chỉ bộ nhớ của biến bằng id(), bạn sẽ thấy biến bên trong và bên ngoài trỏ tới những vị trí khác nhau.
stock = 100 # biến global
def show_stock():
print(f"trong: {stock}") # 100 — đọc giá trị bên ngoài
def try_change():
stock = 50 # tạo ra biến local hoàn toàn mới
print(f"trong: {stock}") # 50
show_stock() # trong: 100
try_change() # trong: 50
print(f"ngoài: {stock}") # ngoài: 100 ← biến bên ngoài không thay đổi
- stock = 100 — biến global
- Hàm chỉ có thể đọc từ bên trong
print(stock)→ đọc 100 bên ngoài- Không tạo biến local
stock = 50— tạo local mới bên trong hàm- Không ảnh hưởng tới
stockbên ngoài
Ghi đè biến bên ngoài bằng từ khóa global
Khi bạn thực sự muốn ghi đè một global bên ngoài từ bên trong hàm, hãy khai báo global tên_biến ở đầu hàm. Điều đó báo cho Python: "đây là global bên ngoài đó, không phải local mới." Nếu thiếu nó, một phép gán như count += 1 trộn việc đọc và ghi cùng tên và bạn nhận được UnboundLocalError ("local variable referenced before assignment").
visit_count = 0
# × Không có global → UnboundLocalError
# def increment():
# visit_count += 1
# increment() ← UnboundLocalError
# ○ global cho phép ghi đè global bên ngoài
def increment():
global visit_count
visit_count += 1
increment()
increment()
increment()
print(visit_count) # 3
Hạn chế dùng global
global cho phép một hàm ghi đè trạng thái bên ngoài một cách âm thầm, khiến một dòng ghi sai cách đó 100 dòng trở nên khó tìm khi mở rộng quy mô.
Mặc định nên trả về giá trị bằng return và gán ở phía bên gọi, và dùng class (sẽ học sau) khi bạn thực sự cần hành vi có trạng thái.
Hàm và biến sống trong bộ nhớ thế nào — Tên và Đối tượng
Bên trong, Python giữ một không gian tên — bảng tra cứu "tên → đối tượng". x = 5 nói "tạo số nguyên 5 trong bộ nhớ và làm cho tên x trỏ tới nó."
def f(): ... hoạt động tương tự: nó xây một đối tượng hàm (thân hàm) trong bộ nhớ và làm cho tên f trỏ tới nó. Có đúng một bảng tra cứu loại này cho mỗi chương trình — không gian tên global — được thiết lập khi module load và tồn tại đến khi chương trình thoát.
- x = 5 — tên
xtrỏ tới số nguyên 5 - def f — tên
ftrỏ tới đối tượng hàm - Sống đến khi chương trình thoát
- Giữ đối số và biến local
- Bị bỏ hoàn toàn khi return
- Hoàn toàn tách biệt với frame đầu
- Biến local độc lập
def một lần, hàm được đăng ký vào không gian tên global và ở đó đến khi thoát. Mỗi lần gọi lại tạo frame local riêng và bỏ đi khi return. Hàm lồng, closure và nonlocal ở phần sau đều dựa trên cấu trúc frame này.Hàm lồng — Định nghĩa hàm bên trong hàm
Đặt thêm một def bên trong một hàm và bạn có hàm lồng — hàm chỉ tồn tại để dùng bên trong hàm bên ngoài. Đó là cách hay để đặt tên cho một khối logic có ý nghĩa trong một hàm dài, nhờ đó thân hàm đọc lên như một chuỗi bước rõ ràng.
Vì hàm lồng không thể truy cập từ bên ngoài, nó cũng phù hợp để giấu các hàm phụ mà bạn không muốn lộ ra.
- Gọi
validatetừ ngoài này → NameError - Không tồn tại bên ngoài
- Giữ đối số
name/age - Có thể gọi
validatetừ bên trong
- Đọc
name/agebên ngoài - Không lộ ra ngoài
def process_user(name, age):
def validate():
if not name or not isinstance(age, int) or age < 0:
raise ValueError("Dữ liệu không hợp lệ")
validate() # gọi hàm lồng
print(f"Đã xử lý: {name} ({age})")
process_user("Minh", 25)
# Đã xử lý: Minh (25)
# validate không truy cập được từ ngoài process_user
# validate() ← NameError
Tuyệt vời khi muốn tách một hàm dài
Khi một hàm vượt quá 50 hay 100 dòng, tách nó thành các hàm lồng được đặt tên cho từng khối có ý nghĩa — process_name() / process_age() v.v. — biến hàm bên ngoài thành một danh sách các bước dễ đọc. Nếu sau này bạn muốn dùng lại một hàm bên ngoài, đẩy hàm lồng lên thành hàm thông thường rất đơn giản.
Closure — Hàm nhớ giá trị bên ngoài
Hàm lồng có thể đọc đối số và biến local của hàm bên ngoài. Tiến thêm một bước — để hàm bên ngoài return hàm lồng — và bạn đã xây được một hàm tiếp tục hoạt động với các giá trị bên ngoài đã được nhúng vào. Đó là closure.
Đây là cách gọn gàng để tạo hàng loạt hàm tương tự nhau chỉ khác mỗi một thiết lập, ví dụ "hàm nhân ba" và "hàm nhân năm". Nhận thiết lập (factor) làm đối số bên ngoài và tham chiếu nó từ hàm lồng: make_multiplier(3) trả về "một phép nhân nhớ 3", và make_multiplier(5) trả về "một phép nhân nhớ 5".
def make_multiplier(factor):
def multiply(x):
return x * factor # tham chiếu factor bên ngoài
return multiply
times3 = make_multiplier(3) # hàm nhớ factor=3
times5 = make_multiplier(5) # hàm tách biệt nhớ factor=5
print(times3(10)) # 30
print(times5(10)) # 50
print(times3(7)) # 21
Closure trao ra hàm "đã được cấu hình sẵn"
Khi bạn muốn tạo nhiều phép tính tương tự nhau chỉ khác mỗi thiết lập — "thuế 10%" so với "thuế 8%" v.v. — closure phát huy thế mạnh. Thay vì truyền thiết lập mỗi lần gọi, bạn trao ra một hàm đã cấu hình sẵn, và phía gọi trở nên gọn gàng hơn nhiều.
nonlocal — Ghi đè biến của hàm bao quanh
Closure có thể đọc biến bên ngoài bình thường, nhưng cũng giống global, thử ghi đè nó bằng count += 1 sẽ phát nổ với UnboundLocalError. Từ khóa cho phép phép ghi đè đó là nonlocal. Trong khi global nhắm vào cấp module, nonlocal nhắm vào biến local của hàm bao quanh ngay bên ngoài.
Bọc trạng thái bên trong một đối tượng hàm
nonlocal là cách chuẩn để bọc bộ đếm tăng theo mỗi lần gọi vào bên trong một hàm.
Khác với global, trạng thái được niêm phong bên trong một đối tượng hàm cụ thể, nên các tác động phụ không lan ra, và bạn có trạng thái an toàn hơn so với việc dùng global.
def create_counter():
x = 0
def increment():
nonlocal x # khai báo rằng ta đang cập nhật x của create_counter
x += 1
return x
return increment
counter = create_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
# Bộ đếm thứ hai có x độc lập riêng
counter2 = create_counter()
print(counter2()) # 1 — không liên quan đến counter
counter = create_counter()— nhậnincrement- Code bên ngoài không thể chạm vào
xtrực tiếp
- x = 0 — bộ đếm, được tạo đúng một lần
- Cái mà
incrementtham chiếu quanonlocal
nonlocal x— trỏ tớixbên ngoàix += 1cập nhật x bên ngoài
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Vì sao code này lỗi? Chọn lý giải tốt nhất.count = 0
def inc():
count += 1
inc()
Câu 3print(times3(10)) in ra gì?def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
times3 = make_multiplier(3)