静かなる名辞

pythonとプログラミングのこと


【python】random.shuffleについて

はじめに

 標準モジュールのrandom.shuffleは直感と違う挙動をするので、メモしておきます。

 参考:
9.6. random — 擬似乱数を生成する — Python 3.6.5 ドキュメント

問題のコード

 このようなコードです。

import random
lst = list(range(10))
shuffled = random.shuffle(lst)
for x in shuffled:
    print(x)

 いかにも人畜無害に見えるコードですが、実行すると「TypeError: 'NoneType' object is not iterable」を吐いてくれます。

 どうやらrandom.shuffle()はNoneを返すらしい。どういうことだろう? ああ、なんかなんとなく想像はできるけど・・・。

正しいやり方

import random
lst = list(range(10))
random.shuffle(lst)
for x in lst:
    print(x)
""" =>
1
9
5
3
7
0
4
2
8
6
"""

 random.shuffleはいわゆる「破壊的操作」で、listの参照を受け取って(pythonのオブジェクトはいわゆる参照で渡されるから)、その参照先をシャッフルします。Cかよ。

 ともかく、こいつは副作用のない、お行儀の良い関数じゃないのです。pythonは『マルチパラダイム言語』なので、そしてその『マルチパラダイム』の中にはオールドファッションな手続き型も入ってると思うので、こういうものが標準ライブラリの中にあっても怒ってはいけません。

この挙動が嫌なとき

 random.shuffleは引数のオブジェクトを直接変更するので、シャッフル前の情報は残してくれません。

 元のリストも取っておきたいときは自前でやる必要があります。

 これにはいろいろな方法があります。

  • list(lst)でオブジェクトを新しく生成する
  • lst[:]のように空のスライスを使う
  • lst.copy()のようにlist.copy()メソッドを使う

 どれが優れているとかは特にないと思いますが、lst.copy()で書いてみることにします。

import random

lst = list(range(10))
shuffled = lst.copy()
random.shuffle(shuffled)

for x, y in zip(lst, shuffled):
    print(x, y)
""" =>
0 0
1 3
2 8
3 6
4 4
5 9
6 1
7 7
8 5
9 2

"""

 shuffled = lst.copy()の時点ではシャッフルされていないという気持ち悪さがある。変数名には改善の余地があります。

 ちなみに、random.shuffle(lst.copy())としてもちゃんと動きますが、動き終わった後に結果のlistがゴミ集めに回収されていくだけなので無意味です。

まとめ

 知っていればハマりませんが、知らないと戸惑います。