ข้ามไปยังเนื้อหาหลัก
Editor

สโคปของตัวแปร โคลเชอร์ และกฎ LEGB ใน Python

ปานกลาง25 นาที5 แบบฝึกหัด70 XP

บทเรียนนี้แปลจากต้นฉบับภาษาอังกฤษRead in EnglishLeer en españolЧитать на русскомLer em portuguêsแจ้งข้อผิดพลาดในการแปล

0/5 แบบฝึกหัด

ลองนึกภาพบ้านที่มีหลายห้อง แต่ละห้องมีข้าวของของตัวเอง เช่น โคมไฟบนโต๊ะข้างเตียงในห้องนอน มีดในลิ้นชักห้องครัว คุณใช้ของในห้องที่คุณยืนอยู่ได้ และเดินไปที่โถงทางเดินเพื่อหยิบของส่วนกลางได้ แต่คุณเอื้อมเข้าไปในห้องของคนอื่นที่ปิดประตูอยู่ไม่ได้ ถ้าไม่เปิดประตูก่อน

ตัวแปรใน Python ก็ทำงานแบบเดียวกัน ตัวแปรทุกตัวอยู่ใน สโคป (scope) ซึ่งคือบริเวณหนึ่งในโค้ดที่ตัวแปรนั้นมีอยู่และใช้งานได้ การเข้าใจสโคปคือกุญแจสำคัญในการเขียนฟังก์ชันที่เรียบร้อย ไม่เผลอเขียนทับข้อมูลของกันและกัน

ในบทเรียนนี้ คุณจะได้เรียนสโคป 4 ระดับใน Python (กฎ LEGB) วิธีใช้คีย์เวิร์ด global และ nonlocal และวิธีที่โคลเชอร์ (closure) ช่วยให้ฟังก์ชันด้านในจำค่าจากฟังก์ชันแม่ได้ เมื่อจบบทเรียน คุณจะดีบัก error แบบ "หาตัวแปรไม่เจอ" ที่ชวนงงที่สุดได้อย่างมั่นใจ

สโคปโลคอลคืออะไร

เมื่อคุณสร้างตัวแปรภายในฟังก์ชัน ตัวแปรนั้นจะมีอยู่แค่ภายในฟังก์ชันนั้น เราเรียกว่า สโคปโลคอล (local scope) ให้นึกว่าเป็นห้องส่วนตัว ไม่มีอะไรจากนอกฟังก์ชันมองเห็นหรือแตะต้องมันได้

ตัวแปรโลคอลอยู่ภายในฟังก์ชัน
def greet():
message = "Hello from inside!"
print(message)
greet()
# This would cause an error:
# print(message) # NameError: name 'message' is not defined
ผลลัพธ์
Hello from inside!

ตัวแปร message ถูกสร้างภายใน greet() จึงเป็นตัวแปรโลคอลของฟังก์ชันนั้น เมื่อ greet() ทำงานเสร็จ message ก็หายไป ถ้าคุณพยายามใช้มันนอกฟังก์ชัน Python จะแจ้ง NameError

การเรียกฟังก์ชันแต่ละครั้งจะสร้างชุดตัวแปรโลคอลขึ้นใหม่ ค่าไม่ถูกส่งต่อจากการเรียกครั้งก่อน:

ตัวแปรโลคอลเริ่มใหม่ทุกครั้งที่เรียก
def count_up():
x = 0
x = x + 1
print(f"x is {x}")
count_up() # x is 1
count_up() # x is 1 (starts fresh each time)
count_up() # x is 1
ผลลัพธ์
x is 1
x is 1
x is 1

สโคปโกลบอลคืออะไร

ตัวแปรที่สร้างนอกฟังก์ชันใดๆ จะอยู่ใน สโคปโกลบอล (global scope) เหมือนของที่วางอยู่ในโถงทางเดินของบ้าน ทุกห้องมองเห็นได้

ฟังก์ชันมองเห็นตัวแปรโกลบอลได้
language = "Python" # Global variable
def show_language():
print(f"Language: {language}")
def show_greeting():
print(f"Welcome to {language}!")
show_language()
show_greeting()
ผลลัพธ์
Language: Python
Welcome to Python!

