Pythonリストシャッフル完全ガイド – random.shuffle・sampleの使い分けから応用テクニックまで

 

Pythonでリストの要素をランダムに並び替える(シャッフルする)処理は、ゲーム開発、データサンプリング、機械学習のデータ前処理など、様々な場面で必要になります。Pythonにはrandom.shuffle()random.sample()という2つの主要な方法があり、それぞれ異なる特徴を持っています。この記事では、これらの関数の使い分けから実用的な応用例まで、詳しく解説します。

1. shuffle()とsample()の基本的な違い

主要な違いの比較表

関数元リストへの影響戻り値要素数重複用途
shuffle()変更されるNone同じなし元のリストをシャッフル
sample()変更されない新しいリスト指定可能なしサンプリング

2. random.shuffle()の基本的な使い方

インプレース操作によるシャッフル

import random

# 基本的なシャッフル
cards = ['A', 'K', 'Q', 'J', '10', '9', '8', '7']
random.shuffle(cards)
print(cards)  # ['Q', '7', 'A', '10', 'K', '9', 'J', '8'] (ランダム)

# 元のリストが変更されることに注意
original = [1, 2, 3, 4, 5]
print("シャッフル前:", original)
random.shuffle(original)
print("シャッフル後:", original)  # 元のリストが変更される

数値リストのシャッフル

import random

# 数値リストのシャッフル
numbers = list(range(1, 11))  # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
random.shuffle(numbers)
print("シャッフル結果:", numbers)

# 複数回シャッフル
for i in range(3):
    random.shuffle(numbers)
    print(f"シャッフル{i+1}回目:", numbers)

3. random.sample()の基本的な使い方

非破壊的なサンプリング

import random

# 元のリストを変更せずにサンプリング
original = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
shuffled = random.sample(original, len(original))

print("元のリスト:", original)      # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("シャッフル結果:", shuffled)  # [7, 2, 9, 1, 5, 3, 8, 4, 6, 10]

部分的なサンプリング

import random

# 元のリストから一部をランダムに選択
data = ['apple', 'banana', 'cherry', 'date', 'elderberry', 'fig']
sample_3 = random.sample(data, 3)
sample_4 = random.sample(data, 4)

print("元データ:", data)
print("3個サンプル:", sample_3)  # ['cherry', 'apple', 'elderberry']
print("4個サンプル:", sample_4)  # ['fig', 'banana', 'date', 'apple']

4. シード値を使った再現可能なシャッフル

固定シードによる再現性

import random

def reproducible_shuffle_demo():
    """再現可能なシャッフルの例"""
    data = [1, 2, 3, 4, 5]
    
    # 同じシードで同じ結果を得る
    random.seed(42)
    shuffled1 = random.sample(data, len(data))
    
    random.seed(42)
    shuffled2 = random.sample(data, len(data))
    
    print("1回目:", shuffled1)
    print("2回目:", shuffled2)
    print("同じ結果:", shuffled1 == shuffled2)  # True

reproducible_shuffle_demo()

テスト用の固定シャッフル

import random

class TestableShuffler:
    def __init__(self, seed=None):
        self.seed = seed
    
    def shuffle_list(self, data):
        if self.seed is not None:
            random.seed(self.seed)
        return random.sample(data, len(data))

# テストで使用
shuffler = TestableShuffler(seed=123)
test_data = [1, 2, 3, 4, 5]
result = shuffler.shuffle_list(test_data)
print("テスト結果:", result)  # 常に同じ結果

5. 大量データの効率的なシャッフル

パフォーマンス比較

import random
import time

def benchmark_shuffle_methods(size=100000):
    """シャッフル手法のパフォーマンス比較"""
    
    # テストデータ準備
    data = list(range(size))
    
    # shuffle()のテスト
    test_data1 = data.copy()
    start = time.time()
    random.shuffle(test_data1)
    shuffle_time = time.time() - start
    
    # sample()のテスト
    start = time.time()
    shuffled_sample = random.sample(data, len(data))
    sample_time = time.time() - start
    
    print(f"データサイズ: {size:,}")
    print(f"shuffle(): {shuffle_time:.4f}秒")
    print(f"sample(): {sample_time:.4f}秒")
    print(f"差: {abs(shuffle_time - sample_time):.4f}秒")

