Simple Random Character Function の比較

口内炎が治らないbonlifeです。こちらで紹介されていたランダムな英数字の文字列を生成する方法が気になったので、それに関してアレコレしてみたメモです。

  • random_chars
def random_chars(length):
    import random
    allowed_chars = "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0123456789"
    word = ""
    for i in xrange(0, length):
        word = word + allowed_chars[random.randint(0,0xffffff) % len(allowed_chars)]
    return word

確かに想定した通りの動きをするのですが、ランダムに文字列を選ぶ部分がちょっと意味不明です。まず、 0xffffff なんて数字を持ち出す必要が全くないです。また、randomモジュールの randint() は、内部的には randrange() を呼び出してるだけなので、大きい方の値が含まれるかどうかをちゃんと意識して randrange() を使う方がいくらか速いはずです。そのあたりの情報についてはこちらを参照あれ。
と思っていたら、最初のページのコメント部分に「まさに!」なコードが記載されてました。(くっそう、第一発見者かと思ったのにぃ。) 以下の通りです。

  • random_chars2
def random_chars2(length):
    import random
    allowed_chars = "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0123456789"
    word = ""
    for i in xrange(0,length):
        word = word + allowed_chars[random.randrange(0,len(allowed_chars))]
    return word

だいぶスッキリしました。でも、もうちょっと改善の余地がありそうです。というのも、この関数の中では文字列の連結が何度も行われる可能性があります。文字列連結は意外とコストがかかるらしいですよ。

ということで、その部分をリストを生成して、最後にリストを join() で結合する方法に変えてみたのが3つ目のサンプルです。(個人的には、allowed_chars は string.letter + string.digits といった感じで定数を使うところですが、stringモジュールをインポートするオーバーヘッドが小さくなさそうなので今回ばかりは使わない方向で。)

  • random_chars3
def random_chars3(length):
    import random
    allowed_chars = "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWZYZ0123456789"
    words = []
    for i in xrange(0,length):
        words.append(allowed_chars[random.randrange(0,len(allowed_chars))])
    return "".join(words)

こうやって3つのサンプルスクリプトが出来ると比較してみたくなるのが男心。以下のような関数を何度も繰り返す処理時間計測用の簡易関数を作って実験です。

  • measure_time
def measure_time(f,cols,times):
    import time
    start = time.clock()
    for i in xrange(times):
        f(cols)
    return time.clock() - start

実験の結果は以下のようになりました。

In [22]: measure_time(random_chars,1000000,10)
Out[22]: 117.09026361306195

In [23]: measure_time(random_chars2,1000000,10)
Out[23]: 84.046407792891728

In [24]: measure_time(random_chars3,1000000,10)
Out[24]: 78.230117112030371

In [25]: measure_time(random_chars,10,1000000)
Out[25]: 103.63933225250571

In [26]: measure_time(random_chars2,10,1000000)
Out[26]: 88.459748436921927

In [27]: measure_time(random_chars3,10,1000000)
Out[27]: 105.81907706563288

100万文字をランダムに取得して結合するのを10回やった場合、リスト生成 → join() の random_chars3 が一番速いですね。10文字の結合を100万回やった場合、random_chars2 が一番速いです。この結果をどう受け止めれば良いのか難しいところですね。スケーラブルな設計にしておくことは大切ですが、この程度の差だったら気にしなくても良いのかも。
ちなみに、最初に処理時間計測用の関数を以下のように書いて、想定していた結果が出ず、コノヤロー!って気分になりました。

  • measure_time_wrong
def measure_time_wrong(f,times):
    import time
    start = time.clock()
    for i in xrange(times):
        f
    return time.clock() - start

この関数を使って、 measure_time_wrong(random_chars(1000000),10) などとやると、おそろしく小さい数字が返ってきます。色々と試す中で気付いたのですが、random_chars() の戻り値を処理しちゃってますね。体感的には最初に random_chars() を処理する時間がだいぶかかるので、時間かかってるなぁ…という印象なのに、この関数内では、1つの戻り値をただただ f って何度も評価してるだけなので、処理時間はごくごく小さいという罠。(罠でもなんでもないですが。)
で、何が言いたいのかと言うと、「他人のソースコードを疑って色々と試してみると、思いもよらないところで発見があるかも!」ってこと。素人なbonlifeは、今後も攻めの姿勢を失わずに色々と試していきます!(たぶん)