Chuyển đến nội dung chính

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ế.
· 8 phút đọc

Khám phá cùng AI

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

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 literal

True = "yes"  # cannot assign to a keyword
# SyntaxError: cannot assign to True

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.

def show_order_id():
    order_id = 42
    print("inside function:", order_id)

show_order_id()
print("outside function:", order_id)  # NameError

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)

def price_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 function
    return int(amount_cents * (1 - discount_rate))

# UnboundLocalError: cannot access local variable 'discount_rate' where it is not associated with a value

Để 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.

def make_step_counter():
    count = 0  # enclosing scope for 'increment'

    def increment():
        nonlocal count  # rebind the 'count' in the nearest enclosing function
        count += 1
        return count

    return increment

step = make_step_counter()
print(step())  # 1
print(step())  # 2

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"

def greet_city(city_name):
    print(greeting, city_name)  # reads global

def set_greeting(new_greeting):
    global greeting
    greeting = new_greeting     # rebinds global

greet_city("Nairobi")  # Hello Nairobi
set_greeting("Hi")
greet_city("Nairobi")  # Hi Nairobi

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' constructor
list("abc")          # TypeError: 'list' object is not callable
del list             # fix by deleting the shadowing name

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.

if True:
    status = "ready"
print(status)  # "ready"

for i in range(3):
    pass
print(i)  # 2 (the last value from the loop)

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" in globals() or "x" in locals())  # False

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.

tax_rate = 0.08

def configure_tax(rate):
    global tax_rate
    tax_rate = float(rate)

def total_with_tax(cents):
    return int(cents * (1 + tax_rate))

configure_tax(0.10)
print(total_with_tax(1000))  # 1100

Sử dụng từ khóa nonlocal

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.

def make_accumulator(start=0):
    total = start
    def add(amount):
        nonlocal total
        total += amount
        return total
    return add

acc = make_accumulator()
print(acc(5))   # 5
print(acc(10))  # 15

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:

def probe():
    project = "alpha"
    snap1 = locals()
    snap1["project"] = "beta"  # edits the snapshot only
    observed = project         # still "alpha"
    snap2 = locals()           # new snapshot
    return snap1["project"], observed, snap2["project"]

print(probe())  # ('beta', 'alpha', 'alpha')

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ờ.

Sejal Jaiswal

Tác giả

Sejal Jaiswal

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!

Chủ đề

Liên quan