benchmark_shuffle_methods(100000)

メモリ効率的なシャッフル

import random

def memory_efficient_shuffle(data, chunk_size=1000):
    """メモリ効率的な大量データシャッフル"""
    # インデックスのリストを作成してシャッフル
    indices = list(range(len(data)))
    random.shuffle(indices)
    
    # チャンク単位で処理
    for i in range(0, len(indices), chunk_size):
        chunk_indices = indices[i:i + chunk_size]
        yield [data[idx] for idx in chunk_indices]

# 使用例
large_data = list(range(10000))
for chunk in memory_efficient_shuffle(large_data, 1000):
    print(f"チャンクサイズ: {len(chunk)}, 最初の3要素: {chunk[:3]}")
    break  # 最初のチャンクのみ表示

6. 文字列のシャッフル

文字列をシャッフルする方法

import random

def shuffle_string(text):
    """文字列をシャッフル"""
    chars = list(text)
    random.shuffle(chars)
    return ''.join(chars)

def shuffle_string_sample(text):
    """sample()を使った文字列シャッフル"""
    chars = list(text)
    shuffled_chars = random.sample(chars, len(chars))
    return ''.join(shuffled_chars)

# 使用例
original_text = "Hello World"
shuffled1 = shuffle_string(original_text)
shuffled2 = shuffle_string_sample(original_text)

print(f"元の文字列: {original_text}")
print(f"シャッフル1: {shuffled1}")
print(f"シャッフル2: {shuffled2}")

単語単位でのシャッフル

import random

def shuffle_words(sentence):
    """文の単語をシャッフル"""
    words = sentence.split()
    random.shuffle(words)
    return ' '.join(words)

def shuffle_words_sample(sentence):
    """sample()を使った単語シャッフル"""
    words = sentence.split()
    shuffled_words = random.sample(words, len(words))
    return ' '.join(shuffled_words)

text = "Python is a powerful programming language"
print(f"元の文: {text}")
print(f"単語シャッフル: {shuffle_words(text)}")

7. ゲーム開発での応用

トランプカードのシャッフル

import random

class Deck:
    def __init__(self):
        suits = ['♠', '♥', '♦', '♣']
        ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
        self.cards = [f"{rank}{suit}" for suit in suits for rank in ranks]
        self.original_deck = self.cards.copy()
    
    def shuffle(self):
        """デッキをシャッフル"""
        random.shuffle(self.cards)
    
    def deal(self, num_cards=1):
        """カードを配る"""
        if len(self.cards) < num_cards:
            return None
        dealt = self.cards[:num_cards]
        self.cards = self.cards[num_cards:]
        return dealt
    
    def reset(self):
        """デッキをリセット"""
        self.cards = self.original_deck.copy()

# 使用例
deck = Deck()
print(f"デッキサイズ: {len(deck.cards)}")
deck.shuffle()
hand = deck.deal(5)
print(f"配られたカード: {hand}")
print(f"残りカード数: {len(deck.cards)}")

クイズ問題のシャッフル

import random

class QuizShuffler:
    def __init__(self, questions):
        self.questions = questions
        self.original_order = questions.copy()
    
    def shuffle_questions(self):
        """問題順序をシャッフル"""
        return random.sample(self.questions, len(self.questions))
    
    def shuffle_options(self, question_dict):
        """選択肢をシャッフル"""
        if 'options' in question_dict:
            question_dict['options'] = random.sample(
                question_dict['options'], 
                len(question_dict['options'])
            )
        return question_dict
    
    def create_random_quiz(self, num_questions=None):
        """ランダムクイズを作成"""
        if num_questions is None:
            num_questions = len(self.questions)
        
        selected = random.sample(self.questions, 
                                min(num_questions, len(self.questions)))
        
        # 各問題の選択肢もシャッフル
        for question in selected:
            self.shuffle_options(question)
        
        return selected

# 使用例
questions = [
    {"q": "Pythonの作者は?", "options": ["Guido", "Larry", "Dennis"], "answer": "Guido"},
    {"q": "1+1=?", "options": ["1", "2", "3"], "answer": "2"},
    {"q": "Python は何年にリリース?", "options": ["1989", "1991", "1995"], "answer": "1991"}
]

