문서 읽는 데 57분 · B1

B-1: 리스트와 튜플 — 여러 값을 한 그릇에 담기

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

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

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

지난 시간엔 if/elif/else 로 갈림길을 만들고, for/while 로 같은 일을 되풀이했죠. 마지막엔 좋아요 수를 "8900 320 15000 87".split() 으로 빙 둘러 만들어 for 로 훑었고요. 그때 제가 "이 split() 이 만드는 게 바로 다음 시간에 배울 그릇" 이라고 살짝 흘렸던 거 기억하세요?

오늘 그 그릇의 정체를 밝힙니다. 바로 리스트(list) 예요. 지금까지 우리는 값을 하나씩 변수에 담았어요. like1 = 8900, like2 = 320... 이런 식으로요. 게시물이 100개라면? 변수를 100개 만들 순 없잖아요. 리스트는 여러 값을 번호 매겨 한 그릇에 담습니다. 게시물 목록, 좋아요 목록을 자유자재로 다루기 시작해요.

오늘 우리는 이런 길을 따라갑니다.

텍스트
 오늘의 여정 — B-1 (총 8 단계)

  1. 리스트 만들기        여러 값을 한 그릇에
  2. 인덱싱 · 슬라이싱     몇 번째를 꺼낼까
  3. 추가 · 제거          담고 빼는 메서드
  4. 정렬 · 탐색 · 집계    줄 세우고 세어보기
  5. 가변 vs 불변         바뀌는 그릇, 안 바뀌는 값
  6. 튜플 · 언패킹        묶어서 한 번에
  7. 중첩 리스트          리스트 안의 리스트
  8. 종합: 게시물 매니저   오늘 배운 걸 한데 모아

앞쪽은 리스트를 만들고 꺼내고 다루는 법, 뒤쪽은 튜플과 중첩까지 넓혀가요. 한꺼번에 외우려 하지 말고, 직접 실행해 보면서 하나씩 익혀가요.

💡 오늘 수업의 핵심 — "여러 값을 리스트에 담고([]), 꺼내고([0]), 다루고(append·sort), 튜플로 묶기"

🎯 학습 목표

  • 여러 값을 리스트에 담고, 인덱싱·슬라이싱으로 원하는 자리를 꺼낼 수 있다
  • append/pop/sort 등 리스트 메서드로 데이터를 추가·삭제·정렬할 수 있다
  • 가변(리스트)과 불변(문자열·튜플)의 차이, 그리고 같은 그릇을 가리키는 별칭 함정을 이해한다
  • 튜플로 값을 묶고 언패킹하며, 중첩 리스트로 여러 묶음을 다룰 수 있다

Step 1: "여러 개를 한 그릇에 — 리스트 만들기"

값을 하나만 담을 땐 변수 하나면 충분했어요. like = 8900 처럼요. 그런데 좋아요 수가 네 개라면 어떨까요? like1, like2, like3, like4... 변수를 네 개나 만드는 건 번거롭고, 게시물이 늘면 감당이 안 돼요.

이럴 때 쓰는 게 리스트(list) 예요. 여러 값을 대괄호 [] 안에 쉼표로 늘어놓으면, 하나의 그릇에 순서대로 담깁니다.

코드부터 실행해 봅시다.

Python
# src/b_structures/b1_create.py
# 리스트: 여러 값을 한 그릇에 순서대로 담는다

# 좋아요 수 네 개를 변수 네 개로? → 그릇 하나로!
likes = [8900, 320, 15000, 87]
print(likes)
print(type(likes))      # <class 'list'>

# 빈 리스트로 시작해서 나중에 채울 수도 있다
empty = []
print(empty)            # []

# 문자열도 담는다 — 게시물 제목 목록
posts = ["첫 게시물", "여행 사진", "맛집 탐방"]
print(posts)

▶ 버튼으로 실행하거나 터미널에서 uv run python src/b_structures/b1_create.py 를 입력하면 이렇게 나와요.

텍스트
[8900, 320, 15000, 87]
<class 'list'>
[]
['첫 게시물', '여행 사진', '맛집 탐방']

likes = [8900, 320, 15000, 87] 한 줄로 값 네 개가 하나의 그릇에 담겼어요. type(likes) 를 찍어보면 <class 'list'>, 즉 "이건 리스트예요" 라고 알려줍니다. 빈 리스트 [] 로 시작해 나중에 값을 채워 넣을 수도 있고요. 숫자뿐 아니라 문자열(게시물 제목)도 똑같이 담깁니다.

리스트는 이렇게 칸칸이 번호가 매겨진 그릇이라고 생각하면 편해요.

텍스트
 likes = [8900, 320, 15000, 87]

  번호:    0       1       2      3
         ┌──────┬───────┬───────┬──────┐
         │ 8900 │  320  │ 15000 │  87  │
         └──────┴───────┴───────┴──────┘

칸마다 0, 1, 2, 3 번호가 붙어 있죠? 이 번호로 값을 꺼내는 게 다음 단계예요.

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

Python
# 한 그릇에 종류가 섞여도 된다 (보통은 같은 종류로 담는 게 깔끔)
mixed = ["jaehoon", 25, True]
print(mixed)            # ['jaehoon', 25, True]

# 몇 개 들어있나?
print(len(likes))       # 4

# 문자열을 글자 리스트로 쪼개기 — list()
chars = list("hello")
print(chars)            # ['h', 'e', 'l', 'l', 'o']

# 지난 시간 split() 도 사실 리스트를 만들고 있었다!
tags = "#여행 #맛집 #일상".split()
print(tags)             # ['#여행', '#맛집', '#일상']

파이썬 리스트는 한 그릇에 글자와 숫자를 섞어 담아도 동작해요. 다만 보통은 "좋아요는 좋아요끼리, 제목은 제목끼리" 같은 종류로 담는 게 다루기 깔끔합니다. len(likes) 는 지난 시간 문자열 길이를 잴 때 썼던 그 len 이에요. 리스트에 쓰면 "몇 개 들어있나" 를 알려줍니다. list("hello") 는 문자열을 글자 하나하나로 쪼개 리스트로 만들어줘요.

그리고 마지막 줄, 지난 시간 그렇게 썼던 split() 의 결과가 바로 리스트였어요! "#여행 #맛집 #일상".split() 은 공백으로 잘라 ['#여행', '#맛집', '#일상'] 리스트를 돌려줍니다. 지난 시간엔 그릇의 정체를 몰랐는데, 이제 그게 리스트라는 걸 알게 됐죠.

💡 한 줄 정리

리스트는 여러 값을 대괄호 [] 안에 순서대로 담는 그릇이에요. 빈 [] 로 시작할 수도, len() 으로 개수를 셀 수도 있고, 지난 시간의 split() 도 사실 리스트를 만들고 있었어요.

🙋 학생 질문 — "한 리스트에 숫자랑 글자를 섞어 담아도 정말 괜찮아요?"

문법적으로는 전혀 문제없어요. ["jaehoon", 25, True] 처럼 섞어도 파이썬은 군말 없이 받아줍니다.

다만 실무에선 보통 한 종류로 담아요. "좋아요 숫자들" 은 숫자 리스트로, "게시물 제목들" 은 문자열 리스트로요. 그래야 나중에 sum() 으로 합을 구하거나 정렬할 때 깔끔하게 돌아가거든요. 숫자와 글자가 섞인 리스트를 정렬하려 하면 "숫자랑 글자를 어떻게 비교해?" 하며 에러가 나기도 해요.

