53747


# 背景

「エキスパートPythonプログラミング 改訂2版」の11.3.1章「CPU使用量のプロファイル」を読んでいます。

# 質問

文字列連結より`str.join()`の方が速いはずなのに、以下の測定では文字列連結の方が速いように見えています。  
これはなぜでしょうか？

[code]

    $ python3 -m timeit -s 'a=map(str, range(1000))' '"".join(a)'
    1000000 loops, best of 3: 0.124 usec per loop
    
    $ python3 -m timeit -s 'a=map(str, range(1000)); s=""' 'for i in a: s+=i'
    10000000 loops, best of 3: 0.0295 usec per loop
    
[/code]

本書には「読書への課題」となっていましたが分からなかったので、ここで質問しました。  
本書には、以下の通り記載されていました。

> 落とし穴は`setup`引数の使い方と、Python3における`map`の振る舞いにあります。

# 環境

  * Python 3.6.6


CC BY-SA 4.0
2019-03-26T13:22:14.350
53743
2019-03-27T03:34:40.467
2019-03-26T23:35:49.873
9820
19524
question
python
timeitモジュールで測定すると、`str.join()`より文字列連結が速い結果になってしまう理由（エキスパートPythonプログラミング より）


まずは中で何が起こっているのか把握するために、`print`文で覗いてみましょう。

そのまま実行すると1万や10万回`print`文が実行されるので、[`number引数(-n,
--number)`](https://docs.python.org/ja/3/library/timeit.html)で回数を制限します。  
ついでに`repeat引数(-r, --repeat)`で試行回数(`best of r`の`r`に該当する回数)も減らしておきます。

[code]

    PS C:\Users\payaneco> python -m timeit -r 1 -n 3 -s "a=map(str, range(1000))" "s=''" "for i in a: s+=i;" "print(len(s))" 
    2890
    0
    0
    3 loops, best of 1: 211 usec per loop
    PS C:\Users\payaneco> python -m timeit -r 1 -n 3 -s "a=map(str, range(1000))" "s=''.join(a)" "print(len(s))" 
    2890
    0
    0
    3 loops, best of 1: 168 usec per loop
    
[/code]

`len(s)`で文字列の長さを計ったところ、2回目、3回目は0になってしまいました！

ここで本のヒントを読み解きます。

> 落とし穴はsetup引数の使い方と、Python3におけるmapの振る舞いにあります。

試しに[setup引数](https://docs.python.org/ja/3/library/timeit.html#module-
timeit)(-s --setup)を消してみましょう。

[code]

    PS C:\Users\payaneco> python -m timeit -r 1 -n 3 "a=map(str, range(1000))" "s=''" "for i in a: s+=i;" "print(len(s))" 
    2890
    2890
    2890
    3 loops, best of 1: 378 usec per loop
    PS C:\Users\payaneco> python -m timeit -r 1 -n 3 "a=map(str, range(1000))" "s=''.join(a)" "print(len(s))" 
    2890
    2890
    2890
    3 loops, best of 1: 257 usec per loop
    
[/code]

欲しい結果が得られました。  
`len(s)`は常に正しい値を返し、速度も`for`文より`join`の方が速くなりました。  
ただし`setup引数`がある時よりも速度は遅いです。

`setup引数`は最初のステートメントに適用され、`map`関数は初めの1回のみ初期化されて、2,3回目には初期化されません。  
逆に`setup引数`が無ければ毎回`a=map(str, range(1000))`が実行されるので、その分遅くなります。

ようやくpython3における[`map`](https://docs.python.org/ja/3/library/functions.html#map)とは何かという話が関係してきます。  
`map`とは配列に関数を適用して[`イテレータ`](https://docs.python.org/ja/3/howto/functional.html#iterators)を返す高階関数です。  
ここで言う「配列」とは`range(1000)`で作成した0..999の数値です。  
ここで言う「関数」とは`str`です。n=0..999の数値を`str(n)`で文字列化しています。

さてpythonのイテレータは、`for`文や`next()`関数で次の値を取得できますが、いわゆる`seek`や`prev`のように特定の場所へアクセスしたり、前の値を取得したりすることはできません。  
つまり一度`for`や`join`でアクセスすると、また先頭から読み取ることができないのです。

pythonで下記のコードを実行すると、1回目は2890が取得できますが、2回目は0になってしまいます。  
これが`map`というかイテレータの振る舞いです。

[code]

    a=map(str, range(1000))
    s=''.join(a)
    print(len(s))
    # 2890
    s=''.join(a)
    print(len(s))
    # 0
    
[/code]

ちなみに`map`を`list`化すればイテレータではなくリストになるので、正しい値を取得することができます。

[code]

    PS C:\Users\payaneco> python -m timeit -s "a=list(map(str, range(1000)))" "s=''.join(a)" 
    100000 loops, best of 3: 10.3 usec per loop
    PS C:\Users\payaneco> python -m timeit -s "a=list(map(str, range(1000)))" "s=''" "for i in a: s+=i;" 
    10000 loops, best of 3: 176 usec per loop
    
[/code]

速度も`join`の方が圧倒的に速くなりました！

おそらくですが、使い終わったイテレータに`for`を実行してもループ冒頭の1回目で中断しますが、0件のイテレータに`join`を掛けるとオーバーヘッドが発生して`join`の方が遅くなると推測されます。

以上のことから、以下の「読者への課題」を理解しないと正しい結果が得られません。

  * 毎回setupしないとイテレータを再利用できない
  * mapはイテレータを作るだけなので、リストや配列にしないと罠にハマることがある


CC BY-SA 4.0
2019-03-27T01:08:47.533
53747
2019-03-27T03:34:40.467
2019-03-27T03:34:40.467
9820
9820
53743
answer