Giải thích Phạm vi Biến trong Python và Quy tắc LEGB
Tìm hiểu mọi khía cạnh của phạm vi biến và làm quen với quy tắc 'LEGB'. Bạn cũng sẽ thấy các kịch bản nơi từ khóa global và nonlocal được dùng thực tế.
Gặp lỗi “name is not defined” hoặc “local variable referenced before assignment” ngay sau khi viết vài dòng mã thật khó chịu. Mã trông ổn, nhưng Python lại không đồng ý. Nguyên nhân gốc rễ gần như luôn là phạm vi (scope): nơi một tên khả dụng và đối tượng mà nó tham chiếu tại thời điểm đó.
Khi bạn hiểu cơ chế tra cứu tên của Python—thường được tóm tắt bằng quy tắc LEGB—những lỗi này trở nên có thể dự đoán và dễ sửa. Trong hướng dẫn này, tôi sẽ đi từng bước qua biến và phạm vi trong Python.
Biến là gì?
Biến là một tên được liên kết (bound) với một đối tượng. Trong Python, bạn tạo liên kết bằng toán tử gán =. Tên không có kiểu; chúng có thể trỏ tới bất kỳ đối tượng nào (string, int, list, function, v.v.).
customer_name ="Ava Torres"
order_count =1
total_cents =1+2*300# evaluated first, then boundĐược hỗ trợ bởi
Hãy làm theo các quy tắc và quy ước sau khi đặt tên biến để tránh lỗi cú pháp và các lỗi khó phát hiện.
Chỉ dùng chữ cái, chữ số và dấu gạch dưới; tên không được bắt đầu bằng chữ số.
Không dùng từ khóa như True, for hoặc class làm tên.
Tránh che khuất (shadow) các built-in như list, dict, sum hoặc max.
Ưu tiên snake_case để dễ đọc (PEP 8).
Các ví dụ sau minh họa những điều không nên làm và các lỗi Python đưa ra:
first string value ="First string"# spaces not allowed# SyntaxError: invalid syntax
1st_value =10# cannot start with a digit# SyntaxError: invalid decimal literalTrue="yes"# cannot assign to a keyword# SyntaxError: cannot assign to TrueĐược hỗ trợ bởi
Cách phạm vi trong Python hoạt động (LEGB)
Phạm vi là vùng mã nơi một tên khả dụng. Khi bạn sử dụng một tên, Python tra cứu theo thứ tự sau (LEGB):
Local: phạm vi của hàm hiện tại.
Enclosing: bất kỳ phạm vi hàm bao ngoài nào (đối với hàm lồng nhau).
Global: cấp cao nhất của module (không gian tên module).
Built-in: các tên do Python định nghĩa trong builtins (ví dụ, len, print).
Tên tồn tại trong các không gian tên (hãy hình dung như các dictionary ánh xạ tên tới đối tượng). Phạm vi nói về những không gian tên nào Python tham chiếu để xử lý một tên tại một điểm nhất định trong mã.
Phạm vi cục bộ
Các tên được gán bên trong một hàm là cục bộ với hàm đó trừ khi được khai báo khác. Chúng không khả dụng bên ngoài hàm.
Python quyết định phạm vi tại thời điểm biên dịch. Nếu một hàm gán cho một tên ở bất kỳ đâu trong thân hàm, mọi lần sử dụng tên đó trong hàm sẽ được coi là cục bộ—dẫn đến UnboundLocalError phổ biến khi bạn đọc trước khi gán:
discount_rate =0.10# module-level (global)defprice_with_discount(amount_cents):print("configured discount:", discount_rate)# looks local because of the assignment below
discount_rate =0.20# assignment makes 'discount_rate' local in this functionreturnint(amount_cents *(1- discount_rate))# UnboundLocalError: cannot access local variable 'discount_rate' where it is not associated with a valueĐược hỗ trợ bởi
Để dùng tên ở cấp module trong hàm, hoặc là tránh gán cho nó hoặc đánh dấu nó là global (trình bày bên dưới).
Phạm vi bao ngoài (closure)
Hàm lồng nhau có thể nhìn thấy các tên từ hàm bao ngay phía ngoài. Để gán lại (rebind) tên như vậy (không chỉ đọc), hãy khai báo nonlocal.
defmake_step_counter():
count =0# enclosing scope for 'increment'defincrement():nonlocal count # rebind the 'count' in the nearest enclosing function
count +=1return count
return increment
step = make_step_counter()print(step())# 1print(step())# 2Được hỗ trợ bởi
Không có nonlocal, việc gán cho count bên trong increment() sẽ tạo một tên cục bộ mới và để nguyên count bên ngoài.
Phạm vi toàn cục
Các tên được gán ở cấp cao nhất của một module tồn tại trong không gian tên toàn cục của module. Bất kỳ hàm nào cũng có thể đọc chúng. Để gán cho một tên cấp module từ trong một hàm, hãy khai báo nó là global.
greeting ="Hello"defgreet_city(city_name):print(greeting, city_name)# reads globaldefset_greeting(new_greeting):global greeting
greeting = new_greeting # rebinds global
greet_city("Nairobi")# Hello Nairobi
set_greeting("Hi")
greet_city("Nairobi")# Hi NairobiĐược hỗ trợ bởi
Hãy dùng global một cách tiết chế. Ưu tiên truyền giá trị qua tham số và trả về kết quả để giữ cho mã dễ kiểm thử và có thể dự đoán.
Phạm vi built-in (và lưu ý về từ khóa)
Phạm vi built-in chứa các tên như len, print và Exception. Tránh che khuất chúng, nếu không bạn sẽ mất quyền truy cập built-in trong phạm vi đó.
list=[1,2,3]# shadows the built-in 'list' constructorlist("abc")# TypeError: 'list' object is not callabledellist# fix by deleting the shadowing nameĐược hỗ trợ bởi
Từ khóa (như if, for, def) là một phần cú pháp của Python, không nằm trong không gian tên built-in, và không bao giờ có thể dùng làm định danh.
Khối lệnh, vòng lặp và comprehension
Các câu lệnh khối và comprehension của Python có hành vi phạm vi cụ thể, thường gây bất ngờ cho lập trình viên đến từ ngôn ngữ khác.
Không có phạm vi khối cho if/for/while/with
Các phép gán bên trong những khối này ảnh hưởng tới phạm vi bao chứa (hàm hoặc module). Biến vòng lặp cũng vẫn tồn tại sau khi vòng lặp kết thúc.
ifTrue:
status ="ready"print(status)# "ready"for i inrange(3):passprint(i)# 2 (the last value from the loop)Được hỗ trợ bởi
Comprehension cô lập biến lặp của chúng
List, dict và set comprehension có phạm vi cục bộ riêng cho biến vòng lặp. Biến lặp không rò rỉ ra phạm vi xung quanh.
numbers =[1,2,3][x for x in numbers]print("x"inglobals()or"x"inlocals())# FalseĐược hỗ trợ bởi
Python 3.12 (PEP 709) nội tuyến comprehension để tăng tốc nhưng vẫn giữ sự cô lập này; bạn vẫn có biến vòng lặp rõ ràng, không rò rỉ cùng với tốc độ thực thi nhanh hơn.
Sử dụng từ khóa global
Khai báo một tên là global bên trong hàm khi bạn cần gán lại một biến cấp module. Đặt khai báo gần đầu hàm để rõ ràng biến nào bạn đang sửa.
Dùng nonlocal để gán lại một tên từ phạm vi hàm bao gần nhất. Điều này thường gặp trong các closure duy trì trạng thái.
defmake_accumulator(start=0):
total = start
defadd(amount):nonlocal total
total += amount
return total
return add
acc = make_accumulator()print(acc(5))# 5print(acc(10))# 15Được hỗ trợ bởi
Dùng nonlocal cho một tên không được định nghĩa trong hàm bao là một SyntaxError. Nếu tên là toàn cục, hãy dùng global thay thế.
locals() và globals() vào năm 2025
Những hàm này hữu ích cho việc kiểm tra và gỡ lỗi, không phải để cập nhật biến. Bắt đầu từ Python 3.13 (PEP 667), mỗi lần gọi locals() trong một hàm sẽ trả về một bản chụp độc lập. Chỉnh sửa bản chụp đó không thay đổi biến cục bộ thực. Tôi đã xác nhận hành vi này bằng cách chạy đoạn mã sau trên Python 3.13:
defprobe():
project ="alpha"
snap1 =locals()
snap1["project"]="beta"# edits the snapshot only
observed = project # still "alpha"
snap2 =locals()# new snapshotreturn snap1["project"], observed, snap2["project"]print(probe())# ('beta', 'alpha', 'alpha')Được hỗ trợ bởi
Sử dụng globals() tương tự cho không gian tên của module. Khi viết mã ứng dụng, hãy ưu tiên tham số và giá trị trả về tường minh hơn là tra cứu động.
Những lỗi thường gặp và cách khắc phục
Những sai sót này chiếm phần lớn lỗi liên quan đến phạm vi mà tôi gặp trong thực tế.
UnboundLocalError do gán trong hàm: Python coi một tên là cục bộ nếu nó được gán ở bất kỳ đâu trong hàm. Hoặc chuyển phần đọc xuống sau khi gán, đổi tên, hoặc thêm global/nonlocal nếu phù hợp.
Kỳ vọng có phạm vi khối: Các tên được gán trong if/for/while/with vẫn tồn tại trong phạm vi bao chứa. Hãy dùng các hàm tiện ích nhỏ để giới hạn phạm vi.
Che khuất built-in: Tránh các tên như list, dict, sum, id hoặc min. Chọn tên mô tả hơn như customer_list hoặc min_allowed.
Quên nonlocal trong closure: Nếu bạn định cập nhật một biến của hàm bao, hãy khai báo nonlocal. Nếu không, bạn sẽ tạo biến cục bộ mới và giá trị bên ngoài không đổi.
Nhầm lẫn từ khóa với built-in: Từ khóa (cú pháp) không bao giờ được dùng làm tên. Built-in có thể bị che khuất nhưng không nên.
Thực hành tốt giúp đơn giản hóa phạm vi
Những thói quen này giúp mã dễ đọc, dễ kiểm thử và gỡ lỗi hơn.
Ưu tiên các hàm nhỏ, tập trung. Định nghĩa biến trong phạm vi hẹp nhất có thể.
Truyền dữ liệu vào/ra qua tham số và giá trị trả về. Giảm thiểu trạng thái cấp module.
Dùng closure một cách chủ đích. Ghi chú những tên bên ngoài mà hàm lồng nhau bắt giữ, và dùng nonlocal khi thực sự cần gán lại.
Chọn tên mô tả, không xung đột. Dấu gạch dưới đầu (ví dụ, _cache) báo hiệu dùng nội bộ.
Xem locals()/globals() như công cụ chẩn đoán chỉ đọc, không phải cơ chế cấu hình.
Kết luận
Python xử lý tên bằng cách tìm theo thứ tự Local → Enclosing → Global → Built-in. Hiểu thứ tự đó—và khi nào dùng global hoặc nonlocal—sẽ loại bỏ các lỗi thường gặp như NameError và UnboundLocalError. Giữ biến trong phạm vi nhỏ nhất có thể, tránh che khuất built-in và dùng closure một cách có chủ đích. Với mô hình tư duy đó, hàm của bạn hoạt động có thể dự đoán và phạm vi không còn là nguồn gây bất ngờ.
Tôi đã làm việc trong nhiều ngành và đảm nhiệm nhiều vai trò: nhà phát triển phần mềm, nhà nghiên cứu học máy, nhà khoa học dữ liệu, quản lý sản phẩm. Nhưng cốt lõi của tất cả, tôi là một lập trình viên yêu thích học hỏi và chia sẻ kiến thức!
Gặp lỗi “name is not defined” hoặc “local variable referenced before assignment” ngay sau khi viết vài dòng mã thật khó chịu. Mã trông ổn, nhưng Python lại không đồng ý. Nguyên nhân gốc rễ gần như luôn là phạm vi (scope): nơi một tên khả dụng và đối tượng mà nó tham chiếu tại thời điểm đó.
Khi bạn hiểu cơ chế tra cứu tên của Python—thường được tóm tắt bằng quy tắc LEGB—những lỗi này trở nên có thể dự đoán và dễ sửa. Trong hướng dẫn này, tôi sẽ đi từng bước qua biến và phạm vi trong Python.
Biến là gì?
Biến là một tên được liên kết (bound) với một đối tượng. Trong Python, bạn tạo liên kết bằng toán tử gán
=. Tên không có kiểu; chúng có thể trỏ tới bất kỳ đối tượng nào (string, int, list, function, v.v.).Hãy làm theo các quy tắc và quy ước sau khi đặt tên biến để tránh lỗi cú pháp và các lỗi khó phát hiện.
True,forhoặcclasslàm tên.list,dict,sumhoặcmax.snake_caseđể dễ đọc (PEP 8).Các ví dụ sau minh họa những điều không nên làm và các lỗi Python đưa ra:
Cách phạm vi trong Python hoạt động (
LEGB)Phạm vi là vùng mã nơi một tên khả dụng. Khi bạn sử dụng một tên, Python tra cứu theo thứ tự sau (LEGB):
builtins(ví dụ,len,print).Tên tồn tại trong các không gian tên (hãy hình dung như các dictionary ánh xạ tên tới đối tượng). Phạm vi nói về những không gian tên nào Python tham chiếu để xử lý một tên tại một điểm nhất định trong mã.
Phạm vi cục bộ
Các tên được gán bên trong một hàm là cục bộ với hàm đó trừ khi được khai báo khác. Chúng không khả dụng bên ngoài hàm.
Python quyết định phạm vi tại thời điểm biên dịch. Nếu một hàm gán cho một tên ở bất kỳ đâu trong thân hàm, mọi lần sử dụng tên đó trong hàm sẽ được coi là cục bộ—dẫn đến
UnboundLocalErrorphổ biến khi bạn đọc trước khi gán:Để dùng tên ở cấp module trong hàm, hoặc là tránh gán cho nó hoặc đánh dấu nó là
global(trình bày bên dưới).Phạm vi bao ngoài (closure)
Hàm lồng nhau có thể nhìn thấy các tên từ hàm bao ngay phía ngoài. Để gán lại (rebind) tên như vậy (không chỉ đọc), hãy khai báo
nonlocal.Không có
nonlocal, việc gán chocountbên trongincrement()sẽ tạo một tên cục bộ mới và để nguyêncountbên ngoài.Phạm vi toàn cục
Các tên được gán ở cấp cao nhất của một module tồn tại trong không gian tên toàn cục của module. Bất kỳ hàm nào cũng có thể đọc chúng. Để gán cho một tên cấp module từ trong một hàm, hãy khai báo nó là
global.Hãy dùng
globalmột cách tiết chế. Ưu tiên truyền giá trị qua tham số và trả về kết quả để giữ cho mã dễ kiểm thử và có thể dự đoán.Phạm vi built-in (và lưu ý về từ khóa)
Phạm vi built-in chứa các tên như
len,printvàException. Tránh che khuất chúng, nếu không bạn sẽ mất quyền truy cập built-in trong phạm vi đó.Từ khóa (như
if,for,def) là một phần cú pháp của Python, không nằm trong không gian tên built-in, và không bao giờ có thể dùng làm định danh.Khối lệnh, vòng lặp và comprehension
Các câu lệnh khối và comprehension của Python có hành vi phạm vi cụ thể, thường gây bất ngờ cho lập trình viên đến từ ngôn ngữ khác.
Không có phạm vi khối cho
if/for/while/withCác phép gán bên trong những khối này ảnh hưởng tới phạm vi bao chứa (hàm hoặc module). Biến vòng lặp cũng vẫn tồn tại sau khi vòng lặp kết thúc.
Comprehension cô lập biến lặp của chúng
List, dict và set comprehension có phạm vi cục bộ riêng cho biến vòng lặp. Biến lặp không rò rỉ ra phạm vi xung quanh.
Python 3.12 (PEP 709) nội tuyến comprehension để tăng tốc nhưng vẫn giữ sự cô lập này; bạn vẫn có biến vòng lặp rõ ràng, không rò rỉ cùng với tốc độ thực thi nhanh hơn.
Sử dụng từ khóa
globalKhai báo một tên là
globalbên trong hàm khi bạn cần gán lại một biến cấp module. Đặt khai báo gần đầu hàm để rõ ràng biến nào bạn đang sửa.Sử dụng từ khóa
nonlocalDùng
nonlocalđể gán lại một tên từ phạm vi hàm bao gần nhất. Điều này thường gặp trong các closure duy trì trạng thái.Dùng
nonlocalcho một tên không được định nghĩa trong hàm bao là mộtSyntaxError. Nếu tên là toàn cục, hãy dùngglobalthay thế.locals()vàglobals()vào năm 2025Những hàm này hữu ích cho việc kiểm tra và gỡ lỗi, không phải để cập nhật biến. Bắt đầu từ Python 3.13 (PEP 667), mỗi lần gọi locals() trong một hàm sẽ trả về một bản chụp độc lập. Chỉnh sửa bản chụp đó không thay đổi biến cục bộ thực. Tôi đã xác nhận hành vi này bằng cách chạy đoạn mã sau trên Python 3.13:
Sử dụng
globals()tương tự cho không gian tên của module. Khi viết mã ứng dụng, hãy ưu tiên tham số và giá trị trả về tường minh hơn là tra cứu động.Những lỗi thường gặp và cách khắc phục
Những sai sót này chiếm phần lớn lỗi liên quan đến phạm vi mà tôi gặp trong thực tế.
UnboundLocalErrordo gán trong hàm: Python coi một tên là cục bộ nếu nó được gán ở bất kỳ đâu trong hàm. Hoặc chuyển phần đọc xuống sau khi gán, đổi tên, hoặc thêm global/nonlocal nếu phù hợp.customer_listhoặcmin_allowed.nonlocaltrong closure: Nếu bạn định cập nhật một biến của hàm bao, hãy khai báo nonlocal. Nếu không, bạn sẽ tạo biến cục bộ mới và giá trị bên ngoài không đổi.Thực hành tốt giúp đơn giản hóa phạm vi
Những thói quen này giúp mã dễ đọc, dễ kiểm thử và gỡ lỗi hơn.
_cache) báo hiệu dùng nội bộ.Kết luận
Python xử lý tên bằng cách tìm theo thứ tự Local → Enclosing → Global → Built-in. Hiểu thứ tự đó—và khi nào dùng global hoặc nonlocal—sẽ loại bỏ các lỗi thường gặp như
NameErrorvàUnboundLocalError. Giữ biến trong phạm vi nhỏ nhất có thể, tránh che khuất built-in và dùng closure một cách có chủ đích. Với mô hình tư duy đó, hàm của bạn hoạt động có thể dự đoán và phạm vi không còn là nguồn gây bất ngờ.