파이썬 자료형과 변수 - 동적 타이핑의 편함과 대가 (파이썬 초급 2단원)
이 단원에서 배우는 것
1단원에서 inventory.py에 title, price, stock 세 변수를 만들었다. 자바나 C를 먼저 본 사람이라면 이상했을 것이다. 타입을 한 글자도 적지 않았는데 그냥 됐다. 이번 단원은 그게 어떻게 가능한지, 그리고 공짜가 아니라 어디에서 값을 치르는지를 다룬다. 서점 소재는 그대로 쓰되, 가격 계산이라는 실무에서 제일 사고가 잦은 영역으로 들어간다.
- 정수·실수·문자열·불린과
None을 구분하고 서로 변환한다. - 변수가 값을 담는 상자가 아니라 값에 붙이는 이름표라는 것을 이해한다.
- 돈 계산에 실수(float)를 쓰면 안 되는 이유를 직접 확인하고 대안을 쓴다.
왜 필요한가
파이썬에서 타입은 변수가 아니라 값이 가진다. price = 28000이라고 쓰면 파이썬은 정수 28000이라는 값을 메모리에 만들고, price라는 이름표를 그 값에 붙인다. 변수는 상자가 아니라 이름표다. 이름표는 언제든지 다른 값으로 옮겨 붙일 수 있으므로 다음 줄에서 price = "공짜"라고 써도 아무 문제가 없다. 이것을 동적 타이핑이라 한다.
편한 만큼 실무에서 문제가 생기는 지점이 정해져 있다. 외부에서 들어오는 값은 전부 문자열이라는 사실이다. 키보드 입력, CSV 파일의 한 칸, 웹 폼에서 넘어온 값, 이 모두가 파이썬에게는 문자열이다. 화면에 28000이라고 찍혀 있어도 그것이 정수 28000인지 문자열 "28000"인지는 눈으로 구별할 수 없다. 자바라면 int price = request.get(...)이 컴파일 단계에서 막아 줬을 일이, 파이썬에서는 그 값을 실제로 더하거나 비교하는 줄에 도달해서야 터진다.
price = "28000" # 파일에서 읽어 온 값이라고 하자
print(price + 1000) # TypeError: can only concatenate str (not "int") to str
이건 그나마 착한 경우다. 오류로 죽어 주니까. 정말 위험한 것은 오류 없이 틀린 답을 내는 쪽이다.
print("10" > "9") # False
재고 10권이 9권보다 적다고 나온다. 문자열끼리 비교하면 사전 순으로 비교하기 때문에 "1"이 "9"보다 앞선다. 프로그램은 멀쩡히 돌고 결과만 틀린다. 자료형을 아는 것은 문법 지식이 아니라 이런 사고를 예방하는 일이다.
문법과 예제
숫자 두 종류
정수는 int, 소수점이 있으면 float이다. 파이썬의 정수는 자릿수 제한이 없다. 자바의 int가 21억에서 넘쳐 음수가 되는 것과 다르다.
print(2 ** 100) # 1267650600228229401496703205376 (제한 없이 커진다)
total = 28_000 # 언더스코어는 자릿수 구분용이다. 값은 28000
print(total)
나눗셈 연산자는 세 개이고, 초보가 가장 자주 헷갈리는 지점이다.
print(7 / 2) # 3.5 ← 나누기는 결과가 항상 float 이다
print(7 // 2) # 3 ← 몫 (소수점 버림)
print(7 % 2) # 1 ← 나머지
print(-7 // 2) # -4 ← 0 쪽이 아니라 작은 쪽으로 내린다
/가 언제나 float을 낸다는 점이 중요하다. 재고 10권을 2개 상자에 나눠 담는 계산에 /를 쓰면 5.0이 나오고, 이 값을 나중에 개수로 쓰면 출력이 5.0권이 된다. 개수는 //다.
돈에 float 을 쓰면 안 되는 이유
컴퓨터는 0.1 같은 십진 소수를 이진법으로 정확히 표현하지 못한다. 근사값을 저장하고, 그 오차가 계산을 거치며 드러난다.
print(0.1 + 0.2) # 0.30000000000000004
print(22000 * 0.7) # 15399.999999999998
print(int(22000 * 0.7)) # 15399 ← 30% 할인가가 1원 모자란다
마지막 줄이 실제로 서비스에서 나는 사고다. int()는 반올림이 아니라 소수점 아래를 버린다. 하루 수천 건이면 결제 금액과 정산 금액이 계속 어긋난다. 해결책은 두 가지다.
방법 1 — 원 단위 정수로만 계산한다. 원화는 소수점 이하를 쓰지 않으므로 이 방법으로 대부분 해결된다. 나눗셈이 필요한 곳에서만 마지막에 반올림한다.
price = 22000
sale = round(price * 0.7) # round 는 반올림이다
print(sale) # 15400
방법 2 — decimal 모듈을 쓴다. 소수점 계산이 꼭 필요하거나(외화, 이자, 세금) 반올림 규칙을 명시해야 하면 이쪽이다. 여기 처음 나오는 from ... import ...는 파이썬이 기본으로 갖고 있는 기능 묶음에서 필요한 것을 가져오는 문장이다. 지금은 "이 한 줄을 맨 위에 적으면 Decimal을 쓸 수 있다" 정도로 넘어가고, 어떻게 동작하는지는 8단원에서 다룬다.
from decimal import Decimal, ROUND_HALF_UP
price = Decimal("22000") # 반드시 문자열로 만든다
sale = price * Decimal("0.7")
print(sale.quantize(Decimal("1"), rounding=ROUND_HALF_UP)) # 15400
Decimal(0.1)처럼 float 을 넘기면 이미 오차가 들어간 값을 그대로 받아 아무 소용이 없다. Decimal("0.1"), 문자열이다.
문자열과 불린, 그리고 None
title = "파이썬 개론"
author = '김서점' # 작은따옴표와 큰따옴표는 같다
note = "저자가 말하길 \"재미있다\"" # 같은 따옴표는 역슬래시로 탈출한다
in_stock = True # 첫 글자가 대문자다. true 는 이름 오류가 난다
discontinued = False
release_date = None # "아직 값이 없다"를 뜻하는 전용 값
None은 0도 빈 문자열도 아니다. "값이 정해지지 않았다"는 상태 자체를 나타내는 값이며, 다른 언어의 null에 해당한다. 절판 여부처럼 참·거짓이 명확한 것은 불린으로, 아직 조사하지 않은 항목은 None으로 둔다.
파이썬은 불린이 아닌 값도 참·거짓으로 판정한다. 규칙은 짧다. 비어 있거나 0이면 거짓, 나머지는 참이다.
print(bool(0), bool(0.0), bool(""), bool(None)) # False False False False
print(bool("0"), bool(-1), bool(" ")) # True True True
"0"이 참이라는 점을 눈여겨본다. 파일에서 재고를 문자열 "0"로 읽어 오면 품절 판정이 통째로 뒤집힌다.
타입 확인과 변환
print(type(28000)) # <class 'int'>
print(type("28000")) # <class 'str'>
print(int("28000")) # 28000 문자열 -> 정수
print(int(" 42 ")) # 42 앞뒤 공백은 알아서 무시한다
print(float("3.5")) # 3.5
print(str(28000)) # '28000' 정수 -> 문자열
int("28,000") # ValueError: invalid literal for int() with base 10: '28,000'
변환이 실패하면 ValueError다. 콤마가 든 금액, 빈 칸, 원 기호가 붙은 값은 전부 여기서 걸린다. 7단원에서 CSV 파일을 읽을 때 이 문제를 정면으로 다룬다.
타입 힌트 — 동적 타이핑의 안전벨트
파이썬 3.5부터 함수에, 3.6부터 변수에 타입을 적어 둘 수 있다. 적어도 실행에는 아무 영향이 없다. 파이썬은 그대로 무시하고 돌린다.
title: str = "파이썬 개론"
price: int = 28000
stock: int = 3
release_date: str | None = None # 3.10 이상에서 쓰는 표기다
그럼 왜 쓰는가. 첫째, 다음 사람에게 "이 변수엔 문자열이 온다"고 알려 준다. 둘째, 에디터가 오타와 타입 오류를 실행 전에 잡아 준다. 셋째, mypy나 pyright 같은 도구를 붙이면 자바의 컴파일러 검사와 비슷한 검증을 받을 수 있다. 앞서 본 "28000" + 1000류의 사고를 실행 전에 걸러 내는 현실적인 수단이 이것이다. 초급 단계에서 전부 붙일 필요는 없지만, 변수가 무슨 타입인지 헷갈리는 순간이 오면 그때가 붙일 때다.
실무에서 자주 틀리는 것
1. 입력값을 숫자로 바꾸지 않고 쓴다
input()은 무엇을 입력하든 문자열을 돌려준다. 숫자를 쳐도 문자열이다.
qty = input("몇 권 사시겠습니까? ") # 사용자가 3 을 입력하면 qty 는 문자열 "3"
total = 28000 * qty
print(type(total), len(total)) # <class 'str'> 28000
합계 금액이 나올 자리에 "3"이 28000번 반복된 문자열이 만들어진다. 문자열에 정수를 곱하면 반복이라는 정상 동작이라서 오류조차 나지 않는다. 화면에 찍어 보기 전까지 아무도 모른다. 받은 즉시 qty = int(input(...))로 변환하는 것을 습관으로 만든다.
2. 금액을 float 으로 들고 다닌다
앞에서 본 int(22000 * 0.7) 문제다. 정리하면 규칙은 세 줄이다. 금액은 정수(원 단위)로 저장한다. 곱셈·나눗셈 결과는 round()로 정수로 되돌린다. 소수점이 꼭 필요하면 Decimal을 쓰되 반드시 문자열로 만든다. 그리고 float 끼리 ==로 같은지 비교하는 코드는 그 자체로 잘못이다. 0.1 + 0.2 == 0.3은 거짓이다.
3. == 를 써야 할 자리에 is 를 쓴다
==는 값이 같은지, is는 메모리에 있는 그 값 자체인지를 본다. 파이썬은 작은 정수와 짧은 문자열을 내부적으로 재사용하기 때문에, is로 비교해도 우연히 참이 나오는 경우가 있다. 그래서 개발 중에는 통과하다가 값이 커지는 운영 환경에서 갑자기 거짓이 된다.
stock = 3
print(stock is 3) # True 로 보이지만 SyntaxWarning 이 뜬다
stock = 3000
print(stock is 3000) # 환경에 따라 False
3.8부터 파이썬이 SyntaxWarning: "is" with 'int' literal. Did you mean "=="?이라고 경고해 준다. 경고가 보이면 고친다. is를 쓰는 곳은 사실상 x is None, x is not None 두 가지뿐이라고 기억하면 된다.
4. None 검사에 참·거짓 판정을 쓴다
stock = 0
if not stock:
print("재고 정보 없음") # 0권이라는 정확한 정보가 있는데도 여기로 온다
if not stock은 None과 0과 빈 문자열을 전부 같은 것으로 취급한다. "값이 없다"와 "값이 0이다"를 구분해야 하는 자리에서는 반드시 if stock is None:이라고 쓴다. 재고, 할인율, 평점처럼 0이 의미를 갖는 값에서 이 실수가 잦다.
스스로 확인하기
- 정가 19800원짜리 책을 15% 할인해 파는 가격을 계산한다.
int(19800 * 0.85)와round(19800 * 0.85)중 어느 쪽을 써야 하며 왜인가? - 다음 코드는 무엇을 출력하는가? 그리고 의도가 "재고가 없으면 경고"였다면 어떻게 고쳐야 하는가?
stock = "0" if stock: print("판매 가능") else: print("품절") - 파일에서 읽은 재고가
"12", 기준 수량이 정수9다."12" > 9를 실행하면 어떻게 되는가?
정답
- 이 값은 마침
16830.0으로 딱 떨어져서 둘 다16830이 나온다. 그래서 더 위험하다. 우연히 맞은 것을 보고int()를 써도 된다고 배우면, 가격이 22000원으로 바뀌는 날 1원이 사라진다. 오차가 생길지 아닐지는 값마다 다르고 미리 알 수 없으므로, 처음부터round()를 기본으로 쓴다. 금액을 다루는 코드에int(실수)가 보이면 일단 의심한다. 판매 가능이 출력된다."0"은 비어 있지 않은 문자열이므로 참이다.stock = int(stock)으로 먼저 정수로 바꾼 뒤if stock > 0:으로 비교해야 한다. 파일에서 읽은 값을 검사보다 변환 먼저 하는 것이 순서다.TypeError: '>' not supported between instances of 'str' and 'int'가 난다. 여기서는 오류로 죽지만, 실수로 기준 수량까지 문자열"9"로 두면 오류 없이False가 나와 재고 12권을 부족으로 판정한다. 오류가 나는 쪽이 오히려 다행인 경우다.
다음 단원에서는 지금까지 다룬 값 하나짜리 변수를 벗어나, 책 여러 권을 한 덩어리로 다루는 리스트·튜플·딕셔너리·셋을 본다. 여기서 바꿀 수 있는 값과 없는 값의 구분이 처음으로 실제 문제가 된다.