quiz = QuizShuffler(questions)
random_quiz = quiz.create_random_quiz(2)
for i, q in enumerate(random_quiz, 1):
    print(f"{i}. {q['q']}")
    print(f"   選択肢: {q['options']}")

8. データサイエンス・機械学習での応用

データセットのシャッフル

import random

def shuffle_dataset(features, labels):
    """特徴量とラベルを同時にシャッフル"""
    # インデックスを作成してシャッフル
    indices = list(range(len(features)))
    random.shuffle(indices)
    
    shuffled_features = [features[i] for i in indices]
    shuffled_labels = [labels[i] for i in indices]
    
    return shuffled_features, shuffled_labels

# サンプルデータ
features = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
labels = ['A', 'B', 'C', 'D', 'E']

shuffled_X, shuffled_y = shuffle_dataset(features, labels)
print("シャッフル後の特徴量:", shuffled_X)
print("シャッフル後のラベル:", shuffled_y)

k-分割交差検証用のシャッフル

import random

def create_k_folds(data, k=5, shuffle=True):
    """k-分割交差検証用のデータ分割"""
    if shuffle:
        data = random.sample(data, len(data))
    
    fold_size = len(data) // k
    folds = []
    
    for i in range(k):
        start = i * fold_size
        if i == k - 1:  # 最後のfoldは残り全部
            end = len(data)
        else:
            end = (i + 1) * fold_size
        
        folds.append(data[start:end])
    
    return folds

# 使用例
dataset = list(range(100))
folds = create_k_folds(dataset, k=5, shuffle=True)
for i, fold in enumerate(folds):
    print(f"Fold {i+1}: {len(fold)}個のサンプル, 最初の5個: {fold[:5]}")

9. A/Bテストでのランダム化

ユーザーのランダム振り分け

import random

class ABTestRandomizer:
    def __init__(self, variants=['A', 'B'], weights=None):
        self.variants = variants
        self.weights = weights or [1] * len(variants)
    
    def assign_users(self, user_ids):
        """ユーザーをランダムにバリアントに振り分け"""
        # ユーザーIDをシャッフル
        shuffled_users = random.sample(user_ids, len(user_ids))
        
        assignments = {}
        total_weight = sum(self.weights)
        
        for i, user_id in enumerate(shuffled_users):
            # 重み付きランダム選択
            rand_val = random.random() * total_weight
            cumulative = 0
            
            for variant, weight in zip(self.variants, self.weights):
                cumulative += weight
                if rand_val <= cumulative:
                    assignments[user_id] = variant
                    break
        
        return assignments
    
    def get_assignment_summary(self, assignments):
        """振り分け結果のサマリー"""
        summary = {}
        for variant in self.variants:
            summary[variant] = sum(1 for v in assignments.values() if v == variant)
        return summary

# 使用例
user_ids = [f"user_{i:03d}" for i in range(100)]
ab_test = ABTestRandomizer(['A', 'B'], weights=[3, 7])  # A:B = 3:7

assignments = ab_test.assign_users(user_ids)
summary = ab_test.get_assignment_summary(assignments)

print("振り分け結果:")
for variant, count in summary.items():
    print(f"バリアント{variant}: {count}人 ({count/len(user_ids)*100:.1f}%)")

10. カスタムシャッフル関数の実装

重み付きシャッフル

import random

def weighted_shuffle(items, weights):
    """重み付きシャッフル"""
    if len(items) != len(weights):
        raise ValueError("itemsとweightsの長さが一致しません")
    
    # 重みに基づいてアイテムを選択
    result = []
    items_copy = items.copy()
    weights_copy = weights.copy()
    
    while items_copy:
        # 重み付きランダム選択
        total_weight = sum(weights_copy)
        rand_val = random.random() * total_weight
        cumulative = 0
        
        for i, weight in enumerate(weights_copy):
            cumulative += weight
            if rand_val <= cumulative:
                result.append(items_copy.pop(i))
                weights_copy.pop(i)
                break
    
    return result

# 使用例
items = ['A', 'B', 'C', 'D', 'E']
weights = [1, 2, 3, 4, 5]  # Eが最も選ばれやすい

shuffled = weighted_shuffle(items, weights)
print(f"重み付きシャッフル結果: {shuffled}")

条件付きシャッフル

import random

