Python(オブジェクト指向で逆ポーランド記法電卓)

前回の投稿では演算子の辞書やクラス変数がグローバルに置かれていたため、カプセル化が不十分でした。
今回はCalculatorクラス内にスタックや辞書等を入れ、外から不要な変数の書き換えができないような設計にしています。

import math

class Stack:
    def __init__(self):
        self._stack = []

    def push(self,x):
        self._stack.append(x)

    def pop(self):
        return self._stack.pop()

    def clear(self):
        self._stack.clear()

    def depth(self):
        return len(self._stack)

class Calculator(Stack):
    def __init__(self):
        super().__init__()
        self._floating_point = 5
        self.op_dic = {}

    @staticmethod
    def err(message=""):
        raise ValueError(message)

    def generate(self,n,f):
        """<高階関数かつクロージャ>
           要素数分スタックからpopして関数を適用しスタックにpushする
           @param n 要素数
           @param f 数式関数オブジェクト
           @return proc 適用処理関数"""
        def proc():
            args = [float(self.pop()) for _ in range(n)]
            args.reverse()
            tmp = f(*args)
            self.push(round(tmp,self._floating_point))
        return proc

    def set_operator_dict(self):
        _op_table = ( ("+",2,lambda a,b:a+b),
                      ("-",2,lambda a,b:a-b),
                      ("*",2,lambda a,b:a*b),
                      ("/",2,lambda a,b:a/b),
                      ("%",2,lambda a,b:a%b),
                      ("//",2,lambda a,b:a//b),
                      ("**",2,lambda a,b:a**b),
                      ("root",1,math.sqrt),
                      ("log",1,math.log),
                      ("sin",1,lambda a:math.sin(math.radians(a))),
                      ("cos",1,lambda a:math.con(math.radians(a))),
                      ("tan",1,lambda a:math.tan(math.radians(a))),
                      ("if",3,lambda a,b,c: b if a else c) )
        
        self.op_dic = dict([(name,self.generate(n,f)) for name,n,f in _op_table])
        del _op_table

    def calc(self,xs):
        print(xs,self._stack)
        if not xs:
            if self.depth() == 1:
                return self.pop()
            else:
                print("スタックに値が残っています!")
                Calculator.err()
                
        self.operate(xs[0])
        return self.calc(xs[1:])

    def operate(self,x):
        if x in self.op_dic:
            self.op_dic[x]()
        elif Calculator.validate_num(x):
            self.push(x)
        else:
            Calculator.err(f"unknown operator: {x}")

    @staticmethod
    def input_num_expression():
        """入力を求める関数"""
        return input("後置記法で数式を入力してください:")

    def get_num_expression(self,function):
        """<高階関数>入力を求める関数の値を判断する関数
           @param function  入力処理を行う関数
           @return tmp_list 認識可能なlist"""
        while True:
            s = function()
            if s.lower() in {"exit","quit","bye"}:
                break
            try:
                if s == "":
                    Calculator.err()
                tmp_list = s.split(" ")
                tmp_list = list(filter(lambda a:a is not "", tmp_list))                
                self.validate_element(tmp_list)
                return tmp_list
            except:
                print(f"「{s}」は無効な入力値です\n正しい入力値をお願いします")
        return "exit"

    def validate_element(self,xs):
        """<検査関数>有効な要素かどうか検査する関数
           @param xs 入力値の各要素リスト
           @return  各要素リスト"""
        if xs == []:
            return xs
        elif xs[0] in self.op_dic:
            self.validate_element(xs[1:])
        elif Calculator.validate_num(xs[0]):
            self.validate_element(xs[1:])
        else:
            Calculator.err("無効な入力値です")

    @staticmethod
    def validate_num(s):
        """<検査関数>有効な数値かどうか検査する関数
           @param s 文字列
           @return  有効な数値の文字列"""
        if s[0] == "-":
            Calculator.validate_num(s[1:])
        elif s[0] == "+":
            Calculator.validate_num(s[1:])
        elif s.count(".") == 1:
            [a,b] = s.split(".")
            if not a.isdigit() or not b.isdigit():
                Calculator.err()
            return True
        elif not s.isdigit():
            Calculator.err()
        return True

    def test_exec(self):
        self.set_operator_dict()        
        while True:
            num_expression = self.get_num_expression(Calculator.input_num_expression)
            if num_expression == "exit":
                return False            
            try:
                print("\n計算結果>",self.calc(num_expression),"\n")
            except:
                print(f"「{num_expression}」は計算不可能な式です")
                print("正しい入力値をお願いします")
                self.clear()
            
def test():
    calc = Calculator()
    while calc.test_exec():
        pass
    print("end")

# 実行部分
test()

class Calculator(Stack):
Calculator クラスを Stack クラスを継承したクラスとして定義しました。
コンストラクタで
super().init()
を行うことで継承元の変数や関数をこのクラスで扱うことができます。
Stack.__init__(self)
のように記述することで同様に継承先のコンストラクタを呼ぶこともできます。
また継承元を複数のクラスとする多重継承を行う場合は色々と注意が必要になりますが、詳しくは以下のサイトで説明があります。

Python3の多重継承(菱形継承)について
https://qiita.com/edad811/items/824e02a474e71df88745

@staticmethod
という記述はデコレータを用いた記述となり、詳しい説明は今後とさせていただきますが、 クラス内で関数を作るとき、インスタンスに紐づく関数ではないため、 「self」を使用する必要がない関数があります。
関数でインスタンスを扱うことはないので、selfを引数にとること自体無意味です。
そこで、@staticmethodをつけてあげることで、明示的に静的関数であることを示し、関数の役割も把握しやすくなります。 詳しくは以下のサイトを確認してください。

【Python】インスタンスメソッド、staticmethod、classmethodの違いと使い方
https://djangobrothers.com/blogs/class_instance_staticmethod_classmethod_difference/

クラス内の記述では自身のクラス内で定義したオブジェクトについてはインスタンスメソッドとしてself.関数名または変数名として呼び出すことに注意しましょう。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Python

前の記事

Python(オブジェクト指向)
Python

次の記事

Python(クラス定義と継承)