
目次
サンプル・プログラム
リスト
Python には、複数のデータをひとまとめのデータとして扱うコンテナデータ型と呼ぶデータ型が複数備わっている。
データ型 | ミュータブル | イテラブル | シーケンス | |
---|---|---|---|---|
リスト | list | ○ | ○ | ○ |
配列 | array | ○ | ○ | ○ |
タプル | tuple | × | ○ | ○ |
集合型 | set | ○ | ○ | × |
辞書型 | dict | ○ | ○ | × |
まず リスト(list)を見ていこう。

リストは、1つ1つのデータ(要素)をカンマ , で区切り、全体をブラケット [...] で囲む。たとえば [1, 2, 3] は 1, 2, 3の3つの要素からなるリストである。
要素には、左から0番、1番、2番‥‥というインデックスが順序(シーケンス)として付加される。
要素のデータ型は問わない。異なるデータ型が混在していても構わない。
リストは変数に代入することができる。
要素には、左から0番、1番、2番‥‥というインデックスが順序(シーケンス)として付加される。
要素のデータ型は問わない。異なるデータ型が混在していても構わない。
リストは変数に代入することができる。
list1.py
# リスト‥‥整数型のみ
list1 = [0, 1, 2, 3, 4]
print(list1)
プログラム "list1.py" を実行してみてほしい。
変数 list1 は整数型の要素のみを格納したリストである。print関数を使うと、リストの形のまま画面に表示する。
変数 list1 は整数型の要素のみを格納したリストである。print関数を使うと、リストの形のまま画面に表示する。
list1.py
# リスト‥‥複数のデータ型が混在
list2 = [1, "abc", 3.13, "あいうえお", list1]
print(list2)
変数 list2 はデータ型が混在したリストである。リストの中にリストを入れ子にしても、問題なく格納されていることがわかる。
list1.py
# リストの3番目
print(list1[3])
リストには0番からの順序(インデックス)が付加される。リスト list1 の3番目を表示する。
list1.py
# リストの3番目
index = 3
print(list2[index])
インデックスとして変数を指定することもできる。
list1.py
# 入れ子になったリスト
print(list2[4][2])
入れ子になったリストの要素をターゲットにしたいときは、list2[][] と2つのブラケットでインデックスを指定してやる。
in演算子
list2.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# in演算子
a = 3
print(a in list1)
プログラム "list2.py" を実行してみてほしい。
in演算子 は a in l のように用い、リスト l の要素として a があれば True を、なければ False を返す。リストの中に探すデータがあるかどうかを調べるときに役立つだろう。
in演算子 は a in l のように用い、リスト l の要素として a があれば True を、なければ False を返す。リストの中に探すデータがあるかどうかを調べるときに役立つだろう。
list2.py
# in演算子‥‥for文と組み合わせ
for i in list1:
print(i)
indexメソッド
list3.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# indexメソッド
a = 3
i = list1.index(a)
print(i)
プログラム "list3.py" を実行してみてほしい。
in演算子 はリストの中にデータがあるかどうかを調べるだけだったが、indexメソッドを使うと、何番目のインデックスにあるかを求めることができる。
indexメソッド は リスト.index(値) のようにして使う。リスト(変数名)の後にドット . を挟んで記述する index をメソッドと呼ぶ。リストには index 以外にも幾つかの有効なメソッドがあり、リストが作られると同時に、リストに複数のメソッドが付いてくると覚えておいてほしい。
in演算子 はリストの中にデータがあるかどうかを調べるだけだったが、indexメソッドを使うと、何番目のインデックスにあるかを求めることができる。
indexメソッド は リスト.index(値) のようにして使う。リスト(変数名)の後にドット . を挟んで記述する index をメソッドと呼ぶ。リストには index 以外にも幾つかの有効なメソッドがあり、リストが作られると同時に、リストに複数のメソッドが付いてくると覚えておいてほしい。

list3.py
b = 5
i = list1.index(b)
print(i)
indexメソッドは、目的のデータが見つからないと ValueError例外を発生する。プログラムを作るときには、例外処理が必要になる。
len関数
list4.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# len関数
l = len(list1)
print(l)
リストに要素を追加する
append1.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# appendメソッド‥‥要素を1つ追加
list1.append(6)
print(list1)
プログラム "append1.py" を実行してみてほしい。
appendメソッド を使うと、リストの末尾に要素を1つ追加することができる。
appendメソッド を使うと、リストの末尾に要素を1つ追加することができる。

append1.py
# appendメソッド‥‥要素を1つ追加
a = 9
list1.append(a)
print(list1)
appendメソッドの引数に変数を指定することもできる。
append1.py
# appendメソッド‥‥リストを追加
list2 = [10, 11, 12]
list1.append(list2)
print(list1)
appendメソッドでリストを追加すると、入れ子になって格納する。
append1.py
list2 = [10, 11, 12]
list1.append(list2)
print(list1)
# extendメソッド‥‥リストを追加
list3 = [20, 21, 22]
list1.extend(list3)
print(list1)
リストを入れ子にせず追加したければ、extendメソッドを使おう。
insert1.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# insertメソッド‥‥要素を途中に追加
list1.insert(2, "x")
print(list1)
次にプログラム "insert1.py" を実行してみてほしい。
insertメソッド は リスト.insert(挿入するインデックス, 挿入する値) のようにして使い、リストの途中に要素を1つ挿入することができる。
insertメソッド は リスト.insert(挿入するインデックス, 挿入する値) のようにして使い、リストの途中に要素を1つ挿入することができる。

insert1.py
# insertメソッド‥‥先頭にリストを追加
list2 = [11, 12, 13]
list1.insert(0, list2)
print(list1)
insertメソッドのインデックスに 0 を指定すると、先頭に挿入する。値としてリストを指定すると、入れ子になって挿入する。
要素を挿入するとインデックスが振り直され、順序(シーケンス)が保たれる。
要素を挿入するとインデックスが振り直され、順序(シーケンス)が保たれる。
リストを複製する
copy1.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# リストの複製?
list2 = list1
list2.append(6)
print(list2)
print(list1)
プログラム "copy1.py" を実行してみてほしい。
リスト list1を別の変数 list2 に代入する。このとき、リスト list1 の内容が list2 に代入されるわけではなく、同じリストを指し示す。
したがって、appendメソッド を使って list2 に要素を追加すると、list1 からも追加した要素を見ることができる。
リスト list1を別の変数 list2 に代入する。このとき、リスト list1 の内容が list2 に代入されるわけではなく、同じリストを指し示す。
したがって、appendメソッド を使って list2 に要素を追加すると、list1 からも追加した要素を見ることができる。
copy2.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# リストの複製‥‥copyメソッド
list2 = list1.copy()
list2.append(6)
print(list2)
print(list1)
リストを複製したければ、プログラム "copy2.py" のように copyメソッドを使う。
copy3.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# リストのスライス
list2 = list1[2:4]
print(list2)
次に、プログラム "copy3.py" を実行してみてほしい。
list1[2:4] のように指定すると、リスト list1 のインデックス2~3番目の要素を取り出す。取り出した値はリストになる。これをスライスと呼ぶ。
list1[2:4] のように指定すると、リスト list1 のインデックス2~3番目の要素を取り出す。取り出した値はリストになる。これをスライスと呼ぶ。
copy3.py
# 要素の追加
list2.append(6)
print(list2)
print(list1)
スライスして得られたリストは、最初のリストとは別のリストになり、appendメソッドを使っても元のリストに影響しないことが分かる。
要素を削除する
pop1.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# 指定した要素の削除
index = 2
list1.pop(index)
print(list1)
プログラム "pop1.py" を実行してみてほしい。
リスト list1 から要素を1つ削除したいときは popメソッドを使う。リスト.pop[削除したい要素のインデックス] のようにして使う。
リスト list1 から要素を1つ削除したいときは popメソッドを使う。リスト.pop[削除したい要素のインデックス] のようにして使う。

pop1.py
# 末尾の要素を削除
list1.pop()
print(list1)
popメソッドに引数を指定しないと、末尾の要素を削除する。
pop1.py
# 後ろから指定した要素を削除
index = -2
list1.pop(index)
print(list1)
引数(インデックス)に負数を指定すると、末尾から数えて要素を1つ削除する。
要素が削除されるとインデックスが振り直され、順序(シーケンス)が保たれる。
要素が削除されるとインデックスが振り直され、順序(シーケンス)が保たれる。
del1.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# 指定した要素の削除
index = 2
del list1[index]
print(list1)
del1.py
# 指定する範囲の要素を削除
idx0 = 2
idx1 = 4
del list1[slice(idx0, idx1)]
print(list1)
スライスを使って指定した範囲の複数の要素を一気に削除することもできる。slice関数は変数を使ってスライス・オブジェクトを生成する組み込み関数だ。
リストを使った計算
sumNaturalNumbers3.py
def sumNaturalNumbers3(a, b):
"""aからbまでの自然数の和を求めるユーザー関数(リスト版)
Args:
a(int): 開始値
b(int): 終了値
Returns:
int 合計値
"""
list1 = list(range(a, b + 1)) # リスト化
return sum(list1)
プログラム "sumNaturalNumbers3.py" は、aからbまでの整数の和を求める関数のリスト版だ。range関数 は rangeオブジェクトを返すが、これを list関数に通してリストに変換する。こうして、aからbまでの整数を要素とするリスト list1 を生成する。
sum関数 はリストの内容を合計する組み込み関数だ。
sum関数 はリストの内容を合計する組み込み関数だ。
minmax.py
# リスト‥‥整数型のみ
list1 = [0, 1, 2, 3, 4]
print(list1)
# 最小値
print(min(list1))
# 最大値
print(max(list1))
また、min関数、max関数を使うことで、リストの最小値や最大値を求めることができる。
リストのシャフルとソート
shuffle1.py
import random
# スート S:spade, H:heart, D:daiamond, C:clubの略記とする
suits = [ "S", "H", "D", "C" ]
# 山札(list)
stocks = []
for s in suits:
for i in range(1, 14):
stocks.append(f"{s}{i:02}")
# カードの最初の5枚を表示
print(stocks[:5])
# カードをシャフルして最初の5枚を表示
random.shuffle(stocks)
print(stocks[:5]) # 再び山札の最初の5枚を表示
プログラム "shuffle1.py" は、リストをトランプに見立てて、山札から手札に5枚のカードを加える操作をプログラミングしたものだ。
まず、スートに対応するリスト suits を用意し、for文 を使って52枚のカード(52個の要素)から成るリスト stocks を生成する。これを山札に見立てる。
山札の最初の5枚を表示する。

続いて、randomモジュールの shuffle関数を使って山札 stocks をシャフルする。
再び山札の最初の5枚を表示してみると、シャフルされたことが分かる。
まず、スートに対応するリスト suits を用意し、for文 を使って52枚のカード(52個の要素)から成るリスト stocks を生成する。これを山札に見立てる。
山札の最初の5枚を表示する。

続いて、randomモジュールの shuffle関数を使って山札 stocks をシャフルする。
再び山札の最初の5枚を表示してみると、シャフルされたことが分かる。
shuffle1.py
# 山札から5枚取り出して手札handsに加える
n = slice(None, 5)
hands = stocks[n]
print(hands)
# 山札から5枚を削除する
del stocks[n]
次に、slice関数を使い、山札 stocks から5枚取り出して、手札 hands に加える。取り出した5枚は、del文を使って山札 stocks から削除する。
先ほど山札の最初の5枚が、そのまま手札に移動したことが分かるだろう。
先ほど山札の最初の5枚が、そのまま手札に移動したことが分かるだろう。
shuffle1.py
# 手札を3枚捨てて山札から3枚取って加える
n = slice(None, 3)
del hands[n]
hands.extend(stocks[n])
del stocks[n]
print(hands)
同様に del文を使って手札 hands から3枚捨てて、山札 stocks から3枚を取りだし、extendsメソッドを使って手札 hands に加える。
shuffle1.py
# 手札をソートする
hands.sort()
print(hands)
最後に sortメソッドを使って hands を小さい順に並び替える。並び替えにあたってはスートの強さを比較しているわけではなく、文字列は辞書順に並べ替えているだけだが、数字部分は小さい順に並び替えていることが分かる。
リストと配列
他のプログラミング言語を触ったことがある方は、リストは配列のことではないかと思うだろう。むしろ、他の言語より便利なメソッドや関数が揃っていることから、使い勝手のいい配列に見えるかもしれない。
だが、Python の標準ライブラリには配列を扱う arrayモジュールが別に用意されている。したがって、リストは配列ではない。
だが、Python の標準ライブラリには配列を扱う arrayモジュールが別に用意されている。したがって、リストは配列ではない。
array1.py
from array import array
# 配列を生成する
array1 = array("b", range(1, 10))
print(array1)
# 要素を加える
array1.append(10)
print(array1)
# 合計を計算する
print(sum(array1))
プログラム "array1.py" を実行してみてほしい。
Python の配列は、array(配列の型, 値) を使って生成する。値には、単独のデータ型やリスト、rangeオブジェクトなどを入れることができる。
配列の型は Python独特の概念で、要素として代入できるデータ型(メモリ上のサイズ)を限定する。これはC言語の配列に似ている。
Python の配列は、array(配列の型, 値) を使って生成する。値には、単独のデータ型やリスト、rangeオブジェクトなどを入れることができる。
配列の型は Python独特の概念で、要素として代入できるデータ型(メモリ上のサイズ)を限定する。これはC言語の配列に似ている。
配列の型 | Cのデータ型 | Pythonのデータ型 |
---|---|---|
b | signed char | int |
B | unsigned char | int |
h | signed short | int |
H | unsigned short | int |
i | signed int | int |
I | unsigned int | int |
l | signed long | int |
L | unsigned long | int |
q | signed long long | int |
Q | unsigned long long | int |
f | float | float |
d | double | float |

array1.py
# 要素を加える
array1.append(10)
print(array1)
# 合計を計算する
print(sum(array1))
配列にも、リストのようなメソッドがあり、sum関数を適用することができる。

配列がリストと決定的に異なるのは、配列の型を指定しているように、要素に入れることができるデータ型が決まっているということだ。リストのように、データ型に依存しない柔軟な運用はできない。その代わり、配列に対する処理はリストより速い。逆に言うと、リストは、コンピュータにとって複雑なデータ構造であるため、処理に時間がかかる。算術計算などで高速性を要求されるようなときは配列を使った方がいい。

配列がリストと決定的に異なるのは、配列の型を指定しているように、要素に入れることができるデータ型が決まっているということだ。リストのように、データ型に依存しない柔軟な運用はできない。その代わり、配列に対する処理はリストより速い。逆に言うと、リストは、コンピュータにとって複雑なデータ構造であるため、処理に時間がかかる。算術計算などで高速性を要求されるようなときは配列を使った方がいい。
max1.py
from array import array
import random
import time
# 生成する最大値
n = 1000000
# 繰返し回数
cnt = 10
# リストの最大値を求める
time1 = 0
for i in range(1, cnt + 1):
list1 = list(range(1, n + 1))
random.shuffle(list1)
start = time.perf_counter() # 実行時間計測開始
max(list1)
end = time.perf_counter() # 実行時間計測終了
time1 += end - start
list1.clear() # リストを削除
print(f"リスト = {(time1 / cnt):.5f}秒") # 実行時間の平均を表示
# 配列の最大値を求める
time2 = 0
for i in range(1, cnt + 1):
array1 = array("L", range(1, n + 1))
random.shuffle(array1)
start = time.perf_counter() # 実行時間計測開始
max(array1)
end = time.perf_counter() # 実行時間計測終了
time2 += end - start
array1 = array("L", []) # 配列を削除
print(f"配列 = {(time2 / cnt):.5f}秒") # 実行時間の平均を表示
リストより配列の方が処理が早いという実験を行うプログラムが "max1.py" である。1から100万までのリストおよび配列をシャフルし、最大値を求める作業を10回繰り返し、平均実行時間を表示する。
練習問題
次回予告
次回は、コンテナデータ型のタプル、集合型、辞書型の使い方を学ぶ。
最後に、コンテナデータ型を活用してクラスの成績表を作り、個人合計点や科目毎平均点を計算するプログラムを作る。
最後に、コンテナデータ型を活用してクラスの成績表を作り、個人合計点や科目毎平均点を計算するプログラムを作る。
コラム:データ構造
プログラミングはアルゴリズムとデータ構造と言われる。制御文や関数、メソッドを使って処理の流れを組み立てるのがアルゴリズムで、データを入れる器がデータ構造だ。
そして、Python のリストは、リストと呼ばれるデータ構造そのものである。
そして、Python のリストは、リストと呼ばれるデータ構造そのものである。

一般にリストとは、上図のように先頭(root)と末尾(tail)があり、その間に順番にノード(node)があり、ノードに様々なデータがぶら下がる。つまり、配列(array)とは異なるデータ構造をしている。

代表的なデータ構造としては、リスト以外にもキュー,スタック、ツリー、連想配列、ヒープなどがある。Python では、今回と次回を通して学ぶコレクションデータ型を活用することで、これらのデータ構造に標準で対応することができる。
Python が多くの分野で利用されているのは、データ構造の記述のしやすさが備わっているからだろう。ただし、本文でも触れたように、そのことが Python の実行速度の足を引っ張っている。

これを読んでデータ構造について興味をお持ちになったら、「IT技術 - データ構造の話」をご覧いただきたい。

代表的なデータ構造としては、リスト以外にもキュー,スタック、ツリー、連想配列、ヒープなどがある。Python では、今回と次回を通して学ぶコレクションデータ型を活用することで、これらのデータ構造に標準で対応することができる。
Python が多くの分野で利用されているのは、データ構造の記述のしやすさが備わっているからだろう。ただし、本文でも触れたように、そのことが Python の実行速度の足を引っ張っている。

これを読んでデータ構造について興味をお持ちになったら、「IT技術 - データ構造の話」をご覧いただきたい。
コラム:乱数の一様性

本編で random モジュールを用いたが、ゲームプログラミングでは乱数は欠かせない。乱数は、指定した範囲で一様に出現してくれないとゲーム・バランスが崩れる。たとえばサイコロであれば、それぞれの目が \( \displaystyle \frac{1}{6} \) のk確率で――すなわち、1,000回振って、それぞれの目が \( 1000 \div 6 = 166.666 \dots \) 回だけ出るのが一様乱数だ。
ところが、コンピュータで一様乱数を出すのは難しい。なぜならば、コンピュータは数学的なアルゴリズムに基づき、与えられた入力が同じなら必ず同じ出力をするという機能を備えているからだ。その意味では、コンピュータは決定論的な機械とも言える。
一様乱数は、自然現象や量子効果といった予測不可能なプロセスの中に見ることができるのだが、人類はその再現に成功していない。

プログラミング言語にある乱数関数/メソッドは、限りなく一様乱数に近い乱数を擬似的に発生させるもので、本当の意味での一様乱数にはならないことに注意が必要だ。
たとえば、Pythonには複数の乱数が用意されているが、1から6の乱数を10万回発生させ、それらが一様乱数からどの程度偏っているかを百分率で示すプログラムが "randomNumberBias.py" である。実行してみてほしい。
このプログラムでは4つの乱数の偏りを計算する。なお、計算結果は処理系によって異なるので、このプログラムだけで一様性の優劣を判定することはできない。

random.randint は、randomモジュールにある整数の乱数を発生する関数だ。乱数生成器としてメルセンヌ・ツイスタを利用している。メルセンヌ・ツイスタについては、「補足:乱数 - 補足:乱数」で解説しているので、あわせてご覧いただきたい。

numpy.random.default_rng は、NumPyモジュールにある乱数で、こちらもメルセンヌ・ツイスタを利用している。randomモジュールに比べて選択できる確率分布の数が非常に多いので、より偏りが少なくなっている(はずである)。

secrets.randbelow は、機密を扱うために安全な乱数を生成できる secretsモジュールの乱数だ。OSが提供する最も高品質なソースを用いて乱数を生成することができる。

os.urandom は、暗号用途に適したRandomな文字列を返す。Windowsでは CryptGenRandom() を使い、Linuxでは/dev/urandom デバイスから読み込む。
一様乱数は、自然現象や量子効果といった予測不可能なプロセスの中に見ることができるのだが、人類はその再現に成功していない。

プログラミング言語にある乱数関数/メソッドは、限りなく一様乱数に近い乱数を擬似的に発生させるもので、本当の意味での一様乱数にはならないことに注意が必要だ。
たとえば、Pythonには複数の乱数が用意されているが、1から6の乱数を10万回発生させ、それらが一様乱数からどの程度偏っているかを百分率で示すプログラムが "randomNumberBias.py" である。実行してみてほしい。
このプログラムでは4つの乱数の偏りを計算する。なお、計算結果は処理系によって異なるので、このプログラムだけで一様性の優劣を判定することはできない。

random.randint は、randomモジュールにある整数の乱数を発生する関数だ。乱数生成器としてメルセンヌ・ツイスタを利用している。メルセンヌ・ツイスタについては、「補足:乱数 - 補足:乱数」で解説しているので、あわせてご覧いただきたい。

numpy.random.default_rng は、NumPyモジュールにある乱数で、こちらもメルセンヌ・ツイスタを利用している。randomモジュールに比べて選択できる確率分布の数が非常に多いので、より偏りが少なくなっている(はずである)。

secrets.randbelow は、機密を扱うために安全な乱数を生成できる secretsモジュールの乱数だ。OSが提供する最も高品質なソースを用いて乱数を生成することができる。

os.urandom は、暗号用途に適したRandomな文字列を返す。Windowsでは CryptGenRandom() を使い、Linuxでは/dev/urandom デバイスから読み込む。
コラム:完全数と計算量
自分自身が自分自身を除く正の約数の和に等しくなる自然数のことを完全数(perfect number)と呼ぶ。
たとえば 6 の約数は 1, 2, 3, 6 だが、自分自身を除いた和 1 + 2 + 3 は 6 に等しくなるので、6 は完全数である。
古代から 6, 28, 496, 8128 の4つが完全数であることが知られているが、ピタゴラスが名付けたとか、世界を創造した6日間と月の公転周期の28日が含まれているからキリスト教の神を完全性を表しているなどと言われてきた。
完全数を求める方程式は発見されていない。自然数を1つずつ虱潰しにしていかなければならないわけだが、こういうときに黙々と作業をするコンピュータが重宝する。
たとえば 6 の約数は 1, 2, 3, 6 だが、自分自身を除いた和 1 + 2 + 3 は 6 に等しくなるので、6 は完全数である。
古代から 6, 28, 496, 8128 の4つが完全数であることが知られているが、ピタゴラスが名付けたとか、世界を創造した6日間と月の公転周期の28日が含まれているからキリスト教の神を完全性を表しているなどと言われてきた。
完全数を求める方程式は発見されていない。自然数を1つずつ虱潰しにしていかなければならないわけだが、こういうときに黙々と作業をするコンピュータが重宝する。
perfectNumbers1.py
import time
# 探索範囲の最小値
RANGE_MIN = 2;
# 探索範囲の最大値
RANGE_MAX = 100000
def isPerfectNumber1(num):
"""完全数かどうかを判定する.
Args:
num(int): 判定する値
Returns:
bool: true:完全数である / false:完全数ではない
"""
sum = 0;
# 自分自身以外の約数の和を求める
for i in range(1, num):
if (num % i == 0):
sum += i
# 和が元の数と等しければ完全数
return sum == num;
def findPerfectNumbers1(min, max):
"""完全数を求める.
Args:
min(int): 探索範囲の最小値
max(int): 探索範囲の最大値
Returns:
list: 完全数のリスト
"""
perfectNumbers = [] # 完全数を格納するリスト
for i in range(min, max + 1):
if (isPerfectNumber1(i)):
perfectNumbers.append(i);
return perfectNumbers;
# メイン・プログラム =======================================================
# 計算開始時刻
startTime = time.monotonic()
# 完全数を求める
perfectNumbers = findPerfectNumbers1(RANGE_MIN, RANGE_MAX)
# 計算終了時刻
finishTime = time.monotonic()
# 完全数を画面に表示する.
for val in perfectNumbers:
print(f"{val:,}")
# 計算時間
print("計算時間 : " + format((finishTime - startTime), f".3f") + "秒")
"perfectNumbers1.py" は、上述の定義をそのままプログラムにした。冒頭の RANGE_MAX で指定した10万までの探索をやらせると、手元のパソコンで264秒かかった(Intel Core i5-1235U 1.30GHz)。
しかも悲しいことに、冒頭の4つの完全数しか見つからない。5番目の完全数は 33550336 であり、これに到達するまでには相当な時間がかかるだろう。2021年(令和3年)8月現在発見されている完全数は51個で、51個目は2486万2048桁もある。
しかも悲しいことに、冒頭の4つの完全数しか見つからない。5番目の完全数は 33550336 であり、これに到達するまでには相当な時間がかかるだろう。2021年(令和3年)8月現在発見されている完全数は51個で、51個目は2486万2048桁もある。
次に紹介する "perfectNumbers2.py" は、外部ライブラリ NumPy を使って高速化したプログラムである。NumPy を使うには、pipコマンドを使って
pip install numpyとしてインストールしてほしい。
perfectNumbers2.py
import time
import numpy as np
# 探索範囲の最小値
RANGE_MIN = 2;
# 探索範囲の最大値
RANGE_MAX = 100000
def isPerfectNumber2(num):
"""完全数かどうかを判定する.
Args:
num(int): 判定する値
Returns:
bool: true:完全数である / false:完全数ではない
"""
if num < 2:
return False
# 自分自身以外の約数の和を求める
divisors = np.arange(1, int(np.sqrt(num)) + 1)
sumDiv = np.sum(divisors[num % divisors == 0])
# 約数のペアを考慮する
sumDiv += np.sum(num // divisors[(num % divisors == 0) & (divisors != 1)])
return sumDiv == num
def findPerfectNumbers2(min, max):
"""完全数を求める.
Args:
min(int): 探索範囲の最小値
max(int): 探索範囲の最大値
Returns:
list: 完全数のリスト
"""
perfectNumbers = [] # 完全数を格納するリスト
for i in range(min, max + 1):
if (isPerfectNumber2(i)):
perfectNumbers.append(i);
return perfectNumbers;
# メイン・プログラム =======================================================
# 計算開始時刻
startTime = time.monotonic()
# 完全数を求める
perfectNumbers = findPerfectNumbers2(RANGE_MIN, RANGE_MAX)
# 計算終了時刻
finishTime = time.monotonic()
# 完全数を画面に表示する.
for val in perfectNumbers:
print(f"{val:,}")
# 計算時間
print("計算時間 : " + format((finishTime - startTime), f".3f") + "秒")
NumPy を使うと Python の計算処理が速くなるような話を耳にするが、厳密にはそうではない。NumPy は、ベクトルや行列、テンソル式を、リストに代わって高速に計算する仕組みを提供するライブラリである。
そこで、総当たりで完全数を求めた "perfectNumbers1.py" の中で繰り返し呼び出される完全数かどうかを判定する関数に numpy.arange を導入する。これは、Pythonの関数range を高速化する仕組みだ。
先ほどと同じパソコンを使って 1.25秒と、劇的に速くなった。
そこで、総当たりで完全数を求めた "perfectNumbers1.py" の中で繰り返し呼び出される完全数かどうかを判定する関数に numpy.arange を導入する。これは、Pythonの関数range を高速化する仕組みだ。
先ほどと同じパソコンを使って 1.25秒と、劇的に速くなった。
さて、偶数の完全数に限っては、18世紀の数学者レオンハルト・オイラーがユークリッド・オイラーの定理を発見しており、これを使うと計算回数を劇的に少なくすることができる。
ユークリッド・オイラーの定理によると、
\[ P = 2^{p - 1} \times (2^p - 1) \]
\( p \)が素数であり、なおかつ \( 2^p - 1 \) も素数である場合(これをメルセンヌ素数と呼ぶ)、\( P \) は偶数の完全数になる。
これを Pythonプログラムにしたものが "perfectNumbers3.py" である。
ユークリッド・オイラーの定理によると、
\[ P = 2^{p - 1} \times (2^p - 1) \]
\( p \)が素数であり、なおかつ \( 2^p - 1 \) も素数である場合(これをメルセンヌ素数と呼ぶ)、\( P \) は偶数の完全数になる。
これを Pythonプログラムにしたものが "perfectNumbers3.py" である。
perfectNumbers2.py
import time
import numpy as np
# 探索範囲の最小値
RANGE_MIN = 2;
# 探索範囲の最大値
RANGE_MAX = 100000
def isPerfectNumber2(num):
"""完全数かどうかを判定する.
Args:
num(int): 判定する値
Returns:
bool: true:完全数である / false:完全数ではない
"""
if num < 2:
return False
# 自分自身以外の約数の和を求める
divisors = np.arange(1, int(np.sqrt(num)) + 1)
sumDiv = np.sum(divisors[num % divisors == 0])
# 約数のペアを考慮する
sumDiv += np.sum(num // divisors[(num % divisors == 0) & (divisors != 1)])
return sumDiv == num
def findPerfectNumbers2(min, max):
"""完全数を求める.
Args:
min(int): 探索範囲の最小値
max(int): 探索範囲の最大値
Returns:
list: 完全数のリスト
"""
perfectNumbers = [] # 完全数を格納するリスト
for i in range(min, max + 1):
if (isPerfectNumber2(i)):
perfectNumbers.append(i);
return perfectNumbers;
# メイン・プログラム =======================================================
# 計算開始時刻
startTime = time.monotonic()
# 完全数を求める
perfectNumbers = findPerfectNumbers2(RANGE_MIN, RANGE_MAX)
# 計算終了時刻
finishTime = time.monotonic()
# 完全数を画面に表示する.
for val in perfectNumbers:
print(f"{val:,}")
# 計算時間
print("計算時間 : " + format((finishTime - startTime), f".3f") + "秒")
実行時間は千分の1秒未満という驚異的な速度である。それもそのはず、繰り返し計算をほとんど行わないからだ。また、最初の4個の完全数は偶数であるため、たまたま "perfectNumbers1" と同じ結果が得られる。
ただし、偶数にしても奇数にしても、完全数が無限に存在するのかどうかは未解決の問題である。
ただし、偶数にしても奇数にしても、完全数が無限に存在するのかどうかは未解決の問題である。
このような計算量の多い問題では、前提条件を置くことで計算量を劇的に減らせることができる場合がある。結果を早く求めることができればユーザー喜ぶから、システムの非機能要件として計算量を減らすことを検討するといい。
(この項おわり)
今回は、リストと配列の使い方を学ぶ。