Python 裝飾器入門(上)


翻譯前想說的話:

  這是一篇介紹python裝飾器的文章,對比之前看到的類似介紹裝飾器的文章,個人認為無人可出其右,文章由淺到深,由函數介紹到裝飾器的高級應用,每個介紹必有例子說明。文章太長,看完原文后我計划按照文章作者的划分,將分為兩章翻出來和大家分享,如果你覺得干的還不錯,就點個贊吧.

 

目錄:

    • 函數
      • 一等對象
      • 內部函數
      • 從函數中返回函數
    • 簡單裝飾器
      • 語法糖
      • 復用裝飾器
      • 裝飾器傳參
      • 從裝飾器返回值
      • 你是誰?
    • 一些現實中的例子
      • 時間函數
      • 調試代碼
      • 給代碼降速
      • 注冊插件
      • 用戶是否登錄?
    • 有想象力的裝飾器
      • 裝飾類
      • 嵌套的裝飾器
      • 帶參數的裝飾器
      • Both Please, But Never Mind the Bread 這句話開始我不知道怎么翻,直到我看到了維尼熊......,請在這里www.google.com檢索Winnie the Pooh  Both Please, But Never Mind the Bread
      • 有狀態的裝飾器
      • 類裝飾器
    • 更多現實中的例子
      • 代碼降速,重新訪問
      • 創建單例模式
      • 緩存返回值
      • 添加單元信息
      • 驗證JSON

 正文開始:

在本次的裝飾器教程中,將介紹何為裝飾器以及如何創建和使用它們,裝飾器提供了簡單的語法來調用高階函數。

從定義上講,裝飾器是一個函數,它接收另一個函數作為參數並且擴展它的功能,但不會顯式的去修改它

說起來可能會讓人覺得難理解,但它(裝飾器)確實不會這么做,特別是一會你會看到一些裝飾器如何工作的例子

 函數

在理解裝飾器之前,你首先需要理解函數如何工作。函數會基於給定的參數返回值。這里有一個非常簡單的例子:

>>> def add_one(number):
...     return number + 1

>>> add_one(2)
3

通常情況下,函數在python中也會有其它功效而不是僅僅接收輸入並返回輸出。print()函數是一個例子。在控制台輸出的時候它會返回None(1),然而,為了理解裝飾器,
將函數認為是接收參數並返回值就足夠了

注意:在面向函數編程,你幾乎只會使用純函數,不會有其它功能,然而python不是一個純函數式語言,python支持許多函數式編程概念,包括一等對象

 一等對象

在python中,函數是一等對象,意思是函數可以作為參數被傳遞,就像其它的對象(string,int,fload,list和其它),思考下面的三個函數

def say_hello(name):
    return f"Hello {name}"

def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def greet_bob(greeter_func):
    return greeter_func("Bob")

在這里,say_hello()和be_awsone()是常規函數,接收一個name參數返回一個字符串,然而greet_bob()函數,接收一個函數作為他的參數,我們可以將say_hello()或者be_awesome()函數傳遞給它

>>> greet_bob(say_hello)
'Hello Bob'

>>> greet_bob(be_awesome)
'Yo Bob, together we are the awesomest!'

注意greet_bob(say_hello) 涉及到兩個函數,但是不同的是:greet_bob()和say_hello,say_hello函數並沒有使用(),代表只傳遞了對函數的引用,函數沒有運行,greet_bob()函數,是使用了括號,所以它會被正常調用

 內部函數

在函數內定義函數是被允許的。這類函數被稱為內部函數,這里有一個函數和兩個內函數的例子

def parent():
    print("Printing from the parent() function")

    def first_child():
        print("Printing from the first_child() function")

    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()

當你調用parent()的時候會發生什么? 請考慮一分鍾。會出現下面的輸出結果

>>> parent()
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function

注意內部函數定義的順序無關緊要,和其它的函數一樣,打印只會發生在內部函數運行的時候

而且,內部函數在父函數被調用之前不會生效,它們的局部作用域是父(),它們只作為局部變量存在在父()函數的內部,嘗試調用first_child(),你會得到下面的錯誤

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'first_child' is not defined

不管你何時調用parent(),內部函數first_child()和second_child()都會被調用,因為它們的局部作用域,它們無法再parent()函數外使用

從函數中返回函數

python允許使用函數來作為返回值,下面的例子從外部的父函數parent()返回了一個內部函數

def parent(num):
    def first_child():
        return "Hi, I am Emma"

    def second_child():
        return "Call me Liam"

    if num == 1:
        return first_child
    else:
        return second_chil

