Categories
Computer

#30 สรุป + เสริมจาก Workshop PyCon TW เรื่องการใช้ Decorator

ปลายเดือนที่แล้ว เราได้เข้า Wrorkshop All about decorators ในงาน Pycon TW 2024 โดยในบทความนี้เราจะสรุปกับเสริมข้อมูลการใช้งาน Decorator

ปลายเดือนที่แล้ว เราได้ไปงานประชุม PyCon Taiwan 2024 ตัวงานนั้นมีจัดทั้ง Presentation กับ Workshop โดยหนึ่งใน Workshop ที่เราเข้าร่วมนั่นก็คือ All about decorators

Workshop All about decorators นี้เป็น Workshop ที่จัดโดย Reuven M. Lerner ที่

  • สอนภาษา Python กับ Data Science ตั้งแต่ปี 1995
  • เป็นผู้เขียนหนังสืออย่าง Python Workout กับ Pandas Workshop
  • และเป็นผู้เขียนบล็อกรายสัปดาห์ที่มีชื่อว่า Bamboo Weekly

โดยในบทความนี้ เราจะมาสรุปเรื่อง Decorators จาก Workshop + หาข้อมูลมาเสริมตามด้านล่างนี้ครับ

Decorators

ภาพโดย Tim Mossholder จากเว็บ Unsplash

Decorators เป็น Pattern อย่างหนึ่งในภาษา Python ที่ทำหน้านี้ปรับแต่งการทำงานของ Function นั้น ๆ โดย

  • เราสามารถเพิ่มบริเวณก่อน/หลังการทำงาน Function ที่เราใช้ Decorators นั้น ๆ ได้ครับ
  • ไม่จำเป็นต้องเข้าไปแก้ไขที่ตัวโค้ดนั้น ๆ ด้วยตัวเอง ที่เข้ากันได้กับหลักการ DRY (Don’t repeat yourself)

หลักการ Decorator

หลักการพื้นฐานของ Decorator [1-3] นั่นก็คือ Function ในภาษาไพทอนนั้น จะได้รับการมองว่าเป็นตัวแปรประเภท Object ที่เป็น First-Class Object โดยเรา

  • สามารถกำหนดให้เป็น Argument สำหรับฟังก์ชันนั้น
  • กับกำหนดให้สามารถส่งคืนค่าออกมาจาก Function ที่สร้างขึ้น
  • และทำ Function ซ้อน Function

หนึ่ง เราสามารถส่งตัวแปรนั้นเป็น Argument สำหรับ Function อื่น (Functions as Arguments) โดยแสดงตัวอย่างตามโค้ดด้านล่างนี้

def func(arg_func):
    print(f"Hello { arg_func() }")

def print_my_name():
    return "Me"

func(print_my_name) <-- ผลลัพธ์จะขึ้นเป็น "Hello Me"

สอง เราสามารถส่งคืนค่าออกมาจาก Function ที่เราสร้างขึ้นมาได้ (Functions Returning Functions) โดยแสดงตัวอย่างตามโค้ดด้านล่างนี้

def func():
    def wrapper():
        print("Hello")
    return wrapper

wrap = func()
wrap() <-- ผลลัพธ์จะขึ้นเป็น "Hello"

สาม เราสามารถเขียน Function ซ้อน Function ได้ (Nested Functions) ตามโค้ดด้านบนที่เราสร้าง Function func ที่มี Function wrapper ที่อยู่ข้างใน

จุดนี้จะแตกต่างกับภาษาอื่นที่ตัว Function จะมองแค่เป็น Function สำหรับการเรียกใช้งานเท่านั้น

คุณสมบัติของ Decorator

เมื่อทราบหลักการพื้นฐานของ Function ที่ไพทอนมองว่าเป็น Object แล้ว เรามากล่าวถึงคุณสมบัติของตัว Decorators

Decorators มีคุณสมบัติตามด้านล่างนี้

  1. มันเป็น Function (It’s a function)
  2. รับ Function เป็น Argument (Takes function as an argument)
  3. ตัว Function ที่ทำหน้าที่เป็น Decorator จะคืนผลลัพธ์ในรูปแบบ Function (Returns a function as a result)
  4. Function ที่คืนออกมาจาก Function ที่เป็น Decorator จะปรับแต่งการทำงานของ Function ที่เราใส่เข้าไปเป็น Argument (The returned function replaces the behavior of the original)

