【Python】リスト等のシーケンスをランダムにシャッフルする(random.shuffle, random.sample)

Python モジュール

Python標準ライブラリのrandomモジュールには、リストや文字列などのシーケンス型をランダムにシャッフルする関数があります。

  • リストをシャッフル: random.shuffle()
  • シーケンスまたは集合をシャッフル:random.sample()

本記事ではこれらの関数の違いおよび使い方について具体例を用いて解説します。また、shuffle()については第二引数randomの便利な使い方についても紹介します。

リストの各要素をシャッフル:random.shuffle()

基本の使い方

random.shuffle() は、リストの要素をランダムにシャッフルする関数です。
下記の通り、シャッフルしたいミュータブルなシーケンス型 (リスト)を引数xに設定します。

random.shuffle(x[, random])

インプレースで処理されるので、元のデータは新しいものに置き換わります
戻り値はNoneです。

具体例を示します。

# リストaを定義
>>> a = ['a', 'b', 'c', 'd', 'e']

# 戻り値はNoneです。
>>> print(random.shuffle(a))
None

# 元のシーケンスaが置き換わっています
>>> random.shuffle(a)
>>> a
['d', 'b', 'c', 'a', 'e']

オプション引数randaom

オプション引数randomは、

  • 引数を取らず、
  • 0.0≦n<1.0となるような浮動小数点数(float)を返す

関数を設定します。
デフォルトではrandom.random()が使われます。
(と公式リファレンスには記載がありますが、この関数のコード を見る限りgetrandbits() が呼び出されているように見えます。が、結局どちらの関数もos.urandom() というハードウエアに依存した乱数から生成されているのでそんなに変わりはないかもしれません)

>>> a = ['a', 'b', 'c', 'd', 'e']
>>> random.shuffle(a, random = random.random)
>>> a
['c', 'a', 'd', 'b', 'e']

イミュータブルなシーケンスの場合の挙動について

イミュータブル(変更不可)なシーケンス(タブル、文字列、range)の場合はTypeErrorとなります。

  • タプルの場合
  • >>> a = ('a', 'b', 'c', 'd', 'e')
    >>> random.shuffle(a)
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "/home/hibikisan/ProgramFiles/py37env/lib/python3.7/random.py", line 278, in shuffle
        x[i], x[j] = x[j], x[i]
    TypeError: 'tuple' object does not support item assignment
  • 文字列の場合
  • >>> s = 'abcdef'
    >>> random.shuffle(s)
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "/home/hibikisan/ProgramFiles/py37env/lib/python3.7/random.py", line 278, in shuffle
        x[i], x[j] = x[j], x[i]
    TypeError: 'str' object does not support item assignment
  • rangeの場合
  • >>> a = range(5)
    >>> random.shuffle(a)
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "/home/hibikisan/ProgramFiles/py37env/lib/python3.7/random.py", line 278, in shuffle
        x[i], x[j] = x[j], x[i]
    TypeError: 'range' object does not support item assignment

尚、このTypeErrorで表示されるエラーメッセージは、イミュータブルなデータを書き換えようとした時に送出されるものです。
shuffle()関数は、内部処理

x[i], x[j] = x[j], x[i]

という箇所があり、i番目の要素とランダムに選択されたj番目の要素を入れ替える処理をしています。
引数xがイミュータブルの場合は書き換えが出来ないので、上記のエラーメッセージが出ています。

ちなみに、どうしてもこの関数でイミュータブルなシーケンスをシャッフルしたい場合は、list()でリスト化する方法もあります。
下記は、タプルの場合です。かなり分かり難いですね。。

>>> a = ('a', 'b', 'c', 'd', 'e')

# リスト化
>>> b = list(a)

# リストにしたデータをシャッフル
>>> random.shuffle(b)

# 再度タプルに戻す
>>> a = tuple(b)
>>> a
('c', 'd', 'b', 'a', 'e')

このようにイミュータブルなシーケンスをシャッフルする場合は次章に述べるsample()を使うと便利です。

ランダムな並び替えを固定する

通常、ランダム値を固定するには下記のようにseedを固定します。