注意這里返回的first_child是沒有括號的,也就是返回了對函數first_child的引用, 帶括號的first_child() 指的是對函數求值的結果,這個可以在下面的實例中看到

>>> first = parent(1)
>>> second = parent(2)

>>> first
<function parent.<locals>.first_child at 0x7f599f1e2e18>

>>> second
<function parent.<locals>.second_child at 0x7f599dad5268>

這個輸出代表first變量引用了在parent()中的本地函數first_child(),second則指向了second_child()

你現在可以像常規函數一樣使用first和second,雖然他們指向的函數無法被直接訪問

>>> first()
'Hi, I am Emma'

>>> second()
'Call me Liam'

請注意,在前面的例子中我們在父函數中運行內部函數,例如first_child(),然后在最后的例子中,返回的時候沒有給內部函數first_child添加括號。這樣,就獲取了將來可以調用的函數的引用。這樣有意義嗎?

簡單裝飾器

現在你已經看到函數和python中的其它對象一樣,你已經准備好前進來認識python裝飾器,讓我們以一個例子開始:

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")

say_whee = my_decorator(say_whee)

你能猜到當你調用say_whee()的時候回發生什么么?試一下:

>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

要理解這里發生了什么,需要回看下之前的例子,我們只是應用了你到目前為止學到的所有東西

所謂的裝飾器發生在下面這行:

say_whee = my_decorator(say_whee)

事實上,say_whee現在指向了內部函數wrapper(),當你調用my_decorator(say_whee)的時候會將wrapper作為函數返回

>>> say_whee
<function my_decorator.<locals>.wrapper at 0x7f3c5dfd42f0>

wrapper()引用原始的say_whee()作為func,在兩個print()之間調用這個函數

簡而言之:裝飾器包裹一個函數,並改變它的行為

在繼續之前,讓我們看下第二個例子。因為wrapper()是一個常規的函數,裝飾器可以以一種動態的方式來修改函數。為了不打擾你的鄰居,下面的示例演示只會在白天運行的裝飾器

from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

def say_whee():
    print("Whee!")

say_whee = not_during_the_night(say_whee)

如果你在睡覺的時間調用say_whee(),不會發生任何事情

>>> say_whee()
>>>

語法糖

上面的裝飾器say_whee()用起來有一點笨拙。首先,你鍵入了三次say_whee,另外,裝飾器隱藏在了函數的定義之下

作為替代,python允許你使用@symbol的方式使用裝飾器,有時被稱為"pie"語法,下面的例子和之前第一個裝飾器做了同樣的事情

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_whee():
    print("Whee!")

所以,@my_decorator 只是say_whee = my_decorator(say_whee)的一種快捷方式,這就是如何將裝飾器應用到函數上

復用裝飾器

回想一下,裝飾器只是一個普通的函數。所有常用的工具都是方便重復利用的,讓我們將裝飾器移動到他自己的模型上以便於在其它的函數上使用

下面創建了一個decorators.py

def do_twice(func):
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice

注意:你可以隨意定義內部函數的名稱,通常像wrapper()用起來是沒問題的。你在這篇文章中會遇到許多裝飾器。為了區別開它們,我們將使用decorator名稱來命名內部函數,但會加上wrapper_前綴。

你可以使用常規導入來使用一個新的裝飾器

from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")

當你運行這個例子,你會看到原始韓式say_whee()執行兩次

>>> say_whee()
Whee!
Whee!

裝飾器傳參

如果你有一個函數需要接收一些參數,這時候還可以再使用裝飾器么,然我們試試

from decorators import do_twice

@do_twice
def greet(name):
    print(f"Hello {name}")

不幸的是,運行代碼拋出了錯誤

>>> greet("World")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given

問題在於內部函數wrapper_do_twice()沒有接收任何參數,但是name="World"卻傳給了它。你可以讓wrapper_do_twice()接收一個參數來修補這個問題,但是這樣前面的say_whee()函數就無法工作了

解決方案是在內部函數使用*args和**kwargs ,這樣它會允許接收任意個關鍵參數,下面重寫了decorators.py

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper_do_twice

內部函數wrapper_do_twice()現在接收任意數量的參數並會傳遞給裝飾的函數,目前say_whee()和greet()都會正常工作

>>> say_whee()
Whee!
Whee!

>>> greet("World")
Hello World
Hello World

從裝飾器返回值

