>

자바에서 몇 년간의 경험이 있다고 말하면서 시작하겠습니다. 그러나 이제는 파이썬을 배워야합니다. 그래서 커뮤니티 도전이기도하기 때문에 계산기를 만들기로 결정했습니다.

코드가 전문적인 것처럼 보이도록 초심자 실수를 찾는 코드를 검토하십시오. 이것은 파이썬으로 만든 첫 번째 실제 프로그램이며 이틀 전에 파이썬에 대해서는 전혀 몰랐습니다.

현재 계산기에는 다음과 같은 기능이 있습니다 :

  • 표현식을 평가하므로 상호 작용할 수 없습니다.
  • 지원되는 연산자는 + 입니다. - *  그리고 / .
  • 기능은 지원되지 않습니다.
  • 분류 장 알고리즘 을 사용합니다.
  • 역 폴란드 표기법 을 사용합니다.

계산기 /tokens.py

from decimal import Decimal
__author__ = 'Frank van Heeswijk'

class Token:
    pass

class ValueToken(Token):
    def __init__(self, value: Decimal):
        self.value = value
    def __repr__(self):
        return "VT(" + str(self.value) + ")"
    def __hash__(self):
        return hash(self.value)
    def __eq__(self, other):
        if type(self) != type(other):
            return False
        return self.value == other.value
    def __ne__(self, other):
        return not self == other

class OperatorToken(Token):
    def __init__(self, operator: str):
        self.operator = operator
    def __repr__(self):
        return "OT(" + self.operator + ")"
    def __hash__(self):
        return hash(str)
    def __eq__(self, other):
        if type(self) != type(other):
            return False
        return self.operator == other.operator
    def __ne__(self, other):
        return not self == other

class LeftParenthesesToken(Token):
    def __repr__(self):
        return "LPT"
    def __hash__(self):
        return 0
    def __eq__(self, other):
        if type(self) != type(other):
            return False
        return True
    def __ne__(self, other):
        return not self == other

class RightParenthesesToken(Token):
    def __repr__(self):
        return "RPT"
    def __hash__(self):
        return 0
    def __eq__(self, other):
        if type(self) != type(other):
            return False
        return True
    def __ne__(self, other):
        return not self == other

계산기 /calculator.py

from decimal import Decimal
from enum import Enum
import inspect
import re
from calculator.tokens import OperatorToken, ValueToken, LeftParenthesesToken, RightParenthesesToken

__author__ = 'Frank van Heeswijk'

class Associativity(Enum):
    left = 1
    right = 2