그래서 "담을 순 있지만, 같은 종류로 담는 습관" 을 들이면 좋아요.


Step 2: "몇 번째를 꺼낼까 — 인덱싱과 슬라이싱"

그릇에 값을 담았으니, 이제 원하는 값을 꺼내야겠죠. 좋은 소식이 있어요. 사실 우리는 이미 이걸 해봤어요! A-2 에서 문자열의 글자를 [0], [-1], [0:4] 로 꺼냈던 거 기억하세요? 리스트도 완전히 똑같은 규칙 으로 값을 꺼냅니다. 번호(인덱스)로요.

Python
# src/b_structures/b1_index.py
# 인덱싱과 슬라이싱: 리스트의 몇 번째를 꺼낼까

posts = ["첫 게시물", "여행 사진", "맛집 탐방", "운동 기록", "독서 노트"]

# 인덱스는 0 부터! (문자열에서 봤던 그 규칙 그대로)
print(posts[0])         # 첫 게시물  (맨 앞)
print(posts[1])         # 여행 사진

# 음수 인덱스 — 뒤에서부터
print(posts[-1])        # 독서 노트  (맨 뒤 = 최신글)
print(posts[-2])        # 운동 기록

실행하면 이렇게 나와요.

텍스트
첫 게시물
여행 사진
독서 노트
운동 기록

번호는 0부터 시작해요. 그래서 posts[0] 이 맨 앞(첫 게시물), posts[1] 이 두 번째예요. 음수를 쓰면 뒤에서부터 세요. posts[-1] 은 맨 뒤(독서 노트), posts[-2] 는 뒤에서 두 번째고요. 게시물 목록이라면 맨 뒤 [-1] 이 가장 최근에 올린 글이 되겠죠.

번호를 그림으로 보면 이래요.

텍스트
   번호:    0     1     2     3     4       앞에서 (0부터)
           -5    -4    -3    -2    -1       뒤에서 (-1부터)

같은 칸을 앞에서는 0, 뒤에서는 -5 처럼 두 가지로 부를 수 있어요.

이번엔 여러 칸을 한 번에 꺼내는 슬라이싱을 볼게요.

Python
# 슬라이싱 [시작:끝] — 끝은 포함 안 함
print(posts[1:3])       # ['여행 사진', '맛집 탐방']
print(posts[:2])        # ['첫 게시물', '여행 사진']  (처음부터)
print(posts[2:])        # ['맛집 탐방', '운동 기록', '독서 노트']  (끝까지)

# 전체 개수
print(len(posts))       # 5

# 값 바꾸기 — 리스트는 자리를 직접 고칠 수 있다
posts[0] = "수정한 첫 게시물"
print(posts[0])         # 수정한 첫 게시물

posts[1:3] 은 "1번부터 3번 앞까지", 즉 1번과 2번을 꺼내요. 지난 시간에도 강조했죠? 슬라이싱은 끝 번호를 포함하지 않아요. 그래서 1, 2 두 개만 나와요. posts[:2] 는 시작을 비워 "처음부터 2번 앞까지", posts[2:] 는 끝을 비워 "2번부터 끝까지" 예요.

여기서 문자열과 다른 점이 하나 있어요. 지난 시간에 문자열은 "한 글자도 직접 못 바꾼다" 고 했죠? 그런데 리스트는 posts[0] = "수정한 첫 게시물" 처럼 특정 자리의 값을 직접 갈아끼울 수 있어요. 이 차이가 오늘 Step 5의 핵심 주제로 이어집니다. 지금은 "리스트는 자리를 고칠 수 있구나" 정도만 기억해 두세요.

💡 한 줄 정리

리스트도 문자열처럼 [번호] 로 값을 꺼내요. 번호는 0부터, 음수는 뒤에서부터. [시작:끝] 슬라이싱은 끝을 포함하지 않고요. 단, 리스트는 posts[0] = ... 로 특정 자리를 직접 고칠 수 있어요.

🙋 학생 질문 — "왜 번호가 1이 아니라 0부터 시작해요? 헷갈리는데요."

처음엔 다들 헷갈려요. "첫 번째" 인데 0번이라니 어색하죠.

이건 파이썬만의 고집이 아니라 거의 모든 프로그래밍 언어가 공유하는 약속이에요. 컴퓨터가 값을 찾을 때 "맨 앞에서 몇 칸 떨어졌나" 로 세는데, 맨 앞은 0칸 떨어진 자리거든요. 그래서 첫 칸이 0번이 됩니다.

처음엔 "0번 = 첫 번째, 1번 = 두 번째" 로 한 칸씩 밀어서 생각하면 편해요. 음수 인덱스 [-1] 이 "맨 마지막" 이라는 것만 익혀두면, 최신 글을 꺼낼 때 아주 자주 쓰게 될 거예요.


Step 3: "담고 빼기 — 추가·삽입·제거 메서드"

지난 시간에 리스트는 "자리를 고칠 수 있다" 고 했죠. 그렇다면 칸 자체를 늘리거나 줄이는 것도 되겠네요. 게시물을 새로 올리면 목록에 추가되고, 지우면 빠지는 것처럼요. 리스트에는 이런 일을 해주는 메서드(method) 들이 딸려 있어요. 메서드는 "리스트야, 이것 좀 해줘" 하고 시키는 명령이에요. 리스트.명령() 형태로 씁니다.

먼저 추가하는 메서드부터요.

Python
# src/b_structures/b1_methods.py
# 리스트에 담고 빼기 — 추가·삽입·제거 메서드

posts = ["첫 게시물", "여행 사진"]

# append: 맨 뒤에 하나 추가
posts.append("맛집 탐방")
print(posts)            # ['첫 게시물', '여행 사진', '맛집 탐방']

# insert: 원하는 자리에 끼워넣기 (인덱스, 값)
posts.insert(0, "공지글")
print(posts)            # ['공지글', '첫 게시물', '여행 사진', '맛집 탐방']

# extend: 여러 개를 한꺼번에 이어붙이기
posts.extend(["운동 기록", "독서 노트"])
print(posts)            # ['공지글', ..., '운동 기록', '독서 노트']

posts.append("맛집 탐방") 은 맨 뒤에 값 하나를 붙여요. 새 게시물을 올리는 것과 같죠. 지난 시간 마무리에서 예고했던 그 append 가 바로 이거예요. insert(0, "공지글") 은 "0번 자리에 끼워넣어라" 예요. 공지글을 맨 위로 올리는 셈이죠. extend([...]) 는 여러 값을 한꺼번에 이어붙여요. append 가 하나씩이라면 extend 는 묶음으로요.

이제 빼는 메서드를 볼게요.

Python
# pop: 맨 뒤를 꺼내며 그 값을 돌려받는다 (꺼낸 값을 쓸 수 있다)
last = posts.pop()
print(last)             # 독서 노트
print(posts)

# pop(인덱스): 특정 자리를 꺼낸다
first = posts.pop(0)
print(first)            # 공지글

# remove: 값으로 찾아서 첫 번째 하나 삭제
posts.remove("여행 사진")
print(posts)            # ['첫 게시물', '맛집 탐방', '운동 기록']

# del: 인덱스로 삭제
del posts[0]
print(posts)            # ['맛집 탐방', '운동 기록']

