LoginSignup
56
62

More than 3 years have passed since last update.

Pythonのタプルについて、腑に落ちるまで手を動かして検証してみた

Last updated at Posted at 2019-06-22

きっかけ & 方針

Pythonのタプルに関しては、分かりやすい解説のページが多数存在します。

それらを読んで何となく分かった気にはなっていたのですが、別の記事でタプルに言及しようとした際、「タプル、ちゃんと説明できない・・・」という事態に陥りました。

なので、今後、記事を書く時に困らないよう、腑に落ちるまで手を動かして色々と検証してみようと思います。

環境

  • Python 3.7.2
  • Win10

タプルの定義の仕方とアクセス方法

まずは基本的な所から、タプルの定義の仕方とアクセス方法について、リストとの比較しながら。

#基本的な定義の仕方(リストと比較しつつ)
# リスト => 要素を,で区切り、[]で囲む
lst = ["a", "b", "c"]
print(type(lst)) # => <class 'list'>

# タプル => 要素を,で区切り、()で囲む(※)
tpl = ("a", "b", "c")
print(type(tpl)) # => <class 'tuple'>


#アクセス方法はどちらも同じ
# 全体
print(lst)    # => ['a', 'b', 'c']
print(tpl)    # => ('a', 'b', 'c')

# 添え字で特定要素を取得 
print(lst[1])    # => b
print(tpl[1])    # => b

# スライスで範囲指定
print(lst[1:])   # => ['b', 'c']
print(tpl[1:])   # => ('b', 'c')
print(lst[::-1]) # => ['c', 'b', 'a']
print(tpl[::-1]) # => ('c', 'b', 'a')

イテラブルオブジェクトに対しては、組み込み関数tuple()を使いタプルを作成することも出来ます。

#組み込み関数をtuple()使う
# 文字列 => タプル
tpl_taxi = tuple("タクシー")
print(tpl_taxi) # => ('タ', 'ク', 'シ', 'ー')

# リスト => タプル
tpl_fruits = tuple(["apple", "orange", "banana"])
print(tpl_fruits) # => ('apple', 'orange', 'banana')

# 数字からの変換は不可(数字はイテラブルオブジェクトではないため)
tpl_nums = tuple(123)
print(tpl_nums) # => 'int' object is not iterable

タプルを定義する際の注意点

  • タプルの()は省略可。というか、タプルは()ではなく、,で作成される
  • このため、要素が1個のタプルを定義する際も,が必要
  • ただし、要素が0個のタプルを定義する際は()が必要
  • 文法があいまいな場合も()が必要

丸括弧形式 (parenthesized form) — Python 3.7.3 ドキュメント

タプルは丸括弧で作成されるのではなく、カンマによって作成されることに注意してください。例外は空のタプルで、この場合には丸括弧が 必要です --- 丸括弧のつかない "何も記述しない式 (nothing)" を使えるようにしてしまうと、文法があいまいなものになってしまい、よくあるタイプミスが検出されなくなってしまいます。

ということで検証を。

タプルは()ではなく,で作成される

tpl = "a", "b", "c"
print(type(tpl)) # => <class 'tuple'>

実は、この()が無い形のタプル、頻繁に見かけます。

#2つの変数に2つの値を同時に代入している、ように見えますが・・・
police_car, taxi = "パトカー", "タクシー"
print(police_car) # => パトカー
print(taxi)       # => タクシー

右辺は文字列が2つ並んでいるように見えますが、実は("パトカー", "タクシー")というタプルの丸括弧()が省略されたている形です。

なので、これは、左辺の2つの変数にに対して、右辺のタプルをアンパックしていることになります。

Pythonでタプルやリストをアンパック(複数の変数に展開して代入)

要素が1つのタプルを定義する際も,が必要

これは、1つ上で触れている、「タプルは,で作成される」という話と同じ話です。

タプルは,で作成されるので、つまり、タプルをタプルたらしめているのは,なので、タプルを定義したいのであれば要素が1つであっても,が必要です。

#要素が1つしか存在しないタプルを定義する場合もカンマが必要
python = ("パイソン", )
print(python)       # => ('パイソン', )
print(type(python)) # => <class 'tuple'>

# これもタプル
python = "パイソン", 
print(python)       # => ('パイソン', )
print(type(python)) # => <class 'tuple'>

