Python WebAcademy Blog

Pythonのrandomとは?サイコロやシャッフルの書き方と、パスワードに使ってはいけない理由を解説

|

Pythonで乱数を扱うrandomモジュールを、IT初心者向けにやさしく解説します。randintとrandrangeの終わりの違い、choiceやshuffleやsampleの使い分け、seedで結果を固定して再現性を作る方法まで。さらに、randomの乱数がなぜ本当の偶然ではないのか、パスワードやトークンにはsecretsを使うべき理由も、動かせるコードと表で順番に整理していきます。

じゃんけんゲームを作ろうとして、コンピューターの手をどう決めればいいのか迷ったことはありませんか。

くじ引き、シャッフル、サンプル抽出。プログラムに偶然を持ち込みたい場面は、思ったより多く出てきます。

Pythonには、そのための道具が標準で入っています。random というモジュールです。

ただ、この便利さには落とし穴もあります。使い方を間違えると、セキュリティ事故につながる場面があるのです。

この記事では、randomの基本から、使ってはいけない場面までを順番に整理していきます。追加のインストールは必要ありません。

randomは、コンピューターに偶然を作らせるためのモジュール

まずは立ち位置の確認から始めましょう。randomはPythonの標準ライブラリなので、import random と書くだけですぐ使えます。

やることはシンプルです。呼ぶたびに違う数字を返してくれます。

その数字をもとに、サイコロの目を決めたり、リストから1つ選んだり、順番を混ぜたりできます。

実際に1行動かしてみましょう。0以上1未満の小数がひとつ返ってきます。

import random

print(random.random())
# 例: 0.991336641576708(実行するたびに変わります)

小数が返るだけでは使いどころが見えにくいので、次はもっと実用的な関数を見ていきます。

サイコロを振る、範囲から数字を選ぶ

整数がほしいときに使うのが randint です。サイコロなら1から6の範囲を指定します。

引数の書き方が直感的なので、最初に覚えるならここからで十分です。

import random

# サイコロを1回振る(1〜6のどれか)
print(random.randint(1, 6))

# 10回振って結果を並べる
rolls = [random.randint(1, 6) for _ in range(10)]
print(rolls)
# 例: [3, 1, 6, 6, 2, 5, 1, 4, 4, 2]

# 小数がほしいときはuniform
print(random.uniform(0, 100))
# 例: 22.33941859097999

ここでひとつ、初心者がよくつまずくポイントがあります。よく似た2つの関数で、終わりの数字の扱いが違うのです。

randintとrandrangeは、終わりの扱いが違う

random.randint(1, 6) は1から6までで、6も含まれます。サイコロにぴったりの挙動です。

一方の random.randrange(1, 6) は1から5までで、6は含まれません

これは range(1, 6) と同じ考え方だと思ってください。Pythonでは範囲の終わりを含めない書き方が標準的で、randrangeもそちらに合わせています。

rangeの挙動そのものがあいまいな方は、こちらで基本から整理しています。【関連記事】pythonのrange関数の使い方は?ステップ指定と逆順ループの小技をご紹介!

私も10年ほど開発に関わってきましたが、この2つを取り違えたバグは何度か見てきました。抽選で最後の1人だけ絶対に当たらない、という不具合の原因がこれだった、というのは珍しくない話です。

迷ったら、サイコロのように上限も含めたいときはrandint、rangeと同じ感覚で書きたいときはrandrangeと覚えておくと安全です。

リストから選ぶ、混ぜる、抜き出す

数字だけでなく、リストの中身を相手にする関数もそろっています。よく使うのは次の3つです。

それぞれ似ているようで、返ってくるものが違います。まずは動かして違いを見てみましょう。

import random

fruits = ['りんご', 'みかん', 'ぶどう', 'もも', 'なし']

# 1つだけ選ぶ
print(random.choice(fruits))
# 例: ぶどう

# 重複ありで3つ選ぶ(同じものが出ることがある)
print(random.choices(fruits, k=3))
# 例: ['もも', 'りんご', 'もも']

