1. 程式人生 > 其它 >讓程式碼更具 Python 範兒的裝飾器

讓程式碼更具 Python 範兒的裝飾器

技術標籤:pythonpython

在 Python 中,裝飾器的作用是在不改變函式或類的程式碼的前提下,改變函式或類的功能。在介紹裝飾器之前,我們先來複習下 Python 中的函式。

函式

1. 函式也是物件
def foo():
    print("Hello World!")

bar = foo

bar()

output:

Hello World!

在上面的例子中,首先定義了函式 foo() ,然後將函式 foo 賦給變數 bar ,這樣我們便可以通過 bar() 來呼叫函式。

2. 函式作為引數
def foo():
    print("I am foo"
) def bar(func): print("I am bar") func() bar(foo)

output:

I am bar
I am foo

在上面的例子中,函式 foo() 作為引數傳入到 bar() 函式,然後再 bar() 函式內部通過引數來呼叫函式 foo()

3. 函式巢狀
def parent():
    print("I am parent")

    def foo_child():
        print("I am foo")

    def bar_child(
): print("I am bar") foo_child() bar_child() parent()

output:

I am parent
I am foo
I am bar

在上面的例子中,函式 foo_child()bar_child() 巢狀在函式 parent() 的內部。巢狀在函式內部的函式只能在函式內部呼叫,從外部呼叫會報錯。例如:

def parent():
    print("I am parent")

    def foo_child():
        print("I am foo"
) def bar_child(): print("I am bar") foo_child() bar_child() foo_child()

output:

Traceback (most recent call last):
  File "/Users/weisong/PycharmProjects/pythonProject/greet.py", line 14, in <module>
    foo_child()
NameError: name 'foo_child' is not defined
4. 函式作為返回值
def parent(name):
    def foo_child():
        print("I am foo")

    def bar_child():
        print("I am bar")

    if name == 'foo':
        return foo_child
    else:
        return bar_child


foo = parent("foo")
bar = parent("bar")


foo()
bar()

output:

I am foo
I am bar

在上面的例子中,parent() 函式根據傳入的引數返回函式 foo_child 或者 bar_child,得到 parent() 函式的返回值之後,我們可以使用返回值呼叫相應的函式。

裝飾器

1. 一個簡單的裝飾器

介紹完函式的各種用法後,我們來看一個簡單的裝飾器(decorator)。

def my_decorator(func):
    def wrapper():
        print("Do something before function is called")
        func()
        print("Do something after function is called")
    return wrapper


def foo():
    print("I am foo")


foo = my_decorator(foo)
foo()

output:

Do something before function is called
I am foo
Do something after function is called

在上面的例子中,語句 foo = my_decorator(foo)my_decorator 函式的返回值 wrapper 函式賦給 foo ,這樣我們便可以使用 foo 來呼叫 wrapper 函式,在 wrapper 函式中包含著作為引數傳入的函式的呼叫,所以會輸出 I am foo

2. 語法糖

上面使用裝飾器的方式有點笨重,Python 提供了一種更簡單的方式來使用裝飾器,這便是使用 @ 符號,我們稱之為語法糖。使用 @ 符號,將上面的裝飾器修改如下:

def my_decorator(func):
    def wrapper():
        print("Do something before function is called")
        func()
        print("Do something after function is called")
    return wrapper


@my_decorator
def foo():
    print("I am foo")


foo()

output:

Do something before function is called
I am foo
Do something after function is called

@my_decorator 相當於前面的 foo = my_decorator(foo),使用方式較前面簡潔了許多。另外,如果程式中有其他的函式需要類似的裝飾,只需要在它們的上方加上 @my_decorator 就可以了,大大提高了程式的可複用性和可讀性。

3. 帶引數的裝飾器
def my_decorator(func):
    def wrapper(greet):
        print("Do something before function is called")
        func(greet)
        print("Do something after function is called")
    return wrapper


@my_decorator
def foo(greet):
    print(f"{greet}, I am foo")


foo("Hello")

output:

Do something before function is called
Hello, I am foo
Do something after function is called

在為函式 foo() 加了引數 greet 之後,呼叫函式 foo() 時便可以傳入引數。當然裝飾器也要做相應的修改,為函式 wrapper 也添加了引數 greet。但是上述加引數的方式有一個缺點,當使用這個裝飾器來裝飾一個不帶引數的函式時,呼叫便會發生錯誤。例如:

