GUI 계산기 (후위표기법)
개요
대학시절 간단한 계산기를 C++ GUI로 구현하다 생각보다 너무 어려워 🥵 구현하지 못한 경험이 있습니다.
그 때 목표는 아래 윈도우 계산기와 비슷하게(사칙연산만) 구현하는 것이었죠.
가장 큰 실패의 원인은 중위(Infix)표현식, 후위(Postfix)표현식이라는 개념을 몰랐기 때문입니다.
최근 편입 준비중인 컴공과 대학생과 수업하며 이 문제가 지방국립대 컴공과 편입시험문제로 출제된 것을 확인한 적이 있습니다.
그럼 왜 계산기 구현이 생각보다 여려운 주제인지 살펴보겠습니다.
중위(Infix) 표현식이란?
평소 우리가 사용하는 수식표기방식입니다.
예) 2 + 3 * 4
연산자 (+, -, *, / 등) 가 피연산자 (숫자) 사이에 위치합니다
후위(Postfix) 표현식이란?
컴퓨터가 사용하는 수식표기방식입니다.
예) 2 3 4 * +
연산자가 피연산자 뒤에 위치합니다. 처음보면 좀 당황스럽습니다.
(Stack : Last In First Out 자료구조로 구현)
2+3*4, 중위식이 컴퓨터에게 어려운 이유
- 3*4부터 먼저 계산
- 그 결과(12) 에 2를 더하기 답 14
문제 1. 컴퓨터가 마음속으로 수식의 우선순위를 기억 X.
문제 2. 괄호나 연산자의 우선순위를 관리해야 할 필요.
컴퓨터는 후위식이 쉽다
위에서 우리가 사용하는 중위표현식이 컴퓨터에게 어려운 이유를 설명드렸으나, 우리는 인간이므로 잘 이해되지 않으리라 생각합니다.
가령, 기계입장에서 "2 3 4 * +"라는 후위식이 왜 "2+3*4"보다 이해하기 쉬운지 말이죠.
일단 아래에 그 변환과정을 적어 두었으므로 다 읽고 나면 컴퓨터의 입장이 이해되리라 생각합니다.
중위식에서 후위식변환
중위식 '2 + 3 * 4' 를 후위식 '2 3 4 * +' 로 바꾸는 방법입니다.
(머리속에 "2 + 3 * 4" 라는 글자의 왼쪽 2부터 시작한다 상상)
1. 수식의 왼쪽에서 오른쪽으로 이동.
2. 숫자 2를 만나서 저장소 저장.
- 저장소 : 2
- 스택 : 비어있음
3. '+' 연산자를 만나면 연산자는 스택저장.
- 저장소 : 2
- 스택 : + (최초 빈 스택이므로 그냥 저장)
4. 숫자 3 만나서 저장소 저장
- 저장소 : 2 3
- 스택 : +
5. '*' 연산자 만남, 연산자 스택저장.
- 저장소 : 2 3
- 스택 : + *
- 위에서 * 연산자가 + 연산자보다 우선순위↑, 스택 저장(Push)
- 아니면 스택에서 이전 연산자 꺼내 저장소 저장 후, 새연산자는 스택저장(Push)
6. 마지막 숫자 4를 만나면 저장소 저장.
- 저장소 : 2 3 4
- 스택 : + *
7. 모든 식이 끝나면 스택이 비워질 때 까지 스택에서 연산자 꺼내 저장소 저장.
- 저장소 : 2 3 4 *
- 스택 : +
8. 아직 스택에 남은 연산자가 있으므로 반복.
- 저장소 : 2 3 4 * +
- 스택 : 비어있음
9. 최종 "2 3 4 * +"라는 후위식 완성.
이해되지 않는다면 처음부터 설명을 종이위에 적어가며 다시 진행해 보세요.
만약 "2 * 3 + 4" 라면 결과 후위식은 "2 3 * 4 +" 입니다.
후위식 계산 방법
위에서 설명한 과정이 "2 + 3 * 4" 를 "2 3 4 * +" 로 변환하는 과정이라면,
이제 컴퓨터가 이해하기 좋게 후위식으로 변환된 "2 3 4 * +" 를 계산해 14라는 답을 생성하는 과정입니다.
후위식 : "2 3 4 * +" 계산
1. 숫자 2 스택에 저장
- 스택 : 2
2. 숫자 3 스택에 저장 (3이 스택의 위(Top) 임)
- 스택 : 2 3
3. 숫자 4 스택에 저장
- 스택 : 2 3 4
4. 연산자 '*'을 만남, 스택 Top에서 2개 꺼냄(Pop), 후 연산
- 스택에서 4, 3을 꺼내 연산 : 3 * 4 = 12
- 스택 : 2 남음
5. 연산 결과 12는 다시 스택 저장
- 스택 : 2 12
6. 연산자 '+' 만남, 스택 Top에서 2개 꺼냄(Pop), 후 연산
- 스택에서 12, 2를 꺼내 연산 : 2 + 12 = 14
- 스택 : 비어있음
7. 연산 결과 14는 다시 스택 저장
- 스택 : 14
8. 연산자가 더 이상 없으므로 종료, 최종 스택 Pop
- 답은 14
- 스택 : 비어있음
이번에는 그림으로 그려서 표현해 보겠습니다.
![]() |
[후위식 계산과정] |
구현
이론적인 과정이 이해된다면 코드로 구성해봐야겠죠.
Python 과 PyQt6로 구성한 계산기입니다.
괄호의 구현이나 음수, 소수점의 표현등은 위에서 설명하기 복잡해 코드로만 구현해 두었습니다.
![]() |
[내계산기 vs 윈도우 11 계산기] |
앱을 실행한 모습입니다.
소스코드
2개의 소스코드 파일로 구성되어 있으며, 아래 링크의 Qt designer의 UI 파일을 다운받아, 코드와 같은 곳에 두고 실행하면 됩니다.
UI 파일 링크 : calc.ui
main.py
from PyQt6.QtWidgets import QApplication, QWidget from PyQt6.uic import loadUi import sys from calc import Calc class Window(QWidget): def __init__(self): super().__init__() loadUi('calc.ui', self) self.setWindowTitle('Ocean Coding School') self.initUi() def initUi(self): self.nums = [self.pb_0, self.pb_1, self.pb_2, self.pb_3, self.pb_4, self.pb_5,self.pb_6, self.pb_7, self.pb_8, self.pb_9, self.pb_dot] self.ops = [self.pb_plus, self.pb_minus, self.pb_mul, self.pb_div] self.bk = [self.pb_open, self.pb_close] # signal for numbers for pb in self.nums: pb.clicked.connect(self.onNumbers) # signal for operators for pb in self.ops: pb.clicked.connect(self.onOperators) # signal for bracket for pb in self.bk: pb.clicked.connect(self.onBracket) # other signals self.pb_back.clicked.connect(self.onBack) self.pb_eq.clicked.connect(self.onEqual) self.pb_clear.clicked.connect(self.onClear) def writeExpression(self, sender): s = sender.text() exp = self.le.text() if not exp: # 처음 입력 규칙, 시작X if s in "+*/)": return if s == '.': return # 연산자 중복 방지, [-1:] 은 빈문자열도 오류처리 O if exp[-1:] in "+-*/" and s in "+-*/": # 단, 음수는 허용 if s == '-' and exp[-1:] in "*/(" or exp == '': pass else: return # 닫는 괄호 앞에는 숫자나 )만 허용 if s == ")" and exp[-1:] not in "0123456789)": return # 여는 괄호 앞에는 연산자나 시작이어야 함 if s == "(" and exp[-1:] not in "+-*/(" and exp: return # dot 처리 if s == '.': # 직전에 연산자, 괄호가 온 경우 dot 시작 불가 if exp[-1:] in "+-*/()": return # 같은 숫자 내에서 dot 중복 방지(뒤에서부터, 즉 최근숫자만 체크) num = "" for ch in reversed(exp): if ch in '0123456789.': num = ch + num else: break if '.' in num: return self.le.setText(exp + s) def onNumbers(self): sender = self.sender() self.writeExpression(sender) def onOperators(self): sender = self.sender() self.writeExpression(sender) def onBracket(self): sender = self.sender() self.writeExpression(sender) def onBack(self): exp = self.le.text() if exp: self.le.setText(exp[:-1]) def onClear(self): self.le.clear() def onEqual(self): exp = self.le.text() if exp: calc = Calc(exp) result = calc.calcPostfix() if result: self.le.setText(f'{exp} = {result}') if __name__ == '__main__': app = QApplication(sys.argv) w = Window() w.show() sys.exit(app.exec())
calc.py
class Calc: def __init__(self, exp): self.infix = exp def priority(self, op): if op == '^': return 3 elif op=='*' or op=='/': return 2 elif op=='+' or op=='-': return 1 else: return -1 def parsing(self): if not self.infix: return [] s = 0 exp = [] i = 0 while i < len(self.infix): c = self.infix[i] if c in '+-*/^()': # 음수처리 if c=='-' and (i==0 or self.infix[i-1] in '+-*/^('): j = i+1 while j < len(self.infix) and (self.infix[j].isdigit() or self.infix[j]=='.'): j+=1 exp.append(self.infix[i:j]) i = j s = i continue if s!=i: exp.append(self.infix[s:i]) exp.append(c) s = i+1 i+=1 if s<len(self.infix): exp.append(self.infix[s:]) return exp def infixToPostfix(self): lst = self.parsing() postfix = [] stack = [] for c in lst: if self.isNumber(c): postfix.append(c) elif c=='(': stack.append(c) elif c==')': isOpen = False while stack: top = stack.pop() if top=='(': isOpen=True break postfix.append(top) if not isOpen: print('Mismatched parentheses.') return elif c in '+-*/^': # '^'연산자 우측결합처리 while stack and ((c != '^' and self.priority(c) <= self.priority(stack[-1])) or (c == '^' and self.priority(c) < self.priority(stack[-1]))): postfix.append(stack.pop()) stack.append(c) while stack: top = stack.pop() if top == '(': print('Mismatched parentheses.') return postfix.append(top) #print(postfix) return postfix def calcPostfix(self): postfix = self.infixToPostfix() stack = [] for c in postfix: if self.isNumber(c): stack.append( self.strToNumber(c) ) else: if len(stack)<2: print('Not enough operands') return a = stack.pop() b = stack.pop() if c=='+': stack.append(b+a) elif c=='-': stack.append(b-a) elif c=='*': stack.append(b*a) elif c=='/': if a==0: print('Zero division error.') return stack.append(b/a) elif c=='^': stack.append(b**a) return stack.pop() def isNumber(self, s): if not s: return False if s[0] == '-': s = s[1:] return s.replace('.', '', 1).isdigit() def strToNumber(self, s): if '.' in s: return float(s) else: return int(s)
개발환경
- Windows 11 Pro, VS code
- Python 3.12
- PyQt6 6.9
이상으로 모든 설명을 마칩니다.
감사합니다.
댓글
댓글 쓰기