【Python】[・ι・]{・ω・}(・ε・)は家族なのか?

Pythonを利用していると誰しもが遭遇する{これ}[これ](これ)

[・ι・]:リスト

{・ω・}:ディクショナリ

(・ε・):タプル

この子たちは漠然と違いは分かっているものの
根本的に何が違うのかが具体的に分かりません。

そこで、今回はこの子達の簡単な生態と関係性を調べてみようと思います。

少し長くなったので目次を置いておきます。

0. 動作環境

こちらの記事に記載してあるので、ぜひ参照してください。


1. リストとディクショナリとタプルとは何か?

まず簡単に、こんなことできますよ。
っていうのをそれぞれで示してみたいと思います。


1-1. リスト

type_list = []
type_list.append("1000")
type_list.append(1)
type_list.append(0.003)

出力結果

['1000', 1, 0.003]

他の言語でプログラミングをしてきた方であれば”配列”といえば”[]”ではないでしょうか?

Pythonの場合は、純粋な配列(array)は別のモジュールになっているので
これは配列ではなくリストになります。(ややこしい)

Pythonの場合は、全てがオブジェクトなので数字でも文字列でもリストに一緒に入れられるのが良いところでもあり、扱いずらいところでもあります。


1-2. ディクショナリ

type_dict = {}
type_dict["tomato"] = 2.11
type_dict[1024] = 1024
type_dict[3.14] = "昔喧嘩した親友。今でも当時のことは割り切れていない"
 
print(type_dict)

出力結果

{'tomato': 2.11, 1024: 1024, 3.14: '昔喧嘩した親友。今でも当時のことは割り切れていない'}

単純なKey-Valueになっています。
面白いのは、キーでもバリューでも型が”ユーザーの認識”としてなんでも良いというところ。ここは、Pythonのなんでもオブジェクトとして扱う性質がうまく働いています。

ただ、クラスもKey-Value両方に入れられるのかは検証してないので
今後の課題としたいと思います。

ここまではC#のジェネリックコレクションにあるListやDictionaryと動きが非常に似ています。
C#が分からない方は、似たようなのがあるんだと思って下さい。


1-3. タプル

type_tuple = ()
type_tuple += (0,)
type_tuple += ("image",)
type_tuple += (0.98,)   
 
print(type_tuple)

出力結果

(0, 'image', 0.98)

Pythonを触る上で避けて通れないタプル。

一見するとなんでもないように見えます。
しかし、ここでもPythonのミューイミューが良い感じに働いていて、Pythonっていう言語はここまで考えられているのかと関心しました。

タプルに要素を追加する際の記述にある(0,)の","は、省略すると
TypeError: can only concatenate tuple (not "int") to tuple
(意訳-> タイプエラー: タプルに連結できるのはタプルだけです。(int型は連結できません))

のようにエラーを出力します。

タプルとして認識されるために必要なので、省略はできません

また、+=演算子が分かりにくい場合は以下のようなことを簡単化しているものだと思ってください。

type_tuple = ()
type_tuple = type_tuple + (0,)
type_tuple = type_tuple + ("image",)
type_tuple = type_tuple + (0.98,)   
 
print(type_tuple)

タプルにタプルを足すことでタプルに要素が追加できます。
何故、こういう表現になるのかは後半で自ずと分かりますので今はこういうものだと思っていてください。


2. オブジェクトから見るリストとディクショナリとタプル

一般的な利用方法は、上記でほぼ全てです。

細かいところでは、イテレーションや関数による操作などがありますが
そういうのは私よりもPythonに詳しい方々に譲るとして、ここでは各オブジェクトがどのような変遷をするのかを見ていきます。

変遷を見るためのコードはこちら

type_list = []
type_tuple = ()
type_dict = {}

print("----- Before -----")
print("list id -> {}".format(id(type_list)))
print("tuple id -> {}".format(id(type_tuple)))
print("dict id -> {}".format(id(type_dict)))

type_dict["tomato"] = 2.11
type_dict[1024] = 1024
type_dict[3.14] = "昔喧嘩した親友。今でも当時のことは割り切れていない"


type_list.append("1000")
type_list.append(1)
type_list.append(0.003)

type_tuple = type_tuple + (0,)
type_tuple += ("image",)
type_tuple += (0.98,)

print("----- After -----")
print("list id -> {}".format(id(type_list)))
print("tuple id -> {}".format(id(type_tuple)))
print("dict id -> {}".format(id(type_dict)))

print("----- Value -----")
print(type_list)
print(type_tuple)
print(type_dict)

出力結果

