B-2: 딕셔너리와 집합 — 이름표로 찾고, 중복 없이 모으기
목차 29
안녕하세요! 여러분의 파이썬 가이드, 홍순구 튜터입니다.
지난 시간엔 여러 값을 리스트([])라는 한 그릇에 담고, 번호([0], [1])로 꺼냈죠.
마지막에 제가 이런 질문을 던졌어요. "재훈이의 팔로워 수가 리스트의 몇 번 자리인지, 번호로 기억하는 게 편할까요?"
게시물이 100개라면 87번 자리에 뭐가 있는지 번호로 외우는 건 너무 힘들어요.
오늘은 번호 대신 이름표(key) 로 찾는 그릇, 딕셔너리(dictionary) 를 배웁니다.
프로필["팔로워수"] 처럼, 사전(dictionary)에서 단어로 뜻을 찾듯이 이름으로 값을 꺼내요.
그리고 "중복 없는 모음" 인 집합(set) 으로, 두 사람의 팔로우 관계에서 맞팔(교집합) 을 한 줄에 골라내는 것도 해봅니다.
오늘 우리는 이런 길을 따라갑니다.
오늘의 여정 — B-2 (총 8 단계)
1. 딕셔너리 만들기 이름표로 찾기
2. 더하고 고치고 빼기 값을 수정·추가·삭제
3. get 과 in 없는 이름표를 불러도 안전하게
4. 통째로 훑기 keys · values · items
5. 그릇 조합하기 딕셔너리 여러 개를 리스트에
6. 집합 만들기 중복 없는 모음
7. 집합 연산 맞팔은 교집합 (& | -)
8. 종합: 프로필 관리 딕셔너리 + 맞팔 한데 모아
앞쪽(1~5)은 딕셔너리, 뒤쪽(6~8)은 집합과 종합이에요. 한꺼번에 외우려 하지 말고, 직접 실행해 보면서 하나씩 익혀가요.
💡 오늘 수업의 핵심 — "이름표(key)로 값을 찾는 딕셔너리({})와, 중복 없이 모아 교집합·합집합을 구하는 집합(set)"
🎯 학습 목표
- 딕셔너리로 이름표(key)와 값(value)을 짝지어 담고, 이름표로 값을 꺼낼 수 있다
get/in으로 안전하게 접근하고,keys/values/items로 전체를 훑을 수 있다- 집합(set)으로 중복을 없애고, 교집합·합집합·차집합으로 두 모임을 비교할 수 있다
- 딕셔너리와 리스트를 조합해 회원 명부 같은 데이터를 다룰 수 있다
Step 1: "이름표로 찾기 — 딕셔너리 만들기"
지난 시간 리스트는 값을 번호 로 찾았어요. profile_list[1] 처럼요.
그런데 [1] 이 팔로워 수인지 게시물 수인지, 번호만 봐선 알 수 없죠.
"재훈이의 팔로워 수" 처럼 이름으로 찾을 수 있다면 훨씬 편할 거예요.
이럴 때 쓰는 게 딕셔너리(dictionary) 예요. 우리말로 "사전" 이죠.
사전에서 단어(이름표)로 뜻(값)을 찾듯이, 딕셔너리는 이름표(key) 로 값(value) 을 찾습니다.
중괄호 {} 안에 "이름표": 값 짝을 쉼표로 늘어놓으면 돼요.
코드부터 실행해 봅시다.
# src/b_structures/b2_create.py
# 딕셔너리: 번호 대신 '이름표(key)' 로 값을 찾는 그릇
# 지난 시간 리스트는 번호(0, 1, 2)로 찾았다
profile_list = ["jaehoon", 8500, 42]
print(profile_list[1]) # 8500 — 근데 1번이 뭐였더라?
# 딕셔너리는 '이름표' 로 찾는다 — 사전에서 단어로 뜻 찾듯이
profile = {
"name": "jaehoon",
"followers": 8500,
"posts": 42,
}
print(profile)
print(type(profile)) # <class 'dict'>
# 이름표(key)로 값(value) 꺼내기
print(profile["name"]) # jaehoon
print(profile["followers"]) # 8500
▶ 버튼으로 실행하거나 터미널에서 uv run python src/b_structures/b2_create.py 를 입력하면 이렇게 나와요.
8500
{'name': 'jaehoon', 'followers': 8500, 'posts': 42}
<class 'dict'>
jaehoon
8500
리스트로 담았던 profile_list[1] 은 8500을 주긴 하는데, "1번이 팔로워였나?" 하고 매번 헷갈려요.
딕셔너리 profile 은 profile["followers"] 라고 이름으로 부르니 한눈에 또렷하죠.
type(profile) 을 찍으면 <class 'dict'>, "이건 딕셔너리예요" 라고 알려줍니다.
번호로 찾기와 이름표로 찾기를 나란히 보면 차이가 분명해요.
리스트는 번호로 찾는다
["jaehoon", 8500, 42]
0 1 2
profile_list[1] → 8500 (근데 1번이 뭐였지?)
딕셔너리는 이름표로 찾는다
{"name": "jaehoon", "followers": 8500, "posts": 42}
profile["followers"] → 8500 (이름표라 한눈에!)
이어서 파일의 나머지도 볼게요.
# 빈 딕셔너리로 시작할 수도 있다
empty = {}
print(empty) # {}
print(type(empty)) # <class 'dict'>
# 몇 쌍이 들어있나?
print(len(profile)) # 3
리스트처럼 빈 딕셔너리 {} 로 시작해 나중에 채울 수도 있어요.
len(profile) 은 "이름표가 몇 쌍 들어있나" 를 알려줘요. 여기선 name, followers, posts 세 쌍이라 3이고요.
💡 한 줄 정리
딕셔너리는 중괄호 {} 안에 "이름표": 값 짝을 담는 그릇이에요. 리스트가 번호([0])로 찾았다면, 딕셔너리는 이름표(["followers"])로 찾아요. 사전에서 단어로 뜻을 찾는 것과 똑같죠.
🙋 학생 질문 — "이름표(key)는 꼭 따옴표 붙인 글자여야 하나요?"
꼭 글자만 되는 건 아니에요. 숫자도 이름표가 될 수 있어요. {1: "첫째", 2: "둘째"} 처럼요.
다만 실무에서는 "name", "followers" 같은 글자 이름표 가 압도적으로 많아요. 이름만 봐도 무슨 값인지 알 수 있으니까요.
한 가지만 기억해 두세요. 지난 시간 배운 리스트([])는 이름표로 쓸 수 없어요. "바뀔 수 있는 그릇" 은 이름표 자격이 없거든요. 반대로 글자·숫자·튜플처럼 안 바뀌는 값은 이름표가 될 수 있고요. 깊은 이유는 나중에 다루니, 지금은 "보통 글자 이름표를 쓴다" 정도만 익혀두면 충분해요.
Step 2: "값을 더하고 고치고 빼기"
지난 시간 리스트는 "가변(바뀌는 그릇)" 이라 자리를 고칠 수 있었죠. 딕셔너리도 마찬가지예요. 팔로워 수는 매일 바뀌고, 소개글은 새로 추가되니까요. 이름표를 이용해 값을 수정하고, 새 이름표를 추가하고, 필요 없는 건 빼봅시다.
# src/b_structures/b2_modify.py
# 딕셔너리는 가변 — 값을 더하고 고치고 뺄 수 있다
profile = {
"name": "jaehoon",
"followers": 8500,
"posts": 42,
}
# 1) 값 수정 — 이름표에 새 값 대입
profile["followers"] = 9000
print(profile["followers"]) # 9000
# 2) 새 이름표 추가 — 없던 key 에 대입하면 새로 생긴다
profile["bio"] = "여행과 사진"
print(profile) # bio 가 추가됨
# 3) 삭제 — del
del profile["posts"]
print(profile) # posts 가 사라짐
# 4) pop — 빼면서 그 값을 돌려받는다
removed = profile.pop("bio")
print(removed) # 여행과 사진
print(profile) # bio 가 사라짐
# 몇 쌍 남았나?
print(len(profile)) # 2
실행하면 이렇게 나와요.
9000
{'name': 'jaehoon', 'followers': 9000, 'posts': 42}
{'name': 'jaehoon', 'followers': 9000, 'bio': '여행과 사진'}
여행과 사진
{'name': 'jaehoon', 'followers': 9000}
2
여기서 재미있는 점이 있어요. 수정과 추가가 똑같이 생겼다 는 거예요.
profile["followers"] = 9000 은 이미 있는 이름표라 값을 갈아끼우고, profile["bio"] = "여행과 사진" 은 없던 이름표라 새로 만들어요.
파이썬이 "이름표가 있으면 고치고, 없으면 새로 추가" 를 알아서 판단해주는 거죠.
빼는 방법은 두 가지예요.
del profile["posts"] 는 그냥 지우기만 해요.
profile.pop("bio") 는 지난 시간 리스트의 pop 처럼 빼면서 그 값을 돌려줘요. removed = profile.pop("bio") 로 꺼낸 값을 받아 쓸 수 있죠.
💡 한 줄 정리
딕셔너리는 가변이에요. profile["키"] = 값 한 줄로, 있는 이름표면 수정하고 없는 이름표면 추가해요. 뺄 땐 del(지우기만) 또는 pop(빼면서 값을 돌려받기)을 써요.
🙋 학생 질문 — "같은 이름표에 또 대입하면 값이 두 개로 쌓이나요?"
아니요, 덮어써요. 이게 리스트의 append 와 결정적으로 다른 점이에요.
리스트는 append 할 때마다 칸이 하나씩 늘었죠. [8900] 에 또 넣으면 [8900, 8900] 이 됐고요.
딕셔너리는 이름표가 곧 "유일한 주소" 라서, 같은 이름표에 다시 대입하면 새 값으로 갈아끼울 뿐 두 개가 쌓이지 않아요. profile["followers"] = 9000 을 또 = 10000 하면 그냥 10000이 되는 거죠.
그래서 "한 사람의 팔로워 수" 처럼 하나뿐이어야 하는 값을 다룰 때 딕셔너리가 딱 맞아요.
Step 3: "없는 이름표를 부르면? — get 과 in"
딕셔너리를 쓰다 보면 이런 상황이 생겨요.
"이 프로필에 소개글(bio)이 있던가? 없으면 어떻게 되지?"
없는 이름표를 [] 로 부르면 어떤 일이 벌어지는지, 일부러 한번 마주쳐 봅시다. 에러는 친구니까요.
# src/b_structures/b2_keyerror.py
# 없는 이름표를 부르면? — 에러가 난다 (에러는 친구!)
profile = {"name": "jaehoon", "followers": 8500}
print(profile["age"]) # 'age' 라는 이름표는 없다 → KeyError
uv run python src/b_structures/b2_keyerror.py 로 실행하면 프로그램이 멈추면서 이런 메시지가 나와요.
Traceback (most recent call last):
File ".../b2_keyerror.py", line 5, in <module>
print(profile["age"]) # 'age' 라는 이름표는 없다 → KeyError
~~~~~~~^^^^^^^
KeyError: 'age'
맨 아랫줄 KeyError: 'age' 가 핵심이에요. "age 라는 이름표(key)는 없는데요?" 라는 뜻이죠.
파이썬 3.14는 친절하게 ~~~~~~~^^^^^^^ 로 어디가 문제인지 화살표까지 짚어줘요.
없는 이름표를 [] 로 부르면 이렇게 프로그램이 멈춰버려요.
이 멈춤을 피하려면 get 을 쓰면 돼요.
# src/b_structures/b2_get.py
# get() — 없는 이름표를 불러도 에러 대신 안전하게
profile = {"name": "jaehoon", "followers": 8500}
# get 은 없는 key 면 None 을 돌려준다 (에러 안 남)
print(profile.get("name")) # jaehoon
print(profile.get("age")) # None
# 기본값을 정해줄 수도 있다 — 없으면 이걸 대신 돌려준다
print(profile.get("age", 0)) # 0
print(profile.get("bio", "소개 없음")) # 소개 없음
# in — 이름표가 있는지 먼저 확인 (지난 시간 리스트의 in 과 같은 결)
print("name" in profile) # True
print("age" in profile) # False
# if 와 함께 — 있을 때만 꺼내기
if "followers" in profile:
print(f"팔로워: {profile['followers']}명")
실행 결과예요.
jaehoon
None
0
소개 없음
True
False
팔로워: 8500명
get 은 없는 이름표를 불러도 멈추지 않고 None(값이 없음)을 돌려줘요.
profile.get("age", 0) 처럼 기본값을 정해주면, 없을 때 대신 그 값을 줘요. "소개글 없으면 '소개 없음' 으로" 같은 처리가 한 줄로 끝나죠.
in 은 지난 시간 리스트에서 "이 값이 목록에 있나?" 확인할 때 썼던 그 in 이에요.
딕셔너리에 쓰면 이름표가 있는지 를 확인해줘요. if "followers" in profile: 로 "있으면 꺼내기" 를 안전하게 할 수 있고요.
💡 한 줄 정리
없는 이름표를 [] 로 부르면 KeyError 로 멈춰요. get("키", 기본값) 은 없어도 멈추지 않고 기본값을 돌려주고, in 은 이름표가 있는지 먼저 확인해줘요.
🙋 학생 질문 — "그냥 [] 로 꺼내면 되는데 왜 굳이 get 을 써요?"
이름표가 확실히 있을 때 는 [] 가 더 또렷하고 좋아요. profile["name"] 처럼요.
get 이 빛나는 건 "있을 수도, 없을 수도 있는" 값을 다룰 때예요. 사용자가 소개글을 안 적었을 수도 있잖아요?
그럴 때 [] 로 부르면 프로그램이 KeyError 로 뚝 멈춰버려요. 하지만 get("bio", "소개 없음") 은 없어도 차분하게 "소개 없음" 을 돌려주니, 프로그램이 끊기지 않고 자연스럽게 흘러가요.
"이 값은 없을 수도 있겠다" 싶으면 get, "이건 반드시 있다" 싶으면 [] — 이렇게 상황 따라 골라 쓰면 돼요.
Step 4: "통째로 훑기 — keys · values · items"
지금까지는 이름표 하나를 콕 집어 값을 꺼냈어요.
그런데 "이 프로필의 모든 정보를 한 줄씩 출력해줘" 같은 일도 자주 필요하죠.
딕셔너리에는 전체를 훑게 해주는 세 가지 메서드가 있어요. keys, values, items 예요.
# src/b_structures/b2_iterate.py
# 딕셔너리 통째로 훑기 — keys / values / items
profile = {
"name": "jaehoon",
"followers": 8500,
"posts": 42,
}
# keys() — 이름표만 모아서
print(profile.keys()) # dict_keys(['name', 'followers', 'posts'])
# values() — 값만 모아서
print(profile.values()) # dict_values(['jaehoon', 8500, 42])
# items() — (이름표, 값) 짝으로
print(profile.items())
실행하면 이렇게 나와요.
dict_keys(['name', 'followers', 'posts'])
dict_values(['jaehoon', 8500, 42])
dict_items([('name', 'jaehoon'), ('followers', 8500), ('posts', 42)])
세 메서드가 무엇을 주는지 그림으로 보면 또렷해요.
profile = {"name": "jaehoon", "followers": 8500, "posts": 42}
.keys() → name, followers, posts (이름표만)
.values() → jaehoon, 8500, 42 (값만)
.items() → ("name", "jaehoon"), ... (이름표·값 짝으로)
keys() 는 이름표만, values() 는 값만, items() 는 (이름표, 값) 짝으로 모아줘요.
items() 가 주는 짝, 어디서 본 모양 같지 않나요? 지난 시간 배운 튜플 이에요. ("name", "jaehoon") 처럼요.
이제 이걸 for 로 돌려봅시다.
# 그냥 for 는 이름표(key)를 돈다
for key in profile:
print(key) # name / followers / posts
# items() + 튜플 언패킹 — 이름표와 값을 한 번에
# (지난 시간 lat, lng = ... 처럼 짝을 한 번에 푼다)
for key, value in profile.items():
print(f"{key}: {value}")
실행 결과예요.
name
followers
posts
name: jaehoon
followers: 8500
posts: 42
딕셔너리를 그냥 for 로 돌면 이름표(key) 를 하나씩 줘요.
값까지 같이 쓰고 싶으면 for key, value in profile.items(): 가 제격이에요.
items() 가 주는 튜플 짝을, 지난 시간 배운 언패킹(lat, lng = location)으로 key 와 value 에 한 번에 풀어 담는 거죠. 지난 시간 도구가 여기서 다시 빛나네요.
💡 한 줄 정리
keys()(이름표만)·values()(값만)·items()(짝으로)로 딕셔너리 전체를 훑어요. for key, value in profile.items(): 는 items() 의 튜플 짝을 언패킹으로 풀어, 이름표와 값을 한 번에 다뤄요.
🙋 학생 질문 — "dict_keys([...]) 는 리스트랑 뭐가 달라요? 그냥 리스트면 안 돼요?"
거의 비슷하게 동작해요. for 로 돌 수 있고, in 으로 "있나?" 확인도 되거든요. 그래서 지금 단계에선 "리스트 비슷한 거" 로 생각해도 충분해요.
딱 하나 다른 점은, dict_keys 는 딕셔너리를 들여다보는 창문 같은 거라서, 딕셔너리가 바뀌면 같이 바뀌어요. 그리고 번호로 [0] 처럼 콕 집어 꺼내는 건 안 돼요.
번호로 꺼내거나 정렬해서 따로 쓰고 싶으면 list(profile.keys()) 로 진짜 리스트로 바꾸면 돼요. 지난 시간 배운 list() 가 여기서도 쓰이는 거죠. 지금은 "for 로 훑는 용도" 로만 알아두면 충분해요.
Step 5: "그릇을 조합하기 — 딕셔너리 여러 개"
지금까지는 프로필 한 명 을 딕셔너리로 다뤘어요. 그런데 인스타그램엔 회원이 한둘이 아니죠. 여러 명을 어떻게 담을까요? 정답은 지난 시간 배운 그릇과의 조합 이에요. 딕셔너리(한 명)를 리스트에 여러 개 담는 거죠.
지난 시간 리스트 안에 리스트를 담는 "중첩 리스트"(feed[i][j])를 배웠죠? 그 확장판이에요.
# src/b_structures/b2_combine.py
# 그릇을 조합 — 딕셔너리(프로필) 여러 개를 리스트에 담는다
# 프로필 한 명 = 딕셔너리, 여러 명 = 딕셔너리들의 리스트
members = [
{"name": "jaehoon", "followers": 8500},
{"name": "minji", "followers": 12000},
{"name": "seungwoo", "followers": 320},
]
# 리스트 번호로 한 명 고르고, 그 안에서 이름표로 꺼내기
print(members[0]["name"]) # jaehoon
print(members[1]["followers"]) # 12000
실행하면 이렇게 나와요.
jaehoon
12000
members 는 리스트인데, 칸마다 딕셔너리(프로필 한 명)가 들어있어요.
그래서 값을 꺼낼 때 두 단계 를 거쳐요. members[0] 으로 첫 번째 사람을 고르고, 거기에 ["name"] 을 붙여 이름을 꺼내는 거죠.
"리스트 번호로 한 명 고르고 → 딕셔너리 이름표로 정보 꺼내기" — 두 그릇이 만나는 자리예요.
이제 전체를 for 로 훑어봅시다.
# 전체 훑기 — for 로 한 명씩, 안에서 이름표로 꺼내기
for member in members:
name = member["name"]
followers = member["followers"]
print(f"{name}: 팔로워 {followers}명")
# 팔로워 1000명 넘는 사람만 세기
popular = 0
for member in members:
if member["followers"] >= 1000:
popular = popular + 1
print(f"인기 회원: {popular}명")
실행 결과예요.
jaehoon: 팔로워 8500명
minji: 팔로워 12000명
seungwoo: 팔로워 320명
인기 회원: 2명
for member in members: 는 회원을 한 명씩(딕셔너리 하나씩) 꺼내줘요.
그 안에서 member["name"], member["followers"] 로 정보를 꺼내 쓰는 거죠.
지난 시간 배운 for + if 로 "팔로워 1000명 넘는 사람" 을 세는 것도, 데이터가 딕셔너리로 바뀌었을 뿐 흐름은 똑같아요.
💡 한 줄 정리
딕셔너리(한 명)를 리스트에 여러 개 담으면 회원 명부가 돼요. members[0]["name"] 처럼 "리스트 번호 → 딕셔너리 이름표" 두 단계로 꺼내고, for 로 한 명씩 훑어요.
🙋 학생 질문 — "딕셔너리 안에 또 딕셔너리, 리스트 안에 딕셔너리... 얼마나 깊어질 수 있어요?"
원하는 만큼 깊어질 수 있어요. 리스트 안에 딕셔너리, 그 딕셔너리 값으로 또 리스트, 그 안에 또 딕셔너리... 이론적으로는 끝이 없어요.
실제로 인터넷에서 주고받는 데이터(나중에 배울 JSON 같은 것)가 딱 이렇게 생겼어요. 리스트와 딕셔너리가 겹겹이 쌓인 모양이죠.
다만 너무 깊어지면 data[0]["posts"][2]["likes"] 처럼 꺼내는 길이 길어져서 읽기 힘들어져요. 그래서 실무에선 "적당히 깊게, 너무 깊으면 나눠서" 가 요령이에요. 오늘은 "리스트 안에 딕셔너리" 한 겹 정도만 익혀두면 충분해요.
Step 6: "중복 없는 모음 — 집합(set)"
이제 딕셔너리를 잠시 내려놓고, 또 다른 그릇을 만나봅시다. 이런 상황을 떠올려 보세요. 한 게시물에 좋아요를 누른 사람 목록이 있는데, 같은 사람이 실수로 두 번 눌렀어요. "진짜로 좋아요 누른 사람은 몇 명?" 을 세려면 중복을 빼야겠죠.
이럴 때 쓰는 게 집합(set) 이에요. 집합은 중복을 자동으로 없애는 그릇이에요.
# src/b_structures/b2_set_create.py
# 집합(set): 중복 없는 모음 — 순서도 없다
# 좋아요 누른 사람 목록에 중복이 있다면?
likers = ["minji", "jaehoon", "minji", "seungwoo", "jaehoon"]
print(len(likers)) # 5 — 중복 포함
# set 으로 만들면 중복이 사라진다
unique_likers = set(likers)
print(unique_likers) # 중복 없는 모음 (순서는 보장 안 됨)
print(len(unique_likers)) # 3 — minji, jaehoon, seungwoo
# 다시 리스트로 — 중복 제거된 리스트
unique_list = list(set(likers))
print(len(unique_list)) # 3
실행하면 이렇게 나와요.
5
{'minji', 'jaehoon', 'seungwoo'}
3
3
likers 리스트엔 minji와 jaehoon이 두 번씩 들어 5개였죠.
set(likers) 로 집합으로 만드는 순간, 중복이 사라져 3명만 남아요.
"진짜 좋아요 누른 사람 수" 는 len(set(likers)) 한 줄로 끝나는 거죠.
⚠️ 한 가지 주의할 점이 있어요. 집합은 순서가 없어요. 위 출력에서
{'minji', 'jaehoon', 'seungwoo'}순으로 나왔지만, 실행할 때마다 순서가 달라질 수 있어요. 집합은 "누가 들어있나" 와 "중복 제거" 가 목적이지, "몇 번째" 를 따지는 그릇이 아니거든요. 순서가 중요하면 지난 시간의 리스트를 쓰면 돼요.
집합을 직접 만들 수도 있어요. 여기서 한 가지 함정을 짚고 갈게요.
# 집합 직접 만들기 — 중괄호 {}
tags = {"여행", "맛집", "여행", "일상"}
print(len(tags)) # 3 — '여행' 은 한 번만 (중복 자동 제거)
# 주의! 빈 중괄호 {} 는 딕셔너리다. 빈 집합은 set()
empty_set = set()
print(type(empty_set)) # <class 'set'>
print(type({})) # <class 'dict'>
실행 결과예요.
3
<class 'set'>
<class 'dict'>
{"여행", "맛집", "여행", "일상"} 처럼 중괄호로 직접 만들면, "여행" 이 두 번 적혀 있어도 알아서 한 번만 남겨요.
그런데 여기서 헷갈리기 쉬운 함정! 빈 중괄호 {} 는 집합이 아니라 딕셔너리예요.
빈 집합을 만들려면 꼭 set() 이라고 써야 해요. type({}) 이 <class 'dict'> 로 나오는 걸 보면 알 수 있죠.
💡 한 줄 정리
집합(set)은 중복을 자동으로 없애는 그릇이에요. set(리스트) 로 중복을 제거하고, {} 로 직접 만들 수도 있어요. 단 순서가 없고, 빈 집합은 {} 가 아니라 set() 으로 만들어야 해요.
🙋 학생 질문 — "순서가 매번 다르면 불편하지 않아요? 왜 이렇게 만들었대요?"
좋은 질문이에요. 순서가 없다는 게 단점처럼 느껴지죠.
그런데 집합은 순서를 포기한 대신 두 가지를 아주 빠르게 해내요. 하나는 방금 본 "중복 제거", 또 하나는 다음 Step에서 배울 "이 사람이 들어있나?" 확인이에요.
리스트에서 "이 값이 있나?" 를 확인하면 맨 앞부터 하나씩 다 뒤져야 해요. 사람이 100만 명이면 100만 번 비교할 수도 있죠. 집합은 순서를 포기한 덕분에 이걸 거의 한 번에 찾아내요.
그래서 "순서는 필요 없지만 중복 없이 빠르게 다뤄야 하는" 데이터엔 집합이, "순서가 중요한" 데이터엔 리스트가 어울려요. 둘은 쓰임새가 다른 도구인 거죠.
Step 7: "맞팔은 교집합 — 집합 연산"
집합이 진짜 빛나는 순간이 왔어요. 바로 두 모임을 비교 할 때예요. 중학교 수학 시간에 배운 교집합·합집합·차집합, 기억나세요? 파이썬 집합으로 그걸 그대로 해요. 인스타그램으로 비유하면 이래요. "내가 팔로우하는 사람" 과 "나를 팔로우하는 사람", 두 모임을 비교하면 맞팔 을 찾을 수 있죠.
# src/b_structures/b2_set_ops.py
# 집합 연산 — 맞팔은 교집합!
# 내가 팔로우하는 사람들
i_follow = {"minji", "seungwoo", "yuna", "dohyun"}
# 나를 팔로우하는 사람들
follows_me = {"minji", "yuna", "jisoo"}
# 교집합 & — 내가 팔로우 + 그쪽도 나를 팔로우 = 맞팔!
mutual = i_follow & follows_me
print(mutual) # minji, yuna (순서는 보장 안 됨)
# 합집합 | — 둘 중 하나라도 (나와 연결된 모든 사람, 중복 없이)
connected = i_follow | follows_me
print(len(connected)) # 5
# 차집합 - — 나는 팔로우하는데 그쪽은 날 안 함 (외사랑)
one_way = i_follow - follows_me
print(one_way) # seungwoo, dohyun (순서는 보장 안 됨)
# 부분집합 <= — 이 모임이 저 모임에 통째로 들어가나?
besties = {"minji", "yuna"}
print(besties <= i_follow) # True (둘 다 i_follow 안에 있음)
실행하면 이렇게 나와요. (집합이라 순서는 그때그때 다를 수 있어요.)
{'yuna', 'minji'}
5
{'seungwoo', 'dohyun'}
True
세 연산을 그림으로 정리하면 이래요.
i_follow = { minji, seungwoo, yuna, dohyun } 내가 팔로우
follows_me = { minji, yuna, jisoo } 나를 팔로우
& (교집합) → { minji, yuna } 양쪽 모두 = 맞팔!
| (합집합) → 다섯 명 (중복 없이) 둘 중 하나라도 = 연결된 사람
- (차집합) → { seungwoo, dohyun } 왼쪽에만 = 나만 팔로우(외사랑)
i_follow & follows_me 는 교집합, 양쪽에 다 있는 사람이에요. 내가 팔로우하고 그쪽도 나를 팔로우하니, 이게 바로 맞팔 이죠! 지난 시간 마지막에 예고했던 그 맞팔을, 단 한 줄(&)로 골라낸 거예요.
i_follow | follows_me 는 합집합, 둘 중 하나라도 속한 모든 사람을 중복 없이 모아요.
i_follow - follows_me 는 차집합, 나는 팔로우하는데 그쪽은 날 안 하는 사람(살짝 서운한 외사랑)이고요.
besties <= i_follow 는 "besties가 i_follow 안에 통째로 들어가나?" 를 묻는 부분집합 검사예요.
💡 한 줄 정리
집합 연산으로 두 모임을 한 줄에 비교해요. 교집합 &(양쪽 모두 = 맞팔), 합집합 |(둘 중 하나라도), 차집합 -(왼쪽에만). 인스타그램 맞팔이 i_follow & follows_me 한 줄로 끝나죠.
🙋 학생 질문 — "& 랑 | 기호가 헷갈려요. 어떻게 외워요?"
외우지 말고, 지난 시간 배운 걸 떠올려 연결하면 쉬워요.
and(그리고)는 "둘 다 참이어야 참" 이었죠? 교집합 & 도 똑같아요. "양쪽 다 있는 사람" 이거든요. & 의 느낌이 and 와 같아요.
or(또는)는 "둘 중 하나라도 참이면 참" 이었죠? 합집합 | 도 "둘 중 하나라도 속한 사람" 이에요. | 의 느낌이 or 와 같고요.
그래서 "교집합은 and 느낌(&), 합집합은 or 느낌(|)" 으로 연결해두면, 기호를 억지로 외우지 않아도 자연스럽게 떠올라요.
Step 8: "종합 — 프로필 관리 + 맞팔 찾기"
오늘 배운 걸 한데 모아봅시다. 딕셔너리로 회원 정보를 관리하고, 집합으로 맞팔을 찾는, 작은 회원 관리 프로그램이에요. 여기엔 오늘 배운 거의 모든 게 들어있어요. 천천히 한 줄씩 읽어봐요.
# src/b_structures/b2_manager.py
# 종합 실습 — 프로필 관리 + 맞팔 찾기
# 회원 프로필 — 이름표(key)로 정보를 담는다 (값이 또 딕셔너리!)
profiles = {
"jaehoon": {"followers": 8500, "posts": 42},
"minji": {"followers": 12000, "posts": 88},
}
# 1) 새 회원 추가
profiles["seungwoo"] = {"followers": 320, "posts": 5}
print(f"회원 수: {len(profiles)}명")
# 2) 한 명씩 정보 출력 (items + 튜플 언패킹)
for name, info in profiles.items():
print(f"{name}: 팔로워 {info['followers']}명, 게시물 {info['posts']}개")
# 3) 팔로워 5000명 넘는 인플루언서 세기
influencers = 0
for name, info in profiles.items():
if info["followers"] >= 5000:
influencers = influencers + 1
print(f"인플루언서: {influencers}명")
# 4) 맞팔 찾기 — 내가 팔로우 ∩ 나를 팔로우 (교집합)
i_follow = {"minji", "seungwoo", "yuna"}
follows_me = {"minji", "yuna", "jisoo"}
mutual = i_follow & follows_me
print(f"맞팔: {len(mutual)}명")
실행하면 이렇게 나와요.
회원 수: 3명
jaehoon: 팔로워 8500명, 게시물 42개
minji: 팔로워 12000명, 게시물 88개
seungwoo: 팔로워 320명, 게시물 5개
인플루언서: 2명
맞팔: 2명
이 프로그램엔 오늘 배운 게 골고루 담겼어요.
profiles 는 딕셔너리 안에 딕셔너리 예요. 이름표 "jaehoon" 의 값이 또 {"followers": ..., "posts": ...} 딕셔너리죠.
profiles["seungwoo"] = {...} 으로 새 회원을 추가하고, for name, info in profiles.items(): 로 한 명씩 훑어요.
info["followers"] 로 안쪽 딕셔너리의 값을 꺼내 인플루언서를 세고, 마지막엔 집합 & 로 맞팔을 한 줄에 찾았어요.
딕셔너리(이름표로 찾기)와 집합(중복 없이 비교하기)이 한 프로그램에서 손발을 맞추는 모습이에요.
💡 한 줄 정리
딕셔너리로 회원 정보를 담고(값이 또 딕셔너리일 수도 있고), items() 로 훑고, 집합 & 로 맞팔을 찾았어요. 오늘 배운 두 그릇이 한 프로그램에서 어우러지는 걸 봤죠.
🙋 학생 질문 — "이걸 매번 for 로 또박또박 짜야 하나요? 더 짧게 못 해요?"
지금은 for 로 한 줄씩 또박또박 짜는 게 맞아요. 무슨 일이 일어나는지 눈에 또렷이 보이니까요.
그런데 사실 더 짧게 쓰는 방법이 있어요. 다음 시간에 배울 컴프리헨션 이라는 도구예요. 예를 들어 "팔로워 5000명 넘는 사람 세기" 같은 걸 단 한 줄로 줄일 수 있어요.
지금 단계에서 그걸 먼저 쓰면 "왜 이게 되는지" 를 건너뛰게 돼요. 그래서 오늘은 for 로 원리를 충분히 익히고, 다음 시간에 "이걸 한 줄로 줄이면 이렇게 된다" 로 넘어갈 거예요.
또박또박 짜본 사람만이 짧은 코드를 제대로 이해할 수 있어요. 지금의 for 가 다음 시간의 밑거름이 되는 거죠.
마무리
오늘은 번호 대신 이름표 로 찾는 딕셔너리와, 중복 없이 모으는 집합을 배웠어요. 리스트와 함께 이 둘은 파이썬에서 데이터를 다루는 가장 기본이 되는 그릇이에요. 정말 고생 많으셨어요.
오늘 배운 핵심 세 가지
💡 하나: 딕셔너리({})는 이름표(key)로 값을 찾는 그릇이에요. 프로필["followers"] 로 꺼내고, get/in 으로 안전하게 접근하고, keys/values/items 로 전체를 훑었어요. 리스트가 번호로 찾았다면, 딕셔너리는 이름으로 찾죠.
💡 둘: 집합(set)은 중복을 자동으로 없애는 그릇이에요. 순서는 없지만, 중복 제거(set(리스트))와 빠른 포함 확인을 잘해요. 교집합 &·합집합 |·차집합 - 로 두 모임을 한 줄에 비교했고요.
💡 셋: 맞팔은 교집합 한 줄(i_follow & follows_me)이에요. 그리고 딕셔너리와 리스트를 조합하면(리스트 안에 딕셔너리, 딕셔너리 안에 딕셔너리) 회원 명부 같은 진짜 데이터를 다룰 수 있어요.
다음 시간 예고
다음 시간엔 "컴프리헨션(comprehension)" 을 배웁니다.
오늘 우리는 for 로 또박또박 리스트와 딕셔너리를 만들고 걸렀죠.
그걸 한 줄로 빚어내는 도구가 컴프리헨션이에요. [글자 for 글자 in 단어] 같은 모양으로요.
"좋아요 1000 넘는 게시물만 골라내기", "해시태그만 뽑아내기" 같은 걸 단 한 줄로 끝내게 됩니다.
또 두 리스트를 번호와 함께 도는 enumerate, 짝지어 도는 zip 도 만나요. 오늘의 for 가 한층 날렵해지는 시간이 될 거예요. 기대해 주세요!
과제
오늘 배운 딕셔너리와 집합을 직접 손으로 익혀봅시다. 눈으로 본 것과 직접 짜보는 것은 완전히 다르답니다.
[기초] 내 프로필 딕셔너리 만들기
나만의 프로필을 딕셔너리로 만들어 다뤄 보세요.
name,followers,posts세 가지 이름표로 프로필 딕셔너리를 만듭니다프로필["name"]으로 이름을 꺼내 출력하고,len()으로 이름표가 몇 쌍인지 셉니다프로필["followers"] = ...로 팔로워 수를 수정하고,프로필["bio"] = ...로 소개글을 새로 추가합니다get("bio", "소개 없음")으로 소개글을,get("age", 0)으로 (없는) 나이를 안전하게 꺼내 출력해 보세요
[응용] 회원 명부 훑기
여러 회원을 리스트에 담아 한 번에 다뤄 보세요.
- 회원 세 명 정도를, 각자
{"name": ..., "followers": ...}딕셔너리로 만들어 리스트에 담습니다 for로 한 명씩 돌며 "이름: 팔로워 N명" 형태로 출력합니다- 팔로워가 가장 많은 사람을 찾아 출력해 봅니다 (지난 시간 배운 "가장 큰 값 찾기" 흐름을 떠올려 보세요)
for와if로 팔로워 1000명 넘는 인기 회원이 몇 명인지 세어 함께 출력해 보세요
[심화] 맞팔과 추천 친구 찾기
집합 연산으로 인스타그램의 관계를 분석해 보세요. (오늘 Step 7을 직접 응용하는 과제예요.)
- "내가 팔로우하는 사람" 과 "나를 팔로우하는 사람" 을 각각 집합으로 만듭니다
- 교집합
&으로 맞팔 인 사람을, 차집합-으로 나만 팔로우하는 사람을 찾아 출력합니다 - (도전) 친구가 팔로우하는 사람 중 내가 아직 팔로우 안 하는 사람을 차집합으로 골라 "추천 친구" 로 출력해 보세요
- 좋아요 누른 사람 목록에 중복이 있을 때,
set으로 "진짜 좋아요 누른 사람 수" 를 세어 보세요
생각해볼 주제
정답이 하나로 정해진 질문이 아니에요. 혼자 곰곰이 생각해 보거나, 함께 공부하는 동료와 이야기 나눠 보면 좋습니다.
1. 리스트로도 되는데, 왜 딕셔너리를 따로 둘까?
회원 정보를 ["jaehoon", 8500, 42] 리스트로 담을 수도 있고, {"name": "jaehoon", "followers": 8500, "posts": 42} 딕셔너리로 담을 수도 있어요.
리스트는 [1] 처럼 번호로, 딕셔너리는 ["followers"] 처럼 이름표로 찾죠.
정보가 많아지고 코드가 길어질수록, 둘 중 어느 쪽이 "나중에 읽기" 좋을지 생각해 보세요.
반대로, 번호로 찾는 리스트가 더 어울리는 경우는 어떤 때일까요?
2. 순서를 포기한 대신 집합이 얻은 것
집합은 순서가 없어요. 처음엔 "순서도 못 지키는 그릇을 왜 써?" 싶을 수 있어요. 그런데 집합은 그 순서를 포기한 대가로 "중복 제거" 와 "빠른 포함 확인" 을 얻었어요. 어떤 데이터가 "순서는 중요하지 않지만 중복은 없어야 하는" 경우일까요? (좋아요 누른 사람, 방문한 페이지 목록 같은 걸 떠올려 보세요.) 무언가를 포기하고 다른 걸 얻는 이런 맞바꿈이, 도구를 고르는 안목의 시작이에요.
3. for 로 짠 걸 한 줄로 줄이면, 항상 좋을까?
다음 시간엔 오늘 for 로 또박또박 짠 걸 한 줄로 줄이는 컴프리헨션을 배워요.
코드가 짧아지면 보통 좋다고들 하죠.
그런데 한 줄로 너무 많은 걸 욱여넣으면, 오히려 읽기 어려워지는 순간도 있어요.
"짧은 코드" 와 "읽기 쉬운 코드" 가 항상 같은 말일까요? 어떤 때 한 줄이 좋고, 어떤 때 여러 줄이 나을지 미리 상상해 보세요.
✅ 예시 답안정답 보기
과제 예시답안
🎯 [기초] 내 프로필 딕셔너리 만들기 — 예시답안
채점 포인트
| 항목 | 배점 | 기준 |
|---|---|---|
| 딕셔너리 생성 + 이름표 접근 | 30% | {} 로 프로필을 만들고 ["name"] 으로 값을 꺼냈는가 |
| 수정 + 추가 | 30% | 팔로워 수를 고치고 소개글을 새 이름표로 추가했는가 |
get 안전 접근 |
40% | 없는 이름표를 기본값과 함께 안전하게 꺼냈는가 |
풀이 예시
# src/b_structures/b2_profile_solution.py
# [기초] 내 프로필 딕셔너리 만들기
# 1) 프로필 딕셔너리 만들기
profile = {
"name": "jaehoon",
"followers": 8500,
"posts": 42,
}
# 2) 이름 꺼내기 + 이름표 개수 세기
print(f"이름: {profile['name']}")
print(f"정보 항목 수: {len(profile)}개")
# 3) 팔로워 수 수정 + 소개글 추가
profile["followers"] = 9000
profile["bio"] = "여행과 사진을 기록합니다"
print(profile)
# 4) get 으로 안전하게 꺼내기
print(profile.get("bio", "소개 없음")) # 소개글이 있으니 실제 값
print(profile.get("age", 0)) # age 이름표가 없으니 기본값 0
실행하면 이렇게 나옵니다.
이름: jaehoon
정보 항목 수: 3개
{'name': 'jaehoon', 'followers': 9000, 'posts': 42, 'bio': '여행과 사진을 기록합니다'}
여행과 사진을 기록합니다
0
💡 튜터의 한마디
수정과 추가가 똑같이 profile["키"] = 값 모양이라는 게 한눈에 보이죠. followers 는 이미 있어서 9000으로 바뀌고, bio 는 없던 이름표라 새로 생겼어요.
마지막 get 두 줄이 핵심이에요. get("bio", ...) 는 소개글이 있으니 실제 값을, get("age", 0) 은 없는 이름표라 기본값 0을 돌려줘요. [] 로 profile["age"] 를 불렀다면 KeyError 로 멈췄겠지만, get 은 차분히 기본값을 주죠.
자주 하는 실수
| 실수 | 무슨 일이 생기나 | 해결법 |
|---|---|---|
profile["age"] 로 없는 값 접근 |
KeyError 로 프로그램이 멈춤 |
없을 수 있는 값은 get("age", 0) 으로 |
profile.get("age") 처럼 기본값 생략 |
없는 이름표라 None 이 출력됨 |
기본값을 정하려면 get("age", 0) 처럼 두 번째 인자를 주기 |
🎯 [응용] 회원 명부 훑기 — 예시답안
채점 포인트
| 항목 | 배점 | 기준 |
|---|---|---|
딕셔너리 리스트 + for 훑기 |
30% | 회원을 딕셔너리 리스트로 담고 한 명씩 출력했는가 |
| 최댓값 찾기 | 40% | 팔로워가 가장 많은 사람을 비교로 찾았는가 |
for+if 조건 세기 |
30% | 1000명 넘는 인기 회원을 셌는가 |
풀이 예시
# src/b_structures/b2_member_book_solution.py
# [응용] 회원 명부 훑기
members = [
{"name": "jaehoon", "followers": 8500},
{"name": "minji", "followers": 12000},
{"name": "seungwoo", "followers": 320},
]
# 1) 한 명씩 출력
for member in members:
print(f"{member['name']}: 팔로워 {member['followers']}명")
# 2) 팔로워가 가장 많은 사람 찾기 (첫 사람을 임시 1등으로 두고 비교)
top_name = members[0]["name"]
top_followers = members[0]["followers"]
for member in members:
if member["followers"] > top_followers:
top_followers = member["followers"]
top_name = member["name"]
print(f"팔로워 최다: {top_name} ({top_followers}명)")
# 3) 팔로워 1000명 넘는 인기 회원 세기
popular = 0
for member in members:
if member["followers"] >= 1000:
popular = popular + 1
print(f"인기 회원: {popular}명")
실행하면 이렇게 나옵니다.
jaehoon: 팔로워 8500명
minji: 팔로워 12000명
seungwoo: 팔로워 320명
팔로워 최다: minji (12000명)
인기 회원: 2명
💡 튜터의 한마디
"가장 많은 사람 찾기" 의 요령은 첫 사람을 임시 1등으로 앉혀두고, 한 명씩 돌며 "얘가 더 많아?" 를 묻는 거예요. 더 많으면 1등을 갈아치우고요.
데이터가 딕셔너리로 바뀌었을 뿐, 흐름은 지난 시간 리스트에서 "가장 큰 값 찾기" 와 똑같죠. member["followers"] 처럼 이름표로 값을 꺼낸다는 점만 새로워요.
자주 하는 실수
| 실수 | 무슨 일이 생기나 | 해결법 |
|---|---|---|
top = 0 으로 최댓값 초기화 |
값이 음수만 있으면 0이 1등이 됨 | 첫 원소 members[0]["followers"] 로 초기화 |
member.followers 처럼 점으로 접근 |
딕셔너리는 점이 아니라 ["키"] 로 꺼냄 |
member["followers"] |
🎯 [심화] 맞팔과 추천 친구 찾기 — 예시답안
채점 포인트
| 항목 | 배점 | 기준 |
|---|---|---|
교집합 & 으로 맞팔 |
30% | 두 집합의 교집합으로 맞팔을 구했는가 |
차집합 - 활용 |
40% | 나만 팔로우·추천 친구를 차집합으로 골랐는가 |
set 중복 제거 |
30% | 중복 좋아요를 집합으로 정리해 세었는가 |
풀이 예시
# src/b_structures/b2_mutual_solution.py
# [심화] 맞팔과 추천 친구 찾기
# 1) 내 팔로우 관계
i_follow = {"minji", "seungwoo", "yuna", "dohyun"}
follows_me = {"minji", "yuna", "jisoo"}
# 2) 맞팔(교집합) + 나만 팔로우(차집합)
mutual = i_follow & follows_me
only_me = i_follow - follows_me
print(f"맞팔: {len(mutual)}명")
print(f"나만 팔로우: {len(only_me)}명")
# 3) 추천 친구 — 친구(minji)가 팔로우하는 사람 중 내가 아직 안 하는 사람
minji_follows = {"yuna", "jisoo", "haeun"}
recommend = minji_follows - i_follow
print(f"추천 친구: {len(recommend)}명")
# 4) 중복 좋아요 정리 — 진짜 좋아요 누른 사람 수
likers = ["minji", "jaehoon", "minji", "seungwoo", "jaehoon"]
print(f"진짜 좋아요: {len(set(likers))}명")
실행하면 이렇게 나옵니다.
맞팔: 2명
나만 팔로우: 2명
추천 친구: 2명
진짜 좋아요: 3명
💡 튜터의 한마디
집합 연산의 힘이 느껴지나요? 맞팔도, 나만 팔로우도, 추천 친구도 전부 한 줄 이에요. for 와 if 로 일일이 비교했다면 훨씬 길어졌을 거예요.
특히 추천 친구가 재미있어요. "친구가 팔로우하는 사람(minji_follows)" 에서 "내가 이미 팔로우하는 사람(i_follow)" 을 빼면, 내가 아직 모르는 사람만 남죠. 실제 SNS의 "알 수도 있는 친구" 가 이런 집합 연산에서 출발해요.
자주 하는 실수
| 실수 | 무슨 일이 생기나 | 해결법 |
|---|---|---|
i_follow - follows_me 와 follows_me - i_follow 혼동 |
차집합은 방향이 있어 결과가 달라짐 | "왼쪽에 있고 오른쪽엔 없는" 순서를 먼저 정하기 |
빈 집합을 {} 로 생성 |
{} 는 딕셔너리라 &/- 가 엉뚱하게 동작 |
빈 집합은 set() 으로 |
생각해볼 주제 예시답안
🤔 [생각해볼 주제 1] 리스트로도 되는데, 왜 딕셔너리를 따로 둘까?
문제 상황 요약
회원 정보를 ["jaehoon", 8500, 42] 리스트로 담을 수도, {"name": "jaehoon", "followers": 8500, "posts": 42} 딕셔너리로 담을 수도 있어요.
둘 다 같은 정보를 담는데, 정보가 많아지고 코드가 길어질수록 어느 쪽이 더 좋을까요?
튜터의 가이드 및 해설
핵심은 "나중에 그 코드를 다시 읽을 때" 예요.
리스트로 담으면 member[1] 이 팔로워 수죠. 그런데 한 달 뒤에 이 코드를 다시 보면, "1번이 팔로워였나, 게시물이었나?" 를 헷갈리게 돼요.
항목이 세 개일 땐 그럭저럭 외우지만, 열 개, 스무 개로 늘면 번호를 외우는 건 사실상 불가능해요.
딕셔너리는 member["followers"] 라고 뜻 그대로 쓰니, 외울 필요가 없어요. 코드만 봐도 무슨 값인지 바로 읽히죠.
값마다 고유한 이름이 있는 데이터(프로필, 설정값, 카드 정보)엔 딕셔너리가 훨씬 읽기 좋아요.
그렇다고 리스트가 밀리는 건 아니에요. "좋아요 수 100개" 처럼 같은 종류가 순서대로 늘어선 데이터엔 이름표가 오히려 거추장스럽죠. 그냥 리스트로 번호 매겨 담는 게 깔끔해요. 결국 "항목마다 이름이 있나(딕셔너리), 같은 게 순서대로 있나(리스트)" 로 갈리는 거예요.
💡 핵심을 한마디로
"항목마다 고유한 이름이 있으면 딕셔너리, 같은 종류가 순서대로 늘어서면 리스트예요. 프로필은 이름표(
['followers'])로 읽어야 한 달 뒤에도 안 헷갈리고, 좋아요 수 목록은 그냥 번호로 담는 게 깔끔하죠. 둘은 '읽기 좋은가' 로 갈려요."
🤔 [생각해볼 주제 2] 순서를 포기한 대신 집합이 얻은 것
문제 상황 요약
집합은 순서가 없어요. 처음엔 "순서도 못 지키는 그릇을 왜 써?" 싶을 수 있어요. 그런데 집합은 그 순서를 포기한 대가로 무언가를 얻었어요. 어떤 데이터에 집합이 어울릴까요?
튜터의 가이드 및 해설
집합이 순서를 내준 대신 얻은 건 두 가지예요. 중복 제거 와 빠른 포함 확인 이죠.
오늘 좋아요 누른 사람을 셀 때, set(likers) 한 줄로 중복이 사라졌죠. 리스트로 같은 일을 하려면 "이미 센 사람인가?" 를 매번 확인하며 돌아야 해요.
또 "이 사람이 좋아요 눌렀나?" 를 확인할 때, 리스트는 맨 앞부터 하나씩 다 뒤져요. 사람이 100만 명이면 100만 번 비교할 수도 있죠. 집합은 거의 한 번에 찾아내요.
그래서 "순서는 중요하지 않지만, 중복이 없어야 하거나 빠르게 찾아야 하는" 데이터에 집합이 빛나요. 좋아요 누른 사람, 방문한 페이지 목록, 출석한 학생처럼요. "누가 있나/없나" 만 중요하고 "몇 번째인지" 는 안 따지는 경우죠.
반대로 게시물을 올린 순서, 댓글이 달린 순서처럼 순서 자체가 의미 인 데이터엔 리스트가 맞아요. 무언가를 포기하고 다른 걸 얻는 이런 맞바꿈을 읽어내는 게, 도구를 고르는 안목의 시작이에요.
💡 핵심을 한마디로
"집합은 순서를 포기한 대신 '중복 제거' 와 '빠른 포함 확인' 을 얻었어요. 좋아요 누른 사람처럼 순서는 안 중요하고 중복만 없으면 되는 데이터엔 집합이, 올린 순서가 의미인 데이터엔 리스트가 어울리죠. 도구를 고를 땐 '무엇을 포기하고 무엇을 얻는가' 를 보면 돼요."
🤔 [생각해볼 주제 3] for 로 짠 걸 한 줄로 줄이면, 항상 좋을까?
문제 상황 요약
다음 시간엔 오늘 for 로 또박또박 짠 걸 한 줄로 줄이는 컴프리헨션을 배워요.
코드가 짧아지면 보통 좋다고들 하죠. 그런데 정말 "짧은 코드 = 좋은 코드" 일까요?
튜터의 가이드 및 해설
짧은 코드가 좋은 건 맞아요. 하지만 "짧다" 와 "읽기 쉽다" 가 항상 같은 말은 아니에요.
"팔로워 1000명 넘는 사람만 골라내기" 같은 단순한 일은, 한 줄로 줄이면 오히려 한눈에 들어와요. 군더더기 for 와 빈 리스트 준비가 사라지니까요.
이런 경우엔 한 줄이 분명히 좋아요. 읽는 사람도 "아, 거르는 거구나" 를 바로 알죠.
문제는 한 줄에 너무 많은 걸 욱여넣을 때예요.
조건이 두세 개 겹치고, 그 안에서 또 계산하고, 중첩까지 들어가면 — 한 줄이지만 암호문처럼 읽혀요. 오히려 for 여러 줄로 풀어 쓴 게 이해하기 쉬울 때가 있죠.
그래서 좋은 기준은 "줄 수" 가 아니라 "다음에 읽을 사람(미래의 나 포함)이 한눈에 이해하나" 예요. 단순한 건 한 줄로 시원하게, 복잡한 건 여러 줄로 또박또박. 도구가 생겼다고 무조건 쓰는 게 아니라, 읽는 사람을 떠올리며 고르는 거예요.
💡 핵심을 한마디로
"짧은 코드와 읽기 쉬운 코드가 늘 같진 않아요. 단순한 거르기·변환은 한 줄(컴프리헨션)이 시원하지만, 조건이 겹치고 복잡해지면
for여러 줄이 오히려 또렷해요. 기준은 '줄 수' 가 아니라 '다음에 읽을 사람이 한눈에 이해하나' 예요."