빼는 방법이 세 가지나 되네요. 비슷해 보이지만 쓰임새가 조금씩 달라요. pop() 은 맨 뒤를 꺼내면서 그 값을 돌려줘요. last = posts.pop() 처럼 꺼낸 값을 변수에 담아 쓸 수 있죠. pop(0) 처럼 번호를 주면 그 자리를 꺼내고요. remove("여행 사진") 은 번호가 아니라 값으로 찾아서 지워요. "여행 사진 이라는 글 좀 지워줘" 처럼요. del posts[0] 은 번호로 지우는데, pop 과 달리 값을 돌려받지 않고 그냥 삭제만 해요.

정리하면, 꺼낸 값을 쓸 거면 pop, 값으로 찾아 지울 거면 remove, 번호로 그냥 지울 거면 del 이에요.

💡 한 줄 정리

append(맨 뒤 추가)·insert(자리 지정 추가)·extend(묶음 추가)로 담고, pop(꺼내며 돌려받기)·remove(값으로 삭제)·del(번호로 삭제)로 빼요.

🙋 학생 질문 — "지우는 게 pop·remove·del 셋이나 있어요? 하나만 외우면 안 돼요?"

상황이 조금씩 달라서 세 개를 다 준비해 둔 거예요. 외울 땐 "무엇으로 지우나" 로 구분하면 쉬워요.

  • 꺼낸 값을 다시 쓸 거예요pop() (꺼내면서 그 값을 돌려줘요)
  • 값을 알지만 번호는 몰라요remove("여행 사진") (값으로 찾아 지워요)
  • 번호로 그냥 지우기만 하면 돼요 → del posts[0]

처음엔 appendpop 두 개만 확실히 익혀도 충분해요. 실제로 가장 많이 쓰는 게 이 둘이거든요. 나머지는 "이런 것도 있었지" 하고 필요할 때 찾아 쓰면 됩니다.


Step 4: "줄 세우고 세어보기 — 정렬·탐색·집계"

좋아요 수 목록이 있다고 해봐요. 가장 인기 있는 글은 뭘까요? 좋아요 합계는 얼마일까요? 이런 걸 알려면 리스트를 정렬하고, 찾아보고, 세어봐야 해요. 파이썬은 이 세 가지를 아주 쉽게 해줍니다.

먼저 정렬이에요.

Python
# src/b_structures/b1_sort.py
# 줄 세우고 세어보기 — 정렬·탐색·집계

likes = [8900, 320, 15000, 87, 1200]

# sorted(): 정렬된 '새 리스트' 를 돌려준다 (원본은 그대로)
print(sorted(likes))            # [87, 320, 1200, 8900, 15000]
print(likes)                    # [8900, 320, 15000, 87, 1200]  (원본 유지)

# sort(): 원본을 직접 정렬한다 (돌려주는 값 없음)
likes.sort()
print(likes)                    # [87, 320, 1200, 8900, 15000]

# 큰 순서로 — reverse=True
likes.sort(reverse=True)
print(likes)                    # [15000, 8900, 1200, 320, 87]

정렬하는 방법이 sorted()sort() 두 가지예요. 비슷하지만 중요한 차이가 있어요. sorted(likes) 는 정렬된 새 리스트 를 돌려줘요. 원본 likes 는 그대로 남고요. (출력을 보면 원본이 안 바뀐 게 보이죠.) 반면 likes.sort()원본을 직접 정렬해요. 돌려주는 값이 없는 대신, likes 자체가 정렬된 상태로 바뀌죠. reverse=True 를 넣으면 큰 순서(내림차순)로 정렬해요. 좋아요 많은 순으로 줄 세우는 거예요.

이번엔 찾고 세는 거예요.

Python
# in: 들어있나? (지난 시간 문자열에서 봤던 그 in)
print(15000 in likes)           # True
print(50 in likes)              # False

# index: 그 값이 몇 번째? / count: 몇 번 나오나?
print(likes.index(8900))        # 1
votes = [1, 1, 2, 1, 3]
print(votes.count(1))           # 3

# 집계 — 합·최대·최소
print(sum(likes))               # 25507
print(max(likes))               # 15000
print(min(likes))               # 87

15000 in likesin 은 지난 시간에 봤던 그 in 이에요. "들어있나?" 를 True/False 로 답하죠. index(8900) 은 "그 값이 몇 번째 자리야?" 를, count(1) 은 "그 값이 몇 번 나와?" 를 알려줘요. 그리고 sum(합)·max(최댓값)·min(최솟값) 으로 한 방에 집계할 수 있어요. 좋아요 목록이라면 sum 은 전체 좋아요 합, max 는 가장 많이 받은 좋아요가 되겠죠.

지난 시간엔 합을 구하려고 for 로 하나씩 더했는데, 숫자 리스트라면 sum() 한 줄이면 끝나요.

💡 한 줄 정리

sort() 는 원본을 직접, sorted() 는 새 리스트로 정렬해요(reverse=True 는 큰 순서). in·index·count 로 찾아보고, sum·max·min 으로 한 번에 집계해요.

🙋 학생 질문 — "sort() 랑 sorted() 는 똑같이 정렬하는데 왜 둘 다 있어요?"

"원본을 바꿀 거냐, 안 바꿀 거냐" 의 차이예요.

likes.sort() 는 원본 likes 자체를 정렬된 상태로 바꿔버려요. 원래 순서는 사라지죠. sorted(likes) 는 원본은 그대로 두고, 정렬된 새 리스트를 따로 만들어 줘요.

그래서 "원래 올라온 순서도 나중에 필요해" 하면 sorted() 로 새 리스트를 받고, "이제 정렬된 순서만 쓸 거야" 하면 sort() 로 원본을 바꾸면 됩니다.

이 "원본을 바꾸나, 새로 만드나" 는 오늘 Step 5에서 더 깊이 다룰, 리스트의 아주 중요한 성질이에요.


Step 5: "바뀌는 그릇, 안 바뀌는 값 — 가변 vs 불변"

지난 단계에서 자꾸 "원본을 바꾼다 / 안 바꾼다" 가 나왔죠. 이게 오늘 가장 중요하면서도, 비전공자분들이 처음에 가장 많이 놀라는 부분이에요. 조금 까다로울 수 있는데, "아직 완벽히 이해 못 해도 괜찮아요." 한 번 보고 "이런 함정이 있구나" 만 챙겨도 충분해요.

핵심은 이거예요. 리스트는 바뀌고(가변), 문자열은 안 바뀌어요(불변).

Python
# src/b_structures/b1_mutable.py
# 바뀌는 그릇(리스트)과 안 바뀌는 값(문자열)

# 리스트는 '가변(mutable)' — 만든 뒤에도 내용을 바꿀 수 있다
likes = [8900, 320]
likes.append(15000)
likes[0] = 9000
print(likes)            # [9000, 320, 15000]

# 문자열은 '불변(immutable)' — 한 글자도 직접 못 바꾼다
name = "jaehoon"
upper = name.upper()    # 바꾸는 게 아니라 '새 문자열' 을 돌려받는 것
print(name)             # jaehoon   (원본 그대로!)
print(upper)            # JAEHOON

리스트는 append 로 칸을 늘리고 likes[0] = 9000 으로 자리를 고쳤어요. 만든 뒤에도 자유롭게 바뀌죠. 이걸 가변(mutable) 이라고 해요. 반대로 문자열은 어떨까요? name.upper()name 을 대문자로 바꾸는 것 같지만, 출력을 보면 name 은 여전히 "jaehoon" 이에요! upper() 는 원본을 건드리지 않고 새 대문자 문자열을 따로 만들어 돌려준 거예요. 그래서 upper 변수에 담아야 쓸 수 있었죠. 이렇게 한 번 만들면 못 바꾸는 걸 불변(immutable) 이라고 해요. 지난 시간 "문자열은 한 글자도 직접 못 바꾼다" 던 게 바로 이거예요.