class Calculator:
    __operators = {
        # reference: http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
        # operator: (precedence, associativity, function)
        "u+": (-3, Associativity.right, lambda op: op),
        "u-": (-3, Associativity.right, lambda op: -op),
        "*": (-5, Associativity.left, lambda op1, op2: op1 * op2),
        "/": (-5, Associativity.left, lambda op1, op2: op1 / op2),
        "+": (-6, Associativity.left, lambda op1, op2: op1 + op2),
        "-": (-6, Associativity.left, lambda op1, op2: op1 - op2)
    }
    def __init__(self):
        self.operators = Calculator.__operators
    def evaluate(self, expression: str) -> Decimal:
        """
        Evaluates an expression and returns its result.
        :param expression:  The input expression
        :return:    The output of evaluating the expression
        """
        tokens = self.to_rpn(self.tokenize(expression))
        stack = []
        for token in tokens:
            if isinstance(token, ValueToken):
                stack.append(token.value)
            elif isinstance(token, OperatorToken):
                function = self.operators[token.operator][2]
                argspec = inspect.getargspec(function)
                argument_count = len(argspec.args)
                if len(stack) < argument_count:
                    raise RuntimeError("not enough tokens for: " + str(token) + ", expected: " + str(argument_count) + ", actual: " + str(len(tokens)))
                values = [stack.pop() for x in range(argument_count)]
                values.reverse()
                result = function(*values)
                stack.append(result)
            else:
                raise RuntimeError("unexpected token: " + token)
        return stack.pop()
    def tokenize(self, expression: str) -> list:
        """
        Tokenizes an expression and produces an output list of tokens.
        :rtype: list of [Token]
        :param expression:  The input expression
        """
        tokens = []
        stripped_expression = expression.replace(' ', '')
        value_regex = re.compile(r"\d+(\.\d+)?")
        operator_regex = re.compile(r"[^\d\.\(\)]")
        left_parentheses_regex = re.compile(r"\(")
        right_parentheses_regex = re.compile(r"\)")
        regexps = [value_regex, operator_regex, left_parentheses_regex, right_parentheses_regex]
        raw_patterns = "|".join(map(lambda regex: regex.pattern, regexps))
        capture_regex = re.compile("(" + raw_patterns + ")")
        for raw_token, something_else in capture_regex.findall(stripped_expression):
            if value_regex.match(raw_token):
                tokens.append(ValueToken(Decimal(raw_token)))
            elif operator_regex.match(raw_token):
                if raw_token not in self.__operators:
                    raise RuntimeError("unsupported operator: " + raw_token)
                tokens.append(OperatorToken(raw_token))
            elif left_parentheses_regex.match(raw_token):
                tokens.append(LeftParenthesesToken())
            elif right_parentheses_regex.match(raw_token):
                tokens.append(RightParenthesesToken())
            else:
                raise RuntimeError("token " + raw_token + " does not match any regex")
        # resolve unary plus and minus operators
        for index, token in enumerate(tokens):
            if isinstance(token, OperatorToken) and token.operator == '-':
                if index == 0\
                or isinstance(tokens[index - 1], LeftParenthesesToken)\
                or isinstance(tokens[index - 1], OperatorToken):
                    tokens[index] = OperatorToken('u-')
            elif isinstance(token, OperatorToken) and token.operator == '+':
                if index == 0\
                or isinstance(tokens[index - 1], LeftParenthesesToken)\
                or isinstance(tokens[index - 1], OperatorToken):
                    tokens[index] = OperatorToken('u+')
        return tokens
    def to_rpn(self, tokens: list) -> list:
        """
        Converts a list of tokens to an output list in Reverse Polish Notation form.
        :rtype: list of [Token]
        :type tokens: list of [Token]
        :param tokens:  The input tokens
        :raise RuntimeError:    If the parentheses are mismatched
        """
        output_queue = []
        stack = []
        for token in tokens:
            if isinstance(token, ValueToken):
                output_queue.append(token)
            elif isinstance(token, LeftParenthesesToken):
                stack.append(token)
            elif isinstance(token, RightParenthesesToken):
                while len(stack) > 0:
                    pop_token = stack.pop()
                    if isinstance(pop_token, LeftParenthesesToken):
                        break
                    output_queue.append(pop_token)
                    # todo implement function support
                else:
                    raise RuntimeError("mismatched parentheses")
            elif isinstance(token, OperatorToken):
                while len(stack) > 0:
                    pop_token = stack.pop()
                    if isinstance(pop_token, OperatorToken) and self.__has_lower_precedence(token, pop_token):
                        output_queue.append(pop_token)
                    else:
                        stack.append(pop_token)
                        break
                stack.append(token)
            else:
                raise RuntimeError("unexpected token: " + token)
        while len(stack) > 0:
            pop_token = stack.pop()
            if isinstance(pop_token, LeftParenthesesToken):
                raise RuntimeError("mismatched parentheses")
            output_queue.append(pop_token)
        return output_queue
    def __has_lower_precedence(self, operatortoken1: OperatorToken, operatortoken2: OperatorToken) -> bool:
        operator1 = operatortoken1.operator
        operator2 = operatortoken2.operator
        if operator1 not in self.operators:
            raise RuntimeError("Unsupported operator token: " + operator1)
        if operator2 not in self.operators:
            raise RuntimeError("Unsupported operator token: " + operator2)
        operator1_tuple = self.operators[operator1]
        operator2_tuple = self.operators[operator2]
        return (operator1_tuple[1] == Associativity.left and operator1_tuple[0] <= operator2_tuple[0]) \
               or (operator1_tuple[1] == Associativity.right and operator1_tuple[0] < operator2_tuple[0])