被裝飾的函數返回值會發生什么?這會由裝飾器來決定,我們下面有一個簡單的裝飾器函數

from decorators import do_twice

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

嘗試運行它:

>>> hi_adam = return_greeting("Adam")
Creating greeting
Creating greeting
>>> print(hi_adam)
None

裝飾器吃掉了從函數返回的值

因為do_twice_wrapper()沒有返回值,調用 return_greeting("Adam") 最后返回了None

修復的方式是,需要確認裝飾器返回它裝飾的函數的值,改變decorators.py文件:

def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice

執行這個函數返回的值:

>>> return_greeting("Adam")
Creating greeting
Creating greeting
'Hi Adam'

你是誰?

在使用Python(尤其是在交互式shell中)時,強大的內省是非常方便的功能。內省是對象在運行時了解其自身屬性的能力。例如,函數知道自己的名稱和文檔:

>>> print
<built-in function print>

>>> print.__name__
'print'

>>> help(print)
Help on built-in function print in module builtins:

print(...)
    <full help message>

內省同樣適用於你自定義的函數:

>>> say_whee
<function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>

>>> say_whee.__name__
'wrapper_do_twice'

>>> help(say_whee)
Help on function wrapper_do_twice in module decorators:

wrapper_do_twice()


然而在被裝飾后,say_whee()會對自身感到疑惑。它現在顯示為 do_twice()裝飾器的內部函數 wrapper_do_twice()
    
為了修復這個,裝飾器需要使用@functools.wraps裝飾器,它會保留原始函數的信息,再次更新下decorators.py:

import functools

def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper_do_twice

不需要對被裝飾的say_whee()函數做任何更改:

>>> say_whee
<function say_whee at 0x7ff79a60f2f0>

>>> say_whee.__name__
'say_whee'

>>> help(say_whee)
Help on function say_whee in module whee:

say_whee()


非常好,現在say_whee()在被裝飾后可以保持自己

技術細節:@funtools.wraps 裝飾器使用函數functools.update_wrapper()來更新指定的屬性,像__name__和__doc__來用於自省

 一些現實中的例子

讓我們看一些用處更大的裝飾器例子。你會注意到他們主要的模式和你現在所學的都是一樣的

import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator

對於構建更復雜的裝飾器,這個是一個很好的模板

時間函數

讓我們從@timer裝飾器開始,它會測量函數運行的時間並且打印持續時間到控制台,這是代碼:

import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 1
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 2
        run_time = end_time - start_time    # 3
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

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

這個函數是在函數運行之前獲取時間(#1行),並且在函數運行結束之后獲取時間(#2行),我們使用 time.perf_counter() 函數,這個函數可以非常好的計算時間間隔。下面是一個示例:

>>> waste_some_time(1)
Finished 'waste_some_time' in 0.0010 secs

>>> waste_some_time(999)
Finished 'waste_some_time' in 0.3260 secs


自己運行測試下,手敲下這里的代碼,確保你理解它的工作原理。如果不明白,也不要擔心。裝飾器是高級方法,試着思考下或者畫下流程圖

注意: 如果你只是想獲取函數的運行時間,@timer 裝飾器可以滿足。如果你想獲取到更精確的數據,你應該考慮使用timeit 模塊來替代它。它臨時禁用了垃圾收集並且運行多次以避免函數快速調用帶來的噪音數據

調試代碼

下面的@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]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug

signature 變量是通過 字符串表示方法 來創建所有的輸入參數。下面的數字對應了代碼中的注釋
    1、將args創建為列表,使用repr修飾
    2、將kwargs創建為列表,使用f-string格式化參數為key=value,!r表示使用repr()表示值
    3、args和kwargs轉換后會合並在signature變量中,使用逗號分隔每個變量
    4、函數運行結束后會返回值

讓我們在一個簡單的函數中使用裝飾器被觀察它是如何運行的,被裝飾的函數只有一個位置參數和一個關鍵字參數

@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!"

注意@debug裝飾器如何打印make_greeting()函數的signature 和返回值

>>> make_greeting("Benjamin")
Calling make_greeting('Benjamin')
'make_greeting' returned 'Howdy Benjamin!'
'Howdy Benjamin!'

>>> make_greeting("Richard", age=112)
Calling make_greeting('Richard', age=112)
'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'
'Whoa Richard! 112 already, you are growing up!'

>>> make_greeting(name="Dorrisile", age=116)
Calling make_greeting(name='Dorrisile', age=116)
'make_greeting' returned 'Whoa Dorrisile! 116 already, you are growing up!'
'Whoa Dorrisile! 116 already, you are growing up!'     