>>> for i in range(5):                 # for文で5回繰り返し
...     a = ['a', 'b', 'c', 'd', 'e'] # shuffle()はインプレースで書き換えるので毎回設定
...     random.seed(1)         # seedを固定
...     random.shuffle(a)
...     print(a)
... 
['c', 'd', 'e', 'a', 'b']
['c', 'd', 'e', 'a', 'b']
['c', 'd', 'e', 'a', 'b']
['c', 'd', 'e', 'a', 'b']
['c', 'd', 'e', 'a', 'b']

オプション引数randomを使うと、seed値を固定したのと同じ効果が得られます。
具体的には、下記のようにlambda式を使います。
※毎回seedを設定せずに済みますね。

# 最初にrandom.random()で乱数を生成しておく
>>> sd = random.random()
>>> sd
0.49543508709194095

>>> for i in range(5):                 # for文で5回繰り返し
...     a = ['a', 'b', 'c', 'd', 'e'] # shuffle()はインプレースで書き換えるので毎回設定
...     random.shuffle(a, lambda: sd)  # オプション引数randomを設定
...     print(a)
... 
['e', 'a', 'd', 'b', 'c']
['e', 'a', 'd', 'b', 'c']
['e', 'a', 'd', 'b', 'c']
['e', 'a', 'd', 'b', 'c']
['e', 'a', 'd', 'b', 'c']

シーケンスまたは集合をシャッフル:random.sample()

基本の使いかた

sample() において、第二引数kをシャッフルしたいシーケンスまたは集合(第一引数population)の長さに設定すると、シーケンス型(リスト、タプル、文字列、range)または集合をランダムにシャッフルすることが出来ます。

random.sample(population, k)

戻り値は、ランダムにシャッフルされた新しいリストになります。
元のシーケンスは変更されません。
※この時、元のデータ型ではなくリストで戻ることに注意!

具体的な例を以下に記載します。

  • タプルの場合
  • >>> a = ('a', 'b', 'c', 'd', 'e')
    >>> random.sample(a, len(a))
    ['a', 'd', 'e', 'b', 'c'] # ランダムに並び変わったリストが返ります。
    
    >>> a
    ('a', 'b', 'c', 'd', 'e')  # 元のデータは変更ありません
  • 文字列の場合
  • >>> s = 'abcde'
    >>> random.sample(s, len(s))
    ['b', 'a', 'e', 'd', 'c'] # ランダムに並び変わったリストが返ります。
    
    # 文字列に直したい時はjoin()を使うとよいです。
    >>> s_ = random.sample(s, len(s))
    >>> ''.join(s_)
    'cabde'
  • rangeの場合
  • >>> r = range(5)
    >>> random.sample(r, len(r))
    [0, 4, 1, 3, 2] # ランダムに並び変わったリストが返ります。
    
    >>> r
    range(0, 5)      # 元のデータは変更ありません
  • 集合の場合
  • >>> a = {'a', 'b', 'c', 'd', 'e'}
    >>> random.sample(a, len(a))
    ['e', 'c', 'd', 'b', 'a'] # ランダムに並び変わったリストが返ります。
    
    >>> a
    {'e', 'c', 'b', 'd', 'a'}  # 元のデータは変更ありません
  • もちろんリストも使えます。
  • >>> a = ['a', 'b', 'c', 'd', 'e']
    >>> random.sample(a, len(a))
    ['d', 'e', 'c', 'b', 'a'] # ランダムに並び変わったリストが返ります。
    
    >>> a
    ['a', 'b', 'c', 'd', 'e']  # 元のデータは変更ありません

参考記事)【Python】リストや文字列の要素をランダムに抽出する(random.choice, choices, sample)

shuffle()とsample()の違い

最後に、sample()とshuffle()の違いをまとめます。

引数 返り値 元のデータ
shuffle() ミュータブルなシーケンス(リスト) None 変更される
sample() シーケンス(リスト、タプル、文字列、range)および集合 ランダムに並び替えられた新しいリスト 変更されない

今回確認した環境

  • OS: Ubuntu 18.04LTS
  • Python: ver3.7.4

まとめ

randomモジュールを使ってシーケンス型(リスト、タプル、文字列、range)や集合型の要素をシャッフルする方法についてまとめました。

  • リストをシャッフルする: random.shuffle(x)
  • シーケンスまたは集合をシャッフルする: random.sample(x, len(x))

また、これらの関数の違いについても整理しました。

Learn more...

書籍でもう少し詳しく学びたい場合はこちらもどうぞ。筆者もかなり参考にさせてもらっています!

シェアする
ひびきをフォローする
Hbk project