Python: decoratorの簡単な練習4-フィボナッチ数の算出にかかる時間を計算

Python

細かく分かれてしまいましたが、Decorator4つ目。
(再帰の説明に言葉足らずな所があったので修正しました: 2021/11/14)

ベンチマーク計測を続けます。

フィボナッチ数の処理にかかる時間を測る

ベンチマーク計測と言えばやはりフィボナッチ数(Fibonacci number)

詳細は上記リンク先wikipediaをご参照頂くのが一番ですが、簡単に言うと
F0(初項)は1、F1(第2項)は1、F2(第3項)以降は前の2項の数字を足した合計を解とする一連の数字。

初項から第10項迄のフィボナッチ数列は1, 1, 2, 3, 5, 8, 13, 21, 34, 55となります。

プログラムを組んで1から計算するとなると、最初は分かり易い、再帰(recursion)を使用したコードになりがち。

再帰を利用した計算では、Fx(第x項)の数は、前の2項の計算結果、Fx-1とFx-2を足した合計、と言う事になる。

しかしそのFx-1とFx-2は、Fx-2 と Fx-3の合計とFx-3 と Fx-4の合計であり、またそれぞれが前の2項の合計、と言う事になり、xが大きくなるほどに計算量が激増して時間が掛かってしまう。

この再帰で計算しているフィボナッチのコードを測ると測り甲斐がありそう。

ベンチマークをはかるdecoratorの下に、再帰を利用して第n項のフィボナッチ数を計算する関数fibo1(n)を記載した下記コードをbench2.pyとして保存する。
nが多いと計算が終わらなくなるので、第5項までとする。

import time
import functools

def Timer(func):
    @functools.wraps(func)
    def Wrapper(*args, **kwargs):
        stime = time.process_time()
        ret = func(*args, **kwargs)
        etime = time.process_time()
        print('{0}: {1:,f}ms'.format(func.__name__, (etime - stime)*1000))
        return ret
    return Wrapper 

# Fibonacci recursion1
@Timer
def fibo1(n):
    if (n == 1) or (n == 2):
        return 1
    return fibo1(n - 2) + fibo1(n - 1)

print(fibo1(5))

実行すると、あれ?今までと違う出力。

% python3 bench2.py
fibo1: 0.000000ms
fibo1: 0.001000ms
fibo1: 0.065000ms
fibo1: 0.001000ms
fibo1: 0.000000ms
fibo1: 0.001000ms
fibo1: 0.007000ms
fibo1: 0.014000ms
fibo1: 0.088000ms
5

このエントリの3つ前の下記エントリでは
「decoratorは関数forloop()全体をデコレートする」と言う理解でした。

なぜbench2.pyでは何度もdecoratorが走っているのか?

これはrecursion(再帰)を利用したからだと考えられる。

因みにこのrecursion(再帰)(以下recursion)と は、「関数の中で自分自身を呼びだす事」である。

今回のfibo1(5)においては、下記計算が行われているが、これは実際の所、1から関数fibo1(n)を呼び出して実行し、実行の結果返ってきた戻り値を利用して更なるfibo1(n)を実行している。

1:f1を計算
2:f2を計算
3:f3を計算(f3=f1 + f2)
4:f4を計算1(f3を算出= f1 + f2)
5:f4を計算2(f4=「4で計算したf3」 + f2)
6:f5を計算1(f3を算出= f1 + f2)
7:f5を計算2(f4を算出1: f3を算出=f1+ f2)
8:f5を計算3(f4を算出2:f4=「7で計算したf3」 + f2)
9:f5を計算4(f5= 「7で計算したf3」 + 「8で計算したf4」)


関数の中で何度もfibo1(n)関数を呼び出している訳だが、Decoratorが走って時間を計測しているのも9回なので、それぞれの関数呼び出しの際にDecorator時間が計測したと思って差し支えないだろう。

再帰を使用する関数の全体の時間を図る

上記の様に、再帰の関数では関数が呼び出される都度decoratorが走って時間を計測してしまう。

こんな場合は再帰の関数ごと関数で括ってしまおう。
fibo1(n)をTestfibo1(n)で括り、そのTestfibo1(n)をdecorateした下記コードをbench2b.pyとして保存、実行。

import time
import functools

def Timer(func):
    @functools.wraps(func)
    def Wrapper(*args, **kwargs):
        stime = time.process_time()
        ret = func(*args, **kwargs)
        etime = time.process_time()
        print('{0}: {1:,f}ms'.format(func.__name__, (etime - stime)*1000))
        return ret
    return Wrapper 

# Fibonacci recurcion1r
@Timer
def Testfibo1r(n):
    def fibo1(n):
        if (n == 1) or (n == 2):
            return 1
        return fibo1(n - 2) + fibo1(n - 1)
    return fibo1(n)

print(Testfibo1r(5))
%python3 bench2b.py 
Testfibo1r: 0.016000ms
5

測ることが出来た。

色々なFibonacci numberの計算方法を試す。

import time
import functools

def Timer(func):
    @functools.wraps(func)
    def Wrapper(*args, **kwargs):
        stime = time.process_time()
        ret = func(*args, **kwargs)
        etime = time.process_time()
        print('{0}: {1:,f}ms'.format(func.__name__, (etime - stime)*1000))
        return ret
    return Wrapper 
    
# Fibonacci recurcion1r
@Timer
def Testfibo1r(n):
    def fibo1(n):
        if (n == 1) or (n == 2):
            return 1
        return fibo1(n - 2) + fibo1(n - 1)
    return fibo1(n)

print(Testfibo1r(30))

# Fibonacci recursion & memo
memo = {1:1, 2:1}
@Timer
def Testfibo2rm(n):
    def fibo2(n):
        if n in memo:
            return memo[n]
    
        memo[n] = fibo2(n - 2) + fibo2(n - 1)
        return memo[n]
    return fibo2(n)

print(Testfibo2rm(30)) 

# Fibonacci loop & list(fib)
@Timer
def fibo3fl(n):
    fib = [1, 1]
    for i in range(2, n):
        fib.append(fib[i - 2] + fib[i - 1])
    return fib[n - 1]

print(fibo3fl(30))

今回は違いを明確にする為に、第30項の数を算出。

14行目# Fibonacci recurcion1rの関数Testfibo1r(30)はさっきと同じ、再帰による計算。

25行目# Fibonacci recursion & memoのの関数Testfibo2rm(30)は計算途中の第x項の数をdictionary型のmemoに残していく処理。今まで計算していた数がmemo内にあればそれを利用し、無ければ前2項の数を足した上でmemoに残す。計算を繰り返す必要がないので、時間は格段に早くなるはず。

39行目# Fibonacci loopの関数fibo3fl(30)は、フィボナッチ数をfor loopで算出、list”fib”に残していく方法。
再帰を使用していないので、関数で纏めなくても時間を計ることが出来る
計算の都度結果をlist”fib”に残していくので計算は早いはず。

実行結果

% python3 bench3.py
Testfibo1r: 187.830000ms
832040
Testfibo2rm: 0.018000ms
832040
fibo3: 0.012000ms
832040

ただ再帰で計算した場合と比べ、再帰+メモ、for loopは格段に早いことが分かる。

コメント

タイトルとURLをコピーしました