Python & AI Tutorials Logo
Python プログラミング

31. 高度なクラス機能

第30章では、インスタンス属性とメソッドを持つ基本的なクラスの作り方を学びました。ここからは、オブジェクトの振る舞いをより細かく制御できる、より高度なクラス機能を見ていきます。これらの機能により、加算・比較・インデックスアクセスのような操作を自然な構文で行える、組み込みの Python 型のように感じられるクラスを作れるようになります。

31.1) クラス変数とインスタンス変数

クラス内で属性を作るとき、保存先には根本的に2つの異なる場所があります。クラス自体に保存するか、個々のインスタンスに保存するかです。この違いを理解することは、正しいオブジェクト指向コードを書くうえで重要です。

31.1.1) インスタンス変数の理解

インスタンス変数(instance variables) は、特定のオブジェクトに属する属性です。各インスタンスは、これらの変数の独立したコピーをそれぞれ持ちます。第30章を通してインスタンス変数を使ってきました。self を使って __init__ の中で作る属性のことです。

python
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 インスタンスは、自分専用の ownerbalance を持ちます。account1.balance を変更しても account2.balance には影響しません。完全に独立しています。

31.1.2) クラス変数の理解

クラス変数(class variables) は、特定のインスタンスではなく、クラスそのものに属する属性です。すべてのインスタンスが同じクラス変数を共有します。クラス変数は、メソッドの外側で、クラス本体に直接定義します。

python
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.02

interest_rate は、インスタンス経由(account1.interest_rate)でもクラス自体経由(BankAccount.interest_rate)でもアクセスできることに注目してください。どちらも同じ変数を参照しています。

クラス変数が強力な理由はここです。クラス変数を変更すると、すべてのインスタンスがその変更を見られます。

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)
 
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 はクラス変数をシャドーイング(隠す)するインスタンス変数を作成します。

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) クラス変数の実用的な使い道

クラス変数は、すべてのインスタンスで共有されるべきデータに役立ちます。

python
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_studentsself.total_students ではない)を使っている点に注目してください。これにより、インスタンス変数を作っているのではなく、クラス変数を変更していることが明確になります。

クラス変数

クラス本体で定義される

すべてのインスタンスで共有される

ClassName.variable でアクセスする

インスタンス変数

init で self を使って定義される

各インスタンス固有

instance.variable でアクセスする

31.2) @property で属性を管理する

属性へアクセスしたり変更したりするときに、何が起こるかを制御したい場合があります。たとえば、値が正の数であることを検証したい、または値を保存するのではなく必要に応じて計算したい、といった場合です。Python の @property デコレータは、単純な属性アクセスのように見えるメソッドを書けるようにします。

31.2.1) 問題: 属性を直接アクセスすると検証できない

属性に直接アクセスしていると、値の検証や変換ができません。

python
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」に変えます。

python
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 と同期されます。

python
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 メソッドを追加します。

python
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.0

setter メソッドは新しい値を受け取り、保存する前に検証や変換を行えます。

31.2.4) 検証にプロパティを使う

プロパティは制約の強制に最適です。

python
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 を定義しないプロパティは、読み取り専用になります。

python
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

これは、保存ではなく計算すべき派生値に便利です。

@property デコレータ

メソッドを getter に変える

属性のようにアクセスされる

その場で値を計算できる

@property_name.setter

プロパティに setter を追加する

保存前に検証できる

値を変換できる

31.3) @classmethod によるクラスメソッド

インスタンスではなくクラスそのものを扱う必要があるメソッドもあります。クラスメソッド(class methods) は、最初の引数としてインスタンス(self)ではなくクラス(慣習的に cls と名付ける)を受け取ります。

31.3.1) クラスメソッドの定義

クラスメソッドは @classmethod デコレータで作成します。

python
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 School

cls パラメータには、通常メソッドで self が自動的にインスタンスを受け取るのと同様に、クラスが自動的に渡されます。

31.3.2) クラスメソッドによる別コンストラクタ

クラスメソッドの最も一般的な用途のひとつが、別コンストラクタ(alternative constructors)を作ることです。つまり、インスタンスを生成する別の方法を提供します。

python
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-27

from_stringtoday がどちらも cls(...) を返している点に注目してください。これにより、そのクラスの新しいインスタンスが作られます。Date をベタ書きせず cls を使うことで、サブクラスでも正しく動作します(継承については第32章で学びます)。

31.3.3) ファクトリパターンのためのクラスメソッド

クラスメソッドは、異なる設定でインスタンスを作るのに役立ちます。

python
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:5432

31.3.4) インスタンス数を数えるクラスメソッド

クラスメソッドは、クラス変数と組み合わせて、すべてのインスタンスに関する情報を追跡できます。

python
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: 0

31.4) @staticmethod によるスタティックメソッド

スタティックメソッド(static methods) は、最初の引数としてインスタンス(self)もクラス(cls)も受け取りません。クラスの中に定義されているだけの通常の関数で、そのクラスと論理的に関連しているためにクラス内に置かれています。

31.4.1) スタティックメソッドの定義

スタティックメソッドは @staticmethod デコレータで作成します。

python
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) スタティックメソッド / クラスメソッド / インスタンスメソッドの使い分け

選び方は次のとおりです。

python
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) 実用例: バリデーション用ユーティリティ

スタティックメソッドは、バリデーションやユーティリティ関数に最適です。

python
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 username

31.5) 特殊メソッド(マジックメソッド)の理解

Python の 特殊メソッド(special methods)マジックメソッド(magic methods) や、アンダースコアが2つずつ付くことから dunder methods とも呼ばれます)は、Python の組み込み演算でのオブジェクトの振る舞いをカスタマイズできます。第30章ではすでに __init____str____repr__ を使いました。ここではさらに多くのものを見ていきます。

31.5.1) 特殊メソッドがすること

特殊メソッドは、特定の構文や組み込み関数を使ったときに、Python によって自動的に呼び出されます。

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() を使ったときに呼ばれます。

python
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 演算子を使ったときに呼ばれます。

python
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) より完成度の高いコレクションクラスを作る

学生の成績を追跡する、より現実的なコレクションクラスを作ってみましょう。

python
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.66666666666667

get_average()if student not in self を使っている点に注目してください。これは自作の __contains__() を呼び出すため、コードが自然に読めるようになります。

31.7) 例2: 演算子オーバーロード(add, eq, lt

演算子オーバーロード(operator overloading) とは、+==< のような演算子が自作クラスに対して何を意味するかを定義することです。これにより、Python の構文でオブジェクトを自然に扱えるようになります。

31.7.1) 加算のために add を実装する

__add__() という特殊メソッドは、+ 演算子を使ったときに呼ばれます。

python
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__() という特殊メソッドは、== 演算子を使ったときに呼ばれます。

python
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 クラスの比較演算子を実装してみましょう。

python
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: True

31.7.4) 演算子での型不一致を扱う

演算子を実装するときは、相手のオペランドが期待する型でない場合も扱うべきです。

python
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)を試します。これは、異なる型同士で演算子を正しく動かすために重要です。

演算子オーバーロード

算術演算子

比較演算子

+ のための __add__
- のための __sub__
* のための __mul__

/ のための truediv

== のための eq

< のための lt

<= のための le

> のための __gt__
>= のための __ge__

31.8) 例3: シーケンスアクセス(getitem, setitem

__getitem__()__setitem__() という特殊メソッドにより、自作クラスでインデックス構文(obj[key])を使えるようになります。これによって、オブジェクトをリスト、辞書、その他のシーケンスのように振る舞わせられます。

31.8.1) インデックスアクセスのために getitem を実装する

__getitem__() メソッドは、角括弧を使って要素へアクセスしたときに呼ばれます。

python
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__() メソッドはスライスも扱います。

python
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__() メソッドは、インデックスに代入したときに呼ばれます。

python
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 B

31.8.4) getitem でオブジェクトを反復可能にする

興味深い副作用があります。__getitem__() を 0 から始まる整数インデックスで実装すると、オブジェクトは自動的に反復可能(iterable)になります。

python
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 C

Python は __getitem__(0)、次に __getitem__(1)、というように IndexError を受け取るまで呼び出して反復しようとします。これは古い反復プロトコルです。モダンなイテレータプロトコルについては第35章で学びます。

31.8.5) 文字列キーで辞書のようにアクセスする

__getitem__()__setitem__() は整数に限らず、任意の型のキーで動作します。

python
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

シーケンスアクセス

getitem

setitem

obj[key] のために呼ばれる

インデックスアクセスを扱う

スライスを扱う

オブジェクトを反復可能にする

obj[key] = value のために呼ばれる

代入を可能にする

値を検証できる


この章では、Python の構文とシームレスに統合される高度なクラスを作る方法を示しました。クラス変数、プロパティ、クラスメソッド、スタティックメソッド、特殊メソッドを実装することで、自作クラスを組み込み型のように振る舞わせられます。第32章では継承とポリモーフィズムを扱います。これにより、振る舞いを共有しつつ拡張できる関連クラスの階層を構築できます。


© 2025. Primesoft Co., Ltd.
support@primesoft.ai