본문으로 건너뛰기

세기: 메뉴별로 모으기

파이썬 목차딕셔너리와 집합레슨 34 / 59

목차

세기는 리스트의 값마다 몇 번 나왔는지를 딕셔너리에 모으는 틀입니다. 값을 키로 삼아, 그 값을 꺼낼 때마다 그 키의 값을 1 올립니다.

합 모으기는 모으는 값이 이름 하나였습니다. 메뉴별 판매 수처럼 모을 것이 메뉴마다 따로면, 어떤 메뉴가 주문될지 몰라 이름을 미리 만들어 둘 수 없습니다. 딕셔너리는 처음 보는 메뉴가 나올 때 그 메뉴의 칸을 새로 만들어, 메뉴가 몇 가지든 같은 코드로 셉니다. 이 레슨은 딕셔너리로 세는 코드를 직접 쓰는 틀 레슨입니다.

14단계 중 1단계. 메뉴별 판매 수를 세는 코드를 하위 목표 셋으로 나눠 따라갑니다.

1세기의 세 하위 목표

주문 리스트에서 메뉴별 판매 수를 세는 풀이 예제입니다. 그림이 이 코드를 한 줄씩 따라가며 추적 표를 쌓았습니다.

orders = ['커피', '라테', '커피']
# 1. 빈 딕셔너리
sold = {}
# 2. 하나씩 꺼내 칸 올리기
for item in orders:
    before = sold.get(item, 0)
    sold[item] = before + 1
# 3. 끝나고 쓰기
print(sold)
실행 결과
{'커피': 2, '라테': 1}
  • 1. 빈 딕셔너리: 반복 앞에서 sold를 빈 딕셔너리 {}에 붙입니다. 아직 아무것도 세지 않았습니다.
  • 2. 하나씩 꺼내 칸 올리기: 주문을 하나씩 꺼내 item을 붙이고, 그 메뉴의 칸을 1 올립니다. 6번 줄이 지금까지 센 수를 꺼내 before를 붙이고, 7번 줄이 거기에 1을 더해 같은 키에 다시 담습니다. 처음 보는 메뉴는 키가 없어 get()이 기본값 0을 돌려주므로, 그 메뉴의 칸이 1로 생깁니다. 셋째 주문 '커피'는 키가 있어 before가 1에 붙고 칸이 2가 됩니다.
  • 3. 끝나고 쓰기: 반복이 끝난 뒤 한 번 찍습니다.

item처럼 리스트의 값을 차례로 받는 이름은 걸음꾼, sold처럼 센 것을 담아 두는 이름은 담는 그릇입니다.

2빈 딕셔너리는 반복 앞에

빈 딕셔너리를 만드는 줄을 반복 안으로 옮겼습니다. 무엇이 찍힐까요?

orders = ['라테', '녹차', '라테']
for item in orders:
    sold = {}
    before = sold.get(item, 0)
    sold[item] = before + 1
print(sold)
실행 결과 보기
실행 결과
{'라테': 1}

예상과 다르면 그곳이 배울 곳입니다. 3번 줄은 덩어리 안이라 바퀴마다 실행되어 sold를 새 빈 딕셔너리에 다시 붙입니다(규칙 4). 앞 바퀴까지 센 딕셔너리와의 연결이 끊겨, 마지막 바퀴의 '라테' 1만 남습니다. 빈 딕셔너리는 1번 하위 목표대로 반복 앞에서 한 번 만듭니다.

예제

메뉴별 매출 모으기

칸을 1 대신 가격만큼 올리면 메뉴별 매출이 모입니다. 틀은 그대로이고 10번 줄에서 더하는 값만 다릅니다.

menu = {
    '커피': 4000,
    '라테': 4500,
}
orders = ['커피', '라테', '커피']
sales = {}
for item in orders:
    price = menu[item]
    before = sales.get(item, 0)
    sales[item] = before + price
print(sales)
실행 결과
{'커피': 8000, '라테': 4500}

판매 수와 매출을 함께 알고 싶으면 sold와 sales처럼 딕셔너리를 따로 둡니다. 한 칸에 1과 가격을 함께 더하면 어느 쪽도 아닌 수가 됩니다.

자주 틀리는 곳

틀린 생각: 빈 딕셔너리를 만드는 줄은 반복 안에 둬도 한 번만 실행된다.

빈 딕셔너리를 만드는 일은 처음에 한 번 하는 일이라, 어디에 두든 한 번만 실행될 것 같습니다. 덩어리 안의 줄은 바퀴마다 실행됩니다.

orders = ['커피', '커피']
for item in orders:
    sold = {}
    before = sold.get(item, 0)
    sold[item] = before + 1
print(sold)
실행 결과
{'커피': 1}

커피가 두 번인데 1이 찍힙니다. 바퀴마다 sold가 새 빈 딕셔너리에 다시 붙어 앞 바퀴에 센 1이 사라졌습니다.

맞는 규칙: 덩어리 안의 sold = {}는 바퀴마다 sold를 새 빈 딕셔너리에 다시 붙여 센 것을 잃습니다. 빈 딕셔너리는 반복 앞에서 한 번 만듭니다(규칙 4).

틀린 생각: 처음 보는 메뉴도 sold[item] += 1로 바로 1을 올릴 수 있다.

+= 1이 '1 올리기'로 읽혀서, 칸이 없으면 알아서 0에서 시작할 것 같습니다. sold[item] += 1은 sold[item] = sold[item] + 1⁠과 같아서 오른쪽에서 지금 값부터 꺼냅니다.

orders = ['커피', '라테']
sold = {}
for item in orders:
    sold[item] += 1