def conditional_shuffle(data, condition_func, shuffle_ratio=0.5):
    """条件に基づく部分的シャッフル"""
    # 条件に合う要素と合わない要素を分離
    matching = [item for item in data if condition_func(item)]
    non_matching = [item for item in data if not condition_func(item)]
    
    # 指定した割合でシャッフル
    num_to_shuffle = int(len(matching) * shuffle_ratio)
    
    if num_to_shuffle > 0:
        # シャッフルする要素としない要素を選択
        to_shuffle = random.sample(matching, num_to_shuffle)
        to_keep = [item for item in matching if item not in to_shuffle]
        
        # シャッフル実行
        random.shuffle(to_shuffle)
        
        # 結合
        result = to_keep + to_shuffle + non_matching
        random.shuffle(result)  # 最終的な配置をランダム化
    else:
        result = data.copy()
        random.shuffle(result)
    
    return result

# 使用例:偶数のみ50%をシャッフル
numbers = list(range(1, 21))
result = conditional_shuffle(numbers, lambda x: x % 2 == 0, 0.5)
print(f"条件付きシャッフル結果: {result}")

11. エラーハンドリングと安全な実装

安全なシャッフル関数

import random

def safe_shuffle(data, method='shuffle', sample_size=None):
    """エラーハンドリングを含む安全なシャッフル"""
    try:
        if not data:
            return [] if method == 'sample' else None
        
        if not hasattr(data, '__len__'):
            data = list(data)
        
        if method == 'shuffle':
            if not isinstance(data, list):
                raise TypeError("shuffle()にはlistが必要です")
            
            result = data.copy()
            random.shuffle(result)
            return result
        
        elif method == 'sample':
            if sample_size is None:
                sample_size = len(data)
            
            if sample_size > len(data):
                raise ValueError("sample_sizeがデータサイズを超えています")
            
            return random.sample(data, sample_size)
        
        else:
            raise ValueError(f"未知のメソッド: {method}")
    
    except Exception as e:
        print(f"シャッフルエラー: {e}")
        return data if method == 'shuffle' else []

# テスト
test_cases = [
    ([1, 2, 3, 4, 5], 'shuffle', None),
    ([1, 2, 3, 4, 5], 'sample', 3),
    ([], 'shuffle', None),
    ("hello", 'sample', 3),
    ([1, 2], 'sample', 5)  # エラーケース
]

for data, method, size in test_cases:
    result = safe_shuffle(data, method, size)
    print(f"{data} -> {result}")

12. パフォーマンス最適化

大規模データの最適化

import random
import time

class OptimizedShuffler:
    @staticmethod
    def fisher_yates_shuffle(data):
        """Fisher-Yatesアルゴリズムによる効率的シャッフル"""
        result = data.copy()
        for i in range(len(result) - 1, 0, -1):
            j = random.randint(0, i)
            result[i], result[j] = result[j], result[i]
        return result
    
    @staticmethod
    def chunked_shuffle(data, chunk_size=10000):
        """チャンク単位での大規模データシャッフル"""
        # データを小さなチャンクに分割
        chunks = [data[i:i + chunk_size] for i in range(0, len(data), chunk_size)]
        
        # 各チャンクをシャッフル
        shuffled_chunks = []
        for chunk in chunks:
            random.shuffle(chunk)
            shuffled_chunks.append(chunk)
        
        # チャンクの順序もシャッフル
        random.shuffle(shuffled_chunks)
        
        # 結合
        result = []
        for chunk in shuffled_chunks:
            result.extend(chunk)
        
        return result
    
    @staticmethod
    def benchmark_methods(size=100000):
        """各シャッフル手法のベンチマーク"""
        data = list(range(size))
        
        methods = {
            'random.shuffle': lambda d: random.shuffle(d.copy()) or d,
            'random.sample': lambda d: random.sample(d, len(d)),  
            'fisher_yates': OptimizedShuffler.fisher_yates_shuffle,
            'chunked': OptimizedShuffler.chunked_shuffle
        }
        
        results = {}
        for name, method in methods.items():
            start = time.time()
            method(data)
            results[name] = time.time() - start
        
        return results

# ベンチマーク実行
shuffler = OptimizedShuffler()
benchmark_results = shuffler.benchmark_methods(50000)

print("シャッフル手法のパフォーマンス比較:")
for method, time_taken in sorted(benchmark_results.items(), key=lambda x: x[1]):
    print(f"{method:15}: {time_taken:.4f}秒")