자, 이제 진짜 함정이에요. 집중해서 봐주세요.

Python
# 함정: b = a 는 '같은 그릇' 에 이름표를 하나 더 붙이는 것!
a = [1, 2, 3]
b = a                   # 복사가 아니다! a 와 b 는 같은 리스트
b.append(4)
print(a)                # [1, 2, 3, 4]  ← a 도 같이 바뀜!
print(b)                # [1, 2, 3, 4]

b = a 라고 썼으니 ba 의 복사본일 것 같죠? 아니에요. 리스트에서 b = a 는 그릇을 복사하는 게 아니라, 같은 그릇에 이름표를 하나 더 붙이는 거예요. 그래서 b.append(4)b 를 바꿨는데 a 도 같이 [1, 2, 3, 4] 가 돼버려요. 둘이 같은 그릇이니까요.

그림으로 보면 이래요.

텍스트
   a = [1, 2, 3]
   b = a

        a ─┐
           ├── [1, 2, 3, 4]    그릇은 하나, 이름표만 둘
        b ─┘

   b.append(4) 했더니 a 로 봐도 [1, 2, 3, 4]

그럼 진짜 복사는 어떻게 할까요?

Python
# 진짜 복사하려면 — [:] 또는 .copy()
original = [1, 2, 3]
clone = original[:]     # 통째 슬라이싱 = 새 그릇
clone.append(99)
print(original)         # [1, 2, 3]   (안 바뀜!)
print(clone)            # [1, 2, 3, 99]

copy2 = original.copy() # .copy() 도 같은 효과
copy2.append(77)
print(original)         # [1, 2, 3]

original[:] 처럼 통째로 슬라이싱하면 새 그릇 이 만들어져요. Step 2에서 배운 슬라이싱을 통째로 한 거죠. 이렇게 만든 clone 을 바꿔도 original 은 그대로예요. 이제 둘은 다른 그릇이니까요. .copy() 메서드도 똑같이 새 그릇을 만들어 줍니다.

"내용을 바꿔도 원본을 지키고 싶다" 면 [:].copy() 로 복사한 뒤 바꾸세요. 이 함정 하나만 기억해도 나중에 정말 골치 아픈 버그를 피할 수 있어요.

💡 한 줄 정리

리스트는 가변(바뀜), 문자열은 불변(안 바뀜)이에요. 특히 b = a 는 복사가 아니라 같은 그릇에 이름표를 더 붙이는 것이라, 진짜 복사는 a[:]a.copy() 로 해야 해요.

🙋 학생 질문 — "b = a 가 복사가 아니라니 불편한데, 왜 그렇게 만들어 놨어요?"

처음 보면 정말 함정 같죠. 그런데 여기엔 이유가 있어요.

리스트는 좋아요 수백 개, 게시물 수천 개를 담는 큰 그릇이 될 수 있어요. b = a 할 때마다 그 큰 그릇을 통째로 복사하면, 메모리도 많이 쓰고 느려져요. 그래서 파이썬은 기본적으로 "그릇은 그대로 두고 이름표만 붙이는" 가벼운 방식을 택했어요.

대신 "진짜 복사가 필요할 때만" [:].copy() 로 명시하게 한 거죠. 지금은 "리스트를 b = a 로 넘기면 같은 그릇" 이라는 것, 복사는 [:]/.copy() 라는 것만 기억하면 충분해요. 참고로 숫자나 문자열은 불변이라 이런 함정이 없어요. 이 함정은 리스트처럼 바뀌는 그릇에서만 생겨요.


Step 6: "묶어서 한 번에 — 튜플과 언패킹"

리스트는 마음껏 바꿀 수 있어서 편했죠. 그런데 가끔은 "바뀌면 안 되는 값" 도 있어요. 예를 들어 게시물에 찍힌 위치 좌표 (위도, 경도) 는 한 번 정해지면 바뀌면 안 되겠죠. 위도 따로 경도 따로 흩어지면 의미가 깨지고요. 이렇게 여러 값을 한 묶음으로 묶되, 바꾸지 못하게 하는 게 튜플(tuple) 이에요. 리스트가 대괄호 [] 라면, 튜플은 소괄호 () 를 써요.

Python
# src/b_structures/b1_tuple.py
# 튜플: 묶어서 한 번에, 그리고 못 바꾼다

# 좌표처럼 '한 쌍' 으로 묶여 의미 있는 값 — 튜플 (소괄호)
location = (37.5, 127.0)        # (위도, 경도)
print(location)                 # (37.5, 127.0)
print(location[0])              # 37.5   (인덱싱은 리스트와 똑같이)
print(type(location))           # <class 'tuple'>

location = (37.5, 127.0) 으로 위도와 경도를 한 묶음으로 묶었어요. 값을 꺼낼 땐 리스트와 똑같이 location[0] 처럼 번호로 꺼내요. type 을 찍으면 <class 'tuple'>, 즉 튜플이라고 알려주고요.

튜플의 진짜 매력은 언패킹(unpacking) 이에요. 묶음을 한 번에 풀어 여러 변수에 담는 거죠.

Python
# 언패킹: 묶음을 한 번에 풀어 변수에 담기
lat, lng = location
print(lat)                      # 37.5
print(lng)                      # 127.0

# 두 값 동시에 교환(스왑) — 튜플 언패킹의 깔끔한 활용
a = 10
b = 20
a, b = b, a
print(a, b)                     # 20 10

lat, lng = location 한 줄로 묶음이 풀려 lat 엔 위도, lng 엔 경도가 담겼어요. 좌표를 따로따로 꺼내 쓰기 편하죠. 그리고 이 언패킹의 멋진 활용이 스왑(맞바꾸기) 이에요. a, b = b, a 한 줄로 두 변수의 값이 통째로 교환돼요. 다른 언어에선 임시 변수를 둬야 하는 일을, 파이썬은 이렇게 한 줄로 끝냅니다.

마지막으로, 튜플이 정말 "못 바꾸는지" 확인해 볼게요.

Python
# 튜플은 '불변' — 인덱싱으로 읽기는 되지만 자리를 고치진 못한다
point = (1, 2)
print(point[0])                 # 1
# point[0] = 99   # 이 줄은 에러! (b1_tuple_error.py 에서 직접 확인)

읽기(point[0])는 되지만, 고치려 하면 에러가 나요. 지난 시간처럼 "에러는 친구" 니까, 일부러 에러를 내보고 파이썬이 뭐라는지 들어봅시다.

Python
# src/b_structures/b1_tuple_error.py
point = (1, 2)
point[0] = 99   # 튜플은 불변! 자리를 고치려 하면 에러

실행하면 이런 빨간 메시지가 떠요.

텍스트
TypeError: 'tuple' object does not support item assignment

"tuple 객체는 자리 바꾸기(item assignment)를 지원하지 않아요" 라는 뜻이에요. 바로 이 "못 바꿈" 이 튜플의 핵심이에요. 좌표처럼 바뀌면 안 되는 값을 안전하게 지켜주죠.

💡 한 줄 정리

튜플은 소괄호 () 로 여러 값을 묶는 불변 그릇이에요. lat, lng = location 처럼 언패킹으로 한 번에 풀고, a, b = b, a 로 값을 맞바꿀 수 있어요. 바꾸려 하면 에러가 나며 값을 지켜줘요.