실행 결과
Traceback (most recent call last):  File "main.py", line 4, in <module>    sold[item] += 1    ~~~~^^^^^^KeyError: '커피'

첫 주문 '커피'는 아직 키가 없어 지금 값을 꺼낼 수 없으니, 4번 줄에서 KeyError가 납니다.

맞는 규칙: 칸 올리기는 지금 값을 먼저 꺼내므로 처음 보는 키에서 멈춥니다. before = sold.get(item, 0)⁠처럼 기본값 0을 받아 올립니다.

확인 문제

1. 1절 코드의 1번 줄을 orders = ['쿠키', '녹차', '쿠키']⁠로 바꿨습니다. 무엇이 찍히는지 쓰세요.

orders = ['쿠키', '녹차', '쿠키']
# 1. 빈 딕셔너리
sold = {}
# 2. 하나씩 꺼내 칸 올리기
for item in orders:
    before = sold.get(item, 0)
    sold[item] = before + 1
# 3. 끝나고 쓰기
print(sold)
힌트

바퀴마다 6번 줄에서 before가 무엇에 붙는지 적어 봅니다.

정답
실행 결과
{'쿠키': 2, '녹차': 1}

'쿠키'와 '녹차'는 처음 나올 때 칸이 1로 생기고, 셋째 바퀴의 '쿠키'는 before가 1에 붙어 칸이 2가 됩니다. 키는 처음 담은 차례대로 쿠키, 녹차 차례로 찍힙니다.

흔한 오답은 {'녹차': 1, '쿠키': 2}입니다. 키가 가나다 차례로 찍힌다고 본 것입니다. 딕셔너리는 키를 담은 차례를 지킵니다.

2. 컵 크기별 판매 수를 셉니다.

(가) 다 채우면 {'S': 2, 'L': 1}이 찍혀야 하고, 1번 줄이 cups = ['L', 'L', 'L']⁠이면 {'L': 3}이 찍혀야 합니다. 7번 줄의 빈칸을 채우세요.

cups = ['S', 'L', 'S']
# 1. 빈 딕셔너리
sold = {}
# 2. 하나씩 꺼내 칸 올리기
for cup in cups:
    before = sold.get(cup, 0)
    sold[cup] = ________
# 3. 끝나고 쓰기
print(sold)
힌트

2번 하위 목표 '하나씩 꺼내 칸 올리기'입니다. 6번 줄의 before가 무엇에 붙는지 봅니다.

정답

before + 1입니다.

cups = ['S', 'L', 'S']
# 1. 빈 딕셔너리
sold = {}
# 2. 하나씩 꺼내 칸 올리기
for cup in cups:
    before = sold.get(cup, 0)
    sold[cup] = before + 1
# 3. 끝나고 쓰기
print(sold)
실행 결과
{'S': 2, 'L': 1}

다르게 써도 되는 곳: 1 + before나 sold.get(cup, 0) + 1도 같습니다.

흔한 오답은 1만 쓰는 것입니다. 지금 값에 더하지 않고 칸마다 1을 다시 담아 {'S': 1, 'L': 1}이 찍힙니다. 1을 담으면 칸이 1 오른다고 본 것입니다.

(나) 2번 하위 목표를 통째로 채우세요. 다 채운 코드는 {'L': 3, 'S': 1}을 찍어야 하고, 1번 줄이 cups = ['S']이면 {'S': 1}을 찍어야 합니다.

cups = ['L', 'S', 'L', 'L']
# 1. 빈 딕셔너리
sold = {}
# 2. 하나씩 꺼내 칸 올리기

# 3. 끝나고 쓰기
print(sold)
힌트

1절 풀이 예제의 2번 하위 목표와 같은 세 줄입니다. 걸음꾼 이름만 이 코드에 맞춥니다.

정답
cups = ['L', 'S', 'L', 'L']
# 1. 빈 딕셔너리
sold = {}
# 2. 하나씩 꺼내 칸 올리기
for cup in cups:
    before = sold.get(cup, 0)
    sold[cup] = before + 1
# 3. 끝나고 쓰기
print(sold)
실행 결과
{'L': 3, 'S': 1}

다르게 써도 되는 곳: 걸음꾼 이름은 다른 낱말이어도 됩니다. 7번 줄의 before 자리에 sold.get(cup, 0)을 바로 써서 6·7번 줄을 한 줄로 합쳐도 같습니다.

흔한 오답은 6·7번 줄 대신 sold[cup] += 1을 쓰는 것입니다. 첫 컵 'L'에서 키가 없어 KeyError가 납니다. 처음 보는 키도 바로 1을 올릴 수 있다고 본 것입니다.

3. 낱말에 든 글자를 셉니다. 무엇이 찍히는지 쓰세요.

count = {}
for ch in '바나나':
    before = count.get(ch, 0)
    count[ch] = before + 1
print(count)
힌트

문자열을 for로 돌면 한 글자씩 꺼냅니다. 바퀴마다 before를 적어 봅니다.

정답
실행 결과
{'바': 1, '나': 2}

'바'와 첫 '나'는 처음 보는 키라 칸이 1로 생기고, 셋째 글자 '나'는 before가 1에 붙어 칸이 2가 됩니다.

흔한 오답은 {'바': 1, '나': 1}입니다. 셋째 글자 '나'에서도 before가 0에 붙는다고 본 것입니다. 키 '나'는 둘째 바퀴에 이미 담겨 get()이 1을 돌려줍니다.

아래 코드 6번 줄 sold.get(item, 0)의 0을 1로 바꾸면 무엇이 찍히는지 봅니다.

처음 펼 때 약 6MB를 받습니다.컴퓨터에서 해 보기

끝까지 내려오면 저절로 표시됩니다. 이 기기에만 저장합니다.

파이썬 목차로