werry-chanの日記.料理とエンジニアリング

料理!コーディング!研究!日常!飯!うんち!睡眠!人間の全て!

operator overloadでオリジナルの演算子を実装 for Python

非常に稀かもしれませんが, 一般的に使われる四則演算とは異なったルールで計算をしたいことがあるかと思います。

筆者は, 少し変わった暗号計算解くために
例えば, N桁M進数 を符号なしの循環数として大量に演算する必要があり, N digit Unsigned M decimalクラスを作ったことがありました。
その件については, こちらの記事で簡易実装例を載せておきました。
Pythonクラス継承とoperator overloadingで四則演算可能なMACアドレスクラスを自作する - werry-chanの日記.料理とエンジニアリング


そのような時には, operator overloadingを実装しましょう。

今回は分数を計算するためのfractionクラスを例に説明していきます。

fractionクラスは計算上の丸め,桁落ち誤差などを避けることが可能なクラスで, 標準ライブラリとしても実装されているものになります。
今回はこれを自作していこうと思います。
標準ライブラリの中身を実際には見ていないので, 異なる部分多数あるかと思いますが, 今回はoperator overloadingの実装がメインテーマなのでお目こぼしください

まずはfractionクラスを作成した時に呼ばれる初期化関数を作成します。

class fraction:
    def __init__(self, num: int, den: int):
        self.num = num
        self.den = den

Pythonは勝手に型推定されて変換されて面倒なので, 関数設定でint型しか入れれないように拘束します。
さらに, その後も勝手にfloat型に変換されないように気を付けましょう。

次に, 分数を扱うので, 入力された数値がきちんと約分された状態にしましょう。
約分はこの後もたくさん使うので, 早めに書いておくと素敵です。

import math

class fraction:
    def __init__(self, num: int, den: int):
        self.num = num
        self.den = den
        self.transmission() #約分する
    def transmission(self):
        gcd_val  = math.gcd(self.num, self.den)
        self.num = int(self.num / gcd_val) #型変換避けint()
        self.den = int(self.den / gcd_val) #型変換避けint()

では次に, きちんと約分が出来ているか確認ついでに, 表示関数を作成しましょう。

import math

class fraction:
    def __init__(self, num: int, den: int):
        self.num = num
        self.den = den
        self.transmission()
    def transmission(self):
        gcd_val  = math.gcd(self.num, self.den)
        self.num = int(self.num / gcd_val)
        self.den = int(self.den / gcd_val)
    def show(self):
        self.transmission()
        print(self.num, '/', self.den, end='')

if __name__ == "__main__":
    f1 = fraction(-2,5)
    f1.show() # output -> -2 / 5
    print(f1) # output -> <__main__.fraction object at 0x000001E7D6B54508>

これで, 今後の作業中においても適宜数値が確認できるようになりました。
ちなみにオリジナルのクラスなので, 普通にprint()しても数値は見えません。

では次は, 加算演算子を作成していきましょう。

自作したfractionクラスを
f_0 = f_0 + f_1, あるいは f_0 += f_1
のように扱いたい場合には, 演算子を自分で定義する必要があります。

def __add__(self, val): # valは上記のf_1にあたる
と自作の+演算子による結果を定義してやります。

import math
import copy

class fraction:
    def __init__(self, num: int, den: int):
        self.num = num
        self.den = den
        self.transmission()
    def transmission(self):
        gcd_val = math.gcd(self.num, self.den)
        self.num = int(self.num / gcd_val)
        self.den = int(self.den / gcd_val)
    def show(self):
        self.transmission()
        print(self.num, '/', self.den, end='')
    def __add__(self, val):
        if type(val) != self.__class__: # 演算相手が別の型の時は
            val_ = fraction(val, 1) # 型を合わせてやる, 元のvalを壊さない形で
        else:
            val_ = val #元のvalを壊さないように計算用の変数を用意してやる
        ret_val      = copy.deepcopy(self) #同上
        ret_val.num  = (ret_val.num * val_.den) + (val_.num * ret_val.den) # 通分して分子を計算
        ret_val.den *= val_.den # 分母も通分状態にする
        ret_val.transmission() # 最後に約分して返す
        return ret_val

分数の計算だったので, 若干癖のある計算かもしれないですが, 上記のような形になります。
まぁ小学生の範囲でも実装すると意外と面倒です。
毎回返す直前呼んでいる約分(transmission())関数でint型の分子, 分母になるようにしているので, 推定型変換への注意が少なく済んで良いですね。
筆者は, Pythonのバグの5割は型推定のせいだと思っています

それでは同様にして他の演算子なども作成してしまいましょう。
#edited 20230703, before __dev__(), after __truediv__()

# this code is werry-chann lib
# this lib provide fraction class
import copy
import math


class fraction:
    def __init__(self, num: int, den: int):
        self.num = num
        self.den = den
        self.transmission()
    def transmission(self):
        gcd_val = math.gcd(self.num, self.den)
        self.num = int(self.num / gcd_val)
        self.den = int(self.den / gcd_val)
    def show(self):
        self.transmission()
        print(self.num, '/', self.den, end='')
    def to_int(self):
        self.transmission()
        return int(self.num / self.den)
    def to_float(self):
        self.transmission()
        return float(self.num / self.den)
    def __add__(self, val):
        if type(val) != self.__class__:
            val_ = fraction(val, 1)
        else:
            val_ = val
        ret_val      = copy.deepcopy(self)
        ret_val.num  = (ret_val.num * val_.den) + (val_.num * ret_val.den)
        ret_val.den *= val_.den
        ret_val.transmission()
        return ret_val
    def __sub__(self, val):
        if type(val) != self.__class__:
            val_ = fraction(val, 1)
        else:
            val_ = val
        ret_val     = copy.deepcopy(self)
        ret_val.num  = (ret_val.num * val_.den) - (val_.num * ret_val.den)
        ret_val.den *= val_.den
        ret_val.transmission()
        return ret_val
    def __mul__(self, val):
        if type(val) != self.__class__:
            val_ = fraction(val, 1)
        else:
            val_ = val
        ret_val      = copy.deepcopy(self)
        ret_val.num  = ret_val.num * val_.num
        ret_val.den *= val_.den
        ret_val.transmission()
        return ret_val
    def __truediv__(self, val): #edited 20230703, before __dev__(), after __truediv__()
        if type(val) != self.__class__:
            val_ = fraction(val, 1)
        else:
            val_ = val
        ret_val     = copy.deepcopy(self)
        ret_val.num = ret_val.num * val_.den
        ret_val.den =  ret_val.den * val_.num
        ret_val.transmission()
        return ret_val


if __name__ == "__main__":
    f1 = fraction(-2,5)
    print(f1) # <__main__.fraction object at 0x0000020364C2FFC8>
    f2 = fraction(1,20)
    f3 = f1 + f2
    f3.show() # -7 / 20
    f4 = f1 - 3
    f4.show() # -17 / 5

以上で四則演算の実装が完了しました。

ここまで実装が出来た人であれば残りの比較演算子(==, <, >)などについても調べながらできると思います。

以下の参考文献1つ目のリンクには他の演算子についても記述してありますので, 参考になりましたら幸いです。

参考文献
Operator Overloading in Python - GeeksforGeeks
https://docs.python.org/3/reference/datamodel.htm
operator — Standard operators as functions — Python 3.11.4 documentation
2, 3番目のリンクが公式なのですが, なかなか初心者には厳しそうでした。