🙋 학생 질문 — "리스트가 있는데 튜플은 왜 또 필요해요? 못 바꾸는 게 무슨 장점이죠?"

"못 바꾼다" 가 단점 같지만, 오히려 그게 장점이 되는 상황이 있어요.

좌표 (위도, 경도) 나 RGB 색상 (255, 128, 0) 처럼, 한 묶음이 통째로 의미를 갖는 값 을 생각해 보세요. 여기서 누군가 실수로 위도만 슬쩍 바꿔버리면 좌표 전체가 엉뚱해지겠죠. 튜플로 묶어두면 "이건 못 바꿔" 라고 파이썬이 막아주니, 실수로 망가질 일이 없어요.

정리하면, 자주 바뀌는 목록은 리스트, 한 번 정하면 안 바뀌어야 하는 묶음은 튜플 이에요. 지금은 "좌표 같은 건 튜플로 묶는다" 정도만 알아도 충분해요.


Step 7: "리스트 안의 리스트 — 중첩 리스트"

지금까지 리스트엔 숫자나 글자를 담았어요. 그런데 리스트 안에 또 리스트 를 담을 수도 있어요. 게시물마다 받은 좋아요들을 묶어서 관리한다고 해봐요. "1번 게시물의 좋아요들", "2번 게시물의 좋아요들"... 각 게시물이 좋아요 리스트를 하나씩 갖는 거죠. 이렇게 리스트가 겹쳐진 걸 중첩 리스트(nested list) 라고 해요.

Python
# src/b_structures/b1_nested.py
# 리스트 안의 리스트 — 중첩 리스트

# 게시물별 좋아요 수 묶음 (게시물마다 받은 좋아요들)
feed = [
    [8900, 320, 1500],     # 1번 게시물이 받은 좋아요들
    [87, 1200],            # 2번 게시물
    [15000, 4300, 900],    # 3번 게시물
]

# 바깥 인덱스 → 안쪽 인덱스 순서로 꺼낸다
print(feed[0])             # [8900, 320, 1500]   (1번 게시물 전체)
print(feed[0][0])          # 8900                (1번 게시물의 첫 좋아요)
print(feed[2][1])          # 4300                (3번 게시물의 두 번째)

feed 는 리스트 세 개를 담은 리스트예요. 각 칸이 또 하나의 리스트인 거죠. 값을 꺼낼 땐 번호를 두 번 써요. feed[0] 은 1번 게시물 전체 리스트, feed[0][0] 은 "1번 게시물의 첫 좋아요" 예요. 바깥 번호로 게시물을 고르고, 안쪽 번호로 그 안의 좋아요를 고르는 거예요.

그림으로 보면 이렇게 두 층이에요.

텍스트
   feed = [ [8900, 320, 1500] ,  [87, 1200] ,  [15000, 4300, 900] ]
            └──── feed[0] ────┘  └─ feed[1] ┘  └───── feed[2] ─────┘

   feed[0]      [8900, 320, 1500]    (바깥 0번 = 1번 게시물 전체)
   feed[0][0]   8900                 (그 안의 0번 = 첫 좋아요)
   feed[2][1]   4300                 (바깥 2번의 안쪽 1번)

이제 중첩 리스트를 for 로 훑어볼게요.

Python
print(len(feed))           # 3   (게시물 수)
print(len(feed[0]))        # 3   (1번 게시물의 좋아요 개수)

# 중첩 for — 게시물마다 좋아요 합을 구한다
post_number = 1
for likes in feed:
    total = sum(likes)
    print(f"{post_number}번 게시물 좋아요 합계: {total}")
    post_number = post_number + 1

len(feed) 는 게시물 수(3), len(feed[0]) 은 1번 게시물의 좋아요 개수(3)예요. 바깥과 안쪽 길이를 따로 잴 수 있죠. for likes in feed:feed 를 하나씩 도는데, 꺼내지는 likes 가 각 게시물의 좋아요 리스트 예요. 그 리스트에 Step 4의 sum() 을 쓰면 게시물별 좋아요 합이 나오죠.

실행하면 이렇게 나와요.

텍스트
3
3
1번 게시물 좋아요 합계: 10720
2번 게시물 좋아요 합계: 1287
3번 게시물 좋아요 합계: 20200

Step 4의 집계와 지난 시간의 for 가 중첩 리스트와 만나니, 게시물별 통계가 술술 나오네요.

💡 한 줄 정리

리스트 안에 리스트를 담은 게 중첩 리스트예요. feed[0][0] 처럼 번호를 두 번 써서 꺼내고, for 로 돌면 각 칸이 안쪽 리스트가 되어 sum() 같은 집계를 게시물마다 적용할 수 있어요.

🙋 학생 질문 — "중첩 리스트는 몇 층까지 넣을 수 있어요?"

이론적으로는 층 제한이 거의 없어요. 리스트 안의 리스트 안의 리스트... 계속 겹칠 수 있죠.

하지만 실무에선 보통 두 층(feed[i][j])을 가장 많이 써요. 표(행과 열)나 격자처럼 "줄과 칸" 으로 떨어지는 데이터가 딱 두 층이거든요. 세 층, 네 층으로 깊어지면 data[1][0][2][3] 처럼 번호가 줄줄이 붙어서, 읽는 사람도 짜는 사람도 헷갈려요.

그래서 너무 깊어진다 싶으면, 다음 시간에 배울 다른 그릇으로 정리하는 게 더 깔끔해요. 지금은 "두 층까지가 흔하다" 정도만 알면 충분합니다.


Step 8: "오늘 배운 걸 한데 모아 — 게시물 매니저"

자, 오늘의 마지막입니다. 지금까지 배운 걸 전부 모아 작은 프로그램을 만들어 볼게요. 게시물 목록 매니저 — 게시물을 추가하고, 좋아요 순으로 줄 세우고, 통계를 내는 프로그램이에요. 각 게시물을 [좋아요 수, 제목] 형태의 작은 리스트로 묶고, 그걸 다시 리스트에 담을 거예요. (Step 7의 중첩 리스트죠!)

먼저 게시물을 담고 새 글을 추가해요.

Python
# src/b_structures/b1_manager.py
# 종합 실습 — 게시물 목록 매니저
# (각 게시물 = [좋아요 수, 제목] 형태로 담는다)

feed = [
    [8900, "여행 사진"],
    [320, "맛집 탐방"],
    [15000, "운동 기록"],
]

# 1) 새 게시물 추가
feed.append([1200, "독서 노트"])
print(f"게시물 수: {len(feed)}개")

feed 는 게시물 세 개를 담은 중첩 리스트예요. 각 칸이 [좋아요, 제목] 묶음이죠. feed.append([1200, "독서 노트"]) 로 새 게시물을 맨 뒤에 추가했어요. (Step 3의 append!) len(feed) 로 게시물 수를 세면 4개가 나와요.

이제 좋아요 많은 순으로 정렬하고 순위를 매겨요.

Python
# 2) 좋아요 많은 순으로 정렬
#    각 칸이 [좋아요, 제목] 이라, 정렬하면 첫 칸(좋아요) 기준으로 줄을 선다
feed.sort(reverse=True)

# 3) 순위 출력
rank = 1
for post in feed:
    post_likes = post[0]
    title = post[1]
    print(f"{rank}위: {title} (좋아요 {post_likes})")
    rank = rank + 1

