2.16. ขอบเขต

เมื่อ Python ค้นหาชื่อภายในฟังก์ชัน มันจะค้นหาตามลำดับ ขอบเขต ที่เฉพาะเจาะจง การเข้าใจลำดับนั้นอธิบายได้ว่าทำไมการกำหนดค่าบางครั้งบดบังชื่อภายนอก ทำไมบางครั้งแก้ไขชื่อเหล่านั้น และทำไมฟังก์ชันซ้อนกันจึงสามารถจำค่าจากที่ที่นิยามได้

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.

การค้นหาชื่อเริ่มต้นที่ขอบเขต local ของฟังก์ชันและเดินออกไปยังขอบเขต module และ built-in จนกว่าจะพบตัวที่ตรงกัน

2.16.1. ขอบเขต local และ module

ชื่อที่กำหนดภายในฟังก์ชันเป็น local ของฟังก์ชันนั้นและหายไปเมื่อการเรียกสิ้นสุด:

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

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

ชื่อที่กำหนดที่ระดับบนสุดของไฟล์ .py เป็น ระดับ module (บางครั้งเรียกว่า global) และมองเห็นได้ทุกที่ในไฟล์นั้น รวมถึงภายในฟังก์ชัน:

CAMERA = "OpenMV"

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

การอ่านชื่อระดับ module จากภายในฟังก์ชันเป็นอัตโนมัติ การกำหนดค่า ให้กับชื่อนั้นจากภายในฟังก์ชันไม่ใช่ -- การกำหนดค่าสร้างตัวแปร local ใหม่ที่บดบังชื่อระดับ module ตลอดการเรียกที่เหลือ:

counter = 0

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

counter ทางซ้ายทำให้ counter กลายเป็นชื่อ local ใน bump ดังนั้นการอ่านทางขวาจึงไม่มีค่าให้ค้นหา

2.16.1.1. คีย์เวิร์ด global

หากต้องการกำหนดค่าใหม่ให้กับชื่อระดับ module จากภายในฟังก์ชัน ให้ประกาศว่าเป็น global ก่อน:

counter = 0

def bump():
    global counter
    counter += 1

ใช้ global อย่างประหยัด ฟังก์ชันที่เปลี่ยนแปลง state ที่ซ่อนอยู่นั้นยากต่อการตรวจสอบกว่าฟังก์ชันที่รับค่าเป็นอาร์กิวเมนต์และคืนค่าใหม่ออกมา วิธีแก้ปกติสำหรับ "ต้องการแชร์ state" คือส่งอ็อบเจกต์ (list, dict, หรือ class instance) เป็นอาร์กิวเมนต์และเปลี่ยนแปลง มัน แทน

2.16.2. Lambda

lambda สร้างฟังก์ชันนิรนามขนาดเล็กในนิพจน์เดียว:

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

มันเทียบเท่ากับ

def square(x):
    return x * x

เนื้อหาของ lambda ต้องเป็นนิพจน์เดียว -- ไม่มีคำสั่ง ไม่มีหลายบรรทัด การใช้งานหลักคือการส่งฟังก์ชันขนาดเล็กเป็นอาร์กิวเมนต์ให้กับสิ่งที่รับฟังก์ชัน:

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

เมื่อเนื้อหาขยายเกินหนึ่งนิพจน์ ให้เปลี่ยนไปใช้ def จริงๆ การตั้งชื่อฟังก์ชันด้วย def ยังทำให้มีชื่อใน traceback ซึ่ง lambda ไม่มี

2.16.3. Closure

ฟังก์ชันที่กำหนดภายในฟังก์ชันอื่นสามารถอ่านชื่อจากขอบเขตของฟังก์ชันที่ครอบ ฟังก์ชันภายใน จับ ชื่อเหล่านั้นและทำงานต่อไปได้แม้ว่าการเรียกภายนอกจะสิ้นสุดแล้ว:

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

ผลลัพธ์:

105 110

add5 และ add10 เป็นสองฟังก์ชันที่แยกจากกัน แต่ละตัวจำ n ของตัวเอง ฟังก์ชันที่สร้างและคืนฟังก์ชันภายในที่ปรับแต่งแล้วในลักษณะนี้เรียกว่า closure มันเป็นเหตุผลหลักที่ภาษาต้องการฟังก์ชันซ้อนกัน -- วิธีการอบ state บางส่วนลงในค่าฟังก์ชันแล้วส่งค่านั้นออกไปเป็น callable เดียว

การอ่านชื่อที่ถูกจับเป็นอัตโนมัติ การ rebind ต้องใช้คีย์เวิร์ดเพิ่มเติม ตัวอย่างด้านล่างทำในสิ่งที่ดูถูกต้องแต่ล้มเหลว:

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

การกำหนดค่าให้ count ภายใน tick ทำให้ count กลายเป็น local ของ tick เหมือนกับที่มันจะทำให้เป็น local ในฟังก์ชันระดับบนสุด คีย์เวิร์ด nonlocal บอก Python ว่า "ชื่อนี้อยู่ในฟังก์ชันที่ครอบ ให้ rebind ที่นั่น":

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

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

ผลลัพธ์:

1 2 3

nonlocal สำหรับขอบเขตฟังก์ชันที่ครอบเหมือนกับที่ global ใช้กับขอบเขต module โปรดทราบว่าการ เปลี่ยนแปลง อ็อบเจกต์ที่ถูกจับ (การเรียก some_list.append(...), some_dict[k] = v) ไม่ต้องใช้ nonlocal -- ชื่อไม่ถูก rebind แค่อ็อบเจกต์ที่มันชี้ไปถูกเปลี่ยนแปลง