เมื่อเห็นข้อความตามด้านบนนี้แล้ว อาจจะยังไม่เห็นภาพมากนัก เรามาเขียนโค้ด Decorator กันครับ

เขียนโค้ด Decorators

เรามาเขียนโค้ด Decorators กันเถอะ ในขั้นตอนแรก เรามาสร้าง Function ที่เป็น Decorator โดย

  • ตั้งชื่อ Function ว่า with_lines_func
  • และเรากำหนดชื่อตัวแปรสำหรับการรับ Parameter ที่เป็น Function อย่าง func
def with_lines_func(func):

ต่อมา เราสร้าง Function ที่ปรับแต่งการทำงานของ Function ที่ใช้ชื่อตัวแปรอย่าง func ตัว Function ที่ปรับแต่งการทำงานนี้เราจะใช้ชื่อว่า wrapper

def with_lines_func(func):
    def wrapper():
        pass

    return wrapper

จากโค้ดด้านบนนี้ ตัว pass ที่เรากำหนดไว้ข้างใน Function wrapper มีหน้าที่ระบุว่าไม่มีการดำเนินการใด ๆ ในฟังก์ชันนั้น แต่มีการสร้าง/ประกาศ Function, Loops เอาไว้ก่อน เผื่อสำหรับการเขียนโค้ดในคราวหลัง

Python pass Keyword (w3schools.com)

จากนั้น เราเขียนโค้ดข้างใน Function wrapper เพื่อปรับแต่งการทำงานของ Function ที่ใช้ตัวแปร func โดยตัวอย่างนี้ เราจะกำหนดให้พิมพ์ข้อความที่มีลักษณะแบบด้านล่างนี้

-------------------
ข้อความที่พิมพ์ออกมาจาก Function ที่ใช้ตัวแปร func
-------------------

จาก Output ที่เราต้องการตามด้านบนนี้ เราสามารถเขียนโค้ดได้ตามด้านล่างนี้ครับ โดยลบข้อความ pass ใน Function wrapper ก่อนที่จะเขียนโค้ดเข้าไปครับ

def with_lines_func(func):
    def wrapper():
        line = "-------------------\n"
        return f"{ line }{ func() }\n{ line }"

    return wrapper

เมื่อเขียน Function Decorators เสร็จแล้ว เรามาทดลองเรียกใช้กันครับ เราสามารถเรียกใช้ได้โดยการเขียนโค้ดตามด้านล่างนี้

@with_lines_func
def hello():
    return "Hello"

print(hello())

ผลลัพธ์ที่ได้จะแสดงตามด้านล่างนี้ครับ

-------------------
Hello
-------------------

อันนี้จะเป็นการเขียน Decorator เบื้องต้นครับ

อย่างไรก็ดี เวลาที่เราสร้าง Function นั้น หลายครั้งเราสร้าง function ที่รับตัวแปร Argument ต่าง ๆ เข้าไปใน Function เพื่อกำหนดการทำงานของ Function นั้นตามที่เราต้องการ

โดยเราจะลองเขียนฟังก์ชันที่รับ Argument ที่ใช้ Decorator เดิมอย่าง with_lines_func

@with_lines_func
def add(a, b):
    return a + b

print(add(1, 3))

จากนั้น ทดลองให้โค้ดนี้ทำงาน ตัวระบบจะแจ้งว่าโค้ดมี Error ขึ้นว่า

TypeError: with_lines_func.<locals>.wrapper() takes 0 positional arguments but 2 were given

ก็ไม่แปลกใจอะไร เพราะ Function wrapper ที่อยู่ใน Decorator with_lines_func เราได้สร้างขึ้นนั้นไม่ได้กำหนดให้รับค่า Argument เพื่อนำไปใช้สำหรับการส่งค่านั้นเข้า Function add

ต่อมา เรามาปรับโค้ดส่วน Decorator เพื่อให้ Decorator ทำงานกับ Function ที่รับ Argument ได้ตามด้านล่างนี้ โดยปรับโค้ดได้ตามด้านล่งานี้