여기 작은 마법이 있어요. feed.sort(reverse=True) 한 줄로 좋아요 순 정렬이 됩니다. 각 칸이 [좋아요, 제목] 이라, 정렬하면 파이썬이 첫 칸(좋아요 수)을 기준으로 줄을 세워요. 그래서 좋아요 많은 게시물이 위로 올라오죠. 그다음 for 로 돌면서 post[0](좋아요)과 post[1](제목)을 꺼내 순위를 출력해요. Step 7의 중첩 인덱싱이네요.

마지막으로 통계를 내요.

Python
# 4) 통계 — 인기글(1000 이상) 세기 + 전체 좋아요 합
popular_count = 0
total_likes = 0
for post in feed:
    post_likes = post[0]
    total_likes = total_likes + post_likes
    if post_likes >= 1000:
        popular_count = popular_count + 1

print(f"인기글: {popular_count}개 / 전체 좋아요: {total_likes}")

# 5) 가장 인기 있는 게시물 (정렬했으니 맨 앞)
top = feed[0]
print(f"최고 인기글: {top[1]} (좋아요 {top[0]})")

for 로 돌며 좋아요를 더해 합(total_likes)을 내고, 1000 이상이면 인기글 수(popular_count)를 1씩 늘려요. 지난 시간의 if 와 카운트가 그대로 쓰였죠. 그리고 이미 좋아요 순으로 정렬했으니, 맨 앞 feed[0] 이 최고 인기글이에요.

전체를 실행하면 이렇게 나와요.

텍스트
게시물 수: 4개
1위: 운동 기록 (좋아요 15000)
2위: 여행 사진 (좋아요 8900)
3위: 독서 노트 (좋아요 1200)
4위: 맛집 탐방 (좋아요 320)
인기글: 3개 / 전체 좋아요: 25420
최고 인기글: 운동 기록 (좋아요 15000)

오늘 배운 리스트 생성·append·sort·중첩 리스트·for 순회·if 가 한 프로그램에 모두 모였어요. 이게 바로 게시물 목록을 다루는 백엔드의 출발점이랍니다.

💡 한 줄 정리

게시물을 [좋아요, 제목] 중첩 리스트로 담아, append(추가)·sort(reverse=True)(좋아요 순 정렬)·for+if(통계)를 한데 엮으면 작은 게시물 매니저가 완성돼요.

🙋 학생 질문 — "feed.sort() 가 어떻게 좋아요 기준으로 정렬되는 거예요? 제목 기준이 아니라?"

좋은 관찰이에요! 비밀은 각 칸을 어떤 순서로 묶었는지에 있어요.

각 게시물을 [좋아요, 제목] 으로, 좋아요를 맨 앞에 두고 묶었죠. 리스트끼리 정렬할 때 파이썬은 먼저 첫 칸끼리 비교해요. 여기선 첫 칸이 좋아요 수니까, 좋아요 기준으로 줄이 서는 거예요. 만약 [제목, 좋아요] 순으로 묶었다면 제목(글자) 기준으로 정렬됐겠죠.

그래서 "무엇을 기준으로 정렬하고 싶은가" 를 맨 앞 칸에 두면 돼요. 참고로 "좋아요는 그대로 두고 제목 기준으로 정렬" 처럼 더 자유롭게 기준을 고르는 방법은 나중 과목(함수를 배운 뒤)에 나와요. 지금은 "맨 앞 칸 기준으로 정렬된다" 만 챙기면 충분해요.


마무리

오늘 드디어 값을 하나씩이 아니라 여러 개를 한 그릇에 담기 시작했어요. 리스트와 튜플은 앞으로 다룰 거의 모든 데이터의 기본 그릇이에요. 정말 고생 많으셨어요.

오늘 배운 핵심 세 가지

💡 하나: 리스트([])는 여러 값을 번호 매겨 담는 그릇이에요. [번호] 로 꺼내고(인덱싱), [시작:끝] 으로 잘라내고(슬라이싱), append·pop·sort 로 담고 빼고 줄 세웠어요. 지난 시간의 split() 도 사실 리스트를 만들고 있었죠.

💡 : 리스트는 가변(바뀜), 문자열·튜플은 불변(안 바뀜)이에요. 특히 b = a 는 복사가 아니라 같은 그릇에 이름표를 더 붙이는 것이라, 진짜 복사는 [:].copy() 로 해야 한다는 함정을 만났어요.

💡 : 튜플(())은 못 바꾸는 묶음이라 좌표 같은 값을 안전하게 지키고, 언패킹(lat, lng = location)으로 한 번에 풀 수 있어요. 리스트 안에 리스트를 담는 중첩 리스트로 게시물별 데이터까지 다뤘고요.

다음 시간 예고

다음 시간엔 "딕셔너리(dict)와 집합(set)" 을 배웁니다. 오늘 리스트는 값을 번호(0, 1, 2...) 로 찾았죠? 그런데 게시물 좋아요 수가 100개라면, 87번 게시물의 좋아요를 번호로 찾는 건 너무 불편해요. 다음 시간엔 번호 대신 이름표(key) 로 찾는 그릇을 배워요. 프로필["이름"], 프로필["팔로워수"] 처럼요. 사전에서 단어로 뜻을 찾듯이요. 그리고 "중복 없는 모음" 인 집합으로, 두 사람의 팔로워에서 맞팔(교집합) 을 한 줄로 골라내는 것도 해볼 거예요. 오늘의 리스트가 "순서 있는 목록" 이라면, 다음 시간엔 "이름표로 찾기" 와 "중복 없는 모음" 으로 넓혀갑니다. 기대해 주세요!


과제

오늘 배운 리스트와 튜플을 직접 손으로 익혀봅시다. 눈으로 본 것과 직접 짜보는 것은 완전히 다르답니다.

[기초] 좋아요 통계 내기

좋아요 수 목록을 받아 기본 통계를 내는 프로그램을 만들어 보세요.

  • 좋아요 수 다섯 개 정도를 리스트로 준비합니다 (예: [8900, 320, 15000, 87, 1200])
  • len() 으로 게시물 수, sum() 으로 전체 좋아요 합, max()/min() 으로 최고·최저 좋아요를 구합니다
  • sorted() 로 정렬된 새 리스트를 만들어, 원본은 그대로 두고 정렬 결과를 함께 출력합니다
  • f-string 으로 "게시물 N개 / 합계 N / 최고 N / 최저 N" 형태로 보기 좋게 출력해 보세요

[응용] 게시물 목록 관리하기

게시물을 추가하고 삭제하는 작은 관리 프로그램을 만들어 보세요.

  • 게시물 제목 두세 개를 리스트로 준비합니다
  • append() 로 새 게시물을 맨 뒤에 추가하고, insert(0, ...) 로 공지글을 맨 앞에 끼워넣습니다
  • pop() 으로 맨 뒤 게시물을 꺼내 "삭제된 게시물: ..." 으로 출력합니다
  • 중간에 len() 으로 게시물 수가 어떻게 변하는지 단계마다 출력해, 추가·삭제가 반영되는 걸 눈으로 확인해 보세요

[심화] 게시물 순위표 만들기

게시물별 좋아요로 순위표를 만들어 보세요. (오늘 Step 8을 직접 응용해 보는 과제예요.)

  • 각 게시물을 [좋아요 수, 제목] 형태로 묶어, 게시물 네 개를 중첩 리스트로 준비합니다
  • sort(reverse=True) 로 좋아요 많은 순으로 정렬합니다 (각 칸의 첫 번째가 좋아요라 그 기준으로 줄을 서요)
  • for 로 돌며 "N위: 제목 (좋아요 N)" 형태로 순위를 출력합니다
  • (도전) 좋아요 1000 이상인 인기글이 몇 개인지 forif 로 세어 함께 출력해 보세요

