Skip to content
This page has been auto-translated and may contain errors.View in English

タプルとセット

リストはすでに知っていますね。Pythonにはリストでは解決できない問題に対応する、もう2つのコレクション型があります。タプルは決して変更されない固定の値のグループを保持します。セットは一意の値のみを保持し、コレクションがどれだけ大きくなってもメンバーシップを即座にチェックできます。

Pythonのコレクションツールキットには4つの型があります。リストと辞書はほとんどの一般的なケースを扱います。タプルとセットは特定のケースを解決します:イミュータブル性が利点となる固定のレコードと、O(1)のメンバーシップテストが優先される一意の値のコレクションです。

listdict以外に、Pythonはtuple(イミュータブルな固定長シーケンス)とset(ハッシュテーブルに支えられた、ハッシュ可能なオブジェクトの順序のないユニークなコレクション)を提供しています。それぞれ異なるメモリモデル、ハッシュ可能性の特性、パフォーマンスプロファイルを持つので、選択する前に知っておく価値があります。

タプル

タプルは、作成後に変更できない順序付けされた値のグループです。括弧でタプルを定義しますが、これは省略可能です。実際にタプルにするのはカンマです。1要素のタプルには末尾にカンマが必要です。

タプルはイミュータブルなシーケンスです。タプルを作るのは括弧ではなく、カンマです。イミュータブル性により、要素もすべてハッシュ可能であればタプル自体もハッシュ可能となり、リストでは満たせないユースケースが開かれます:辞書のキー、セットのメンバー、固定構造のレコードなどです。

tupleは固定サイズのC配列に支えられたイミュータブルなシーケンスです。すべての要素がハッシュ可能な場合、__hash__は要素のハッシュから計算され、タプルを辞書のキーやセットのメンバーとして有効にします。__getitem__は整数とスライスをサポートします;__setitem__は実装されていないため、変更を試みるとTypeErrorが発生します。1要素形式の(42,)では末尾のカンマが必須です;カンマがなければ、括弧は単なるグループ化になります。

python
point      = (10, 20)
rgb        = (255, 128, 0)
dimensions = (1920, 1080)
single     = (42,)            # 1要素のタプルには末尾のカンマが必要
also_tuple = 42, 99           # 括弧は省略可能;カンマがタプルを作る

インデックスによるアクセスはリストとまったく同じように動作します。要素を変更しようとするとTypeErrorが発生します:

インデックス、スライス、負のインデックスはすべてリストと同様に動作します。インデックスによる代入を試みるとTypeErrorが発生します;これは意図的なものであり、制限ではありません。

整数とsliceオブジェクトを使った__getitem__listと同じクランプルールに従います。__setitem__はありません:タプル型はそれを登録していないため、解析時ではなく実行時にTypeErrorが発生します。

python
point = (10, 20)
point[0]    # 10
point[1]    # 20
point[-1]   # 20

point[0] = 99    # TypeError: 'tuple' object does not support item assignment

タプルを使うとき

タプルは、互いに関連していて変更されない小さな値のグループに対して使用します。座標(x, y)、色(r, g, b)、名前とスコアのペア("さくら", 87)などです。固定された構造は、コードを読む人にこのグループが単一の単位として扱われることを示します。

タプルは固定された構造を伝えます:位置が意味を持ち、グループが単位として扱われる値のグループです。ハッシュ可能性により、リストでは不可能な辞書のキーとしての使用が可能になります。タプルが示す契約は:これらの値は一緒に属しており、変更されないことが想定されている、というものです。

タプルは固定アリティのレコードに対する慣用的な型です。そのハッシュ可能性により、Hashableが必要なあらゆる場所で使用可能になります:辞書のキー、セットのメンバー、functools.lru_cacheの呼び出しシグネチャなど。意味的な契約はリストとは異なります:タプルは位置に意味を持つ異種混合のレコードを表し、リストは長さと順序が変化する同種のシーケンスを表します。

python
locations = {}
locations[(40, -74)] = "東京"   # タプルを辞書のキーとして使用、動作する
locations[[40, -74]] = "東京"   # リストを辞書のキーとして使用、TypeError

アンパック

アンパックはタプルから値を取り出し、それぞれを1行で独自の名前に代入します。名前の数は値の数と一致する必要があります。残りの項目をリストとして取得するには*を使用します。

アンパックはあらゆるイテラブルに対して動作します:タプル、リスト、文字列など。スター付きターゲットが可変長スライスをキャッチしない限り、ターゲット名の数はイテラブルの長さと一致する必要があります。不一致はValueErrorを発生させます。アンパックは関数からの複数の戻り値を消費する慣用的な方法です。

アンパックは右辺の__iter__を呼び出し、生成された各値を対応するターゲット名にバインドします。スター付きターゲット(*rest)は残りの項目をlistに集めます。不一致は実行時にValueErrorを発生させます。拡張アンパックはforヘッダー内でも動作します:for x, y in list_of_pairsは各反復項目をアンパックします。

python
point = (10, 20)
x, y  = point

print(x)   # 10
print(y)   # 20

first, *rest = [1, 2, 3, 4, 5]
# first = 1, rest = [2, 3, 4, 5]

head, *middle, tail = [1, 2, 3, 4, 5]
# head = 1, middle = [2, 3, 4], tail = 5

名前付きタプル

名前付きタプルは、各位置に名前があるタプルです。point[0]がx座標であることを覚える代わりに、point.xと書きます。値は依然としてイミュータブルです;数値の位置の代わりに、読みやすい属性名が得られるだけです。

namedtupleはタプルとまったく同じように動作するクラスを生成しますが、名前付き属性アクセスを追加します。完全なクラスより軽量で、イミュータブルで、自己文書化されています。プレーンなタプルの位置アクセスが理解されるためにコメントが必要となる場合に使用します。

collections.namedtupleはクラスファクトリです:名前付き属性アクセスがコンパイルされたtupleサブクラスを実行時に生成します。生成されたクラスには_asdict()_replace()_fieldsが含まれます。メモリフットプリントはプレーンなタプルと同じです。より細かい制御(デフォルト値、型注釈、オプションの可変性)には、dataclasses.dataclassが現代的な代替手段です;型注釈付きタプルにはtyping.NamedTupleが慣用的です。

名前付きタプルのインポート

namedtupleはPythonの標準ライブラリにありますが、インポートする必要があります。from collections import namedtupleの行はこのコースで最初のインポートです。インポートについてはモジュールの章で完全に扱われます。

python
from collections import namedtuple

Point  = namedtuple("Point",  ["x", "y"])
Player = namedtuple("Player", ["name", "score", "level"])

p = Point(10, 20)
p.x    # 10
p.y    # 20

sakura = Player("さくら", 87, 5)
sakura.name    # "さくら"
sakura.score   # 87

セット

セットは順序が保証されない一意の値のコレクションです。同じ値を2回追加しても何も起こりません:セットは各項目のコピーを1つだけ保持します。要素を持つセットには波括弧を使用し、空のセットを作成するにはset()を使用します。

setは重複を自動的に拒否する順序のないコレクションです。メンバーシップテストはサイズに関係なくO(1)であり、大きなコレクション全体で値の存在を確認する必要があるときに最適なツールになります。注意:{}は空のセットではなく空の辞書を作成します;そのためにはset()を使用してください。

setはハッシュテーブルに支えられたハッシュ可能なオブジェクトのユニークなコレクションです。メンバーシップテスト、挿入、削除はすべて平均O(1)です。反復順序は内部のハッシュ位置を反映しており、実行間で安定していません。ハッシュ可能なオブジェクトのみがメンバーになれます:intstrtupleは可能;listdictsetは不可。{}は空のセットではなく、空の辞書リテラルとして解析されます。

python
tags     = {"python", "beginner", "tutorial"}
numbers  = {1, 2, 3, 4, 5}
empty    = set()    # {} ではない(それは空の辞書)

同じ値を2回追加してもセットは変更されません:

python
tags.add("python")   # tagsは変更されない、"python"はすでに含まれている

セットを使うとき

セットは3つのことに最適なツールです:リストから重複を削除すること、大きなコレクションに何かが含まれているかを素早くチェックすること、そして2つのグループを比較して共有しているものや異なるものを見つけることです。

3つの異なるユースケースがセットの使用を促します:重複削除(挿入時に自動)、O(1)のメンバーシップテスト(listのO(n)に対して)、そしてセット代数(|&-^)。コレクションが大きく、メンバーシップを頻繁にチェックする場合、パフォーマンスの違いは大きいです。