13. 実用的なユーティリティ関数

多目的シャッフルツール

import random
from typing import List, Any, Optional, Union

class ShuffleUtils:
    @staticmethod
    def multi_list_shuffle(*lists):
        """複数のリストを同期してシャッフル"""
        if not lists:
            return []
        
        # 全てのリストが同じ長さかチェック
        length = len(lists[0])
        if not all(len(lst) == length for lst in lists):
            raise ValueError("全てのリストの長さが同じである必要があります")
        
        # インデックスをシャッフル
        indices = list(range(length))
        random.shuffle(indices)
        
        # 各リストを同じ順序でシャッフル
        return [[lst[i] for i in indices] for lst in lists]
    
    @staticmethod
    def preserve_groups_shuffle(data, group_key_func):
        """グループを保持したままシャッフル"""
        # グループ別にデータを分類
        groups = {}
        for item in data:
            key = group_key_func(item)
            if key not in groups:
                groups[key] = []
            groups[key].append(item)
        
        # 各グループ内をシャッフル
        for group in groups.values():
            random.shuffle(group)
        
        # グループ順序もシャッフル
        group_keys = list(groups.keys())
        random.shuffle(group_keys)
        
        # 結合
        result = []
        for key in group_keys:
            result.extend(groups[key])
        
        return result
    
    @staticmethod
    def smart_shuffle(data, seed=None, preserve_order=False):
        """インテリジェントシャッフル"""
        if seed is not None:
            random.seed(seed)
        
        if preserve_order:
            # 元の順序情報を保持
            indexed_data = list(enumerate(data))
            random.shuffle(indexed_data)
            return [(original_idx, item) for original_idx, item in indexed_data]
        else:
            return random.sample(data, len(data))

# 使用例
utils = ShuffleUtils()

# 複数リストの同期シャッフル
names = ['Alice', 'Bob', 'Charlie']
ages = [25, 30, 35]
scores = [85, 90, 95]

shuffled_lists = utils.multi_list_shuffle(names, ages, scores)
print("同期シャッフル結果:")
for name, age, score in zip(*shuffled_lists):
    print(f"{name}: {age}歳, {score}点")

# グループ保持シャッフル
students = [
    {'name': 'Alice', 'grade': 'A', 'score': 95},
    {'name': 'Bob', 'grade': 'B', 'score': 85},
    {'name': 'Charlie', 'grade': 'A', 'score': 92},
    {'name': 'David', 'grade': 'B', 'score': 88}
]

grouped_shuffled = utils.preserve_groups_shuffle(
    students, 
    lambda x: x['grade']
)
print("\nグループ保持シャッフル:")
for student in grouped_shuffled:
    print(f"{student['name']}: {student['grade']}グレード")

まとめ

Pythonでのリストシャッフルには、用途に応じて適切な手法を選択することが重要です:

使い分けの指針

  1. random.shuffle() – 元のリストを直接変更したい場合(最も高速・省メモリ)
  2. random.sample() – 元のリストを保持したい場合、部分サンプリングが必要な場合
  3. カスタム実装 – 特殊な要件(重み付き、条件付きなど)がある場合

パフォーマンス特性

  • 速度: shuffle() > sample() > カスタム実装
  • メモリ: shuffle() > sample() > カスタム実装
  • 柔軟性: カスタム実装 > sample() > shuffle()

推奨用途

  • ゲーム開発: カードシャッフル、問題順序のランダム化
  • データサイエンス: データセットのシャッフル、交差検証
  • A/Bテスト: ユーザーのランダム振り分け
  • セキュリティ: 暗号学的用途(適切な乱数生成器使用時)

適切な手法を選択し、エラーハンドリングやパフォーマンスも考慮することで、効率的で保守性の高いコードを実現できます。

■プロンプトだけでオリジナルアプリを開発・公開してみた!!

■AI時代の第一歩!「AI駆動開発コース」はじめました!

テックジム東京本校で先行開始。

■テックジム東京本校

「武田塾」のプログラミング版といえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
より早く、より安く、しかも対面型のプログラミングスクールです。

<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。

<月1開催>放送作家による映像ディレクター養成講座

<オンライン無料>ゼロから始めるPython爆速講座