B-3: 컴프리헨션 — for 를 한 줄로 빚어내기
목차 29
안녕하세요, 홍순구 튜터입니다. 지난 시간엔 이름표로 찾는 딕셔너리와 중복을 없애는 집합을 배웠죠. 그러면서 우리는 for 를 참 많이 돌렸어요. 좋아요가 1000 넘는 사람을 세려고 for 를 돌리고, 회원 명부를 훑으려고 또 for 를 돌리고요.
그런데 지난 시간 마지막에 제가 슬쩍 흘린 말이 있어요. "이걸 매번 for 로 또박또박 짜야 하나요?" 라는 질문에, "더 짧게 쓰는 방법이 있다" 고 했죠. 오늘 그 방법을 배웁니다. 바로 컴프리헨션(comprehension) 이에요.
이름이 좀 어렵죠? 우리말로 풀면 "한 줄로 빚어내기" 정도예요. for 로 서너 줄에 걸쳐 만들던 걸 단 한 줄로 줄이는 도구입니다. 처음엔 낯설어도, 익숙해지면 파이썬에서 가장 자주 쓰는 표현이 돼요. 오늘 천천히 익혀 봅시다.
오늘의 여정
"for 를 한 줄로 빚어내기"
for 루프 (지난 시간)
│
▼
[ 표현식 for x in xs ] ← 리스트 컴프리헨션 (Step 1)
│ + if 조건 으로 거르기 ← 조건 필터 (Step 2)
▼
{ key: 값 for ... } ← 딕셔너리·집합 (Step 3)
│
▼
리스트 안의 리스트 펼치기 ← 중첩 (Step 4)
│
▼
( 표현식 for x in xs ) ← 제너레이터, 메모리 절약 (Step 5)
│
▼
enumerate · zip ← 번호와 함께, 짝지어 (Step 6~7)
│
▼
해시태그 추출기 ← 오늘 도구 총동원 (Step 8)
💡 오늘 수업의 핵심 — "for 로 또박또박 만들던 리스트·딕셔너리·집합을, 컴프리헨션 한 줄로 빚어낸다"
🎯 학습 목표
- 리스트·딕셔너리·집합 컴프리헨션으로
for여러 줄을 한 줄로 줄이고,if로 원하는 것만 걸러냅니다. - 제너레이터 표현식으로 큰 데이터를 메모리 부담 없이 다루는 감각을 잡습니다.
enumerate로 번호와 함께 돌고,zip으로 두 리스트를 짝지어 도는 법을 익혀 해시태그 추출기를 완성합니다.
Step 1: "for 세 줄을 한 줄로" (리스트 컴프리헨션 도입)
지난 시간에 우리가 자주 했던 일이 있어요. 빈 리스트를 하나 만들고, for 로 돌면서 append 로 하나씩 채우는 거죠. 좋아요 숫자마다 1을 더한 새 리스트를 만든다고 해봅시다.
# src/b_structures/b3_intro.py
# 지난 시간 방식: for 로 또박또박 새 리스트 만들기
likes = [120, 8500, 320, 1500, 90]
plus_one = []
for like in likes:
plus_one.append(like + 1)
print("for 로 만든 결과:", plus_one)
세 줄이 필요했어요. 빈 리스트 만들고, for 돌고, append 하고요. 이걸 컴프리헨션으로 쓰면 이렇게 됩니다.
# 오늘 방식: 컴프리헨션으로 한 줄에 만들기
plus_one2 = [like + 1 for like in likes]
print("컴프리헨션 결과:", plus_one2)
두 결과는 완전히 똑같아요.
for 로 만든 결과: [121, 8501, 321, 1501, 91]
컴프리헨션 결과: [121, 8501, 321, 1501, 91]
자, 이 한 줄을 뜯어볼까요. 대괄호 [] 안에 이렇게 생겼어요.
[ like + 1 for like in likes ]
───┬─── ──────┬─────────
무엇을 담을지 어디서 하나씩 꺼낼지
(표현식) (for 부분)
오른쪽 for like in likes 가 먼저예요. "likes 에서 like 를 하나씩 꺼낸다" 는 뜻이죠. 지난 시간 for 와 똑같아요. 그리고 왼쪽 like + 1 이 "그 꺼낸 값으로 무엇을 만들어 담을지" 예요. 읽는 순서는 "likes 에서 like 를 하나씩 꺼내서, like + 1 을 담아라" 입니다.
문자열도 똑같이 변환할 수 있어요. 이름을 모두 대문자로 바꿔봅시다. (upper() 는 지난 시간에 봤던 문자열 메서드죠.)
# 문자열도 변환 — 이름을 모두 대문자로
names = ["jaehoon", "minji", "seungwoo"]
upper_names = [name.upper() for name in names]
print("대문자 이름:", upper_names)
대문자 이름: ['JAEHOON', 'MINJI', 'SEUNGWOO']
한 가지 더. 컴프리헨션은 리스트뿐 아니라 문자열에서도 글자를 하나씩 꺼낼 수 있어요. 지난 시간 예고편에서 보여드린 [글자 for 글자 in 단어] 모양이 바로 이거예요.
# 글자 하나하나 떼어내기 ([글자 for 글자 in 단어])
letters = [ch for ch in "insta"]
print("글자 분해:", letters)
글자 분해: ['i', 'n', 's', 't', 'a']
"insta" 라는 단어에서 글자를 하나씩 꺼내 리스트로 만들었어요. 문자열도 for 로 돌 수 있다는 걸 여기서 처음 확인하네요.
💡 한 줄 정리
리스트 컴프리헨션은 [표현식 for 항목 in 반복가능] 처럼 써요. 오른쪽 for 가 하나씩 꺼내고, 왼쪽 표현식이 무엇을 담을지 정해요. for + append 세 줄을 한 줄로 줄여줍니다.
🙋 학생 질문 — "그냥 for 써도 되는데, 왜 굳이 컴프리헨션을 배워요?"
좋은 질문이에요. 사실 for 로 써도 결과는 똑같아요. 그런데 "새 리스트를 만든다" 는 의도가 있을 때, 컴프리헨션은 그 의도를 한 줄로 또렷하게 보여줘요. 빈 리스트 만들고 append 하는 준비 과정이 사라지니까요.
파이썬을 쓰는 사람들 사이에선 "리스트를 새로 만들 땐 컴프리헨션" 이 거의 약속처럼 쓰여요. 그래서 남의 코드를 읽을 때도 이 모양을 알아야 술술 읽혀요.
다만 한 가지. 컴프리헨션이 항상 정답은 아니에요. 한 줄에 너무 복잡한 걸 욱여넣으면 오히려 for 보다 읽기 어려워지거든요. 이 균형은 오늘 마지막 Step 과 생각해볼 주제에서 다시 이야기할게요.
Step 2: "필요한 것만 골라내기" (조건 필터)
이제 한 걸음 더 나가볼게요. 지난 시간에 "좋아요 1000 넘는 사람만 세기" 같은 걸 for 와 if 로 했죠. 컴프리헨션에도 if 를 붙일 수 있어요. 바로 조건 필터 입니다.
# src/b_structures/b3_filter.py
likes = [120, 8500, 320, 1500, 90, 12000]
# 좋아요 1000 넘는 것만 골라내기 (if 필터)
popular = [like for like in likes if like > 1000]
print("인기 게시물 좋아요:", popular)
인기 게시물 좋아요: [8500, 1500, 12000]
for like in likes 뒤에 if like > 1000 을 붙였어요. "하나씩 꺼내되, 조건을 통과한 것만 담아라" 는 뜻이에요. 1000 이하인 120, 320, 90 은 걸러지고 통과한 셋만 남았죠.
모양을 다시 뜯어보면 이래요.
[ like for like in likes if like > 1000 ]
──┬── ───────┬──────── ──────┬──────
담을 값 하나씩 꺼내기 통과 조건
하나 더 연습해 봅시다. 1부터 10까지 중 짝수만 골라내기예요. range 와 % 도 지난 시간에 봤죠.
# 1~10 중 짝수만 골라내기
evens = [n for n in range(1, 11) if n % 2 == 0]
print("1~10 중 짝수:", evens)
1~10 중 짝수: [2, 4, 6, 8, 10]
💡 한 줄 정리
for 뒤에 if 조건 을 붙이면 통과한 것만 골라 담아요. [x for x in xs if 조건] 모양이죠. "거르기" 가 필요할 때 쓰는 컴프리헨션이에요.
🙋 학생 질문 — "거르는 게 아니라, 조건에 따라 다른 값을 담고 싶으면요?"
아주 날카로운 질문이에요. 방금 배운 if 는 "담을지 말지" 를 정하는 필터였어요. 그런데 "1000 넘으면 '인기', 아니면 '일반'" 처럼 조건에 따라 다른 값을 담고 싶을 때가 있죠. 이땐 if 의 위치가 달라져요.
# 삼항(조건식): 1000 넘으면 "인기", 아니면 "일반"
labels = ["인기" if like > 1000 else "일반" for like in likes]
print("등급 표시:", labels)
등급 표시: ['일반', '인기', '일반', '인기', '일반', '인기']
이번엔 if ... else 가 for 앞쪽, 그러니까 "담을 값" 자리에 왔어요. "인기" if 조건 else "일반" 은 "조건이 참이면 '인기', 거짓이면 '일반'" 이라는 한 줄짜리 갈림길이에요.
정리하면, for 뒤의 if 는 거르기(필터), for 앞의 if ... else 는 값 고르기예요. 위치로 역할이 갈린다는 것만 기억해 두면 돼요. 지금 헷갈려도 괜찮아요.
Step 3: "중괄호로 빚는 딕셔너리·집합" (dict / set 컴프리헨션)
컴프리헨션은 리스트만의 것이 아니에요. 지난 시간에 배운 딕셔너리와 집합도 한 줄로 빚어낼 수 있어요. 대괄호 [] 대신 중괄호 {} 를 쓰면 됩니다.
먼저 딕셔너리예요. 이름마다 "이름 길이" 를 값으로 갖는 딕셔너리를 만들어 봅시다.
# src/b_structures/b3_dict_set.py
names = ["jaehoon", "minji", "seungwoo"]
followers = [1240, 8500, 320]
# 딕셔너리 컴프리헨션 — 이름표: 값
name_lengths = {name: len(name) for name in names}
print("이름 길이:", name_lengths)
이름 길이: {'jaehoon': 7, 'minji': 5, 'seungwoo': 8}
리스트 컴프리헨션과 거의 같은데, 담을 값 자리에 name: len(name) 처럼 이름표: 값 한 쌍을 적었어요. 그래서 딕셔너리가 나오죠. 지난 시간에 배운 "이름표로 찾는 그릇" 을 한 줄로 만든 거예요.
이번엔 두 리스트를 짝지어 딕셔너리로 만들어 볼게요. 이름과 팔로워 수를 인덱스 번호로 맞춰서요.
# 두 리스트를 인덱스로 짝지어 딕셔너리로 (Step 7 zip 으로 더 깔끔해질 예정)
profile = {names[i]: followers[i] for i in range(len(names))}
print("프로필:", profile)
프로필: {'jaehoon': 1240, 'minji': 8500, 'seungwoo': 320}
range(len(names)) 로 0, 1, 2 번 인덱스를 돌면서 names[i] 를 이름표로, followers[i] 를 값으로 짝지었어요. 살짝 번거롭죠? 이 번거로움을 Step 7 에서 zip 이 깔끔하게 해결해 줄 거예요. 지금은 "이렇게도 된다" 만 봐두세요.
이제 집합 컴프리헨션이에요. 중괄호를 쓰되 이름표: 값 쌍이 아니라 값 하나만 적으면 집합이 돼요. 해시태그에서 # 를 떼면서 중복을 없애봅시다.
# 집합 컴프리헨션 — # 떼면서 중복 자동 제거
tags = ["#일상", "#여행", "#일상", "#맛집", "#여행"]
unique_tags = {tag.replace("#", "") for tag in tags}
print("중복 없는 해시태그:", unique_tags)
print("해시태그 종류 수:", len(unique_tags))
중복 없는 해시태그: {'맛집', '여행', '일상'}
해시태그 종류 수: 3
#일상 과 #여행 이 두 번씩 있었지만, 집합이라 중복이 자동으로 사라졌어요. 종류는 셋이죠.
⚠️ 출력에서
{'맛집', '여행', '일상'}의 순서는 실행할 때마다 달라질 수 있어요. 지난 시간에 배웠듯 집합은 순서가 없는 그릇이라, 파이썬이 내부적으로 담는 순서가 정해져 있지 않거든요. 종류와 개수는 똑같지만 나열 순서는 신경 쓰지 마세요.
💡 한 줄 정리
중괄호 {} 를 쓰면 딕셔너리·집합도 컴프리헨션으로 만들어요. {key: 값 for ...} 는 딕셔너리, {값 for ...} 는 집합이에요. 집합 컴프리헨션은 변환하면서 중복까지 없애줍니다.
🙋 학생 질문 — "집합 컴프리헨션이랑, 그냥 set(리스트) 랑 뭐가 달라요?"
좋은 비교예요. set(tags) 도 중복을 없애주죠. 둘의 차이는 "담으면서 바꿀 수 있느냐" 예요.
set(tags) 는 tags 안의 값을 그대로 담아요. #일상, #여행 처럼 # 가 붙은 채로요. 반면 집합 컴프리헨션 {tag.replace("#", "") for tag in tags} 는 담기 전에 # 를 떼는 변환을 한 번 거쳐요.
그래서 "중복만 없애면 된다" 면 set(리스트) 가 간단하고, "바꾸면서 중복도 없앤다" 면 컴프리헨션이 맞아요. 상황에 맞게 고르면 됩니다.
Step 4: "리스트 안의 리스트를 펼치기" (중첩 컴프리헨션)
지난 시간에 리스트 안의 리스트(중첩 리스트)를 다뤘던 거 기억하시죠? 회원별 게시물 좋아요를 이렇게 담았어요. 한 회원이 게시물을 여러 개 가지니까요.
# src/b_structures/b3_nested.py
# 피드: 회원별 게시물 좋아요 (리스트 안의 리스트)
feed = [
[120, 300, 90], # jaehoon 의 게시물 3개
[8500, 12000], # minji 의 게시물 2개
[320, 150, 400, 60], # seungwoo 의 게시물 4개
]
그런데 "회원 구분 없이, 모든 게시물의 좋아요를 한 줄로 쭉 펼치고 싶다" 고 해봅시다. 지난 시간엔 for 를 두 번 겹쳐서 했어요.
# 지난 시간 방식: for 두 번으로 펼치기
flat = []
for posts in feed:
for like in posts:
flat.append(like)
print("for 로 펼친 결과:", flat)
for 로 펼친 결과: [120, 300, 90, 8500, 12000, 320, 150, 400, 60]
바깥 for 는 회원 하나(게시물 리스트)를 꺼내고, 안쪽 for 는 그 안의 좋아요를 하나씩 꺼냈죠. 이 두 겹을 컴프리헨션 한 줄로 쓸 수 있어요.
# 중첩 컴프리헨션으로 한 줄에 펼치기 (왼쪽 for 가 바깥, 오른쪽 for 가 안쪽)
flat2 = [like for posts in feed for like in posts]
print("컴프리헨션으로 펼친 결과:", flat2)
print("전체 게시물 수:", len(flat2))
컴프리헨션으로 펼친 결과: [120, 300, 90, 8500, 12000, 320, 150, 400, 60]
전체 게시물 수: 9
여기서 핵심은 for 두 개를 쓰는 순서 예요. 위에서 for 를 겹쳐 쓴 순서 그대로, 왼쪽부터 적으면 돼요.
[ like for posts in feed for like in posts ]
───────┬──────── ───────┬────────
바깥 for (먼저) 안쪽 for (나중)
바깥 for posts in feed 를 먼저, 안쪽 for like in posts 를 나중에. 평소 for 를 겹쳐 쓰던 순서와 똑같으니, "위에서 아래로 쓰던 걸 왼쪽에서 오른쪽으로 눕혔다" 고 생각하면 편해요.
물론 펼치면서 거를 수도 있어요. Step 2 의 if 를 끝에 붙이면 되죠.
# 펼치면서 조건도 — 1000 넘는 것만
hits = [like for posts in feed for like in posts if like > 1000]
print("전체 중 인기 게시물:", hits)
전체 중 인기 게시물: [8500, 12000]
💡 한 줄 정리
중첩 컴프리헨션은 for 를 두 번 적어 리스트 안의 리스트를 한 줄로 펼쳐요. 왼쪽 for 가 바깥, 오른쪽 for 가 안쪽 이에요. 겹쳐 쓰던 순서 그대로 왼쪽부터 적으면 됩니다.
🙋 학생 질문 — "for 두 개 순서가 자꾸 헷갈려요. 외우는 법 없을까요?"
외우기보다, 평소 for 겹쳐 쓰던 모양을 떠올리는 게 좋아요. 이렇게요.
for posts in feed: → ... for posts in feed
for like in posts: → for like in posts
위에서 아래로 들여쓰며 쓰던 두 for 를, 그대로 왼쪽에서 오른쪽으로 눕히기만 하면 돼요. 바깥에 있던 게 왼쪽, 안에 있던 게 오른쪽. 순서를 바꾸면 posts 가 뭔지 모르는 채로 안쪽 for 가 먼저 나와서 에러가 나요.
그러니 "겹쳐 쓰던 순서대로 눕힌다" 만 기억하면 충분해요.
Step 5: "메모리를 아끼는 괄호" (제너레이터 표현식)
이번 Step 은 살짝 깊은 이야기예요. "아직 다 이해 못 해도 괜찮다" 는 마음으로 따라와 주세요.
지금까지 컴프리헨션은 대괄호 [] 로 리스트를 만들었죠. 그런데 괄호를 소괄호 () 로 바꾸면 어떻게 될까요? 좋아요의 합계를 구해봅시다.
# src/b_structures/b3_genexpr.py
likes = [120, 8500, 320, 1500, 90]
# 리스트 컴프리헨션: 결과를 모두 메모리에 쌓은 뒤 합산
total_list = sum([like for like in likes])
print("리스트로 합계:", total_list)
# 제너레이터 표현식: 괄호 () — 하나씩 꺼내 쓴다 (메모리 절약)
total_gen = sum(like for like in likes)
print("제너레이터로 합계:", total_gen)
리스트로 합계: 10530
제너레이터로 합계: 10530
결과는 똑같이 10530 이에요. 그런데 속에서 벌어지는 일은 달라요. 그림으로 보면 이래요.
리스트 컴프리헨션 [ ... ]
120, 8500, 320, 1500, 90 ← 다섯 개를 전부 메모리에 만들어 쌓아두고
그 다음 sum 이 더한다
제너레이터 표현식 ( ... )
120 → 더하고 버림
8500 → 더하고 버림 ← 하나 만들어 쓰고 바로 버린다
320 → 더하고 버림 (한꺼번에 쌓아두지 않음 = 메모리 절약)
...
리스트는 좋아요 다섯 개를 전부 메모리에 만들어 쌓은 뒤 더해요. 제너레이터는 하나 꺼내 더하고 버리고, 또 하나 꺼내 더하고 버려요. 다섯 개라면 차이가 없지만, 좋아요가 1억 개라면 리스트는 1억 개를 통째로 메모리에 쌓고 제너레이터는 하나씩만 다뤄요. 큰 데이터일수록 제너레이터가 메모리를 아끼는 거죠.
대신 제너레이터엔 독특한 성질이 하나 있어요. 한 번 쓰면 소진된다 는 거예요.
# 제너레이터는 한 번만 소진된다
gen = (like for like in likes)
print("제너레이터 객체:", gen)
print("처음 합계:", sum(gen))
print("두 번째 합계:", sum(gen)) # 이미 소진 → 0
제너레이터 객체: <generator object <genexpr> at 0x...>
처음 합계: 10530
두 번째 합계: 0
(0x... 부분은 메모리 주소라 실행할 때마다 다른 값이 나와요. 신경 쓰지 않아도 돼요.)
gen 을 그냥 출력하면 값이 아니라 "제너레이터 객체" 라고만 나와요. 아직 값을 안 꺼냈으니까요. 첫 sum(gen) 에서 하나씩 꺼내 다 더하면 10530 이 나오죠. 그런데 두 번째 sum(gen) 은 0 이에요. 이미 다 꺼내 써버려서, 남은 게 없거든요. 한 번 따라버린 그릇 같은 거예요.
sum 이나 max 처럼 "한 번 쭉 훑어 값 하나를 내는" 함수엔 제너레이터를 그냥 넣으면 깔끔해요. 굳이 리스트를 따로 만들 필요가 없거든요.
# max 에도 바로 넣을 수 있다
print("가장 많은 좋아요:", max(like for like in likes))
가장 많은 좋아요: 8500
💡 한 줄 정리
소괄호 () 로 쓰면 제너레이터 표현식이에요. 값을 한꺼번에 쌓지 않고 하나씩 꺼내 써서 메모리를 아껴요. 대신 한 번 쓰면 소진돼요. sum/max 에 바로 넣을 때 잘 어울립니다.
🙋 학생 질문 — "메모리를 아낀다면, 그냥 항상 제너레이터를 쓰면 좋은 거 아니에요?"
그럴 것 같지만, 아니에요. 제너레이터는 두 가지 약점이 있어요.
첫째, 방금 봤듯 한 번 쓰면 소진돼요. 같은 데이터를 두 번 훑어야 한다면 제너레이터는 두 번째에 텅 비어 있어요. 그땐 리스트가 맞아요.
둘째, 제너레이터는 번호로 꺼낼 수 없어요. 리스트는 likes[0] 처럼 콕 집을 수 있지만, 제너레이터는 "처음부터 하나씩" 만 돼요.
그래서 기준은 이래요. "한 번 쭉 훑어 합계·최댓값 같은 걸 낸다" → 제너레이터. "여러 번 쓰거나, 번호로 꺼내거나, 결과를 저장해 둔다" → 리스트. 큰 데이터를 한 번만 훑을 때 제너레이터가 빛나요.
Step 6: "번호와 함께 돌기" (enumerate)
여기서부터는 컴프리헨션에서 잠깐 벗어나, for 를 더 똑똑하게 도는 도구 두 개를 배워요. 첫 번째는 enumerate 예요.
게시물 목록을 순서대로 출력한다고 해봅시다. "1번 게시물, 2번 게시물..." 처럼 번호를 붙이고 싶어요. 지난 시간 방식이라면 번호 변수를 따로 만들어 직접 세야 했죠.
# src/b_structures/b3_enumerate.py
posts = ["첫 게시물", "여행 사진", "맛집 후기"]
# 지난 시간 방식: 번호를 직접 세야 했다
i = 0
for post in posts:
print(i, post)
i += 1
0 첫 게시물
1 여행 사진
2 맛집 후기
i = 0 으로 시작해서 매번 i += 1 로 직접 올렸어요. 번거롭고, 깜빡하면 번호가 안 올라가는 실수도 생기죠. enumerate 를 쓰면 번호를 알아서 함께 줘요.
# enumerate: 번호(index)와 값을 함께 돈다
for idx, post in enumerate(posts):
print(idx, post)
0 첫 게시물
1 여행 사진
2 맛집 후기
enumerate(posts) 는 돌 때마다 (번호, 값) 을 짝으로 줘요. 그래서 for idx, post in ... 처럼 두 변수로 받죠. 지난 시간에 배운 "튜플 언패킹" 이 여기서 또 쓰이네요. 번호 변수를 직접 관리할 필요가 사라졌어요.
그런데 번호가 0 부터 시작하죠? 사람에겐 "1위, 2위" 가 자연스러우니, 시작 번호를 1 로 바꿔봅시다.
# start=1 로 1번부터 매기기
for rank, post in enumerate(posts, start=1):
print(f"{rank}위: {post}")
1위: 첫 게시물
2위: 여행 사진
3위: 맛집 후기
enumerate(posts, start=1) 처럼 start=1 을 주면 1 부터 세요. 순위표나 목록 번호를 매길 때 딱이에요.
💡 한 줄 정리
enumerate 는 for 를 돌 때 번호와 값을 함께 줘요. for 번호, 값 in enumerate(목록) 모양이죠. 번호 변수를 직접 세던 수고를 없애주고, start=1 로 시작 번호도 바꿀 수 있어요.
🙋 학생 질문 — "enumerate 없이 range(len(posts)) 써도 되지 않아요?"
맞아요, 됩니다. for i in range(len(posts)): 로 돌면서 posts[i] 로 값을 꺼낼 수 있어요. 결과도 똑같고요.
그런데 그 방식은 두 단계예요. 번호 i 를 받고, 다시 posts[i] 로 값을 꺼내야 하죠. enumerate 는 번호와 값을 한 번에 줘서 posts[i] 단계가 사라져요. 코드가 더 짧고, "지금 이 값이 뭔지" 가 바로 보여요.
파이썬을 쓰는 사람들은 "번호와 값이 둘 다 필요하면 enumerate" 를 자연스러운 약속처럼 써요. range(len(...)) 도 틀린 건 아니지만, enumerate 가 더 파이썬다운 표현이에요.
Step 7: "두 리스트를 짝지어 돌기" (zip)
두 번째 도구는 zip 이에요. 이름이 옷의 지퍼(zipper)에서 왔어요. 지퍼가 양쪽 이를 하나씩 맞물리듯, 두 리스트를 나란히 짝지어 돌아요.
이름 리스트와 팔로워 수 리스트가 따로 있다고 해봅시다. "@이름: 팔로워 N명" 으로 함께 출력하고 싶어요.
# src/b_structures/b3_zip.py
names = ["jaehoon", "minji", "seungwoo"]
followers = [1240, 8500, 320]
# zip: 두 리스트를 짝지어 함께 돈다
for name, count in zip(names, followers):
print(f"@{name}: 팔로워 {count}명")
@jaehoon: 팔로워 1240명
@minji: 팔로워 8500명
@seungwoo: 팔로워 320명
zip(names, followers) 가 두 리스트에서 같은 번째끼리 짝지어 줘요. 첫 번째는 ("jaehoon", 1240), 두 번째는 ("minji", 8500) 이런 식이죠. 그래서 for name, count in ... 로 두 변수에 나눠 받아요. Step 3 에서 인덱스로 번거롭게 했던 일을 zip 이 깔끔하게 해주네요.
그래서 두 리스트를 딕셔너리로 만들 때 zip 이 빛나요. 지난 시간 마지막에 예고했던 거예요.
# 두 리스트를 딕셔너리로 (Step 3 인덱스 방식보다 깔끔)
profile = dict(zip(names, followers))
print("프로필 딕셔너리:", profile)
프로필 딕셔너리: {'jaehoon': 1240, 'minji': 8500, 'seungwoo': 320}
dict(zip(names, followers)) 한 줄로 끝났어요. Step 3 의 {names[i]: followers[i] for i in range(len(names))} 와 결과는 같지만 훨씬 읽기 쉽죠.
하나 주의할 점. 두 리스트의 길이가 다르면 어떻게 될까요?
# 길이가 다르면 짧은 쪽에 맞춰 멈춘다
tags = ["#일상", "#여행"]
likes = [120, 8500, 320]
for tag, like in zip(tags, likes):
print(tag, like)
#일상 120
#여행 8500
tags 는 2개, likes 는 3개인데, 짧은 쪽(2개)에 맞춰 멈춰요. 남은 320 은 짝이 없어 버려지죠. 짝이 안 맞으면 짧은 쪽 기준이라는 것만 기억해 두세요.
💡 한 줄 정리
zip 은 두 리스트를 같은 번째끼리 짝지어 함께 돌아요. for a, b in zip(리스트1, 리스트2) 모양이죠. dict(zip(...)) 로 두 리스트를 딕셔너리로 묶을 수 있고, 길이가 다르면 짧은 쪽에 맞춰 멈춰요.
🙋 학생 질문 — "zip(names, followers) 를 그냥 print 하면 왜 값이 안 보여요?"
직접 해보면 이상한 게 나와요. <zip object at 0x...> 같은 거요. 값이 안 보이죠.
이유는 Step 5 의 제너레이터와 똑같아요. zip 도 "미리 다 만들어두지 않고, 필요할 때 하나씩 짝지어 주는" 게으른 도구거든요. 그래서 for 로 돌거나, list()/dict() 로 풀어줘야 비로소 값이 보여요.
list(zip(names, followers)) 로 풀면 [('jaehoon', 1240), ('minji', 8500), ('seungwoo', 320)] 처럼 짝(튜플)들의 리스트가 나오고, dict(zip(...)) 으로 풀면 딕셔너리가 돼요. "zip 결과는 풀어줘야 보인다" 만 기억하면 됩니다.
Step 8: "해시태그 추출기 만들기" (종합)
자, 드디어 마지막이에요. 오늘 배운 도구를 전부 동원해서 작은 해시태그 추출기 를 만들어 봅시다. 게시물 캡션 여러 개에서 해시태그만 쏙 뽑아내는 프로그램이에요. 인스타그램이 실제로 하는 일이죠.
먼저 캡션들이 이렇게 있다고 해요.
# src/b_structures/b3_extractor.py
# 게시물 캡션들
captions = [
"오늘 날씨 좋다 #일상 #날씨",
"제주도 여행 #여행 #제주 #일상",
"맛집 발견 #맛집 #여행",
]
각 캡션을 단어로 쪼개고(split), 그중 # 로 시작하는 단어만 모으고 싶어요. 캡션이 여러 개니까 중첩 컴프리헨션(Step 4)에 필터(Step 2)를 붙이면 한 줄로 됩니다.
# 1) 각 캡션을 단어로 쪼개 # 로 시작하는 것만 모으기 (split + 중첩 컴프리헨션 + 필터)
all_tags = [word for caption in captions for word in caption.split() if word.startswith("#")]
print("뽑아낸 해시태그 전체:", all_tags)
뽑아낸 해시태그 전체: ['#일상', '#날씨', '#여행', '#제주', '#일상', '#맛집', '#여행']
한 줄에 많은 게 담겼죠? 천천히 읽어볼게요. 바깥 for caption in captions 로 캡션을 하나 꺼내요. 그 안의 for word in caption.split() 로 캡션의 단어를 하나씩 꺼내고요. 마지막으로 if word.startswith("#") 로 # 로 시작하는 것만 걸렀어요.
(startswith 는 "이 글자로 시작하니?" 를 묻는 문자열 메서드예요.)
그런데 #일상, #여행 이 여러 번 나오죠. 종류만 세려면 집합 컴프리헨션(Step 3)으로 # 를 떼며 중복을 없애요.
# 2) 중복 없는 해시태그 종류 (set 컴프리헨션 + # 떼기)
unique = {tag.replace("#", "") for tag in all_tags}
print("해시태그 종류:", unique)
print("종류 수:", len(unique))
해시태그 종류: {'여행', '날씨', '제주', '일상', '맛집'}
종류 수: 5
(집합이라 종류의 나열 순서는 실행마다 달라질 수 있어요. 개수 5 는 그대로고요.)
여기에 좋아요 필터(Step 2)도 곁들여 볼게요. 게시물별 좋아요에서 1000 넘는 인기글만 골라요.
# 3) 좋아요 필터 — 1000 넘는 인기글만
post_likes = [120, 8500, 320]
popular = [like for like in post_likes if like > 1000]
print("인기 게시물 좋아요:", popular)
인기 게시물 좋아요: [8500]
마지막으로 게시물마다 번호를 붙여, 그 게시물의 태그를 따로 보여줄게요. enumerate(Step 6)가 나설 차례예요.
# 4) 게시물 번호와 그 게시물의 태그를 함께 (enumerate)
for num, caption in enumerate(captions, start=1):
tags_in_post = [w for w in caption.split() if w.startswith("#")]
print(f"{num}번 게시물 태그: {tags_in_post}")
1번 게시물 태그: ['#일상', '#날씨']
2번 게시물 태그: ['#여행', '#제주', '#일상']
3번 게시물 태그: ['#맛집', '#여행']
오늘 배운 게 한 프로그램에서 손발을 맞췄어요. 중첩 컴프리헨션으로 펼치고, 필터로 거르고, 집합으로 중복을 없애고, enumerate 로 번호를 붙였죠. 작지만 진짜 인스타그램이 하는 일의 축소판이에요.
💡 한 줄 정리
해시태그 추출기 하나에 오늘 배운 컴프리헨션·필터·집합·enumerate 가 모두 들어갔어요. 여러 도구를 엮어 "캡션에서 해시태그를 뽑고 정리하는" 실제 작업을 한 거예요.
🙋 학생 질문 — "이걸 전부 한 줄에 욱여넣으면 더 멋있지 않아요?"
기술적으로는 가능해요. 추출하고, 중복 없애고, 세는 걸 한 줄에 다 넣을 수도 있죠. 그런데 그게 정말 좋을까요?
지난 시간 마지막 생각거리에서 던졌던 질문이 바로 이거예요. "한 줄로 줄이면 항상 좋을까?" 답은 "아니오" 예요. 한 줄에 for 두 개, if 하나, replace 까지 욱여넣으면, 한 달 뒤 그 코드를 읽는 사람(미래의 나 포함)이 한참을 들여다봐야 해요.
좋은 기준은 이거예요. "한 줄을 읽고 무슨 일을 하는지 한눈에 들어오면" 컴프리헨션, "고개를 갸웃하게 되면" 여러 줄로 풀어쓰기. 짧은 코드가 목표가 아니라, 읽기 쉬운 코드가 목표예요. 컴프리헨션은 그걸 돕는 도구지, 짧게 만들기 시합이 아니에요.
마무리
오늘은 for 로 또박또박 만들던 걸 컴프리헨션 한 줄로 빚어내는 법을 배웠어요. 리스트·딕셔너리·집합을 한 줄로 만들고, if 로 거르고, 제너레이터로 메모리를 아끼고, enumerate·zip 으로 더 똑똑하게 돌았죠. 카테고리 B(자료구조)의 마지막 모듈을 잘 마쳤어요. 정말 고생 많으셨어요.
오늘 배운 핵심 세 가지
💡 하나: 컴프리헨션은 [표현식 for x in xs] 처럼 써서 for 여러 줄을 한 줄로 줄여요. 뒤에 if 조건 을 붙이면 거르기(필터), 중괄호 {} 를 쓰면 딕셔너리·집합도 만들어요. for 를 두 번 적으면 리스트 안의 리스트를 펼치고요.
💡 둘: 소괄호 () 로 쓰면 제너레이터 표현식이에요. 값을 한꺼번에 쌓지 않고 하나씩 꺼내 써서 큰 데이터에서 메모리를 아껴요. 대신 한 번 쓰면 소진되니, 여러 번 쓸 거면 리스트를 쓰세요.
💡 셋: enumerate 는 번호와 함께 돌고, zip 은 두 리스트를 짝지어 돌아요. dict(zip(...)) 으로 두 리스트를 딕셔너리로 묶을 수 있죠. 이 둘은 for 를 한층 날렵하게 만들어줘요.
다음 시간 예고
오늘로 자료구조(B)를 마치고, 다음 시간부터 함수(C-1) 로 넘어갑니다.
생각해 보면 우리는 비슷한 코드를 자꾸 반복해서 썼어요. "캡션에서 해시태그 뽑기" 를 Step 8 에서 두 번이나 짰죠. 만약 이걸 이름 붙여 묶어두고, 필요할 때마다 그 이름만 부를 수 있다면 어떨까요? 해시태그_뽑기(캡션) 처럼요.
그게 바로 함수 예요. 다음 시간엔 def 라는 키워드로 나만의 명령을 만들어, 같은 코드를 두 번 쓰지 않는 법을 배웁니다. 우리가 지금까지 써온 print, len, sum, sorted 같은 것들도 사실 다 함수였거든요. 이제 그걸 직접 만들어 볼 차례예요. 기대해 주세요!
과제
오늘 배운 컴프리헨션을 직접 손으로 익혀봅시다. 눈으로 본 것과 직접 짜보는 것은 완전히 다르답니다.
[기초] 좋아요 컴프리헨션으로 다루기
좋아요 숫자 리스트를 컴프리헨션으로 이리저리 빚어보세요.
- 좋아요 리스트(예:
[120, 8500, 320, 1500, 90, 12000])를 하나 준비합니다 - 컴프리헨션으로 모든 좋아요에 1을 더한 새 리스트를 만들어 출력합니다
if필터로 1000 넘는 인기글의 좋아요만 골라 출력합니다- 삼항(
"인기" if ... else "일반")으로 각 좋아요에 등급 라벨을 붙인 리스트를 만들어 봅니다 len()으로 인기글이 몇 개인지 함께 세어 출력해 보세요
[응용] 해시태그 추출기 만들기
오늘 Step 8 을 직접 손으로 다시 만들어 보세요.
- 캡션 문자열 세 개 정도를 리스트에 담습니다 (각 캡션에
#태그를 두세 개 넣어 보세요) - 중첩 컴프리헨션과 필터로, 모든 캡션에서
#로 시작하는 단어만 뽑아 리스트로 만듭니다 - 집합 컴프리헨션으로
#를 떼면서 중복 없는 해시태그 종류를 만들고, 종류가 몇 개인지 셉니다 - (도전) 전체 해시태그가 몇 개인지(중복 포함)와 종류가 몇 개인지(중복 제외)를 함께 출력해, 두 숫자의 차이를 비교해 보세요
[심화] 두 리스트로 프로필 만들기
zip·enumerate·제너레이터를 엮어 회원 데이터를 다뤄 보세요.
- 이름 리스트와 팔로워 수 리스트를 각각 준비합니다 (네 명 정도)
dict(zip(...))으로 두 리스트를 "이름: 팔로워 수" 딕셔너리로 묶어 출력합니다enumerate(..., start=1)로 회원마다 번호를 붙여 "1번 회원: @이름" 형태로 출력합니다- 제너레이터 표현식과
sum으로 전체 팔로워 합계를 구합니다 (리스트를 따로 만들지 말고요) - (도전)
zip과 필터를 엮어 팔로워 5000 넘는 인플루언서의 이름만 골라 출력해 보세요
생각해볼 주제
정답이 하나로 정해진 질문이 아니에요. 혼자 곰곰이 생각해 보거나, 함께 공부하는 동료와 이야기 나눠 보면 좋습니다.
1. 짧은 코드가 항상 좋은 코드일까?
오늘 우리는 for 서너 줄을 컴프리헨션 한 줄로 줄였어요. 코드가 짧아지면 보통 좋다고들 하죠.
그런데 Step 8 처럼 한 줄에 for 두 개, if 하나, replace 까지 들어가면 어떨까요? 짧긴 한데, 한눈에 무슨 일을 하는지 들어오나요? 어디까지 한 줄로 줄이는 게 좋고, 어디서부터는 여러 줄로 풀어쓰는 게 나을지 생각해 보세요. "짧다" 와 "읽기 쉽다" 가 항상 같은 말일까요?
2. 리스트와 제너레이터, 언제 무엇을 고를까?
같은 데이터를 리스트 컴프리헨션 [...] 으로도, 제너레이터 표현식 (...) 으로도 만들 수 있어요. 결과는 같아 보여도 속은 달랐죠.
제너레이터는 메모리를 아끼지만 한 번 쓰면 소진되고, 번호로 꺼낼 수도 없어요. 어떤 상황이 "한 번만 쭉 훑으면 되는" 경우이고, 어떤 상황이 "여러 번 쓰거나 저장해 둬야 하는" 경우일지 떠올려 보세요. 좋아요 1억 개의 합계를 구할 땐 어느 쪽이 맞을까요?
3. enumerate 와 zip 이 없던 시절엔 어떻게 했을까?
enumerate 없이도 range(len(...)) 로 번호를 만들 수 있고, zip 없이도 인덱스로 두 리스트를 짝지을 수 있어요. 실제로 Step 3 에서 우리는 인덱스로 딕셔너리를 만들었죠.
그런데도 파이썬은 enumerate 와 zip 을 따로 두었어요. 왜 굳이 이런 도구를 만들었을까요? "되긴 되는데 번거로운" 것과 "도구가 따로 있는" 것의 차이가, 코드를 읽는 사람에게 어떤 영향을 줄지 생각해 보세요.
✅ 예시 답안정답 보기
과제와 생각해볼 주제의 예시답안이에요. 먼저 스스로 충분히 손으로 짜본 뒤에 펼쳐 보세요. 정답을 외우는 게 아니라, "내가 짠 것과 어디가 다른지" 를 비교하는 게 진짜 공부예요.
🎯 [과제 1 예시답안] 좋아요 컴프리헨션으로 다루기
채점 포인트
| 항목 | 확인 | 핵심 |
|---|---|---|
| 변환 컴프리헨션 | [like + 1 for like in likes] |
for + append 대신 한 줄로 |
| 조건 필터 | [like for like in likes if like > 1000] |
for 뒤에 if 가 거르기 |
| 삼항 조건식 | ["인기" if like > 1000 else "일반" for like in likes] |
for 앞의 if ... else 는 값 고르기 |
| 개수 세기 | len(popular) |
필터 결과의 길이 |
풀이 예시
# src/b_structures/b3_likes_filter_solution.py
# [기초] 좋아요 다루기 — 컴프리헨션으로 변환·필터·라벨
likes = [120, 8500, 320, 1500, 90, 12000]
# 1) 모두 1씩 증가시킨 새 리스트
plus_one = [like + 1 for like in likes]
print("좋아요 +1:", plus_one)
# 2) 1000 넘는 인기글만 골라내기
popular = [like for like in likes if like > 1000]
print("인기글 좋아요:", popular)
# 3) 등급 라벨 붙이기 (삼항)
labels = ["인기" if like > 1000 else "일반" for like in likes]
print("등급:", labels)
# 4) 인기글이 몇 개인지
print("인기글 개수:", len(popular))
실행 결과예요.
좋아요 +1: [121, 8501, 321, 1501, 91, 12001]
인기글 좋아요: [8500, 1500, 12000]
등급: ['일반', '인기', '일반', '인기', '일반', '인기']
인기글 개수: 3
💡 튜터의 한마디: 이 과제의 핵심은 if 의 위치 두 가지를 구분하는 거예요. for 뒤에 오면 거르기(필터), for 앞에 오면 값 고르기(삼항)였죠. 둘을 헷갈리면 "걸러내고 싶었는데 라벨이 붙는" 식으로 결과가 달라져요.
라벨 결과를 보면 걸러지지 않고 여섯 개가 그대로 등급만 받았죠? 이게 삼항이 "거르지 않는다" 는 증거예요.
🎯 [과제 2 예시답안] 해시태그 추출기 만들기
채점 포인트
| 항목 | 확인 | 핵심 |
|---|---|---|
| 중첩 컴프리헨션 | for caption in captions for word in caption.split() |
왼쪽이 바깥, 오른쪽이 안쪽 |
# 필터 |
if word.startswith("#") |
해시태그만 통과 |
| 집합 컴프리헨션 | {tag.replace("#", "") for tag in tags} |
변환 + 중복 제거 |
| 두 숫자 비교 | len(tags) vs len(unique) |
중복 포함 vs 제외 |
풀이 예시
# src/b_structures/b3_hashtag_solution.py
# [응용] 해시태그 추출기 — 중첩 컴프리헨션 + 필터 + set 중복 제거
captions = [
"오늘 #일상 #브이로그",
"여행 가자 #여행 #일상 #제주",
"#맛집 탐방 #여행",
]
# 모든 캡션에서 해시태그만 추출 (바깥 for = 캡션, 안쪽 for = 단어)
tags = [word for caption in captions for word in caption.split() if word.startswith("#")]
print("전체 해시태그:", tags)
print("전체 개수:", len(tags))
# 중복 없는 종류 (set 컴프리헨션, # 떼기)
unique = {tag.replace("#", "") for tag in tags}
print("해시태그 종류:", unique)
print("종류 수:", len(unique))
실행 결과예요.
전체 해시태그: ['#일상', '#브이로그', '#여행', '#일상', '#제주', '#맛집', '#여행']
전체 개수: 7
해시태그 종류: {'제주', '여행', '브이로그', '일상', '맛집'}
종류 수: 5
집합 해시태그 종류 의 나열 순서는 실행할 때마다 달라질 수 있어요. 종류 수 5 는 그대로고요.
💡 튜터의 한마디: 전체 개수 7 과 종류 수 5 의 차이가 핵심이에요. #일상 과 #여행 이 두 번씩 나와서, 중복을 빼면 둘이 줄어든 거죠. "전체 몇 번 등장했나" 는 리스트(중복 유지)로, "몇 종류인가" 는 집합(중복 제거)으로 센다는 걸 한 프로그램에서 본 거예요. 어떤 그릇을 쓰느냐가 곧 "무엇을 세느냐" 를 정합니다.
🎯 [과제 3 예시답안] 두 리스트로 프로필 만들기
채점 포인트
| 항목 | 확인 | 핵심 |
|---|---|---|
dict(zip(...)) |
두 리스트를 딕셔너리로 | 인덱스 없이 짝짓기 |
enumerate(start=1) |
1번부터 번호 매기기 | 번호 변수 직접 관리 안 함 |
제너레이터 + sum |
sum(f for f in followers) |
리스트를 따로 안 만듦 |
zip + 필터 |
[name for name, count in zip(...) if count > 5000] |
짝지으며 거르기 |
풀이 예시
# src/b_structures/b3_profile_solution.py
# [심화] 프로필 만들기 — zip 으로 dict, enumerate 순위, 제너레이터 합계
names = ["jaehoon", "minji", "seungwoo", "yuna"]
followers = [1240, 8500, 320, 5600]
# 1) 두 리스트를 딕셔너리로
profile = dict(zip(names, followers))
print("프로필:", profile)
# 2) 순위 매기기 (enumerate, 1번부터)
for rank, name in enumerate(names, start=1):
print(f"{rank}번 회원: @{name}")
# 3) 전체 팔로워 합계 (제너레이터 표현식 — 리스트를 따로 안 만든다)
total = sum(follower for follower in followers)
print("전체 팔로워 합계:", total)
# 4) 팔로워 5000 넘는 인플루언서만 (zip + 필터)
influencers = [name for name, count in zip(names, followers) if count > 5000]
print("인플루언서:", influencers)
실행 결과예요.
프로필: {'jaehoon': 1240, 'minji': 8500, 'seungwoo': 320, 'yuna': 5600}
1번 회원: @jaehoon
2번 회원: @minji
3번 회원: @seungwoo
4번 회원: @yuna
전체 팔로워 합계: 15660
인플루언서: ['minji', 'yuna']
💡 튜터의 한마디: 4번 항목이 오늘 배운 걸 가장 잘 엮어요. zip(names, followers) 가 이름과 팔로워를 짝지어 (name, count) 로 주고, for name, count in ... 로 받아, if count > 5000 으로 거르고, name 만 담았죠.
zip(짝짓기) + 필터(거르기) + 컴프리헨션(담기)이 한 줄에서 협력해요. 8500 인 minji 와 5600 인 yuna 만 통과했네요.
🤔 [생각해볼 주제 1] 짧은 코드가 항상 좋은 코드일까?
문제 상황 요약
for 서너 줄을 컴프리헨션 한 줄로 줄이면 분명 깔끔해져요. 그런데 Step 8 처럼 한 줄에 for 두 개, if 하나, replace 까지 들어가면, 짧긴 한데 한눈에 안 들어와요. "줄 수가 적다" 와 "읽기 쉽다" 는 같은 말일까요?
튜터의 가이드 및 해설
결론부터 말하면, 짧음은 목표가 아니라 읽기 쉬움의 부산물일 때만 좋아요.
컴프리헨션이 빛나는 경우는 "한 가지 일" 을 할 때예요. "좋아요에 1 더하기", "1000 넘는 것만 거르기" 처럼요. 이런 건 한 줄로 읽으면 의도가 바로 들어와요. for + 빈 리스트 + append 의 준비 과정이 사라지니 오히려 더 또렷하죠.
반대로 한 줄에 변환·필터·중첩이 두세 개씩 겹치면, 읽는 사람이 머릿속에서 그걸 다시 여러 줄로 풀어야 해요. 그 순간 "짧게 쓴 대가" 를 읽는 사람이 치르는 거예요. 코드는 한 번 쓰고 여러 번 읽혀요. 쓰는 1 분을 아끼려고 읽는 사람의 5 분을 빼앗으면 손해죠.
좋은 기준은 "한 줄을 소리 내 읽었을 때 무슨 일을 하는지 한 문장으로 말할 수 있는가" 예요. 말할 수 있으면 컴프리헨션, 말하다 막히면 여러 줄로 풀어쓰기. 실무에서도 "중첩 컴프리헨션은 두 겹까지만, 그 이상은 for 로" 같은 약속을 두는 팀이 많아요.
💡 핵심을 한마디로
"컴프리헨션은 짧게 만들기 시합이 아니라, 의도를 또렷하게 만드는 도구예요. 한 줄이 한 문장으로 읽히면 쓰고, 갸웃하게 되면 풀어쓰는 게 좋아요."
🤔 [생각해볼 주제 2] 리스트와 제너레이터, 언제 무엇을 고를까?
문제 상황 요약
같은 데이터를 리스트 컴프리헨션 [...] 으로도, 제너레이터 표현식 (...) 으로도 만들 수 있어요. 결과는 같아 보이는데 속은 달랐죠. 둘은 언제 어느 쪽을 골라야 할까요?
튜터의 가이드 및 해설
핵심은 "그 데이터를 몇 번, 어떻게 쓸 거냐" 예요.
제너레이터는 값을 한꺼번에 쌓지 않고 하나씩 꺼내 써서 메모리를 아껴요. 그래서 "한 번 쭉 훑어 결과 하나를 내는" 일에 딱이에요. 좋아요 1억 개의 합계나 최댓값을 구한다면, 리스트는 1억 개를 통째로 메모리에 올리지만 제너레이터는 하나씩만 다뤄요. 이땐 제너레이터가 확실히 유리하죠.
하지만 제너레이터엔 두 약점이 있었어요. 첫째, 한 번 쓰면 소진돼요. 같은 데이터로 합계도 구하고 평균도 구하려면, 제너레이터는 두 번째 작업 때 텅 비어 있어요. 둘째, 번호로 꺼낼 수 없어요. gen[0] 같은 게 안 돼요.
그래서 기준은 이래요. "한 번만 쭉 훑는다 + 큰 데이터" 면 제너레이터. "여러 번 쓴다 + 번호로 꺼낸다 + 저장해 둔다" 면 리스트. 작은 데이터(몇 개~몇백 개)라면 메모리 차이가 거의 없으니, 그냥 다루기 편한 리스트를 써도 괜찮아요. 도구의 성질을 알고 고르는 것, 그게 오늘의 진짜 수확이에요.
💡 핵심을 한마디로
"제너레이터는 '큰 데이터를 한 번만 훑을 때' 빛나요. 여러 번 쓰거나 번호로 꺼내야 하면 리스트예요. 메모리 절약과 재사용 가능성을 맞바꾸는 선택이죠."
🤔 [생각해볼 주제 3] enumerate 와 zip 이 없던 시절엔 어떻게 했을까?
문제 상황 요약
enumerate 없이도 range(len(...)) 로 번호를 만들 수 있고, zip 없이도 인덱스로 두 리스트를 짝지을 수 있어요. 실제로 Step 3 에서 인덱스로 딕셔너리를 만들었죠. 그런데도 파이썬은 왜 이 도구들을 따로 두었을까요?
튜터의 가이드 및 해설
"되긴 되는" 것과 "도구가 따로 있는" 것의 차이는, 코드를 읽는 사람 에게서 갈려요.
for i in range(len(names)): 를 쓰면, 코드를 읽는 사람은 "i 는 번호구나" 를 알고, 다시 names[i] 를 보고 "아 값을 꺼내는구나" 를 이해해야 해요. 두 단계의 해석이 필요하죠. 게다가 names[i] 를 names[j] 로 잘못 쓰는 실수도 끼어들 틈이 생겨요.
enumerate 와 zip 은 그 틈을 없애요. for rank, name in enumerate(names) 는 "번호와 이름을 함께 받는다" 가 한눈에 보여요. for name, count in zip(names, followers) 는 "이름과 팔로워를 짝지어 받는다" 가 바로 읽히고요.
인덱스라는 중간 단계가 사라지니, 읽는 사람은 "지금 다루는 값" 에만 집중할 수 있어요.
이게 파이썬의 철학과 닿아 있어요. "사람이 읽기 좋은 코드" 를 위해, 자주 쓰는 패턴엔 전용 도구를 둬요. range(len(...)) 가 틀린 건 아니지만, 더 또렷한 표현이 있으면 그걸 쓰는 게 파이썬다움이에요. 도구를 아는 것에서 그치지 않고, "왜 이 도구가 있는가" 를 물으면 언어를 보는 눈이 깊어져요.
💡 핵심을 한마디로
"enumerate 와 zip 은 인덱스라는 중간 단계를 없애 의도를 또렷하게 만들어요. '되긴 되는' 코드와 '한눈에 읽히는' 코드의 차이가 바로 이런 전용 도구에서 나와요."