カスタム数値マジックメソッド

PythonBeginner
オンラインで実践に進む

はじめに

このチュートリアルでは、数値演算に関連する Python のマジックメソッドについて説明します。マジックメソッドは、Python クラスにおいて前後にダブルアンダースコア (__) が付く特別なメソッドです。また、「ダンダー」メソッド(ダブルアンダースコア)とも呼ばれます。

これらのマジックメソッドを使うことで、クラスのインスタンスが加算や減算などの特定の演算でどのように振る舞うかを定義することができます。

以下のセクションで説明します。

  1. 単項演算子
  2. 二項演算子
  3. インプレース演算

始めましょう!

単項演算子

単項演算とは、否定や絶対値など、1 つのオペランドのみを含む演算です。

まずは簡単なオブジェクトから始めましょう。number.py で、属性 value を持つ MyNumber という名前のクラスを作成します。

class MyNumber:
    def __init__(self, value: float):
        self.value = value

__neg__

__neg__ マジックメソッドは、否定演算の振る舞いを定義します。クラスのインスタンスに - 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __neg__(self) -> 'MyNumber':
        """インスタンスの値の否定を返します。"""
        return MyNumber(-self.value)

__abs__

__abs__ マジックメソッドは、絶対値演算の振る舞いを定義します。クラスのインスタンスに abs() 関数を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __abs__(self) -> 'MyNumber':
        """インスタンスの値の絶対値を返します。"""
        return MyNumber(abs(self.value))

__round__

__round__ マジックメソッドは、丸め演算の振る舞いを定義します。クラスのインスタンスに round() 関数を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __round__(self, ndigits: int = None) -> 'MyNumber':
        """インスタンスの値を最も近い整数または指定された桁数に丸めます。"""
        return MyNumber(round(self.value, ndigits))

__floor__

__floor__ マジックメソッドは、切り捨て演算の振る舞いを定義します。クラスのインスタンスに math.floor() 関数を使用すると、このメソッドが呼び出されます。

## math モジュールは number.py の先頭でインポートする必要があります
import math

    #... (number.py の前のコード)

    def __floor__(self) -> 'MyNumber':
        """インスタンスの値以下の最大の整数を返します。"""
        return MyNumber(math.floor(self.value))

__ceil__

__ceil__ マジックメソッドは、切り上げ演算の振る舞いを定義します。クラスのインスタンスに math.ceil() 関数を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __ceil__(self) -> 'MyNumber':
        """インスタンスの値以上の最小の整数を返します。"""
        return MyNumber(math.ceil(self.value))

例:単項演算子の使用

ここで、MyNumber クラスの単項演算子を定義しましたので、unary_example.py でそれらがどのように機能するか見てみましょう:

import math
from number import MyNumber

## 新しい MyNumber オブジェクトを作成
a = MyNumber(5)
## __neg__メソッドを print 関数と一緒に使用
print(f'{a.value=}, {-a.value=}')  ## 出力:a.value=5, -a.value=-5

## もう 1 つの新しい MyNumber オブジェクトを作成
a = MyNumber(-5)
## __abs__メソッドを print 関数と一緒に使用
print(f'{a.value=}, {abs(a).value=}')  ## 出力:a.value=-5, abs(a).value=5

## 3 つ目の新しい MyNumber オブジェクトを作成
a = MyNumber(5.678)
## __round__メソッドを print 関数と一緒に使用
print(f'{a.value=}, {round(a, 2).value=}')  ## 出力:a.value=5.678, round(a, 2).value=5.68

## __floor__メソッドを print 関数と一緒に使用
print(f'{a.value=}, {math.floor(a).value=}')  ## 出力:a.value=5.678, math.floor(a).value=5

## __ceil__メソッドを print 関数と一緒に使用
print(f'{a.value=}, {math.ceil(a).value=}')  ## 出力:a.value=5.678, math.ceil(a).value=6

次に、ターミナルで以下のコマンドを入力してスクリプトを実行します。

python unary_example.py

二項演算子

二項演算とは、加算、減算、乗算、除算などの算術演算や、等価性、非等価性、未満、超過などの比較演算のように、2 つのオペランドを含む演算です。

__add__

__add__ マジックメソッドは、加算演算の振る舞いを定義します。クラスのインスタンスに + 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __add__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値と他のインスタンスの値の和を返します。"""
        return MyNumber(self.value + other.value)

__sub__

__sub__ マジックメソッドは、減算演算の振る舞いを定義します。クラスのインスタンスに - 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __sub__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値と他のインスタンスの値の差を返します。"""
        return MyNumber(self.value - other.value)

__mul__

__mul__ マジックメソッドは、乗算演算の振る舞いを定義します。クラスのインスタンスに * 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __mul__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値と他のインスタンスの値の積を返します。"""
        return MyNumber(self.value * other.value)

__truediv__

__truediv__ マジックメソッドは、通常の除算演算の振る舞いを定義します。クラスのインスタンスに / 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __truediv__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値を他のインスタンスの値で割った結果を返します。"""
        return MyNumber(self.value / other.value)

__floordiv__

__floordiv__ マジックメソッドは、切り捨て除算演算の振る舞いを定義します。クラスのインスタンスに // 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __floordiv__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値を他のインスタンスの値で割った結果以下の最大の整数を返します。"""
        return MyNumber(self.value // other.value)

__mod__

__mod__ マジックメソッドは、剰余演算の振る舞いを定義します。クラスのインスタンスに % 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __mod__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値を他のインスタンスの値で割った余りを返します。"""
        return MyNumber(self.value % other.value)

__pow__

__pow__ マジックメソッドは、累乗演算の振る舞いを定義します。クラスのインスタンスに ** 演算子または pow() 関数を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __pow__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値を他のインスタンスの値の累乗にした結果を返します。"""
        return MyNumber(self.value ** other.value)

例:二項演算子の使用

ここで、MyNumber クラスの二項演算子を定義しましたので、binary_example.py でそれらがどのように機能するか見てみましょう:

from number import MyNumber

## 2 つの新しい MyNumber オブジェクトを作成
a = MyNumber(5)
b = MyNumber(3)
print(f'{a.value=}, {b.value=}') ## 出力:a.value=5, b.value=3

## __add__メソッドを print 関数と一緒に使用
print(f'{(a+b).value=}')  ## 出力:(a+b).value=8

## __sub__メソッドを print 関数と一緒に使用
print(f'{(a-b).value=}')  ## 出力:(a-b).value=2

## __mul__メソッドを print 関数と一緒に使用
print(f'{(a*b).value=}')  ## 出力:(a*b).value=15

## __truediv__メソッドを print 関数と一緒に使用
print(f'{(a/b).value=}')  ## 出力:(a/b).value=1.6666666666666667

## __floordiv__メソッドを print 関数と一緒に使用
print(f'{(a//b).value=}')  ## 出力:(a//b).value=1

## __mod__メソッドを print 関数と一緒に使用
print(f'{(a%b).value=}')  ## 出力:(a%b).value=2

## __pow__メソッドを print 関数と一緒に使用
print(f'{(a**b).value=}')  ## 出力:(a**b).value=125

次に、ターミナルで以下のコマンドを入力してスクリプトを実行します。

python binary_example.py

インプレース演算

インプレース演算とは、新しいオブジェクトを作成することなく、オブジェクトの値をその場で変更する演算です。+=-=*=/= などの拡張代入演算子で表されます。

Python クラスでインプレース演算子が定義されていない場合、インプレース演算が試行されたときには代わりに二項演算子が使用されます。

inplace_example1.py に例があります。二項演算子をインプレース演算子に変更します。

from number import MyNumber

## 2 つの新しい MyNumber オブジェクトを作成
a = MyNumber(5)
b = MyNumber(3)
print(f'{a.value=}, {b.value=}') ## 出力:a.value=5, b.value=3

a += b
## __add__メソッドを print 関数と一緒に使用
print(f'after a+=b: {a.value=}')  ## 出力:after a+=b: (a+b).value=8

この例を実行するには、ターミナルで以下のコマンドを入力します。

python inplace_example1.py

結果から、+= 演算を試行したときに __add__ が使用されたことがわかります。

次に、MyNumber でインプレース演算を実装します。そして、対応する二項演算子との挙動には些細な違いがあります。

__iadd__

__iadd__ マジックメソッドは、インプレース加算演算の振る舞いを定義します。クラスのインスタンスに += 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __iadd__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値に他のインスタンスの値をインプレースで加算します。"""
        print(f'input: {self.value=}, {other.value=}')
        self.value += other.value
        print(f'after +=: {self.value=}')
        return self

__isub__

__isub__ マジックメソッドは、インプレース減算演算の振る舞いを定義します。クラスのインスタンスに -= 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __isub__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値から他のインスタンスの値をインプレースで減算します。"""
        print(f'input: {self.value=}, {other.value=}')
        self.value -= other.value
        print(f'after -=: {self.value=}')
        return self

__imul__

__imul__ マジックメソッドは、インプレース乗算演算の振る舞いを定義します。クラスのインスタンスに *= 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __imul__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値に他のインスタンスの値をインプレースで乗算します。"""
        print(f'input: {self.value=}, {other.value=}')
        self.value *= other.value
        print(f'after *=: {self.value=}')
        return self

__itruediv__

__itruediv__ マジックメソッドは、インプレース通常の除算演算の振る舞いを定義します。クラスのインスタンスに /= 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __itruediv__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値を他のインスタンスの値でインプレースで割ります。"""
        print(f'input: {self.value=}, {other.value=}')
        self.value /= other.value
        print(f'after /=: {self.value=}')
        return self

__ifloordiv__

__ifloordiv__ マジックメソッドは、インプレース切り捨て除算演算の振る舞いを定義します。クラスのインスタンスに //= 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __ifloordiv__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値を他のインスタンスの値でインプレースで切り捨て除算します。"""
        print(f'input: {self.value=}, {other.value=}')
        self.value //= other.value
        print(f'after //=: {self.value=}')
        return self

__imod__

__imod__ マジックメソッドは、インプレース剰余演算の振る舞いを定義します。クラスのインスタンスに %= 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __imod__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値を他のインスタンスの値でインプレースで剰余演算します。"""
        print(f'input: {self.value=}, {other.value=}')
        self.value %= other.value
        print(f'after %=: {self.value=}')
        return self

__ipow__

__ipow__ マジックメソッドは、インプレース累乗演算の振る舞いを定義します。クラスのインスタンスに **= 演算子を使用すると、このメソッドが呼び出されます。

    #... (number.py の前のコード)

    def __ipow__(self, other: 'MyNumber') -> 'MyNumber':
        """インスタンスの値を他のインスタンスの値の累乗にインプレースでする。"""
        print(f'input: {self.value=}, {other.value=}')
        self.value **= other.value
        print(f'after **=: {self.value=}')
        return self

例:インプレース演算の使用

ここで、MyNumber クラスのインプレース演算子を定義しましたので、inplace_example2.py でそれらがどのように機能するか見てみましょう:

from number import MyNumber

## 新しい MyNumber オブジェクトを作成
a = MyNumber(13)

## __iadd__メソッドを使用
a += MyNumber(5)
## 出力:
## input: self.value=13, other.value=5
## after +=: self.value=18

## __isub__メソッドを使用
a -= MyNumber(5)
## 出力:
## input: self.value=18, other.value=5
## after -=: self.value=13

## __imul__メソッドを使用
a *= MyNumber(5)
## 出力:
## input: self.value=13, other.value=5
## after *=: self.value=65

## __itruediv__メソッドを使用
a /= MyNumber(5)
## 出力:
## input: self.value=65, other.value=5
## after /=: self.value=13.0

## __ifloordiv__メソッドを使用
a //= MyNumber(2)
## 出力:
## input: self.value=13.0, other.value=2
## after //=: self.value=6.0

## __imod__メソッドを使用
a %= MyNumber(4)
## 出力:
## input: self.value=6.0, other.value=4
## after %=: self.value=2.0

## __ipow__メソッドを使用
a **= MyNumber(3)
## 出力:
## input: self.value=2.0, other.value=3
## after **=: self.value=8.0

次に、ターミナルで以下のコマンドを入力してスクリプトを実行します。

python inplace_example2.py

まとめ

このチュートリアルでは、数値演算に関連する Python のマジックメソッドを探りました。これにより、さまざまな種類の数値演算とのやり取りの際に、クラスの独自の振る舞いを定義することができます。単項演算子、二項演算子、およびインプレース演算について説明し、それぞれのマジックメソッドの実装方法を学びました。

カスタムクラスにこれらのマジックメソッドを実装することで、標準的な Python 演算とシームレスに動作する直感的で使いやすいオブジェクトを作成できます。これは、コードの読みやすさを向上させるだけでなく、保守性とユーザーフレンドリネスも向上させます。

Python のスキルをさらに磨き続ける際には、他のマジックメソッドを試してみることを検討してください。それにより、クラスの振る舞いをさらにカスタマイズし、さらに強力な抽象化を作成することができます。