# これは、カンマがないので文字列として認識される
python = ("パイソン")
print(python)       # => パイソン
print(type(python)) # => <class 'str'>

ちなみに、スライスを使いタプルから要素の範囲を1つに絞って取得したり、tuple()を使い要素が1つだけのタプルを作成した場合も、,が付きます。

#スライスで範囲を1つの要素に絞ってアクセスした際の挙動をリストと比較
lst = ["a", "b", "c"]
tpl = ("a", "b", "c")

print(lst[0:1]) # => ['a']  
print(tpl[0:1]) # => ('a',)  要素は1つだがカンマで区切られている
#組み込み関数を使い要素が1つだけのオブジェクトを作成した際の挙動をリストと比較
lst = list("a") 
tpl = tuple("a")

print(lst) # => ['a']  
print(tpl) # => ('a',)  要素は1つだがカンマで区切られている

ところで、要素が1つだけのタプルを定義するケースというのは実務において存在するのでしょうか?

存在するのかもしれませんが、(少なくとも僕は)イマイチ想像できませんでした。

ただ、こちら、逆に、本当は文字列を定義してしたかったのに、誤って末尾にカンマを突けてしまい予期せぬバグを引き起こしてしまう、というケースがあるみたいなので、注意が必要です。

なお、要素が2つ以上のタプルを定義する場合も、末尾に,を付ける形を取っても特に問題ありません。

cars = ("パトカー", "タクシー",) 
#                           ↑ 要素が2つ以上の場合も末尾にカンマを付けても大丈夫

print(cars) # => ('パトカー', 'タクシー')

わかっちゃいるけど、やめられない - Pythonあるある凡ミス集

要素が0個のタプルを定義する際は()が必要

#タプルの()は省略可能だが、要素が存在しない場合は()が必須
empty = ()
print(empty)       # => ()
print(type(empty)) # => <class 'tuple'>

#コレだと、文字列として判定される
empty = ""
print(empty)       # => 
print(type(empty)) # => <class 'str'>

#コレだと、この時点でSyntaxError
empty = , # => SyntaxError: invalid syntax

文法があいまいな場合も()が必要

文法があいまいなケースとは、例えば、タプルをリストの要素にするケースや、メソッドや関数の引数としてタプルを渡すケースが考えられます。

リストをタプルの要素にするケース

#リストの要素としてタプルを使用
boolean = [(1, True), (0, False)]
print(len(boolean)) #=> 2

#丸括弧を省略すると、リストの要素が4つあることになる
boolean = [1, True, 0, False]
print(len(boolean)) #=> 4

関数やメソッドの引数としてタプルを渡すケース

#type関数は1つor3つの引数を取る
# 丸括弧を省略して引数を渡すと、複数の文字列を渡されたと認識してしまう
type( "パトカー", "タクシー" ) # => TypeError: type() takes 1 or 3 arguments

# 丸括弧で囲んで渡すとタプルと認識されて正しく判定される
type(("パトカー", "タクシー")) # => tuple

タプルとリストの比較

タプルを考える上で最も頭を悩ませたのは、「で、結局、リストと何が違うの?」という点でした。

この記事を書こうと思ったそもそものきっかけも、その点にあります。

リストとタプルの違いは、下記の点にあります。

  1. タプルを使うと、リストに比べてメモリを節約できる
  2. タプルはイミュータブル(変更できない)、リストはミュータブル(変更可能)
  3. タプルは辞書のキーとして使用可能、リストは辞書のキーとして使えない
  4. タプルは集合の要素として使用可能、リストは集合の要素として使えない

「1.」に関しては、あくまでも比較の問題でそこまで劇的な差はなく、タプルの使い所としては、2~4が大事なようです。

タプルは要素を変更できない(タプルはイミュータブル)

イミュータブル(変更不可)であることは、タプルの重要な属性です。

一度作成したら、その要素を変更したくない場合、誤って変更されてしまうのを防ぎたい場合は、リストではなくタプルを使います。

# リストはミュータブル(変更可能)
lst = [1, 2, 3]
lst[0] = 100
print(lst) # => [100, 2, 3]

# タプルはイミュータブル(変更不可)
tpl = (1, 2, 3)
tpl[0] = 100 # => TypeError: 'tuple' object does not support item assignment

