WebEngine

だらだらと綴る技術系メモ

引っ掛けの多いタプル【Python】

f:id:web-engine:20190615135810p:plain

環境

すべてコマンドライン上で確認

タプルってなに?

タップルではありません。タプルです。


タプルってなに?

「コレクション」タイプのデータ型の一種です。

アクセスの仕方はリストとだいたい同じですが、リストと異なり、生成したあとに変更ができないオブジェクトになります。
主として、この変更不可能性を利用して、変更を許したくない変数を定義したりするのに使います。

使い方はこんな感じ。

tp = ('apple', 'orange', 'grape')
type(tp)  # <class 'tuple'>

# アクセス方法
tp[0]  # 'apple'
tp[1:]  # ('orange', 'grape')

# タプルの生成後に変更が不可能
tp[1] = 'banana' # TypeError: 'tuple' object does not support item assignment


Pythonのタプルには、大きく分けて2つ引っかかるところがあると思います。

1. 「変更できない」の厳密な意味
2. 要素が1つだけのタプルを生成するとき

順に見ていきます。

引っ掛け1.「変更できない」の厳密な意味

注意点として、「変更ができない」とは、厳密にはオブジェクト id を変えずに、要素を追加・変更・削除することができないという意味です。

この性質をイミュータブル( immutable )と呼びます。

どういうことかと言うと、以下の例はタプルでも可能だということです。

  1. タプルを参照している変数への再代入
  2. タプル内のミュータブルな要素の変更


1. タプルを参照している変数への再代入

tp = ('apple', 'orange', 'grape')
tp = ('apple', 'orange', 'banana')

変数がタプルを参照している場合でも、そこに別のオブジェクトを代入し中身を置き換えることは可能です。

ですが、これは Python には定数(再代入ができないもの)がないというルールに起因するものなので、理解の範疇といえるでしょう。


混乱するのは+=演算を利用した場合です。

tp = ('apple', 'orange', 'grape')
tp += ('banana', 'grapefruit')
print(tp)  # ('apple', 'orange', 'grape', 'banana', 'grapefruit')

なぜこのソースコードも通るのでしょうか。

実は、+=を使っている行では、既存のタプルが変更されているのではありません。新しいタプルが生成されていて、オブジェクトのidが変わっています。tpに再代入されているのは新しいオブジェクトなのです。

tp = ('apple', 'orange', 'grape')
id(tp)  # 2252232
tp += ('banana', 'grapefruit')
id(tp)  # 2732848


リストの場合は、オブジェクトのidが維持されます。

lt = ['apple', 'orange', 'grape']
id(lt)  # 2274480
lt += ['banana', 'grapefruit']
id(lt)  # 2274480


2. タプル内のミュータブルな要素の変更

ミュータブルというのは、イミュータブルの逆で、要は変更可能という意味です。

sites = ({'name': 'WebEngine', 'url': 'https://example.com'}, {'name': 'hoge', 'url': 'https://hoge.co.jp'})
sites[0]['url'] = 'http://web-engine.hatenadiary.com'
print(sites)  #  ({'name': 'WebEngine', 'url': 'http://web-engine.hatenadiary.com'}, {'name': 'hoge', 'url': 'https://hoge.co.jp'})

このソースコードは通ります。
タプル内の要素である辞書(dict型)がミュータブルだからです。

Pythonでは、タプル内の要素そのものが変更可能であれば、その要素自体の編集は問題ないのです。

引っ掛け2.要素が1つだけのタプルを生成するとき

要素が1つだけのタプルを生成しようとして()内に1つだけオブジェクトを書くと、()は無視されて処理され、タプルとは見なされません。

tp = ('apple')
print(tp)  # 'apple'
type(tp)  # <class 'str'>

要素が1つだけのタプルを生成するには、カンマが必要になります。

tp = ('apple',)
print(tp)  # ('apple', )
type(tp)  # <class 'tuple'>

+=等で要素を1つ加えるときなどには、カンマを忘れないように注意する必要があります。

tp = ('apple', 'orange', 'grape')
tp += ('banana')  # TypeError: can only concatenate tuple (not "str") to tuple
tp += ('banana',)  # OK


どうして要素が1つのタプルにカンマが必要なのかといいますと、タプルは()に囲まれた値なのではなく、カンマで区切られた値だからです。

驚くことに、()が省略されていてもタプルとして処理されます。

tp = 'apple','orange','grape'
type(tp)  # <class 'tuple'>


ただし、空のタプルをつくる場合は素直に書きます。
カンマだけ書くとエラーになります。

tp = ()
type(tp)  # <class 'tuple'>
tp = ,  # error


まとめ

Pythonはリストや辞書型の利便性に目が行きがちで、タプルが拗ねている節がある。


参考