乱数のシードを固定して同じ結果を再現するには
乱数のシードを固定して、実行のたびに同じ数列を得る書き方を各言語で示す。
同じシード 42 でも言語ごとに数列が違う点や、シードを渡せない乱数の落とし穴もまとめる。
公開:
選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。
Python
import random
random.seed(42)
first = [random.randint(1, 100) for _ in range(5)]
random.seed(42)
second = [random.randint(1, 100) for _ in range(5)]
print(first) # [82, 15, 4, 95, 36]
print(first == second) # True
# 専用の乱数生成器にすると、他のコードの random 呼び出しに影響されない
rng = random.Random(42)
print([rng.randint(1, 100) for _ in range(5)]) # [82, 15, 4, 95, 36]乱数を作る前に random.seed(42) を呼ぶと、そのあとの数列が毎回同じになる。
グローバルな状態に頼ると他の処理の乱数呼び出しに影響されるので、再現したい処理には random.Random(42) で専用の生成器を作って使う。
JavaScript
// Math.random() にはシードを渡せない。シード付きの生成器を自分で用意する(mulberry32)
function mulberry32(seed) {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const randint = (rand, min, max) => min + Math.floor(rand() * (max - min + 1));
const rand1 = mulberry32(42);
const first = Array.from({ length: 5 }, () => randint(rand1, 1, 100));
const rand2 = mulberry32(42);
const second = Array.from({ length: 5 }, () => randint(rand2, 1, 100));
console.log(first); // [ 61, 45, 86, 67, 18 ]
console.log(JSON.stringify(first) === JSON.stringify(second)); // trueMath.random() にはシードを渡す手段がないので、シード付きの生成器を自分で用意する。
ここでは数行で書ける mulberry32 を使い、0 以上 1 未満の値を返す関数として Math.random() の代わりに差し込む。
暗号用途には使えない。
TypeScript
type Rand = () => number;
// Math.random() にはシードを渡せない。シード付きの生成器を自分で用意する(mulberry32)
function mulberry32(seed: number): Rand {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const randint = (rand: Rand, min: number, max: number): number =>
min + Math.floor(rand() * (max - min + 1));
const rand1 = mulberry32(42);
const first: number[] = Array.from({ length: 5 }, () => randint(rand1, 1, 100));
const rand2 = mulberry32(42);
const second: number[] = Array.from({ length: 5 }, () => randint(rand2, 1, 100));
console.log(first); // [ 61, 45, 86, 67, 18 ]
console.log(first.join() === second.join()); // true生成器を () => number という型にしておくと、Math.random もシード付きの生成器も同じ引数で受け取れる。
本番では Math.random を、テストでは mulberry32(42) を渡す使い分けができる。
Go
package main
import (
"fmt"
"math/rand/v2"
)
func draw(seed uint64) []int {
rng := rand.New(rand.NewPCG(seed, 0))
out := make([]int, 5)
for i := range out {
out[i] = rng.IntN(100) + 1 // 1〜100
}
return out
}
func main() {
first := draw(42)
second := draw(42)
fmt.Println(first) // [86 97 14 9 19]
fmt.Println(fmt.Sprint(first) == fmt.Sprint(second)) // true
}math/rand/v2 では rand.New(rand.NewPCG(seed1, seed2)) でシード付きの生成器を作る。
トップレベルの rand.IntN などはシードを指定できないため、再現したい処理では生成器を自分で持って関数に渡す。
Rust
// Cargo.toml: rand = "0.10"
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
fn draw(seed: u64) -> Vec<u32> {
let mut rng = StdRng::seed_from_u64(seed);
(0..5).map(|_| rng.random_range(1..=100)).collect()
}
fn main() {
let first = draw(42);
let second = draw(42);
println!("{:?}", first); // [14, 53, 25, 55, 87]
println!("{}", first == second); // true
}標準ライブラリには乱数がないので rand クレートを使う。StdRng::seed_from_u64 でシードから生成器を作り、random_range で範囲を指定して値を取り出す。
Java
import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;
public class Main {
static int[] draw(long seed) {
Random rng = new Random(seed);
return IntStream.range(0, 5).map(i -> rng.nextInt(1, 101)).toArray(); // 1〜100
}
public static void main(String[] args) {
int[] first = draw(42);
int[] second = draw(42);
System.out.println(Arrays.toString(first)); // [31, 64, 49, 85, 71]
System.out.println(Arrays.equals(first, second)); // true
}
}new Random(seed) で同じシードから同じ数列が得られる。nextInt(1, 101) のように下限と上限を渡せるのは Java 17 以降で、上限は含まれない。
C#
using System;
using System.Linq;
static int[] Draw(int seed)
{
var rng = new Random(seed);
return Enumerable.Range(0, 5).Select(_ => rng.Next(1, 101)).ToArray(); // 1〜100
}
var first = Draw(42);
var second = Draw(42);
Console.WriteLine($"[{string.Join(", ", first)}]"); // [67, 15, 13, 53, 17]
Console.WriteLine(first.SequenceEqual(second)); // Truenew Random(seed) のようにシードを渡して生成器を作る。Next(1, 101) の上限 101 は含まれないので、1〜100 の値になる。
同じシード 42 でも言語が違えば数列は違う
各言語で、シード 42 から 1〜100 の整数を 5 個取り出して比べた。
- Python は 82, 15, 4, 95, 36 だった。
- JavaScript と TypeScript(
mulberry32)は 61, 45, 86, 67, 18 だった。 - Go(
PCG)は 86, 97, 14, 9, 19 だった。 - Rust(
StdRng、rand 0.10.3)は 14, 53, 25, 55, 87 だった。 - Java は 31, 64, 49, 85, 71 だった。
- C# は 67, 15, 13, 53, 17 だった。
再現できるのは同じ言語・同じ実装の中だけである。
Python で作ったテストデータを別の言語の生成器で再現しようとしても、数列は一致しない。
シードを渡せない乱数がある
手近な乱数関数がシードに対応しているとは限らない。
- JavaScript の
Math.random()にはシードを渡す手段がない。自作するか、ライブラリのシード付き生成器を使う。 - Go の
math/rand/v2にはrand.Seedがなく、rand.Seed(42)と書くとundefined: rand.Seedでコンパイルエラーになる(Go 1.27.3 で確認)。rand.Newで生成器を作る。 - Java の
ThreadLocalRandom.current().setSeed(42)はUnsupportedOperationExceptionを投げる(Java 21 で確認)。new Random(42)を使う。 - C# の
Random.Sharedはシードを受け付けない型(実行時の型名はThreadSafeRandom)だった。new Random(42)を使う。
速くて手軽な乱数ほどシード固定に向かない作りになっている。
再現が要る処理では、最初から生成器を引数で受け取る形にしておくとよい。
Python のグローバルな乱数は余分な呼び出しで数列がずれる
random.seed(42) のあとに別の場所で random.random() を 1 回呼ぶと、続く randint の結果が変わった。
- 余分な呼び出しなしでは 82, 15, 4, 95, 36 になる。
- 1 回呼んだあとは 4, 95, 36, 32, 29 になり、元の数列の 3 番目から始まっていた。
random モジュールの生成器は 1 つを全体で共有している。
ライブラリや別のスレッドが乱数を使うだけで、テストの再現性が崩れる。
再現したい処理には random.Random(42) で専用の生成器を作る。
再現できる範囲と使ってはいけない場面
シードを固定しても、同じ数列が得られる範囲には限りがある。
- 生成アルゴリズムをドキュメントで保証していない型は、ライブラリの版が上がると数列が変わりうる。Rust の
StdRngや .NET のRandomがその例である。版をまたいで固定したいときは、アルゴリズムを名指しできる生成器(Rust ならrand_chachaのChaCha8Rngなど)を使う。 - 固定したシードの乱数は予測できる。パスワード、トークン、抽選の本番には使わず、OS の乱数を使う関数を選ぶ。
用途はテストの再現、シミュレーションの比較、機械学習の実験の条件をそろえることなどに限るのがよい。