----- Before -----
list id -> 4476948296
tuple id -> 4476477512
dict id -> 4477477152
----- After -----
list id -> 4476948296
tuple id -> 4477302848
dict id -> 4477477152
----- Value -----
['1000', 1, 0.003]
(0, 'image', 0.98)
{'tomato': 2.11, 1024: 1024, 3.14: '昔喧嘩した親友。今でも当時のことは割り切れていない'}

前回の"ミューとイミューに首ったけ"の記事でも利用したid関数を利用して
オブジェクトIDを表示させています。

各オブジェクトIDを見てみましょう。


2-1. リスト

リストの中身が変わってもリスト自体のオブジェクトIDは切り替わっていません。

これはリストとリストに追加した要素は、それぞれ独立したオブジェクトとして成り立っているからでしょう。

内部要素のオブジェクトIDが変わっても、リスト自体のオブジェクトIDは変わりません。

その証拠として以下のようなプログラムを動かしてみます。

type_list = []
print("----- Before -----")
print("list id -> {}".format(id(type_list)))

type_list.append("900")
type_list.append(1)
type_list.append(0.003)

print("----- After -----")
print("list id -> {}".format(id(type_list)))
print("list[0] id -> {}".format(id(type_list[0])))
print("list[1] id -> {}".format(id(type_list[1])))
print("list[2] id -> {}".format(id(type_list[2])))
print(type_list)

print("----- Other -----")
type_list[0] = "901"
print("list id -> {}".format(id(type_list)))
print("list[0] id -> {}".format(id(type_list[0])))
print("list[1] id -> {}".format(id(type_list[1])))
print("list[2] id -> {}".format(id(type_list[2])))
print(type_list) 

出力結果

----- Before -----
list id -> 4481978184
----- After -----
list id -> 4481978184
list[0] id -> 4482491200
list[1] id -> 4480614528
list[2] id -> 4481753472
['900', 1, 0.003]
----- Other -----
list id -> 4481978184
list[0] id -> 4482491256
list[1] id -> 4480614528
list[2] id -> 4481753472
['901', 1, 0.003]

ここでもPythonのミューとイミューが上手く働いていると考えられます。

また、リストは変数間のやり取りにおいて、オブジェクト自体を渡すようで、新しい変数に放り込んでもオブジェクトIDは切り替わりませんでした。

ここはクラスと少し挙動が違います。

先ほどのコードに新しい変数を用意して表示する部分を追加して動かしてみます。

type_list = []
print("----- Before -----")
print("list id -> {}".format(id(type_list)))

type_list.append("900")
type_list.append(1)
type_list.append(0.003)

print("----- After -----")
print("list id -> {}".format(id(type_list)))
print("list[0] id -> {}".format(id(type_list[0])))
print("list[1] id -> {}".format(id(type_list[1])))
print("list[2] id -> {}".format(id(type_list[2])))
print(type_list)

print("----- Other -----")
type_list[0] = "901"
print("list id -> {}".format(id(type_list)))
print("list[0] id -> {}".format(id(type_list[0])))
print("list[1] id -> {}".format(id(type_list[1])))
print("list[2] id -> {}".format(id(type_list[2])))
print(type_list)

type_other_list = type_list

print("other list id -> {}".format(id(type_other_list)))
print("other list[0] id -> {}".format(id(type_other_list[0])))
print("other list[1] id -> {}".format(id(type_other_list[1])))
print("other list[2] id -> {}".format(id(type_other_list[2])))

出力結果

----- Before -----
list id -> 4444290888
----- After -----
list id -> 4444290888
list[0] id -> 4444803904
list[1] id -> 4442910848
list[2] id -> 4444066176
['900', 1, 0.003]
----- Other -----
list id -> 4444290888
list[0] id -> 4444803960
list[1] id -> 4442910848
list[2] id -> 4444066176
['901', 1, 0.003]
other list id -> 4444290888
other list[0] id -> 4444803960
other list[1] id -> 4442910848
other list[2] id -> 4444066176

では、other listを中身をそのままに別のリストとして使う場合はどうしたら良いのでしょうか?

それはdeepcopy関数で解決できるのですが、主題から逸れるので別の機会にしたいと思います。


2-2. ディクショナリ

概ね、リストと同じ挙動を見せます。

確認してみましょう。

type_dict = {}
print("----- Before -----")
print("dict id -> {}".format(id(type_dict)))

type_dict["tomato"] = 2.11
type_dict[1024] = 1024
type_dict[3.14] = "昔喧嘩した親友。今でも当時のことは割り切れていない"

