今回の話題:
フィボナッチ数列([1, 1, 2, 3, 5, 8, 13, 21,...])とは
$a_0 = 1$
$a_1 = 1$
$a_n = a_{n-1} + a_{n-2} \qquad \text{ ( for}\ n \gt 1 \text{ )}$
で定義される数列です。
漸化式を解いて、
$a_=\frac{1}{\sqrt 5}\left\lbrace(\frac{1+\sqrt 5}{2})^{n+1} - (\frac{1-\sqrt 5}{2})^{n+1}\right\rbrace$
と定義することも可能です。
整数$n$に対してフィボナッチの数列の$n$番目の数、$a_n$は一意に定りますから、これを関数と考えることもできます。
$fib: n \mapsto a_n$
漸化式を直接使って、Pythonでこの数列を関数として表現して見ます。
def fib(n:int)->int:
"""
フィボナッチ数列の n番目(n:= 0,1,2,..)の値を求める。
例:
>>> fib(2)
2
>>> fib(5)
8
>>> fib(0), fib(1), fib(2), fib(3), fib(4), fib(5),
(1, 1, 2, 3, 5, 8)
"""
if n == 0 or n == 1:
value= 1
else:
value= fib(n-1) + fib(n-2)
return value
fib
の定義にはdocstring
が含まれています。
これを使うことで、実行時にこの関数の使い方を確認することができます。
help(fib)
Help on function fib in module __main__: fib(n: int) -> int フィボナッチ数列の n番目(n:= 0,1,2,..)の値を求める。 例: >>> fib(2) 2 >>> fib(5) 8 >>> fib(0), fib(1), fib(2), fib(3), fib(4), fib(5), (1, 1, 2, 3, 5, 8)
定義した関数 fib(n)
が、正しくフィボナッチの数列を求めているかどうかを確かめて見ましょう。
fib(0), fib(1), fib(2), fib(3), fib(4), fib(5),
(1, 1, 2, 3, 5, 8)
とフィボナッチ数が正しく計算されていることが確認できました。
さきほどは、fib
関数の動作確認のために、fib(0),...
などを書き並べました。
しかし、いちいち引数の値を書くのは大変です。
for
文を使えば異なる引数の値に対して、関数の値を求めることも簡単です。
for i in range(0,10,1):
print("fib(",i,")=",fib(i), end=", ")
print("...")
fib( 0 )= 1, fib( 1 )= 1, fib( 2 )= 2, fib( 3 )= 3, fib( 4 )= 5, fib( 5 )= 8, fib( 6 )= 13, fib( 7 )= 21, fib( 8 )= 34, fib( 9 )= 55, ...
for
文は
for <識別子> in <リストやタプルなどのオブジェクト:iterable> :
<プログラム文>
...
の形をしています。この文を実行すると、<リスト、タプル、などのオブジェクト>
の要素毎に、その値が<識別子>
に割り当てられて、:
の後に続く <プログラム文>
が実行されます。
for i in range(0,10,1):
print("fib(",i,")=", fib(i), end=", ")
print("...")
fib( 0 )= 1, fib( 1 )= 1, fib( 2 )= 2, fib( 3 )= 3, fib( 4 )= 5, fib( 5 )= 8, fib( 6 )= 13, fib( 7 )= 21, fib( 8 )= 34, fib( 9 )= 55, ...
この繰り返しの中で、i
は 0
から始まり、i < 10
の範囲で1
ずつ増えていることがわかります。
list()
関数を使うことで明示的に値の範囲を確認できます。
range(0,10,1)
は0から始まって、10以下の、1づつ増える整数の列を表現しています。
list(range(0, 10, 1))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
type(range(0, 10, 1))
range
for
文を使うことで、 一連の$n$の値に対して、フィボナッチ数列の値を求めることができました。 せっかく計算したこれらの結果を印刷するだけでなく、データとして再利用できれば便利です。
こうした一連のデータをまとめて表現するために、Pythonにはリスト
と タブル
という2種類の一列に並んだデータを表現するデータ型が用意されています。
1から5までの整数に対するフィボナッチ数列の値を要素に持つリスト
は次のようにして、作ることができます。
list_of_fib=[ fib(0),fib(1),fib(2),fib(3),fib(4),fib(5) ]
print(list_of_fib)
[1, 1, 2, 3, 5, 8]
一方、1から5までの整数に対するフィボナッチ数列の値を要素に持つタプル
は次のように作ることができます。
tuple_of_fib=( fib(0),fib(1),fib(2),fib(3),fib(4),fib(5) )
print(tuple_of_fib)
(1, 1, 2, 3, 5, 8)
このように、リストは[]
で囲まれたデータの並び、タプルは()
で囲まれたデータの並びとして表現されます。
リストやタプルは、for
文での変数の範囲を指定するために使うことができます。
print("for loop with a list:")
for i in list_of_fib:
print(i, end=", ")
print("\n***")
print("for loop with a tuple:")
for i in tuple_of_fib:
print(i, end=", ")
for loop with a list: 1, 1, 2, 3, 5, 8, *** for loop with a tuple: 1, 1, 2, 3, 5, 8,
リスト
:記号[
と]
でリストの要素を,
で区切って並べる。 ([1,2,3]
と[1,2,3,]
は同じ意味)タブル
:記号(
と)
でリストの要素を,
で区切って並べる。((1,2,3)
と(1,2,3,)
は同じ意味)[]
は空のリスト(要素数が0)を表す。()
は空のタプル(要素数が0)を表す。(1,)
と書く必要がある。 [(1)
は 数式と解釈されるため][1] == [1,], (1)==(1,)
(True, False)
リストやタプルの要素を取り出すことができます。
S[0]
S[-1]
S[n]
S[n:m]
S[n:]
S[n:m:l]
pythonでは指標(インデックス)は要素と要素の間を指していると考えると、理解しやすいでしょう。
list_of_fib
[1, 1, 2, 3, 5, 8]
list_of_fib[0:-1:2]
[1, 2, 5]
0:-1:2
はpythonではスライスと呼ばれます。
スライスの増分を負の整数にすれば、リスト/タプルを逆順に取り出すことも可能です。
list_of_fib[:-8:-1]
[8, 5, 3, 2, 1, 1]
tuple_of_fib[:-8:-1]
(8, 5, 3, 2, 1, 1)
s= slice(0,-1,2) #スライスオブジェクトを作成する。
list_of_fib[s]
[1, 2, 5]
- リストもタプルも n番目(nは0始まり)の要素を [n]
を変数名(オブジェクト)に追加することで取り出すことができます。
ところが
という大きな違いがあります。
リストの最初の要素(0番の要素)を置き換えてみます。
try:
list_of_fib[0]=2; print("list:", list_of_fib)
except TypeError as err:
print("TypeError:",err)
list: [2, 1, 2, 3, 5, 8]
tupleの要素を置き換えようとすると、...
try:
tuple_of_fib[0]=2; print("tuple:", tuple_of_fib)
except TypeError as err:
print("TypeError:",err)
TypeError: 'tuple' object does not support item assignment
とエラー(TypeError 例外発生)となります。 このように、 リストの要素を置き換えることはできますが、タプルの要素を置き換えることはできません。
リスト/タプルの要素の変更と同様に、リストに要素を追加する(append
)ことはできますが、タプルに要素を追加することはできません。
#リストへの要素の追加 append
print("追加前:", id(list_of_fib), list_of_fib )
list_of_fib.append(fib(len(list_of_fib)))
print("追加後:", id(list_of_fib), list_of_fib)
追加前: 140681530275776 [2, 1, 2, 3, 5, 8] 追加後: 140681530275776 [2, 1, 2, 3, 5, 8, 13]
要素の追加前後で、リストの id
が変わっていないことに注意してください。
# タプルへの要素の追加
print("追加前:",id(tuple_of_fib), tuple_of_fib)
try:
tuple_of_fib.append(fib(len(tuple_of_fib)))
print("追加後:", id(tuple_of_fib), tuple_of_fib)
except AttributeError as err:
print("AttributeError:", err)
tuple_of_fib=tuple_of_fib + (fib(len(tuple_of_fib)),)
print("追加後:",id(tuple_of_fib), tuple_of_fib)
追加前: 140681530408672 (1, 1, 2, 3, 5, 8) AttributeError: 'tuple' object has no attribute 'append' 追加後: 140681530408576 (1, 1, 2, 3, 5, 8, 13)
というように、タプルに要素を追加することはできません。そもそもタプルは.append
メソッドを持っていません(AttributeError
)。
タプルに新しい要素を付け加えたタプルを作り直して、変数に割り当て直すことは可能です。
フィボナッチ数列の最初の10項を成分にもつリストはリスト内包表記を使って作ることができます。
# リスト内包表記(list comprehension
list_of_fibs=[fib(i) for i in range(10)]
list_of_fibs
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
ジェネレーター式はリスト内包表記に似た形ですが、()
で囲われています。(リスト内包表記は[]
)
ジェネレーター式の値は ジェネレーター オブジェクトです。
ジェネレーター オブジェクトは、next()
で呼ばれるたびに、次の要素を値として返します。
[参考: このように、next()
の呼び出しに対して、次々と値を返すオブジェクトをiterable
と呼びます。]
# ジェネレーター式
g=(fib(i) for i in range(10))
print ("type of generator expression:", type(g))
for f in range(10):
print(next(g),end=", ")
type of generator expression: <class 'generator'> 1, 1, 2, 3, 5, 8, 13, 21, 34, 55,
ジェネレーター式はfor()
文で変数の範囲を示すために利用できます。
# ジェネレーター式
g=(fib(i) for i in range(10))
for i in g:
print(i,end=", ")
1, 1, 2, 3, 5, 8, 13, 21, 34, 55,
for x in (fib(i) for i in range(10)):
print (x, end=", ")
1, 1, 2, 3, 5, 8, 13, 21, 34, 55,
print([fib(i) for i in range(6)])
[1, 1, 2, 3, 5, 8]
pythonのリストおよびタプルというデータ型を紹介しました。
ここで、pythonに組み込みのデータ型を挙げて見ます。今回取り上げないデータ型は後ほど取り上げてい行きます。
Pythonではさらにclass
文を使い、 新しいデータ型を定義することができます。 class
文はブジェクト志向プログラム(OOP:Object Oriented Program)で使われます。
(class
についてはまた別の機会に)
int
) :明示的な桁数の制限はない(実行時にメモリが確保できる限りの桁数)bool
) : True
と False
float
) : 倍精度浮動小数点数 (4倍精度浮動小数点などにはnumpyなどのライブラリを使う。)math.nan
, math.inf
が定義されている。numpy.nan
, numpy.inf
がある。complex
) 数データには通常の四則演算(+-*/
)の他にも、整数除算(//
),剰余(%
)などの演算も定義されている。
シークェンス型: immutable
tuple
)str
) : Pythonでは単独の文字というデータ型はありません。文字は1文字でも文字列型のデータとなります。 (文字列型 については次回取り上げます。)bytes
) :シークェンス型: mutable
list
)集合
- 集合型 (set
) : mutableなオブジェクトは要素になれない。 例: { 1,2,3 ,"a","b","c"}
- 不変な集合型 ( frozenset
): immutable & hashable
辞書型 ( dict
) :
key
とvalue
の組を要素にもつ。 例: d={'a':1, 'b':2, }
-key
による検索が可能。 例: d['a']
→ 1class
) :class
文によって生成される。数字が一列に並んだものがあると、グラフにして見なくなるのは、物理研究者の(? )習い性です。
matplotlib
ライブラリの中のpyplot
モジュールを使って、フィボナッチ数列のグラフを描いてみます。
import matplotlib.pyplot as pyplot
x=range(10)
pyplot.plot(x,[fib(n) for n in x])
pyplot.savefig("./_images/fibonacci.png")
pyplot.show()
「Python入門講座」でのご質問の中で、fib()
関数の実行測度についての回答にちょっと不正確なところがあったので、改めてご説明します。
まずここで定義したフィボナッチ関数の実行時間は、timeit
モジュールを使うとfib(40)
の計算に35秒かかっていることがわかります。(python3.9.7/macOS)
import timeit
timeit.timeit("(fib(40))", globals=globals(), number=1)
35.871306792
講座のお話の中では、Pythonホームページに掲載されているフィボナッチ数列のプログラムが早いというお話をしました。よくこのプログラムをみてみるとここで定義されている関数は、
引数に与えられた整数n
以下のフィボナッチ数列を印刷するプログラムでした。
# version in Python.org
def fib_python_homepage(n):
a, b = 0, 1
while b < n :
a, b = b, a+b
print(a, end=", ")
fib_python_homepage(1000)
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987,
このプログラムを変更して、同じアルゴリズムでn
番目のフィボナッチ数を求めるプログラムをfib_python()
としました。
def fib_python(n):
a, b = 0, 1
while n > 0 :
a, b = b, a+b
n-=1
return b
[fib_python(n) for n in range(16)]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
fib_python()
の実行時間は、fib_python(40)
で 5マイクロ秒、fib_python(1000)
で 100マイクロ秒となりました。
print(timeit.timeit("(fib_python(40))", globals=globals(), number=1))
print(timeit.timeit("(fib_python(1000))", globals=globals(), number=1))
4.625000002533852e-06 0.00010066600000158132
ご質問にあった「途中結果を保存しておいて、高速化する」バージョンとして、次のfibb()
を
定義してみました。
fibb(1000)
の実行結果は、1回目が 700マイクロ秒前後、2回目が 1マイクロ秒前後となっています。1回目はfib_python
に比べると遅いわけですが、同じ計算を繰り返し行う時は、効果が高くなっています。
def fibb(n, buf={}):
if n not in buf:
if n in (0,1):
buf[n]=1
else:
buf[n]=fibb(n-2,buf)+fibb(n-1,buf)
return buf[n]
num=1
print(timeit.timeit("(fibb(1000))", globals=globals(), number=num)/num)
print(timeit.timeit("(fibb(1000))", globals=globals(), number=num)/num,)
buf={}
print(timeit.timeit("(fibb(1000,buf))", globals=globals(), number=num)/num)
print(timeit.timeit("(fibb(1000,buf))", globals=globals(), number=num)/num)
0.0006977500000004966 7.089999982667905e-07 0.00043099999999896 4.999999987376214e-07
num=100
print(timeit.timeit("(fibb(1000,buf))", globals=globals(), number=num)/num)
1.5916000002391683e-07
フィボナッチ数列 $a_n$ は整数 $n$ について定義されています。
では、ここで定義した関数 fib(n)
のn
に整数でない数値をあたえると何が起きるでしょう? 一例としてfib(1.1)
を計算してみます。
try:
fib(1.1)
except Exception as e:
print("Error:",e)
Error: maximum recursion depth exceeded in comparison
整数でない入力値n
にたいしては停止条件n==0
あるいはn==1
が決して満たされないため、システムの条件までプログラムはある意味暴走してしまいます。
このように、関数に与えられた引数が適切なものであるかを判断し、条件が満たされない場合には、そのことを関数の呼び出しもとにつたえるための仕組みとして、例外(Exception)の仕組みがpython
には用意されています。
入力値のチェックを組み込んだfib
関数をご覧ください。
def fib(n:int)->int:
"""
return a value of fibonacci series for integer n.
>>> fib(10)
89
>>> fib(-1)
Traceback (most recent call last):
...
ValueError: n must be >= 0
>>> fib(1.0)
Traceback (most recent call last):
...
TypeError: n must be an exact integer
"""
if type(n) is not int:
raise TypeError("n must be an exact integer")
if not n >= 0:
raise ValueError("n must be >= 0")
if n == 0 or n == 1:
value = 1
else:
value= fib(n-1) + fib(n-2)
return value
この関数で実際にn=1.1
のfib
関数を計算して見ます。
try:
fib(1.1)
except Exception as e:
print("Error:",e)
Error: n must be an exact integer
このように、整数以外の数値が引数として与えられたことがエラーの原因であることがすぐにわかります。
この定義ではdocstringも入れてあるので、help
メッセージも表示されます。
help(fib)
Help on function fib in module __main__: fib(n: int) -> int return a value of fibonacci series for integer n. >>> fib(10) 89 >>> fib(-1) Traceback (most recent call last): ... ValueError: n must be >= 0 >>> fib(1.0) Traceback (most recent call last): ... TypeError: n must be an exact integer
さらに、docstring
にはこの関数で予期される動作の例も書き込まれているので、
doctest
をつかって、この定義が設計を満たしているかどうかを確認することが
できます。
import doctest
doctest.testmod(verbose=True)
Trying: fib(10) Expecting: 89 ok Trying: fib(-1) Expecting: Traceback (most recent call last): ... ValueError: n must be >= 0 ok Trying: fib(1.0) Expecting: Traceback (most recent call last): ... TypeError: n must be an exact integer ok 4 items had no tests: __main__ __main__.fib_python __main__.fib_python_homepage __main__.fibb 1 items passed all tests: 3 tests in __main__.fib 3 tests in 5 items. 3 passed and 0 failed. Test passed.
TestResults(failed=0, attempted=3)
doctest.testmod(verbose=False)
TestResults(failed=0, attempted=3)
n=8
try:
r=fib(n)
except TypeError as err:
print("TypeError:", err)
except ValueError as err:
print("ValueError:", err)
else:
print(r)
34
import timeit
timeit.timeit("fib(1)",setup="from __main__ import fib", number=100)/100
2.1374999995771304e-07
import timeit
timeit.timeit("(fib_python(1000))", globals=globals(), number=1)
0.00010179099999163554
(timeit.timeit("(fib_b(1000))", globals=globals(), number=1),
timeit.timeit("(fib_b(1000))", globals=globals(), number=1))
(0.0004622919999945907, 0.00043295800000464624)
def fibb(n, /, buf={}):
if n not in buf:
if n in (0,1):
buf[n]=1
else:
buf[n]=fibb(n-2,buf)+fibb(n-1,buf)
return buf[n]
(timeit.timeit("(fibb(1000))", globals=globals(), number=1),
timeit.timeit("(fibb(1000))", globals=globals(), number=1),
timeit.timeit("(fibb(1000,{}))", globals=globals(), number=1))
(0.00043241600000953895, 5.410000056826902e-07, 0.0004169589999918344)
(timeit.timeit("(fib_c(1000))", globals=globals(), number=1),
timeit.timeit("(fib_c(1000))", globals=globals(), number=1))
(0.0004345420000078093, 0.00039570799999921746)
import sys
sys.version_info
sys.version_info(major=3, minor=9, micro=7, releaselevel='final', serial=0)