Devin.KR
로그인

파이썬 제어문 - if for while과 for else, range 동작 (파이썬 초급 4단원)

개발자 조회 1

이 단원에서 배우는 것

3단원에서 재고를 딕셔너리에 담았다. 하지만 지금까지는 inventory["파이썬 개론"]처럼 책 이름을 코드에 직접 적어 한 건씩만 꺼낼 수 있었다. 책이 300권이면 이 방식은 끝난다. 이번 단원의 반복문부터는 몇 건이 들었는지 몰라도 같은 코드가 돈다. 조건문까지 붙으면 "재고 2권 이하인 책만 골라 발주 목록을 만든다" 같은 진짜 업무가 코드가 된다.

  • if/elif/else로 조건을 나누고 논리 연산자를 정확히 쓴다.
  • for로 리스트와 딕셔너리를 돌고, range가 실제로 무엇인지 안다.
  • break·continuefor ... else로 "찾았는가"를 깔끔하게 처리한다.

왜 필요한가

프로그램을 쓰는 시점에 우리는 데이터가 몇 건인지 모른다. 오늘 판매 기록이 3건일지 3만 건일지는 실행해 봐야 안다. 반복문은 개수를 모르는 데이터에 같은 처리를 적용하는 장치이고, 조건문은 건마다 다른 판단이 필요할 때 갈라지는 장치다. 이 둘이 없으면 데이터 건수만큼 코드를 적는 수밖에 없다.

실무에서 조건·반복이 진짜 문제가 되는 지점은 문법이 아니다. 돌고 있는 대상을 도는 도중에 건드리는 것과, 조건식을 사람 말처럼 써 놓고 뜻이 다른 것이다. 둘 다 오류 없이 조용히 틀린 답을 낸다. 뒤에서 자세히 본다.

문법과 예제

조건문

3단원의 재고 딕셔너리를 그대로 쓴다. 재고 수량에 따라 상태를 정하는 코드다.

stock = 1

if stock == 0:
    state = "품절"
elif stock <= 2:
    state = "부족"
else:
    state = "충분"

print(state)   # 부족

elifelse if의 줄임이고 몇 개든 이어 쓸 수 있다. 위에서부터 검사해서 처음 참이 된 하나만 실행하고 나머지는 건너뛴다. 그래서 stock <= 20 < 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 ... elseelse는 "조건이 거짓일 때"가 아니라 "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("클린 코드 - 미취급, 발주 필요")

elsefor와 같은 칸에 들여 쓴다는 점에 주의한다. ifelse가 아니다.

실무에서 자주 틀리는 것

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가 안전하다.

스스로 확인하기

  1. list(range(1, 10, 4))의 결과는?
  2. 판매 기록 sold = ["파이썬 개론", "리팩터링", "파이썬 개론"]을 돌면서 책별 판매 수량 딕셔너리를 만든다. {'파이썬 개론': 2, '리팩터링': 1}이 나오게 하려면?
  3. 다음 코드는 "재고 3권"을 몇 번 출력하는가?
    for stock in [3, 0, 3]:
        if stock == 0:
            break
        print("재고 3권")

정답

  1. [1, 5, 9]다. 1에서 시작해 4씩 커지고 10은 포함하지 않는다. 다음 값 13은 범위를 넘으므로 9에서 끝난다.
  2. 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부터"를 처리해 준다. 이 세 줄은 실무에서 통계를 낼 때 계속 쓰게 되는 관용구다.
  3. 한 번이다. 두 번째 원소 0에서 break로 반복이 통째로 끝나므로 세 번째 3에는 도달하지 못한다. breakcontinue로 바꾸면 그 회차만 건너뛰므로 두 번 출력된다.

다음 단원에서는 지금 만든 발주 목록 코드를 함수로 묶는다. 같은 판단을 여러 곳에서 써야 할 때 복사해 붙이는 대신 이름을 붙여 부르는 방법이고, 여기서 파이썬 초급의 최대 함정인 가변 기본값 인자를 만난다.