簡體   English   中英

Python 裝飾器到時間遞歸函數

[英]Python decorator to time recursive functions

我有一個簡單的裝飾器來跟蹤 function 調用的運行時間:

def timed(f):
    def caller(*args):
        start = time.time()
        res = f(*args)
        end = time.time()
        return res, end - start
    return caller

這可以如下使用,並返回 function 結果和執行時間的元組。

@timed
def test(n):
    for _ in range(n):
        pass
    return 0

print(test(900)) # prints (0, 2.69e-05)

很簡單。 但現在我想將其應用於遞歸函數。 正如預期的那樣,將上述包裝器應用於遞歸 function 會生成帶有每個遞歸調用時間的嵌套元組。

@timed
def rec(n):
    if n:
        return rec(n - 1)
    else:
        return 0

print(rec(3)) # Prints ((((0, 1.90e-06), 8.10e-06), 1.28e-05), 1.90e-05)

編寫裝飾器以便正確處理遞歸的優雅方式是什么? 顯然,如果是定時 function,您可以結束通話:

@timed
def wrapper():
    return rec(3)

這將給出結果和時間的元組,但我希望所有這些都由裝飾器處理,以便調用者不必擔心為每次調用定義一個新的 function。 想法?

這里的問題不是裝飾者。 問題是rec需要rec成為一種行為方式的函數,但是你希望rec是一個行為不同的函數。 沒有干凈的方法來協調單個rec函數。

最干凈的選擇是停止要求rec同時做兩件事。 而不是使用裝飾符號,將timed(rec)分配給另一個名稱:

def rec(n):
    ...

timed_rec = timed(rec)

如果您不想要兩個名稱,則需要編寫rec以了解裝飾的rec將返回的實際值。 例如,

@timed
def rec(n):
    if n:
        val, runtime = rec(n-1)
        return val
    else:
        return 0

到目前為止,我更喜歡其他答案(特別是user2357112的答案 ),但您也可以創建一個基於類的裝飾器來檢測該功能是否已被激活,如果是,則繞過時間:

import time

class fancy_timed(object):
    def __init__(self, f):
        self.f = f
        self.active = False

    def __call__(self, *args):
        if self.active:
            return self.f(*args)
        start = time.time()
        self.active = True
        res = self.f(*args)
        end = time.time()
        self.active = False
        return res, end - start


@fancy_timed
def rec(n):
    if n:
        time.sleep(0.01)
        return rec(n - 1)
    else:
        return 0
print(rec(3))

(用(object)編寫的類,以便與py2k和py3k兼容)。

請注意,要真正正常工作,最外層的調用應該使用tryfinally 這是__call__幻想版本:

def __call__(self, *args):
    if self.active:
        return self.f(*args)
    try:
        start = time.time()
        self.active = True
        res = self.f(*args)
        end = time.time()
        return res, end - start
    finally:
        self.active = False

您可以通過* ahem *以不同的方式構建您的計時器*濫用contextmanagerfunction attribute ...

from contextlib import contextmanager
import time

@contextmanager
def timed(func):
    timed.start = time.time()
    try:
        yield func
    finally:
        timed.duration = time.time() - timed.start

def test(n):
    for _ in range(n):
        pass
    return n

def rec(n):
    if n:
        time.sleep(0.05) # extra delay to notice the difference
        return rec(n - 1)
    else:
        return n

with timed(rec) as r:
    print(t(10))
    print(t(20))

print(timed.duration)

with timed(test) as t:
    print(t(555555))
    print(t(666666))

print(timed.duration)

結果:

# recursive
0
0
1.5130000114440918

# non-recursive
555555
666666
0.053999900817871094

如果這被認為是一個糟糕的黑客,我會很樂意接受你的批評。

雖然它不是整合遞歸與裝飾器問題的整體解決方案,但對於時間問題,我已經驗證了時間元組的最后一個元素是整體運行時間,因為這是從上層開始的時間 - 最近的遞歸調用。 因此,如果你有

@timed
def rec():
    ...

給出你可以簡單做的原始函數定義來獲得整個運行時

rec()[1]

另一方面,獲取調用的結果將需要通過嵌套元組重用:

def get(tup):
    if isinstance(tup, tuple):
        return get(tup[0])
    else:
        return tup

這可能太復雜了,無法簡單地獲得您的函數的結果。

在嘗試分析簡單的quicksort實現時,我遇到了同樣的問題。

主要問題是裝飾器在每個 function 調用上執行,我們需要一些可以保留 state 的東西,所以我們可以在最后總結所有調用。 裝飾器不是工作的正確工具

然而,一種想法是濫用函數是對象並且可以具有屬性的事實。 下面用一個簡單的裝飾器對此進行探討。 必須了解的是,通過使用裝飾器的 sintax 糖 ( @ ),function 將始終累積其時序。

from typing import Any, Callable
from time import perf_counter

class timeit:

    def __init__(self, func: Callable) -> None:
        self.func = func
        self.timed = []

    def __call__(self, *args: Any, **kwds: Any) -> Any:
        start = perf_counter()
        res = self.func(*args, **kwds)
        end = perf_counter()        
        self.timed.append(end - start)
        return res

# usage

@timeit
def rec(n):
    ...

if __name__ == "__main__":
    result = rec(4) # rec result
    print(f"Took {rec.timed:.2f} seconds")
    # Out: Took 3.39 seconds
    result = rec(4) # rec result
    # timings between calls are accumulated
    # Out: Took 6.78 seconds

這給我們帶來了一個受@r.ook啟發的解決方案,下面是一個簡單的上下文管理器,它存儲每個運行時間並在最后打印其總和( __exit__ )。 請注意,因為對於每個時間我們都需要一個with語句,所以這不會累積不同的運行。

from typing import Any, Callable
from time import perf_counter

class timeit:

    def __init__(self, func: Callable) -> None:
        self.func = func
        self.timed = []

    def __call__(self, *args: Any, **kwds: Any) -> Any:
        start = perf_counter()
        res = self.func(*args, **kwds)
        end = perf_counter()
        self.timed.append(end - start)
        return res

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        # TODO: report `exc_*` if an exception get raised
        print(f"Took {sum(self.timed):.2f} seconds")
        return

# usage

def rec(n):
    ...

if __name__ == "__main__":

    with timeit(rec) as f:
        result = f(a) # rec result
    # Out: Took 3.39 seconds

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM