DHDataHọc
Lý thuyếtBài 22/305 phút đọc+10 XP

Phạm vi biến

Bạn sẽ học được

  • Quy tắc LEGB: local → enclosing → global → builtin.
  • Vì sao gán vào biến toàn cục trong hàm gây UnboundLocalError.
  • Vì sao nên tránh global.
  • nonlocal và closure.
  • Phạm vi của biến trong comprehension và việc che builtin.

Khái niệm cốt lõi

Python tìm tên theo thứ tự LEGB:

ChữPhạm viVí dụ
LocalTrong hàm hiện tạiTham số, biến gán trong hàm
EnclosingHàm bao ngoàiBiến của hàm chứa hàm lồng
GlobalCấp module/fileBiến gán ngoài mọi hàm
BuiltinTên có sẵnprint, len, sum

Tìm theo thứ tự, dừng ở nơi đầu tiên có tên. Hiểu quy tắc này giải thích gần hết các lỗi phạm vi.

code
count = 0

def tang():
    count = 5
    print("trong hàm:", count)

tang()
print("ngoài hàm:", count)
code
trong hàm: 5
ngoài hàm: 0

count = 5 trong hàm tạo biến cục bộ mới, không sửa biến toàn cục. Hai biến trùng tên nhưng khác phạm vi.

UnboundLocalError — cái bẫy lớn

Nếu vừa đọc vừa gán cùng một tên trong hàm, Python coi tên đó là cục bộ trong toàn bộ hàm:

code
count = 0

def tang_sai():
    count += 1

tang_sai()
code
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

count += 1 tương đương count = count + 1: đọc count trước khi gán. Vì có phép gán ở đâu đó trong hàm, Python xếp count vào local, và lúc đọc thì local chưa có giá trị. Biến toàn cục count = 0 không được dùng.

Đây là lỗi rất khó hiểu nếu không biết quy tắc: biến toàn cục rõ ràng tồn tại, nhưng hàm vẫn báo chưa gán.

global — dùng hạn chế

Khai báo global buộc tên trỏ về phạm vi module:

code
count = 0

def tang():
    global count
    count += 1

tang()
tang()
print(count)
code
2

Chạy đúng, nhưng đây là thiết kế nên tránh:

  • Hàm phụ thuộc trạng thái bên ngoài, khó kiểm thử độc lập.
  • Gọi hàm hai lần cho kết quả khác nhau — khó suy luận.
  • Nhiều hàm cùng sửa một biến toàn cục tạo bug khó truy vết.
  • Trong đa luồng, biến toàn cục cần đồng bộ.

Cách tốt hơn là truyền vào và trả ra:

code
def tang(count):
    return count + 1

count = 0
count = tang(count)
count = tang(count)
print(count)
code
2

Hàm thuần (chỉ phụ thuộc tham số, chỉ trả kết quả) dễ kiểm thử và tái sử dụng. global chỉ nên xuất hiện trong script nhỏ hoặc khi thật sự cần trạng thái module.

nonlocal và closure

Hàm lồng nhau có thể đọc biến của hàm ngoài. Muốn gán vào biến đó, cần nonlocal:

code
def outer():
    total = 0

    def inner(x):
        nonlocal total
        total += x
        return total

    print(inner(5))
    print(inner(3))
    return total

print(outer())
code
5
8
8

nonlocal total nói rằng total thuộc hàm bao ngoài, không phải local của inner. Không có nonlocal, dòng total += x sẽ gây UnboundLocalError giống trường hợp global.

Closure hữu ích khi cần state riêng cho từng thực thể:

code
def make_counter():
    count = 0

    def counter():
        nonlocal count
        count += 1
        return count

    return counter

c = make_counter()
print(c(), c(), c())
code
1 2 3

Mỗi lần gọi make_counter() tạo một count riêng. Hai bộ đếm độc lập nhau — điều mà biến toàn cục không làm được.

Phạm vi trong comprehension

Từ Python 3, biến trong comprehension có phạm vi riêng, không rò ra ngoài:

code
vals = [x for x in range(3)]
print(vals)
print(x)
code
[0, 1, 2]
NameError: name 'x' is not defined

Đây là thay đổi so với Python 2 và là lý do không thể dùng biến vòng lặp sau comprehension. Nếu cần giá trị cuối, gán tường minh trong vòng lặp thường.

Che builtin

Đặt tên biến trùng builtin sẽ che nó trong phạm vi hiện tại:

code
list = [1, 2, 3]
print(list)
# print(list("abc"))   # TypeError: 'list' object is not callable

Sau khi gán list, hàm list() không còn gọi được trong phạm vi đó. Tương tự với sum, max, input, id, type. Tránh đặt tên biến là list, dict, sum, max, min, input, file, type. Nếu lỡ che, del list hoặc khởi động lại kernel/REPL.

Thực hành

  1. Gán count = 5 trong hàm và kiểm tra biến ngoài không đổi.
  2. Tái hiện UnboundLocalError với count += 1, đọc thông báo.
  3. Sửa bằng global, sau đó sửa lại bằng truyền tham số và trả kết quả.
  4. Viết closure đếm bằng nonlocal, tạo hai bộ đếm độc lập.
  5. Dùng comprehension rồi thử in biến vòng lặp để thấy NameError.
  6. Đặt list = [1, 2], thử gọi list("abc"), rồi khôi phục.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
UnboundLocalErrorVừa đọc vừa gán trong hàmglobal/nonlocal hoặc truyền tham số
Biến ngoài không đổiHàm tạo biến local trùng tênTrả giá trị và gán lại
NameError sau comprehensionComprehension có phạm vi riêngGán kết quả hoặc dùng vòng lặp
TypeError: 'list' object is not callableChe builtin listĐổi tên biến, del list
Closure không giữ stateThiếu nonlocalThêm nonlocal
Hai bộ đếm dùng chung stateDùng biến toàn cụcDùng closure/class
global gây bug khó tái hiệnTrạng thái chia sẻThiết kế hàm thuần

Tóm tắt

  • Tên tìm theo LEGB; gán trong hàm tạo biến local.
  • Vừa đọc vừa gán cùng tên → UnboundLocalError.
  • global chạy được nhưng nên tránh; truyền tham số và trả kết quả tốt hơn.
  • nonlocal cho closure ghi vào biến của hàm ngoài.
  • Comprehension có phạm vi riêng; đừng đặt tên biến trùng builtin.

Dùng ← → để chuyển bài