2.16. Phạm vi

Khi Python tra cứu một tên bên trong hàm, nó tìm kiếm theo một chuỗi phạm vi cụ thể. Hiểu chuỗi đó giải thích tại sao một số phép gán che khuất tên bên ngoài, tại sao một số khác sửa đổi chúng, và tại sao các hàm lồng nhau có thể nhớ các giá trị từ nơi chúng được định nghĩa.

Nested boxes showing a local scope inside a module scope inside the built-in scope, with an arrow indicating that name lookup walks outward from the innermost frame.

Việc tra cứu tên bắt đầu từ phạm vi cục bộ của hàm và đi ra ngoài đến phạm vi mô-đun và phạm vi tích hợp sẵn cho đến khi tìm thấy kết quả khớp.

2.16.1. Phạm vi cục bộ và phạm vi mô-đun

Các tên được định nghĩa bên trong một hàm là cục bộ của hàm đó và biến mất khi lời gọi kết thúc:

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

Các tên được định nghĩa ở cấp cao nhất của tệp .pycấp mô-đun (đôi khi gọi là toàn cục) và có thể nhìn thấy ở khắp nơi trong tệp đó, kể cả bên trong các hàm:

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

Đọc tên cấp mô-đun từ bên trong hàm là tự động. Gán cho tên đó từ bên trong hàm thì không -- phép gán tạo ra một biến cục bộ mới che khuất tên cấp mô-đun trong suốt phần còn lại của lời gọi:

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

counter ở vế trái làm cho counter trở thành tên cục bộ trong bump, vì vậy việc đọc ở vế phải không có giá trị để tìm.

2.16.1.1. Từ khóa global

Để thực sự gán lại tên cấp mô-đun từ bên trong hàm, hãy khai báo nó là global trước:

counter = 0

def bump():
    global counter
    counter += 1

Dùng global một cách thận trọng. Các hàm thay đổi trạng thái ẩn khó suy luận hơn các hàm nhận giá trị qua đối số và trả về giá trị mới. Giải pháp thường gặp cho "tôi cần chia sẻ trạng thái" là truyền một đối tượng (list, dict, instance của class) như một đối số và thay đổi thay thế.

2.16.2. Lambda

Một lambda xây dựng một hàm ẩn danh nhỏ trong một biểu thức duy nhất:

square = lambda x: x * x
square(7)             # 49

Nó hoàn toàn tương đương với

def square(x):
    return x * x

Thân của một lambda phải là một biểu thức đơn -- không có câu lệnh, không có nhiều dòng. Cách dùng chính là truyền một hàm nhỏ như đối số cho thứ gì đó nhận một hàm:

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

Khi thân hàm dài hơn một biểu thức, hãy chuyển sang dùng def thực sự. Đặt tên hàm bằng def cũng đặt tên cho nó trong tracebacks, điều mà lambda không có.

2.16.3. Closure

Một hàm được định nghĩa bên trong một hàm khác có thể đọc các tên từ phạm vi của hàm bao. Hàm bên trong nắm bắt các tên đó và tiếp tục hoạt động ngay cả sau khi lời gọi bên ngoài đã trả về:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

Kết quả:

105 110

add5add10 là hai hàm riêng biệt, mỗi hàm nhớ n của riêng nó. Một hàm xây dựng và trả về một hàm bên trong được tùy chỉnh theo cách này được gọi là closure. Đây là lý do chính một ngôn ngữ cần các hàm lồng nhau -- một cách để đưa một số trạng thái vào một giá trị hàm và sau đó chuyển giá trị đó đi như một callable duy nhất.

Việc đọc các tên được nắm bắt xảy ra tự động. Gán lại một trong số chúng cần một từ khóa thêm. Ví dụ bên dưới trông có vẻ đúng nhưng lại thất bại:

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

Phép gán cho count bên trong tick làm cho count trở thành cục bộ của tick, giống như cách nó sẽ làm cho nó cục bộ trong một hàm cấp cao nhất. Từ khóa nonlocal nói với Python "tên này nằm trong hàm bao, hãy gán lại nó ở đó":

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

Kết quả:

1 2 3

nonlocal đối với phạm vi hàm bao giống như global đối với phạm vi mô-đun. Lưu ý rằng việc thay đổi một đối tượng được nắm bắt (gọi some_list.append(...), some_dict[k] = v) không cần nonlocal -- tên không bị gán lại, chỉ là đối tượng nó trỏ đến đang bị thay đổi.