できない.dev

配列からランダムに1つ選ぶには

配列やリストから要素を 1 つランダムに選ぶ方法を各言語で示す。
添字を乱数で作る書き方の落とし穴と、空の配列を渡したときの挙動が言語ごとに違う点を扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

import random
 
fruits = ["apple", "banana", "cherry", "date"]
print(random.choice(fruits))        # 例: cherry
print(random.sample(fruits, k=2))   # 例: ['date', 'apple'](重複なしで 2 個)
print(random.choices(fruits, k=3))  # 例: ['banana', 'banana', 'apple'](重複を許して 3 個)
 
try:
    random.choice([])
except IndexError as e:
    print(type(e).__name__, e)      # IndexError Cannot choose from an empty sequence

random.choice はシーケンスから 1 要素を等確率で選ぶ。
重複なしで複数選ぶなら random.sample、重複を許すなら random.choices を使う。
空のシーケンスを渡すと IndexError になるので、空かもしれないリストは先に長さを確かめる。

JavaScript

function pick(items) {
  if (items.length === 0) {
    throw new RangeError("空の配列からは選べない");
  }
  return items[Math.floor(Math.random() * items.length)];
}
 
const fruits = ["apple", "banana", "cherry", "date"];
console.log(pick(fruits)); // 例: cherry
 
// ガードが無いと、空の配列でもエラーにならず undefined が返る
const empty = [];
console.log(empty[Math.floor(Math.random() * empty.length)]); // undefined

標準に 1 要素を選ぶ関数は無いため、Math.random() に長さを掛けて切り捨てた値を添字にする。
Math.random() は 0 以上 1 未満なので、添字は 0 から長さ−1 の範囲に収まる。
空の配列でも例外にならず undefined が返るので、必要なら先に長さを確かめる。

TypeScript

// 空の配列では選べないので、戻り値の型に undefined を含める
function pick<T>(items: readonly T[]): T | undefined {
  return items[Math.floor(Math.random() * items.length)];
}
 
const fruits: string[] = ["apple", "banana", "cherry", "date"];
const fruit = pick(fruits); // 型は string | undefined
console.log(fruit?.toUpperCase()); // 例: CHERRY
 
console.log(pick([])); // undefined

空の配列では選べないため、戻り値の型を T | undefined にして呼び出し側に空の場合を処理させる。
型にしておくと、fruit.toUpperCase() のように確認なしで使ったときに 'fruit' is possibly 'undefined' というコンパイルエラーで気づける。

できないときTypeScript「Object is possibly 'null'」の原因と直し方

Go

package main
 
import (
	"fmt"
	"math/rand/v2"
)
 
func main() {
	fruits := []string{"apple", "banana", "cherry", "date"}
	fmt.Println(fruits[rand.IntN(len(fruits))]) // 例: cherry
 
	var empty []string
	defer func() {
		fmt.Println("recovered:", recover()) // recovered: invalid argument to IntN
	}()
	fmt.Println(empty[rand.IntN(len(empty))]) // IntN(0) で panic する
}

math/rand/v2 の rand.IntN(n) は 0 以上 n 未満の整数を返すので、len を渡してスライスの添字に使う。
n が 0 以下だと panic するため、空のスライスかもしれないときは先に len を確かめる。

Rust

// Cargo.toml: rand = "0.10"
use rand::seq::IndexedRandom;
 
fn main() {
    let fruits = vec!["apple", "banana", "cherry", "date"];
    let mut rng = rand::rng();
 
    println!("{:?}", fruits.choose(&mut rng)); // 例: Some("cherry")
 
    let empty: Vec<&str> = Vec::new();
    println!("{:?}", empty.choose(&mut rng)); // None(空でも panic しない)
}

rand クレートの IndexedRandom トレイトを use すると、スライスとベクタで choose メソッドが使える。
戻り値は Option で、空なら None になるので、添字の範囲外で panic することがない。

Java

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
 