print("----- After -----")
print("dict id -> {}".format(id(type_dict)))
print("dict['tomato'] id -> {}".format(id(type_dict["tomato"])))
print("dict[1024] id -> {}".format(id(type_dict[1024])))
print("dict[3.14] id -> {}".format(id(type_dict[3.14])))

print("----- Value -----")
print(type_dict)

type_dict["tomato"] = "101010"
print("")
print("----- Other Value -----")
print("dict id -> {}".format(id(type_dict)))
print("dict['tomato'] id -> {}".format(id(type_dict["tomato"])))
print("dict[1024] id -> {}".format(id(type_dict[1024])))
print("dict[3.14] id -> {}".format(id(type_dict[3.14])))

print("----- Value -----")
print(type_dict)

type_dict[1024] = type_dict["tomato"]
print("")
print("----- Other Object -----")
print("dict id -> {}".format(id(type_dict)))
print("dict['tomato'] id -> {}".format(id(type_dict["tomato"])))
print("dict[1024] id -> {}".format(id(type_dict[1024])))
print("dict[3.14] id -> {}".format(id(type_dict[3.14])))

print("----- Value -----")
print(type_dict)

出力結果

----- Before -----
dict id -> 4478226720
----- After -----
dict id -> 4478226720
dict['tomato'] id -> 4477473152
dict[1024] id -> 4478675472
dict[3.14] id -> 4478234416
----- Value -----
{'tomato': 2.11, 1024: 1024, 3.14: '昔喧嘩した親友。今でも当時のことは割り切れていない'}

----- Other Value -----
dict id -> 4478226720
dict['tomato'] id -> 4478210992
dict[1024] id -> 4478675472
dict[3.14] id -> 4478234416
----- Value -----
{'tomato': '101010', 1024: 1024, 3.14: '昔喧嘩した親友。今でも当時のことは割り切れていない'}

----- Other Object -----
dict id -> 4478226720
dict['tomato'] id -> 4478210992
dict[1024] id -> 4478210992
dict[3.14] id -> 4478234416
----- Value -----
{'tomato': '101010', 1024: '101010', 3.14: '昔喧嘩した親友。今でも当時のことは割り切れていない'}

Key-Valueという性質の違いはあるもののオブジェクトとしての挙動はリストとほぼ同じであることが分かりました。

最後の同じディクショナリの要素別の要素に代入する同じオブジェクトIDになるのは、リストも同じ挙動をします。

これを別のオブジェクトと見なすためにはcopy関数を利用するのですが、主題から外れるので別の機会にしたいと思います。


2-3.  タプル

分かっていたことですが、タプルではしっかりオブジェクトIDが切り替わります。

これは、内と外で独立な関係が成り立っている。。。という訳ではありません。

正確には「イミュータブルな値の変更は、オブジェクト自体を新しく作り直す」という原則に乗っ取ってオブジェクトIDが切り替わっているのです。

先ほどの+=演算子で要素を足し合わせていたのは
イミュータブルな値同士の計算を行って新しいオブジェクトを生成する。

ということをしていたわけです。

もう少し映像的に説明しましょう。
1-3のコードを例に動きを一つずつ見ていきます。

(0,) = (,) + (0,)                             # 初期値と整数のタプルの足し算
(0, "image", ) = (0,) + ("image",)            # 整数と文字列のタプルの足し算
(0, "image", 0.98) = (0, "image", ) + (0.98,) # 整数&文字列と浮動小数のタプルの足し算

右辺の値の合算で左辺が生成されています。

興味深いのは、右辺の値が並び順の通りに左辺に反映されているところです。

実際にそうなっているか短いプログラムでもう少し確認してみましょう。

type_tuple = ()
type_tuple = type_tuple + (0,)
type_tuple += ("image",)
type_tuple = ("road", 1, 99,) + type_tuple
type_tuple += (0.98,)
type_tuple += (1,10,"edo",)

print(type_tuple)

出力結果

('road', 1, 99, 0, 'image', 0.98, 1, 10, 'edo')

どうやらタプルの要素の追加というのは、先頭から順番に詰め込んでいく方式で成り立っているようです。

オブジェクト同士の加算は左から順番に行われていくということで
別のオブジェクト同士の計算の際には、ここら辺も気をつけないと得たい結果と違うことになりそうです。

因みに、タプルの要素へのアクセスはリストと同様にIndexを指定することで行います。

type_tuple = ()
type_tuple = type_tuple + (0,)
type_tuple += ("image",)
type_tuple += (0.98,)

print(type_tuple[0])

出力結果

0

こうして見ると、順序やIndexが利いている部分でリストに似ているところがあるようです。

が、なにより一番Pythonしているのがタプルかも知れません。
イミュータブルなお陰で、下手に複雑なことをしなければ一番シンプルに扱えます。


