Học bằng cách đọc theo thứ tự

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 globalnonlocal để 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
Trong và Ngoài là các phạm vi tách biệt — Sơ đồ tập hợp
Module (Không gian tên Global)
  • stock = 100 — biến global
  • Hàm chỉ có thể đọc từ bên trong
Không gian tên local của show_stock()
  • print(stock) → đọc 100 bên ngoài
  • Không tạo biến local
Không gian tên local của try_change()
  • stock = 50 — tạo local mới bên trong hàm
  • Không ảnh hưởng tới stock bên ngoài
Không gian tên local lồng vào trong module. Đọc thì vươn ra ngoài, nhưng phép gán bị niêm phong thành local riêng.

Dùng biến global stock để xác nhận rằng đọc thì vươn ra ngoài, nhưng gán bên trong thì không.

① Khai báo global stock = 100.

② Định nghĩa def show_stock(): và in f"trong: {stock}".

③ Định nghĩa def try_change(): và gán stock = 50, rồi in f"trong: {stock}".

④ Gọi show_stock()try_change()print(f"ngoài: {stock}") và kiểm tra giá trị bên ngoài không thay đổi.

(Khi đáp án đúng, phần giải thích sẽ xuất hiện.)

Python Editor

Chạy code để xem đầu ra

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").

global khai báo "biến bên ngoài đó"
không globalcount += 1UnboundLocalErrorglobal countcount += 1ghi đècount bên ngoàithất bạithành công
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.

Xây bộ đếm lượt truy cập trang bằng global.

① Khai báo visit_count = 0 ngoài mọi hàm.

② Định nghĩa def increment_visit():. Ở đầu hàm, viết global visit_count, rồi visit_count += 1.

③ Gọi increment_visit() ba lần, rồi in f"lượt truy cập: {visit_count}".

Python Editor

Chạy code để xem đầu ra

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.

Module và Frame của hàm trong bộ nhớ
Không gian tên Global (Module)
  • x = 5 — tên x trỏ tới số nguyên 5
  • def f — tên f trỏ tới đối tượng hàm
  • Sống đến khi chương trình thoát
Frame lần gọi đầu của f()
  • Giữ đối số và biến local
  • Bị bỏ hoàn toàn khi return
Frame lần gọi thứ hai của f()
  • Hoàn toàn tách biệt với frame đầu
  • Biến local độc lập
Mỗi lần gọi hàm tạo một không gian tên local mới (frame) trong bộ nhớ, rồi biến mất khi thoát. Chỉ có một không gian tên global cho toàn chương trình.
Cách def và lệnh gọi hàm dùng bộ nhớ
①Chạy`def f(): ...`Đăng ký hàmvào ns globalGiữ trong ns globalđến khi thoát②Gọi lần 1: f()Tạo không gian tênlocal mới (frame)Bỏ framekhi `return`③Gọi lần 2: f()Tạo frame hoàn toàn mới(độc lập với #1)Bỏ framekhi `return`lúc chạygọikhi thoátgọi lạikhi thoát
Chạy 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.

Phạm vi của hàm lồng — Sơ đồ tập hợp
Module (Không gian tên Global)
  • Gọi validate từ ngoài này → NameError
  • Không tồn tại bên ngoài
Frame của process_user()
  • Giữ đối số name / age
  • Có thể gọi validate từ bên trong
Frame của validate()
  • Đọc name / age bên ngoài
  • Không lộ ra ngoài
validate chỉ sống bên trong process_user. Nó có thể đọc đối số bên ngoài, và thế giới bên ngoài không bao giờ thấy nó.
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ĩaprocess_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.

Tái cấu trúc một hàm xử lý đơn hàng với hàm lồng riêng cho việc kiểm tra đầu vào.

① Định nghĩa def process_order(item, quantity):.

② Bên trong, định nghĩa def validate():. Bên trong nó, raise ValueError("Đơn không hợp lệ") nếu item rỗng, quantity không phải int, hoặc quantity bằng 0 hoặc nhỏ hơn.

③ Gọi validate(), rồi in f"Đã nhận đơn: {quantity} x {item}".

④ Gọi process_order("táo", 3) và kiểm tra kết quả.

Python Editor

Chạy code để xem đầu ra

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 hoạt động thế nào — Định nghĩa và luồng gọi
①Gọimake_multiplier(3)②Frame ngoàitạo với factor=3④return multiply(nhớ factor=3)③def multiplytham chiếu factor⑤times3 = make_multiplier(3)⑥times3(10)→ 10 × 3(factor) = 30chạyđịnh nghĩa bên trongtrả về hàm nhớnhậngọi
Frame ngoài của make_multiplier biến mất khi return, nhưng multiply được tạo bên trong vẫn đi ra ngoài và nhớ factor=3.

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.

Xây một hàm trả về hàm áp dụng giảm giá nhớ tỷ lệ giảm.

① Định nghĩa def make_discounter(rate):. Bên trong, định nghĩa def apply(price): trả về int(price * (1 - rate)). Đừng quên return apply.

② Tạo hai hàm: discount_10 = make_discounter(0.1)discount_30 = make_discounter(0.3).

③ In discount_10(1000)discount_30(1000), và kiểm tra rằng các tỷ lệ giảm khác nhau áp dụng cho cùng một giá.

Python Editor

Chạy code để xem đầu ra

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
nonlocal cập nhật biến của hàm bao quanh — Sơ đồ tập hợp
Module (Không gian tên Global)
  • counter = create_counter() — nhận increment
  • Code bên ngoài không thể chạm vào x trực tiếp
Frame của create_counter()
  • x = 0 — bộ đếm, được tạo đúng một lần
  • Cái mà increment tham chiếu qua nonlocal
Frame của increment()
  • nonlocal x — trỏ tới x bên ngoài
  • x += 1 cập nhật x bên ngoài
nonlocal nhắm tới biến của hàm bao quanh ngay bên ngoài. Trạng thái sống bên trong đối tượng hàm, không phải không gian tên global.

Xây một hàm kiểu bộ phát ID phát ra ID đơn hàng bắt đầu từ 1, dùng closure với nonlocal.

① Định nghĩa def create_order_id_issuer(): và khởi tạo next_id = 1 ở đầu.

② Bên trong, định nghĩa def issue():. Sau nonlocal next_id, viết current = next_idnext_id += 1return current.

③ Cuối cùng return issue để trao hàm lồng ra ngoài.

④ Lấy nó qua issue_id = create_order_id_issuer() và gọi print(issue_id()) ba lần — xác nhận thấy 1 → 2 → 3.

Python Editor

Chạy code để xem đầu ra
QUIZ

Kiểm tra kiến thức

Hãy trả lời từng câu hỏi một.

Câu 1Code này in ra gì?
stock = 100
def f():
stock = 50
f()
print(stock)

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)