Python : Decorator ตอน 2 : Passing arguments และ Chaining


ในตอนที่ 1 ได้กล่าวถึงเรื่องการเป็น first-class citizen ของ function ในภาษา Python และการสร้าง function decoration แบบง่ายมาแล้ว ตอนนี้จะกล่าวถึงเรื่อง การส่ง arguments และ chaining decorator พร้อมยกตัวอย่างเพิ่มเติมเพื่อให้เห็นภาพชัดขึ้น


Recap *args และ **kwargs

ในภาษา Python มี keyword 2 ตัวคือ *args และ **kwargs ที่เกี่ยวข้องกับการส่งผ่าน arguments ทั้งสองมีความหมายต่างกัน *args ใช้ส่ง arguments เป็นชุดโดยไม่ต้องมี keyword ในขณะที่ **kwargs ใช้ส่ง arguments เป็นชุดและมีการใช้ keyword ยกตัวอย่างข้างล่างนี้ เป็นการส่ง arguments เป็นชุดโดยไม่มี keywords

def X(*args):
	for a in args:
		print(a)
		
X(10,20,30)		

#-- output is
#10
#20
#30
    

ตัวอย่างการส่ง arguments เป็นชุดมี keywords

def X(**kwargs):
	for k,v in kwargs.items():
		print("{} = {}".format(k,v))
		
X(a=10,b=20,c=30)		

#-- output is
#a=10
#b=20
#c=30
    

ตัวอย่างการส่ง arguments โดยใช้แบบไม่มี keywords และมี keywords

def X(*args.**kwargs):
	for a in args:
		print(a)
	for k,v in kwargs.items():
		print("{} = {}".format(k,v))
		
X(10,20,30,a=10,b=20,c=30)		

#-- output is
#10
#20
#30
#a=10
#b=20
#c=30
    

ในทางปฏิบัติมีข้อควรจำอยู่ 2 อย่าง คือ ไม่จำเป็นต้องใช้กลุ่มอักษร args,kwargs ใช้อักษรอื่นแทนได้ แต่ต้องมี *,** เพื่อระบุว่าเป็น positional arguments หรือ keyword arguments และหากต้องการใช้ทั้งสองแบบใน function เดียวกัน จะต้องให้ positional arguments มาก่อน keyword arguments เสมอ

def X(*args.**kwargs):
	for a in args:
		print(a)
	for k,v in kwargs.items():
		print("{} = {}".format(k,v))
		
X(a=10,10,b=20,20,30,c=30)		

#-- output is
# SyntaxError: positional argument follows keyword argument
    

การส่ง arguments ไปยัง decorator function

ตัวอย่างแรกคือการเลียนแบบการทำงานของ time_it() ของ Python

import time
import math

def mimic_time_it(func):
	def wrapper(*args):
		start = time.time()
		result = func(*args)
		print("This function took {} seconds to work.".format(time.time() - start))
		return result
	return wrapper
	
@mimic_time_it
def power(x,y):
	return "result is {}".format(math.pow(x,y))

print(power(3,5))

#-- result is
#This function took 3.4332275390625e-05 seconds to work.
#243.0    

ตัวอย่างนี้ argument ของ stand alone function ถูกส่งเข้าไปยัง wrapper function ภายใน decorator (mimic_time_it)

อีกตัวอย่าง เก็บ state ไว้ใน decorator

def count_it(func):
	count = 0
	def wrapper(*args):
		nonlocal count	
		count+=1
		print("This is your {} times.".format(count))
		return func(*args)
	return wrapper
	
@count_it
def power(x,y):
	return "result is {}\n".format(math.pow(x,y))

print(power(1,2))
print(power(3,4))
print(power(5,6))
print(power(7,8))
'''
-- result is
This is your 1 times.
result is 1.0

This is your 2 times.
result is 81.0

This is your 3 times.
result is 15625.0

This is your 4 times.
result is 5764801.0
'''
  

ตัวอย่างนี้ใช้ keyword "nonlocal" หมายถึงตัวแปรที่ไม่ใช่ global (ประกาศไว้นอก scope ของ function) และ local (ประกาศภายใน scope ของ function) จะนำมาใช้ในกรณีที่มีการประกาศ inner function หรือ nested function โดย inner function แต่ละ function สามารถเข้าถึงได้ ในตัวอย่างนี้ใช้เก็บจำนวนครั้งของการเรียกใช้ function

ตัวอย่างข้างต้น เป็นตัวอย่างที่ส่งตัว stand alone function เป็น argument ให้กับ decorator function ตัวอย่างต่อไปนี้ใช้การตัวแปรเป็น arguments อาจพบได้บ่อยใน web application framework เช่น Flask, Bottle

import time
import datetime;

def log_it(*args,**kwargs):
	def midle_function(func):
		log_fname = kwargs['file_name']		
		
		def wrapper(*args,**kwargs):
			nonlocal log_fname
			ts = datetime.datetime.now().timestamp()
			line = "{} : {}\n".format(ts,kwargs['text'])
			with open(log_fname,'a') as f :
				f.write(line)
			func()
		return wrapper	
		
	return midle_function
    
@log_it(file_name="data.txt")
def test_log(**kwargs):
	print("It is logged!")
	
test_log(text="Hello World 1")	
test_log(text="Hello World 2")
test_log(text="Hello World 3")		
test_log(text="Hello World 1")	
test_log(text="Hello World 2")
test_log(text="Hello World 3")		

'''
Would see these on screen :

It is logged!
It is logged!
It is logged!
It is logged!
It is logged!
It is logged!

and these in "data.txt"

1623382604.706355 : Hello World 1
1623382604.706575 : Hello World 2
1623382604.706677 : Hello World 3
1623382626.668089 : Hello World 1
1623382626.668272 : Hello World 2
1623382626.668382 : Hello World 3
'''
    

ในตัวอย่างนี้ file_name="data.txt" ถูกส่งไปยัง log_it function และ text="Hello World 1" ถูกส่งไปยัง wrapper function โดยมี midle_function เป็นตัวรับ test_log function เป็น argument
Chain decorator

stand alone function 1 function สามารถถูก decorate ได้มากกว่า 1 decorator

import time
import math

def mimc_time_it(func):
	def wrapper(*args):
		start = time.time()
		result = func(*args)
		print("This function took {} seconds to work.".format(time.time() - start))
		return result
	return wrapper

def count_it(func):
	count = 0
	def wrapper(*args):
		nonlocal count	
		count+=1
		print("This is your {} times.".format(count))
		return func(*args)
	return wrapper
	
@count_it
@mimc_time_it
def power(x,y):
	return "result is {}\n".format(math.pow(x,y))

print(power(1,2))
print(power(3,4))
print(power(5,6))
print(power(7,8))

'''
result are :

This is your 1 times.
This function took 2.0503997802734375e-05 seconds to work.
result is 1.0

This is your 2 times.
This function took 1.3828277587890625e-05 seconds to work.
result is 81.0

This is your 3 times.
This function took 2.1457672119140625e-06 seconds to work.
result is 15625.0

This is your 4 times.
This function took 3.337860107421875e-06 seconds to work.
result is 5764801.0

'''




ความคิดเห็น