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
이상으로 모든 설명을 마칩니다.
감사합니다.



댓글
댓글 쓰기