생각해볼 주제

정답이 하나로 정해진 질문이 아니에요. 혼자 곰곰이 생각해 보거나, 함께 공부하는 동료와 이야기 나눠 보면 좋습니다.

1. 왜 리스트는 바뀌고, 튜플은 안 바뀌게 나눠 놨을까?

오늘 리스트(가변)와 튜플(불변)을 둘 다 배웠어요. "하나로 통일하면 덜 헷갈릴 텐데" 싶기도 하죠. 그런데 파이썬은 굳이 둘을 나눠 뒀어요. 자주 바뀌는 게시물 목록엔 리스트가, 한 번 정하면 안 바뀌어야 하는 좌표엔 튜플이 어울리는 것처럼요. "바꿀 수 있다" 와 "바꿀 수 없다" 가 각각 어떤 상황에서 장점이 되는지 생각해 보세요.

2. b = a 가 복사가 아닌 게 함정일까, 합리적인 설계일까?

오늘 b = a 가 그릇을 복사하지 않고 이름표만 붙인다는 함정을 만났어요. 처음엔 "왜 이렇게 헷갈리게 만들었지?" 싶죠. 그런데 큰 리스트를 넘길 때마다 통째로 복사한다면 어떤 문제가 생길까요? 파이썬의 이 선택이 불편한 함정인지, 아니면 그럴 만한 이유가 있는 합리적 설계인지 따져 보세요.

3. 번호로 찾기 vs 이름표로 찾기

오늘 리스트는 값을 번호([0], [1])로 찾았어요. 그런데 "재훈이의 팔로워 수" 를 찾고 싶을 때, 그게 리스트의 몇 번 자리인지 번호로 기억하는 건 불편하겠죠. 다음 시간에 배울 "이름표로 찾기"(프로필["팔로워수"])는 이런 불편을 어떻게 풀어줄까요? 번호로 찾는 게 더 편한 경우와, 이름표로 찾는 게 더 편한 경우를 각각 떠올려 보세요.

✅ 예시 답안정답 보기

과제 예시답안

🎯 [기초] 좋아요 통계 내기 — 예시답안

채점 포인트

항목 배점 기준
리스트 생성 + len/sum 30% 좋아요를 리스트로 담아 개수·합을 구했는가
max/min 집계 30% 최고·최저 좋아요를 구했는가
sorted() 로 원본 보존 정렬 40% 원본은 그대로 두고 정렬된 새 리스트를 따로 만들었는가

풀이 예시

Python
# src/b_structures/b1_likes_stats_solution.py
# [기초] 좋아요 통계 내기

likes = [8900, 320, 15000, 87, 1200]

count = len(likes)
total = sum(likes)
highest = max(likes)
lowest = min(likes)

# sorted() 는 정렬된 '새 리스트' — 원본은 그대로 둔다
ordered = sorted(likes)

print(f"게시물 {count}개 / 합계 {total} / 최고 {highest} / 최저 {lowest}")
print(f"원본 순서: {likes}")
print(f"정렬 결과: {ordered}")

실행하면 이렇게 나옵니다.

텍스트
게시물 5개 / 합계 25507 / 최고 15000 / 최저 87
원본 순서: [8900, 320, 15000, 87, 1200]
정렬 결과: [87, 320, 1200, 8900, 15000]

💡 튜터의 한마디 sum·max·min 은 숫자 리스트라면 한 줄로 집계를 끝내줘요. 지난 시간엔 합을 구하려고 for 로 하나씩 더했는데, 숫자 리스트엔 sum() 한 방이면 충분하죠. 그리고 sorted() 를 쓰면 원본 순서를 잃지 않고 정렬 결과를 따로 받을 수 있어요. "원래 올라온 순서" 도 나중에 필요할 때 이 방식이 안전합니다.

자주 하는 실수

실수 무슨 일이 생기나 해결법
likes.sort() 로 정렬 원본 likes 가 정렬돼 버려 원래 순서가 사라짐 원본을 지킬 땐 sorted(likes) 로 새 리스트를 받기
print(sorted) 처럼 괄호 빠뜨림 함수 자체가 출력돼 엉뚱한 결과 sorted(likes) 처럼 괄호로 호출하기

🎯 [응용] 게시물 목록 관리하기 — 예시답안

채점 포인트

항목 배점 기준
append + insert 40% 맨 뒤 추가와 맨 앞 끼워넣기를 모두 썼는가
pop 으로 꺼낸 값 활용 30% 꺼낸 게시물을 변수로 받아 출력했는가
단계별 len 확인 30% 추가·삭제마다 개수 변화를 출력했는가

풀이 예시

Python
# src/b_structures/b1_post_manager_solution.py
# [응용] 게시물 목록 관리하기

posts = ["첫 게시물", "여행 사진"]
print(f"시작: {posts} ({len(posts)}개)")

# 새 게시물 추가
posts.append("맛집 탐방")
print(f"추가 후: {posts} ({len(posts)}개)")

# 공지글을 맨 앞에 끼워넣기
posts.insert(0, "공지글")
print(f"공지 후: {posts} ({len(posts)}개)")

# 맨 뒤 게시물 꺼내기
removed = posts.pop()
print(f"삭제된 게시물: {removed}")
print(f"삭제 후: {posts} ({len(posts)}개)")

실행하면 이렇게 나옵니다.

텍스트
시작: ['첫 게시물', '여행 사진'] (2개)
추가 후: ['첫 게시물', '여행 사진', '맛집 탐방'] (3개)
공지 후: ['공지글', '첫 게시물', '여행 사진', '맛집 탐방'] (4개)
삭제된 게시물: 맛집 탐방
삭제 후: ['공지글', '첫 게시물', '여행 사진'] (3개)

💡 튜터의 한마디 appendinsert(0, ...) 의 차이가 출력에 또렷이 보이죠. 하나는 맨 뒤, 하나는 맨 앞이에요. pop() 의 진가는 "꺼내면서 그 값을 돌려준다" 예요. removed = posts.pop() 으로 꺼낸 게시물을 받아 "삭제된 게시물: ..." 으로 알려줄 수 있어요. 단순히 지우기만 하는 del 과 다른 점이죠.

자주 하는 실수

실수 무슨 일이 생기나 해결법
removed = posts.append(...) append 는 돌려주는 값이 없어 removedNone 이 됨 꺼낸 값을 받는 건 pop, append 는 추가만
insert("공지글") 처럼 자리 번호 생략 인자가 모자라 에러 insert(0, "공지글") 처럼 (번호, 값) 둘 다 주기

🎯 [심화] 게시물 순위표 만들기 — 예시답안

채점 포인트

항목 배점 기준
[좋아요, 제목] 중첩 리스트 30% 좋아요를 앞에 둔 묶음으로 구성했는가
sort(reverse=True) 정렬 30% 좋아요 많은 순으로 정렬했는가
for 순위 출력 + 인기글 세기 40% 순위를 출력하고 1000 이상을 셌는가

풀이 예시

Python
# src/b_structures/b1_ranking_solution.py
# [심화] 게시물 순위표 만들기

feed = [
    [8900, "여행 사진"],
    [320, "맛집 탐방"],
    [15000, "운동 기록"],
    [1200, "독서 노트"],
]

# 좋아요 많은 순으로 정렬 (각 칸의 첫 번째 = 좋아요 기준)
feed.sort(reverse=True)

