PythonのListとTupleの違いとTupleはなぜ早いのかを調べてみた
データアナリティクス事業本部@札幌の佐藤です。
ListとTuple使いこなせてますか?
私はPythonをやる前はTupleの存在しない世界の住人だったため、なにこのカッコは……た、Tuple……???って感じでした。
同じような方もいらっしゃるのかなと思いますし、あの頃の自分に教えてあげたいというのもありますのでそんな意味も込めてListとTupleの違いとTupleはなぜ早いのかについて書きたいと思います。
そもそもListとTupleとは
そもそもListとTupleは共にシーケンス型です。いわゆる配列ができる型になります。
上記のほかにはstrやrangeもシーケンシャルに値を取得できます。
基本的なシーケンス型は 3 つあります: リスト、タプル、range オブジェクトです。バイナリデータ や テキスト文字列 を処理するように仕立てられたシーケンス型は、セクションを割いて解説します。
ref:シーケンス型 --- list, tuple, range
上記公式ドキュメントの通り、シーケンス型のためループして配列走査したり、要素指定して取得したりすることができます。
# Listの場合 >>> list_pure_pallet = ["友希あいね", "湊みお"] >>> print(list_pure_pallet[0]) 友希あいね # Tupleの場合 >>> tuple_pure_pallet = ("友希あいね", "湊みお",) >>> print(tuple_pure_pallet[0]) 友希あいね
このような感じで共に要素指定してデータが取得できますし、
# Listの場合 >>> honey_cat = ["日向エマ", "蝶野舞花"] >>> [pop, sexy] = honey_cat >>> print(pop) 日向エマ # Tupleの場合 >>> pure_pallet = ("友希あいね", "湊みお",) >>> (cute, cool,) = pure_pallet >>> print(cool) 湊みお
共にアンパックして取得することもできるので、データを取得するという意味では同じように使っていける感じですね。
じゃあListとTuple同じかというと違います。
ListとTupleの大きな違いは、Listがミュータブル(可変)でTupleがイミュータブル(不変)であるという点です。
Listは要素を置き換えたり、追加することが可能です。
>>> list_soleil = ["星宮いちご", "霧矢あおい"] >>> print(list_soleil) ['星宮いちご', '霧矢あおい'] >>> list_soleil[0] = "紫吹蘭" >>> print(list_soleil) ['紫吹蘭', '霧矢あおい'] >>> list_soleil.append("星宮いちご") >>> print(list_soleil) ['紫吹蘭', '霧矢あおい', '星宮いちご']
TupleはListと同じようにはできません。やろうとするとTypeErrorになります。
>>> tuple_soleil = ("星宮いちご", "霧矢あおい") >>> tuple_soleil[0] = "紫吹蘭" >>> print(tuple_soleil) TypeError: 'tuple' object does not support item assignment
dir()関数でみてみると、Tupleは要素追加や削除のような機能が存在しないのが分かります。
>>> dir(tuple) ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']
でもちょっと待って、Tupleでも要素を変更したり追加したりできるじゃん
>>> tristar = ("神崎美月", "一ノ瀬かえで", "紫吹蘭") >>> print(tristar) ('神崎美月', '一ノ瀬かえで', '紫吹蘭') >>> tristar = ("神崎美月", "一ノ瀬かえで", "藤堂ユリカ") >>> print(tristar) ('神崎美月', '一ノ瀬かえで', '藤堂ユリカ')
これって要素の変更ですよね?って思いません?
答えはNOです。
これは同じ変数名なだけの別人です。
変数名が同じでも、メモリに生成されるオブジェクトが違います。
どういうことかオブジェクトに割り振られる番号を確認できる id()関数
でチェックします。
この番号が同じであればメモリに生成されているオブジェクトが同じです。
>>> tristar = ("神崎美月", "一ノ瀬かえで", "紫吹蘭") >>> print(id(tristar), tristar) 2876853768336 ('神崎美月', '一ノ瀬かえで', '紫吹蘭') >>> tristar = ("神崎美月", "一ノ瀬かえで", "藤堂ユリカ") >>> print(id(tristar), tristar) 2876853108880 ('神崎美月', '一ノ瀬かえで', '藤堂ユリカ')
ここに記載の通り、オブジェクトに割り振られる番号が違います。つまりこの tristar変数
は同じ名前の別人であるということが分かります。
Tupleの要素追加(っぽい挙動)についても同じです。
>>> Soleil = ("星宮いちご", "霧矢あおい",) >>> print(id(Soleil), Soleil) 2257422256712 ('星宮いちご', '霧矢あおい') >>> Soleil += ("紫吹蘭",) >>> print(id(Soleil), Soleil) 2257417653920 ('星宮いちご', '霧矢あおい', '紫吹蘭')
ListとTupleどっちを使えばいいの
ここからが本題です。
じゃあListとTupleどっちを使えばいいのという話です。
単純に要素が追加されたり削除されたりする場合はListを使えばいいと思います。
この話題についてよく目にするのはTupleのほうが処理が早いという記載ですが、なぜ早いのかについては記載されていることが少ないかなと思います。
なぜ早いのかということについて、調べてみたいと思います。
データをappendするなら断然List
そもそもappendするときにTupleを使うのか? という疑問はありますが検証します。
import time # Listの場合 list_array = [] start = time.time() for i in range(10000): list_array.append(i) elapsed_time = time.time() - start print ("elapsed_time:{0}".format(elapsed_time) + "[sec]") # Tupleの場合 tuple_array = () start = time.time() for i in range(10000): tuple_array += (i,) elapsed_time = time.time() - start print ("elapsed_time:{0}".format(elapsed_time) + "[sec]")
List
1回目:elapsed_time:0.004985332489013672[sec]
2回目:elapsed_time:0.0029952526092529297[sec]
3回目:elapsed_time:0.002988100051879883[sec]
Tuple
1回目:elapsed_time:0.24312567710876465[sec]
2回目:elapsed_time:0.2084956169128418[sec]
3回目:elapsed_time:0.30466675758361816[sec]
圧倒的に違いますね。
Tupleにしたいのであれば、Listを tuple()
でTupleに変換するのがベターですね。
パフォーマンスはTuple
パフォーマンス観点として、まず裏でどのような挙動をしているのかを dis()
で確認したいと思います。
dis モジュールは CPython バイトコード bytecode を逆アセンブルすることでバイトコードの解析をサポートします。 このモジュールが入力として受け取る CPython バイトコードはファイル Include/opcode.h に定義されており、 コンパイラとインタプリタが使用しています。
ref:dis --- Python バイトコードの逆アセンブラ
>>> def list_array(): >>> dream_Academy = ["音城セイラ","冴草きい","風沢そら","姫里マリア","音城ノエル"] >>> student = dream_Academy[2] >>> def tuple_array(): >>> dream_Academy = ("音城セイラ","冴草きい","風沢そら","姫里マリア","音城ノエル") >>> student = dream_Academy[2] >>> import dis >>> # Listの場合 >>> dis.dis(list_array) 2 0 LOAD_CONST 1 ('音城セイラ') 2 LOAD_CONST 2 ('冴草きい') 4 LOAD_CONST 3 ('風沢そら') 6 LOAD_CONST 4 ('姫里マリア') 8 LOAD_CONST 5 ('音城ノエル') 10 BUILD_LIST 5 12 STORE_FAST 0 (dream_Academy) 3 14 LOAD_FAST 0 (dream_Academy) 16 LOAD_CONST 6 (2) 18 BINARY_SUBSCR 20 STORE_FAST 1 (student) 22 LOAD_CONST 0 (None) 24 RETURN_VALUE >>> # Tupleの場合 >>> dis.dis(tuple_array) 6 0 LOAD_CONST 1 (('音城セイラ', '冴草きい', '風沢そら', '姫里マリア', '音城ノエル')) 2 STORE_FAST 0 (dream_Academy) 7 4 LOAD_FAST 0 (dream_Academy) 6 LOAD_CONST 2 (2) 8 BINARY_SUBSCR 10 STORE_FAST 1 (student) 12 LOAD_CONST 0 (None) 14 RETURN_VALUE
これを見たらわかるのが要素がどのようにスタックされているのか(LOAD_CONSTの挙動)です。
Listは要素をひとつひとつスタックしていますが、Tupleはまとめてスタックしています。(定数畳み込み)
パフォーマンスの違いはここにあります。
次にメモリの割り当てについてですが、イミュータブルということもあり想像通りTupleのほうが優秀です。
>>> # Tupleの場合 >>> import sys >>> print("List:",sys.getsizeof(list(iter(range(10000))))) List: 90112 >>> print("Tuple:",sys.getsizeof(tuple(iter(range(10000))))) Tuple: 80048
最後に
同じように見えるListとTupleですが、こうして改めてみてみると全然別物ですよね。
誰かの参考になれば幸いです。
「アイカツ!」シリーズが好きなので、データと変数は関連するキャラクターに寄せました。
最新シリーズ『アイカツオンパレード!』はテレビ東京系 毎週土曜日 午前10時30分~/BSテレ東毎週月曜日夕方5時~から好評放送中です!