まとめ
-
[*a, *b]の方が遅いですよ-
BUILD_LIST_UNPACKが発生するため
-
アンパックについて
Python2 と Python3 では山より高く海より深い溝がある(誇張表現)と思っていますが、その中に一つに リストのアンパック があります。
リストやタプルの変数の前に * を付けることで、そのリストを展開した結果を直接扱うことができます。
分かりづらいので例を用いますと、例えば下記のようなコードを実行すると、
a1 = [0, 1, 2]
print(a1)
print(*a1)
[0, 1, 2]
0 1 2
このように、*a1 はリストの中身を展開したものが表示されます。
これによってより柔軟に変数への代入ができたりします。
(気になる人は first, *middle, last = [0, 1, 2, 3, 4] で middle に何が入るか確認してみてください)
a + b と [*a, *b] について
導入
アンパックを使うことで、2つのリスト a, b の連結を以下の 2通りで書くことができます。
a = list(range(5))
b = list(range(5, 10))
print(a + b)
print([*a, *b])
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
実行速度
上記より、リストの連結は a + b と [*a, *b] の 2通りで書けることが確認できました。
ここで疑問ですが、この2通りのうちどちらの方が実行速度が速い のでしょうか?
- アンパックはコストが高い。だから
a + bの方が速い - リストの足し算はコストが高い。だから
[*a, *b]の方が速い - あくまで表現の違いで内部的には同じだから、実行速度は同じ
僕がパッと思いついたのは上記の 3通りの答えです。
それでは実際に検証してみます。
今回は JupyterNotebook 上(正確には Google Colaboratory上)で検証したため %time を使いましたが、JupyterNotebook を使わない場合は Time モジュールなどを使ってください。
注記(2018/04/26 追記)
下記のコードでは関数の中で for 文を回していますが、今回のような計測用途であれば、%timeit を使うともっとスマートに記述することができます(下部おまけ参照)。
@69guitar1015 さんご指摘ありがとうございました!
x1 = list(range(5))
x2 = list(range(5,10))
count = 1000000
def sample1(a, b, c):
"""二つのリスト a, b に対して a + b を指定回数実行"""
print("sample1, count: ", c)
for i in range(c):
z = a + b
def sample2(a, b, c):
"""二つのリスト a, b に対して [*a, *b] を指定回数実行"""
print("sample2, count: ", c)
for i in range(c):
z = [*a, *b]
print("x1 + x2: ", x1 + x2)
print("[*x1, *x2]: ", [*x1, *x2])
print("-----------------------")
% time sample1(x1, x2, count)
print("-----------------------")
% time sample2(x1, x2, count)
print("-----------------------")
% time sample1(x1, x2, count*10)
print("-----------------------")
% time sample2(x1, x2, count*10)
print("-----------------------")
% time sample1(x1, x2, count*100)
print("-----------------------")
% time sample2(x1, x2, count*100)
x1 + x2: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[*x1, *x2]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
-----------------------
sample1, count: 1000000
CPU times: user 88 ms, sys: 1e+03 µs, total: 89 ms
Wall time: 90.2 ms
-----------------------
sample2, count: 1000000
CPU times: user 109 ms, sys: 0 ns, total: 109 ms
Wall time: 109 ms
-----------------------
sample1, count: 10000000
CPU times: user 890 ms, sys: 0 ns, total: 890 ms
Wall time: 895 ms
-----------------------
sample2, count: 10000000
CPU times: user 1.18 s, sys: 0 ns, total: 1.18 s
Wall time: 1.2 s
-----------------------
sample1, count: 100000000
CPU times: user 10.5 s, sys: 0 ns, total: 10.5 s
Wall time: 10.5 s
-----------------------
sample2, count: 100000000
CPU times: user 12.2 s, sys: 0 ns, total: 12.2 s
Wall time: 12.2 s
上記の結果から、Python3 以前からある a + b の方が速い ことが確認できました。
解析(dis モジュール)
ではその内訳を dis モジュールで見ていきます。
from dis import dis
dis(sample1) # x1 + x2
7 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('sample1, count: ')
4 LOAD_FAST 2 (c)
6 CALL_FUNCTION 2
8 POP_TOP
8 10 SETUP_LOOP 24 (to 36)
12 LOAD_GLOBAL 1 (range)
14 LOAD_FAST 2 (c)
16 CALL_FUNCTION 1
18 GET_ITER
>> 20 FOR_ITER 12 (to 34)
22 STORE_FAST 3 (i)
9 24 LOAD_FAST 0 (a)
26 LOAD_FAST 1 (b)
28 BINARY_ADD
30 STORE_FAST 4 (z)
32 JUMP_ABSOLUTE 20
>> 34 POP_BLOCK
>> 36 LOAD_CONST 2 (None)
38 RETURN_VALUE
dis(sample2) # [*x1, *x2]
13 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('sample2, count: ')
4 LOAD_FAST 2 (c)
6 CALL_FUNCTION 2
8 POP_TOP
14 10 SETUP_LOOP 24 (to 36)
12 LOAD_GLOBAL 1 (range)
14 LOAD_FAST 2 (c)
16 CALL_FUNCTION 1
18 GET_ITER
>> 20 FOR_ITER 12 (to 34)
22 STORE_FAST 3 (i)
15 24 LOAD_FAST 0 (a)
26 LOAD_FAST 1 (b)
28 BUILD_LIST_UNPACK 2
30 STORE_FAST 4 (z)
32 JUMP_ABSOLUTE 20
>> 34 POP_BLOCK
>> 36 LOAD_CONST 2 (None)
38 RETURN_VALUE
ぱっと見では気づかないかもですが、よーく見ると下の結果には BUILD_LIST_UNPACK があります。
リストのアンパックを行うとこのバイトコード命令が発生するようです。
結論
-
a + bと[*a, *b]は違う - どうしてもパフォーマンスが気になってしまう人は
a + bを使うと良い
おまけ(%timeit について)
上記では関数内で for 文を回していましたが、下記のように %timeit を使えば計測のために for 文を使わずにすみます。
def sample3(a, b):
z = a + b
def sample4(a, b):
z = [*a, *b]
p1 = list(range(5))
p2 = list(range(5, 10))
print("sample3, count: ", 1000000)
%timeit -n 1000000 sample3(p1, p2)
print("--------------------------")
print("sample4, count: ", 1000000)
%timeit -n 1000000 sample4(p1, p2)
sample3, count: 1000000
1000000 loops, best of 3: 199 ns per loop
--------------------------
sample4, count: 1000000
1000000 loops, best of 3: 232 ns per loop