# 重複なしで3つ選ぶ(くじ引きに近い)
print(random.sample(fruits, 3))
# 例: ['なし', 'りんご', 'ぶどう']

# 順番を混ぜる(元のリストが書き換わる)
random.shuffle(fruits)
print(fruits)
# 例: ['みかん', 'なし', 'もも', 'りんご', 'ぶどう']

違いがつかみにくいので、表にまとめておきます。

関数 返るもの 重複 元のリスト
choice(seq) 要素を1つ 変わらない
choices(seq, k=3) 要素のリスト あり 変わらない
sample(seq, 3) 要素のリスト なし 変わらない
shuffle(seq) None 並びが書き換わる

表で見ると、shuffleだけ性格が違うのが分かるはずです。並べ替えた結果を返すのではなく、渡したリストそのものを書き換えます。

そのため new_list = random.shuffle(fruits) と書くと、new_listには None が入ってしまいます。ここは本当によくある失敗です。

またsampleは、リストの数より多く取り出そうとするとエラーになります。5個しかない中から10個は選べない、という当たり前の話が例外として返ってきます。

なお choices には重みも指定できます。当たりが出にくいガチャのような仕組みは、この引数だけで表現できます。

import random

# 90%の確率でハズレ、10%の確率で当たり
result = random.choices(['ハズレ', '当たり'], weights=[90, 10], k=10)
print(result)

seedを決めると、同じ結果が何度でも出る

ここからが、randomのいちばん面白いところです。乱数なのに、結果を固定できます。

使うのは random.seed() です。同じ数字を渡してから呼び出すと、毎回まったく同じ並びが返ってきます。

import random

random.seed(42)
print([random.randint(1, 6) for _ in range(5)])
# 実行環境が同じなら、いつでも同じ並びになります

random.seed(42)
print([random.randint(1, 6) for _ in range(5)])
# 上とまったく同じ結果

不思議に感じるかもしれませんが、これは仕様どおりの動きです。randomが返しているのは、種になる数字から計算で作られた数列だからです。

種が同じなら、計算結果も同じ。だから再現できるわけです。

テストで乱数を扱うときのコツ

この性質は、テストを書くときに効いてきます。乱数が混ざった処理は、そのままだと結果が毎回変わってテストが安定しません。

そこでテストの先頭でseedを固定します。すると期待値を書けるようになり、失敗したときの再現もできます。

テストの書き方そのものが気になる方は、あわせてこちらもどうぞ。【関連記事】pytest入門|Pythonでテストを書く基本を初心者向けに解説

実務でも、seedを固定していなかったせいで、たまにだけ落ちるテストに悩まされたことがあります。原因が乱数だと気づくまでに半日ほど溶かしました。

シミュレーションや機械学習でも同じです。結果を他の人に見せるなら、seedを書き残しておくと親切です。

randomの乱数は、本当の偶然ではない

seedで再現できるということは、裏を返すと決まった手順で計算しているということです。

Pythonの公式ドキュメントによると、randomが内部で使っているのはメルセンヌ・ツイスタと呼ばれるアルゴリズムです。周期は2の19937乗から1を引いた長さで、実用上は同じ並びが繰り返される心配はありません。

統計的な性質もよく調べられていて、シミュレーションやゲームの用途なら申し分ない品質です。

ただし公式ドキュメントには、完全に決定的であり、すべての目的に適しているわけではない、という但し書きが添えられています。

このアルゴリズムは内部に624個の数値で状態を持っています。つまり、出力を十分な数だけ観測できれば、内部の状態を復元して次に何が出るかを言い当てられます。

乱数に見えていても、外から予測できる可能性がある。 ここが次の話につながります。

randomで作った円周率の推定など、当てられて困らない用途なら何も問題はありません。モンテカルロ法のような遊び方は、むしろ乱数の入り口としておすすめです。【関連記事】Pythonで円周率を使うには?math.piの基本から計算例まで初心者向けに解説

パスワードやトークンにrandomを使ってはいけない

問題になるのは、当てられたら困る値を作るときです。