calculator_application.py

import sys
from calculator.calculator import Calculator
__author__ = 'Frank van Heeswijk'
if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: calculator_application \"<expression>\"")
        sys.exit(1)
    calculator = Calculator()
    expression = " ".join(sys.argv[1:])
    result = calculator.evaluate(expression)
    print(result)

사용에 대한 느낌을주기위한 작은 단위 테스트 샘플 :

def test_evaluate(self):
    calculator = Calculator()
    self.assertEqual(Decimal(4), calculator.evaluate("4"))
    self.assertEqual(Decimal(21), calculator.evaluate("7 * 3"))
    self.assertEqual(Decimal(11), calculator.evaluate("2 * 4 + 3"))
    self.assertEqual(Decimal(45), calculator.evaluate("(3 * (2 + 5)) + 6 * (4)"))
    self.assertEqual(Decimal("25.92"), calculator.evaluate("2.7 * (3.2 + 6.4)"))
    self.assertEqual(Decimal(1), calculator.evaluate("-2 * -4 + -7"))
def test_evaluate_operators(self):
    calculator = Calculator()
    self.assertEqual(Decimal(3), calculator.evaluate("+3"))
    self.assertEqual(Decimal(-3), calculator.evaluate("-3"))
    self.assertEqual(Decimal(6), calculator.evaluate("2 * 3"))
    self.assertEqual(Decimal(2), calculator.evaluate("6 / 3"))
    self.assertEqual(Decimal(5), calculator.evaluate("2 + 3"))
    self.assertEqual(Decimal(3), calculator.evaluate("7 - 4"))
def test_evaluate_operator_precedences(self):
    calculator = Calculator()
    self.assertEqual(Decimal(-14), calculator.evaluate("-3 * 5 + +1"))
    self.assertEqual(Decimal("6.5"), calculator.evaluate("8 / -16 - -7"))
    self.assertEqual(Decimal(30), calculator.evaluate("5 * 3 * 8 / 4 / 2 * 6 / 3"))
    self.assertEqual(Decimal(-3), calculator.evaluate("2 + 3 + 4 - 5 - 8 + 6 + 4 - 9"))

다른 단위 테스트에 관심이 있다면 my GitHub에서 전체 프로젝트를 볼 수 있습니다. 저장소 .

  • 답변 # 1

    이것은 꽤 좋아 보인다! 약간의 작은 이쑤시개와 실용적인 팁이 있습니다.

    부울 값을 직접 반환

    나는 이것에 약간 놀랐다 :

    와이즈 비즈 와이즈 비즈

    간단히 글을 쓰지 않는 특별한 이유가 있는지 궁금합니다 :

    모든 if type(self) != type(other): return False return True 도 마찬가지입니다.  단일 return type(self) == type(other) 로 단순화 할 수있는 방법  진술.

    정규 컴파일하기

    나는 이것에 약간 놀랐다 :

    와이즈 비즈 와이즈 비즈

    이 메소드가 호출 될 때마다 정규 표현식을 컴파일합니까? 일반적으로 사전 컴파일 된 정규 표현식을 파일 맨 위에 전역 상수로 넣습니다. 하지만 문서를 보면 정규식을 몇 개 컴파일해야하는지 궁금합니다. 캐싱 메커니즘이 내장되어있는 것 같습니다.

    복잡한 조건 지정

    이거 예쁘지 않아 :

    와이즈 비즈 와이즈 비즈

    __eq__ 사용  긴 줄을 끊는 것은 약간 추한 경향이 있습니다. (이것은 주관적 일 수 있습니다.) 또 다른 대안은 복잡한 표현을 parens 안에 넣는 것입니다.

    return
    
    

    그러나 이것도 좋지 않습니다 :

    PEP8은 다음과 같이 불평합니다 :다음 논리적 줄과 같은 들여 쓰기가있는 E129 시각적 들여 쓰기 줄

    의 들여 쓰기를 늘리면
    , 같은 불만 (솔직히 이해하지 못합니다)

    PEP8을 통과하지만 실제로는 합리적으로 보이지 않는 것을 얻기 위해 들여 쓰기를 가지고 놀 수 있습니다

    결론은 복잡한 부울 식에 대한 최상의 솔루션은 도우미 함수로 추출하는 것입니다. 기능은 어디에서나 정의 할 수 있습니다. 사용하기 직전에도 이름이 있다는 이점도 있습니다.

    아무것도 이것의 가치가 없으며 당신의 원본이 최고입니다. 생각할 음식이 있습니다.

    빈 것을 점검하는 파이썬적인 방법

    대신 :

    와이즈 비즈 와이즈 비즈

    파이썬의 방법은 간단하게 :

    def tokenize(self, expression: str) -> list:
        # ...
        value_regex = re.compile(r"\d+(\.\d+)?")
        operator_regex = re.compile(r"[^\d\.\(\)]")
        left_parentheses_regex = re.compile(r"\(")
        right_parentheses_regex = re.compile(r"\)")
    
    

  • 답변 # 2

    버그

     평가
     
    if isinstance(token, OperatorToken) and token.operator == '-':
        if index == 0\
        or isinstance(tokens[index - 1], LeftParenthesesToken)\
        or isinstance(tokens[index - 1], OperatorToken):
            tokens[index] = OperatorToken('u-')
    의 결과
    \
    의 무분별한 사용으로 인해
     와이즈 비즈
    .

    if (index == 0 or isinstance(tokens[index - 1], LeftParenthesesToken) or isinstance(tokens[index - 1], OperatorToken)): tokens[index] = OperatorToken('u-') 평가   tokens[index] = ... 의 결과 . 접두사 표현식은 RPN 표현식

    로 변환됩니다.
    . 스택 상단의 값 
    while len(stack) > 0:
        pop_token = stack.pop()
    
    가비지가 스택에 남아 있는지 확인하지 않고 결과로 반환됩니다. 와이즈 비즈
     오류이거나 암시 적 곱셈으로 해석되어야합니다.

    토큰 화

    while stack: pop_token = stack.pop() 에 코드가 너무 많습니다 . 특히

    1 2 3  오리 타이핑으로 정교한 클래스 계층 구조가 필요하지 않기 때문에 기본 클래스는 Python에서 드문 경우입니다.

    하위 클래스 사이에는 공통점이 많으므로 실제로 유용한 코드를 포함하는 공통 기본 클래스가 있어야합니다

    약식 표현 123 stripped_expression = expression.replace(' ', '') 등은 사치입니다. 디버깅에 도움이됩니다. 어쨌든 Calculator.tokenize()  객체를 재생성하는 데 사용할 수있는 유효한 Python 표현식처럼 보이는 문자열을 반환해야합니다.

    12(3)
    
    

    <시간>

    와이즈 비즈  기능은 또한 개선을 사용할 수 있습니다. 특히

    3  위에서 언급 한대로 제거해야합니다.

    와이즈 비즈  값이나 괄호로 허용되지 않은 공백이 아닌 문자 만 허용합니다. 이를 표현하는 덜 중복적인 방법이 있습니다. 정규식 대체의 마지막 부분으로 넣으면됩니다.

    와이즈 비즈  필요합니다. 와이즈 비즈  생성자는 기호가 지원되는지 여부를 확인해야하며, 그렇지 않은 경우 동일한 [VT(12), VT(3)] 를 얻게됩니다.  결국 3 에 전화하면  어쨌든. (나는 12(3) 를 논의 할 것이다  "정규식과 일치하지 않습니다"오류에 대해서는 tokens.py 이후 가능하다는 확신이 없습니다.  다른 정규 표현식에 의해 거부 된 모든 항목과 일치합니다.

    와이즈 비즈에게 전화 그러면 구성 정규 표현식과 다시 일치하는 것은 낭비입니다. 대신, 캡처 그룹 (특히 각 토큰 유형에 따라 이름이 지정된 그룹 캡처)을 사용해야합니다. 처리 한 후에는 더 이상 각 정규 표현식을 개별적으로 컴파일 할 필요가 없습니다.

    단항 ±을 처리하는 코드에는 많은 중복성이 있습니다. 이전 토큰을 추적하여 완전히 다르게 할 것입니다.

    목록을 반환하는 함수보다 생성기를 선호하십시오. Python 2 → 3 전환의 주요 테마 중 하나입니다.

    Token   LPT 를 수락하지 않습니다  명시적인 VT(n) 가없는 한. 좀 더 관대 한 정규식으로 해결할 수 있습니다.

    repr()
    
    

    평가 중 from decimal import Decimal class Token: def __init__(self, text: str): self._text = text def __repr__(self): return "{0}('{1}')".format(type(self).__name__, self._text) def __hash__(self): return hash(self._text) def __eq__(self, other): return type(self) == type(other) and self._text == other._text def __ne__(self, other): return not self == other class ValueToken(Token): @property def value(self): return Decimal(self._text) class OperatorToken(Token): @property def operator(self): return self._text class LeftParenthesesToken(Token): pass class RightParenthesesToken(Token): pass 와 마찬가지로 Calculator.tokenize()  발전기 여야합니다. 즉, 당신은 expression.replace(' ', '') 를 제거 할 수 있습니다  모든 operator_regex 를 변경하여   raise RuntimeError() 로 .

    당신의 OperatorToken() 같은 느낌  수업이 부족합니다. 더 복잡한 RuntimeError 를 부담을 풀 수 있습니다  코드를 __has_lower_precedence() 로 이동하여 클래스 .

    와이즈 비즈  사전은 OperatorToken 로 이동해야합니다 . 그것은 또한 operator_regex 를 허용합니다  유효성 검사를 수행하는 생성자 capture_regex.findall() 내에서 유효성을 검사하는 것보다 낫습니다. .

    ValueToken  이동해야합니다. ( .5 를 재정의하기로 선택했습니다.  그런 0  연산자를 우선 순위별로 비교합니다.)

    def tokenize(self, expression: str): """ Generates tokens from an expression. :rtype: generator of Token :param expression: The input expression """ capture_regex = re.compile('''\s*(?: (?P<ValueToken>\d*\.?\d+) | (?P<LeftParenthesesToken>\() | (?P<RightParenthesesToken>\)) | (?P<OperatorToken>[^\s]) )''', re.VERBOSE) token = None for match in capture_regex.finditer(expression): kind, value = eval(match.lastgroup), match.group(match.lastgroup) if kind == OperatorToken and value in ('+', '-') and \ type(token) not in (RightParenthesesToken, ValueToken): # Unary + or unary - token = OperatorToken('u' + value) else: token = kind(value) yield token tokenize()to_rpn()  우선 순위, 연관성 및 기능을 각각 얻는 비밀스러운 방법입니다. output_queue 사용  대신에. (다중 상속 및 재정의 된 output_queue.append(token)  아래에서 사용한 방법은 분명히 정통입니다.)

    yield token
    
    

    OperatorToken  하나의 단순화와 약간의 변경을 사용할 수 있습니다.

    반사를 사용하여 팝할 피연산자 수를 결정하는 것은 복잡해 보입니다. RPN 계산기는 운영자가 스택을 직접 조작 할 때 훨씬 간단합니다 (예 1, 2).

    위에서 언급했듯이 완료되면 스택에 남아있는 쓰레기가 없는지 확인해야합니다.

    오류 메시지 Calculator  프로그래머 전문 용어를 사용하고 있습니다. 최종 사용자는 토큰이 아닌 피연산자를 생각합니다.

    OperatorToken
    
    

    <시간> 위의 모든 변경 사항으로 Calculator.operators 의 코드 줄이 줄어 듭니다.  Wyzwyz 동안 약 40 %  거의 동일하게 유지됩니다.

    OperatorToken

  • 이전 raspbian - 키비와 라즈베리 파이
  • 다음 java - 이진 트리에서 노드 교체