3. [・ι・]{・ω・}(・ε・)は家族なのか?

ここまでのまとめを以下に模式図で示してみます。

━:依存関係なし
->:依存関係あり

リスト:
[・ι・] ┳ [0] -> object
             ┣ [1] -> object
             ┗ [2] -> object

ディクショナリ:
{・ω・} ┳ {key_object ━ value_object}
              ┣ {key_object ━ value_object}
              ┗ {key_object ━ value_object}

タプル:
(・ε・) -> (object,)

こうして見ると何となくですが
リストとディクショナリは、遠縁の親戚。(潜在的な共通点がある)
リストとタプルは、友人。(表面的な共通点がある)
ディクショナリとタプルは、赤の他人。(共通点がほとんどない)

ぐらいの関係性で考えられるかなと思います。


4. 強みを活かした使い方を心掛けよう

組み合わせて使う場面では、関係性を覚えておくと強みを活かした運用ができるかもしれません。

単体で使う場面であれば、無暗にやたらに使うよりはそれぞれの特性を理解した上で、適切な子が使えるようになると良いでしょう。


例えば

配列的に使いたいと盲目的にリストを利用するとします。

この時、中身のオブジェクト外身のリストのオブジェクトのあり方を理解していないと
単純に別の変数に分けただけで、別のリストになったと思い込むような簡単なバグを引き起こす可能性があるかも知れません。(結果、何時間もGoogle先生のお世話になることでしょう)

その点、タプルは要素を追加した段階で別のオブジェクトになるので別の変数に突っ込んでおけば別のタプルと理解してもバグは起きにくいです。

type_tuple = ()
print("----- Before -----")
print("tuple id -> {}".format(id(type_tuple)))

type_tuple = type_tuple + (0,)
type_tuple += ("image",)
type_tuple = ("road", 1, 99,) + type_tuple
type_tuple += (0.98,)
type_tuple += (1,10,"edo",)

print("----- After -----")
print("tuple id -> {}".format(id(type_tuple)))

print("----- Value -----")
print(type_tuple)

print("")
print("----- Other Before -----")
type_other_tuple = type_tuple
print("tuple id -> {}".format(id(type_tuple)))
print("tuple other id -> {}".format(id(type_other_tuple)))

print("----- Value -----")
print(type_tuple)
print(type_other_tuple)

type_other_tuple += ("other",)
print("")
print("----- Other After -----")
print("tuple id -> {}".format(id(type_tuple)))
print("tuple other id -> {}".format(id(type_other_tuple)))

print("----- Value -----")
print(type_tuple)
print(type_other_tuple)

出力結果

----- Before -----
tuple id -> 4394369096
----- After -----
tuple id -> 4395194432
----- Value -----
('road', 1, 99, 0, 'image', 0.98, 1, 10, 'edo')

----- Other Before -----
tuple id -> 4395194432
tuple other id -> 4395194432
----- Value -----
('road', 1, 99, 0, 'image', 0.98, 1, 10, 'edo')
('road', 1, 99, 0, 'image', 0.98, 1, 10, 'edo')

----- Other After -----
tuple id -> 4395194432
tuple other id -> 4394602056
----- Value -----
('road', 1, 99, 0, 'image', 0.98, 1, 10, 'edo')
('road', 1, 99, 0, 'image', 0.98, 1, 10, 'edo', 'other')


逆に、配列的に利用する時に順序が重要でかつ更新が頻繁に行われる場面でタプルを利用するとします。

その場合、順番の入れ替えや毎回の更新が発生する度に全体のオブジェクトが再生成されるので、オブジェクト生成が無駄なオーバーヘッドに繋がって処理時間が無駄に伸びるかも知れません。
その上、オブジェクトの結合の順番で中身があべこべになったり、途中で値を挿入するのが困難だったりと順序が破綻する可能性もあります。

こういう時は、リストを利用した方がオブジェクト生成が単体で済みますし、途中挿入してもIndexは正常に更新されるのでバグは起きにくいでしょう。


こういう細かい違いがあることを理解しておくことで
Pythonにおけるリストとディクショナリとタプルを利用する際のバグの発生は、かなり抑えられるような気がします。

タプルの利便性をこの記事を書く前は理解していませんでしたが
Pythonにおいてはこの「tuple」の存在がとても大きいことをよくよく理解できました。(あと、綴りを覚えました。)


[・ι・]{・ω・}(・ε・)、みんな違って、みんな良い。



長々とお付き合いありがとうございました。

では。

この記事が気に入ったらサポートをしてみませんか?