def my_decorator(func):
    def wrapper(greet):
        print("Do something before function is called")
        func(greet)
        print("Do something after function is called")
    return wrapper


@my_decorator
def foo():
    print("I am foo")


foo()

output:

Traceback (most recent call last):
  File "/Users/weisong/PycharmProjects/pythonProject/greet.py", line 14, in <module>
    foo()
TypeError: wrapper() missing 1 required positional argument: 'greet'

為了相容帶引數的呼叫和不帶引數的呼叫,可以將裝飾器函式修改如下:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Do something before function is called")
        func(*args, **kwargs)
        print("Do something after function is called")
    return wrapper


@my_decorator
def foo():
    print("I am foo")


@my_decorator
def bar(greet):
    print(f"{greet}, I am bar")


foo()
bar("Hello")

output:

Do something before function is called
I am foo
Do something after function is called
Do something before function is called
Hello, I am bar
Do something after function is called
4. 帶自定義引數的裝飾器
def repeat(num):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            print("do something before function is called")
            for i in range(num):
                func(*args, **kwargs)
            print("do something after function is called")
        return wrapper
    return my_decorator


@repeat(2)
def foo():
    print("I am foo")


@repeat(3)
def bar(greet):
    print(f"{greet}, I am bar")


foo()
bar("Hello")

output:

Do something before function is called
I am foo
I am foo
Do something after function is called
Do something before function is called
Hello, I am bar
Hello, I am bar
Hello, I am bar
Do something after function is called

上面的例子中,使用裝飾器時傳入了引數 num ,用來表示內部函式執行的次數。

5. 被裝飾的函式還是它自己嗎?

還是使用前面的例子,我們打出函式 foobar() 的元資訊,看看被裝飾器修飾後還是不是它們自己。

def repeat(num):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            print("Do something before function is called")
            for i in range(num):
                func(*args, **kwargs)
            print("Do something after function is called")
        return wrapper
    return my_decorator


@repeat(2)
def foo():
    print("I am foo")


@repeat(3)
def bar(greet):
    print(f"{greet}, I am bar")
    

print(foo.__name__)
help(foo)
print("**************************")
print(bar.__name__)
help(bar)

output:

wrapper
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

**************************
wrapper
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

從輸出結果可以看出,在被裝飾器修飾後,被修飾函式的元資訊改變了。變成了 wrapper() 函式。可以使用內建的裝飾器@functools.wrap 來解決這個問題,它會保留被修飾函式的元資訊。

import functools


def repeat(num):
    def my_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print("Do something before function is called")
            for i in range(num):
                func(*args, **kwargs)
            print("Do something after function is called")
        return wrapper
    return my_decorator


@repeat(2)
def foo():
    print("I am foo")


@repeat(3)
def bar(greet):
    print(f"{greet}, I am bar")


print(foo.__name__)
help(foo)
print("**************************")
print(bar.__name__)
help(bar)

output:

foo
Help on function foo in module __main__:

foo()

**************************
bar
Help on function bar in module __main__:

bar(greet)

在使用@functools.wrap 之後,被修飾函式保留了原有的元資訊。

裝飾器的應用例項

1. 記錄函式執行的時間
import functools
import time


def timer(func):
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()
        func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
    return wrapper_timer


@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])


waste_some_time(1)
waste_some_time(1000)

output:

Finished 'waste_some_time' in 0.0037 secs
Finished 'waste_some_time' in 3.3343 secs
2. 控制函式相鄰兩次執行的時間間隔
import functools
import time


def slow_down(func):
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down


@slow_down
def countdown(from_number):
    if from_number < 1:
        print("Liftoff!")
    else:
        print(from_number)
        countdown(from_number - 1)


countdown(6)

output:

6
5
4
3
2
1
Liftoff!
3. 程式碼 Debug
import functools


def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  
        signature = ", ".join(args_repr + kwargs_repr)           
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           
        return value
    return wrapper_debug


@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"


make_greeting("foo")
make_greeting("foo", 18)

output:

Calling make_greeting('foo')
'make_greeting' returned 'Howdy foo!'
Calling make_greeting('foo', 18)
'make_greeting' returned 'Whoa foo! 18 already, you are growing up!'

總結

本文講述了裝飾器的原理以及用法,裝飾器的存在大大提高了程式碼的可複用性以及簡潔性。
在這裡插入圖片描述