パスワードの初期値、パスワード再設定用のURLに載せるトークン、APIキー、セッションID。この手の値をrandomで作ってはいけません。

公式ドキュメントもはっきり書いています。randomの擬似乱数はセキュリティ目的に使うべきではなく、そうした用途には secrets モジュールを使うようにと案内されています。

randomのままだと、攻撃者に予測される余地が残ります。他人のパスワード再設定リンクを言い当てられる、といった事故につながる話です。

secretsで安全な文字列を作る

secretsも標準ライブラリなので、インストールは要りません。使い方もrandomとよく似ています。

import secrets
import string

# ランダムなトークン(16進数の文字列)
print(secrets.token_hex())
# 64文字の文字列が返ります

# URLに埋め込みやすい形
print(secrets.token_urlsafe())
# 43文字の文字列が返ります

# 英数字12文字のパスワード
alphabet = string.ascii_letters + string.digits
password = ''.join(secrets.choice(alphabet) for _ in range(12))
print(password)

token_hex()token_urlsafe() は、引数を省略すると32バイト分の乱数から文字列を作ります。何文字にすべきか迷わなくて済むように、安全側の初期値が決められています。

secrets.choice() の使い方は random.choice() とまったく同じです。書き方はほぼ変わらないので、迷ったらsecretsを選んでおけば損はありません。

使い分けを表にしておきます。

用途 使うもの 理由
ゲーム、くじ、シャッフル random 速く、seedで再現できる
シミュレーション、機械学習 random / NumPy 再現性が求められる
パスワード、トークン、APIキー secrets 予測されては困る
秘密の値を安全に保管する 環境変数 コードに直接書かない

作った秘密の値をどこに置くかも、同じくらい大事な話です。生成だけ安全にしても、コードに直接書いてしまえば意味がありません。【関連記事】環境変数とは?PythonでAPIキーを安全に扱う.envと os.environ の基本を初心者向けに解説

なおrandomの中にも、OSの乱数を使う random.SystemRandom というクラスがあります。こちらはseedを渡しても無視され、結果を再現できません。安全な代わりに、再現性を捨てているわけです。

NumPyの乱数は、また別の仕組み

データ分析の記事を読んでいると np.random という書き方をよく見かけます。これはNumPyが持っている、別の乱数の仕組みです。

配列をまとめて作れるので、大量の乱数が必要なときはこちらのほうが向いています。標準のrandomで1万個作るより、ずっと素直に書けます。

ただしseedの管理は別物なので、random.seed() を呼んでもNumPyの結果は固定されません。ここは混同しやすい部分です。

NumPyそのものがまだ手つかずの方は、こちらから読むと流れがつかめます。【関連記事】PythonのNumPyとは?配列計算がリストより速くなる仕組みを初心者向けに解説

まとめ

最後に、この記事の要点を振り返っておきます。

randomは、偶然をプログラムに持ち込むための標準ライブラリです。randintで整数、choiceで1つ選ぶ、sampleで重複なし、shuffleで並べ替え。この4つを押さえれば、たいていの場面は書けます。

randintは上限を含み、randrangeは含みません。shuffleは値を返さず、元のリストを書き換えます。まずはこの2点だけ気をつけてください。

そしてseedを決めれば、同じ結果を何度でも再現できます。テストやシミュレーションでは、この性質が武器になります。

ただし、再現できるということは予測もできるということです。 パスワードやトークンを作るときは、必ずsecretsに切り替えてください。

まずは手元で random.randint(1, 6) を10回動かしてみるところから始めてみてください。数字が毎回変わる様子を見るだけでも、乱数の感覚はぐっとつかみやすくなります。

ここまでお読みいただきありがとうございました。

参考情報

次のアクション

記事で学んだ内容を実際に動かしてみよう

Python WebAcademyでは、ブラウザ上でコードを書きながら基礎から実践まで体系的に学べます。

Python WebAcademyの学習画面

あわせて読む

関連記事

ブログ一覧へ

Python学習ロードマップ

まずはこの3講座から

記事で気になったテーマを、順番に手を動かしながら学べます。

ロードマップを見る