สโคปของตัวแปร โคลเชอร์ และกฎ LEGB ใน Python
บทเรียนนี้แปลจากต้นฉบับภาษาอังกฤษRead in EnglishLeer en españolЧитать на русскомLer em portuguêsแจ้งข้อผิดพลาดในการแปล
ลองนึกภาพบ้านที่มีหลายห้อง แต่ละห้องมีข้าวของของตัวเอง เช่น โคมไฟบนโต๊ะข้างเตียงในห้องนอน มีดในลิ้นชักห้องครัว คุณใช้ของในห้องที่คุณยืนอยู่ได้ และเดินไปที่โถงทางเดินเพื่อหยิบของส่วนกลางได้ แต่คุณเอื้อมเข้าไปในห้องของคนอื่นที่ปิดประตูอยู่ไม่ได้ ถ้าไม่เปิดประตูก่อน
ตัวแปรใน Python ก็ทำงานแบบเดียวกัน ตัวแปรทุกตัวอยู่ใน สโคป (scope) ซึ่งคือบริเวณหนึ่งในโค้ดที่ตัวแปรนั้นมีอยู่และใช้งานได้ การเข้าใจสโคปคือกุญแจสำคัญในการเขียนฟังก์ชันที่เรียบร้อย ไม่เผลอเขียนทับข้อมูลของกันและกัน
ในบทเรียนนี้ คุณจะได้เรียนสโคป 4 ระดับใน Python (กฎ LEGB) วิธีใช้คีย์เวิร์ด global และ nonlocal และวิธีที่โคลเชอร์ (closure) ช่วยให้ฟังก์ชันด้านในจำค่าจากฟังก์ชันแม่ได้ เมื่อจบบทเรียน คุณจะดีบัก error แบบ "หาตัวแปรไม่เจอ" ที่ชวนงงที่สุดได้อย่างมั่นใจ
สโคปโลคอลคืออะไร
เมื่อคุณสร้างตัวแปรภายในฟังก์ชัน ตัวแปรนั้นจะมีอยู่แค่ภายในฟังก์ชันนั้น เราเรียกว่า สโคปโลคอล (local scope) ให้นึกว่าเป็นห้องส่วนตัว ไม่มีอะไรจากนอกฟังก์ชันมองเห็นหรือแตะต้องมันได้
Hello from inside!
ตัวแปร message ถูกสร้างภายใน greet() จึงเป็นตัวแปรโลคอลของฟังก์ชันนั้น เมื่อ greet() ทำงานเสร็จ message ก็หายไป ถ้าคุณพยายามใช้มันนอกฟังก์ชัน Python จะแจ้ง NameError
การเรียกฟังก์ชันแต่ละครั้งจะสร้างชุดตัวแปรโลคอลขึ้นใหม่ ค่าไม่ถูกส่งต่อจากการเรียกครั้งก่อน:
x is 1 x is 1 x is 1
สโคปโกลบอลคืออะไร
ตัวแปรที่สร้างนอกฟังก์ชันใดๆ จะอยู่ใน สโคปโกลบอล (global scope) เหมือนของที่วางอยู่ในโถงทางเดินของบ้าน ทุกห้องมองเห็นได้
Language: Python Welcome to Python!
ทั้งสองฟังก์ชัน อ่าน ตัวแปรโกลบอล language ได้ แต่จะเกิดอะไรขึ้นถ้าคุณพยายาม เปลี่ยน ค่าของมันภายในฟังก์ชัน
(ไม่มีผลลัพธ์)
โค้ดนี้พังด้วย UnboundLocalError เพราะ Python เห็น counter = ... ภายในฟังก์ชัน จึงถือว่า counter เป็นตัวแปรโลคอล แต่ฝั่งขวากลับพยายามอ่าน counter ก่อนที่จะมีการกำหนดค่าให้ภายในฟังก์ชัน Python จึงสับสน
กฎ LEGB: Python หาตัวแปรอย่างไร
เมื่อคุณใช้ชื่อตัวแปร Python จะค้นหาใน 4 ที่ตามลำดับที่แน่นอน เรียกว่า กฎ LEGB:
print, len, rangePython ตรวจตามลำดับนี้: L ก่อน แล้ว E แล้ว G แล้ว B และหยุดที่ชื่อแรกที่เจอ ถ้าไม่เจอเลยสักที่ คุณจะได้ NameError
local x enclosing x global x
print(x) แต่ละตัวเจอ x คนละตัว เพราะมันค้นหาจากสโคปของตัวเองออกไปด้านนอก ฟังก์ชันด้านในเจอ x ที่เป็นโลคอลของตัวเองก่อน จึงหยุดค้นหา
คีย์เวิร์ด global
ถ้าคุณต้อง แก้ไข ตัวแปรโกลบอลจากภายในฟังก์ชัน คุณต้องบอก Python อย่างชัดเจนด้วยคีย์เวิร์ด global เหมือนได้กุญแจพิเศษที่ให้เอื้อมไปจัดของในโถงทางเดินได้
Score is now 10 Score is now 35 Score is now 40 Final score: 40
การประกาศ global score บอก Python ว่า "ฉันไม่ได้สร้างตัวแปรโลคอลใหม่ ฉันต้องการใช้ตัวแปรโกลบอล" ตอนนี้การกำหนดค่า score = score + points ก็ทำงานได้ถูกต้อง
total = 0
def add(n):
global total
total = total + n
add(5)
add(3)
print(total) # 8def add(current, n):
return current + n
total = 0
total = add(total, 5)
total = add(total, 3)
print(total) # 8สโคปที่ครอบอยู่และโคลเชอร์
เมื่อคุณนิยามฟังก์ชันไว้ภายในอีกฟังก์ชันหนึ่ง ฟังก์ชันด้านในจะเข้าถึงตัวแปรของฟังก์ชันด้านนอกได้ สโคปของฟังก์ชันด้านนอกนี้เรียกว่า สโคปที่ครอบอยู่ (enclosing scope) ซึ่งก็คือ "E" ใน LEGB
Hello, Alice! Hello, Bob! Howdy, Alice!
ตรงนี้มีเรื่องน่าทึ่งเกิดขึ้น make_greeting("Hello") ทำงานเสร็จและคืนฟังก์ชัน greet ที่อยู่ด้านในออกมา ปกติตัวแปรโลคอล greeting จะถูกทำลาย แต่ฟังก์ชันด้านในยังต้องใช้มันอยู่
Python เก็บ greeting ไว้ไม่ให้หายไป เพราะ greet อ้างถึงมันอยู่ การรวมกันของฟังก์ชันกับตัวแปรที่มันจำไว้จากสโคปที่ครอบอยู่นี้เรียกว่า โคลเชอร์ (closure)
โคลเชอร์มีประโยชน์สำหรับสร้างฟังก์ชันเฉพาะทาง นี่คือตัวอย่างที่ใช้ได้จริง: โรงงานผลิตฟังก์ชันคูณ
10 15 200
คีย์เวิร์ด nonlocal
เช่นเดียวกับที่ global ให้คุณแก้ไขตัวแปรโกลบอล nonlocal ให้คุณแก้ไขตัวแปรในสโคปที่ครอบอยู่ ถ้าไม่ใช้ การกำหนดค่าให้ตัวแปรจะกลายเป็นการสร้างตัวแปรโลคอลตัวใหม่แทน
1 2 3
ถ้าไม่มี nonlocal count บรรทัด count = count + 1 จะพังด้วย UnboundLocalError ซึ่งเป็นปัญหาเดียวกับที่เราเจอกับตัวแปรโกลบอล คีย์เวิร์ด nonlocal บอก Python ให้ใช้ตัวแปรชื่อนี้จากฟังก์ชันที่ครอบอยู่ซึ่งใกล้ที่สุด
รูปแบบนี้ทรงพลังมาก ตัวแปร count ถูกซ่อนจากโลกภายนอกโดยสมบูรณ์ ไม่มีโค้ดใดนอก make_counter เข้าถึงหรือแอบแก้ไขมันได้ นี่คือรูปแบบหนึ่งของ การห่อหุ้ม (encapsulation) คือการเก็บข้อมูลไว้เป็นส่วนตัว
ข้อผิดพลาดเรื่องสโคปที่พบบ่อยและวิธีแก้
นี่คือบั๊กเรื่องสโคปที่นักพัฒนา Python พลาดกันบ่อยที่สุด การรู้จักรูปแบบเหล่านี้จะช่วยประหยัดเวลาดีบักได้หลายชั่วโมง
ข้อผิดพลาดที่ 1: ตัวแปรโลคอลบังตัวแปรโกลบอล
Inside: ['date'] Outside: ['apple', 'banana', 'cherry']
items = [] ภายในฟังก์ชันสร้างตัวแปรโลคอลตัวใหม่ที่ บัง (shadow) ตัวแปรโกลบอลไว้ ลิสต์ items ที่เป็นโกลบอลจึงไม่เคยถูกแก้ไขเลย ส่วนใหญ่แล้วนี่คือบั๊ก
ข้อผิดพลาดที่ 2: UnboundLocalError จากการอ่านและเขียนปนกัน
(ไม่มีผลลัพธ์)
Python อ่านตัวฟังก์ชันทั้งหมดก่อนรัน ถ้าเห็นการกำหนดค่าให้ total ที่ไหนก็ตาม มันจะถือว่า total เป็นตัวแปรโลคอลตลอดทั้งฟังก์ชัน บรรทัด print(total) จึงพยายามอ่านตัวแปรโลคอลที่ยังไม่ได้กำหนดค่า
แบบฝึกหัด
ได้เวลาฝึกใช้สโคปและโคลเชอร์แล้ว แบบฝึกหัดเหล่านี้ไล่ตั้งแต่การอ่านสโคปไปจนถึงการสร้างโคลเชอร์ของคุณเอง
อ่านโค้ดนี้อย่างละเอียดแล้วทายว่าจะแสดงผลอะไร จากนั้นเขียนคำสั่ง print() ที่แสดงผลลัพธ์นั้นได้ตรงทุกตัวอักษร
x = 10
def foo():
x = 20
print(x)
foo()
print(x)แสดงตัวเลขสองตัวคนละบรรทัด ให้ตรงกับที่โค้ดด้านบนแสดงผล
โค้ดนี้พยายามเพิ่มค่าตัวนับที่เป็นโกลบอล แต่กลับพังด้วย UnboundLocalError แก้ให้ได้ผลลัพธ์เป็น:
1
2
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!เขียนฟังก์ชันชื่อ make_counter ที่รับค่า start และคืนฟังก์ชันออกมา ทุกครั้งที่เรียกฟังก์ชันที่คืนออกมา ฟังก์ชันนั้นต้องแสดงค่าตัวนับปัจจุบัน แล้วเพิ่มค่าขึ้น 1
การทดสอบจะเรียก:
c = make_counter(1)
c()
c()
c()ผลลัพธ์ที่ต้องได้:
1
2
3เขียนฟังก์ชันชื่อ make_averager ที่คืนฟังก์ชันออกมา ทุกครั้งที่เรียกฟังก์ชันที่คืนออกมาพร้อมตัวเลขหนึ่งตัว ฟังก์ชันนั้นต้องแสดงค่าเฉลี่ยสะสมของตัวเลขทั้งหมดที่ได้รับมาจนถึงตอนนี้
ค่าเฉลี่ยสะสมคือผลรวมของตัวเลขทั้งหมดหารด้วยจำนวนตัวเลข
การทดสอบจะเรียก:
avg = make_averager()
avg(10)
avg(20)
avg(30)ผลลัพธ์ที่ต้องได้:
10.0
15.0
20.0คำใบ้: ตัวหาค่าเฉลี่ยต้องจำทั้งผลรวมและจำนวนตัวเลข
สรุป
สิ่งที่คุณได้เรียนเกี่ยวกับสโคปใน Python:
| แนวคิด | ความหมาย |
|---|---|
| สโคปโลคอล | ตัวแปรที่สร้างภายในฟังก์ชัน มีอยู่เฉพาะในฟังก์ชันนั้น |
| สโคปโกลบอล | ตัวแปรที่สร้างที่ระดับโมดูล มองเห็นได้ทุกที่ในโมดูลนั้น |
| กฎ LEGB | Python ค้นหาใน Local, Enclosing, Global, Built-in ตามลำดับนี้ |
คีย์เวิร์ด global | ให้ฟังก์ชันแก้ไขตัวแปรระดับโมดูลได้ |
| สโคปที่ครอบอยู่ | ตัวแปรของฟังก์ชันชั้นนอก ซึ่งฟังก์ชันด้านในเข้าถึงได้ |
| โคลเชอร์ | ฟังก์ชันที่จำตัวแปรจากสโคปที่ครอบอยู่ได้ |
คีย์เวิร์ด nonlocal | ให้ฟังก์ชันด้านในแก้ไขตัวแปรในสโคปที่ครอบอยู่ได้ |
กฎทอง: ส่งค่าผ่านอาร์กิวเมนต์และคืนค่ากลับ ดีกว่าใช้ global หรือ nonlocal ข้อยกเว้นเดียวที่สโคปที่ครอบอยู่ได้เฉิดฉายคือโคลเชอร์ ซึ่งช่วยให้คุณสร้างฟังก์ชันเฉพาะทางที่มีสถานะ (state) ส่วนตัวได้
เรียนอะไรต่อ
เมื่อเข้าใจสโคปและโคลเชอร์แล้ว คุณก็พร้อมสำหรับ การเรียกซ้ำ (recursion) ที่ฟังก์ชันเรียกตัวเอง ความเข้าใจเรื่องสโคปสำคัญมากสำหรับการเรียกซ้ำ เพราะการเรียกซ้ำแต่ละครั้งจะได้สโคปโลคอลของตัวเอง