Notes & software courses · Free to learn
Aph's Blog
On this page

Scope & Closure

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

ตัวแปรอยู่ "ที่ไหน" และฟังก์ชันจำค่าจาก scope แม่ได้อย่างไร — รากฐานของ decorator

scope คือ "ขอบเขตที่ตัวแปรมองเห็นได้" การเข้าใจว่า Python ค้นหาตัวแปรอย่างไรช่วยให้ไม่งงกับบั๊กแปลก ๆ และเป็นพื้นฐานของ closure ซึ่งต่อยอดไปสู่ decorator ในหัวข้อถัดไป

กฎ LEGB — Python หาตัวแปรจากไหน

เมื่อใช้ตัวแปร Python ค้นหาตามลำดับ Local → Enclosing → Global → Built-in เจอที่ไหนใช้ที่นั่น

ตัวอักษรขอบเขตคือ
LLocalในฟังก์ชันปัจจุบัน
EEnclosingฟังก์ชันแม่ที่ห่อหุ้มอยู่
GGlobalระดับไฟล์/โมดูล
BBuilt-inของ Python เอง (print, len, ...)
python
x = "global"

def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print(x)        # local (เจอ L ก่อน)
    inner()
    print(x)            # enclosing

outer()
print(x)                # global

global และ nonlocal

ปกติการกำหนดค่าในฟังก์ชันสร้างตัวแปร local ใหม่ ถ้าต้องการแก้ตัวแปรชั้นนอกจริง ๆ ต้องประกาศ global (ระดับไฟล์) หรือ nonlocal (ฟังก์ชันแม่)

python
count = 0

def increment():
    global count        # บอกว่าจะแก้ count ตัวข้างนอก
    count += 1

increment()
increment()
print(count)            # 2

def make_counter():
    n = 0
    def step():
        nonlocal n      # แก้ n ของฟังก์ชันแม่ (ไม่ใช่สร้างใหม่)
        n += 1
        return n
    return step

closure: ฟังก์ชันที่จำค่าจาก scope แม่

closure เกิดเมื่อฟังก์ชันชั้นในใช้ตัวแปรจากฟังก์ชันแม่ แล้วฟังก์ชันแม่คืนฟังก์ชันชั้นในออกมา — ฟังก์ชันชั้นในจะ "จำ" ตัวแปรนั้นไว้แม้ฟังก์ชันแม่จบไปแล้ว

python
def make_multiplier(factor):
    def multiply(x):
        return x * factor      # จำ factor จากแม่ไว้
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))    # 20  (จำ factor=2)
print(triple(10))    # 30  (จำ factor=3)

double กับ triple คือฟังก์ชันคนละตัว แต่ละตัวจำ factor ของตัวเองไว้ — นี่คือ closure

counter ด้วย closure

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

c = make_counter()
print(c())   # 1
print(c())   # 2
print(c())   # 3  (จำ count ไว้ข้ามการเรียก)
กับดัก: late binding ใน loop

ถ้าสร้าง closure ใน for-loop โดยอ้างตัวแปรลูป ทุก closure จะจับ "ตัวแปรเดียวกัน" ซึ่งมีค่าสุดท้ายหลัง loop จบ ไม่ใช่ค่าตอนสร้าง วิธีแก้: ส่งค่าผ่าน default argument เช่น lambda x, i=i: ... เพื่อ "แช่แข็ง" ค่า i ตอนนั้น

python
# ❌ ทุก f จะคืน 2 (ค่าสุดท้ายของ i)
funcs = []
for i in range(3):
    funcs.append(lambda: i)
print([f() for f in funcs])    # [2, 2, 2]

# ✅ แช่แข็งค่าด้วย default argument
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)
print([f() for f in funcs])    # [0, 1, 2]

สรุปหัวข้อนี้

  • LEGB: Python หาตัวแปร Local → Enclosing → Global → Built-in
  • global แก้ตัวแปรระดับไฟล์, nonlocal แก้ตัวแปรฟังก์ชันแม่
  • closure = ฟังก์ชันชั้นในที่จำค่าจาก scope แม่แม้แม่จบไปแล้ว
  • ระวัง late binding ใน loop — แช่แข็งค่าด้วย default argument
แบบฝึกหัด

1) เขียน make_adder(n) ที่คืนฟังก์ชันบวก n 2) เขียน counter ที่นับขึ้นเรื่อย ๆ ด้วย closure 3) ทดลองสร้าง lambda ใน loop แบบผิด แล้วแก้ให้ถูกด้วย default argument 4) อธิบายว่าทำไม global count จำเป็นในฟังก์ชัน increment