@debug修飾符看起來只是重復了我們剛才寫的內容 ,並不是非常有用。 但當應用到不能直接修改的其它函數時,它會更加強大。

下面的例子計算了一個數學常數E的近似值

import math
from decorators import debug

# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))

這個例子還演示了如何將裝飾器應用到已經定義了的函數

當調用approximate_e()函數,你可以看到@debug函數在工作:

 

>>> approximate_e(5)
Calling factorial(0)
'factorial' returned 1
Calling factorial(1)
'factorial' returned 1
Calling factorial(2)
'factorial' returned 2
Calling factorial(3)
'factorial' returned 6
Calling factorial(4)
'factorial' returned 24
2.708333333333333

在這個例子中,可以得到一個真實值的近似值e = 2.718281828

給代碼降速

下面的例子看起來可能不是很有用。可能最常見的用例是,您希望對一個不斷檢查資源是否存在的函數進行速率限制 。 @slow_down decorator在調用被修飾的函數之前會暫停一秒鍾

import functools
import time

def slow_down(func):
    """Sleep 1 second before calling the function"""
    @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)       

來看下@slow_down裝飾器的效果,你需要自己運行跑下

>>> countdown(3)
3
2
1
Liftoff!   

countdown()是一個遞歸函數。也就是說,它是一個調用自身的函數 。     

注冊插件

裝飾器不是必須要修飾被裝飾的函數(這句話不太好翻譯,看下面的例子理解起來很容易),它還可以簡單地注冊一個函數,並將其解包返回,例如,可以使用它來創建一個輕量級插件體系結構:

import random
PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name) 

@register裝飾器只是在全局PLUGINS 字典中儲存了被裝飾函數的引用。注意你不需要在例子中寫內部函數或者使用@functools.wraps ,因為返回的是一個未經過修改的初始函數

randomly_greet()函數在注冊函數中隨機選擇一個使用。注意PLUGINS字典已經包含了對注冊為插件的每個函數對象的引用:

 >>> PLUGINS
{'say_hello': <function say_hello at 0x7f768eae6730>,
 'be_awesome': <function be_awesome at 0x7f768eae67b8>}

>>> randomly_greet("Alice")
Using 'say_hello'
'Hello Alice'

這個插件的主要用處在於不需要再單獨維護一個插件列表。這個列表在插件注冊時自動創建,使得添加一個新插件變得很簡單,只需定義函數並用@register裝飾即可。

如果你對python中的globals()函數熟悉,你可能會看到一些和我們的插件結構相似之處。globals()可以訪問當前作用於的所有全局變量

包括我們的插件:

>>> globals()
{..., # Lots of variables not shown here.
 'say_hello': <function say_hello at 0x7f768eae6730>,
 'be_awesome': <function be_awesome at 0x7f768eae67b8>,
 'randomly_greet': <function randomly_greet at 0x7f768eae6840>}

使用@register 裝飾器,可以創建感興趣的變量管理列表,有效地從globals()中篩選出一些函數

用戶是否登錄?

在繼續討論一些更有趣的裝飾器之前,讓我們在最后一個示例中演示通常在處理web框架時使用的裝飾器。在這個例子中,我們使用Flask去設置一個/secret web頁面,這個頁面只對登錄用戶或者其他有權限的用戶展示

from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)

def login_required(func):
    """Make sure user is logged in before proceeding"""
    @functools.wraps(func)
    def wrapper_login_required(*args, **kwargs):
        if g.user is None:
            return redirect(url_for("login", next=request.url))
        return func(*args, **kwargs)
    return wrapper_login_required

@app.route("/secret")
@login_required
def secret():
    ...    

雖然這里演示了如何對web框架添加身份驗證嗎,但通常不應該自己編寫這些類型的裝飾器。對於Flask可以使用Flask-login擴展,這里的功能更豐富也更加安全

有想象力的裝飾器
到目前為止,你已經看到了如何創建簡單的裝飾器並且非常了解什么是裝飾器以及它們是如何工作的。請從這篇文章中休息一下,練習學到的一切。

在本教程的第二部分中,我們將探索更高級的特性,包括如何使用以下特性:
    1、在類上使用裝飾器(裝飾類)
    2、在一個函數上應用多個裝飾器
    3、帶參數的裝飾器
    4、可以選擇是否接收參數的裝飾器
    5、帶狀態的裝飾器
    6、類裝飾器


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM