31. 高度なクラス機能
第30章では、インスタンス属性とメソッドを持つ基本的なクラスの作り方を学びました。ここからは、オブジェクトの振る舞いをより細かく制御できる、より高度なクラス機能を見ていきます。これらの機能により、加算・比較・インデックスアクセスのような操作を自然な構文で行える、組み込みの Python 型のように感じられるクラスを作れるようになります。
31.1) クラス変数とインスタンス変数
クラス内で属性を作るとき、保存先には根本的に2つの異なる場所があります。クラス自体に保存するか、個々のインスタンスに保存するかです。この違いを理解することは、正しいオブジェクト指向コードを書くうえで重要です。
31.1.1) インスタンス変数の理解
インスタンス変数(instance variables) は、特定のオブジェクトに属する属性です。各インスタンスは、これらの変数の独立したコピーをそれぞれ持ちます。第30章を通してインスタンス変数を使ってきました。self を使って __init__ の中で作る属性のことです。
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # インスタンス変数
self.balance = balance # インスタンス変数
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.balance) # Output: 1000
print(account2.balance) # Output: 500それぞれの BankAccount インスタンスは、自分専用の owner と balance を持ちます。account1.balance を変更しても account2.balance には影響しません。完全に独立しています。
31.1.2) クラス変数の理解
クラス変数(class variables) は、特定のインスタンスではなく、クラスそのものに属する属性です。すべてのインスタンスが同じクラス変数を共有します。クラス変数は、メソッドの外側で、クラス本体に直接定義します。
class BankAccount:
interest_rate = 0.02 # クラス変数 - すべてのインスタンスで共有される
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def apply_interest(self):
self.balance += self.balance * BankAccount.interest_rate
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
print(BankAccount.interest_rate) # Output: 0.02interest_rate は、インスタンス経由(account1.interest_rate)でもクラス自体経由(BankAccount.interest_rate)でもアクセスできることに注目してください。どちらも同じ変数を参照しています。
クラス変数が強力な理由はここです。クラス変数を変更すると、すべてのインスタンスがその変更を見られます。
class BankAccount:
interest_rate = 0.02
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
# クラス変数を変更する
BankAccount.interest_rate = 0.03
print(account1.interest_rate) # Output: 0.03
print(account2.interest_rate) # Output: 0.03両方のインスタンスが同じクラス変数を見ているため、どちらもすぐに新しい利率が反映されます。
31.1.3) シャドーイングの罠: インスタンス変数がクラス変数を隠すとき
微妙ですが重要な挙動があります。インスタンス経由で属性に代入すると、Python はクラス変数をシャドーイング(隠す)するインスタンス変数を作成します。
class BankAccount:
interest_rate = 0.02
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
# クラス変数をシャドーイングするインスタンス変数を作成する
account1.interest_rate = 0.05
print(account1.interest_rate) # Output: 0.05 (instance variable)
print(account2.interest_rate) # Output: 0.02 (class variable)
print(BankAccount.interest_rate) # Output: 0.02 (class variable)これで account1 は、クラス変数を隠す独自の interest_rate インスタンス変数を持つことになります。クラス変数は依然として存在しますが、account1.interest_rate は代わりにインスタンス変数を参照します。これは通常望ましくありません。クラス変数を変更する必要がある場合は、インスタンス経由ではなくクラス名経由で変更してください。
31.1.4) クラス変数の実用的な使い道
クラス変数は、すべてのインスタンスで共有されるべきデータに役立ちます。
class Student:
school_name = "Python High School" # すべての学生で同じ
total_students = 0 # 何人の学生が存在するかを追跡する
def __init__(self, name, grade):
self.name = name
self.grade = grade
Student.total_students += 1 # 学生を作成するときにインクリメントする
def __str__(self):
return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
print(student1) # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}") # Output: Total students: 3__init__ の中で Student.total_students(self.total_students ではない)を使っている点に注目してください。これにより、インスタンス変数を作っているのではなく、クラス変数を変更していることが明確になります。
31.2) @property で属性を管理する
属性へアクセスしたり変更したりするときに、何が起こるかを制御したい場合があります。たとえば、値が正の数であることを検証したい、または値を保存するのではなく必要に応じて計算したい、といった場合です。Python の @property デコレータは、単純な属性アクセスのように見えるメソッドを書けるようにします。
31.2.1) 問題: 属性を直接アクセスすると検証できない
属性に直接アクセスしていると、値の検証や変換ができません。
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
temp = Temperature(25)
print(temp.celsius) # Output: 25
# 物理的にありえない温度を設定することも止められない
temp.celsius = -500 # Below absolute zero (-273.15°C)!
print(temp.celsius) # Output: -500
# あるいは不合理に高い値も
temp.celsius = 1000000
print(temp.celsius) # Output: 1000000検証がないと、誤って無効なデータを設定してしまい、プログラムの後半でバグにつながることがあります。get_celsius() や set_celsius() のようなメソッドを使うこともできますが、それは Python らしい書き方ではありません。Python 開発者は、Java や C++ のような getter/setter メソッド経由ではなく、属性を直接アクセスすることを期待します。
31.2.2) 計算される属性に @property を使う
@property デコレータは、メソッドを属性のようにアクセスされる「getter」に変えます。
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
"""celsius を fahrenheit にその場で変換する"""
return self.celsius * 9/5 + 32
temp = Temperature(25)
print(temp.celsius) # Output: 25
print(temp.fahrenheit) # Output: 77.0 (computed, not stored)括弧なしで temp.fahrenheit を呼んでいることに注目してください。属性へアクセスしているように見えますが、実際にはメソッドが呼ばれています。fahrenheit の値はアクセスするたびに計算されるので、常に celsius と同期されます。
temp = Temperature(0)
print(temp.fahrenheit) # Output: 32.0
temp.celsius = 100
print(temp.fahrenheit) # Output: 212.0 (automatically updated)31.2.3) @property_name.setter で setter を追加する
プロパティに値を設定できるようにするには、@property_name.setter デコレータを使って setter メソッドを追加します。
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""設定時に fahrenheit を celsius に変換する"""
self.celsius = (value - 32) * 5/9
temp = Temperature(0)
print(temp.celsius) # Output: 0
print(temp.fahrenheit) # Output: 32.0
# fahrenheit を使って温度を設定する
temp.fahrenheit = 212
print(temp.celsius) # Output: 100.0
print(temp.fahrenheit) # Output: 212.0setter メソッドは新しい値を受け取り、保存する前に検証や変換を行えます。
31.2.4) 検証にプロパティを使う
プロパティは制約の強制に最適です。
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # アンダースコアは「内部利用」を示唆する
@property
def balance(self):
"""現在の残高を取得する"""
return self._balance
@balance.setter
def balance(self, value):
"""残高を設定する。ただし 0 以上の場合のみ"""
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
account = BankAccount("Alice", 1000)
print(account.balance) # Output: 1000
account.balance = 1500 # 問題なく動く
print(account.balance) # Output: 1500
# これはエラーを発生させる
account.balance = -100
# Output: ValueError: Balance cannot be negative命名規則に注目してください。実際の値は _balance(先頭にアンダースコア)に保存し、balance プロパティ経由で公開しています。アンダースコアは Python の慣習で「これは内部実装の詳細です」を示唆しますが、属性自体は技術的にはアクセス可能です。このパターンにより、実際の保存先を分けたまま、プロパティを通してアクセスを制御できます。
31.2.5) 読み取り専用プロパティ
setter を定義しないプロパティは、読み取り専用になります。
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
"""計算される読み取り専用プロパティ"""
return self.width * self.height
rect = Rectangle(5, 3)
print(rect.area) # Output: 15
rect.width = 10
print(rect.area) # Output: 30 (automatically updated)
# area を設定しようとするとエラーになる
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setterこれは、保存ではなく計算すべき派生値に便利です。
31.3) @classmethod によるクラスメソッド
インスタンスではなくクラスそのものを扱う必要があるメソッドもあります。クラスメソッド(class methods) は、最初の引数としてインスタンス(self)ではなくクラス(慣習的に cls と名付ける)を受け取ります。
31.3.1) クラスメソッドの定義
クラスメソッドは @classmethod デコレータで作成します。
class Student:
school_name = "Python High School"
def __init__(self, name, grade):
self.name = name
self.grade = grade
@classmethod
def get_school_name(cls):
"""クラスメソッド - インスタンスではなくクラスを受け取る"""
return cls.school_name
# クラス自体に対して呼び出す
print(Student.get_school_name()) # Output: Python High School
# インスタンスに対して呼び出すこともできる(ただし cls は依然としてクラス)
student = Student("Alice", 10)
print(student.get_school_name()) # Output: Python High Schoolcls パラメータには、通常メソッドで self が自動的にインスタンスを受け取るのと同様に、クラスが自動的に渡されます。
31.3.2) クラスメソッドによる別コンストラクタ
クラスメソッドの最も一般的な用途のひとつが、別コンストラクタ(alternative constructors)を作ることです。つまり、インスタンスを生成する別の方法を提供します。
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""'2024-12-27' のような文字列から Date を作成する"""
year, month, day = date_string.split('-')
return cls(int(year), int(month), int(day))
@classmethod
def today(cls):
"""今日の日付の Date を作成する(簡略化した例)"""
# 実際のコードでは datetime モジュールを使います
return cls(2024, 12, 27)
def __str__(self):
return f"{self.year}-{self.month:02d}-{self.day:02d}"
# 通常のコンストラクタ
date1 = Date(2024, 12, 27)
print(date1) # Output: 2024-12-27
# 文字列から作る別コンストラクタ
date2 = Date.from_string("2024-12-27")
print(date2) # Output: 2024-12-27
# 今日の日付の別コンストラクタ
date3 = Date.today()
print(date3) # Output: 2024-12-27from_string と today がどちらも cls(...) を返している点に注目してください。これにより、そのクラスの新しいインスタンスが作られます。Date をベタ書きせず cls を使うことで、サブクラスでも正しく動作します(継承については第32章で学びます)。
31.3.3) ファクトリパターンのためのクラスメソッド
クラスメソッドは、異なる設定でインスタンスを作るのに役立ちます。
class DatabaseConnection:
def __init__(self, host, port, database, username):
self.host = host
self.port = port
self.database = database
self.username = username
@classmethod
def for_development(cls):
"""開発用に設定された接続を作成する"""
return cls("localhost", 5432, "dev_db", "dev_user")
@classmethod
def for_production(cls):
"""本番用に設定された接続を作成する"""
return cls("prod.example.com", 5432, "prod_db", "prod_user")
def __str__(self):
return f"Connection to {self.database} at {self.host}:{self.port}"
# 事前設定された接続を簡単に作成できる
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
print(dev_conn) # Output: Connection to dev_db at localhost:5432
print(prod_conn) # Output: Connection to prod_db at prod.example.com:543231.3.4) インスタンス数を数えるクラスメソッド
クラスメソッドは、クラス変数と組み合わせて、すべてのインスタンスに関する情報を追跡できます。
class Product:
total_products = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.total_products += 1
@classmethod
def get_total_products(cls):
"""作成された商品の総数を返す"""
return cls.total_products
@classmethod
def reset_count(cls):
"""商品カウンタをリセットする"""
cls.total_products = 0
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
print(Product.get_total_products()) # Output: 3
Product.reset_count()
print(Product.get_total_products()) # Output: 031.4) @staticmethod によるスタティックメソッド
スタティックメソッド(static methods) は、最初の引数としてインスタンス(self)もクラス(cls)も受け取りません。クラスの中に定義されているだけの通常の関数で、そのクラスと論理的に関連しているためにクラス内に置かれています。
31.4.1) スタティックメソッドの定義
スタティックメソッドは @staticmethod デコレータで作成します。
class MathUtils:
@staticmethod
def is_even(number):
"""数が偶数かどうかを確認する"""
return number % 2 == 0
@staticmethod
def is_prime(number):
"""数が素数かどうかを確認する(簡略化)"""
if number < 2:
return False
for i in range(2, int(number ** 0.5) + 1):
if number % i == 0:
return False
return True
# クラスに対してスタティックメソッドを呼び出す
print(MathUtils.is_even(4)) # Output: True
print(MathUtils.is_even(7)) # Output: False
print(MathUtils.is_prime(17)) # Output: True
print(MathUtils.is_prime(18)) # Output: False
# インスタンスに対して呼び出すこともできる(ただし同じ関数)
utils = MathUtils()
print(utils.is_even(10)) # Output: Trueスタティックメソッドはインスタンスやクラスのデータへアクセスする必要がありません。自己完結したユーティリティ関数です。
31.4.2) スタティックメソッド / クラスメソッド / インスタンスメソッドの使い分け
選び方は次のとおりです。
class Temperature:
# クラス変数
absolute_zero_celsius = -273.15
def __init__(self, celsius):
self.celsius = celsius
# インスタンスメソッド - インスタンスデータ(self)へのアクセスが必要
def to_fahrenheit(self):
return self.celsius * 9/5 + 32
# クラスメソッド - クラスデータ(cls)へのアクセスが必要
@classmethod
def get_absolute_zero(cls):
return cls.absolute_zero_celsius
# スタティックメソッド - インスタンス/クラスデータが不要
@staticmethod
def celsius_to_kelvin(celsius):
return celsius + 273.15
@staticmethod
def fahrenheit_to_celsius(fahrenheit):
return (fahrenheit - 32) * 5/9
temp = Temperature(25)
# インスタンスメソッド - インスタンスデータを使う
print(temp.to_fahrenheit()) # Output: 77.0
# クラスメソッド - クラスデータを使う
print(Temperature.get_absolute_zero()) # Output: -273.15
# スタティックメソッド - 単なるユーティリティ関数
print(Temperature.celsius_to_kelvin(25)) # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77)) # Output: 25.0ガイドライン:
- インスタンス属性(
self)へのアクセスが必要なら インスタンスメソッド(instance methods) を使います - クラス属性へのアクセスが必要、または別コンストラクタが欲しいなら クラスメソッド(class methods)(
cls)を使います - インスタンス/クラスデータへのアクセスが不要で、その関数がクラスと論理的に関連するなら スタティックメソッド(static methods) を使います
注: スタティックメソッドは単体の関数にもできますが、クラスに入れることで関連機能をまとめられ、グローバル名前空間を散らかさずに済みます。
| メソッド種別 | 第1引数 | 使う場面 |
|---|---|---|
| インスタンスメソッド | self | インスタンスデータへのアクセスが必要 |
| クラスメソッド | cls | クラスデータへのアクセスが必要、または別コンストラクタが欲しい |
| スタティックメソッド | (なし) | クラスに関連するユーティリティ関数 |
31.4.3) 実用例: バリデーション用ユーティリティ
スタティックメソッドは、バリデーションやユーティリティ関数に最適です。
class User:
def __init__(self, username, password):
if not User.is_valid_username(username):
raise ValueError("Invalid username")
if not User.is_valid_password(password):
raise ValueError("Invalid password")
self.username = username
self._password = password
@staticmethod
def is_valid_username(username):
"""username が要件を満たしているかを確認する"""
return len(username) >= 3 and username.isalnum()
@staticmethod
def is_valid_password(password):
"""password がセキュリティ要件を満たしているかを確認する"""
return len(password) >= 8 and any(c.isdigit() for c in password)
# これらのバリデーションメソッドは独立して使える
print(User.is_valid_username("alice123")) # Output: True
print(User.is_valid_username("ab")) # Output: False
print(User.is_valid_password("pass1234")) # Output: True
# そしてクラス内のどのメソッドからでも使える
try:
user = User("ab", "short")
except ValueError as e:
print(f"Error: {e}") # Output: Error: Invalid username31.5) 特殊メソッド(マジックメソッド)の理解
Python の 特殊メソッド(special methods)(マジックメソッド(magic methods) や、アンダースコアが2つずつ付くことから dunder methods とも呼ばれます)は、Python の組み込み演算でのオブジェクトの振る舞いをカスタマイズできます。第30章ではすでに __init__、__str__、__repr__ を使いました。ここではさらに多くのものを見ていきます。
31.5.1) 特殊メソッドがすること
特殊メソッドは、特定の構文や組み込み関数を使ったときに、Python によって自動的に呼び出されます。
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
point = Point(3, 4)
# print() を呼ぶと、Python は __str__() を呼ぶ
print(point) # Output: Point(3, 4)
# これは次と等価です: print(point.__str__())特殊メソッドを使うと、自作クラスを組み込み型のように振る舞わせられます。たとえば、オブジェクトを次のようにできます。
- 算術演算(
+,-,*,/)をサポートする - 比較可能にする(
<,>,==) len()、in、インデックスアクセスと連携させる- コンテナやシーケンスのように振る舞わせる
31.5.2) よくある特殊メソッドのカテゴリ
特殊メソッドの主なカテゴリは次のとおりです。
文字列表現(オブジェクトがどう表示されるか):
__str__()-print()とstr()用__repr__()- REPL とrepr()用
比較(オブジェクト同士の比較):
__eq__()-==用__ne__()-!=用__lt__()-<用__le__()-<=用__gt__()->用__ge__()->=用
算術(数学的な操作):
__add__()-+用__sub__()--用__mul__()-*用__truediv__()-/用
コンテナ/シーケンス(コレクションのような振る舞い):
__len__()-len()用__contains__()-in用__getitem__()- インデックスアクセスobj[key]用__setitem__()- 代入obj[key] = value用
これらは以降の節で詳しく見ていきます。
31.6) 例1: コレクションインターフェース(len, contains)
アイテムのコレクションを管理するクラスを作り、Python の組み込み len() 関数や in 演算子で動くようにしてみましょう。
31.6.1) len() のために len を実装する
__len__() という特殊メソッドは、オブジェクトに対して len() を使ったときに呼ばれます。
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
"""カート内のアイテム数を返す"""
return len(self.items)
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
# len() は __len__() を呼ぶ
print(len(cart)) # Output: 3__len__() がなければ、len(cart) は TypeError を発生させます。これを実装することで、ShoppingCart は組み込みコレクションのように動作します。
31.6.2) in 演算子のために contains を実装する
__contains__() という特殊メソッドは、in 演算子を使ったときに呼ばれます。
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
def __contains__(self, item):
"""アイテムがカート内にあるかを確認する"""
return item in self.items
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
# in 演算子は __contains__() を呼ぶ
print("Apple" in cart) # Output: True
print("Orange" in cart) # Output: Falseこれで、メンバーシップテストを Python らしい自然な構文で書けるようになりました。
31.6.3) より完成度の高いコレクションクラスを作る
学生の成績を追跡する、より現実的なコレクションクラスを作ってみましょう。
class GradeBook:
def __init__(self):
self.grades = {} # student_name: list of grades
def add_grade(self, student, grade):
"""学生の成績を追加する"""
if student not in self.grades:
self.grades[student] = []
self.grades[student].append(grade)
def __len__(self):
"""学生の人数を返す"""
return len(self.grades)
def __contains__(self, student):
"""学生に成績が1つでもあるかを確認する"""
return student in self.grades
def get_average(self, student):
"""学生の平均点を取得する"""
if student not in self:
return None
grades = self.grades[student]
return sum(grades) / len(grades)
def __str__(self):
return f"GradeBook with {len(self)} students"
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
print(gradebook) # Output: GradeBook with 2 students
print(len(gradebook)) # Output: 2
print("Alice" in gradebook) # Output: True
print("Carol" in gradebook) # Output: False
print(f"Alice's average: {gradebook.get_average('Alice')}") # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}") # Output: Bob's average: 82.66666666666667get_average() が if student not in self を使っている点に注目してください。これは自作の __contains__() を呼び出すため、コードが自然に読めるようになります。
31.7) 例2: 演算子オーバーロード(add, eq, lt)
演算子オーバーロード(operator overloading) とは、+、==、< のような演算子が自作クラスに対して何を意味するかを定義することです。これにより、Python の構文でオブジェクトを自然に扱えるようになります。
31.7.1) 加算のために add を実装する
__add__() という特殊メソッドは、+ 演算子を使ったときに呼ばれます。
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""2つのベクトルを足す"""
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
# + 演算子は __add__() を呼ぶ
v3 = v1 + v2
print(v3) # Output: Vector(4, 6)Python が v1 + v2 を見ると、v1.__add__(v2) を呼びます。左オペランドの __add__() メソッドが、右オペランドを引数として受け取ります。
31.7.2) 等価比較のために eq を実装する
__eq__() という特殊メソッドは、== 演算子を使ったときに呼ばれます。
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
"""2つのベクトルが等しいかを確認する"""
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
# == 演算子は __eq__() を呼ぶ
print(v1 == v2) # Output: True
print(v1 == v3) # Output: False__eq__() がない場合、Python は値ではなくオブジェクトの同一性(メモリ上で同じオブジェクトかどうか)を比較します。__eq__() によって、このクラスでの「等しい」の意味を定義できます。
31.7.3) 比較演算子を実装する
Money クラスの比較演算子を実装してみましょう。
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
"""金額が等しいかを確認する"""
return self.amount == other.amount
def __lt__(self, other):
"""この金額が other より小さいかを確認する"""
return self.amount < other.amount
def __le__(self, other):
"""この金額が other 以下かを確認する"""
return self.amount <= other.amount
def __gt__(self, other):
"""この金額が other より大きいかを確認する"""
return self.amount > other.amount
def __ge__(self, other):
"""この金額が other 以上かを確認する"""
return self.amount >= other.amount
def __str__(self):
return f"${self.amount:.2f}"
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
print(price1 == price3) # Output: True
print(price1 < price2) # Output: True
print(price1 <= price3) # Output: True
print(price2 > price1) # Output: True
print(price2 >= price1) # Output: True31.7.4) 演算子での型不一致を扱う
演算子を実装するときは、相手のオペランドが期待する型でない場合も扱うべきです。
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""2つのベクトルを足す、またはスカラーを両成分に足す"""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
elif isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
else:
return NotImplemented # Python に other.__radd__(self) を試させる
def __eq__(self, other):
if not isinstance(other, Vector):
return False
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Output: Vector(4, 6) (vector addition)
print(v1 + 5) # Output: Vector(6, 7) (scalar addition)
print(v1 == v2) # Output: False
print(v1 == "not a vector") # Output: False (no error)NotImplemented(組み込みの特別な定数)を返すと、Python は相手オペランド側の反射演算(reflected operation)を試します。これは、異なる型同士で演算子を正しく動かすために重要です。
31.8) 例3: シーケンスアクセス(getitem, setitem)
__getitem__() と __setitem__() という特殊メソッドにより、自作クラスでインデックス構文(obj[key])を使えるようになります。これによって、オブジェクトをリスト、辞書、その他のシーケンスのように振る舞わせられます。
31.8.1) インデックスアクセスのために getitem を実装する
__getitem__() メソッドは、角括弧を使って要素へアクセスしたときに呼ばれます。
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
"""インデックスで曲を取得する"""
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
# インデックスアクセスは __getitem__() を呼ぶ
print(playlist[0]) # Output: Song A
print(playlist[1]) # Output: Song B
print(playlist[-1]) # Output: Song C (negative indexing works!)self.songs[index] に委譲しているので、リストのインデックス機能がすべて自動的に使えます。正のインデックス、負のインデックス、そして不正なインデックスで IndexError を送出することも含まれます。
31.8.2) getitem でスライスをサポートする
同じ __getitem__() メソッドはスライスも扱います。
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
"""インデックスまたはスライスで曲を取得する"""
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
# スライスも __getitem__() を呼ぶ
print(playlist[1:3]) # Output: ['Song B', 'Song C']
print(playlist[:2]) # Output: ['Song A', 'Song B']
print(playlist[::2]) # Output: ['Song A', 'Song C']スライスを使うと、Python は slice オブジェクトを __getitem__() に渡します。self.songs[index] に委譲することで、すべてのスライス構文を自動的にサポートできます。
31.8.3) 代入のために setitem を実装する
__setitem__() メソッドは、インデックスに代入したときに呼ばれます。
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __setitem__(self, index, value):
"""特定のインデックスの曲を置き換える"""
self.songs[index] = value
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
print(playlist[1]) # Output: Song B
# 代入は __setitem__() を呼ぶ
playlist[1] = "New Song B"
print(playlist[1]) # Output: New Song B31.8.4) getitem でオブジェクトを反復可能にする
興味深い副作用があります。__getitem__() を 0 から始まる整数インデックスで実装すると、オブジェクトは自動的に反復可能(iterable)になります。
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
# for ループが自動的に動く!
for song in playlist:
print(song)
# Output:
# Song A
# Song B
# Song CPython は __getitem__(0)、次に __getitem__(1)、というように IndexError を受け取るまで呼び出して反復しようとします。これは古い反復プロトコルです。モダンなイテレータプロトコルについては第35章で学びます。
31.8.5) 文字列キーで辞書のようにアクセスする
__getitem__() と __setitem__() は整数に限らず、任意の型のキーで動作します。
class ScoreBoard:
def __init__(self):
self.scores = {}
def __getitem__(self, player_name):
"""プレイヤーのスコアを取得する"""
return self.scores.get(player_name, 0)
def __setitem__(self, player_name, score):
"""プレイヤーのスコアを設定する"""
self.scores[player_name] = score
def __contains__(self, player_name):
return player_name in self.scores
def __len__(self):
return len(self.scores)
scoreboard = ScoreBoard()
# 文字列キーを使ってスコアを設定する
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
# スコアを更新する
scoreboard["Alice"] = 120
# スコアを取得する
print(scoreboard["Alice"]) # Output: 120
print(scoreboard["Bob"]) # Output: 85
print(scoreboard["Carol"]) # Output: 0
print("Alice" in scoreboard) # Output: True
print(len(scoreboard)) # Output: 2この章では、Python の構文とシームレスに統合される高度なクラスを作る方法を示しました。クラス変数、プロパティ、クラスメソッド、スタティックメソッド、特殊メソッドを実装することで、自作クラスを組み込み型のように振る舞わせられます。第32章では継承とポリモーフィズムを扱います。これにより、振る舞いを共有しつつ拡張できる関連クラスの階層を構築できます。