Kodokon kodokon.com

เดคอเรเตอร์และฟังก์ชันอันดับสูง

ใช้ประโยชน์จากการที่ฟังก์ชันเป็นอ็อบเจกต์ชั้นหนึ่ง เพื่อเขียนเดคอเรเตอร์ที่แข็งแรง ปรับพารามิเตอร์ได้ และโปร่งใส

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

ใน Python ฟังก์ชันคืออ็อบเจกต์ตัวหนึ่งเหมือนอ็อบเจกต์อื่น ๆ คุณกำหนดมันให้ตัวแปรได้ ส่งมันเป็นอาร์กิวเมนต์ได้ และคืนมันกลับมาได้ ฟังก์ชันอันดับสูง (higher-order function) คือฟังก์ชันที่รับหรือคืนฟังก์ชัน - sorted(data, key=...) ก็เป็นหนึ่งในนั้น เพิ่ม closure เข้าไป ซึ่งฟังก์ชันข้างในจับตัวแปรของฟังก์ชันที่ครอบมันไว้และรักษามันให้มีชีวิตอยู่ต่อหลังฟังก์ชันนั้นคืนค่าไปแล้ว เท่านี้คุณก็มีส่วนผสมครบทุกอย่างของเดคอเรเตอร์

PYTHON
def make_multiplier(factor):
    def multiply(value):
        return value * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))  # 20 30
closure ตัวหนึ่ง: multiply จับ factor เอาไว้

เดคอเรเตอร์ คือฟังก์ชันที่รับฟังก์ชันเข้าไปและคืนอีกฟังก์ชันหนึ่งออกมา - โดยปกติคือ wrapper ที่เพิ่มพฤติกรรมรอบ ๆ การเรียกใช้ ไวยากรณ์ @timed เหนือ def slow_sum เป็นเพียงน้ำตาลเคลือบของ slow_sum = timed(slow_sum) เท่านั้นเป๊ะ ๆ ตัว wrapper รับ *args, kwargs เพื่อไม่ผูกติดกับลายเซ็นของฟังก์ชัน และ คืน** ผลลัพธ์ของฟังก์ชันที่ถูกตกแต่ง - การลืมข้อนี้คือความผิดพลาดคลาสสิกที่เปลี่ยนค่าคืนกลับทุกค่าให้กลายเป็น None

PYTHON
import functools
import time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        delay = time.perf_counter() - start
        print(f"{func.__name__}: {delay:.4f}s")
        return result
    return wrapper

@timed
def slow_sum(n):
    return sum(range(n))

print(slow_sum(1_000_000))
เดคอเรเตอร์จับเวลา พร้อมใช้ในงานจริง

หากต้องการให้เดคอเรเตอร์รับพารามิเตอร์ได้ (@repeat(times=3)) คุณต้องมีอีกหนึ่งชั้น: โรงงาน (factory) ที่รับพารามิเตอร์และคืนเดคอเรเตอร์ออกมา ซึ่งเดคอเรเตอร์นั้นจะคืน wrapper ต่ออีกที ฟังก์ชันซ้อนกันสามชั้น - นั่นคือราคาของความยืดหยุ่น หากสถานะซับซ้อนขึ้น คลาสที่มีเมทอด __call__ มักอ่านง่ายกว่า จงสงวนเดคอเรเตอร์ไว้สำหรับเรื่องที่ตัดขวางทุกส่วนของระบบ: การบันทึกล็อก การแคช การลองใหม่ การควบคุมสิทธิ์การเข้าถึง เดคอเรเตอร์ที่ไปเปลี่ยนตรรกะเชิงธุรกิจจะซ่อนสาระสำคัญไว้จากผู้อ่าน

PYTHON
import functools

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hello {name}")

greet("Ada")
เดคอเรเตอร์ที่รับพารามิเตอร์: โรงงาน เดคอเรเตอร์ wrapper

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ไวยากรณ์ @timed ที่วางเหนือ def slow_sum เทียบเท่ากับอะไร?
    • timed(slow_sum())
    • slow_sum = timed(slow_sum)
    • slow_sum.timed = True
  2. @functools.wraps แก้ปัญหาอะไร?
    • มันทำให้การเรียกฟังก์ชันที่ถูกตกแต่งเร็วขึ้น
    • มันรักษาชื่อและ docstring ของฟังก์ชันที่ถูกตกแต่งไว้
    • มันทำให้เดคอเรเตอร์ใช้ได้กับเมทอดของคลาส
  3. ใน @repeat(times=3) การเรียก repeat(times=3) คืนอะไรออกมา?
    • ผลลัพธ์ของฟังก์ชันที่ถูกตกแต่ง
    • เดคอเรเตอร์ ที่พร้อมรับฟังก์ชันเข้าไป
    • wrapper ตัวสุดท้ายโดยตรง
    • ลิสต์ของผลลัพธ์สามตัว