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

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

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

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

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

タプルとは

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

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

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

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

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

タプルとリストの違い

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

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

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

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

「イミュータブル」というのはあくまでも「オブジェクト id を変えずに要素を追加・変更・削除をすることができないこと」のみを表します。逆に言うと、次の 2 つはイミュータブルなオブジェクトでも特に問題なくできるので、ここを混同しないように注意してください。

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

ことばでの説明だけだと意味がわかりづらいので、以下、サンプルを使って説明します。

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

次の例を見てください。

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

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

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

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

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

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

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

# タプルは += の結果新しいタプルが生成される
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 要素に dict を持つタプルを定義する
# (要素数が 1 のタプルを定義するときは () 内の末尾に , を入れる必要あり)
t1 = ({'mark': 'ハート', 'number': '7'}, )

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

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

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

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

タプルの使いどころ

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

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

  • タプルの使いどころ 1: 変更を許可しない変数を定義する
  • タプルの使いどころ 2: dict のキーに使う
  • タプルの使いどころ 3: パフォーマンスを改善する

タプルの使いどころ 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 はタプルを使うことで比較的かんたんにこのようなロジックを実現することができます。

同様に、タプルは set() の要素としても利用できます。

タプルの使いどころ 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))

参考