30. クラスとオブジェクトの導入
30.1) オブジェクト指向プログラミングの考え方(独自の型を作る)
本書を通して、これまで Python の組み込み型を扱ってきました。整数、文字列、リスト、辞書などです。各型は、データ(たとえば文字列内の文字)と、そのデータに対して実行できる操作(たとえば .upper() や .split())をひとまとめにしています。このデータと振る舞いの組み合わせは強力です。文字列を単なる生の文字シーケンスとしてではなく、固有の能力を持つ完全な実体として考えられるようになります。
オブジェクト指向プログラミング(object-oriented programming: OOP) はこの考え方を拡張します。問題領域に固有のデータと振る舞いをひとまとめにした、クラス(class) と呼ばれる独自のカスタム型を作れるようになります。Python がテキストを扱うための str 型や、シーケンスを扱うための list 型を提供しているのと同じように、金融取引を管理する BankAccount 型、学業成績を追跡する Student 型、在庫システムのための Product 型などを作れます。
なぜ独自の型を作るのか?
学校システムで学生の情報を管理することを考えてみましょう。クラスがなければ、別々の変数や辞書を使うかもしれません。
# 個別の変数を使う方法 - すぐにごちゃごちゃします
student1_name = "Alice Johnson"
student1_id = "S12345"
student1_gpa = 3.8
student2_name = "Bob Smith"
student2_id = "S12346"
student2_gpa = 3.5
# あるいは辞書を使う方法 - より良いですが、それでも制限があります
student1 = {"name": "Alice Johnson", "id": "S12345", "gpa": 3.8}
student2 = {"name": "Bob Smith", "id": "S12346", "gpa": 3.5}このアプローチは単純なケースでは機能しますが、限界があります。
- バリデーションがない:
gpaに-5.0や"excellent"のような不正な値を設定してしまうのを防げません - 関連する振る舞いがない: 優等生かどうかの判定や学生情報の整形のような操作は、コード全体に散らばった別々の関数になります
- 型チェックができない: 学生を表す辞書は、他のどんな辞書とも同じに見えます。学生辞書が期待されているところに誤って商品辞書を使ってしまうようなミスを、Python が見つける助けになりません
クラスは、学生とは何で、学生に対してどんな操作が意味を持つのかを正確に表す新しい型を定義できるため、これらの問題を解決します。
# これを目指して作っていきます - データと振る舞いをひとまとめにする Student クラス
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def is_honors(self):
return self.gpa >= 3.5
def display_info(self):
status = "Honors" if self.is_honors() else "Regular"
return f"{self.name} ({self.student_id}) - GPA: {self.gpa} [{status}]"
# これで student オブジェクトを作れます
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
print(alice.display_info()) # Output: Alice Johnson (S12345) - GPA: 3.8 [Honors]
print(bob.is_honors()) # Output: Trueこの章では、こうしたクラスをゼロから作る方法を学びます。まずは最もシンプルなクラスから始め、徐々に機能を追加して、リッチで便利な独自型を作れるようにしていきます。
クラスとインスタンス: 設計図のたとえ
クラス(class) と インスタンス(instance) の違いを理解することは、オブジェクト指向プログラミングの基本です。
-
クラス は設計図やテンプレートのようなものです。ある種類のオブジェクトがどんなデータを持ち、どんな操作を実行できるかを定義します。クラス自体は特定の学生ではなく、「学生である」とはどういうことかの定義です。
-
インスタンス(オブジェクト(object) とも呼ばれます)は、その設計図から作られた具体例です。
alice = Student("Alice Johnson", "S12345", 3.8)を作るとき、Alice 固有のデータを持つ、特定の学生インスタンスを 1 つ作っています。
1 つのクラスから必要なだけインスタンスを作れます。建築家が 1 枚の設計図を使って多くの家を建てられるのと同じです。各インスタンスはそれぞれ独自のデータ(Alice の GPA は Bob と異なる)を持ちますが、クラスが定義した同じ構造と能力を共有します。
この章で学ぶこと
この章では、Python におけるオブジェクト指向プログラミングの中核概念を紹介します。
classキーワードによる クラス定義- インスタンス作成 と属性へのアクセス
- インスタンスデータを操作する メソッド(method)の追加
selfの理解 と、メソッドがインスタンスデータへアクセスする方法__init__メソッドによる インスタンス初期化__str__と__repr__による 文字列表現の制御- 1 つのクラスから 複数の独立したインスタンスを作る
この章の終わりまでに、プログラムをより整理しやすく、保守しやすく、表現力の高いものにする独自のカスタム型を設計・実装できるようになります。第31章では、より高度なクラス機能でこの基礎を発展させ、第32章では継承とポリモーフィズムを扱います。
30.2) class でシンプルなクラスを定義する
まずは、可能な限り最もシンプルなクラスを作りましょう。まだデータも振る舞いも持たず、新しい型を定義するだけのものです。
class キーワード
クラスは class キーワードに続けてクラス名を書き、最後にコロンを付けて定義します。
class Student:
pass # 今は空のクラスです
# インスタンスを作成
alice = Student()
print(alice) # Output: <__main__.Student object at 0x...>
print(type(alice)) # Output: <class '__main__.Student'>この最小限のクラスでも有用です。Student という新しい型を作成します。alice = Student() でインスタンスを作ると、Python は Student 型の新しいオブジェクトを作成します。出力から、alice が確かに Student オブジェクトであることが分かりますが、まだ面白いことは何もしません。
クラス名の命名規則
Python のクラス名は、CapWords または PascalCase と呼ばれる特定の規約に従います。各単語を大文字で始め、単語の間にアンダースコアは入れません。
class BankAccount: # 良い: CapWords
pass
class ProductInventory: # 良い: CapWords
pass
class HTTPRequest: # 良い: 略語はすべて大文字
pass
# クラスではこれらのスタイルを避けましょう:
# class bank_account: # 悪い: snake_case は関数/変数向け
# class bankaccount: # 悪い: 読みにくい
# class BANKACCOUNT: # 悪い: ALL_CAPS は定数向けこの規約は、コードを読むときにクラスと関数・変数(snake_case を使う)を区別しやすくします。
インスタンスを作成する
クラスからインスタンスを作る見た目は、関数呼び出しと同じです。クラス名に続けて丸括弧を付けます。
class Product:
pass
# 3 つの異なる product インスタンスを作成
item1 = Product()
item2 = Product()
item3 = Product()
# 各インスタンスは別々のオブジェクトです
print(item1) # Output: <__main__.Product object at 0x...>
print(item2) # Output: <__main__.Product object at 0x...>
print(item3) # Output: <__main__.Product object at 0x...>
# 同じ型でも、別のオブジェクトです
print(item1 is item2) # Output: False
print(type(item1) is type(item2)) # Output: TrueProduct() を呼ぶたびに、新しい独立したインスタンスが作成されます。メモリアドレス(0x... の部分)が異なり、メモリ上で別々のオブジェクトであることが確認できます。
なぜ空のクラスから始めるのか?
何もしないクラスから始める理由が気になるかもしれません。理由は 2 つあります。
-
概念の明確さ: クラスがデータや振る舞いとは別の「新しい型」そのものであることを理解すると、複雑さを足す前に基本概念をつかみやすくなります。
-
実用性: 空のクラスでも、マーカーやプレースホルダーとして役立つことがあります。たとえば、独自の例外型を定義できます。
class InvalidGradeError:
pass
class StudentNotFoundError:
pass
# これらの空クラスは、区別できるエラー型として機能しますただし、実際のコードでは空のクラスはまれです。次はデータを追加して、クラスを有用なものにしていきましょう。
30.3) インスタンスを作成して属性にアクセスする
クラスはデータを保持できるようになると有用になります。Python では、インスタンスに紐づくデータである 属性(attribute) を、代入するだけでいつでも追加できます。
インスタンスに属性を追加する
ドット記法を使ってインスタンスに属性を追加できます。
class Student:
pass
# インスタンスを作成
alice = Student()
# 属性を追加
alice.name = "Alice Johnson"
alice.student_id = "S12345"
alice.gpa = 3.8
# 属性にアクセス
print(alice.name) # Output: Alice Johnson
print(alice.student_id) # Output: S12345
print(alice.gpa) # Output: 3.8ドット(.)演算子で属性にアクセスします。alice.name は「alice オブジェクトの name 属性を取得する」という意味です。これは、文字列(text.upper() のような)やリスト(numbers.append(5) のような)でこれまで使ってきた構文と同じです。これらはオブジェクトのメソッドや属性にアクセスしています。
各インスタンスは固有の属性を持つ
同じクラスの異なるインスタンスは、それぞれ独立した属性を持ちます。
class Student:
pass
# 2 人の学生を作成
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
# 各インスタンスは独自のデータを持ちます
print(alice.name) # Output: Alice Johnson
print(bob.name) # Output: Bob Smith
# 片方を変更してももう片方には影響しません
alice.gpa = 3.9
print(alice.gpa) # Output: 3.9
print(bob.gpa) # Output: 3.5 (unchanged)この独立性は重要です。alice と bob は別々のオブジェクトで、別々のデータを持っています。alice.gpa を変更しても bob.gpa には影響しません。
属性はどんな型でもよい
属性は単純な型に限りません。どんな Python の値でも保持できます。
class Student:
pass
student = Student()
student.name = "Carol Davis"
student.grades = [95, 88, 92, 90] # リスト属性
student.contact = { # 辞書属性
"email": "carol@example.com",
"phone": "555-0123"
}
student.is_active = True # 真偽値属性
# ネストしたデータにアクセス
print(student.grades[0]) # Output: 95
print(student.contact["email"]) # Output: carol@example.comこの柔軟性により、リッチなデータ構造を使って複雑な現実世界の実体をモデル化できます。
存在しない属性にアクセスする
存在しない属性にアクセスしようとすると AttributeError が発生します。
class Student:
pass
student = Student()
student.name = "David Lee"
print(student.name) # Output: David Lee
# print(student.age) # AttributeError: 'Student' object has no attribute 'age'このエラーは役に立ちます。属性が存在すると期待しているのに存在しない、というタイプミスやロジックエラーを捕捉してくれます。
手動で属性を割り当てる問題点
インスタンスを作成した後で属性を手動で追加することは できます が、この方法には深刻な欠点があります。
class Student:
pass
# 属性の設定忘れやスペルミスが起きやすい
alice = Student()
alice.name = "Alice Johnson"
alice.student_id = "S12345"
# gpa の設定を忘れた!
bob = Student()
bob.name = "Bob Smith"
bob.stuent_id = "S12346" # Typo: stuent instead of student
bob.gpa = 3.5
# これで alice には gpa がなく、bob にはタイポがあります
# print(alice.gpa) # AttributeError
# print(bob.student_id) # AttributeErrorこれはミスが起きやすく、面倒です。すべてのインスタンスが正しい属性を持った状態で始まるようにする方法が必要です。そこで登場するのが __init__ メソッドで、30.5 節で扱います。ただしその前に、メソッド(クラスに属する関数)について学びましょう。
30.4) インスタンスメソッドを追加する: self を理解する
メソッド(method)はクラス内で定義され、インスタンスデータを操作する関数です。クラスに、データだけでなく振る舞いも与えます。
シンプルなメソッドを定義する
Student クラスにメソッドを追加してみましょう。
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
# インスタンスを作成して属性を追加
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
# メソッドを呼び出す
alice.display_info() # Output: Alice Johnson - GPA: 3.8メソッド display_info は、通常の関数と同様に def でクラス内に定義されます。重要な違いは最初の引数 self です。
self を理解する
self 引数は、メソッドが操作対象の特定のインスタンスにアクセスするためのものです。alice.display_info() を呼ぶと、Python は自動的に alice をメソッドの第 1 引数として渡します。メソッド内では self が alice を参照するため、self.name は alice.name に、self.gpa は alice.gpa にアクセスします。
裏側では次のようなことが起きています。
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
# 次の 2 つの呼び出しは等価です:
alice.display_info() # 通常の書き方
Student.display_info(alice) # Python が実際に行うこと
# どちらも出力: Alice Johnson - GPA: 3.8alice.display_info() と書くと、Python はそれを Student.display_info(alice) に変換します。インスタンス(alice)がメソッド内の self 引数になります。
なぜ "self" なのか?
self はキーワードではなく慣習上の名前です。技術的には別の名前でも動作します。
class Student:
def display_info(this): # 動きますが、こうしないでください
print(f"{this.name} - GPA: {this.gpa}")しかし、必ず self を使ってください。これは Python の普遍的な慣習であり、他の Python プログラマにとって読みやすいコードになります。別の名前を使うと、読み手を混乱させ、コミュニティの標準にも反します。
複数インスタンスでのメソッド
self の力は、複数のインスタンスがあると明確になります。
class Student:
def display_info(self):
print(f"{self.name} - GPA: {self.gpa}")
# 2 人の学生を作成
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.5
# 同じメソッド、異なるデータ
alice.display_info() # Output: Alice Johnson - GPA: 3.8
bob.display_info() # Output: Bob Smith - GPA: 3.5alice.display_info() を呼ぶと self は alice です。bob.display_info() を呼ぶと self は bob です。同じメソッドコードがどのインスタンスにも使えるのは、self が呼び出し元のインスタンスに応じて切り替わるからです。
メソッドは追加の引数を取れる
メソッドは self 以外の引数も受け取れます。
class Student:
def update_gpa(self, new_gpa):
self.gpa = new_gpa
print(f"Updated {self.name}'s GPA to {self.gpa}")
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
alice.update_gpa(3.9) # Output: Updated Alice Johnson's GPA to 3.9
print(alice.gpa) # Output: 3.9alice.update_gpa(3.9) を呼ぶと、Python は alice を self として、3.9 を new_gpa として渡します。メソッド定義は def update_gpa(self, new_gpa) ですが、呼び出し時に渡す引数は 1 つだけです。self は Python が自動的に処理します。
メソッドは値を返せる
メソッドは通常の関数と同様に値を返せます。
class Student:
def is_honors(self):
return self.gpa >= 3.5
def get_status(self):
if self.is_honors():
return "Honors Student"
else:
return "Regular Student"
alice = Student()
alice.name = "Alice Johnson"
alice.gpa = 3.8
bob = Student()
bob.name = "Bob Smith"
bob.gpa = 3.2
print(alice.get_status()) # Output: Honors Student
print(bob.get_status()) # Output: Regular Studentget_status が self.is_honors() を使って別のメソッド(is_honors)を呼び出している点に注目してください。メソッドは同じインスタンス上の別メソッドを呼び出せます。
メソッドと関数: どちらを使うべきか
メソッドと、独立した(スタンドアロンの)関数のどちらを使うべきか迷うかもしれません。次が指針です。
次の場合はメソッドを使います:
- インスタンスデータ(
self.name、self.gpaなど)へのアクセスが必要 - その型に論理的に属する(Student が する/である こと)
- インスタンスの状態を変更する
次の場合はスタンドアロン関数を使います:
- インスタンスデータが不要
- 複数の型で動作する
- 一般的なユーティリティである
class Student:
# メソッド: インスタンスデータが必要
def is_honors(self):
return self.gpa >= 3.5
# 関数: 汎用ユーティリティで、どんな GPA 値でも動作
def calculate_letter_grade(gpa):
if gpa >= 3.7:
return "A"
elif gpa >= 3.0:
return "B"
elif gpa >= 2.0:
return "C"
else:
return "D"
alice = Student()
alice.gpa = 3.8
# インスタンス固有の判定にはメソッドを使う
print(alice.is_honors()) # Output: True
# 一般的な計算には関数を使う
print(calculate_letter_grade(alice.gpa)) # Output: A
print(calculate_letter_grade(2.5)) # Output: Cよくあるメソッドのパターン
以下は頻繁に使う一般的なパターンです。
ゲッター(getter)メソッド(計算した情報を取り出す):
class Student:
def get_full_info(self):
return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"セッター(setter)メソッド(バリデーション付きで属性を変更する):
class Student:
def set_gpa(self, new_gpa):
if 0.0 <= new_gpa <= 4.0:
self.gpa = new_gpa
else:
print("Invalid GPA: must be between 0.0 and 4.0")問い合わせ(query)メソッド(はい/いいえの質問に答える):
class Student:
def is_honors(self):
return self.gpa >= 3.5
def is_failing(self):
return self.gpa < 2.0アクション(action)メソッド(操作を実行する):
class Student:
def add_grade(self, grade):
self.grades.append(grade)
# 全 grade に基づいて GPA を再計算する
self.gpa = sum(self.grades) / len(self.grades)30.5) __init__ でインスタンスを初期化する
インスタンス作成後に属性を手動で設定するのは面倒で、ミスも起きやすいです。__init__ メソッドを使うと、インスタンス生成時にデータを渡して初期化できます。
__init__ メソッド
__init__ メソッド(「ダンダー・イニット」または「イニット」と発音します)は、Python が新しいインスタンスを作成するときに自動的に呼び出す特殊メソッドです。
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
# 初期データ付きでインスタンスを作成
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346", 3.5)
print(alice.name) # Output: Alice Johnson
print(bob.gpa) # Output: 3.5Student("Alice Johnson", "S12345", 3.8) と書くと、Python は次を行います。
- 空の
Studentインスタンスを新規作成する - そのインスタンスを
selfとして、あなたが渡した引数とともに__init__を呼び出す - 初期化されたインスタンスを返す
__init__ メソッドは値を明示的に返しません。属性を設定してインスタンスをその場で変更します。__init__ から値を返そうとすると、Python は TypeError を発生させます。
class Student:
def __init__(self, name):
self.name = name
# __init__ からは何も返さないでください
# return self # Wrong! TypeError: __init__() should return None, not 'Student'__init__ の動作
何が起きているのかを段階的に分解してみましょう。
class Student:
def __init__(self, name, student_id, gpa):
print(f"Initializing student: {name}")
self.name = name
self.student_id = student_id
self.gpa = gpa
print(f"Initialization complete")
alice = Student("Alice Johnson", "S12345", 3.8)
# Output:
# Initializing student: Alice Johnson
# Initialization complete
print(alice.name) # Output: Alice Johnsonself の後にある引数(name、student_id、gpa)は、インスタンス作成時の必須引数になります。指定しない場合、Python は TypeError を発生させます。
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
# student = Student() # TypeError: __init__() missing 3 required positional arguments
# student = Student("Alice") # TypeError: __init__() missing 2 required positional arguments
student = Student("Alice Johnson", "S12345", 3.8) # Correct手動で属性を割り当てるよりもずっと良いです。Python が、すべてのインスタンスが必須データを持って開始することを強制してくれます。
__init__ におけるデフォルト引数
__init__ でも、通常の関数と同様にデフォルト引数を使えます。
class Student:
def __init__(self, name, student_id, gpa=0.0):
self.name = name
self.student_id = student_id
self.gpa = gpa
# GPA は省略可能で、デフォルトは 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
bob = Student("Bob Smith", "S12346") # デフォルトの gpa=0.0 を使用
print(alice.gpa) # Output: 3.8
print(bob.gpa) # Output: 0.0これは、妥当なデフォルト値がありつつ、必要に応じてカスタマイズできる属性に便利です。
__init__ でのバリデーション
__init__ で入力値を検証し、インスタンスが妥当な状態で開始することを保証できます。
class Student:
def __init__(self, name, student_id, gpa):
if not name:
print("Error: Name cannot be empty")
self.name = "Unknown"
else:
self.name = name
self.student_id = student_id
if 0.0 <= gpa <= 4.0:
self.gpa = gpa
else:
print(f"Warning: Invalid GPA {gpa}, setting to 0.0")
self.gpa = 0.0
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice.gpa) # Output: 3.8
bob = Student("", "S12346", 5.0)
# Output:
# Error: Name cannot be empty
# Warning: Invalid GPA 5.0, setting to 0.0
print(bob.name) # Output: Unknown
print(bob.gpa) # Output: 0.0これにより、たとえ不正なデータが渡されたとしても、インスタンスは妥当な状態に落ち着きます。
30.6) __str__ と __repr__ による文字列表現
print() でインスタンスを表示したり、対話シェルで参照したりするとき、Python はそれを文字列に変換する必要があります。デフォルトでは、あまり役に立たない表示になります。
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # Output: <__main__.Student object at 0x...>デフォルトの出力はクラス名とメモリアドレスを示すだけで、Alice の実データは何も含みません。これを __str__ と __repr__ という特殊メソッドでカスタマイズできます。
__str__ メソッド
__str__ メソッドは、print() と str() がインスタンスを文字列に変換する方法を定義します。
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __str__(self):
return f"{self.name} ({self.student_id}) - GPA: {self.gpa}"
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # Output: Alice Johnson (S12345) - GPA: 3.8
print(str(alice)) # Output: Alice Johnson (S12345) - GPA: 3.8__str__ は、エンドユーザー向けに読みやすく有益な文字列を返すべきです。「親しみやすい」表現だと考えてください。
__repr__ メソッド
__repr__ メソッドは、REPL や repr() で使われる「公式」な文字列表現を定義します。
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __repr__(self):
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
alice = Student("Alice Johnson", "S12345", 3.8)
print(repr(alice)) # Output: Student('Alice Johnson', 'S12345', 3.8)REPL では次のようになります。
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice
Student('Alice Johnson', 'S12345', 3.8)__repr__ は、そのオブジェクトを再生成できる有効な Python コードのように見える文字列を返すべきです。「開発者向け」表現だと考えてください。曖昧さがなく、デバッグに役立つ必要があります。
__str__ と __repr__ の両方を使う
用途に応じて両方を定義できます。
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def __str__(self):
# 親しみやすく、読みやすい形式
return f"{self.name} - GPA: {self.gpa}"
def __repr__(self):
# 曖昧さのない、コードのような形式
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
alice = Student("Alice Johnson", "S12345", 3.8)
print(alice) # __str__ を使用
# Output: Alice Johnson - GPA: 3.8
print(repr(alice)) # __repr__ を使用
# Output: Student('Alice Johnson', 'S12345', 3.8)REPL では次のようになります。
>>> alice = Student("Alice Johnson", "S12345", 3.8)
>>> alice # __repr__ を使用
Student('Alice Johnson', 'S12345', 3.8)
>>> print(alice) # __str__ を使用
Alice Johnson - GPA: 3.8どちらのメソッドを定義すべきか
次が指針です。
__repr__は常に定義する: REPL やデバッグツールで使われます。どちらか一方だけ定義するなら、これを定義してください。- ユーザー向けの読みやすい形式が必要なら
__str__を定義する: エンドユーザー向けに表示されるなら、読みやすい__str__を用意します。 __repr__だけを定義した場合: Python はrepr()にそれを使い、str()は__repr__をフォールバックとして使います(つまりprint()もそれを使います)。__str__だけを定義した場合:print()は__str__を使いますが、repr()と REPL はデフォルトの__repr__(メモリアドレスが表示される)を使います。これが、通常__repr__のほうが重要である理由です。
# __repr__ のみ定義
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"Product('{self.name}', {self.price})"
item = Product("Laptop", 999.99)
print(item) # フォールバックとして __repr__ を使用
# Output: Product('Laptop', 999.99)
print(repr(item)) # __repr__ を使用
# Output: Product('Laptop', 999.99)コレクション内での文字列表現
インスタンスがコレクション(リスト、辞書など)の中にある場合、Python は __str__ ではなく __repr__ を使って表示します。
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
def __str__(self):
return f"{self.name}: {self.gpa}"
def __repr__(self):
return f"Student('{self.name}', {self.gpa})"
students = [
Student("Alice", 3.8),
Student("Bob", 3.5),
Student("Carol", 3.9)
]
# リストの print は、各 student に対して __repr__ を使います
print(students)
# Output: [Student('Alice', 3.8), Student('Bob', 3.5), Student('Carol', 3.9)]
# 個々の student の print は __str__ を使います
for student in students:
print(student)
# Output:
# Alice: 3.8
# Bob: 3.5
# Carol: 3.9このため __repr__ は曖昧さがないべきです。デバッグ中にデータ構造の中身を理解する助けになります。リストを print するとき、Python は基本的に各要素に repr() を呼んで構造が分かるように表示します。
30.7) 複数の独立したインスタンスを作成する
クラスの最も強力な点の 1 つは、独自のデータを持つ独立したインスタンスを数多く作れることです。これを詳しく見ていきましょう。
各インスタンスは独自のデータを持つ
同じクラスから複数のインスタンスを作ると、それぞれが独立した属性を維持します。
class BankAccount:
def __init__(self, account_number, holder_name, balance=0.0):
self.account_number = account_number
self.holder_name = holder_name
self.balance = balance
def deposit(self, amount):
self.balance += amount
print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")
return True
else:
print(f"Insufficient funds. Balance: ${self.balance:.2f}")
return False
def __str__(self):
return f"{self.holder_name}'s account ({self.account_number}): ${self.balance:.2f}"
# 3 つの独立したアカウントを作成
alice_account = BankAccount("ACC-001", "Alice Johnson", 1000.0)
bob_account = BankAccount("ACC-002", "Bob Smith", 500.0)
carol_account = BankAccount("ACC-003", "Carol Davis", 2000.0)
# あるアカウントへの操作は他に影響しません
alice_account.deposit(500)
# Output: Deposited $500.00. New balance: $1500.00
bob_account.withdraw(200)
# Output: Withdrew $200.00. New balance: $300.00
# 各アカウントは独自の残高を維持します
print(alice_account) # Output: Alice Johnson's account (ACC-001): $1500.00
print(bob_account) # Output: Bob Smith's account (ACC-002): $300.00
print(carol_account) # Output: Carol Davis's account (ACC-003): $2000.00この独立性は、オブジェクト指向プログラミングの基本です。各インスタンスは、独自の状態を持つ別個の実体です。
コレクション内のインスタンス
インスタンスは、リストや辞書など任意のコレクションに格納できます。
class Student:
def __init__(self, name, student_id, gpa):
self.name = name
self.student_id = student_id
self.gpa = gpa
def is_honors(self):
return self.gpa >= 3.5
def __repr__(self):
return f"Student('{self.name}', '{self.student_id}', {self.gpa})"
# 学生のリストを作成
students = [
Student("Alice Johnson", "S12345", 3.8),
Student("Bob Smith", "S12346", 3.2),
Student("Carol Davis", "S12347", 3.9),
Student("David Lee", "S12348", 3.4)
]
# 優等生をすべて見つける
honors_students = []
for student in students:
if student.is_honors():
honors_students.append(student)
print("Honors students:")
for student in honors_students:
print(f" {student.name}: {student.gpa}")
# Output:
# Honors students:
# Alice Johnson: 3.8
# Carol Davis: 3.9
# GPA の平均を計算する
total_gpa = sum(student.gpa for student in students)
average_gpa = total_gpa / len(students)
print(f"Average GPA: {average_gpa:.2f}") # Output: Average GPA: 3.58これはよくあるパターンです。複数のインスタンスを作成し、コレクションに格納し、ループや内包表記で処理します。
インスタンスは他のインスタンスを参照できる
インスタンスの属性が別のインスタンスを参照することで、オブジェクト同士の関係を作れます。
class Course:
def __init__(self, course_code, course_name):
self.course_code = course_code
self.course_name = course_name
def __str__(self):
return f"{self.course_code}: {self.course_name}"
class Student:
def __init__(self, name, student_id):
self.name = name
self.student_id = student_id
self.courses = [] # Course インスタンスのリスト
def enroll(self, course):
self.courses.append(course)
print(f"{self.name} enrolled in {course.course_name}")
def list_courses(self):
print(f"{self.name}'s courses:")
for course in self.courses:
print(f" {course}")
# コースを作成
python_course = Course("CS101", "Introduction to Python")
data_course = Course("CS102", "Data Structures")
web_course = Course("CS103", "Web Development")
# 学生を作成してコースに登録させる
alice = Student("Alice Johnson", "S12345")
alice.enroll(python_course)
alice.enroll(data_course)
# Output:
# Alice Johnson enrolled in Introduction to Python
# Alice Johnson enrolled in Data Structures
bob = Student("Bob Smith", "S12346")
bob.enroll(python_course)
bob.enroll(web_course)
# Output:
# Bob Smith enrolled in Introduction to Python
# Bob Smith enrolled in Web Development
# 各学生のコースを一覧表示する
alice.list_courses()
# Output:
# Alice Johnson's courses:
# CS101: Introduction to Python
# CS102: Data Structures
bob.list_courses()
# Output:
# Bob Smith's courses:
# CS101: Introduction to Python
# CS103: Web DevelopmentAlice と Bob の両方が python_course に登録されている点に注目してください。2 人とも同じ Course インスタンスを参照しています。これは、複数の学生が同じコースを受講できるという現実世界の関係をモデル化しています。
インスタンスの同一性と等価性
同じデータを持っていても、各インスタンスは一意のオブジェクトです。
class Student:
def __init__(self, name, gpa):
self.name = name
self.gpa = gpa
alice1 = Student("Alice", 3.8)
alice2 = Student("Alice", 3.8)
# 同一データでも別オブジェクト
print(alice1 is alice2) # Output: False
print(id(alice1) == id(alice2)) # Output: Falseデフォルトでは、== も等価性(同じデータか)ではなく同一性(同じオブジェクトか)をチェックします。第31章では、__eq__ 特殊メソッドを使って等価比較をカスタマイズする方法を学びます。
この章では、Python におけるオブジェクト指向プログラミングの基礎を紹介しました。クラスの定義、インスタンスの作成、メソッドの追加、__init__ によるインスタンス初期化、文字列表現の制御、複数の独立したインスタンスの扱いを学びました。これらの概念は、第31章と第32章で探求する、より高度な OOP 機能の土台になります。