Python Decorators
Yiwei
Syntax Sugar
@some_decorator
def my_func(my_arg1, my_arg2):
......
def my_func(my_arg1, my_arg2):
......
my_func = some_decorator(my_func)
decorator: 一個吃 function 吐 function 的 function
def some_decorator(func):
def wrapper(a1, a2):
print('call '+func.__name__)
return func(a1, a2)
return wrapper
def my_func(my_arg1, my_arg2):
......
my_func = some_decorator(my_func)
大家可以回家啦 (?)
my_func = some_decorator(my_func) # WTF?
function 是一個「東西」
● 可以 assign
● 可以傳進另一個 function
● 可以定義在另一個 function,並回傳之
def f1(n):
print('I am f1')
sum = 0
for i in range(n+1):
sum += i
return sum
f2 = f1
def f1(n):
print('I am f1')
sum = 0
for i in range(n+1):
sum += i
return sum
f2 = f1
print('I am f1')
sum = 0
for i in range(n+1):
sum += i
return sum
f1f2
function 的定義
def f1(n):
print('I am f1')
sum = 0
for i in range(n+1):
sum += i
return sum
f2 = f1
f1 # <function f1 at 0x7f807e6261e0>
f2 # <function f1 at 0x7f807e6261e0>
f2(10) # 55
print('I am f1')
sum = 0
for i in range(n+1):
sum += i
return sum
f1f2
function 的定義
def f(number):
return number*10
def g(func, n):
return func(n)+8
# ⇧目前為止都只是定義,還沒被執行
g(f, 3) # 38
str_list.sort(key=str.lower)
注意不是 str.lower()
def f(number):
return number*10
def g(func, n):
return func(n)+8
# ⇧目前為止都只是定義,還沒被執行
g(f, 3) # 38
def gen_fat_checker(bmi):
bmi_bound = bmi
def f(w, h):
return w/(h*h) > bmi_bound
return f
fat = gen_fat_checker(20)
super_fat = gen_fat_checker(30)
print(fat(80.0, 1.7))
print(super_fat(80.0, 1.7))
def gen_fat_checker(bmi):
bmi_bound = bmi
return f
fat = gen_fat_checker(20) #只拿到函數本體
super_fat = gen_fat_checker(30) #還沒執行 f
print(fat(80.0, 1.7)) #現在才執行
print(super_fat(80.0, 1.7))
f = 敘述 function 要做什麼, 也敘述他吃2個參數
def gen_fat_checker(bmi):
bmi_bound = bmi
def f(w, h):
return w/(h*h) > bmi_bound
return f
def gen_fat_checker(bmi_bound):
def f(w, h):
return w/(h*h) > bmi_bound
return f
my_func = some_decorator(my_func)
function 是一個「東西」
● 可以 assign
● 可以傳進另一個 function
● 可以定義在另一個 function,並回傳之
知道了 function 是頭等公民...
decorator: 一個吃 function 吐 function 的 function
def some_decorator(func):
def wrapper(a1, a2):
print('call '+func.__name__)
return func(a1, a2)
return wrapper
def my_func(m1, m2):
print('{} {}'.format(m1, m2))
return m1 + m2
my_func = some_decorator(my_func)
my_func(5, 8)
Agenda
● Function as a “first-class citizen”
● Why decorator?
● 最簡單的 decorator
● 複雜一點的 decorator
Why decorator?
Query
def query_mysql(sql):
...
return result
res = query_mysql('select *')
Query with retry
def query_mysql(sql):
...
return result
for i in range(5):
try:
res = query_mysql('select *')
except:
sleep(60)
Download with retry
def download_s3(path):
...
return result
for i in range(5):
try:
res = download_s3('s3://a.csv')
except:
sleep(60)
XXXXX with retry
def find_mongo(q):
...
return result
for i in range(5):
try:
res = find_mongo('{f: 1}')
except:
sleep(60)
retry 吃 function (和其參數)
def retry(do_work, work_arg):
for i in range(5):
try:
return do_work(work_arg)
except:
sleep(60)
res = retry(query_mysql, 'select *')
retry 累贅,重點是執行 do_work(work_arg)
def retry(do_work, work_arg):
for i in range(5):
try:
work_res = do_work(work_arg)
return work_res
except:
sleep(60)
retry(query_mysql, 'select *')
retry(download_s3, 's3://a.csv')
retry(find_mongo, '{f: 1}')
重點是執行 do_work(work_arg)
def retry(do_work, work_arg):
for i in range(5):
try:
work_res = do_work(work_arg)
return work_res
except:
sleep(60)
retry(query_mysql, 'select *')
真的在執行
retry_query_mysql( 'select *')
可能寫成這樣嗎?
def retry(do_work) work_arg:
for i in range(5):
try:
work_res = do_work(work_arg)
return work_res
except:
sleep(60)
retry(query_mysql, 'select *')
真的在執行
retry(query_mysql)('select *')
可能寫成這樣嗎?
錯!
def retry(do_work):
def wrapper(work_arg):
for i in range(5):
try:
work_res = do_work(work_arg)
return work_res
except:
sleep(60)
return wrapper
retry(query_mysql)('select *')
只是定義:
吃一個arg的函數
一個吃 function 吐 function 的 function
def retry(do_work):
def wrapper(work_arg):
for i in range(5):
try:
work_res = do_work(work_arg)
return work_res
except:
sleep(60)
return wrapper
retry(query_mysql) #work is not executed
res = retry(query_mysql)('select *')
def retry(do_work):
def wrapper(work_arg):
for i in range(5):
try:
work_res = do_work(work_arg)
return work_res
except:
sleep(60)
return wrapper
res = retry(query_mysql)('select *')
def retry(do_work):
def wrapper(work_arg):
for i in range(5):
try:
work_res = do_work(work_arg)
return work_res
except:
sleep(60)
return wrapper
query_mysql = retry(query_mysql)
res = query_mysql('select *')
和這一個 section 第一頁的
呼叫法一樣了
decorator: syntax sugar
def retry(work):
def wrapper(work_arg):
work(work_arg)
return wrapper
def query_mysql(sql):
...
query_mysql = retry(query_mysql)
res = query_mysql('select *')
如果這 function 有一百行
就很難知道 query_mysql 被改了
def retry(work):
def wrapper(work_arg):
work(work_arg)
return wrapper
@retry
def query_mysql(sql):
...
res = query_mysql('select *')
Syntax Sugar
@decorator
def work():
......
def work():
......
work = decorator(work)
Syntax Sugar
@f2
@f1
def work():
......
def work():
......
work = f2(f1(work))
定義 vs. 使用
@measure_time
@cache
def work(w1):
......
def work(w1):
......
work = measure_time(
cache(
work))
r = work('123')
● 使用方式不變
● 擴充功能:
和黃金聖衣一樣
● 內在不變,但外在
變了
(你的work不是你的work)
def deco(work):
def wrapper(work_arg):
# TRICK BEFORE
work_res = work(work_arg)
# TRICK AFTER
return wrapper
● 根據 arg 來記住結果不執行 work (@lru_cache)
● 計時 work 花多久時間
● assert / verbose / logging
● @app.route(‘/get/<id>/’) 僅回傳 func 本身,同時也記起來
● @property
所以你應該會寫 decorator 給別人用了
def deco(work):
def _wrapper(work_arg):
return work(work_arg)
return _wrapper
def my_work(w1): @deco
... def my_work(w1):
my_work = deco(my_work) ...
res = my_work('123')
最簡單的 decorator
def retry(work):
def wrapper(work_arg):
for i in range(5):
try:
return work(work_arg)
except:
sleep(60)
return wrapper
@retry
def download(file1, file2):
...
事情不是憨人想得這麼簡單
@deco
def work(w1):
...
@deco
def work(w1, w2, w3):
...
@deco(d1=xx)
def work(w1):
...
def deco(work):
def _wrapper(work_arg):
return work(work_arg)
return _wrapper
@deco
def work_a(w1):
@deco
def work_b(w1, w2, w3=None):
1. 不知道使用你 deco 的 function 有多少個參數
def deco(work):
def _wrapper(*args, **kwargs):
return work(*args, **kwargs)
return _wrapper
@deco
def work_a(w1):
@deco
def work_b(w1, w2, w3=None):
就回傳可以吃任意個參數的 wrapper
def deco(work, d_arg1):
def _wrapper(*args, **kwargs):
print(d_arg1)
return work(*args, **kwargs)
return _wrapper
def work_a(w1, w2, w3=None):
...
work_a = deco(work_a, 5)
work_a(11, 22, 33)
2. 想讓 decorator 自身也接受參數
def deco(work, d_arg1):
def _wrapper(*args, **kwargs):
print(d_arg1)
return work(*args, **kwargs)
return _wrapper
def work_a(w1, w2, w3=None):
...
work_a = deco(work_a, 5)
work_a(11, 22, 33)
2. 想讓 decorator 自身也接受參數
可以,
但沒有 syntax sugar
@retry
def download(file):
......
@retry(attempts=5)
def download(file):
......
retry(download)
retry(attempts=5)(download)
藍色的部份,也就是 @ 後面的部份可以視作一個 expression,這個 expression 回傳的東西會被呼叫
def deco(d_arg1):
def _decorator(work):
def _wrapper(*args, **kwargs):
print(d_arg1)
return work(*args, **kwargs)
return _wrapper
return _decorator
def work_a(w1, w2): @deco(5)
... def work_a(w1, w2):
work_a = deco(5)(work_a) ...
work_a(11, 22)
讓 @xxx 獨立於 work 之外
def deco(work):
def _wrapper(*args, **kwargs):
return work(*args, **kwargs)
return _wrapper
deco(my_work)(11, 22)
def p_deco(d_arg1):
def _decorator(work):
def _wrapper(*args, **kwargs):
print(d_arg1)
return work(*args, **kwargs)
return _wrapper
return _decorator
p_deco(42)(my_work)(11, 22) (做了呼叫的) p_deco(42)
是一個吃 work 的 function --
亦即裡面定義的 _decorator
deco
是一個吃 work 的 function
def deco(work):
def _wrapper(*args, **kwargs):
return work(*args, **kwargs)
return _wrapper
@deco
def my_work(w1, w2):
def p_deco(d_arg1):
def _decorator(work):
def _wrapper(*args, **kwargs):
print(d_arg1)
return work(*args, **kwargs)
return _wrapper
return _decorator
@p_deco(42)
def my_work(w1, w2):
多了一層,因為和
“deco” 只是定義相比,
”p_deco(42)” 做了呼叫
Takeaway
my_func = some_decorator(my_func)
function 是一個「東西」
● 可以 assign
● 可以傳進另一個 function
● 可以定義在另一個 function,並回傳之
def retry(do_work):
def wrapper(work_arg):
for i in range(5):
try:
work_res = do_work(work_arg)
return work_res
except:
sleep(60)
return wrapper
retry(query_mysql) #work is not executed
res = retry(query_mysql)('select *')
decorator: syntax sugar
@measure_time
@cache
def work(w1):
......
def work(w1):
......
work = measure_time(
cache(
work))
r = work('123')
● 使用方式不變
● 擴充功能:
和黃金聖衣一樣
● 內在不變,但外在
變了
(你的work不是你的work)
def deco(work):
def _wrapper(*args, **kwargs):
return work(*args, **kwargs)
return _wrapper
@deco
def my_work(w1, w2):
def p_deco(d_arg1):
def _decorator(work):
def _wrapper(*args, **kwargs):
print(d_arg1)
return work(*args, **kwargs)
return _wrapper
return _decorator
@p_deco(42)
def my_work(w1, w2):
大家可以回家啦!
Reference
Reference
● https://www.python.org/dev/peps/pep-0318/ (must see)
● https://docs.python.org/3/reference/compound_stmts.html
#function
● Book “Guide to: learning Python Decorators”
● work 可以動,不代表你的 work 是你的 work
○ print(work.__name__)
○ https://hynek.me/articles/decorators/
○ http://blog.dscpl.com.au/2014/01/how-you-implemented-your-pyth
on.html
○ https://github.com/GrahamDumpleton/wrapt

Python decorators (中文)