演算子のオーバーロード
dunderメソッドの仕組み
Pythonでは、クラスの中で特定の名前を持つメソッドを定義することで、+ や == といった演算子の挙動をカスタマイズできます。
これを演算子のオーバーロードと呼びます。
これらのメソッドは、名前の前後がダブルアンダースコア(__)で挟まれていることから、特殊メソッド、またはdunderメソッドと呼ばれます。
例えば、オブジェクト同士を + 演算子で加算すると、Pythonは内部的に __add__ メソッドを呼び出します。
同様に、オブジェクトの長さを len() 関数で取得しようとすると __len__ メソッドが呼び出されます。
これにより、自作したクラスのオブジェクトであっても、Pythonの組み込みのデータ型と同じように直感的な操作ができるようになります。
Vector2Dクラスによる実装例
二次元ベクトルを表す Vector2D クラスの実装を通じて、各種特殊メソッドの使い方を見ていきましょう。
まず、特殊メソッドを使用せずに、通常のメソッドで加算や文字列表記を行う素朴なプログラムは以下のようになります。
vector_simple.py
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def add(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
def to_string(self):
return f"Vector2D({self.x}, {self.y})"
v1 = Vector2D(2.0, 3.0)
v2 = Vector2D(4.0, 5.0)
v3 = v1.add(v2)
print(v3.to_string())
プログラムの実行結果は以下のようになります。
実行結果
Vector2D(6.0, 8.0)
この実装では、加算のたびに add メソッドを明示的に呼び出す必要があり、数式としての読みやすさに欠けます。
加算と文字列表記を特殊メソッドでオーバーロードしたプログラムは以下のようになります。
vector_overload.py
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
def __str__(self):
return f"({self.x}, {self.y})"
def __repr__(self):
return f"Vector2D({self.x}, {self.y})"
v1 = Vector2D(2.0, 3.0)
v2 = Vector2D(4.0, 5.0)
v3 = v1 + v2
print(v3)
print(repr(v3))
プログラムの実行結果は以下のようになります。
実行結果
(6.0, 8.0)
Vector2D(6.0, 8.0)
__add__ を定義したことで v1 + v2 という自然な記述ができるようになりました。
また、__str__ は print() 関数などに渡された際のユーザー向けの文字列を返し、__repr__ はデバッグ時などに開発者がオブジェクトを識別しやすい文字列を返します。
さらに、スカラー倍、同値性判定、コンテナとしてのアクセスやイテレータの機能をすべて実装した完成形のプログラムは以下のようになります。
vector_complete.py
class Vector2D:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector2D(self.x + other.x, self.y + other.y)
def __mul__(self, scalar):
return Vector2D(self.x * scalar, self.y * scalar)
def __eq__(self, other):
if not isinstance(other, Vector2D):
return False
return self.x == other.x and self.y == other.y
def __len__(self):
return 2
def __getitem__(self, index):
if index == 0:
return self.x
elif index == 1:
return self.y
raise IndexError("インデックスは0または1のみ指定できます")
def __iter__(self):
yield self.x
yield self.y
def __repr__(self):
return f"Vector2D({self.x}, {self.y})"
v1 = Vector2D(2.0, 3.0)
v2 = Vector2D(2.0, 3.0)
v3 = Vector2D(4.0, 6.0)
print(v1 * 2)
print(v1 == v2)
print(v1 == v3)
print(len(v1))
print(v1[0])
x_coord, y_coord = v1
print(x_coord, y_coord)
for val in v1:
print(val)
プログラムの実行結果は以下のようになります。
実行結果
Vector2D(4.0, 6.0)
True
False
2
2.0
2.0 3.0
2.0
3.0
__mul__ を実装したことで v1 * 2 によるスカラー倍ができるようになりました。
__eq__ によってオブジェクト同士の座標が一致しているかを == で比較できます。
さらに、__len__ と __getitem__ を実装したことで、リストのように v1[0] で x 座標を取得できます。
また、__iter__ を実装してジェネレータを使用することで、アンパック代入や for ループによる直接のイテレーションにも対応できます。
標準ライブラリにおける実例
演算子のオーバーロードは、Pythonの標準ライブラリでも非常に実用的な目的で採用されています。
代表的な例が、パス操作を行う pathlib.Path クラスです。
pathlib.Path では、/ 演算子がパスの結合としてオーバーロードされています。
これを使用するプログラムは以下のようになります。
path_example.py
from pathlib import Path
base_dir = Path("/home/user")
config_file = base_dir / "project" / "settings.ini"
print(config_file)
プログラムの実行結果は以下のようになります。
実行結果
/home/user/project/settings.ini
これは、pathlib.Path クラスに割り算を行う __truediv__ メソッドが定義されているために実現しています。
パスという概念における「階層の下に潜る」という操作を、直感的な / 演算子で表現した素晴らしい実例です。
演習問題
演習1
分数を表現する Fraction クラスを作成しなさい。
コンストラクタ __init__(self, numerator, denominator) で分子と分母を受け取り、分母が0の場合は ValueError を発生させること。
また、文字列表現をカスタマイズするための特殊メソッド __str__ と __repr__ を実装し、それぞれ 分子/分母 の形式の文字列と Fraction(分子, 分母) の形式の文字列を返すようにしなさい。
演習2
Fraction クラスのコンストラクタにおいて、標準ライブラリの math.gcd 関数を用いて分子と分母を約分し、さらに分母が負の場合は符号を分子に寄せて分母を常に正の数とする処理を追加しなさい。
その上で、加算演算子 + に対応する __add__ と、乗算演算子 * に対応する __mul__ を実装し、計算結果として約分された新しい Fraction インスタンスを返すようにしなさい。
演習3
Fraction クラスに同値比較演算子 == に対応する __eq__ を実装し、2つの分数の値が等しいかを正しく判定できるようにしなさい。
比較対象が Fraction クラスのインスタンスでない場合は False を返すように実装すること。
解答例
演習問題のプログラムの実装例は以下の通りである。
実装の方針として、まず最大公約数を求める math.gcd を用いて、初期化時に自動的に分子と分母を最小の整数比に約分する処理を実装する。
次に __str__ および __repr__ を定義して文字列表記を設定し、__add__、__mul__、__eq__ をオーバーロードして算術演算と同値比較を行えるようにする。
最後に作成した分数オブジェクト同士の加算や乗算、等値比較を実行し、計算結果が約分された状態で返されることを確認する。
fraction_overload.py
import math
class Fraction:
def __init__(self, numerator, denominator):
if denominator == 0:
raise ValueError("分母に0を指定することはできません")
# 約分と符号の整理
g = math.gcd(numerator, denominator)
self.numerator = numerator // g
self.denominator = denominator // g
if self.denominator < 0:
self.numerator = -self.numerator
self.denominator = -self.denominator
def __add__(self, other):
new_num = self.numerator * other.denominator + other.numerator * self.denominator
new_den = self.denominator * other.denominator
return Fraction(new_num, new_den)
def __mul__(self, other):
new_num = self.numerator * other.numerator
new_den = self.denominator * other.denominator
return Fraction(new_num, new_den)
def __eq__(self, other):
if not isinstance(other, Fraction):
return False
return self.numerator == other.numerator and self.denominator == other.denominator
def __str__(self):
if self.denominator == 1:
return str(self.numerator)
return f"{self.numerator}/{self.denominator}"
def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"
# 動作検証
f1 = Fraction(1, 2)
f2 = Fraction(1, 3)
# 加算と乗算
print(f"f1 + f2 = {f1 + f2}")
print(f"f1 * f2 = {f1 * f2}")
# 約分の確認
f3 = Fraction(2, 4)
print(f"2/4 => {f3}")
print(f"2/4 => {repr(f3)}")
# 同値比較の確認
f4 = Fraction(2, 6)
print(f"1/3 == 2/6: {f2 == f4}")
print(f"1/3 == 1/2: {f2 == f1}")
print(f"1/3 == 'string': {f2 == 'string'}")
プログラムの実行結果は以下のようになります。
実行結果
f1 + f2 = 5/6
f1 * f2 = 1/6
2/4 => 1/2
2/4 => Fraction(1, 2)
1/3 == 2/6: True
1/3 == 1/2: False
1/3 == 'string': False