說說在 Python 中如何實現輸出指定函數運行時長的裝飾器

假設我們需要一個可以輸出某個函數運行時長的裝飾器。

1 基礎實現

一種可能的定義方式爲:

import time


def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        logging.info('[%0.8fs] %s(%s) -> %r', elapsed, name, arg_str, result)
        return result

    return clocked

這裏利用函數裝飾器,在 clock(func) 函數內部定義了一個 clock(*args) 函數,定義好後直接返回。內部利用 perf_count() 函數實現計算函數運行時長。每調用一次 perf_counter(),Python 就會記錄一個時間點,類似於在秒錶上按下開始計時鍵;當第二次調用該函數時,會計算與第一個時間點的時間長,類似於在秒錶上按下結束計時鍵1

func.__name__ 會返回入參函數的名稱。repr(arg) 會返回一個 arg的 string 格式2。通過一系列轉換,我們就可以得到一個以逗號作爲分隔符的入參字符串。

內部函數最後以這樣的一種格式 [時長] 運行函數名(多個入參字符串) -> 輸出結果 輸出函數運行報告。其中的 %0.8fs 表示小數保留8位,然後再轉換爲字符串。

而外部函數最後返回這個 clocked(*args) 函數。

接着我們使用這個 clock 裝飾器,來輸出以下兩個函數的運行報告:

  1. 睡眠函數;
  2. 斐波那契函數。
import time
from course_6.clock_decorator import clock


@clock
def snooze(seconds):
    time.sleep(seconds)


@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)


if __name__ == '__main__':
    logging.info('snooze(.123) -> %s', snooze(.123))
    logging.info('factorial(6) -> %s', factorial(6))

運行結果:


這裏使用 @裝飾函數名 這樣的語法來包裝我們需要運行的函數。

@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)

實際上等價於:

def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)

factorial = clock(factorial)

所以從寫法上來講,第一種方式更加簡潔。

如果輸出 logging.info('factorial.__name__ -> %s',factorial.__name__) 就會得到 factorial.__name__ -> clocked。這就說明了 factorial 實際上是 clocked 函數,也就是說factorial 函數已經被裝飾爲 clocked 函數。所以每次調用 factorial 函數,本質上就是調用 clocked 函數。

2 優化

前面說了,如果輸出 logging.info('factorial.__name__ -> %s',factorial.__name__) 就會得到 factorial.__name__ -> clocked。也就是說,裝飾函數 clock(func) 把 factorial(n) 函數給遮住了。如果我們不想被裝飾函數的 __name__ 屬性被遮住,可以這樣做:

@functools.wraps 也是一個裝飾器,它可以把 func 中的相關屬性複製到 clocked 中。這樣再次輸出logging.info('factorial.__name__ -> %s',factorial.__name__) 就會得到 factorial.__name__ -> factorial 咯。


這實際上就是經典的裝飾器設計模式,但在是實現方式上與普通的面嚮對象語言差別較大。普通的面嚮對象語言採用的是面向對象的編程方式,而 Python 採用的是面向函數的編程方式。


  1. Python3 perf_counter() 用法.
  2. Python repr() 函數.
  3. Luciano Ramalho (作者),安道,吳珂 (譯者).流暢的Python[M].人民郵電出版社,2017:319-322.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章