public class Main {
    public static void main(String[] args) {
        List<String> fruits = List.of("apple", "banana", "cherry", "date");
        String fruit = fruits.get(ThreadLocalRandom.current().nextInt(fruits.size()));
        System.out.println(fruit); // 例: cherry
 
        try {
            List<String> empty = List.of();
            empty.get(ThreadLocalRandom.current().nextInt(empty.size()));
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage()); // bound must be positive
        }
    }
}

標準には 1 要素を選ぶメソッドが無いため、ThreadLocalRandom の nextInt(size) で添字を作って get に渡す。
nextInt は引数が 0 以下だと IllegalArgumentException を投げるので、空のリストでは添字の範囲外より前に失敗する。

C#

using System;
 
var fruits = new[] { "apple", "banana", "cherry", "date" };
Console.WriteLine(fruits[Random.Shared.Next(fruits.Length)]); // 例: cherry
 
// .NET 8 以降。重複を許して 2 個選ぶ
Console.WriteLine(string.Join(", ", Random.Shared.GetItems(fruits, 2))); // 例: date, date
 
var empty = Array.Empty<string>();
try
{
    Console.WriteLine(empty[Random.Shared.Next(empty.Length)]);
}
catch (IndexOutOfRangeException e)
{
    // Next(0) はエラーにならず 0 を返すので、添字の範囲外で初めて失敗する
    Console.WriteLine(e.GetType().Name); // IndexOutOfRangeException
}

Random.Shared.Next(長さ) で 0 以上長さ未満の添字を作る。
.NET 8 以降の GetItems は、重複を許して複数の要素を選ぶときに使える。
Next(0) は例外にならず 0 を返すため、空の配列では添字の範囲外で初めて失敗する。

Math.round で添字を作ると端が出にくい

JavaScript では Math.round(Math.random() * (長さ - 1)) という書き方も見かけるが、これは偏る。
5 要素の配列で 1,000,000 回選んで添字ごとの割合を数えた(Node.js 24.20.0)。

  • Math.floor(Math.random() * 長さ) は、5 つの添字がすべて 20.0〜20.1% だった。
  • Math.round(Math.random() * (長さ - 1)) は、先頭と末尾が 12.5%、真ん中の 3 つが 24.9〜25.1% だった。

round は 0 と 4 の周りが 0.5 幅しかなく、1〜3 の周りは 1 幅あるため、端は半分の確率でしか選ばれない。
添字には floor を使う。

掛ける数を 1 つ間違えると undefined が混ざる

Math.random() * (長さ + 1) のように 1 大きい数を掛けると、添字が長さと同じ値になることがある。
5 要素の配列で 1,000,000 回試すと、約 16.7%(6 通りのうち 1 通り)で範囲外の添字が出た。
JavaScript は範囲外を読んでも例外にならず undefined になるため、気づきにくい。
Python の random.choice のように範囲の計算を任せられる関数があれば、そちらを使うほうが安全である。

空の配列を渡したときは言語ごとに違う

空の配列から選ぼうとしたときの結果を各言語で実行して確かめた。

  • Python の random.choice([]) は IndexError(Cannot choose from an empty sequence)になる。
  • JavaScript は例外にならず undefined が返る。
  • Go の rand.IntN(0) は panic する(invalid argument to IntN)。
  • Rust の choose は None を返す。
  • Java の nextInt(0) は IllegalArgumentException(bound must be positive)になる。
  • C# の Next(0) は 0 を返し、その後の添字アクセスで IndexOutOfRangeException になる。

空になりうる配列から選ぶときは、呼び出し側で空かどうかを先に確かめるか、Rust のように空を型で表せる書き方を選ぶ。

賭けや認証に使うときは暗号用の乱数にする

ここで使った乱数は暗号用途向けではない。
抽選結果を推測されると困る場面、たとえば認証コードの生成や賞品の抽選では、Python なら secrets.choice のように OS の乱数を使う関数を選ぶ。