문서 읽는 데 54분 · B2

B-2: 딕셔너리와 집합 — 이름표로 찾고, 중복 없이 모으기

목차 29
전체 22강 중 6강 · 파이썬 기초
난이도 · 입문

ℹ️프로그래밍 경험이 없어도 시작할 수 있는 파이썬 입문 — ‘개발 입문’을 먼저 들으면 더 수월해요. AI·데이터 분석으로 이어지는 출발점이에요.

안녕하세요! 여러분의 파이썬 가이드, 홍순구 튜터입니다.

지난 시간엔 여러 값을 리스트([])라는 한 그릇에 담고, 번호([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) 을 찾습니다. 중괄호 {} 안에 "이름표": 값 짝을 쉼표로 늘어놓으면 돼요.

코드부터 실행해 봅시다.

Python
# 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번이 팔로워였나?" 하고 매번 헷갈려요. 딕셔너리 profileprofile["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     (이름표라 한눈에!)

이어서 파일의 나머지도 볼게요.

Python
# 빈 딕셔너리로 시작할 수도 있다
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: "값을 더하고 고치고 빼기"

지난 시간 리스트는 "가변(바뀌는 그릇)" 이라 자리를 고칠 수 있었죠. 딕셔너리도 마찬가지예요. 팔로워 수는 매일 바뀌고, 소개글은 새로 추가되니까요. 이름표를 이용해 값을 수정하고, 새 이름표를 추가하고, 필요 없는 건 빼봅시다.

Python
# 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: "없는 이름표를 부르면? — getin"

딕셔너리를 쓰다 보면 이런 상황이 생겨요. "이 프로필에 소개글(bio)이 있던가? 없으면 어떻게 되지?" 없는 이름표를 [] 로 부르면 어떤 일이 벌어지는지, 일부러 한번 마주쳐 봅시다. 에러는 친구니까요.

Python
# 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 을 쓰면 돼요.

Python
# 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 예요.

Python
# 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 로 돌려봅시다.

Python
# 그냥 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)으로 keyvalue 에 한 번에 풀어 담는 거죠. 지난 시간 도구가 여기서 다시 빛나네요.

💡 한 줄 정리

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])를 배웠죠? 그 확장판이에요.

Python
# 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 로 훑어봅시다.

Python
# 전체 훑기 — 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) 이에요. 집합은 중복을 자동으로 없애는 그릇이에요.

Python
# 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'} 순으로 나왔지만, 실행할 때마다 순서가 달라질 수 있어요. 집합은 "누가 들어있나" 와 "중복 제거" 가 목적이지, "몇 번째" 를 따지는 그릇이 아니거든요. 순서가 중요하면 지난 시간의 리스트를 쓰면 돼요.

집합을 직접 만들 수도 있어요. 여기서 한 가지 함정을 짚고 갈게요.

Python
# 집합 직접 만들기 — 중괄호 {}
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: "맞팔은 교집합 — 집합 연산"

집합이 진짜 빛나는 순간이 왔어요. 바로 두 모임을 비교 할 때예요. 중학교 수학 시간에 배운 교집합·합집합·차집합, 기억나세요? 파이썬 집합으로 그걸 그대로 해요. 인스타그램으로 비유하면 이래요. "내가 팔로우하는 사람" 과 "나를 팔로우하는 사람", 두 모임을 비교하면 맞팔 을 찾을 수 있죠.

Python
# 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: "종합 — 프로필 관리 + 맞팔 찾기"

오늘 배운 걸 한데 모아봅시다. 딕셔너리로 회원 정보를 관리하고, 집합으로 맞팔을 찾는, 작은 회원 관리 프로그램이에요. 여기엔 오늘 배운 거의 모든 게 들어있어요. 천천히 한 줄씩 읽어봐요.

Python
# 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명" 형태로 출력합니다
  • 팔로워가 가장 많은 사람을 찾아 출력해 봅니다 (지난 시간 배운 "가장 큰 값 찾기" 흐름을 떠올려 보세요)
  • forif 로 팔로워 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% 없는 이름표를 기본값과 함께 안전하게 꺼냈는가

풀이 예시

Python
# 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명 넘는 인기 회원을 셌는가

풀이 예시

Python
# 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% 중복 좋아요를 집합으로 정리해 세었는가

풀이 예시

Python
# 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명

💡 튜터의 한마디 집합 연산의 힘이 느껴지나요? 맞팔도, 나만 팔로우도, 추천 친구도 전부 한 줄 이에요. forif 로 일일이 비교했다면 훨씬 길어졌을 거예요. 특히 추천 친구가 재미있어요. "친구가 팔로우하는 사람(minji_follows)" 에서 "내가 이미 팔로우하는 사람(i_follow)" 을 빼면, 내가 아직 모르는 사람만 남죠. 실제 SNS의 "알 수도 있는 친구" 가 이런 집합 연산에서 출발해요.

자주 하는 실수

실수 무슨 일이 생기나 해결법
i_follow - follows_mefollows_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 여러 줄이 오히려 또렷해요. 기준은 '줄 수' 가 아니라 '다음에 읽을 사람이 한눈에 이해하나' 예요."

전체 목록 파이썬 기초