rank = 1
for post in feed:
    likes = post[0]
    title = post[1]
    print(f"{rank}위: {title} (좋아요 {likes})")
    rank = rank + 1

# 인기글(1000 이상) 세기
popular_count = 0
for post in feed:
    if post[0] >= 1000:
        popular_count = popular_count + 1

print(f"인기글: {popular_count}개")

실행하면 이렇게 나옵니다.

텍스트
1위: 운동 기록 (좋아요 15000)
2위: 여행 사진 (좋아요 8900)
3위: 독서 노트 (좋아요 1200)
4위: 맛집 탐방 (좋아요 320)
인기글: 3개

💡 튜터의 한마디 핵심은 각 게시물을 [좋아요, 제목] 으로, 좋아요를 맨 앞에 둔 거예요. 리스트끼리 정렬하면 파이썬은 첫 칸부터 비교하니, 자연스럽게 좋아요 기준으로 줄이 섭니다. 만약 [제목, 좋아요] 순으로 묶었다면 제목(글자) 기준으로 정렬됐을 거예요. "무엇으로 정렬할지" 를 맨 앞 칸에 두는 게 오늘의 작은 요령이에요.

자주 하는 실수

실수 무슨 일이 생기나 해결법
[제목, 좋아요] 순으로 묶음 제목(글자) 기준으로 정렬돼 순위가 엉뚱함 정렬 기준인 좋아요를 맨 앞에 두기
reverse=True 빠뜨림 좋아요 적은 순(오름차순)으로 정렬됨 많은 순은 sort(reverse=True)

생각해볼 주제 예시답안


🤔 [생각해볼 주제 1] 왜 리스트는 바뀌고, 튜플은 안 바뀌게 나눠 놨을까?

문제 상황 요약

오늘 리스트(가변)와 튜플(불변)을 둘 다 배웠어요. "하나로 통일하면 덜 헷갈릴 텐데" 싶기도 하죠. 그런데 파이썬은 굳이 둘을 나눠 뒀어요. "바꿀 수 있다" 와 "바꿀 수 없다" 가 각각 어떤 상황에서 장점이 될까요?

튜터의 가이드 및 해설

핵심은 "이 값이 살면서 바뀔 운명인가" 예요.

게시물 목록을 떠올려 보세요. 새 글이 올라오고, 지워지고, 순서가 바뀌죠. 이렇게 계속 변하는 데이터엔 마음껏 바꿀 수 있는 리스트가 어울려요. append 로 더하고 pop 으로 빼면 되니까요.

반대로 위치 좌표 (위도, 경도) 나 색상 (255, 128, 0) 을 떠올려 보세요. 이건 한 묶음이 통째로 하나의 의미예요. 위도만 슬쩍 바뀌면 좌표 전체가 엉뚱해지죠. 튜플로 묶어두면 파이썬이 "이건 못 바꿔" 라고 막아주니, 실수로 망가질 일이 없어요.

그러니까 "안 바뀌는 것" 은 불편한 제약이 아니라 안전장치 예요. 중요한 값에 자물쇠를 채워두면, 나도 동료도 그 값을 실수로 건드릴 수 없거든요.

💡 핵심을 한마디로

"자주 바뀌는 목록은 리스트, 한 번 정하면 안 바뀌어야 하는 묶음은 튜플이에요. 튜플의 '못 바꿈' 은 제약이 아니라, 중요한 값을 실수로 망가뜨리지 않게 채워둔 자물쇠예요."


🤔 [생각해볼 주제 2] b = a 가 복사가 아닌 게 함정일까, 합리적인 설계일까?

문제 상황 요약

오늘 b = a 가 그릇을 복사하지 않고 이름표만 붙인다는 함정을 만났어요. 처음엔 "왜 이렇게 헷갈리게 만들었지?" 싶죠. 그런데 큰 리스트를 넘길 때마다 통째로 복사한다면 어떤 문제가 생길까요?

튜터의 가이드 및 해설

리스트는 좋아요 수만 개, 게시물 수천 개를 담는 큰 그릇이 될 수 있어요.

만약 b = a 할 때마다 그 큰 그릇을 통째로 복사한다면, 값을 다른 곳에 넘길 때마다 메모리에 똑같은 그릇이 하나씩 더 생겨요. 함수에 리스트를 넘기고, 또 넘기고... 그때마다 복사가 일어나면 메모리도 많이 쓰고 속도도 느려지죠.

그래서 파이썬은 기본을 "가벼운 쪽" 으로 잡았어요. 그릇은 그대로 두고 이름표만 붙이는 거죠. 대신 "진짜 복사가 필요할 때만" [:].copy() 로 명시하게 했어요. "복사는 비싸니까, 정말 필요할 때만 직접 시켜라" 는 합리적인 선택인 셈이에요.

물론 이 약속을 모르면 함정이 돼요. b = ab 를 바꿨는데 a 도 바뀌어 깜짝 놀라죠. 그래서 이건 "나쁜 설계" 라기보다, 이유 있는 설계지만 반드시 알아둬야 하는 약속 이에요.

💡 핵심을 한마디로

"큰 리스트를 넘길 때마다 통째로 복사하면 메모리와 속도가 낭비돼요. 그래서 파이썬은 기본을 '이름표만 붙이기' 로 잡고, 진짜 복사는 [:]·.copy() 로 명시하게 했어요. 함정처럼 보이지만 이유 있는 선택이고, 약속만 알면 안전합니다."


🤔 [생각해볼 주제 3] 번호로 찾기 vs 이름표로 찾기

문제 상황 요약

오늘 리스트는 값을 번호([0], [1])로 찾았어요. 그런데 "재훈이의 팔로워 수" 를 찾고 싶을 때, 그게 리스트의 몇 번 자리인지 번호로 기억하는 건 불편하겠죠. 번호로 찾는 게 편한 경우와, 이름표로 찾는 게 편한 경우는 각각 언제일까요?

튜터의 가이드 및 해설

"무엇으로 그 값을 가리키는 게 자연스러운가" 로 갈려요.

번호로 찾는 게 편한 경우는 순서가 의미 있을 때 예요. 게시물을 올린 순서대로 보여주거나, "맨 앞", "맨 뒤", "최근 3개" 처럼 자리로 골라낼 때요. 이럴 땐 0번, 1번, [-1] 같은 번호가 딱이에요. 리스트가 잘 맞죠.

반대로 이름표로 찾는 게 편한 경우는 값마다 고유한 이름이 있을 때 예요. 프로필 정보를 떠올려 보세요. "이름", "팔로워 수", "게시물 수" 처럼요. 이걸 리스트에 [재훈, 1240, 42] 로 담으면, "팔로워 수가 몇 번 자리였더라?" 를 외워야 해요. 헷갈리고 실수하기 쉽죠. 이름표로 프로필["팔로워수"] 라고 쓰면, 번호를 기억할 필요 없이 뜻으로 바로 꺼낼 수 있어요. 사전에서 단어로 뜻을 찾듯이요.

그래서 둘은 경쟁 관계가 아니라 역할 분담 이에요. 순서가 중요하면 리스트(번호), 이름표가 중요하면 다음 시간에 배울 딕셔너리예요.

💡 핵심을 한마디로

"순서가 의미 있으면 번호로 찾는 리스트가, 값마다 고유한 이름이 있으면 이름표로 찾는 딕셔너리가 편해요. 게시물 목록은 리스트, 프로필 정보는 이름표로 찾는 게 자연스럽죠. 둘은 경쟁이 아니라 역할 분담이에요."

전체 목록 파이썬 기초