しかしながら、あたかもタプルが変更されてしまったかのように見えるケースがあります。

  • 変数の再代入
  • タプル内のミュータブルな要素の変更は可能

それぞれ、もう少し詳しく検証してみます。

変数に再代入することでタプルが変更されてしまう・・・?

tpl = (1, 2, 3)
tpl = (100, 2, 3)
print(tpl)    # => (100, 2, 3)

このケースでは、タプルの1つ目の要素が 1 から 100 に変更されているように見えます。

でも、これは、変数tplに別の値を2回代入しているだけであって、挙動としては、次の例と同じです。

one = "一"
one = "壱"
print(one) # => "壱"

つまり、元のタプル「(1, 2, 3)」が変更されてしまったのではなく、同じ変数tplに対して、 新たに別のタプル「(100, 2, 3)」を再代入しているだけです。

念のため、変数tplが参照しているオブジェクトのIDを確認してみると、

tpl = (1, 2, 3)
print(id(tpl)) # => 140393758648288

tpl = (100, 2, 3)
print(id(tpl)) # => 140393758651160

別のタプルを代入したので、つまり、変数tplは別のオブジェクトを参照するようになったので、当然、参照先オブジェクトのIDも変わります。

次の例も同じです。

nums = (1, 2, 3)  # => 変数numsに(1, 2, 3)を代入
nums += (4, 5)    # => 変数numsに(1, 2, 3)+(4, 5)の結果である(1, 2, 3, 4, 5)を再代入
print(nums)       # =>  (1, 2, 3, 4, 5)

#元のタプル(1, 2, 3) が (1, 2, 3, 4, 5)に変更されたわけではない。
#(1, 2, 3, 4, 5)という別のタプルが作成されて、それが変数numsに代入されているだけ。
#再代入により、変数numsの値は (1, 2, 3) => (1, 2, 3, 4, 5) に変更
  • 元のタプルは変わってない
  • 新たに別のタプルを作成している

この2点は、こうすると分かりやすいと思います。

nums1_3 = (1, 2, 3)
nums4_5 = (4, 5)
nums1_5 = nums1_3 + nums4_5 

print(nums1_3) # => (1, 2, 3)       元のタプルの要素は変わってない
print(nums4_5) # => (4, 5)          元のタプルの要素は変わってない
print(nums1_5) # => (1, 2, 3, 4, 5) 新たに別のタプルが作成されている

この問題は、「変数は再代入が可能」という変数の挙動に関する話であって、タプルの挙動とは別の話になります。

タプル内のミュータブルな要素の変更は可能

このケースがややこしいのですが、リストや辞書はミュータブル、つまり、変更可能です。

そして、ミュータブルであるという属性は、リストや辞書がタプルの要素であったとしても変わりません。

traffic_lights = (["青", "黄", "赤"], {"green": "進め", "yellow": "注意して渡れ", "red": "止まれ"})

# タプル自体の変更は出来ない
traffic_lights[0] = ["緑", "黄", "赤"] # => TypeError: 'tuple' object does not support item assignment

#でも、リストや辞書は、タプルの要素であったとしても変更可能。
traffic_lights [0][0] = "緑"
traffic_lights [1]["yellow"] = "基本は止まれ"
print(traffic_lights) # => (['緑', '黄', '赤'], {'green': '進め', 'yellow': '基本は止まれ', 'red': '止まれ'})

上記の例は、タプルの要素が変わっているわけではなく、(タプルの要素になっている)リストや辞書の要素が変更されているだけです。

勿論、タプル内の要素であったとしても、リストや辞書の要素の削除や追加も可能です。

traffic_lights = (["青", "黄", "赤"], {"green": "進め", "yellow": "注意して渡れ", "red": "止まれ"})

#リストや辞書の要素を削除
traffic_lights[0].remove("黄")
traffic_lights[1].pop("yellow")
print(traffic_lights)  # => (['青', '赤'], {'green': '進め', 'red': '止まれ'})
lst = ["小", "中"]
dic = {"S": "small", "M": "Middle"}
size = (lst, dic)

print(size) # => (['小', '中'], {'S': 'small', 'M': 'Middle'})

#リストや辞書の要素を追加
lst.append("大")
dic["L"] = "Large"

print(size) # => (['小', '中', '大'], {'S': 'small', 'M': 'Middle', 'L': 'Large'})

タプルは辞書のkeyとして使用可能

これは便利かも!

