파이썬 제어문 - if for while과 for else, range 동작 (파이썬 초급 4단원)
이 단원에서 배우는 것
3단원에서 재고를 딕셔너리에 담았다. 하지만 지금까지는 inventory["파이썬 개론"]처럼 책 이름을 코드에 직접 적어 한 건씩만 꺼낼 수 있었다. 책이 300권이면 이 방식은 끝난다. 이번 단원의 반복문부터는 몇 건이 들었는지 몰라도 같은 코드가 돈다. 조건문까지 붙으면 "재고 2권 이하인 책만 골라 발주 목록을 만든다" 같은 진짜 업무가 코드가 된다.
if/elif/else로 조건을 나누고 논리 연산자를 정확히 쓴다.for로 리스트와 딕셔너리를 돌고,range가 실제로 무엇인지 안다.break·continue와for ... else로 "찾았는가"를 깔끔하게 처리한다.
왜 필요한가
프로그램을 쓰는 시점에 우리는 데이터가 몇 건인지 모른다. 오늘 판매 기록이 3건일지 3만 건일지는 실행해 봐야 안다. 반복문은 개수를 모르는 데이터에 같은 처리를 적용하는 장치이고, 조건문은 건마다 다른 판단이 필요할 때 갈라지는 장치다. 이 둘이 없으면 데이터 건수만큼 코드를 적는 수밖에 없다.
실무에서 조건·반복이 진짜 문제가 되는 지점은 문법이 아니다. 돌고 있는 대상을 도는 도중에 건드리는 것과, 조건식을 사람 말처럼 써 놓고 뜻이 다른 것이다. 둘 다 오류 없이 조용히 틀린 답을 낸다. 뒤에서 자세히 본다.
문법과 예제
조건문
3단원의 재고 딕셔너리를 그대로 쓴다. 재고 수량에 따라 상태를 정하는 코드다.
stock = 1
if stock == 0:
state = "품절"
elif stock <= 2:
state = "부족"
else:
state = "충분"
print(state) # 부족
elif는 else if의 줄임이고 몇 개든 이어 쓸 수 있다. 위에서부터 검사해서 처음 참이 된 하나만 실행하고 나머지는 건너뛴다. 그래서 stock <= 2를 0 < stock <= 2로 쓸 필요가 없다. 0은 이미 앞에서 걸러졌다. 순서가 곧 조건의 일부다.
비교 연산자는 ==, !=, <, <=, >, >=이고 논리 연산자는 and, or, not이다. &&나 ||는 파이썬 문법이 아니다.
if stock > 0 and price <= 30000:
print("판매 가능한 3만원 이하 도서")
if not discontinued:
print("절판 아님")
파이썬에는 다른 언어에 잘 없는 편한 문법이 하나 있다. 연쇄 비교다.
if 0 < stock <= 5: # 0 < stock and stock <= 5 와 같다
print("소량 재고")
참일 때만 값을 정하는 짧은 형태도 자주 쓴다. 삼항 연산자에 해당한다.
label = "품절" if stock == 0 else "판매중"
값 하나를 여러 경우와 대조할 때는 파이썬 3.10부터 생긴 match를 쓸 수 있다. 다른 언어의 switch와 비슷하되 훨씬 강력하다. 초급 단계에서는 elif 사슬로 충분하지만, 3.9 이하에서는 문법 오류가 난다는 점만 알아 둔다.
match stock:
case 0:
print("품절")
case 1 | 2:
print("부족")
case _: # _ 는 나머지 전부
print("충분")
반복문 for
파이썬의 for는 "몇 번 돌아라"가 아니라 "이 안에 든 것을 하나씩 꺼내라"다. 리스트, 문자열, 딕셔너리 등 원소를 꺼낼 수 있는 것이면 뭐든 온다.
inventory = {"파이썬 개론": 3, "리팩터링": 0, "SQL 첫걸음": 12, "클린 코드": 1}
for title in inventory: # 딕셔너리를 그냥 돌면 '키'가 나온다
print(title)
for title, stock in inventory.items(): # 키와 값을 한 번에
print(title, stock)
두 번째 형태는 3단원의 튜플 언패킹이다. items()가 ("파이썬 개론", 3) 같은 튜플을 하나씩 내주고, 그것을 두 변수로 푸는 것이다.
이제 앞의 조건문과 합쳐 발주 목록을 만든다. 이번 단원의 대표 예제다.
inventory = {"파이썬 개론": 3, "리팩터링": 0, "SQL 첫걸음": 12, "클린 코드": 1}
reorder = []
for title, stock in inventory.items():
if stock == 0:
reorder.append(title)
print(title, "- 품절, 즉시 발주")
elif stock <= 2:
reorder.append(title)
print(title, "- 재고", stock, "권, 발주 권장")
else:
print(title, "- 재고", stock, "권")
print("발주 대상:", reorder)
파이썬 개론 - 재고 3 권
리팩터링 - 품절, 즉시 발주
SQL 첫걸음 - 재고 12 권
클린 코드 - 재고 1 권, 발주 권장
발주 대상: ['리팩터링', '클린 코드']
순번이 필요하면 enumerate()를 쓴다. 번호를 직접 세는 변수를 만들 필요가 없다.
titles = ["파이썬 개론", "리팩터링", "SQL 첫걸음"]
for no, title in enumerate(titles, start=1):
print(no, title)
# 1 파이썬 개론
# 2 리팩터링
# 3 SQL 첫걸음
range 의 정체
range는 정해진 횟수만큼 돌 때 쓴다. 그런데 range(3)은 리스트가 아니다.
print(range(3)) # range(0, 3) ← 리스트가 아니다
print(list(range(3))) # [0, 1, 2]
range는 숫자를 미리 만들어 두지 않고 필요할 때 하나씩 계산해서 내주는 객체다. 그래서 range(10_000_000)을 써도 메모리를 거의 쓰지 않는다. 값을 직접 보고 싶을 때만 list()로 감싼다.
list(range(5)) # [0, 1, 2, 3, 4] 0부터 5 직전까지
list(range(1, 4)) # [1, 2, 3] 시작을 정한다
list(range(0, 10, 3)) # [0, 3, 6, 9] 3칸씩
list(range(3, 0, -1)) # [3, 2, 1] 거꾸로
끝 숫자를 포함하지 않는 규칙은 슬라이싱과 같다. range(n)의 개수가 정확히 n개가 되도록 맞춘 설계다.
while 과 break, continue
for는 "대상이 있을 때", while은 "조건이 만족될 때까지" 쓴다. 몇 번 돌지 미리 알 수 없는 경우다.
stock = 3
days = 0
while stock > 0: # 재고가 다 팔릴 때까지 하루씩 진행
stock -= 1
days += 1
print(days, "일 만에 소진") # 3 일 만에 소진
continue는 이번 회차만 건너뛰고, break는 반복 자체를 끝낸다.
for title, stock in inventory.items():
if stock > 2:
continue # 재고 충분하면 아래를 건너뛴다
print("발주:", title)
for 에 붙는 else
파이썬에만 있는 문법이고 이름이 오해를 부른다. for ... else의 else는 "조건이 거짓일 때"가 아니라 "break 없이 끝까지 다 돌았을 때" 실행된다. nobreak라는 이름이었으면 좋았을 것이다.
왜 필요한지는 이것 없이 같은 일을 해 보면 안다. "목록에서 찾았는가"를 표시하려고 보통 이렇게 쓴다.
found = False
for title in titles:
if title == "클린 코드":
found = True
break
if not found:
print("클린 코드 - 미취급, 발주 필요")
found라는 변수는 업무와 아무 상관이 없다. 순전히 반복문 사정 때문에 만든 변수다. else를 쓰면 사라진다.
for title in titles:
if title == "클린 코드":
print("취급 중")
break
else:
print("클린 코드 - 미취급, 발주 필요")
이 else는 for와 같은 칸에 들여 쓴다는 점에 주의한다. if의 else가 아니다.
실무에서 자주 틀리는 것
1. 반복 도는 리스트에서 원소를 지운다
가장 자주 나오고 가장 안 보이는 버그다. 품절인 책을 목록에서 빼려는 코드다.
stocks = [0, 0, 3, 12]
for s in stocks:
if s == 0:
stocks.remove(s)
print(stocks) # [0, 3, 12] ← 0 이 하나 남았다
파이썬은 리스트를 번호로 훑는다. 0번을 보고 지우면 뒤에 있던 것들이 한 칸씩 앞으로 당겨지는데, 파이썬은 그것도 모르고 다음번에 1번을 본다. 결국 당겨진 자리에 온 원소를 통째로 건너뛴다. 오류가 안 나기 때문에 데이터가 몇 건 조용히 누락된 채 운영된다.
해결은 둘 중 하나다. 복사본을 돌면서 원본을 지우거나, 남길 것만 모아 새 리스트를 만든다. 두 번째가 더 안전하고 읽기도 쉽다.
keep = []
for s in stocks:
if s != 0:
keep.append(s)
stocks = keep # [3, 12]
# 또는 복사본을 돈다
for s in list(stocks):
if s == 0:
stocks.remove(s)
딕셔너리는 더 단호하다. 돌면서 키를 지우면 아예 예외를 던진다.
RuntimeError: dictionary changed size during iteration
2. or 로 값을 여러 개 비교한다
사람 말로 "태그가 초급이거나 베스트면"을 그대로 옮기면 이렇게 된다.
tag = "신간"
if tag == "초급" or "베스트":
print("여기가 실행된다") # 태그가 무엇이든 항상 실행된다
파이썬은 이 조건을 (tag == "초급") or ("베스트")로 읽는다. 앞이 거짓이면 뒤의 "베스트" 자체를 참·거짓으로 판정하는데, 2단원에서 봤듯 비어 있지 않은 문자열은 참이다. 그래서 조건이 항상 참이 된다. 문법 오류도 경고도 없다.
if tag == "초급" or tag == "베스트": # 매번 왼쪽까지 다 쓴다
if tag in ("초급", "베스트"): # 후보가 셋 이상이면 이쪽이 낫다
3. range(len(...)) 로 번호를 돌린다
for i in range(len(titles)):
print(i + 1, titles[i])
돌아가긴 한다. 하지만 대괄호 인덱싱이 들어가는 순간 titles[i+1] 같은 실수와 IndexError의 여지가 생긴다. 원소만 필요하면 for title in titles, 번호까지 필요하면 enumerate()다. range(len(...))이 정말 맞는 경우는 두 리스트를 같은 번호로 짝지어 봐야 할 때 정도이고, 그마저 zip()이라는 더 나은 방법이 있다.
4. while 조건에 쓰는 값을 안 바꾼다
stock = 3
while stock > 0:
print("판매") # stock 을 줄이는 줄이 없다 -> 영원히 멈추지 않는다
for는 대상이 떨어지면 알아서 끝나지만 while은 사람이 끝내야 한다. while을 쓸 때는 조건에 등장하는 변수가 루프 안에서 반드시 변하는지를 눈으로 확인한다. 무한 루프에 빠졌으면 터미널에서 Ctrl+C로 멈춘다. 셀 수 있는 반복이면 while보다 for가 안전하다.
스스로 확인하기
list(range(1, 10, 4))의 결과는?- 판매 기록
sold = ["파이썬 개론", "리팩터링", "파이썬 개론"]을 돌면서 책별 판매 수량 딕셔너리를 만든다.{'파이썬 개론': 2, '리팩터링': 1}이 나오게 하려면? - 다음 코드는 "재고 3권"을 몇 번 출력하는가?
for stock in [3, 0, 3]: if stock == 0: break print("재고 3권")
정답
[1, 5, 9]다. 1에서 시작해 4씩 커지고 10은 포함하지 않는다. 다음 값 13은 범위를 넘으므로 9에서 끝난다.- 3단원의
get()과 이번 단원의for를 합친다.sold = ["파이썬 개론", "리팩터링", "파이썬 개론"] count = {} for title in sold: count[title] = count.get(title, 0) + 1 print(count) # {'파이썬 개론': 2, '리팩터링': 1}count[title] + 1로 쓰면 처음 나오는 책에서KeyError가 난다.get(title, 0)이 "없으면 0부터"를 처리해 준다. 이 세 줄은 실무에서 통계를 낼 때 계속 쓰게 되는 관용구다. - 한 번이다. 두 번째 원소
0에서break로 반복이 통째로 끝나므로 세 번째3에는 도달하지 못한다.break를continue로 바꾸면 그 회차만 건너뛰므로 두 번 출력된다.
다음 단원에서는 지금 만든 발주 목록 코드를 함수로 묶는다. 같은 판단을 여러 곳에서 써야 할 때 복사해 붙이는 대신 이름을 붙여 부르는 방법이고, 여기서 파이썬 초급의 최대 함정인 가변 기본값 인자를 만난다.