15. タプルと range:シンプルな不変シーケンス
第14章では、Python の多用途で可変(mutable)なシーケンス型であるリスト(list)を学びました。ここからは、ほかの重要なシーケンス型である タプル(tuple) と range(range) を見ていきます。リストは時間とともに変化するコレクションの保存に優れている一方、タプルはデータを変更から守る不変(immutable)シーケンスを提供し、range は数の並びをメモリ効率よく表現する方法を提供します。
どのシーケンス型をいつ使うべきかを理解すると、プログラムはより効率的で安全になり、意図も明確になります。この章の終わりまでに、タプルと range を効果的に扱えるようになり、Python のすべてのシーケンス型で共通して使える操作も理解できるようになります。
15.1) タプルの作成と利用(カンマの重要性)
タプル(tuple) は、順序付きで不変(immutable)な要素の並びです。リストと同様に、タプルにはどんな型のデータでも入れられ、要素の順序も保持されます。しかしリストと違い、タプルはいったん作成すると中身を変更できません。
括弧でタプルを作る
タプルを作る最も一般的な方法は、カンマ区切りの値を丸括弧で囲むことです。
# 学生のテスト得点のタプル
scores = (85, 92, 78, 95)
print(scores) # Output: (85, 92, 78, 95)
print(type(scores)) # Output: <class 'tuple'>
# 異なるデータ型を混在させたタプル
student_info = ("Alice", 20, "Computer Science", 3.8)
print(student_info) # Output: ('Alice', 20, 'Computer Science', 3.8)
# 空のタプル
empty = ()
print(empty) # Output: ()
print(len(empty)) # Output: 0タプルはリテラル構文として丸括弧 () を使い、リストは角括弧 [] を使います。この見た目の違いにより、扱っている型をすぐに見分けられます。
タプルを作るのは括弧ではなくカンマ
ここが初心者の多くが驚く重要なポイントです。タプルを実際に作っているのは括弧ではなくカンマです。括弧は省略できることが多く、主にタプルを見やすくしたり、式の中でグルーピングしたりする目的で使われます。
# これらはすべて同じタプルを作ります
coordinates_1 = (10, 20)
coordinates_2 = 10, 20 # 括弧は不要!
print(coordinates_1) # Output: (10, 20)
print(coordinates_2) # Output: (10, 20)
print(coordinates_1 == coordinates_2) # Output: True
# 重要なのはカンマです
x = (42) # これは括弧で囲まれた整数 42 にすぎません
y = (42,) # これは要素が1つのタプルです
print(type(x)) # Output: <class 'int'>
print(type(y)) # Output: <class 'tuple'>
print(y) # Output: (42,)(42) の括弧は、数学の式と同じようなグルーピング用の括弧にすぎません。要素が1つのタプルを作るには、末尾のカンマを 必ず 付ける必要があります: (42,)。このカンマが、グループ化した式ではなくタプルを求めていることを Python に伝えます。
括弧が必要な場合
カンマがタプルを作るとはいえ、曖昧さを避けるために特定の状況では括弧が必要になります。
# 括弧がないと紛らわしくなります
def get_dimensions():
return 1920, 1080 # タプルを返します
width, height = get_dimensions()
print(f"Screen: {width}x{height}") # Output: Screen: 1920x1080
# 関数の引数としてタプルを渡すときは括弧が必要です
print((1, 2, 3)) # Output: (1, 2, 3)
# 括弧がないと、Python は3つの別々の引数だと解釈します
# 複雑な式では括弧が必要です
result = (10, 20) + (30, 40) # タプルの連結
print(result) # Output: (10, 20, 30, 40)要素が1つのタプルを作る
要素が1つのタプルで末尾カンマが必要だという点は、初心者がよく戸惑うところです。
# よくあるミス: カンマを忘れる
not_a_tuple = ("Python")
print(type(not_a_tuple)) # Output: <class 'str'>
print(not_a_tuple) # Output: Python
# 正しい: 末尾にカンマを付ける
is_a_tuple = ("Python",)
print(type(is_a_tuple)) # Output: <class 'tuple'>
print(is_a_tuple) # Output: ('Python',)
# 括弧がなくてもカンマは有効です
also_a_tuple = "Python",
print(type(also_a_tuple)) # Output: <class 'tuple'>
print(also_a_tuple) # Output: ('Python',)なぜ Python はこの一見ぎこちない構文を要求するのでしょうか?それは、Python において丸括弧には別の意味—式をグループ化する—がすでにあるからです。カンマがなければ、(42) が「数のグルーピング」なのか「タプル」なのかを Python は区別できません。
タプル要素へのアクセス
タプルはリストと同じインデックス参照とスライス操作をサポートします。
# 学生情報のタプル
student = ("Bob", 22, "Physics", 3.6)
# 個別要素へのアクセス(0始まり)
name = student[0]
age = student[1]
major = student[2]
gpa = student[3]
print(f"{name} is {age} years old") # Output: Bob is 22 years old
print(f"Major: {major}, GPA: {gpa}") # Output: Major: Physics, GPA: 3.6
# 負のインデックスも使えます
last_item = student[-1]
print(f"Last item: {last_item}") # Output: Last item: 3.6
# スライスは新しいタプルを取り出します
first_two = student[:2]
print(first_two) # Output: ('Bob', 22)
print(type(first_two)) # Output: <class 'tuple'>第14章でリストに対して学んだインデックス参照やスライスのテクニックは、タプルでも同じように使えます。重要な違いは、タプルは作成後に変更できないことです。
15.2) タプルのパックとアンパック
タプルの最も強力で洗練された機能の1つが、複数の値をまとめてパックし、それらを別々の変数へアンパックできることです。この機能により、Python コードは驚くほど簡潔で読みやすくなります。
タプルのパック
タプルのパック(tuple packing) は、複数の値をカンマで区切って並べることでタプルを作るときに起こります。
# 値をタプルにパックする
coordinates = 10, 20, 30
print(coordinates) # Output: (10, 20, 30)
# 異なる型をパックする
user_data = "Alice", 25, "alice@example.com"
print(user_data) # Output: ('Alice', 25, 'alice@example.com')
# 関数の返り値をパックする
def get_statistics(numbers):
total = sum(numbers)
count = len(numbers)
average = total / count
return total, count, average # 3つの値をタプルにパックする
stats = get_statistics([85, 90, 78, 92, 88])
print(stats) # Output: (433, 5, 86.6)関数がカンマ区切りで複数の値を返すと、Python は自動的にそれらをタプルにパックします。これが、関数が複数の値を返しているように見える理由です。実際には、それらの値を含む1つのタプルを返しています。
タプルのアンパック
タプルのアンパック(tuple unpacking) は逆の処理で、タプルから値を取り出して別々の変数へ代入します。
# 基本的なアンパック
point = (100, 200)
x, y = point
print(f"x = {x}, y = {y}") # Output: x = 100, y = 200
# アンパックはタプルに限らず、あらゆるシーケンスで動きます
name, age, email = ["Bob", 30, "bob@example.com"]
print(f"{name} is {age} years old") # Output: Bob is 30 years old
# 関数の返り値を直接アンパックする
total, count, average = get_statistics([95, 88, 92, 85])
print(f"Average of {count} scores: {average}") # Output: Average of 4 scores: 90.0左辺の変数の数は、シーケンス内の要素数と一致している必要があります。一致しない場合、Python は ValueError を送出します。
# これはエラーになります
coordinates = (10, 20, 30)
# x, y = coordinates # ValueError: too many values to unpack (expected 2)
# これもエラーになります
point = (5, 10)
# x, y, z = point # ValueError: not enough values to unpack (expected 3, got 2)タプルのアンパックで変数を入れ替える
タプルのアンパックを使うと、一時変数を使わずに変数の値を入れ替えるエレガントな方法が実現できます。
# 一時変数を使った従来の入れ替え
a = 10
b = 20
temp = a
a = b
b = temp
print(f"a = {a}, b = {b}") # Output: a = 20, b = 10
# タプルのアンパックを使った Python らしい入れ替え
x = 100
y = 200
x, y = y, x # 1行で入れ替え!
print(f"x = {x}, y = {y}") # Output: x = 200, y = 100
# 2つ以上の変数を入れ替える
first = "A"
second = "B"
third = "C"
first, second, third = third, first, second
print(first, second, third) # Output: C A Bこれはどう動いているのでしょうか?Python はまず右辺を評価してタプル (y, x) を作り、それを左辺の変数へアンパックします。これは1ステップで起こるため、一時変数は不要です。
* 演算子を使った拡張アンパック
Python では * 演算子を使って複数要素をまとめて受け取る 拡張アンパック(extended unpacking) ができます。
# 「残り」を受け取る変数を使ったアンパック
scores = (95, 88, 92, 85, 90, 87)
first, second, *rest = scores
print(f"Top two: {first}, {second}") # Output: Top two: 95, 88
print(f"Others: {rest}") # Output: Others: [92, 85, 90, 87]
print(type(rest)) # Output: <class 'list'>
# スターはどこに置いても構いません
numbers = (1, 2, 3, 4, 5)
first, *middle, last = numbers
print(f"First: {first}") # Output: First: 1
print(f"Middle: {middle}") # Output: Middle: [2, 3, 4]
print(f"Last: {last}") # Output: Last: 5
# 先頭側をまとめて受け取る
*beginning, second_last, last = numbers
print(f"Beginning: {beginning}") # Output: Beginning: [1, 2, 3]
print(f"Last two: {second_last}, {last}") # Output: Last two: 4, 5スター付きの変数は、タプルからアンパックしている場合でも、要素を リスト(list) として受け取る点に注意してください。受け取る要素がない場合は、スター付きの変数は空のリストになります。
# 受け取る要素がない場合
a, b, *rest = (10, 20)
print(rest) # Output: []
# アンパックでスターは1つだけ使えます
# first, *middle, *end = (1, 2, 3, 4) # SyntaxError: multiple starred expressionsアンダースコアで値を無視する
タプルから一部の値だけが必要な場合もあります。慣習として、Python プログラマは無視したい値を示す変数名としてアンダースコア _ を使います。
# 日付文字列の解析
date_string = "2024-03-15"
year, month, day = date_string.split("-")
print(f"Month: {month}") # Output: Month: 03
# 月だけが欲しい場合
_, month, _ = date_string.split("-")
print(f"Month: {month}") # Output: Month: 03
# 拡張アンパックと組み合わせる
data = ("Alice", 25, "Engineer", "New York", "alice@example.com")
name, age, *_, email = data
print(f"{name} ({age}): {email}") # Output: Alice (25): alice@example.comアンダースコアは単なる通常の変数名ですが、これを使うことで、意図的にその値を無視していることを他のプログラマ(そして自分自身)に示せます。
パックとアンパックの実用例
# 計算結果を複数値として返す
def calculate_rectangle_properties(width, height):
"""長方形の面積と周長を計算する。"""
area = width * height
perimeter = 2 * (width + height)
return area, perimeter # パック
# 結果をアンパックする
rect_area, rect_perimeter = calculate_rectangle_properties(5, 3)
print(f"Area: {rect_area}, Perimeter: {rect_perimeter}") # Output: Area: 15, Perimeter: 16
# アンパックしながら反復する
students = [
("Alice", 85),
("Bob", 92),
("Carol", 78)
]
for name, score in students: # ループ内でアンパック
print(f"{name}: {score}")
# Output:
# Alice: 85
# Bob: 92
# Carol: 78タプルのパックとアンパックは、Python コードをより読みやすく表現豊かにします。インデックスで要素にアクセスする(student[0]、student[1])代わりに、意味のある名前の変数にアンパックできます。
15.3) タプルは不変:それが役立つ場面
タプルを定義づける特徴は 不変性(immutability) です。いったん作成すると、タプルの内容は変更できません。要素を追加・削除・変更することはできません。この不変性は制約のように見えるかもしれませんが、重要な利点をもたらします。
実際に不変性が意味すること
# タプルを作成する
coordinates = (10, 20, 30)
print(coordinates) # Output: (10, 20, 30)
# 変更しようとするとエラーになる
# coordinates[0] = 15 # TypeError: 'tuple' object does not support item assignment
# 要素を追加しようとするとエラーになる
# coordinates.append(40) # AttributeError: 'tuple' object has no attribute 'append'
# 要素を削除しようとするとエラーになる
# del coordinates[1] # TypeError: 'tuple' object doesn't support item deletionPython が「タプルは item assignment をサポートしない」と言うとき、それはタプル内のどの位置に保存されているものも変更できないという意味です。タプルの構造は作成時に固定されます。
可変のリストと不変のタプルを比べる
# リストは可変 - 変更できます
shopping_list = ["milk", "bread", "eggs"]
shopping_list[1] = "butter" # 要素を変更する
shopping_list.append("cheese") # 要素を追加する
print(shopping_list) # Output: ['milk', 'butter', 'eggs', 'cheese']
# タプルは不変 - 変更できません
product_dimensions = (10, 20, 5) # 幅、高さ、奥行き(cm)
# product_dimensions[0] = 12 # TypeError: cannot modify
# product_dimensions.append(3) # AttributeError: no append method
# タプルを「変更」するには、新しいものを作る必要があります
new_dimensions = (12, 20, 5) # まったく新しいタプルを作成する
print(new_dimensions) # Output: (12, 20, 5)不変性が役立つ理由
不変性にはいくつかの実用的な利点があります。
1. データの整合性と安全性
タプルを関数に渡すとき、その関数が誤ってデータを変更できないことが分かります。
def calculate_distance(point1, point2):
"""2次元の2点間の距離を計算する。"""
x1, y1 = point1
x2, y2 = point2
dx = x2 - x1
dy = y2 - y1
# たとえ変更したくても、入力タプルは変更できません
return (dx**2 + dy**2) ** 0.5
start = (0, 0)
end = (3, 4)
distance = calculate_distance(start, end)
print(f"Distance: {distance}") # Output: Distance: 5.0
print(f"Start point unchanged: {start}") # Output: Start point unchanged: (0, 0)リストだと、関数がデータを変更してしまわないか心配する必要があります。タプルなら、変更されないという保証があります。
2. タプルを辞書キーとして使う
第17章でさらに詳しく扱いますが、辞書(dictionary)のキーは ハッシュ可能(hashable) である必要があります。つまり、決して変わらないハッシュ値を持つ必要があります。タプルのような不変オブジェクトは辞書キーになれますが、リストのような可変オブジェクトはなれません。
# タプルは辞書キーにできます
locations = {
(0, 0): "Origin",
(10, 20): "Point A",
(30, 40): "Point B"
}
print(locations[(10, 20)]) # Output: Point A
# リストは辞書キーにできません
# locations_bad = {
# [0, 0]: "Origin" # TypeError: unhashable type: 'list'
# }3. 意図を示す
リストではなくタプルを使うことで、このデータは変えるべきではないという意図を他のプログラマ(そして自分自身)に伝えられます。
# RGB カラー値 - これらは決して変わるべきではありません
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
# データベース接続パラメータ - 固定の設定
DB_CONFIG = ("localhost", 5432, "myapp", "production")
# 地理座標 - 位置は変わりません
EIFFEL_TOWER = (48.8584, 2.2945) # 緯度、経度コード中でタプルを見ると、このデータは一定のままであるべきだとすぐ分かります。リストを見ると、変更される可能性があると分かります。
4. パフォーマンス上の利点
タプルは不変なので、Python はリストにはできない方法で最適化できます。第27章で sys モジュールを学びますが、ここでは sys.getsizeof() がオブジェクトのメモリ使用量を教えてくれる、ということだけ知っておいてください。
import sys
# タプルは同等のリストよりメモリ使用量が少ない
tuple_data = (1, 2, 3, 4, 5)
list_data = [1, 2, 3, 4, 5]
print(f"Tuple size: {sys.getsizeof(tuple_data)} bytes") # Output: Tuple size: 80 bytes (may vary by Python version)
print(f"List size: {sys.getsizeof(list_data)} bytes") # Output: List size: 104 bytes (may vary by Python version)
# タプルの作成は高速
import timeit
tuple_time = timeit.timeit("(1, 2, 3, 4, 5)", number=1000000)
list_time = timeit.timeit("[1, 2, 3, 4, 5]", number=1000000)
print(f"Tuple creation: {tuple_time:.4f} seconds")
print(f"List creation: {list_time:.4f} seconds")
# Example output: Tuple creation: 0.0055 seconds, List creation: 0.0292 seconds15.4) 不変性の落とし穴:タプルが可変アイテムを含むとき
タプルそのものは不変ですが、内部にリストや辞書のような可変オブジェクトを含めることはできます。これにより、微妙ですが重要な違いが生まれます。タプルの構造は固定ですが、内部の可変オブジェクトの中身は変えられるのです。
違いを理解する
# リストを含むタプル
student_data = ("Alice", 20, [85, 90, 78]) # 名前、年齢、得点
print(student_data) # Output: ('Alice', 20, [85, 90, 78])
# タプルの要素を再代入することはできません
# student_data[0] = "Bob" # TypeError: 'tuple' object does not support item assignment
# しかし、タプル内のリストは変更できます
student_data[2].append(92) # 新しい得点を追加
print(student_data) # Output: ('Alice', 20, [85, 90, 78, 92])
student_data[2][0] = 88 # 既存の得点を変更
print(student_data) # Output: ('Alice', 20, [88, 90, 78, 92])ここでは何が起きているのでしょうか?タプルは3つの参照を保存しています。文字列 "Alice" への参照、整数 20 への参照、そしてリストオブジェクトへの参照です。タプルの構造—どのオブジェクトを参照するか—は変えられません。しかしリストオブジェクト自体は可変なので、その中身は変えられます。
違いを可視化する
# タプルの構造は固定
data = ("Python", [1, 2, 3])
# これはタプルが参照する先を変えようとします - 許可されません
# data[1] = [4, 5, 6] # TypeError
# これはタプルが参照しているリストを変更します - 許可されます
data[1].append(4)
print(data) # Output: ('Python', [1, 2, 3, 4])
# タプルは同じリストオブジェクトを参照し続けています
# 変わったのはリストの中身だけで、タプルが指すリスト自体は変わっていませんこう考えると分かりやすいでしょう。タプルは箱が横に並んだようなもので、各箱にはオブジェクトへの参照が入っています。箱自体は固定(不変)ですが、箱が可変オブジェクトへの参照を持っていれば、そのオブジェクトは変えられます。
辞書を含むタプル
同じ原理は、タプル内の辞書にも当てはまります。
# 辞書を含むタプル
user_profile = ("alice", {"email": "alice@example.com", "age": 25})
print(user_profile) # Output: ('alice', {'email': 'alice@example.com', 'age': 25})
# タプルが参照する辞書そのものを差し替えることはできません
# user_profile[1] = {"email": "newemail@example.com"} # TypeError
# しかし、辞書自体は変更できます
user_profile[1]["age"] = 26
user_profile[1]["city"] = "New York"
print(user_profile) # Output: ('alice', {'email': 'alice@example.com', 'age': 26, 'city': 'New York'})これが辞書キーで重要になる理由
タプルが辞書キーとして使えるのは、すべての要素がハッシュ可能な場合に限られます。
タプル自体は不変でも、可変オブジェクト(リストなど)を含むタプルはそもそもハッシュ可能ではないため、辞書キーとして使えません。
# これは動きますが危険です
tuple_with_list = ("key", [1, 2, 3])
# data = {tuple_with_list: "value"} # TypeError: unhashable type: 'list'辞書キーには、完全に不変のオブジェクト(文字列、数値、frozensets、他のタプル)だけを含むタプルを使ってください。
真に不変なタプルを作る
完全に不変なタプルが必要なら、中身もすべて不変であることを確認してください。
# 完全に不変なタプル - 不変型のみ
point_3d = (10, 20, 30) # すべて整数
rgb_color = (255, 128, 0) # すべて整数
coordinates = ((10, 20), (30, 40)) # タプルのタプル
# これらは辞書キーとして安全に使えます
color_names = {
(255, 0, 0): "Red",
(0, 255, 0): "Green",
(0, 0, 255): "Blue"
}
# ネストしたタプルも不変のままです
nested = ((1, 2), (3, 4))
# nested[0][0] = 5 # TypeError: 'tuple' object does not support item assignment可変の中身が意図的な場合
場合によっては、可変の中身を持つタプルが実際に必要なこともあります。たとえば、レコード構造は固定にしたいが、あるフィールドだけは変化させたいときです。
# 身元は固定だが成績は変化する学生レコード
def create_student(name, student_id):
"""空の成績リストを持つ学生レコードを作成する。"""
return (name, student_id, []) # 名前とIDは固定、成績は変化しうる
student = create_student("Alice", "S12345")
print(student) # Output: ('Alice', 'S12345', [])
# 学生の身元は固定
print(f"Student: {student[0]} (ID: {student[1]})") # Output: Student: Alice (ID: S12345)
# しかし、成績は獲得に応じて追加できます
student[2].append(85)
student[2].append(92)
student[2].append(78)
print(f"Grades: {student[2]}") # Output: Grades: [85, 92, 78]
# タプル構造によって名前とIDが誤って変えられるのを防ぎつつ
# 成績リストは増やせるようにしていますこのパターンは、あるデータは保護しつつ別のデータは変えたいときに有用です。ただし、タプルの不変性と中身の可変性の違いを意識しておいてください。
15.5) リストではなくタプルを使うべきとき
タプルとリストのどちらを使うかは重要な設計判断です。どちらもシーケンスですが、目的や意図の伝え方が異なります。
固定長で異種混在のデータにはタプル
タプルは、1つの論理的な実体を表す固定個数の要素があるときに最適です。多くの場合、要素の型は異なります。
# 学生レコード: 名前、年齢、専攻、GPA
student = ("Alice", 20, "Computer Science", 3.8)
# 地理座標: 緯度、経度
location = (40.7128, -74.0060) # New York City
# RGB カラー: 赤、緑、青
color = (255, 128, 0)
# データベース接続: host、port、database、username
db_connection = ("localhost", 5432, "myapp", "admin")
# 日付: 年、月、日
date = (2024, 3, 15)各タプルは「レコード」として完結しており、各要素の位置には特定の意味があります。1番目は常に名前、2番目は常に年齢、という具合です。
同種のコレクションにはリスト
リストは、似た項目が可変個数あり、追加・削除・並べ替えをする可能性があるときに最適です。
# 買い物リスト - 同じ型(文字列)の要素
shopping_list = ["milk", "bread", "eggs", "butter"]
shopping_list.append("cheese") # 必要なら項目を追加
shopping_list.remove("bread") # 項目を削除
# テスト得点 - 同じ型(数値)の要素
test_scores = [85, 92, 78, 95, 88]
test_scores.append(90) # 新しい得点を追加
test_scores.sort() # 得点を並べ替え
# ユーザー名 - 同じ型(文字列)の要素
active_users = ["alice", "bob", "carol"]
active_users.extend(["dave", "eve"]) # 複数ユーザーを追加リストは、要素数が変わりうるコレクションで、各要素が同じ役割を持つときに使います。
関数の返り値にはタプル
関数が関連する複数の値を返すとき、タプルは自然な選択です。
def get_user_info(user_id):
"""データベースからユーザー情報を取得する。"""
# Simulate database lookup
return "Alice", "alice@example.com", 25, "New York"
# 返ってきたタプルをアンパックする
name, email, age, city = get_user_info(101)
print(f"{name} from {city}") # Output: Alice from New York
def calculate_statistics(numbers):
"""数値の min、max、平均を計算する。"""
if not numbers:
return None, None, None
minimum = min(numbers)
maximum = max(numbers)
average = sum(numbers) / len(numbers)
return minimum, maximum, average
# 結果をアンパックする
min_val, max_val, avg_val = calculate_statistics([85, 92, 78, 95, 88])
print(f"Range: {min_val} to {max_val}, Average: {avg_val}")
# Output: Range: 78 to 95, Average: 87.6タプルを返すことで、これらの値が関連しており、まとめて扱うべきだということが明確になります。
辞書キーにはタプル
辞書で複合キーが必要なとき、タプルは不可欠です。
# 科目と学期ごとの学生の成績
grades = {
("CS101", "Fall2023"): 85,
("CS101", "Spring2024"): 90,
("MATH201", "Fall2023"): 88,
("MATH201", "Spring2024"): 92
}
# 特定の成績を参照する
course = "CS101"
semester = "Spring2024"
grade = grades[(course, semester)]
print(f"Grade in {course} ({semester}): {grade}") # Output: Grade in CS101 (Spring2024): 90
# グリッド座標を辞書キーにする
grid = {
(0, 0): "Start",
(5, 3): "Obstacle",
(10, 10): "Goal"
}
position = (5, 3)
if position in grid:
print(f"At {position}: {grid[position]}") # Output: At (5, 3): Obstacleリストは可変なので辞書キーにはできませんが、タプルはできます。
不変の設定データにはタプル
変更されるべきではない設定データには、タプルを使うことでその意図を示せます。
# 変わるべきではないアプリ設定
APP_CONFIG = (
"MyApp", # Application name
"1.0.0", # Version
"production", # Environment
True, # Debug mode
8080 # Port
)
# UI 用のカラーパレット - 色は固定
COLOR_PALETTE = (
(255, 0, 0), # Primary red
(0, 128, 255), # Primary blue
(255, 255, 255), # White
(0, 0, 0) # Black
)
# API エンドポイント - これらの URL は変わりません
API_ENDPOINTS = (
"https://api.example.com/users",
"https://api.example.com/products",
"https://api.example.com/orders"
)判断ガイド
# タプルを使うのは:
# 1. 固定構造の単一レコードを表すデータの場合
employee = ("E001", "Alice", "Engineering", 75000)
# 2. 関数から複数の値を返す場合
def divide_with_remainder(a, b):
return a // b, a % b
# 3. 辞書キーとして使う必要がある場合
cache = {(5, 10): 50, (3, 7): 21}
# 4. データを変更すべきではない場合
SCREEN_RESOLUTION = (1920, 1080)
# リストを使うのは:
# 1. 同種の項目のコレクションで、変わる可能性がある場合
tasks = ["Write code", "Test code", "Deploy code"]
tasks.append("Document code")
# 2. 追加・削除・並べ替えが必要な場合
scores = [85, 90, 78]
scores.sort()
scores.append(92)
# 3. すべての要素が同じ目的を持つ場合
usernames = ["alice", "bob", "carol"]
# 4. コレクションのサイズが事前に分からない場合
results = []
for i in range(10):
results.append(i * 2)15.6) range オブジェクトを深く理解する
タプルとリストをいつ使い分けるかが分かったところで、Python の3つ目の不変シーケンス型である range を見ていきましょう。range(range) 型は、不変な数値シーケンスを表します。すべての要素をメモリに保持するリストやタプルとは異なり、range オブジェクトは必要に応じて数値を生成するため、大きな数列を表現するのに非常にメモリ効率がよいです。
range オブジェクトの作成
range() 関数は、次の3つの形で range オブジェクトを作成します。
# 引数が1つ: range(stop)
# 0 から stop の直前までの数を生成します
numbers = range(5)
print(list(numbers)) # Output: [0, 1, 2, 3, 4]
# 引数が2つ: range(start, stop)
# start から stop の直前までの数を生成します
numbers = range(2, 7)
print(list(numbers)) # Output: [2, 3, 4, 5, 6]
# 引数が3つ: range(start, stop, step)
# start から stop の直前まで step ずつ増やして生成します
numbers = range(0, 10, 2)
print(list(numbers)) # Output: [0, 2, 4, 6, 8]
# 逆向きに数えるための負の step
numbers = range(10, 0, -1)
print(list(numbers)) # Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]range の中身を確認するために list() でリストへ変換している点に注意してください。range オブジェクトそのものを print しても、すべての値は表示されません。
r = range(5)
print(r) # Output: range(0, 5)
print(type(r)) # Output: <class 'range'>range オブジェクトの仕組み
range オブジェクトはすべての値をメモリに保持しません。代わりに、必要になったときに各値を計算します。
import sys
# 100万個の数を表す range
large_range = range(1000000)
print(f"Range size: {sys.getsizeof(large_range)} bytes") # Output: Range size: 48 bytes (may vary by Python version)
# 100万個の数を含むリスト
large_list = list(range(1000000))
print(f"List size: {sys.getsizeof(large_list)} bytes") # Output: List size: 8000056 bytes (approximately 8MB)
# range は小さく、リストは巨大です!range オブジェクトが保持しているのは start、stop、step の3つだけです。シーケンスの各数値は、求められたときに計算されます。これにより range は大きな数列に対して非常に効率的になります。
for ループで range を使う
第12章で学んだとおり、range は for ループと一緒に使われることが最も多いです。
# 0 から 4 まで数える
for i in range(5):
print(f"Count: {i}")
# Output:
# Count: 0
# Count: 1
# Count: 2
# Count: 3
# Count: 4
# 1 から 10 まで数える
for i in range(1, 11):
print(i, end=" ")
print() # Output: 1 2 3 4 5 6 7 8 9 10
# 2 ずつ数える
for i in range(0, 20, 2):
print(i, end=" ")
print() # Output: 0 2 4 6 8 10 12 14 16 18
# 逆向きに数える
for i in range(5, 0, -1):
print(f"T-minus {i}")
# Output:
# T-minus 5
# T-minus 4
# T-minus 3
# T-minus 2
# T-minus 1range オブジェクトのインデックス参照とスライス
range オブジェクトは、他のシーケンスと同じようにインデックス参照とスライスが可能です。
# range を作成する
numbers = range(10, 50, 5) # 10, 15, 20, 25, 30, 35, 40, 45
# インデックス参照
print(numbers[0]) # Output: 10
print(numbers[3]) # Output: 25
print(numbers[-1]) # Output: 45
# スライスは新しい range を返します
subset = numbers[2:5]
print(subset) # Output: range(20, 35, 5)
print(list(subset)) # Output: [20, 25, 30]
# 長さ
print(len(numbers)) # Output: 8含まれているかの確認
in 演算子を使って、数値が range に含まれるか確認できます。
# 0 から 20 までの偶数
evens = range(0, 21, 2)
print(10 in evens) # Output: True
print(15 in evens) # Output: False
print(20 in evens) # Output: True
# これは非常に効率的です - Python はすべての数を生成しません
# その数がシーケンスに含まれうるかを計算します
large_range = range(0, 1000000, 3)
print(999999 in large_range) # Output: True (instant, no iteration needed)Python はすべての数を生成せずに、数学的に membership を判定できます。そのため、巨大な range でもこの操作は非常に高速です。
空の range と逆向きの range
# 空の range - stop が start と等しい
empty = range(5, 5)
print(list(empty)) # Output: []
print(len(empty)) # Output: 0
# 空の range - 指定した step では stop に到達できない
impossible = range(1, 10, -1) # 負の step で増加はできません
print(list(impossible)) # Output: []
# 逆向きの range
backwards = range(10, 0, -1)
print(list(backwards)) # Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
# 負の数を含む逆向き
negative_range = range(-5, -15, -2)
print(list(negative_range)) # Output: [-5, -7, -9, -11, -13]range とリストを使い分けるとき
# range を使うのは:
# 1. 反復処理のために数のシーケンスが必要な場合
for i in range(100):
# Process something 100 times
pass
# 2. シーケンスのインデックスが必要な場合
items = ["a", "b", "c", "d"]
for i in range(len(items)):
print(f"Index {i}: {items[i]}")
# 3. 大きなシーケンスでメモリ効率が重要な場合
# これは最小限のメモリで動きます
for i in range(1000000):
if i % 100000 == 0:
print(i)
# リストを使うのは:
# 1. 実際の値を保存しておく必要がある場合
squares = [1, 3, 5, 7, 10]
# 2. シーケンスを変更する必要がある場合
numbers = list(range(5))
numbers[2] = 100 # 値を変更
numbers.append(200) # 値を追加
# 3. 複数回使って、異なる操作を行う必要がある場合
data = list(range(10))
print(sum(data))
print(max(data))
print(sorted(data, reverse=True))range オブジェクトは Python の効率性を示す好例です。すべての要素を保持するメモリコストなしに、シーケンスとしての利点をすべて提供します。
15.7) リスト・タプル・range の相互変換
Python では、異なるシーケンス型の間で簡単に変換できます。これらの変換を理解すると、状況に応じて適切な型を選んだり、必要に応じてデータを変換したりできます。
リストへの変換
list() 関数は任意のシーケンスをリストに変換します。
# タプルからリストへ
student_tuple = ("Alice", 20, "CS")
student_list = list(student_tuple)
print(student_list) # Output: ['Alice', 20, 'CS']
print(type(student_list)) # Output: <class 'list'>
# これで変更できます
student_list[1] = 21
student_list.append(3.8)
print(student_list) # Output: ['Alice', 21, 'CS', 3.8]
# range からリストへ
numbers = range(5)
numbers_list = list(numbers)
print(numbers_list) # Output: [0, 1, 2, 3, 4]
# 文字列からリストへ(各文字が要素になります)
text = "Python"
chars = list(text)
print(chars) # Output: ['P', 'y', 't', 'h', 'o', 'n']リストへの変換は、シーケンスを変更したいときや、append()、sort()、remove() のようなリスト固有のメソッドを使いたいときに便利です。
タプルへの変換
tuple() 関数は任意のシーケンスをタプルに変換します。
# リストからタプルへ
scores_list = [85, 90, 78, 92]
scores_tuple = tuple(scores_list)
print(scores_tuple) # Output: (85, 90, 78, 92)
print(type(scores_tuple)) # Output: <class 'tuple'>
# これで不変になります
# scores_tuple[0] = 88 # TypeError: 'tuple' object does not support item assignment
# range からタプルへ
numbers = range(1, 6)
numbers_tuple = tuple(numbers)
print(numbers_tuple) # Output: (1, 2, 3, 4, 5)
# 文字列からタプルへ
text = "Hi"
chars_tuple = tuple(text)
print(chars_tuple) # Output: ('H', 'i')タプルへの変換は、データを変更から守りたいときや、シーケンスを辞書キーとして使いたいときに便利です。
15.8) 文字列・リスト・タプル・range に共通するシーケンス操作
Python のシーケンス型—文字列(strings)、リスト(list)、タプル(tuple)、range(range)—には多くの共通操作があります。これらの共有操作を理解すると、どんなシーケンス型でも効率よく扱えるようになります。
長さ、最小値、最大値
すべてのシーケンスは len()、min()、max() 関数をサポートします。
# 文字列
text = "Python"
print(len(text)) # Output: 6
print(min(text)) # Output: P (Unicode 値で最小の文字)
print(max(text)) # Output: y (Unicode 値で最大の文字)
# リスト
numbers = [45, 12, 78, 23, 56]
print(len(numbers)) # Output: 5
print(min(numbers)) # Output: 12
print(max(numbers)) # Output: 78
# タプル
scores = (85, 92, 78, 95, 88)
print(len(scores)) # Output: 5
print(min(scores)) # Output: 78
print(max(scores)) # Output: 95
# range
nums = range(10, 50, 5)
print(len(nums)) # Output: 8
print(min(nums)) # Output: 10
print(max(nums)) # Output: 45min() と max() が動作するには、要素同士を比較できる必要があります。文字列と数値が混在するリストの最小値は求められません。
mixed = [1, "hello", 3]
# print(min(mixed)) # TypeError: '<' not supported between instances of 'str' and 'int'インデックス参照と負のインデックス
すべてのシーケンスは、正と負のインデックスで要素にアクセスできます。
# 正のインデックス(0始まり)
text = "Python"
numbers = [10, 20, 30, 40, 50]
coords = (5, 10, 15)
values = range(0, 100, 10)
print(text[0]) # Output: P
print(numbers[2]) # Output: 30
print(coords[1]) # Output: 10
print(values[3]) # Output: 30
# 負のインデックス(末尾から)
print(text[-1]) # Output: n (最後の文字)
print(numbers[-2]) # Output: 40 (末尾から2番目)
print(coords[-3]) # Output: 5 (末尾から3番目=先頭)
print(values[-1]) # Output: 90 (range の最後の値)負のインデックスは末尾から数えます。-1 が最後の要素、-2 が末尾から2番目、という具合です。
in と not in による所属判定
すべてのシーケンスは所属判定をサポートします。
# 文字列 - 部分文字列をチェックします
text = "Python Programming"
print("Python" in text) # Output: True
print("Java" in text) # Output: False
print("gram" in text) # Output: True (substring)
print("PYTHON" not in text) # Output: True (case-sensitive)
# リスト
fruits = ["apple", "banana", "cherry", "date"]
print("banana" in fruits) # Output: True
print("grape" in fruits) # Output: False
print("apple" not in fruits) # Output: False
# タプル
coordinates = (10, 20, 30, 40)
print(20 in coordinates) # Output: True
print(25 in coordinates) # Output: False
print(50 not in coordinates) # Output: True
# range - 非常に効率的で、反復は不要です
numbers = range(0, 100, 2) # Even numbers 0 to 98
print(50 in numbers) # Output: True
print(51 in numbers) # Output: False (odd number)
print(100 in numbers) # Output: False (stop is exclusive)range の場合、Python は全要素を調べることなく数学的に判定できるため、巨大な range でも非常に高速です。
連結と繰り返し
文字列・リスト・タプルは、+ による連結と * による繰り返しをサポートします。
# + による連結
text1 = "Hello"
text2 = " World"
print(text1 + text2) # Output: Hello World
list1 = [1, 2, 3]
list2 = [4, 5, 6]
print(list1 + list2) # Output: [1, 2, 3, 4, 5, 6]
tuple1 = (10, 20)
tuple2 = (30, 40)
print(tuple1 + tuple2) # Output: (10, 20, 30, 40)
# * による繰り返し
print("Ha" * 3) # Output: HaHaHa
print([0] * 5) # Output: [0, 0, 0, 0, 0]
print((1, 2) * 3) # Output: (1, 2, 1, 2, 1, 2)重要: range は連結や繰り返しをサポートしません。
r1 = range(5)
r2 = range(5, 10)
# combined = r1 + r2 # TypeError: unsupported operand type(s) for +: 'range' and 'range'
# range を結合するには、先にリストやタプルに変換します
combined = list(r1) + list(r2)
print(combined) # Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]出現回数のカウント
count() メソッドは、要素が何回出現するかを返します。
# 文字列 - 部分文字列の出現回数を数えます
text = "Mississippi"
print(text.count("s")) # Output: 4
print(text.count("ss")) # Output: 2
print(text.count("i")) # Output: 4
# リスト
numbers = [1, 2, 3, 2, 4, 2, 5]
print(numbers.count(2)) # Output: 3
print(numbers.count(6)) # Output: 0
# タプル
grades = (85, 90, 85, 92, 85, 88)
print(grades.count(85)) # Output: 3
print(grades.count(95)) # Output: 0
# range には count() メソッドがありませんが、変換すれば使えます
nums = range(0, 20, 2)
nums_list = list(nums)
print(nums_list.count(10)) # Output: 1要素のインデックスを探す
index() メソッドは、最初に見つかった位置を返します。
# 文字列
text = "Python Programming"
print(text.index("P")) # Output: 0 (first P)
print(text.index("Pro")) # Output: 7 (substring position)
# print(text.index("Java")) # ValueError: substring not found
# リスト
fruits = ["apple", "banana", "cherry", "banana"]
print(fruits.index("banana")) # Output: 1 (first occurrence)
print(fruits.index("cherry")) # Output: 2
# print(fruits.index("grape")) # ValueError: 'grape' is not in list
# タプル
coordinates = (10, 20, 30, 20, 40)
print(coordinates.index(20)) # Output: 1 (first occurrence)
print(coordinates.index(40)) # Output: 4
# range には index() メソッドがありませんが、変換すれば使えます
nums = range(10, 50, 5)
nums_list = list(nums)
print(nums_list.index(25)) # Output: 3要素が見つからない場合、index() は ValueError を送出します。避けるには、先に in で確認してください。
fruits = ["apple", "banana", "cherry"]
search_fruit = "grape"
if search_fruit in fruits:
position = fruits.index(search_fruit)
print(f"{search_fruit} found at position {position}")
else:
print(f"{search_fruit} not found")
# Output: grape not foundfor ループでの反復
すべてのシーケンスは for ループで反復できます。
# 文字列 - 文字を1つずつ反復する
for char in "Python":
print(char, end=" ")
print() # Output: P y t h o n
# リスト
for fruit in ["apple", "banana", "cherry"]:
print(f"I like {fruit}")
# Output:
# I like apple
# I like banana
# I like cherry
# タプル
for score in (85, 90, 78):
print(f"Score: {score}")
# Output:
# Score: 85
# Score: 90
# Score: 78
# range
for i in range(1, 6):
print(f"Count: {i}")
# Output:
# Count: 1
# Count: 2
# Count: 3
# Count: 4
# Count: 5比較演算
シーケンスは ==、!=、<、>、<=、>= で比較できます。
# 等価
print([1, 2, 3] == [1, 2, 3]) # Output: True
print((1, 2, 3) == (1, 2, 3)) # Output: True
print("abc" == "abc") # Output: True
# 非等価
print([1, 2, 3] != [1, 2, 4]) # Output: True
print((1, 2) != (1, 2)) # Output: False
# 辞書式比較(要素ごと)
print([1, 2, 3] < [1, 2, 4]) # Output: True (3 < 4)
print([1, 2, 3] < [1, 3, 0]) # Output: True (2 < 3)
print("apple" < "banana") # Output: True (alphabetical)
print((1, 2) < (1, 2, 3)) # Output: True (shorter is less if equal so far)
# 異なる型の比較
print([1, 2, 3] == (1, 2, 3)) # Output: False (different types)比較は左から右へ要素単位で行われます。最初に差が出た要素が結果を決めます。
これらの共通操作を理解すると、どんなシーケンス型にも対応できるコードが書けるようになり、プログラムはより柔軟で再利用しやすくなります。
15.9) すべてのシーケンス型に共通する高度なスライス
スライスは、Python のシーケンスを扱うための最も強力な機能の1つです。第14章で基本的なスライスを紹介しましたが、すべてのシーケンス型で使える高度なスライステクニックがあります。
基本スライスの復習
スライスは sequence[start:stop:step] という構文で、シーケンスの一部を取り出します。
# 文字列の基本スライス
text = "Python Programming"
print(text[0:6]) # Output: Python
print(text[7:18]) # Output: Programming
print(text[7:]) # Output: Programming (from index 7 to end)
print(text[:6]) # Output: Python (from start to index 6)
# リストの基本スライス
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:7]) # Output: [2, 3, 4, 5, 6]
print(numbers[:5]) # Output: [0, 1, 2, 3, 4]
print(numbers[5:]) # Output: [5, 6, 7, 8, 9]
# タプルの基本スライス
coordinates = (10, 20, 30, 40, 50, 60)
print(coordinates[1:4]) # Output: (20, 30, 40)
print(coordinates[:3]) # Output: (10, 20, 30)
print(coordinates[3:]) # Output: (40, 50, 60)
# range の基本スライス
nums = range(0, 100, 10)
print(list(nums[2:5])) # Output: [20, 30, 40]start は含まれ、stop は含まれず、結果は常に元のシーケンスと同じ型になります。
スライスで step を使う
第3引数 step は、何個飛ばすかを制御します。
# 2個おき
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::2]) # Output: [0, 2, 4, 6, 8]
print(numbers[1::2]) # Output: [1, 3, 5, 7, 9]
# 3個おき
text = "abcdefghijklmnop"
print(text[::3]) # Output: adgjmp
# start と stop を指定した step
print(numbers[2:8:2]) # Output: [2, 4, 6]
print(text[1:10:2]) # Output: bdfhj負の step:シーケンスの反転
負の step を指定すると、スライス方向が逆になります。
# シーケンス全体の反転
text = "Python"
print(text[::-1]) # Output: nohtyP
numbers = [1, 2, 3, 4, 5]
print(numbers[::-1]) # Output: [5, 4, 3, 2, 1]
coordinates = (10, 20, 30, 40)
print(coordinates[::-1]) # Output: (40, 30, 20, 10)
# step を指定して反転
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::-2]) # Output: [9, 7, 5, 3, 1] (every second, backwards)
# 部分的に反転
text = "Python Programming"
print(text[7:18][::-1]) # Output: gnimmargorP (reverse "Programming")負の step を使うとき、start と stop は異なる動作になります。
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 負の step では start は stop より大きい必要があります
print(numbers[7:2:-1]) # Output: [7, 6, 5, 4, 3] (from 7 down to 3)
print(numbers[8:3:-2]) # Output: [8, 6, 4] (from 8 down to 4, step -2)
# start/stop を省略して負の step を使う
print(numbers[:5:-1]) # Output: [9, 8, 7, 6] (from end down to 6)
print(numbers[5::-1]) # Output: [5, 4, 3, 2, 1, 0] (from 5 down to start)スライスで負のインデックスを使う
start と stop に負のインデックスを使えます。
text = "Python Programming"
# 末尾11文字
print(text[-11:]) # Output: Programming
# 末尾11文字以外すべて
print(text[:-11]) # Output: Python
# -15 から -5 まで
print(text[-15:-5]) # Output: hon Progra
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 末尾5要素
print(numbers[-5:]) # Output: [5, 6, 7, 8, 9]
# 末尾3要素以外すべて
print(numbers[:-3]) # Output: [0, 1, 2, 3, 4, 5, 6]
# -7 から -2 まで
print(numbers[-7:-2]) # Output: [3, 4, 5, 6, 7]range のスライス
range をスライスすると、新しい range オブジェクトが返ります。
# range のスライス
numbers = range(0, 100, 5) # 0, 5, 10, 15, ..., 95
print(numbers) # Output: range(0, 100, 5)
# スライスは新しい range を返します
subset = numbers[5:10]
print(subset) # Output: range(25, 50, 5)
print(list(subset)) # Output: [25, 30, 35, 40, 45]
# step を指定
every_other = numbers[::2]
print(list(every_other)) # Output: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
# 負の step
reversed_range = numbers[::-1]
print(list(reversed_range)) # Output: [95, 90, 85, ..., 5, 0]空スライスと境界ケース
numbers = [1, 2, 3, 4, 5]
# 空スライス(正の step で start >= stop)
print(numbers[3:3]) # Output: []
print(numbers[5:10]) # Output: [] (stop beyond length)
print(numbers[10:20]) # Output: [] (both beyond length)
# 範囲外のスライスは安全です
print(numbers[-100:100]) # Output: [1, 2, 3, 4, 5] (entire sequence)
print(numbers[2:100]) # Output: [3, 4, 5] (from 2 to end)
# start/stop が負の step と噛み合わない場合
print(numbers[2:7:-1]) # Output: [] (can't go forward with negative step)
# step に 0 は指定できません
# print(numbers[::0]) # ValueError: slice step cannot be zeroコピーのためのスライス
スライスは新しいシーケンスを作るため、コピーを作る方法になります。
# スライスでコピーする
original = [1, 2, 3, 4, 5]
copy = original[:] # 先頭から末尾までスライス
print(copy) # Output: [1, 2, 3, 4, 5]
# コピーを変更しても元のリストには影響しません
copy[0] = 100
print(f"Original: {original}") # Output: Original: [1, 2, 3, 4, 5]
print(f"Copy: {copy}") # Output: Copy: [100, 2, 3, 4, 5]
# タプルでも同様に動きます(新しいタプルが作られます)
original_tuple = (1, 2, 3, 4, 5)
copy_tuple = original_tuple[:]
print(copy_tuple) # Output: (1, 2, 3, 4, 5)
# 文字列の場合
text = "Python"
text_copy = text[:]
print(text_copy) # Output: Pythonただし、第14章で見たとおり、これは 浅いコピー(shallow copy) を作ります。
# 浅いコピーの制限
original = [[1, 2], [3, 4]]
copy = original[:]
# ネストしたリストを変更すると両方に影響します
copy[0][0] = 100
print(f"Original: {original}") # Output: Original: [[100, 2], [3, 4]]
print(f"Copy: {copy}") # Output: Copy: [[100, 2], [3, 4]]タプルと range は、Python のシーケンス道具箱において欠かせないツールです。タプルは、不意の変更から情報を守り、辞書キーとしての利用も可能にする、不変で構造化されたデータを提供します。range は数列をメモリ効率よく表現し、ループや大きな数列に最適です。どの型をいつ使うべきか、そして相互変換の方法を理解すると、コードはより効率的で安全になり、意図も明確になります。
すべてのシーケンス型に共通する操作—インデックス参照、スライス、反復、所属判定—は、一貫したインターフェースを形作っており、どのシーケンスも直感的に扱えるようになります。高度なスライステクニックは、シーケンスデータを取り出したり操作したりするための強力で表現力の高い方法を提供します。
Python を書き続けるうちに、状況に応じて自然に適切なシーケンス型を選ぶようになるでしょう。変化するコレクションにはリスト、固定レコードにはタプル、数列には range、テキストには文字列です。この章で、それらを自信を持って選び、各型を効果的に使うための知識が身につきました。