2017/12/01

Python のタプルとリストの違い、タプルの使いどころ

今回は基本に立ち返って、 Python の組み込み型である「タプル」と「リスト」の違い、それと「タプルの使いどころ」について説明してみたいと思います。

「 Python タプル リスト 違い 」といったキーワードで Google 検索すると、多くのページで「リストとタプルは記法が異なります」「タプルはイミュータブルです」という説明だけがなされていて、それだけだとなぜタプルが用意されているのか、タプルはどんなときに便利なのかがわからないのではないかと思い、今回このテーマで書いてみようと思いました。

前提として、 Python におけるリストが何なのかというのは読者の方はご存知だという前提でお話ししていきます。

そもそもタプルとは何ぞやというところからまずは見ていきましょう。

タプルとは


Python における「タプル」とは、複数のデータを一直線に並べた「コレクション」タイプのデータ型です。

tuple1 = ('東京都', '神奈川県', '大阪府')
type(tuple1) == tuple  # => True

tuple1[0]  # => '東京都'
tuple1[1:]  # => ('東京都', '大阪府')

見た目上はリストのかっこ []() に変わっただけです。定義の仕方や要素へのアクセス方法もリストとよく似ています。

ただし、タプルはリストとは異なり、いったんオブジェクトを生成した後に変更ができないオブジェクトとなっています。「変更ができない」というのは、厳密には「オブジェクト id を変えずに、要素を追加・変更・削除することができない」という意味です。この性質のことを「イミュータブル」( immutable )と言ったりします。ちなみに、イミュータブルの逆(変更ができるという意味の用語)は「ミュータブル」( mutable )です。

タプルとリストの違い


では、タプルはリストと何が異なるのでしょうか。

上で述べたばかりですが、「リストは変更ができるが、タプルは変更ができない」、これが両者の違いです。上の用語を使うなら、「リストはミュータブルであり、タプルはイミュータブルである」といいます。


「イミュータブル」が意味することとしないこと


タプルを利用する場合には、「タプルはイミュータブルである」ということばの意味を正しく理解しておく必要があります。

「イミュータブル」というのはあくまでも「オブジェクト id を変えずに要素を追加・変更・削除をすることができないこと」のみを表します。逆に言うと、

  • a. タプルを参照している変数に再代入すること
  • b. タプルの中の要素がミュータブルな場合にその要素そのものを変更すること

は特に問題なくできるので、この部分を混同しないように注意が必要です。ことばでの説明だけだと意味がわかりづらいので、以下、サンプルを使ってご説明してみます。

a. タプルを参照している変数に再代入すること

次の例を見てください。

t1 = ('ハート', '7')
t1 = ('ダイヤ', '10')

タプルは変更不可ですが、このコードは問題なく動作します。変数がタプルを参照していても、そこに別のオブジェクトを再代入することは可能です。

これは、ミュータブルかイミュータブルかというポイントというよりもむしろ「 Python には定数(再代入ができないもの)がない」ことと関係していると言えるでしょう。

紛らわしいのは、 += 演算を利用した場合の挙動です。次のコードを見るとタプルが変更可能なように見えるかと思うのですが、いかがでしょうか。

t1 = ('みかん', 'りんご')
t1 += ('バナナ', 'パイナップル')
print(t1)
# => ('みかん', 'りんご', 'バナナ', 'パイナップル')

実はここにはトリックがあって、 += の行では既存のタプルが変更されているのではなく、新しいタプルが生成されています。
左辺の t1 に再代入されているのは新しく作られたタプルオブジェクトです。

同じ += の演算でも、リストの場合はここのところがまったく異なるので注意が必要です。リストの場合は、 += 演算を行っても元のオブジェクトが維持されます。

# タプルは += の結果新しいタプルが生成される
t2 = t1 = ('みかん', 'りんご')
print(id(t1))    # => 4464365064

t1 += ('バナナ', 'パイナップル')
print(id(t1))    # => 4461411048
print(t1 is t2)  # => False

# 一方、リストは += で元のリストが維持される
l2 = l1 = ['みかん', 'りんご']
print(id(l1))    # => 4464250312

l1 += ['バナナ', 'パイナップル']
print(id(l1))    # => 4464250312
print(l1 is l2)  # => True

続いて b の方を見ていきましょう。

b. タプルの中の要素がミュータブルな場合にその要素そのものを変更すること

次のサンプルを見てください(要素数が 1 のタプルを定義するときは () 内の末尾に , を入れる必要があります)。

# 第 1 要素に dict を持つタプルを定義する
t1 = ({'mark': 'ハート', 'number': '7'}, )

# dict を変更する
t1[0]['mark'] = 'スペード'

print(t1)
# => ({'mark': 'スペード', 'number': '7'}, )

この例では、 dict を要素に持つタプル t1 を定義した後に、その dict を変更しています。 dict 型はミュータブルなので、このコードは特に問題なく動作します。