ทั้งสองฟังก์ชัน อ่าน ตัวแปรโกลบอล language ได้ แต่จะเกิดอะไรขึ้นถ้าคุณพยายาม เปลี่ยน ค่าของมันภายในฟังก์ชัน

แก้ค่าตัวแปรโกลบอลตรงๆ ไม่ได้
counter = 0
def increment():
counter = counter + 1 # This causes an error!
print(counter)
# Uncomment the line below to see the error:
# increment()
ผลลัพธ์
(ไม่มีผลลัพธ์)

โค้ดนี้พังด้วย UnboundLocalError เพราะ Python เห็น counter = ... ภายในฟังก์ชัน จึงถือว่า counter เป็นตัวแปรโลคอล แต่ฝั่งขวากลับพยายามอ่าน counter ก่อนที่จะมีการกำหนดค่าให้ภายในฟังก์ชัน Python จึงสับสน

กฎ LEGB: Python หาตัวแปรอย่างไร

เมื่อคุณใช้ชื่อตัวแปร Python จะค้นหาใน 4 ที่ตามลำดับที่แน่นอน เรียกว่า กฎ LEGB:

  • L - Local (โลคอล): ตัวแปรของฟังก์ชันปัจจุบัน
  • E - Enclosing (ที่ครอบอยู่): ตัวแปรในฟังก์ชันชั้นนอก (ฟังก์ชันแม่) ใดๆ (สำหรับฟังก์ชันที่ซ้อนกัน)
  • G - Global (โกลบอล): ตัวแปรที่ระดับบนสุดของโมดูล (ไฟล์)
  • B - Built-in (ในตัว): ชื่อในตัวของ Python เช่น print, len, range
  • Python ตรวจตามลำดับนี้: L ก่อน แล้ว E แล้ว G แล้ว B และหยุดที่ชื่อแรกที่เจอ ถ้าไม่เจอเลยสักที่ คุณจะได้ NameError

    กฎ LEGB ในการทำงานจริง
    # B - Built-in: len is always available
    # G - Global scope
    x = "global x"
    def outer():
    # E - Enclosing scope
    x = "enclosing x"
    def inner():
    # L - Local scope
    x = "local x"
    print(x) # Finds "local x" first (L)
    inner()
    print(x) # Finds "enclosing x" (E is now L for outer)
    outer()
    print(x) # Finds "global x" (G)
    ผลลัพธ์
    local x
    enclosing x
    global x

    print(x) แต่ละตัวเจอ x คนละตัว เพราะมันค้นหาจากสโคปของตัวเองออกไปด้านนอก ฟังก์ชันด้านในเจอ x ที่เป็นโลคอลของตัวเองก่อน จึงหยุดค้นหา

    คีย์เวิร์ด global

    ถ้าคุณต้อง แก้ไข ตัวแปรโกลบอลจากภายในฟังก์ชัน คุณต้องบอก Python อย่างชัดเจนด้วยคีย์เวิร์ด global เหมือนได้กุญแจพิเศษที่ให้เอื้อมไปจัดของในโถงทางเดินได้

    การใช้คีย์เวิร์ด global
    score = 0
    def add_points(points):
    global score
    score = score + points
    print(f"Score is now {score}")
    add_points(10)
    add_points(25)
    add_points(5)
    print(f"Final score: {score}")
    ผลลัพธ์
    Score is now 10
    Score is now 35
    Score is now 40
    Final score: 40

    การประกาศ global score บอก Python ว่า "ฉันไม่ได้สร้างตัวแปรโลคอลใหม่ ฉันต้องการใช้ตัวแปรโกลบอล" ตอนนี้การกำหนดค่า score = score + points ก็ทำงานได้ถูกต้อง

    ใช้ global (ตามรอยยากกว่า)
    total = 0
    
    def add(n):
        global total
        total = total + n
    
    add(5)
    add(3)
    print(total)  # 8
    ใช้ return (สะอาดกว่า)
    def add(current, n):
        return current + n
    
    total = 0
    total = add(total, 5)
    total = add(total, 3)
    print(total)  # 8

    สโคปที่ครอบอยู่และโคลเชอร์

    เมื่อคุณนิยามฟังก์ชันไว้ภายในอีกฟังก์ชันหนึ่ง ฟังก์ชันด้านในจะเข้าถึงตัวแปรของฟังก์ชันด้านนอกได้ สโคปของฟังก์ชันด้านนอกนี้เรียกว่า สโคปที่ครอบอยู่ (enclosing scope) ซึ่งก็คือ "E" ใน LEGB

    โคลเชอร์จำตัวแปรจากสโคปที่ครอบอยู่
    def make_greeting(greeting):
    def greet(name):
    return f"{greeting}, {name}!"
    return greet
    hello = make_greeting("Hello")
    howdy = make_greeting("Howdy")
    print(hello("Alice"))
    print(hello("Bob"))
    print(howdy("Alice"))
    ผลลัพธ์
    Hello, Alice!
    Hello, Bob!
    Howdy, Alice!

    ตรงนี้มีเรื่องน่าทึ่งเกิดขึ้น make_greeting("Hello") ทำงานเสร็จและคืนฟังก์ชัน greet ที่อยู่ด้านในออกมา ปกติตัวแปรโลคอล greeting จะถูกทำลาย แต่ฟังก์ชันด้านในยังต้องใช้มันอยู่

    Python เก็บ greeting ไว้ไม่ให้หายไป เพราะ greet อ้างถึงมันอยู่ การรวมกันของฟังก์ชันกับตัวแปรที่มันจำไว้จากสโคปที่ครอบอยู่นี้เรียกว่า โคลเชอร์ (closure)

    โคลเชอร์มีประโยชน์สำหรับสร้างฟังก์ชันเฉพาะทาง นี่คือตัวอย่างที่ใช้ได้จริง: โรงงานผลิตฟังก์ชันคูณ

    โคลเชอร์ในฐานะโรงงานผลิตฟังก์ชัน
    def make_multiplier(factor):
    def multiply(number):
    return number * factor
    return multiply
    double = make_multiplier(2)
    triple = make_multiplier(3)
    print(double(5)) # 10
    print(triple(5)) # 15
    print(double(100)) # 200
    ผลลัพธ์
    10
    15
    200

    คีย์เวิร์ด nonlocal

    เช่นเดียวกับที่ global ให้คุณแก้ไขตัวแปรโกลบอล nonlocal ให้คุณแก้ไขตัวแปรในสโคปที่ครอบอยู่ ถ้าไม่ใช้ การกำหนดค่าให้ตัวแปรจะกลายเป็นการสร้างตัวแปรโลคอลตัวใหม่แทน

    ใช้ nonlocal แก้ไขตัวแปรในสโคปที่ครอบอยู่
    def make_counter():
    count = 0
    def increment():
    nonlocal count
    count = count + 1
    return count
    return increment
    counter = make_counter()
    print(counter()) # 1
    print(counter()) # 2
    print(counter()) # 3
    ผลลัพธ์
    1
    2
    3

    ถ้าไม่มี nonlocal count บรรทัด count = count + 1 จะพังด้วย UnboundLocalError ซึ่งเป็นปัญหาเดียวกับที่เราเจอกับตัวแปรโกลบอล คีย์เวิร์ด nonlocal บอก Python ให้ใช้ตัวแปรชื่อนี้จากฟังก์ชันที่ครอบอยู่ซึ่งใกล้ที่สุด

    รูปแบบนี้ทรงพลังมาก ตัวแปร count ถูกซ่อนจากโลกภายนอกโดยสมบูรณ์ ไม่มีโค้ดใดนอก make_counter เข้าถึงหรือแอบแก้ไขมันได้ นี่คือรูปแบบหนึ่งของ การห่อหุ้ม (encapsulation) คือการเก็บข้อมูลไว้เป็นส่วนตัว

    ข้อผิดพลาดเรื่องสโคปที่พบบ่อยและวิธีแก้

    นี่คือบั๊กเรื่องสโคปที่นักพัฒนา Python พลาดกันบ่อยที่สุด การรู้จักรูปแบบเหล่านี้จะช่วยประหยัดเวลาดีบักได้หลายชั่วโมง

    ข้อผิดพลาดที่ 1: ตัวแปรโลคอลบังตัวแปรโกลบอล

    Shadowing: ตัวแปรโลคอลบังตัวแปรโกลบอล
    items = ["apple", "banana", "cherry"]
    def process():
    items = [] # Creates a NEW local variable, doesn't touch the global!
    items.append("date")
    print(f"Inside: {items}")
    process()
    print(f"Outside: {items}") # Global is unchanged!
    ผลลัพธ์
    Inside: ['date']
    Outside: ['apple', 'banana', 'cherry']

    items = [] ภายในฟังก์ชันสร้างตัวแปรโลคอลตัวใหม่ที่ บัง (shadow) ตัวแปรโกลบอลไว้ ลิสต์ items ที่เป็นโกลบอลจึงไม่เคยถูกแก้ไขเลย ส่วนใหญ่แล้วนี่คือบั๊ก

    ข้อผิดพลาดที่ 2: UnboundLocalError จากการอ่านและเขียนปนกัน

    อธิบาย UnboundLocalError
    total = 100
    def update():
    # Python sees "total = ..." below and marks total as local
    # But this line tries to READ it before it's assigned locally
    print(total) # UnboundLocalError!
    total = 200
    # Uncomment to see the error:
    # update()
    ผลลัพธ์
    (ไม่มีผลลัพธ์)

    Python อ่านตัวฟังก์ชันทั้งหมดก่อนรัน ถ้าเห็นการกำหนดค่าให้ total ที่ไหนก็ตาม มันจะถือว่า total เป็นตัวแปรโลคอลตลอดทั้งฟังก์ชัน บรรทัด print(total) จึงพยายามอ่านตัวแปรโลคอลที่ยังไม่ได้กำหนดค่า


    แบบฝึกหัด

    ได้เวลาฝึกใช้สโคปและโคลเชอร์แล้ว แบบฝึกหัดเหล่านี้ไล่ตั้งแต่การอ่านสโคปไปจนถึงการสร้างโคลเชอร์ของคุณเอง

    แบบฝึกหัดที่ 1: ทายผลลัพธ์เรื่องสโคป
    ทายผลลัพธ์

    อ่านโค้ดนี้อย่างละเอียดแล้วทายว่าจะแสดงผลอะไร จากนั้นเขียนคำสั่ง print() ที่แสดงผลลัพธ์นั้นได้ตรงทุกตัวอักษร

    x = 10
    
    def foo():
        x = 20
        print(x)
    
    foo()
    print(x)

    แสดงตัวเลขสองตัวคนละบรรทัด ให้ตรงกับที่โค้ดด้านบนแสดงผล

    # Print the exact output of the code above
    แบบฝึกหัดที่ 2: แก้บั๊กตัวนับแบบโกลบอล
    แก้บั๊ก

    โค้ดนี้พยายามเพิ่มค่าตัวนับที่เป็นโกลบอล แต่กลับพังด้วย UnboundLocalError แก้ให้ได้ผลลัพธ์เป็น:

    1
    2
    3
    count = 0
    def increment():
    count = count + 1
    print(count)
    increment()
    increment()
    increment()
    แบบฝึกหัดที่ 3: สร้างโคลเชอร์สำหรับคำทักทาย
    เขียนโค้ด

    เขียนฟังก์ชันชื่อ make_greeter ที่รับสตริง greeting เป็นอาร์กิวเมนต์ และคืนฟังก์ชันใหม่ออกมา ฟังก์ชันที่คืนออกมานั้นต้องรับอาร์กิวเมนต์ name และแสดงคำทักทายพร้อมชื่อ

    ตัวอย่างเช่น:

    hello = make_greeter("Hello")
    hello("World")

    ต้องแสดงผล: Hello, World!

    การทดสอบจะเรียก:

    hi = make_greeter("Hi")
    hi("Alice")
    hi("Bob")

    ผลลัพธ์ที่ต้องได้:

    Hi, Alice!
    Hi, Bob!
    def make_greeter(greeting):
    # Return a function that prints greeting + name
    pass
    hi = make_greeter("Hi")
    hi("Alice")
    hi("Bob")
    แบบฝึกหัดที่ 4: สร้างตัวนับด้วย nonlocal
    เขียนโค้ด

    เขียนฟังก์ชันชื่อ make_counter ที่รับค่า start และคืนฟังก์ชันออกมา ทุกครั้งที่เรียกฟังก์ชันที่คืนออกมา ฟังก์ชันนั้นต้องแสดงค่าตัวนับปัจจุบัน แล้วเพิ่มค่าขึ้น 1

    การทดสอบจะเรียก:

    c = make_counter(1)
    c()
    c()
    c()

    ผลลัพธ์ที่ต้องได้:

    1
    2
    3
    def make_counter(start):
    # Your code here
    pass
    c = make_counter(1)
    c()
    c()
    c()
    แบบฝึกหัดที่ 5: สร้างโคลเชอร์หาค่าเฉลี่ยสะสม
    เขียนโค้ด

    เขียนฟังก์ชันชื่อ make_averager ที่คืนฟังก์ชันออกมา ทุกครั้งที่เรียกฟังก์ชันที่คืนออกมาพร้อมตัวเลขหนึ่งตัว ฟังก์ชันนั้นต้องแสดงค่าเฉลี่ยสะสมของตัวเลขทั้งหมดที่ได้รับมาจนถึงตอนนี้

    ค่าเฉลี่ยสะสมคือผลรวมของตัวเลขทั้งหมดหารด้วยจำนวนตัวเลข

    การทดสอบจะเรียก:

    avg = make_averager()
    avg(10)
    avg(20)
    avg(30)

    ผลลัพธ์ที่ต้องได้:

    10.0
    15.0
    20.0

    คำใบ้: ตัวหาค่าเฉลี่ยต้องจำทั้งผลรวมและจำนวนตัวเลข

    def make_averager():
    # Your code here
    pass
    avg = make_averager()
    avg(10)
    avg(20)
    avg(30)

    สรุป

    สิ่งที่คุณได้เรียนเกี่ยวกับสโคปใน Python:

    แนวคิดความหมาย
    สโคปโลคอลตัวแปรที่สร้างภายในฟังก์ชัน มีอยู่เฉพาะในฟังก์ชันนั้น
    สโคปโกลบอลตัวแปรที่สร้างที่ระดับโมดูล มองเห็นได้ทุกที่ในโมดูลนั้น
    กฎ LEGBPython ค้นหาใน Local, Enclosing, Global, Built-in ตามลำดับนี้
    คีย์เวิร์ด globalให้ฟังก์ชันแก้ไขตัวแปรระดับโมดูลได้
    สโคปที่ครอบอยู่ตัวแปรของฟังก์ชันชั้นนอก ซึ่งฟังก์ชันด้านในเข้าถึงได้
    โคลเชอร์ฟังก์ชันที่จำตัวแปรจากสโคปที่ครอบอยู่ได้
    คีย์เวิร์ด nonlocalให้ฟังก์ชันด้านในแก้ไขตัวแปรในสโคปที่ครอบอยู่ได้

    กฎทอง: ส่งค่าผ่านอาร์กิวเมนต์และคืนค่ากลับ ดีกว่าใช้ global หรือ nonlocal ข้อยกเว้นเดียวที่สโคปที่ครอบอยู่ได้เฉิดฉายคือโคลเชอร์ ซึ่งช่วยให้คุณสร้างฟังก์ชันเฉพาะทางที่มีสถานะ (state) ส่วนตัวได้

    เรียนอะไรต่อ

    เมื่อเข้าใจสโคปและโคลเชอร์แล้ว คุณก็พร้อมสำหรับ การเรียกซ้ำ (recursion) ที่ฟังก์ชันเรียกตัวเอง ความเข้าใจเรื่องสโคปสำคัญมากสำหรับการเรียกซ้ำ เพราะการเรียกซ้ำแต่ละครั้งจะได้สโคปโลคอลของตัวเอง

    บทเรียนที่เกี่ยวข้อง

    บทเรียนนี้มีประโยชน์หรือไม่