18. Python のデータとオブジェクトモデル: 参照、比較、コピー
Python がデータをどのように保存し管理しているかを理解することは、正しいプログラムを書くうえで非常に重要です。この章では、Python のオブジェクトモデル(object model)—Python におけるすべてのデータの動作を支配する基本システム—を探っていきます。ある代入では独立したコピーが作られるのに、別の代入では共有参照が作られるのはなぜか、オブジェクトを正しく比較する方法、そしてコレクションを扱うときによくある落とし穴を避ける方法を学びます。
この知識は、たとえば「あるリストを変更したら別のリストにも影響した」や、「2つのリストを == で比較した結果が is で比較した結果と違う」といった、これまで遭遇したかもしれない驚くような挙動を理解する助けになります。
18.1) Python ではすべてがオブジェクト
Python では、あらゆるデータはオブジェクト(object) です。これは単なる理論的な概念ではなく、プログラムがどのように動くかに実用上の影響があります。
数値、文字列、リストなど、どんな値を作っても、Python はメモリ上に オブジェクト を作成します。オブジェクトとは、次のものを保持するコンテナです:
- 実際のデータ(値(value))
- それがどの種類のデータかという情報(型(type))
- 一意の識別子(同一性(identity))
実際に見てみましょう:
# さまざまな型のオブジェクトを作成する
number = 42
text = "Hello"
items = [1, 2, 3]
# これらの変数はそれぞれ、メモリ上のオブジェクトを参照します
print(number) # Output: 42
print(text) # Output: Hello
print(items) # Output: [1, 2, 3]整数のような単純な値であってもオブジェクトです。つまり、単に数値を保持する以上の機能を持っているということです:
# 整数はメソッドを持つオブジェクトです
number = 42
print(number.bit_length()) # Output: 6
# 文字列はメソッドを持つオブジェクトです
text = "hello"
print(text.upper()) # Output: HELLO
# リストはメソッドを持つオブジェクトです
items = [3, 1, 2]
items.sort()
print(items) # Output: [1, 2, 3]なぜこれが重要なのでしょうか? 変数に代入したり、関数にデータを渡したりするとき、オブジェクトをコピーしているのではなく、同じオブジェクトへの 参照(reference) を作っているからです。これは他のいくつかのプログラミング言語の仕組みとは根本的に異なり、この違いを理解しておくと混乱しやすいバグを多く防げます。
# リストオブジェクトを作成する
original = [1, 2, 3]
# これは新しいリストを作りません。別の参照を作っているだけです
# 同じリストオブジェクトを参照しています
another_name = original
# 片方の参照から変更すると、もう片方にも影響します
another_name.append(4)
print(original) # Output: [1, 2, 3, 4]
print(another_name) # Output: [1, 2, 3, 4]original と another_name はどちらも、メモリ上の同じリストオブジェクトを参照しています。another_name を通してリストを変更すると、両方が同じオブジェクトを見ているため、original からも変更が見えます。
この挙動は 参照セマンティクス(reference semantics) と呼ばれ、Python プログラミングで最重要級の概念のひとつです。この章を通して深く掘り下げていきます。
18.2) オブジェクトの同一性・型・値(type() と id() の使用)
Python のすべてのオブジェクトには、それを定義する3つの基本特性があります。同一性(identity)、型(type)、値(value) です。これらを理解すると、オブジェクトがどう振る舞うか、そしてどう正しく比較すべきかを考えやすくなります。
18.2.1) id() で見るオブジェクトの同一性
オブジェクトの 同一性 は、オブジェクトが作成されたときに Python が割り当てる一意の数値です。この同一性は、オブジェクトの生存期間中は決して変わりません。メモリ上の恒久的な住所のようなものです。
オブジェクトの同一性は id() 関数で取得できます:
# オブジェクトを作成し、その同一性を確認する
x = [1, 2, 3]
y = [1, 2, 3]
z = x
print(id(x)) # Output: 140234567890123 (example - actual number varies)
print(id(y)) # Output: 140234567890456 (different from x)
print(id(z)) # Output: 140234567890123 (same as x)実際に表示される数値は実行のたびに異なりますが、パターンは変わりません。x と y は同じ値を含んでいても別オブジェクトなので同一性が異なります。一方 z は、同じオブジェクトに対する別名にすぎないので x と同じ同一性を持ちます。
同一性がなぜ重要かを示す実用例を見てみましょう:
# 成績が同じ2人の学生
student1_grades = [85, 90, 92]
student2_grades = [85, 90, 92]
# これらは別のオブジェクトです(同一性が異なる)
print(id(student1_grades)) # Output: 140234567890123 (example)
print(id(student2_grades)) # Output: 140234567890456 (different)
# 片方を変更してももう片方には影響しません
student1_grades.append(88)
print(student1_grades) # Output: [85, 90, 92, 88]
print(student2_grades) # Output: [85, 90, 92]では別のシナリオを考えてみましょう:
# 1人の学生の成績を2つの変数で追跡する
original_grades = [85, 90, 92]
backup_reference = original_grades
# これらは同じオブジェクトを参照しています(同一性が同じ)
print(id(original_grades)) # Output: 140234567890123 (example)
print(id(backup_reference)) # Output: 140234567890123 (same!)
# どちらの名前を通して変更しても両方に反映されます
backup_reference.append(88)
print(original_grades) # Output: [85, 90, 92, 88]
print(backup_reference) # Output: [85, 90, 92, 88]重要な洞察: 2つの変数が同じ同一性を持つとき、それらはメモリ上のまったく同じオブジェクトを参照しています。オブジェクトは1つだけが変更されているので、片方の変数を通して行った変更はもう片方からも見えます。
18.2.2) type() で見るオブジェクトの型
オブジェクトの 型 は、そのオブジェクトがどんな種類のデータを保持し、どんな操作ができるかを決めます。第3章で学んだように、オブジェクトの型は type() 関数で確認できます:
# さまざまな型のオブジェクト
number = 42
text = "Hello"
items = [1, 2, 3]
mapping = {"name": "Alice"}
print(type(number)) # Output: <class 'int'>
print(type(text)) # Output: <class 'str'>
print(type(items)) # Output: <class 'list'>
print(type(mapping)) # Output: <class 'dict'>オブジェクトの型は作成後に変わりません。整数を文字列に「変える」ことはできず、整数の値に基づいて新しい文字列オブジェクトを作ることしかできません:
# 型は作成時に固定されます
x = 42
print(type(x)) # Output: <class 'int'>
# これは x の型を変えるのではなく、新しい文字列オブジェクトを作成します
# そして x がその新しいオブジェクトを参照するようにします
x = str(x)
# 元の整数オブジェクト(42)は、ガベージコレクションされるまでメモリに残ります
# x は完全に別のオブジェクトである文字列 "42" を指すようになります
print(type(x)) # Output: <class 'str'>
print(x) # Output: 42 (now a string, not an integer)型の理解は重要です。型によってサポートされる操作が異なるからです:
# リストは append をサポートします
grades = [85, 90]
grades.append(92)
print(grades) # Output: [85, 90, 92]
# 文字列には append がありません - イミュータブルです
text = "Hello"
# text.append(" World") # AttributeError: 'str' object has no attribute 'append'
# しかし文字列は連結をサポートします
text = text + " World"
print(text) # Output: Hello World18.2.3) オブジェクトの値
オブジェクトの 値 は、含まれている実際のデータです。同一性と型とは異なり、値は ミュータブル(mutable) なオブジェクト(リストや辞書など)では変更できますが、イミュータブル(immutable) なオブジェクト(整数や文字列など)では変更できません。
# ミュータブルなオブジェクトでは値を変更できます
shopping_cart = ["milk", "bread"]
print(shopping_cart) # Output: ['milk', 'bread']
shopping_cart.append("eggs")
print(shopping_cart) # Output: ['milk', 'bread', 'eggs']
# 同じオブジェクト(同一性は同じ)で、値だけが変わりました
# イミュータブルなオブジェクトでは値を変更できません
count = 5
print(count) # Output: 5
count = count + 1
print(count) # Output: 6
# これは新しい同一性を持つ新しいオブジェクトを作成しました3つの特性をすべて示す完全な例です:
# リストオブジェクトを作成する
data = [10, 20, 30]
print("Identity:", id(data)) # Output: Identity: 140234567890123 (example)
print("Type:", type(data)) # Output: Type: <class 'list'>
print("Value:", data) # Output: Value: [10, 20, 30]
# 値を変更する(同一性と型は同じまま)
data.append(40)
print("Identity:", id(data)) # Output: Identity: 140234567890123 (unchanged)
print("Type:", type(data)) # Output: Type: <class 'list'> (unchanged)
print("Value:", data) # Output: Value: [10, 20, 30, 40] (changed)これら3つの特性を理解すると、プログラム内でオブジェクトがどう振る舞うかを予測できます。同一性は2つの変数が同じオブジェクトを参照しているかを示し、型は許可される操作を示し、値はオブジェクトが現在保持しているデータを示します。
18.3) ミュータブル型とイミュータブル型
Python で最も重要な区別のひとつが、ミュータブル(mutable) 型と イミュータブル(immutable) 型の違いです。この違いは、オブジェクトを変更しようとしたときにどう振る舞うかに影響し、理解しておくとよくあるプログラミング上のミスを防げます。
18.3.1) イミュータブル型: 変更できない値
イミュータブル なオブジェクトとは、作成後に値を変更できないオブジェクトです。イミュータブルなオブジェクトを変更しているように見える操作を行った場合、Python は実際には変更後の値を持つ新しいオブジェクトを作成します。
Python のイミュータブル型には次のものがあります:
- 整数 (
int) - 浮動小数点数 (
float) - 文字列 (
str) - タプル (
tuple) - 真偽値 (
bool) - None (
NoneType)
整数でイミュータブル性を確認してみましょう:
# 整数を作成する
x = 100
print("Original x:", x) # Output: Original x: 100
print("Identity of x:", id(x)) # Output: Identity of x: 140234567890123 (example)
# x を変更しているように見えますが、実際は新しいオブジェクトを作成しています
x = x + 1
print("Modified x:", x) # Output: Modified x: 101
print("Identity of x:", id(x)) # Output: Identity of x: 140234567890456 (different!)x = x + 1 が値 101 を持つまったく新しい整数オブジェクトを作ったため、同一性が変わりました。値 100 の元のオブジェクトは(Python のガベージコレクタが回収するまで)存在し続けますが、x は別のオブジェクトを参照するようになっています。
文字列はイミュータブル性をさらに分かりやすく示します:
# 文字列を作成する
message = "Hello"
print("Original:", message) # Output: Original: Hello
print("Identity:", id(message)) # Output: Identity: 140234567890789 (example)
# 文字列メソッドは元の文字列を変更せず、新しい文字列を返します
uppercase = message.upper()
print("Original:", message) # Output: Original: Hello (unchanged)
print("Uppercase:", uppercase) # Output: Uppercase: HELLO
print("Identity of original:", id(message)) # Output: Identity of original: 140234567890789 (same)
print("Identity of uppercase:", id(uppercase)) # Output: Identity of uppercase: 140234567891012 (different)文字列を変更しているように見える操作も、実際には新しい文字列オブジェクトを作成します:
# 連結で文字列を組み立てる
text = "Python"
print("Before:", text, "- ID:", id(text)) # Output: Before: Python - ID: 140234567891234 (example)
text = text + " Programming"
print("After:", text, "- ID:", id(text)) # Output: After: Python Programming - ID: 140234567891567 (different)イミュータブル性が重要な理由: イミュータブルなオブジェクトは、プログラム内の異なる箇所で共有しても安全です。どの部分もそれを誤って変更できないからです。これにより、コードの予測可能性が高まり、考えやすくなります。
18.3.2) ミュータブル型: 変更できる値
ミュータブル なオブジェクトとは、新しいオブジェクトを作成せずに、作成後の値を変更できるオブジェクトです。オブジェクトの同一性は変わりませんが、中身は変更できます。
Python のミュータブル型には次のものがあります:
- リスト (
list) - 辞書 (
dict) - セット (
set)
リストでミュータブル性を見てみましょう:
# リストを作成する
numbers = [1, 2, 3]
print("Original:", numbers) # Output: Original: [1, 2, 3]
print("Identity:", id(numbers)) # Output: Identity: 140234567892345 (example)
# リストを変更する - 同じオブジェクトのまま値だけが変わります
numbers.append(4)
print("Modified:", numbers) # Output: Modified: [1, 2, 3, 4]
print("Identity:", id(numbers)) # Output: Identity: 140234567892345 (same!)既存のリストオブジェクトを変更したため、同一性は変わりません。これはイミュータブル型の動きと根本的に異なります。
辞書とセットもミュータブルです:
# 辞書の例
student = {"name": "Alice", "grade": 85}
print("Before:", student, "- ID:", id(student)) # Output: Before: {'name': 'Alice', 'grade': 85} - ID: 140234567893012 (example)
student["grade"] = 90 # 辞書を変更する
print("After:", student, "- ID:", id(student)) # Output: After: {'name': 'Alice', 'grade': 90} - ID: 140234567893012 (same)
# セットの例
unique_numbers = {1, 2, 3}
print("Before:", unique_numbers, "- ID:", id(unique_numbers)) # Output: Before: {1, 2, 3} - ID: 140234567893345 (example)
unique_numbers.add(4) # セットを変更する
print("After:", unique_numbers, "- ID:", id(unique_numbers)) # Output: After: {1, 2, 3, 4} - ID: 140234567893345 (same)18.3.3) 実践でミュータブル性が重要になる理由
ミュータブル型とイミュータブル型の違いは、複数の変数が同じオブジェクトを参照するときに決定的になります:
# イミュータブルの例 - 安全に共有できる
x = "Hello"
y = x # y は同じ文字列オブジェクトを参照します
# x を「変更」すると新しいオブジェクトが作られます
x = x + " World"
print(x) # Output: Hello World
print(y) # Output: Hello (unchanged - y still refers to the original)# ミュータブルの例 - 変更が共有される
list1 = [1, 2, 3]
list2 = list1 # list2 は同じリストオブジェクトを参照します
# list1 を通して変更すると list2 にも影響します
list1.append(4)
print(list1) # Output: [1, 2, 3, 4]
print(list2) # Output: [1, 2, 3, 4] (also changed!)ミュータブル性を理解することは、次の点で不可欠です:
- 挙動の予測: 操作が新しいオブジェクトを作るのか、既存のオブジェクトを変更するのかを理解する
- バグの回避: オブジェクトが共有されているときの意図しない変更を防ぐ
- 効率的なコード: 用途に合った型を選ぶ
- 関数の挙動理解: 関数パラメータが変更され得るかを把握する
次のセクションでは、これらの型に対して代入がどのように動作するか、そして必要に応じて独立したコピーを作る方法を見ていきます。
18.4) オブジェクトに対する代入の仕組み
Python の代入はオブジェクトをコピーしません。オブジェクトへの 参照(reference) を作ります。この違いを理解することは、特にミュータブル型を扱うときに、正しいプログラムを書くために不可欠です。
18.4.1) 代入はコピーではなく参照を作る
x = y と書いたとき、Python は y が参照しているオブジェクトのコピーを作りません。代わりに、x が y と同じオブジェクトを参照するようにします。2つの変数はメモリ上の同じオブジェクトに対する名前になります。
まずはイミュータブルなオブジェクトで見てみましょう:
# 整数(イミュータブル)での代入
a = 100
b = a # b は a と同じ整数オブジェクトを参照するようになります
print("a:", a) # Output: a: 100
print("b:", b) # Output: b: 100
print("Same object?", id(a) == id(b)) # Output: Same object? True
# a を「変更」すると新しいオブジェクトが作られます
a = a + 1
print("a:", a) # Output: a: 101
print("b:", b) # Output: b: 100 (unchanged)
print("Same object?", id(a) == id(b)) # Output: Same object? Falseイミュータブルなオブジェクトでは、元のオブジェクトを変更できないため、この挙動は通常安全です。値が変わる操作を行うと、Python は新しいオブジェクトを作成します。
しかし、ミュータブルなオブジェクトでは挙動が大きく異なります:
# リスト(ミュータブル)での代入
list1 = [1, 2, 3]
list2 = list1 # list2 は list1 と同じリストオブジェクトを参照します
print("list1:", list1) # Output: list1: [1, 2, 3]
print("list2:", list2) # Output: list2: [1, 2, 3]
print("Same object?", id(list1) == id(list2)) # Output: Same object? True
# list1 を通して変更すると list2 にも影響します
list1.append(4)
print("list1:", list1) # Output: list1: [1, 2, 3, 4]
print("list2:", list2) # Output: list2: [1, 2, 3, 4] (also changed!)
print("Same object?", id(list1) == id(list2)) # Output: Same object? Truelist1 と list2 は同じリストオブジェクトの名前です。どちらの名前を通してリストを変更しても、リストは1つしかないため、どちらからも変更が見えます。
なぜ重要なのかを示す実用例です:
# 学生の成績を管理する
alice_grades = [85, 90, 92]
backup_grades = alice_grades # バックアップを作ろうとする
print("Original:", alice_grades) # Output: Original: [85, 90, 92]
print("Backup:", backup_grades) # Output: Backup: [85, 90, 92]
# 新しい成績を追加する
alice_grades.append(88)
# 「バックアップ」も変更されてしまいました!
print("Original:", alice_grades) # Output: Original: [85, 90, 92, 88]
print("Backup:", backup_grades) # Output: Backup: [85, 90, 92, 88]これはバックアップではありません。2つの変数が同じリストを参照しています。本当のバックアップを作るにはコピーを作る必要があります(これはセクション 18.8 で扱います)。
18.4.2) 関数呼び出しにおける代入
引数を関数に渡すときも、Python は同じ参照セマンティクスを使います。パラメータは同じオブジェクトに対する別名になります:
# イミュータブルなパラメータを持つ関数
def increment(number):
number = number + 1 # 新しいオブジェクトを作成する
return number
value = 5
result = increment(value)
print("Original value:", value) # Output: Original value: 5 (unchanged)
print("Returned result:", result) # Output: Returned result: 6パラメータ number は最初、value と同じ整数オブジェクトを参照しています。number = number + 1 を行うと、新しい整数オブジェクトを作成し、number がそれを参照するようになります。元のオブジェクト(および value)は変更されません。
ミュータブルなオブジェクトでは挙動が異なります:
# ミュータブルなパラメータを持つ関数
def add_item(items, new_item):
items.append(new_item) # 元のリストを変更する
shopping_list = ["milk", "bread"]
add_item(shopping_list, "eggs")
print("Original list:", shopping_list) # Output: Original list: ['milk', 'bread', 'eggs']パラメータ items は shopping_list と同じリストオブジェクトを参照します。items を通してリストを変更すると、元のリストを変更したことになります。
よくある間違いと、その回避方法を示します:
# 間違い: 意図せず元のデータを変更してしまう
def process_grades(grades):
grades.append(100) # 元のリストを変更してしまいます!
return grades
student_grades = [85, 90, 92]
processed = process_grades(student_grades)
print("Original:", student_grades) # Output: Original: [85, 90, 92, 100] (modified!)
print("Processed:", processed) # Output: Processed: [85, 90, 92, 100]
# 正しい: 元のデータを変更したくないならコピーを作る
def process_grades_safely(grades):
# 同じ要素を持つ新しいリストを作成する
result = grades + [100] # 連結は新しいリストを作成します
return result
student_grades = [85, 90, 92]
processed = process_grades_safely(student_grades)
print("Original:", student_grades) # Output: Original: [85, 90, 92] (unchanged)
print("Processed:", processed) # Output: Processed: [85, 90, 92, 100]ミュータブルなデフォルト引数に関する重要な注意: 関連するよくある落とし穴として、ミュータブルなオブジェクトをデフォルトのパラメータ値として使うこと(例: def func(items=[]):)があります。デフォルトパラメータは関数定義時に1回だけ作られ、呼び出しごとに作られるわけではないため、デフォルトのリストが複数回の関数呼び出しをまたいで値を蓄積してしまい、予期しない挙動につながることがあります。これは第20章で詳しく扱いますが、ミュータブルなパラメータを扱うときの頻出バグ要因として意識しておいてください。
18.5) 参照セマンティクスとオブジェクトのエイリアシング
参照セマンティクス(reference semantics) とは、Python の変数は値を保持する入れ物ではなく、オブジェクトを参照する名前である、という意味です。複数の変数が同じオブジェクトを参照している場合、それを エイリアシング(aliasing) と呼びます。エイリアシングを理解することは、プログラムの挙動を予測するうえで不可欠です。
18.5.1) エイリアシングとは?
エイリアシング は、2つ以上の変数がメモリ上の同じオブジェクトを参照しているときに起きます。変数は互いに「エイリアス」—同じものに対する別の名前—になります。
簡単な例でエイリアシングを見てみましょう:
# リストを作成し、エイリアスを作る
original = [1, 2, 3]
alias = original # alias は original と同じリストを参照します
print("Original:", original) # Output: Original: [1, 2, 3]
print("Alias:", alias) # Output: Alias: [1, 2, 3]
print("Same object?", id(original) == id(alias)) # Output: Same object? True
# エイリアスを通して変更する
alias.append(4)
# 変更は両方の名前から見えます
print("Original:", original) # Output: Original: [1, 2, 3, 4]
print("Alias:", alias) # Output: Alias: [1, 2, 3, 4]メモリ上のリストオブジェクトは1つだけですが、original と alias の2つの名前を持っています。どちらの名前を通して行った変更も、同じ基盤となるオブジェクトに作用します。
学生の記録を使った、より現実的な例です:
# エイリアシングがある学生データベース
students = {
"alice": {"name": "Alice", "grade": 85},
"bob": {"name": "Bob", "grade": 90}
}
# Alice のレコードへのエイリアスを作成する
alice_record = students["alice"]
print("Alice's grade:", alice_record["grade"]) # Output: Alice's grade: 85
# エイリアスを通して変更する
alice_record["grade"] = 95
# 変更は元の辞書からも見えます
print("Updated grade:", students["alice"]["grade"]) # Output: Updated grade: 95変数 alice_record は students["alice"] に保存されている辞書のエイリアスです。alice_record を変更すると、students 辞書に格納されている同じ辞書が変更されます。
18.5.2) is 演算子でエイリアシングを検出する
2つの変数がエイリアス(同じオブジェクトを参照している)かどうかは、is 演算子で確認できます:
# エイリアシングの確認
list1 = [1, 2, 3]
list2 = list1 # エイリアス
list3 = [1, 2, 3] # 同じ値を持つ別オブジェクト
print("list1 is list2:", list1 is list2) # Output: list1 is list2: True (aliases)
print("list1 is list3:", list1 is list3) # Output: list1 is list3: False (different objects)
print("list1 == list3:", list1 == list3) # Output: list1 == list3: True (same value)is 演算子は同一性(2つの変数が同じオブジェクトを参照しているか)を確認し、== は値(2つのオブジェクトが同じ内容か)を確認します。この違いはセクション 18.6 で詳しく扱います。
18.5.3) コレクションにおけるエイリアシング
オブジェクトがコレクションに格納されると、エイリアシングはより複雑になります:
# リストのリストを作成する
row = [0, 0, 0]
grid = [row, row, row] # 3つの要素はすべて同じリストのエイリアスです!
print("Grid:")
for r in grid:
print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
# 1つの要素を変更するとすべての行に影響します
grid[0][0] = 1
print("\nAfter modification:")
for r in grid:
print(r)
# Output:
# [1, 0, 0]
# [1, 0, 0]
# [1, 0, 0]これは 2D グリッドを作ろうとしたときのよくある間違いです。3行すべてが同じリストのエイリアスなので、1行を変更するとすべてが変更されます。
独立した行を作る正しい方法です:
# 独立した行を作成する
grid = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] # 各行は別々のリストです
print("Grid:")
for r in grid:
print(r)
# Output:
# [0, 0, 0]
# [0, 0, 0]
# [0, 0, 0]
# これで、1つの要素の変更はその行にだけ影響します
grid[0][0] = 1
print("\nAfter modification:")
for r in grid:
print(r)
# Output:
# [1, 0, 0]
# [0, 0, 0]
# [0, 0, 0]18.6) 型をまたいだ等価性・同一性・メンバーシップ(==、is、in)
Python には、オブジェクト同士の比較や関係の確認を行うための3つの基本演算子があります。等価性(equality)の ==、同一性(identity)の is、メンバーシップ(membership)の in です。どれをいつ使うべきかを理解することは、正しいプログラムを書くために不可欠です。
18.6.1) == による等価性(値の比較)
== 演算子は、2つのオブジェクトが 同じ値 を持つかどうかを確認します。同じメモリ上のオブジェクトかどうかは関係なく、中身が等しいかどうかだけを見ます。
# == で値を比較する
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 == list2) # Output: True (same values)
print(list1 == list3) # Output: True (same values)list1 と list2 はメモリ上では別オブジェクトですが、値は同じなので == は True を返します。
== がさまざまな型でどう動くかを見てみましょう:
# 異なる型にまたがる等価性
print(42 == 42) # Output: True (same integer value)
print(42 == 42.0) # Output: True (integer equals float with same value)
print("hello" == "hello") # Output: True (same string value)
print([1, 2] == [1, 2]) # Output: True (same list contents)
print({"a": 1} == {"a": 1}) # Output: True (same dictionary contents)
# 値が異なる場合
print(42 == 43) # Output: False
print("hello" == "Hello") # Output: False (case-sensitive)
print([1, 2] == [2, 1]) # Output: False (order matters)コレクションでは、== は ディープ比較(deep comparison) を行います。つまり全要素が等しいかを確認します:
# ネスト構造のディープ比較
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
print(list1 == list2) # Output: True (all nested elements are equal)
# 内側のリストが別オブジェクトでも
print(id(list1[0]) == id(list2[0])) # Output: False (different objects)
print(list1[0] == list2[0]) # Output: True (same values)18.6.2) is による同一性(オブジェクト同一性の比較)
is 演算子は、2つの変数がメモリ上の 同じオブジェクト を参照しているかどうかを確認します。値ではなく同一性を比較します。
# is で同一性を比較する
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = list1
print(list1 is list2) # Output: False (different objects)
print(list1 is list3) # Output: True (same object)
# id() で確認する
print(id(list1) == id(list2)) # Output: False
print(id(list1) == id(list3)) # Output: Trueis を使う場面: is の最も一般的な用途は None のチェックです:
# None のチェック(正しいやり方)
def find_student(name, students):
"""Return student record or None if not found."""
for student in students:
if student["name"] == name:
return student
return None
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 90}
]
result = find_student("Charlie", students)
# None のチェックには 'is' を使う
if result is None:
print("Student not found") # Output: Student not found
else:
print(f"Found: {result}")18.6.3) in によるメンバーシップ(含まれているかの確認)
in 演算子は、ある値がコレクションに含まれているかどうかを確認します。文字列、リスト、タプル、セット、辞書で使えます:
# さまざまな型でのメンバーシップ
print(2 in [1, 2, 3]) # Output: True
print("hello" in "hello world") # Output: True
print("x" in {"x": 10, "y": 20}) # Output: True (checks keys)
print(5 in {1, 2, 3, 4, 5}) # Output: True辞書では in はキーが存在するかを確認します:
# 辞書のメンバーシップ確認
student = {"name": "Alice", "grade": 85, "age": 20}
print("name" in student) # Output: True (key exists)
print("Alice" in student) # Output: False (value, not key)
print("grade" in student) # Output: True (key exists)
# 値を確認するには .values() にアクセスする必要があります
print("Alice" in student.values()) # Output: Truenot in 演算子は、含まれていないことを確認します:
# 存在しないことの確認
shopping_list = ["milk", "bread", "eggs"]
if "butter" not in shopping_list:
print("Don't forget to buy butter!") # Output: Don't forget to buy butter!各演算子を使う場面のまとめ:
==を使う: 2つのオブジェクトが同じ値かを確認したいときisを使う: 2つの変数が同じオブジェクトを参照しているかを確認したいとき(最も一般的なのはNone、またはエイリアシングのデバッグ時)inを使う: ある値がコレクションに含まれているかを確認したいとき
これらの違いを理解すると、より正確で正しい比較を書けるようになります。
18.7) 他のオブジェクトを含むオブジェクトの比較
オブジェクトが他のオブジェクトを含んでいる場合(たとえばリストの中にリストがある、辞書の中にリストがあるなど)、比較はより微妙になります。ネストした構造を Python がどう比較するかを理解することは、複雑なデータを扱ううえで不可欠です。
18.7.1) ネスト構造での == の動き
== 演算子は、ネスト構造に対して 再帰的な比較(recursive comparison) を行います。外側のコンテナだけでなく、ネストされたオブジェクトもすべて比較します:
# ネストしたリストを比較する
list1 = [[1, 2], [3, 4]]
list2 = [[1, 2], [3, 4]]
print(list1 == list2) # Output: True
# 内側のリストが別オブジェクトでも
print(id(list1[0]) == id(list2[0])) # Output: False
print(list1[0] == list2[0]) # Output: TruePython は各要素を再帰的に比較します。list1 == list2 が True になるには、対応する要素がすべて等しい必要があり、ネストした要素も含まれます。
より複雑な例です:
# 複数レベルのネスト構造
data1 = {
"students": [
{"name": "Alice", "grades": [85, 90, 92]},
{"name": "Bob", "grades": [88, 91, 87]}
],
"class": "Python 101"
}
data2 = {
"students": [
{"name": "Alice", "grades": [85, 90, 92]},
{"name": "Bob", "grades": [88, 91, 87]}
],
"class": "Python 101"
}
print(data1 == data2) # Output: TruePython は次のように比較します:
- トップレベルの辞書のキーと値("students" と "class")
- students のリスト
- 各学生の辞書("name" と "grades" のキー)
- 各学生の grades のリスト
- 各成績の数値
比較が True を返すためには、すべてのレベルが一致している必要があります。
18.7.2) シーケンスでは順序が重要
シーケンス(リストとタプル)では、要素の順序が重要です:
# リストでは順序が重要
list1 = [[1, 2], [3, 4]]
list2 = [[3, 4], [1, 2]]
print(list1 == list2) # Output: False (different order)
# しかしセットでは順序は重要ではありません
set1 = {frozenset([1, 2]), frozenset([3, 4])}
set2 = {frozenset([3, 4]), frozenset([1, 2])}
print(set1 == set2) # Output: True (sets are unordered)18.7.3) 異なる型のコレクション同士の比較
異なるコレクション型(list、tuple、set)は、同じ要素を含んでいても決して等しくなりません:
# 異なる型の比較
print([1, 2, 3] == (1, 2, 3)) # Output: False (list vs tuple)
print([1, 2, 3] == {1, 2, 3}) # Output: False (list vs set)
# 同じ要素でも
list_version = [1, 2, 3]
tuple_version = (1, 2, 3)
set_version = {1, 2, 3}
print(list_version == tuple_version) # Output: False
print(list_version == set_version) # Output: False
print(tuple_version == set_version) # Output: False18.8) リスト・辞書・セットのシャローコピー(そしてコピーされないもの)
ミュータブルなオブジェクトを扱うとき、意図しない変更を避けるために独立したコピーを作る必要がよくあります。たとえば、処理前のデータをバックアップする場合、プロダクションデータに影響を与えずにテストシナリオを作る場合、あるいは元データを変更すべきでない関数にデータを渡す場合などです。Python のコピー機構がどう動くかを理解すると、必要なときに本当に独立したコピーを作れるようになります。
しかし、すべてのコピー方法が完全に独立したコピーを作るわけではありません。シャローコピー(shallow copy) と ディープコピー(deep copy) の違いを理解することは、微妙なバグを避けるために重要です。
18.8.1) シャローコピーとは?
シャローコピー は新しいオブジェクトを作成しますが、その中に含まれるオブジェクトまではコピーしません。代わりに、新しいオブジェクトは、元のオブジェクトと同じネストされたオブジェクトへの参照を保持します。
簡単なリストで見てみましょう:
# 単純なリストのシャローコピーを作成する
original = [1, 2, 3]
copy = original.copy() # シャローコピーを作成します
print("Original:", original) # Output: Original: [1, 2, 3]
print("Copy:", copy) # Output: Copy: [1, 2, 3]
# これらは別オブジェクトです
print("Same object?", original is copy) # Output: Same object? False
# コピーを変更しても元には影響しません
copy.append(4)
print("Original:", original) # Output: Original: [1, 2, 3]
print("Copy:", copy) # Output: Copy: [1, 2, 3, 4]整数のようなイミュータブルなオブジェクトだけを含む単純なリストでは、シャローコピーは完全に問題なく機能します。コピーは元から独立しています。
では、ネスト構造ではどうなるでしょうか?シャローコピーの限界が見える例です:
# ネストしたリストでのシャローコピー
original = [[1, 2], [3, 4]]
copy = original.copy()
print("Original:", original) # Output: Original: [[1, 2], [3, 4]]
print("Copy:", copy) # Output: Copy: [[1, 2], [3, 4]]
# 外側のリストは別オブジェクトです
print("Same outer list?", original is copy) # Output: Same outer list? False
# しかし内側のリストは同じオブジェクトです
print("Same nested list?", original[0] is copy[0]) # Output: Same nested list? True
# 内側のリストを変更すると両方に影響します
copy[0].append(99)
print("Original:", original) # Output: Original: [[1, 2, 99], [3, 4]]
print("Copy:", copy) # Output: Copy: [[1, 2, 99], [3, 4]]18.8.2) リストのシャローコピーを作る
リストのシャローコピーを作る方法はいくつかあります:
# 方法1: copy() メソッドを使う
original = [[1, 2], [3, 4]]
copy1 = original.copy()
# 方法2: リストスライスを使う
copy2 = original[:]
# 方法3: list() コンストラクタを使う
copy3 = list(original)
# いずれもシャローコピーを作成します
print(copy1) # Output: [[1, 2], [3, 4]]
print(copy2) # Output: [[1, 2], [3, 4]]
print(copy3) # Output: [[1, 2], [3, 4]]
# 外側のリストは別オブジェクトです
print(original is copy1) # Output: False
print(original is copy2) # Output: False
print(original is copy3) # Output: False
# しかし内側のリストは共有されています
print(original[0] is copy1[0]) # Output: True
print(original[0] is copy2[0]) # Output: True
print(original[0] is copy3[0]) # Output: True18.8.3) 辞書のシャローコピーを作る
辞書もシャローコピーをサポートします:
# 方法1: copy() メソッドを使う
original = {"name": "Alice", "grade": 85}
copy1 = original.copy()
# 方法2: dict() コンストラクタを使う
copy2 = dict(original)
# いずれもシャローコピーを作成します
print(copy1) # Output: {'name': 'Alice', 'grade': 85}
print(copy2) # Output: {'name': 'Alice', 'grade': 85}
# これらは別オブジェクトです
print(original is copy1) # Output: False
print(original is copy2) # Output: False
# コピーを変更しても元には影響しません
copy1["grade"] = 90
print("Original:", original) # Output: Original: {'name': 'Alice', 'grade': 85}
print("Copy:", copy1) # Output: Copy: {'name': 'Alice', 'grade': 90}ただし、ネスト構造では同じシャローコピーの制限が当てはまります:
# ネストした辞書でのシャローコピー
original = {
"name": "Alice",
"grades": [85, 90, 92]
}
copy = original.copy()
print("Original:", original) # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92]}
print("Copy:", copy) # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92]}
# 辞書は別オブジェクトです
print("Same dict?", original is copy) # Output: Same dict? False
# しかし grades のリストは同じオブジェクトです
print("Same grades list?", original["grades"] is copy["grades"]) # Output: Same grades list? True
# grades のリストを変更すると両方に影響します
copy["grades"].append(88)
print("Original:", original) # Output: Original: {'name': 'Alice', 'grades': [85, 90, 92, 88]}
print("Copy:", copy) # Output: Copy: {'name': 'Alice', 'grades': [85, 90, 92, 88]}