再代入によって「タプルが持つ参照」そのものを変更することと、「タプルが参照するオブジェクトの中身」を変更することは別であって、タプルが許容していないのは前者の変更のみです。参照されるオブジェクトがミュータブルであれば当然そのオブジェクトの中身は更新できるので、このあたりを混同しないよう注意してください。


タプルの使いどころ


では、なぜ Python にはリストとタプルというよく似た 2 つのコレクション型が用意されているのでしょうか。
リストは他のプログラミング言語で「配列」と呼ばれるものに近く、多くのプログラマーにとってより直観的です。リストだけあれば十分じゃないかとも思えます。

このあたりの疑問点はタプルの具体的な使いどころを見るとすっきり理解できるので、タプルを使うべき場面を具体的に見てましょう。

タプルの使いどころ 1: 変更を許可しない変数を定義する

変更を許可したくない変数を定義したい場合にタプルは便利です。

API_KEYS = ('xxx', 'yyy', 'zzz')

例えば、上のコードのように、外部サービスの API キーを格納したタプルを用意すると、再代入さえしなければ、中身が変わっていないことが保証されているという前提の下でその変数を使いまわすことができます。

関数型プログラミングに見られるような「思わぬ変更が生まれる可能性を最小化してバグの発生源を減らす」という考え方でプログラムを組みたい場合なんかにもタプルは便利です。

タプルの使いどころ 2: dict のキーに使う

dict のキーには hashable なオブジェクトのみが利用できます。

リストは hashable ではないので、 dict のキーに使うことはできません。

例えば、 Python で地図アプリのようなものを考えるとします。この場合、処理の途中で、緯度経度の情報をキーに、その建物の名前を値に入れた辞書を作りたくことがあるかもしれませんが、リストは hashable ではないので緯度経度の情報を格納するためには使えません。しかし、タプルは hashable なので、 dict のキーとして使用することができます。

# リストとタプルが hashable かどうかを確認する
from collections import Hashable

print(isinstance([], Hashable))  # => False
print(isinstance((), Hashable))  # => True

# 緯度経度の情報をキーに、建物名を値に入れた dict を用意する
locations = {
    (35.676, 139.744): '国会議事堂',
    (34.669, 135.431): 'ホグワーツ城',
    (35.039, 135.729): '鹿苑寺金閣',
}

dict 的なデータ型のキーに文字列しか受け付けないような言語の場合は、このような数字のペアに特別な意味がある場合でもそれをキーとすることはできませんが、 Python は組み込み型のタプルを使うことで比較的かんたんにこのようなロジックを実現することができます。

タプルの使いどころ 3: パフォーマンスをよりよくする

パフォーマンスの観点で、タプルはリストよりもよいようです。

ただし、よっぽどパフォーマンスが重要となるシビアな場面で無いかぎり、「リストじゃだめだ!やっぱりタプルじゃないと!」となるようなケースは稀なのではないかと思います。パフォーマンスの観点でデータ型を気にするのであれば、「リストかタプルか」の選択ではなく、もっと他の選択肢を選んだ方がよいような気がします。

・・・こんなところでしょうか。

ポイントは「変更ができないという特徴を活かす」「リストではできないことをやる」といったところになるかと思います。

Python のタプルについて理解を深めたいという方の参考になれば幸いです。

最後に、タプルの挙動を確認するユニットテストのサンプルコードを載せておきます。興味のある方は python3 -m unittest などで走らせてみてください。

# coding: utf-8

import unittest

class TupleBasicTest(unittest.TestCase):
    '''タプルの基本的な使い方を確認する
    '''

    def test_creation(self):
        # タプルを生成する
        t1 = ('ギン', 'ギラ', 'ギン')
        self.assertIsInstance(t1, tuple)

        # 実践的な場面で使うことはまずないが空のタプルを生成する
        t2 = ()
        self.assertIsInstance(t2, tuple)

        # 要素数が 1 のタプルについては `,` が必須となるため注意が必要
        t3 = ('さりげなし', )
        self.assertIsInstance(t3, tuple)
        s1 = ('さりげなし')
        self.assertIsInstance(s1, str)

    def test_access(self):
        # タプルの要素にアクセスする
        t1 = ('炎', '天', '下')

        self.assertEqual(t1[0], '炎')
        self.assertEqual(t1[1], '天')

        self.assertEqual(t1[-1], '下')

        self.assertEqual(t1[0:2], ('炎', '天'))

    def test_conversion(self):
        l1 = ('岡山', 15)
        self.assertIsInstance(tuple(l1), tuple)

        t1 = ('香川', 18)
        self.assertEqual(list(t1), ['香川', 18])

    def test_methods(self):
        self.assertEqual((3, 4, 4, 5).count(4), 2)
        self.assertEqual((3, 4, 4, 5).index(5), 3)

    def test_in(self):
        self.assertTrue(3 in (3, 4, 4, 5))
        self.assertFalse(108 in (3, 4, 4, 5))


参考

3. Data model — Python 3.6.3 documentation
I'm able to use a mutable object as a dictionary key in python. Is this not disallowed? - Stack Overflow