と思ったのは、タプルを辞書のkeyとして使える点です。

# 10未満の正の整数に関して、該当する数字が全て揃っているかを確認する

# タプル
odd, even, prime, perfect = (1, 3, 5, 7, 9), (2, 4, 6, 8), (2, 3, 5, 7), (6, )
judge_nums = {
    odd    : "10未満の正の整数の全ての奇数です" , 
    even   : "10未満の正の整数の全ての偶数です" , 
    prime  : "10未満の正の整数の全ての素数です" , 
    perfect: "10未満の正の整数の全ての完全数です"
}
judge_nums[(1, 3, 5, 7, 9)] # => 10未満の正の整数の全ての奇数です
judge_nums[(2, 3, 5, 7)]    # => 10未満の正の整数の全ての素数です
judge_nums[(3, 5, 8)]       # => KeyError: (3, 5, 8)
judge_nums[(4, 6, 8, 2)]    # => KeyError: (4, 6, 8, 2) 順番が違う場合もKeyError

# リスト
odd, even, prime, perfect = [1, 3, 5, 7, 9], [2, 4, 6, 8], [2, 3, 5, 7], [6]
judge_nums = {
    odd    : "10未満の正の整数の全ての奇数です" , 
    even   : "10未満の正の整数の全ての偶数です" , 
    prime  : "10未満の正の整数の全ての素数です" , 
    perfect: "10未満の正の整数の全ての完全数です"
}
# => TypeError: unhashable type: 'list'
# そもそもリストを辞書のkeyにすることが出来ないので、この時点でTypeErrorが発生
#FizzBuzz問題(20以下の正の整数で)
def check_fizz_buzz(n):
    tpl = tuple(filter(lambda d: n%d == 0, (3, 5))) 
    dic = {(3, ): "Fizz", (5, ): "Buzz", (3, 5): "FizzBuzz"} # <= 辞書のkeyとしてタプルを使用
    return dic[tpl] if tpl in dic else n

nums = range(1, 21)
for n in nums:
    ans = check_fizz_buzz(n)
    result = f"{n} => {ans}"
    print(result)

#出力結果
#1 => 1
#2 => 2
#3 => Fizz
#4 => 4
#5 => Buzz
#6 => Fizz
#7 => 7
#8 => 8
#9 => Fizz
#10 => Buzz
#11 => 11
#12 => Fizz
#13 => 13
#14 => 14
#15 => FizzBuzz
#16 => 16
#17 => 17
#18 => Fizz
#19 => 19
#20 => Buzz

Wikipedia ― Fizz Buzz

いくつかの要素が全て順番通りに揃ったら特定の値を返す、というケースで便利に使えそうです。

タプルは集合の要素として使用可能

もう1つ、これも便利に使えそうかも、と思ったのが、集合の要素としてタプルを使える点です。

#積集合を求める
# タプル
fish, grass, seaweed = ("鯛", "鮪", "鮭"), ("よもぎ", "大葉", "バクチ―"), ("ワカメ", "ひじき", "昆布")
see_food, photosynthetic_organisms = {fish, seaweed}, {grass, seaweed}

see_food & photosynthetic_organisms # => {('ワカメ', 'ひじき', '昆布')}

# リスト
fish, grass, seaweed = ["鯛", "鮪", "鮭"], ["よもぎ", "大葉", "バクチ―"], ["ワカメ", "ひじき", "昆布"]
see_food, photosynthetic_organisms = {fish, seaweed}, {grass, seaweed} # => TypeError: unhashable type: 'list'
#そもそもリストを集合の要素にすることが出来ないので、この時点でTypeErrorが発生

こちらも、複雑な集合を扱う際に、ある程度タプルでグループ化して分類することで簡潔に集合を扱うことが出来そうです。

参考文献

この記事は以下の情報を参考にして執筆しました。

雑感

普段、特に何も考えず、「この場合はリスト、この場合はタプル」みたいな使い分けもせず、何となく使ってたタプルですが、今回、色々と調べて検証してみて、使い方を工夫することで、色々と便利に使えそうだということを知りました。

知ってるつもりの中に、知らないことはいっぱい埋まってるもんですね。

手を使って検証することの重要性を改めて身をもって実感しました。

内容に誤りやご意見等ありましたら、ぜひコメント頂ければ幸いです。

56
62
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
62