3つの典型的なセットユースケースは、ハッシュテーブルの特性に直接マッピングされます:一意性(挿入時の重複拒否)、平均O(1)の__contains__(ハッシュ検索)、そしてセット代数(ダンダーメソッドを呼び出すビット演算スタイルの演算子)。O(1)のメンバーシップテストは最も実用的に重要です:10,000項目のセットに対するinは、10項目のセットと同じくらい高速です。

python
# リストから重複を削除
raw    = ["cat", "dog", "cat", "bird", "dog", "cat"]
unique = list(set(raw))   # ["cat", "dog", "bird"](順序は保証されない)
python
# 高速なメンバーシップチェック
valid_codes = {"USD", "EUR", "GBP", "JPY"}
code        = "EUR"

if code in valid_codes:    # 何千ものコードがあっても即座に検索
    print("Valid")

セット操作

セットは数学で学んだのと同じ操作をサポートします:和集合(どちらかのセットにあるすべて)、積集合(両方のセットが共有するもののみ)、差集合(一方が持っていてもう一方が持っていないもの)。Pythonはこれらに演算子記号を使い、それぞれにメソッドの同等物があります。

Pythonのセット演算子は数学的記法を反映しています:|は和集合、&は積集合、-は差集合、^は対称差です。各演算子にはメソッド形式(.union().intersection()など)があり、セットだけでなく任意のイテラブルも受け取ります。

セット演算子はダンダーメソッドを呼び出します:|__or__&__and__-__sub__^__xor__を呼び出します。演算子は両方のオペランドがセットであることを要求し、そうでなければTypeErrorを発生させます。メソッド形式は任意のイテラブルを受け取り、内部で変換します。インプレース形式(|=&=-=^=)は左オペランドを変更し、.update().intersection_update()などと等価です。

python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b    # {1, 2, 3, 4, 5, 6}   (和集合:どちらかにあるすべて)
a & b    # {3, 4}               (積集合:両方にあるもののみ)
a - b    # {1, 2}               (差集合:aにあるがbにない)
b - a    # {5, 6}               (逆方向の差集合)
a ^ b    # {1, 2, 5, 6}        (対称差:一方にあるが両方にはない)

これらにはメソッド形式もあります:.union().intersection().difference().symmetric_difference()

セットの変更

セットはミュータブルです。.add()は1つの項目を追加します。.update()は任意のリストやその他のイテラブルから複数の項目を一度に追加します。.remove()は項目を削除しますが、そこにない場合はエラーを発生させます。.discard()は項目が存在すれば静かに削除し、存在しなければ何もしません。

.add()は平均O(1)です。.update()は任意のイテラブルを受け取り、ループで.add()を呼び出すのと等価です。.remove()はミスでKeyErrorを発生させ、dict.__delitem__を反映します。.discard()は存在が不確実な場合の安全な選択です。.pop()は任意の要素を削除します—セットには順序がないため「最後」の要素ではありません。

.add(x)xをハッシュ化してテーブルに挿入します:平均O(1)。.update(iterable)|=と等価です。.remove()はミスでKeyErrorを発生させます。.discard()は先にハッシュ検索を行い、ミスで削除をスキップします。.pop()は挿入順ではなく、内部ハッシュテーブル状態によって決定される任意の要素を削除します。

python
tags = {"python", "beginner"}

tags.add("tutorial")          # 1つの項目を追加
tags.update(["web", "api"])   # 任意のイテラブルから複数の項目を追加
tags.remove("beginner")       # 削除、見つからなければKeyError
tags.discard("missing")       # 削除、見つからなくてもエラーなし
tags.pop()                    # 任意の項目を削除して返す
tags.clear()                  # すべてを削除

項目が存在するかわからない場合は.discard()を使用してください。

凍結セット

凍結セット(frozen set)は、作成後に変更できないセットです。これを使う主な理由:凍結セットはハッシュ可能なので、辞書のキーとして使ったり、他のセットの中に格納したりできます。

frozensetsetのイミュータブルな対応物です。すべての読み取り操作とセット代数をサポートしますが、変更はサポートしません。そのイミュータブル性によりハッシュ可能となり、辞書のキーや別のセット内のメンバーとして有効になります。

frozensetは要素ハッシュのソート済みリダクションから計算される__hash__を実装し、安定したハッシュ値を持ちます。すべてのセット代数演算子と新しいコレクションを返すメソッドはサポートされています;変更メソッド(addremoveなど)は定義されていません。frozensetは、実行時に変更されてはならず、辞書のキーとして使用する必要があるかもしれない定数ルックアップテーブルに適した型です。

python
valid_statuses = frozenset({"active", "paused", "deleted"})
valid_statuses.add("archived")    # AttributeError、frozensetはイミュータブル

適切なコレクションの選択

4つの型、それぞれに明確な役割があります。データで何をする必要があるかを尋ねれば、適切な選択は通常明らかになります。

コレクション型の選択は、どの操作が重要か、データにどのような制約があるかの問題です:可変性、順序付け、重複処理、検索戦略などです。

コレクション選択はパフォーマンスと意味論の決定です。dictsetはハッシュ化による平均O(1)の検索を提供します。listtupleはO(1)のインデックスアクセスを提供しますが、メンバーシップテストはO(n)です。tupleのイミュータブル性はハッシュ可能性をもたらします。frozensettupleは標準ライブラリ内の2つのハッシュ可能な複合型です。

listtuplesetdict
順序付けありありなしあり(挿入順)
ミュータブルはいいいえはいはい
重複ありありなしなし(キー)
アクセス方法インデックスインデックス該当なしキー
使う場面順序付けされた変更可能なシーケンス固定レコード一意の値、高速なメンバーシップキー・バリュー検索

簡単な決定ルール:

  • 名前で何かを検索する必要がある? → dict
  • 変更する順序付けされたコレクションが必要? → list
  • 関連する値の固定されたグループがある? → tuple
  • 一意の値や高速なメンバーシップテストが必要? → set

実践

固定レコードを格納するためのタプルと、一意の値を追跡するためのセットの使用:

python
home   = (35.6762, 139.6503)   # 緯度、経度
office = (35.6895, 139.6917)

home_lat, home_lon = home
print(f"Home: {home_lat}, {home_lon}")

# セットでユニークな訪問者を追跡
visitors = set()
visitors.add("さくら")
visitors.add("ゆうき")
visitors.add("さくら")    # すでにセットに含まれている、静かに無視される
visitors.add("ひかり")

print(f"Unique visitors: {len(visitors)}")
print(f"さくら visited: {'さくら' in visitors}")
print(f"だいち visited:  {'だいち' in visitors}")

すでに処理されたものを追跡し、残りの作業を計算するためのセットの使用:

python
already_processed = {"report_jan.csv", "report_feb.csv"}
all_files         = {"report_jan.csv", "report_feb.csv", "report_mar.csv", "report_apr.csv"}

to_process = all_files - already_processed
print(f"Files to process: {sorted(to_process)}")

for filename in sorted(to_process):
    print(f"Processing {filename}...")
    already_processed.add(filename)

print(f"Done. Total processed: {len(already_processed)}")

定数ルックアップテーブルにfrozensetを使用し、セット代数とO(1)のメンバーシップテストを実証:

python
ALLOWED_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE"})
SAFE_METHODS    = frozenset({"GET", "HEAD", "OPTIONS"})

# frozenset上のセット代数は通常のセットを返す
unsafe_allowed = ALLOWED_METHODS - SAFE_METHODS
print(f"Non-safe allowed methods: {unsafe_allowed}")

# frozensetはハッシュ可能なので、セット内に格納できる(プレーンなセットでは不可)
method_groups = {
    frozenset({"GET", "HEAD", "OPTIONS"}),
    frozenset({"POST", "PUT", "PATCH"}),
    frozenset({"DELETE"}),
}
print(f"Method groups: {len(method_groups)}")

method = "POST"
print(f"Allowed: {method in ALLOWED_METHODS}")
print(f"Safe:    {method in SAFE_METHODS}")

frozensetはO(1)検索を持ち、ハッシュ可能な型が必要な場所であればどこでも格納できます。2つのfrozensetオブジェクトに対するセット代数はプレーンなsetを返します;結果をイミュータブルに保つにはfrozenset()でラップしてください。