[Python]对于 Decorator 装饰器的理解

Decorator 装饰器

程式设计中的“函式”是为了解决程式码的重复利用、模组化,同时也增加可读性。而 Python 的“装饰器”可以在不改变原本函式的功能之下,又再进一步强化 (简化) 不同函式之间程式码的共用。

Python 的“函式”是所谓的“一级函式”,支援“高阶函式”的用法,可被当成参数来传递。“装饰器”就是一个“两层”的函式,先接收一个函式进行包装 (处理),包装完再回传新的函式。就是“吃进去又吐出来”的意思!

我简单统整出两种装饰器函式的写法:

# 针对没有输入/输出的函式
def DecoratorName1(callback):
    def wrapper():
        # Some Code...
        callback()
        # Some Code...
    return wrapper

# 针对有输入/输出的函式
def DecoratorName2(callback):
    def wrapper(*args, **kwargs):
        # Some Code...
        result = callback(*args, **kwargs)
        # Some Code...
        return result
    return wrapper

使用装饰器的方法:

# 定义 OriginalFunction 函式,并将它套用 DecoratorName 装饰器
@DecoratorName
def OriginalFunction():
    # Some Code...

# 执行原本的函式 OriginalFunction() 即可
OriginalFunction()

此处的 @ 是一个语法糖,相当于在定义完 OriginalFunction 之后,执行了一次:

OriginalFunction = DecoratorName(OriginalFunction)

所以后面执行的 OriginalFunction(),是被装饰器 DecoratorName() 吃进去又吐出来的产物。

范例

我写了一个范例,用一个“计时器”做为装饰器,包装一个“费波那契数列”的函数:

import time

def timer(callback):
    """计时器-装饰器"""
    def wrapper(*args, **kwargs):
        print(callback.__name__)  # 看一下呼叫了哪个函式
        start_time = time.time()
        callback(*args, **kwargs)
        elapsed_time = (time.time() - start_time) * 1000
        print(f"\n{elapsed_time:.06f} ms")
    return wrapper

# 用 timer() 包装 fib()
@timer
def fib(n):
    """求费波那契数列"""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b

# 执行 fib()
fib(1000)

# 执行结果:
# fib
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 
# 0.123456 ms

PS.如果没加上 @timer 来包装 fib() 函式,那么执行 timer(fib)(1000) 也有一样的结果。

再厉害一点,让装饰器也能传入参数:

import time

def showTimer(title):
    """计时器-装饰器工厂"""
    def timer(callback):
        def wrapper(*args, **kwargs):
            print(callback.__name__)  # 看一下呼叫了哪个函式
            start_time = time.time()
            callback(*args, **kwargs)
            elapsed_time = (time.time() - start_time) * 1000
            print(f"\n{title} {elapsed_time:.06f} ms")
        return wrapper
    return timer

# 用 showTimer() 包装 fib(),并于修饰器传入参数 
@showTimer("Elapsed Time:")
def fib(n):
    """求费波那契数列"""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b

# 执行 fib()
fib(1000)

# 执行结果:
# fib
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 
# Elapsed Time: 0.123456 ms

PS.如果没加上 @showTimer() 来包装 fib() 函式,那么执行 showTimer("Elapsed Time:")(fib)(1000) 也有一样的结果。

从程式码可以看出,装饰器函式由两层变成了三层,这种做法称为“Decorator Factory (装饰器工厂)”。每多加一项功能又要多包一层,像洋葱一样。

如果不用 Decorator 装饰器?

从装饰器的原理来看,跟我写一个函式,在函式里呼叫另一个函式也是一样的效果。或是在呼叫函式时,直接传入另一个函式的执行结果,EX: func3(func1(), func2())

像前面的程式,我一样可以用两个函式来处理:

import time

def showTimerFunction(title, callback, *args, **kwargs):
    """计时器-函式"""
    print(callback.__name__)  # 看一下呼叫了哪个函式
    start_time = time.time()
    callback(*args, **kwargs)
    elapsed_time = (time.time() - start_time) * 1000
    print(f"\n{title} {elapsed_time:.06f} ms")

def fib(n):
    """求费波那契数列"""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b

# 执行 showTimerFunction() 去呼叫 fib()
showTimerFunction("Elapsed Time:", fib, 1000)

# 执行结果:
# fib
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 
# Elapsed Time: 0.123456 ms

回归原本函式呼叫函式的作法,这样其实更不容易迷惑,要执行哪个函式还更有弹性。我可以决定要执行原本的 fib() 或是 showTimerFuncion(),而非只能执行被 showTimer() 包装后的 fib()

函式也不用一层一层的回传自己。

如果不用传递参数,那函式写起来还更简单。

结论

目前还没找到非得使用装饰器的场景!

参考网页

  1. No comments yet.

  1. No trackbacks yet.

return top

%d 位部落客按了赞: