AI生成コンテンツ / AI-generated content
目次
サンプル・プログラム
サンプルはPython 3.10以降を想定する。perfectNumbers2.pyとrandomNumberBias.pyにはNumPyも必要である。各ファイルを python ファイル名.py で実行しよう。list3.pyの最後は、見つからない値を検索したときのエラーを確かめるための例である。
リスト
Python には、複数のデータをひとまとめのデータとして扱うコンテナデータ型と呼ぶデータ型が複数備わっている。
| データ型 | ミュータブル | イテラブル | シーケンス | |
|---|---|---|---|---|
| リスト | list | ○ | ○ | ○ |
| 配列 | array | ○ | ○ | ○ |
| タプル | tuple | × | ○ | ○ |
| 集合型 | set | ○ | ○ | × |
| 辞書型 | dict | ○ | ○ | × |
表の用語
ミュータブルは作成後に内容を変更できること、イテラブルはfor文などで要素を順に取り出せることを表す。シーケンスは順序を持ち、整数の位置で要素を取り出せる型である。辞書は挿入順を保つが、位置ではなくキーで値を指定するためシーケンスではない。arrayは標準ライブラリの型で、それ以外の表中の型は組み込みである。

まず リスト(list)を見ていこう。
まず リスト(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
# リストの4番目(インデックス3)
print(list1[3])
インデックスは0から始まる。この例の list1[3] は、先頭から4番目、つまりインデックス3の要素を表示する。
list1.py
# リストの4番目(インデックス3)
index = 3
print(list2[index])
インデックスとして変数を指定することもできる。末尾の要素は list1[-1] で取り出せる。要素数が5なら、0~4または-5~-1が有効で、範囲外の位置を指定すると IndexError になる。
list1.py
# 入れ子になったリスト
print(list2[4][2])
入れ子の要素は list2[4][2] のように指定する。外側のインデックス4のリストから、内側のインデックス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
# for文のin‥‥要素を順に取り出す
for i in list1:
print(i)
for i in list1 の in は、for文の構文の一部である。要素があるかを調べる演算子とは役割が異なり、リストの要素を順に変数 i に代入して処理を繰り返す。「4.3 for文」も参照してほしい。
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 を送出する。このサンプルの最後は、エラーを確認するために意図的に停止する例である。実用のプログラムでは try/except で対応したり、in で存在を確かめてから呼び出したりする。
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
# extendメソッド‥‥リストを追加
list3 = [20, 21, 22]
list1.extend(list3)
print(list1)
リストを入れ子にせず追加するには、extendメソッドを使う。append、extend、insertはいずれも元のリストを変更し、戻り値は None である。
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 を指定すると、先頭に挿入する。値としてリストを指定すると、入れ子になって挿入する。
挿入位置以降の要素のインデックスは1つ増えるが、既存の要素どうしの前後関係は変わらない。
挿入位置以降の要素のインデックスは1つ増えるが、既存の要素どうしの前後関係は変わらない。
リストを複製する
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メソッドを使う。ただし、これは外側だけを複製する浅いコピーである。入れ子のリストは共有されるため、内側の要素を変更すると元にも反映される。入れ子も独立させたい場合は、標準ライブラリの copy.deepcopy を検討する。
copy3.py
# リスト
list1 = [0, 1, 2, 3, 4]
print(list1)
# リストのスライス
list2 = list1[2:4]
print(list2)
次に、プログラム "copy3.py" を実行してみてほしい。
list1[2:4] のように指定すると、リスト list1 のインデックス2と3、つまり先頭から3~4番目の要素を、新しいリストとして取り出す。これをスライスと呼ぶ。開始位置を含み、終了位置4を含まない点に注意しよう。
list1[2:4] のように指定すると、リスト list1 のインデックス2と3、つまり先頭から3~4番目の要素を、新しいリストとして取り出す。これをスライスと呼ぶ。開始位置を含み、終了位置4を含まない点に注意しよう。
copy3.py
# 要素の追加
list2.append(6)
print(list2)
print(list1)
スライスでできた外側のリストは元と別なので、そこに append しても元のリストの長さは変わらない。ただし、スライスも浅いコピーであり、入れ子の要素は共有される。list1[:] なら全体を浅くコピーできる。
要素を削除する
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メソッドは削除した要素を返す。引数を省略すると末尾を取り出す。空のリストや範囲外のインデックスでは IndexError になる。
pop1.py
# 後ろから指定した要素を削除
index = -2
list1.pop(index)
print(list1)
引数(インデックス)に負数を指定すると、末尾から数えて要素を1つ削除する。
削除した位置より後ろの要素のインデックスは1つ減るが、残った要素どうしの前後関係は変わらない。
削除した位置より後ろの要素のインデックスは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関数は変数を使ってスライス・オブジェクトを生成する組み込み関数だ。del list1[2:4] と直接書くこともできる。
リストを使った計算
sumNaturalNumbers3.py
def sumNaturalNumbers3(a, b):
"""aからbまでの自然数の和を求めるユーザー関数(リスト版)
Args:
a(int): 開始値
b(int): 終了値
Returns:
int 合計値
"""
if a > b:
raise ValueError("開始値は終了値以下にしてください")
list1 = list(range(a, b + 1)) # リスト化
return sum(list1)
プログラム "sumNaturalNumbers3.py" は、aからbまでの整数の和を求める関数のリスト版だ。range関数 は rangeオブジェクトを返すが、これを list関数に通してリストに変換する。こうして、aからbまでの整数を要素とするリスト list1 を生成する。
sum関数 は数値を合計する組み込み関数である。この例では学習のためリスト化しているが、sum(range(a, b + 1)) なら途中のリストを作らずに合計できる。サンプルの入力は1~10000で、開始値は終了値以下とする。
sum関数 は数値を合計する組み込み関数である。この例では学習のためリスト化しているが、sum(range(a, b + 1)) なら途中のリストを作らずに合計できる。サンプルの入力は1~10000で、開始値は終了値以下とする。
minmax.py
# リスト‥‥整数型のみ
list1 = [0, 1, 2, 3, 4]
print(list1)
# 最小値
print(min(list1))
# 最大値
print(max(list1))
また、min関数、max関数を使うことで、リストの最小値や最大値を求めることができる。空のリストでは、sumは0を返すが、minとmaxは既定値を指定しないと ValueError になる。
リストのシャフルとソート
shuffle1.py
import random
# スート S:spade, H:heart, D:diamond, 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枚を取りだし、extendメソッドを使って手札 hands に加える。山札からも移した3枚を削除するので、最後は山札44枚、手札5枚、捨てたカード3枚となる。
shuffle1.py
# 手札をソートする
hands.sort()
print(hands)
最後に sortメソッドで手札そのものを並べ替える。この例では文字列の辞書順なのでスートは C、D、H、S の順になる。同じスートでは数字を01~13の2桁にそろえているため、数の小さい順と一致する。トランプのルール上の強さを判定しているわけではない。
sortの戻り値は None なので、hands = hands.sort() と書かないこと。元を変えずに並べ替えたリストを作る場合は sorted(hands) を使う。数値と文字列など、大小を比較できない要素が混在するとエラーになる。
sortの戻り値は None なので、hands = hands.sort() と書かないこと。元を変えずに並べ替えたリストを作る場合は sorted(hands) を使う。数値と文字列など、大小を比較できない要素が混在するとエラーになる。
リストと配列
他のプログラミング言語を触ったことがある方は、リストは配列のことではないかと思うだろう。むしろ、他の言語より便利なメソッドや関数が揃っていることから、使い勝手のいい配列に見えるかもしれない。
だが、Python の標準ライブラリには配列を扱う arrayモジュールが別に用意されている。ここでは、組み込みの list と、標準ライブラリの array.array を区別して扱う。「配列」というデータ構造全般と、Pythonの型の名前を混同しないようにしよう。
だが、Python の標準ライブラリには配列を扱う arrayモジュールが別に用意されている。ここでは、組み込みの list と、標準ライブラリの array.array を区別して扱う。「配列」というデータ構造全般と、Pythonの型の名前を混同しないようにしよう。
array1.py
from array import array
# 配列を生成する
array1 = array("b", range(1, 10))
print(array1)
# 要素を加える
array1.append(10)
print(array1)
# 合計を計算する
print(sum(array1))
プログラム "array1.py" を実行してみてほしい。
from array import array の後、array("b", range(1, 10)) と書くと、1~9を格納した配列ができる。最初の引数は型コードで、格納する値の種類を指定する。初期値にはリストやrangeなど、要素を順に取り出せるものを渡す。整数1個だけを直接渡すことはできない。
次の表は数値用の型コードである。たとえば "b" は符号付きの1バイト整数で、-128~127を格納できる。型によって格納範囲があり、実際の1要素のバイト数は itemsize で確認できる。
from array import array の後、array("b", range(1, 10)) と書くと、1~9を格納した配列ができる。最初の引数は型コードで、格納する値の種類を指定する。初期値にはリストやrangeなど、要素を順に取り出せるものを渡す。整数1個だけを直接渡すことはできない。
次の表は数値用の型コードである。たとえば "b" は符号付きの1バイト整数で、-128~127を格納できる。型によって格納範囲があり、実際の1要素のバイト数は itemsize で確認できる。
| 配列の型 | 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関数を適用することができる。

array.array は同じ種類の数値などをコンパクトに格納できる。一方、list は異なる型を混ぜられる。array.arrayなら必ず処理が速い、というわけではない。処理内容やPythonの実装、データ量によって結果が変わる。大量の数値の一括計算では、標準のarrayとは別物である NumPy の配列を使う方法もある。
array.array は同じ種類の数値などをコンパクトに格納できる。一方、list は異なる型を混ぜられる。array.arrayなら必ず処理が速い、というわけではない。処理内容やPythonの実装、データ量によって結果が変わる。大量の数値の一括計算では、標準のarrayとは別物である NumPy の配列を使う方法もある。
max1.py
from array import array
import random
import time
# 同じ値と順序で、maxだけの時間を比較する。
n = 1000000
cnt = 10
time1 = 0
time2 = 0
for i in range(cnt):
list1 = list(range(1, n + 1))
random.shuffle(list1)
array1 = array("L", list1)
# 測定順による影響を減らすため、順序を交互にする。
datasets = [("list", list1), ("array", array1)]
if i % 2:
datasets.reverse()
for kind, values in datasets:
start = time.perf_counter()
result = max(values)
elapsed = time.perf_counter() - start
assert result == n
if kind == "list":
time1 += elapsed
else:
time2 += elapsed
# 次の測定前に参照を解放する。
del list1, array1, datasets, values
print(f"リスト = {time1 / cnt:.5f}秒")
print(f"配列 = {time2 / cnt:.5f}秒")
print("この条件でのmaxの比較です。生成・変換・シャフルの時間は含みません。")
"max1.py" は、1~100万を同じ順序に並べたリストと配列について、max関数の実行時間を10回測り、平均を比べる実験である。生成やシャフル、配列への変換は計測に含めない。結果はこの条件での比較であり、すべての処理の優劣を示すものではない。
練習問題
次回予告
次回は、コンテナデータ型のタプル、集合型、辞書型の使い方を学ぶ。
最後に、コンテナデータ型を活用してクラスの成績表を作り、個人合計点や科目毎平均点を計算するプログラムを作る。
最後に、コンテナデータ型を活用してクラスの成績表を作り、個人合計点や科目毎平均点を計算するプログラムを作る。
コラム:インデックスはなぜ0からはじまるのか
先頭の要素なのにインデックスが0なのは、先頭から何個分離れているかと考えると理解しやすい。最初は0個分、次は1個分、その次は2個分だけ離れている。横一列のロッカーで、入口に一番近い箱を出発点にするイメージだ。

同じ大きさの要素を並べた配列では、位置を「先頭のアドレス+インデックス×要素の大きさ」で表せる。0始まりはこの考え方と相性がよく、C言語などでも使われる。ただし、1始まりなら必ず遅いわけではなく、言語やコンパイラの設計による。

もう1つの利点は範囲の表しやすさだ。要素が5個ならインデックスは0以上5未満となる。Pythonの a[2:5] も、2を含んで5を含まないので、範囲内なら 5 - 2 = 3 個を取り出す。a[0:2] と a[2:5] は、境目の2で重なりなくつながる。

エドガー・ダイクストラは1982年(昭和57年)の文章で、こうした範囲の扱いやすさを論じた。0始まりだけが正解ということではないが、Pythonでは「先頭からの距離」と「終了位置を含まない範囲」をセットで覚えると、添字の間違いを減らせる。
同じ大きさの要素を並べた配列では、位置を「先頭のアドレス+インデックス×要素の大きさ」で表せる。0始まりはこの考え方と相性がよく、C言語などでも使われる。ただし、1始まりなら必ず遅いわけではなく、言語やコンパイラの設計による。
もう1つの利点は範囲の表しやすさだ。要素が5個ならインデックスは0以上5未満となる。Pythonの a[2:5] も、2を含んで5を含まないので、範囲内なら 5 - 2 = 3 個を取り出す。a[0:2] と a[2:5] は、境目の2で重なりなくつながる。
エドガー・ダイクストラは1982年(昭和57年)の文章で、こうした範囲の扱いやすさを論じた。0始まりだけが正解ということではないが、Pythonでは「先頭からの距離」と「終了位置を含まない範囲」をセットで覚えると、添字の間違いを減らせる。
コラム:データ構造
プログラミングはアルゴリズムとデータ構造と言われる。制御文や関数、メソッドを使って処理の流れを組み立てるのがアルゴリズムで、データを入れる器がデータ構造だ。
Pythonの list という型名と、連結リストという内部のデータ構造は別である。
Pythonの list という型名と、連結リストという内部のデータ構造は別である。
図は連結リストの例である。1つずつの入れ物(ノード)が、次の入れ物へのつながりを持つ。一方、一般的なPython実装である CPython の list は、要素への参照を連続して並べた可変長の配列で実装される。各要素のデータ本体が連続しているとは限らない。

データ構造にはキュー、スタック、ツリー、連想配列、ヒープなどがある。Pythonでは list、dict に加え、標準ライブラリの collections.deque や heapq などを使い分ける。たとえば、listの先頭への挿入は後ろの要素をずらす必要があるので、両端への追加や削除を頻繁に行うならdequeが候補になる。用途に合った構造を選ぶことが大切だ。

これを読んでデータ構造について興味をお持ちになったら、「IT技術 - データ構造の話」をご覧いただきたい。
データ構造にはキュー、スタック、ツリー、連想配列、ヒープなどがある。Pythonでは list、dict に加え、標準ライブラリの collections.deque や heapq などを使い分ける。たとえば、listの先頭への挿入は後ろの要素をずらす必要があるので、両端への追加や削除を頻繁に行うならdequeが候補になる。用途に合った構造を選ぶことが大切だ。
これを読んでデータ構造について興味をお持ちになったら、「IT技術 - データ構造の話」をご覧いただきたい。
コラム:乱数の一様性
サイコロを振る紺乃(AI生成)
AI生成コンテンツ / AI-generated content
AI生成コンテンツ / AI-generated content
本編で使った random モジュールは、ゲームにも役立つ。公平なサイコロなら、各目が出る確率はそれぞれ $1/6$ である。これが一様という意味で、1,000回振ったら必ず各目が同じ回数出る、という意味ではない。各目の回数は平均的には約166.7回だが、実際にはばらつく。
一様性は各値の出やすさについての性質、予測しにくさは次の値を当てにくいという性質であり、同じではない。擬似乱数は計算で作る数列で、同じ初期状態(シード)なら同じ列を再現できる。再現できることは実験や不具合の調査に便利で、一様分布の乱数を作れないという意味ではない。

"randomNumberBias.py" は、4通りの方法で1~6を10万回生成し、各目の出現回数と「10万÷6」との差の絶対値を合計して、10万で割った百分率を表示する。これはその回のばらつきを見る指標で、乱数生成器の良し悪しや暗号用途の安全性を順位付けする検定ではない。実行のたびに結果が変わる。NumPyが必要なので、未導入なら python -m pip install numpy でインストールする。

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

numpy.random.default_rng は乱数生成器を作る関数で、既定の方式は PCG64 である。rng.integers(1, 7) は1以上7未満の整数を返す。扱える確率分布の種類が多いことと、一様分布のばらつきが小さいことは別である。

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

os.urandom は、暗号用途に適したランダムなバイト列を返す。単に各バイトを6で割った余りにすると、0~255の256通りを6等分できず、目に偏りが生じる。サンプルでは252~255を捨て、0~251の252通りを使うことで、この変換による偏りを取り除く。通常のサイコロなら random.randint(1, 6)、予測されにくさも必要なら secrets.randbelow(6) + 1 が使いやすい。
"randomNumberBias.py" は、4通りの方法で1~6を10万回生成し、各目の出現回数と「10万÷6」との差の絶対値を合計して、10万で割った百分率を表示する。これはその回のばらつきを見る指標で、乱数生成器の良し悪しや暗号用途の安全性を順位付けする検定ではない。実行のたびに結果が変わる。NumPyが必要なので、未導入なら python -m pip install numpy でインストールする。
random.randint は、randomモジュールにある整数の乱数を発生する関数だ。乱数生成器としてメルセンヌ・ツイスタを利用している。メルセンヌ・ツイスタについては、「補足:乱数」で解説しているので、あわせてご覧いただきたい。
numpy.random.default_rng は乱数生成器を作る関数で、既定の方式は PCG64 である。rng.integers(1, 7) は1以上7未満の整数を返す。扱える確率分布の種類が多いことと、一様分布のばらつきが小さいことは別である。
secrets.randbelow は、機密を扱うために安全な乱数を生成できる secretsモジュールの乱数だ。OSが提供する最も高品質なソースを用いて乱数を生成することができる。
os.urandom は、暗号用途に適したランダムなバイト列を返す。単に各バイトを6で割った余りにすると、0~255の256通りを6等分できず、目に偏りが生じる。サンプルでは252~255を捨て、0~251の252通りを使うことで、この変換による偏りを取り除く。通常のサイコロなら random.randint(1, 6)、予測されにくさも必要なら secrets.randbelow(6) + 1 が使いやすい。
コラム:完全数と計算量
自分自身を除く正の約数の和が、元の数と等しくなる正の整数を完全数(perfect number)と呼ぶ。
たとえば6の約数は1、2、3、6であり、6以外を足すと 1 + 2 + 3 = 6 になる。最初の4個は6、28、496、8128である。
約数を1つずつ調べる方法のほか、偶数の完全数には後述する公式がある。「公式がないので総当たりしかできない」というわけではない。
たとえば6の約数は1、2、3、6であり、6以外を足すと 1 + 2 + 3 = 6 になる。最初の4個は6、28、496、8128である。
約数を1つずつ調べる方法のほか、偶数の完全数には後述する公式がある。「公式がないので総当たりしかできない」というわけではない。
perfectNumbers1.py
import time
# 探索範囲の最小値
RANGE_MIN = 2;
# 探索範囲の最大値
RANGE_MAX = 100000
def isPerfectNumber1(num):
"""完全数かどうかを判定する.
Args:
num(int): 判定する値
Returns:
bool: true:完全数である / false:完全数ではない
"""
if num < 2:
return 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" は、定義どおりに各数の約数を1から調べる。上限を N とすると、全体ではおよそ $N^2$ に比例する回数の割り算が必要になる。既定の RANGE_MAX = 100000 は時間がかかるので、最初は1000程度に下げて試すとよい。10万以下の完全数は6、28、496、8128であり、次は33550336となる。
次に紹介する "perfectNumbers2.py" は、外部ライブラリ NumPy を使って高速化したプログラムである。NumPy を使うには、pipコマンドを使って
python -m pip install numpyとしてインストールしてほしい。
perfectNumbers2.py
import time
import numpy as np
from math import isqrt
# 探索範囲の最小値
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, isqrt(num) + 1)
sumDiv = np.sum(divisors[num % divisors == 0])
# 約数のペアを加える(1の相手は自分自身、平方根は重複するので除く)
sumDiv += np.sum(num // divisors[(num % divisors == 0) & (divisors != 1) & (divisors * divisors != num)])
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") + "秒")
"perfectNumbers2.py" では、約数が対になる性質も利用する。たとえば36の約数2を見つければ、相手の18も分かるので、調べるのは平方根6まででよい。6×6のような場合は同じ約数を二重に足さない。
numpy.arange は連続した値の配列を作る関数であり、単にrangeを高速化する関数ではない。このサンプルの改善には、NumPyによる一括演算と、約数を調べる範囲を平方根まで減らす工夫の両方が含まれる。全体の調査回数はおよそ $N\sqrt{N}$ に比例する。実行時間は環境によって変わるので、自分のパソコンで比べてほしい。
numpy.arange は連続した値の配列を作る関数であり、単にrangeを高速化する関数ではない。このサンプルの改善には、NumPyによる一括演算と、約数を調べる範囲を平方根まで減らす工夫の両方が含まれる。全体の調査回数はおよそ $N\sqrt{N}$ に比例する。実行時間は環境によって変わるので、自分のパソコンで比べてほしい。
偶数の完全数には、ユークリッド・オイラーの定理が使える。ユークリッドが次の式で完全数を作れることを示し、のちにレオンハルト・オイラーが、すべての偶数の完全数がこの形になることを証明した。
ユークリッド・オイラーの定理によると、
$$ 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" である。
perfectNumbers3.py
import time
import math
# 探索範囲の最小値
RANGE_MIN = 2;
# 探索範囲の最大値
RANGE_MAX = 100000
def isPrime(num):
"""素数かどうかを判定する.
Args:
num(int): 判定する値
Returns:
bool: true:素数である / false:素数ではない
"""
if num < 2:
return False
for i in range(2, math.isqrt(num) + 1):
if (num % i == 0):
return False;
return True
def isMersennePrime(p):
"""メルセンヌ素数かどうかを判定する.
Args:
p(int): 判定する2のべき乗値
Returns:
bool: true:メルセンヌ素数である / false:メルセンヌ素数ではない
"""
mersenneNumber = 2 ** p - 1
return isPrime(mersenneNumber)
def findPerfectNumbers3(min, max):
"""完全数を求める.
Args:
min(int): 探索範囲の最小値
max(int): 探索範囲の最大値
Returns:
list: 完全数のリスト
"""
perfectNumbers = [] # 完全数を格納するリスト
if (min < 2):
min = 2
p = 2
while True:
num = (2 ** (p - 1)) * ((2 ** p) - 1)
if num > max:
break
if num >= min and isMersennePrime(p):
perfectNumbers.append(num)
p += 1
return perfectNumbers
# メイン・プログラム =======================================================
# 計算開始時刻
startTime = time.monotonic()
# 完全数を求める
perfectNumbers = findPerfectNumbers3(RANGE_MIN, RANGE_MAX)
# 計算終了時刻
finishTime = time.monotonic()
# 完全数を画面に表示する.
for val in perfectNumbers:
print(f"{val:,}")
# 計算時間
print("計算時間 : " + format((finishTime - startTime), f".3f") + "秒")
"perfectNumbers3.py" は式から候補を作るので、自然数を1つずつ調べずに済む。ただし、メルセンヌ数が素数かどうかの判定には繰り返し計算が必要であり、この試し割りのプログラムが巨大な完全数まで高速に求められるわけではない。
10万以下では同じ4個が得られるが、この方法は偶数だけを扱う。奇数の完全数が存在するか、また完全数が無限にあるかは未解決である。
10万以下では同じ4個が得られるが、この方法は偶数だけを扱う。奇数の完全数が存在するか、また完全数が無限にあるかは未解決である。
このように、同じ答えを求めるにも、データ構造やアルゴリズムの選び方で必要な計算量は変わる。ただし、速さだけでなく、求めたい範囲や条件を満たしているかも確認しよう。
参考サイト
- データ構造:Python公式ドキュメント
- 組み込み型:Python公式ドキュメント
- array — 効率のよい数値アレイ:Python公式ドキュメント
- CPythonのリストの実装:Python公式ドキュメント
- random — 擬似乱数を生成する:Python公式ドキュメント
- os.urandom:Python公式ドキュメント
- Random Generator:NumPy公式ドキュメント
- numpy.arange:NumPy公式ドキュメント
- なぜ配列の1番目は「0」なのか? プログラマーなら一度は疑問に思う話:Qiita
- Why numbering should start at zero:E. W. Dijkstra Archive
- Mathematics and Research Strategy:GIMPS
- List of Known Mersenne Prime Numbers:GIMPS
(この項おわり)

今回は、リストと配列の使い方を学ぶ。