파이썬으로 간단한 사칙연산 계산기를 만들어보자. 실질적으로는 간단한 계산을 명령줄에서 수행할 수 있는 계산기가 필요해서 (계산기 앱을 실행하는게 귀찮…) 만들어보기로 했다.
실질적으로 간단한 덧셈, 나눗셈이 아니라, 다항식을 계산하고, 괄호가 들어있는 수식을 계산하는 기능이 필요했다. 따라서입력 자체는 명령줄 인자로 받으며, 가능한 한 ‘관대하게’ 처리하고자 했다.
이를 위해서는 입력받은 식을 공백이나 연산기호를 구분점으로 파싱하고, 후위식으로 변환한 후 이 후위식을 하나의 항으로 계산한 다음, 그 결과를 출력해주면 된다. 이들 각각의 과정을 함수로 만들고 최종적으로 하나의 코드로 조립해나간다.
입력된 식을 후위식으로 변환
중위식으로 표현된 식을 후위식으로 변환하는 과정은 이전 글에서도 명시했지만 다음과 같은 알고리듬을 따른다.
- 출력큐와 스택을 준비한다.
- 토큰이 숫자값이면 출력큐에 넣는다.
- 토큰이 왼쪽 괄호이면 스택에 넣는다.
- 토큰이 오른쪽 괄호이면 스택에서 왼쪽 괄호가 나올 때까지 스택에서 연산자를 꺼내어 출력큐에 넣어준다. 마지막으로 왼쪽 괄호를 꺼내어 버린다.
- 토큰이 그외의 연산자이면 스택에서 자신보다 우선순위가 같거나 큰 연산자를 꺼내어 출력큐에 넣은다음, 토큰을 스택에 넣는다.
- 2~5의 과정을 되풀이 하여 더 이상 토큰이 없으면 다시 스택으로부터 연산자를 꺼내어 출력큐에 넣는다.
이를 코드로 옮기면 아래와 같다. 연산자들을 별도로 정의하고, 각 연산자의 우선순위를 부여한다. 만약 거듭제곱 연산이나, 제곱근 연산이 필요한 경우에는 그에 맞는 연산자를 정의해서 추가해줄 수 있겠으나 여기서는 괄호와 사칙 연산만을 고려했다.
후위식 변환 함수 코드
입력된 수식은 토큰화 함수에 의해서 파싱되어 연산자/피연산자 단위의 토큰으로 분리한 후 처리한다. 따라서 실행 시점에 주의깊게 띄워쓰기를 지키지 않아도 입력된 식을 잘 처리할 수 있게 한다.
def parse_expr(expStr):
tokens = tokenize(expStr)
OP = ("*", "/", "+", "-", "(", ")")
P = {
"*" : 50,
"/" : 50,
"+" : 40,
"-" : 40,
"(" : 0
}
output = []
stack = []
for item in tokens:
if item not in OP:
output.append(item)
elif item == "(":
stack.append(item)
elif item == ")":
while stack != [] and
stack[-1] != "(":
output.append(stack.pop())
stack.pop()
else:
while stack != [] and
P[stack[-1]] >= P[item]:
output.append(stack.pop())
stack.append(item)
while stack:
output.append(stack.pop())
return output
후위식 계산 알고리듬
이렇게 만든 후위식은 다음과 같이 계산한다. 후위식의 계산은 사람이라면 뒤에서부터 추적해야 하지만, 컴퓨터는 역시 스택을 써서 계산한다. 기본적으로 후위식의 맨 뒤에서 토큰을 하나씩 꺼내는데, 연산자 피연산자1 피연산자2 순으로 꺼내어 계산한다. 이 때 만약 두 피 연산자 중 하나가 연산자가 나온다면, 다시 이어서 2개의 피연산자를 꺼내어 계산한 결과를 피연산자로 사용한다. 즉 뭔가 재귀 형태의 모양이 나오게 된다.
알고리듬으로 정리하면 다음과 같다. 여기서는 재귀꼴을 사용하지 않고 앞에서 처리하는 방식을 사용했다.
- 후위식 토큰을 앞에서 부터 읽는다.
- 토큰이 숫자값이면 스택에 푸시한다.
- 토큰이 연산자이면 스택에서 두 개의 값을 꺼내어 해당 연산자에 맞는 연산을 수행한다. 이 때 먼저 꺼낸 값이 연산자의 우변이 되어야 한다.
- 연산된 결과를 다시 스택에 푸시한다.
- 모든 토큰을 처리하면 스택에는 1개 값이 남는데, 이 값이 계산 결과이다.
이는 다음과 같이 코딩된다.
계산 함수 코드
def calc_expr(expStr):
tokens = parse_expr(expStr)
OP = ("*", "/", "+", "-",)
FUNC = {
"*": lambda x, y: y * x,
"/": lambda x, y: y / x,
"+": lambda x, y: y + x,
"-": lambda x, y: y - x,
}
stack = []
for item in tokens:
if item not in OP:
if '.' in item:
stack.append(float(item))
else:
stack.append(int(item))
else:
x = stack.pop()
y = stack.pop()
stack.append(FUNC[item](x, y))
return stack.pop()
토큰화 함수
수식을 후위식으로 변환하는 함수를 만들 때 보통 여러 고재들에서는 편의상 공백으로 분리되었다고 가정하는데, 실제로는 일일이 식을 띄워서 쓰는 것이 귀찮을때도 있고 경우에 따라서는 그렇지 않게 쓰여진 식을 그대로 계산해야 할 때도 있다. 따라서 숫자와 연산자를 구분하는 작업을 해야하는데, 파서를 써서 한자씩 읽어처리하는 방법도 있겠지만 너무 복잡하므로 간편하게 정규식을 사용한다.
import re
pat = r'(?:' # 캡쳐하지 않는 그룹, 여기서는 조건문처럼 쓰인다.
r'(?<=[^\d\.])' # look backward로 숫자나 점이 아닌 문자가 왼쪽에 있고
r'(?=\d)' # 오른쪽은 숫자가 있는 지점
r'|(?=[^\d\.])' # 만약, 숫자나 점 다음이라면, 그 다음은 숫자나 소수점이 아니어야 한다.
r')'
regexp = re.compile(pat)
특이한 점은 이 정규식은 특정한 문자를 찾는 패턴이 아니라는 점이다. 이 정규식에서 찾는 패턴은 문자와 문자 사이의 틈이며, 왼쪽이 숫자나 소수점이 아니거나, 오른쪽이 소수나 숫자점이 아닌 곳을 찾는다고 보면 된다. 다음 그림은 이 정규식이 어디를 찾고 있는지를 시각적으로 보여준다.
어쨌든 이렇게 찾은 영역은 숫자값(피연산자)과 연산자간의 경계가 되므로 이 영역을 공백 문자로 치환하면 정성스럽게 공백으로 연산자나 괄호를 띄워서 쓴 것과 동일한 효과를 낼 수 있다.
result = re.sub(regexp, " ", expStr)
따라서 tokenize()
함수는 다음과 같이 코딩한다.
tokeninze 함수
def tokenize(expStr):
RE = re.compile(r'(?:(?<=[^\d\.])(?=\d)|(?=[^\d\.]))', re.MULTILINE)
return [x for x in re.sub(RE, " ", expStr).split(" ") if x]
이상을 조합하면 사칙연산 계산기가 완성된다. 이는 다음과 같이 수식 사이의 공백에 대해 신경쓰지 않아도 결과를 계산할 수 있게 한다.
C:>calc.py 1+ 2 * (45-3*(2 + 1))
73
- 프로그램이 실행되면 인자값의 개수를 센다. 인자가 없으면 무시하고 종료한다.
- 주어진 인자들은파싱함수에 전달되어 토근화되고 후위식으로 변환된다.
- 다시 이 값을 계산함수에 전달하여 수식을 계산한다.
- 결과를 출력한다.
아래는 전체 코드이다.
감사합니다. 덕분에 고민하던 문제를 빠르게 해결했네요~
정규 표현식 이해에 있어 정말 많은 도움을 받았습니다. 정말 감사합니다!
하지만 괄호를 포함한 수식을 계산하기 위해서 정규 표현식으로 하나하나 뽑아서 우선순위를 통해 계산하는 프로그램은 정규표현식을 이해하는 데 있어서는 유익한 코드일지 모르겠지만, 실제로는 괄호를 포함한 수식 전체를 lst로 받아 eval(lst)로 처리하면 수식 계산 정보를 알 수 있는 방법도 있습니다.
대신에 여러가지 문제가 생기죠. 어떤 입력이 들어올지 모르는 상황에서 eval() 같은 함수는 안쓰는 습관을 들이는 것이 좋습니다.
3 – 7 * ( ( 5 – 2 ) * 2 ) + ( 2 + 3 ) * 2 = -29 인데 -49가 나오네요
오타가 있었네요. 수정했습니다.
핑백: 파이썬을 명령 프롬프트에서 실행하는 방법 · Wireframe
저 코드를 실행시켜보고 싶은데 저대로 하면 아무것도 안나와서요 수식을 입력받거나 하는 건 어떻게 해야하나요??
실행방법 본문에 있는 거 처럼, 스크립트 실행할 때 인자로 주는 식이고, 따로 입력받으려고 기다리거나 하지 않습니다.
죄송한데 스크립트 실행할 때 인자로 준다는 뜻이 무엇인가요?ㅠㅠ제가 파이썬 진짜 초보라서요ㅠㅠ다른 식으로도 실행시켜보고 싶은데 어떻게 하면 좋을지 알려주실수 있나요??
아뇨 알려드리지 않는 쪽이 좋을 것 같습니다. 이 글은 수식을 파싱하고, 후위식으로 변환하여 계산처리하는 내용을 다루고 있고요… 님은 그냥 사칙연산하는 프로그램이 필요한 거 같으네요.
댓글이 닫혔습니다.