def with_lines_func(func):
    def wrapper(*args, **kwargs):
        line = "-------------------\n"
        return f"{ line }{ func(*args, **kwargs) }\n{ line }"

    return wrapper

จากโค้ด เราจะเพิ่มตัวแปรอย่าง *args กับ *kwargs [4] เข้าไปครับ โดย

  • *args คือ พารามิเตอร์แบบที่ไม่ใช่คีย์เวิร์ด (Non-keyword Arguments) ที่เราสามารถส่งค่าเข้าไปใน Function ได้เลย โดยตัวแปร args นี้เป็นตัวแปรประเภท Tuple
  • **kwargs คือ พารามิเตอร์แบบที่เป็นคีย์เวิร์ด (Keyword Arguments) ที่เราจำเป็นต้องกำหนดชื่อพารามิเตอร์พร้อมกับค่านั้น ๆ เพื่อส่งเข้าไปใน Function โดยตัวแปร kwargs นี้เป็นตัวแปรประเภท Dictionary ที่ชื่อพารามิเตอร์เป็น key ส่วนค่าของพารามิเตอร์นั้น ๆ เป็น value

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

-------------------
4
-------------------

ตัวอย่างการประยุกต์ใช้งาน Decorators

เมื่อทราบการเขียน Decorators แล้ว ตัว Decorators นั้นสามารถใช้งานได้หลายรูปแบบ ได้แก่ เปลี่ยน Input, เปลี่ยน Output, พิมพ์ Log การทำงานของ Function, เช็ค Permission ของผู้ใช้ กับเก็บ Cache และอื่น ๆ

ในตัวอย่างนี้ เราจะสร้าง Function ที่เป็น Decorators สำหรับการจับเวลารันตัวโค้ด โดยเราสามารถเขียนโค้ดได้ตามด้านล่างนี้

import time, random

def timefunc(func): 
    def wrapper(*args):
        start = time.time()
	result = func(*args)
	end = time.time()
	usage = end - start
		
	print(f"This function ({ func.__name__ }) takes { usage } seconds.\n")
			
	return result
		
    return wrapper

ตัวโค้ดตามด้านบนนี้ เราจะนำเข้าไลบรารีอย่าง

  • time เพื่อที่จะเรียกใช้งานฟังก์ชัน time.time() สำหรับการดึงเวลาในหน่วยวินาที ตั้งแต่วันที่ 1 มกราคม ค.ศ. 1970 จนถึงปัจจุบัน
  • random สำหรับการสุ่มค่า โดยเราใช้ฟังก์ชัน random.randint() สำหรับการสุ่มค่าเป็นจำนวนเต็มเพื่อหยุดรันโค้ดชั่วคราว

เมื่อเราเขียนโค้ดสำหรับ Decorators แล้ว เรามาลองเขียนฟังก์ชันที่ใช้ Decorators ตามข้างบนนี้ครับ

@timefunc
def slow_add(first, second):
	time.sleep(random.randint(0, 3))
	return first + second
	
@timefunc
def slow_mul(first, second):
	time.sleep(random.randint(0, 3))
	return first * second
	
print(slow_add(2, 3))
print(slow_mul(2, 3))
print(slow_add(2 ,3))
print(slow_mul(2, 3))

เมื่อทดลองรันโค้ดแล้ว ผลลัพธ์แสดงตามด้านล่างนี้ครับ

This function (slow_add) takes 1.005018711090088 seconds.
This function (slow_mul) takes 1.5020370483398438e-05 seconds.
This function (slow_add) takes 1.0967254638671875e-05 seconds.
This function (slow_mul) takes 7.867813110351562e-06 seconds.

ที่มา

  1. Decorators in Python – GeeksforGeeks
  2. Python Decorators (With Examples) (programiz.com)
  3. Primer on Python Decorators – Real Python
  4. การใช้งาน *args และ **kwargs ในภาษา Python | DH (devhub.in.th)

By Kittisak Chotikkakamthorn

อดีตนักศึกษาฝึกงานทางด้าน AI ที่ภาควิชาวิศวกรรมไฟฟ้า มหาวิทยาลัย National Chung Cheng ที่ไต้หวัน ที่กำลังหางานทางด้าน Data Engineer ที่มีความสนใจทางด้าน Data, Coding และ Blogging / ติดต่อได้ที่: contact [at] nickuntitled.com