じゃんけんゲームを作ろうとして、コンピューターの手をどう決めればいいのか迷ったことはありませんか。
くじ引き、シャッフル、サンプル抽出。プログラムに偶然を持ち込みたい場面は、思ったより多く出てきます。
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回動かしてみるところから始めてみてください。数字が毎回変わる様子を見るだけでも、乱数の感覚はぐっとつかみやすくなります。
ここまでお読みいただきありがとうございました。