문자열·리스트·딕셔너리·집합 기초
수업 목표
- 문자열(str)과 리스트(list)의 개념과 특징을 구분하여 설명할 수 있다.
- 인덱싱과 슬라이싱의 동작 원리를 이해하고, 원하는 데이터를 정확하게 추출할 수 있다.
- 리스트의 가변성(Mutable)과 문자열의 불변성(Immutable) 차이를 파악하고 적절한 조작 방식을 적용할 수 있다.
- append, pop, remove, sort 등 주요 리스트 메서드를 적절히 활용하여 데이터를 동적으로 관리할 수 있다.
왜 배워야 할까요?
스마트폰 날씨 앱 화면에 표시되는 일주일간의 기온 목록, 온라인 쇼핑몰의 수많은 추천 상품들, 그리고 소셜 미디어 게시물 아래 달린 수백 개의 댓글까지. 현실 세계에서 마주하는 가치 있는 데이터는 대부분 하나의 독립된 값이 아니라 이처럼 여러 정보가 순서대로 연결된 데이터 집합체의 모습을 띠고 있습니다.
문자열과 리스트는 컴퓨터가 이러한 데이터 집합체를 안전하게 보관하고 편리하게 가공할 수 있게 돕는 가장 강력하고 직관적인 도구입니다. 단 하나의 변수로 대량의 정보를 제어하고, 원하는 구간만 추출하거나(인덱싱과 슬라이싱), 상황에 따라 데이터를 동적으로 추가·삭제하는 리스트 메서드 등의 기초 체력을 다져두어야 비로소 실전 문제 해결과 복잡한 알고리즘을 구현할 수 있게 됩니다.
도입 스토리: 낱개로 담기 vs 묶어서 담기
지금까지 우리는 하나의 변수에 하나의 값만 저장해 사용해 왔습니다. 하지만 게임 속에 등장하는 수많은 아이템 목록이나, 사용자가 입력한 패스워드의 글자 하나하나를 처리하려면 어떻게 해야 할까요? 만약 아이템이 10개라고 해서 10개의 변수를 따로따로 선언한다면 코드는 매우 복잡해지고 관리하기도 어려워질 것입니다.
이러한 문제를 해결하기 위해 여러 데이터를 순서대로 엮어서 보관하는 도구들이 필요합니다. 파이썬에서는 글자들의 연속체인 문자열(str)과 다양한 자료형을 자유롭게 담을 수 있는 상자인 리스트(list)를 제공합니다. 이 도구들을 이용하면 복잡한 데이터 묶음도 단 하나의 변수로 편리하게 다룰 수 있습니다.
1. 문자열과 리스트의 기본 개념
문자열은 따옴표(" 또는 ')로 감싸서 선언하며, 글자가 나열된 형태의 자료형입니다. 리스트는 대괄호([ ]) 안에 쉼표(,)로 구분하여 선언하며, 서로 다른 종류의 데이터들을 한곳에 묶어놓은 자료형입니다.
# 문자열 선언
greeting = "Hello Python"
# 리스트 선언
inventory = ["칼", "방패", "포션", 100, True]
2. 인덱싱과 슬라이싱: 데이터 꺼내기와 잘라내기
문자열과 리스트는 내부에 저장된 값들에 순서대로 방 번호, 즉 인덱스(Index)가 부여됩니다. 이 방 번호를 이용하면 특정 위치의 값을 꺼내거나 원하는 만큼 잘라낼 수 있습니다.
인덱싱 (Indexing): 특정 위치의 값 가리키기
파이썬의 인덱스는 0부터 시작합니다. 앞에서부터 셀 때는 0, 1, 2 순서로 늘어나며, 뒤에서부터 역순으로 탐색할 때는 -1, -2, -3과 같이 음수 인덱스를 사용합니다.
| 궁금증 / 질문 | 쉽게 이해하는 컴퓨터 과학 원리 |
|---|---|
| 인덱스는 왜 필요한가요? | 대량의 데이터 더미 속에서 우리가 원하는 특정 데이터에 곧바로(Direct Access) 접근하기 위해 방 번호를 부여한 것입니다. 인덱스가 없다면 매번 첫 번째 데이터부터 차례대로 세어가며 찾아야 하므로 성능이 저하됩니다. |
| 왜 1이 아닌 0부터 시작하나요? | 인덱스는 첫 번째 위치로부터 얼마나 떨어져 있는가(거리/오프셋, Offset)를 의미합니다. 시작 지점(첫 번째 값) 자체는 기준점으로부터의 거리가 0이므로 인덱스가 0이 됩니다. |
| 왜 음수 인덱스는 -0이 아닌 -1부터 시작하나요? | 컴퓨터 과학에서 0과 -0은 완전히 똑같은 숫자입니다. 만약 마지막 방 번호를 -0이라고 부르면, 컴퓨터가 첫 번째 방(0)과 혼동하게 됩니다. 따라서 겹치지 않도록 맨 마지막 방을 -1로 지정한 것입니다. |
| 음수 인덱스가 계산되는 내부 원리는 무엇인가요? | 컴퓨터는 리스트[음수]를 만나면 내부적으로 '리스트 전체 길이 + 음수'를 수행하여 올바른 양수 인덱스로 자동 변환합니다. 예를 들어 길이가 5인 리스트에서 [-1]을 쓰면, 내부적으로 5 + (-1) = 4번 방으로 치환되어 마지막 요소를 꺼내오게 됩니다. |
한눈에 보는 인덱스 관계표 ([10, 20, 30, 40, 50] — 전체 길이: 5)
파이썬은 list[i]에서 i가 음수이면, 인덱싱하기 전에 자동으로 i = i + len(list)로 변환하여 처리합니다.
lst = [10, 20, 30, 40, 50] # len(lst) == 5
lst[-1] # 내부적으로: -1 + 5 = 4 -> lst[4] -> 50
lst[-2] # 내부적으로: -2 + 5 = 3 -> lst[3] -> 40
원래 마지막 원소를 꺼내려면 lst[len(lst) - 1]이라고 써야 하는데, 매번 이렇게 쓰기 번거로우므로 음수를 쓰면 자동으로 전체 길이를 더해주는 문법 설탕(Syntactic Sugar)을 제공하는 것입니다.
시계 비유
원형으로 이어진 인덱스 공간에서 음수는 반대 방향으로 회전한다고 생각하면 이해하기 쉽습니다. 시계에서 12시 방향에서 1시간 뒤로 가면 11시가 되는 것과 유사합니다.
양수 인덱스가 0부터 len-1까지 앞으로 채워나간다면, 음수 인덱스는 -1부터 -len까지 뒤에서부터 거꾸로 채워나가며 결국 동일한 위치를 가리키게 됩니다.
| 양수 인덱스 | 원소 | 음수 인덱스 |
|---|---|---|
| 0 | 10 | -5 |
| 1 | 20 | -4 |
| 2 | 30 | -3 |
| 3 | -2 | |
| 4 | 50 | -1 |
nickname = "분당중앙고"
# 인덱스 매핑:
# 분(0) 당(1) 중(2) 앙(3) 고(4)
# 분(-5) 당(-4) 중(-3) 앙(-2) 고(-1)
print(nickname[0]) # 출력 결과: 분
print(nickname[-1]) # 출력 결과: 고 (맨 마지막 글자 조회 시 유용)
items = ["칼", "방패", "물약"]
print(items[1]) # 출력 결과: 방패
슬라이싱 (Slicing): 범위 지정하여 잘라내기
[시작_인덱스 : 끝_인덱스 : 간격] 형식을 사용하여 데이터의 일부분을 잘라내어 새로운 묶음으로 만듭니다. 이때 끝_인덱스 위치의 데이터는 결과에 포함되지 않는다(미포함)는 규칙을 명심해야 합니다.
nickname = "분당중앙고"
# 1. 인덱스 0부터 2 직전(0, 1)까지 잘라내기
print(nickname[0:2]) # 출력 결과: 분당
# 2. 시작 인덱스를 생략하면 처음부터 잘라냅니다.
print(nickname[:3]) # 출력 결과: 분당중
# 3. 끝 인덱스를 생략하면 마지막까지 잘라냅니다.
print(nickname[2:]) # 출력 결과: 중앙고
# 4. 세 번째 인자(간격)를 지정하여 건너뛰며 가져올 수 있습니다.
numbers = [10, 20, 30, 40, 50, 60]
print(numbers[::2]) # 출력 결과: [10, 30, 50]
# 5. 간격을 -1로 지정하면 전체 데이터를 거꾸로 뒤집을 수 있습니다.
print(nickname[::-1]) # 출력 결과: 고앙중당분
[::-1]은 어떻게 뒤집을까?
[시작 : 끝 : 간격]에서 간격이 음수이면 이동 방향이 오른쪽에서 왼쪽으로 바뀝니다. 시작과 끝을 생략하면, 간격이 음수일 때 자동으로 시작은 맨 끝 원소, 끝은 맨 앞 원소까지 전체 범위로 설정됩니다.
nickname = "분당중앙고"
# 인덱스: 0 1 2 3 4
# 글자: 분 당 중 앙 고
#
# 간격 -1 -> 오른쪽에서 왼쪽으로 한 칸씩 이동
# 4(고) -> 3(앙) -> 2(중) -> 1(당) -> 0(분)
print(nickname[::-1]) # 출력 결과: 고앙중당분
# 숫자 리스트도 동일하게 작동합니다.
numbers = [1, 2, 3, 4, 5]
print(numbers[::-1]) # 출력 결과: [5, 4, 3, 2, 1]
# 간격 -2: 뒤에서부터 두 칸씩 건너뛰며 수집
print(numbers[::-2]) # 출력 결과: [5, 3, 1]
핵심 기억법: 간격의 부호가 방향을 결정합니다. 양수면 왼쪽에서 오른쪽, 음수면 오른쪽에서 왼쪽. [::-1]은 전체 범위를 오른쪽에서 왼쪽으로 한 칸씩이라는 뜻입니다.
3. 가변성(Mutable)과 불변성(Immutable)의 차이
리스트와 문자열은 사용하는 방식이 유사하지만, 메모리 내부의 데이터를 직접 수정할 수 있는지에 대한 결정적인 차이가 있습니다.
리스트 — 가변(Mutable): 같은 자리에서 값 수정
리스트는 인덱스를 지정해 내부 원소를 직접 바꿀 수 있습니다. 메모리 주소는 유지되며 그 자리의 값만 교체됩니다.
inventory = ["단검", "방패", "물약"]
inventory[0] = "장검"
print(inventory) # ['장검', '방패', '물약']
문자열 — 불변(Immutable): 수정 시 새 메모리에 새 문자열 생성
문자열은 한 번 만들어지면 내부 글자를 직접 바꿀 수 없습니다. 수정하려면 원본을 바탕으로 새로운 문자열을 새 메모리 공간에 만들고, 변수가 그 새 주소를 가리키도록 해야 합니다.
player_class = "마법사"
# player_class[0] = "전" # TypeError: 'str' object does not support item assignment
# 수정하려면 슬라이싱으로 새 문자열을 조립해야 합니다.
player_class = "전" + player_class[1:]
print(player_class) # 전법사
왜 문자열은 불변으로 설계했을까?
파이썬이 문자열을 불변으로 설계한 데는 중요한 컴퓨터 과학적 이유가 있습니다.
1. 메모리 공유 최적화 (Interning)
파이썬은 같은 내용의 문자열이 여러 곳에서 필요할 때 하나만 만들어 공유합니다. 만약 문자열이 수정 가능하다면, 한 변수에서 내용을 바꿨을 때 같은 메모리를 공유하던 다른 변수의 값까지 덩달아 바뀌는 치명적인 버그가 생깁니다. 불변 객체이기 때문에 안심하고 공유할 수 있습니다.
2. 해시값 안정성 (딕셔너리의 키 제약 조건)
해시값(Hash Value)은 데이터를 고유한 정수로 표현한 주소록 번호표입니다. 파이썬의 딕셔너리(dict)는 내부적으로 키의 해시값을 계산하여 초고속으로 값을 조회합니다.
만약 키로 사용된 데이터의 내용을 수정할 수 있다면 치명적인 오류가 발생합니다.
이름표(키)에 따라 물건을 보관하는 사물함 시스템이 있다고 가정해 봅시다.
- 이름표 "apple"의 해시값(분류 번호)이 10이므로, 10번 사물함에 물건을 보관했습니다.
- 그런데 이름표 자체를 "banana"로 수정해 버렸습니다.
- 이후 물건을 조회하려고 할 때 오류가 발생합니다.
- "banana"를 검색하면, "banana"의 해시값인 25번 사물함을 찾아갑니다. 하지만 25번 사물함은 비어 있습니다. (찾기 실패)
- "apple"을 검색하면, 해시값 10번 사물함으로 찾아갑니다. 하지만 사물함을 열어보니 이름표가 "banana"로 바뀌어 있어 일치하지 않으므로 물건을 꺼내주지 않습니다. (찾기 실패)
결과적으로 보관된 물건이 사물함에 존재함에도 불구하고 영영 꺼낼 수 없는 상태(데이터 유실)가 됩니다.
따라서 파이썬은 해시값을 안전하게 보장하기 위해 딕셔너리의 키로 사용할 수 있는 대상을 값이 절대 변하지 않는 불변(Immutable) 객체(문자열, 정수, 튜플 등)로 제한합니다. 수정이 가능한 가변(Mutable) 객체인 리스트는 딕셔너리의 키로 사용하려고 하면 다음과 같이 에러가 발생합니다.
# 가변 객체인 리스트를 딕셔너리의 키로 사용하려고 시도하면 에러가 발생합니다.
my_dict = {[1, 2]: "value"}
# TypeError: unhashable type: 'list'
4. 리스트의 주요 연산 및 메서드
리스트를 편리하게 다루기 위해 파이썬이 제공하는 대표적인 도구(메서드)들입니다.
데이터 추가 및 삭제
inventory = ["장검", "방패"]
# append(값): 리스트 맨 끝에 새로운 값 추가
inventory.append("물약")
print(inventory) # 출력 결과: ['장검', '방패', '물약']
# insert(인덱스, 값): 특정 위치에 값 삽입 (뒤에 있던 요소들은 뒤로 밀림)
inventory.insert(1, "길드장검")
print(inventory) # 출력 결과: ['장검', '길드장검', '방패', '물약']
# remove(값): 특정 값을 찾아 제거 (중복된 값이 있다면 가장 앞에 있는 것 하나만 제거)
inventory.remove("방패")
print(inventory) # 출력 결과: ['장검', '길드장검', '물약']
# pop(): 맨 마지막 요소를 제거하고 그 값을 반환
last_item = inventory.pop()
print(last_item) # 출력 결과: 물약
print(inventory) # 출력 결과: ['장검', '길드장검']
데이터 탐색 및 정렬
scores = [35, 95, 50, 95, 80]
# len(리스트): 리스트의 전체 길이(원소의 개수) 반환
print(len(scores)) # 출력 결과: 5
# count(값): 특정 값이 리스트 안에 몇 개 들어있는지 반환
print(scores.count(95)) # 출력 결과: 2
# sort(): 리스트 요소를 오름차순으로 정렬 (원본이 변경됨)
scores.sort()
print(scores) # 출력 결과: [35, 50, 80, 95, 95]
# 내림차순 정렬을 원할 경우 reverse=True 옵션 지정
scores.sort(reverse=True)
print(scores) # 출력 결과: [95, 95, 80, 50, 35]
자주 하는 실수 & 오류 예방
1. 인덱스 범위 초과 (IndexError)
리스트에 존재하지 않는 인덱스를 호출하면 프로그램이 강제로 종료됩니다. 원소 개수가 N개인 리스트의 유효 인덱스는 0부터 N-1까지입니다.
items = ["칼", "방패"]
# print(items[2]) # 에러 발생: IndexError: list index out of range
오류 해결: 인덱스를 사용하기 전에 len() 함수로 리스트의 크기를 항상 먼저 확인하는 습관을 들여야 합니다.
2. 단순 대입을 통한 복사의 복병 (얕은 복사)
리스트 변수를 =로 다른 변수에 저장하면, 데이터 자체가 복사되는 것이 아니라 두 변수가 같은 메모리 주소를 함께 가리키게 됩니다. 어느 쪽에서 수정해도 둘 다 변합니다.
original = [1, 2, 3]
copied = original # 단순 대입: 주소를 복사, 데이터는 하나
copied.append(99)
print(original) # [1, 2, 3, 99] (copied를 바꿨는데 original도 같이 변함)
print(copied) # [1, 2, 3, 99]
해결 방법: 전체 범위 슬라이싱 [:]을 사용하면 새로운 독립된 메모리 공간에 데이터를 복제합니다.
original = [1, 2, 3]
copied = original[:] # 슬라이싱: 새 메모리에 값 복제
copied.append(99)
print(original) # [1, 2, 3] (원본 안전)
print(copied) # [1, 2, 3, 99] (복사본만 변경됨)
5. 딕셔너리 (dict) — 이름표가 붙은 사물함
딕셔너리는 키(key): 값(value) 쌍으로 데이터를 저장하는 자료구조입니다. 인덱스 대신 원하는 이름표(키)로 값을 꺼낼 수 있어 매우 직관적입니다.
# 중괄호 { }와 콜론 : 으로 만듭니다.
menu = {"라면": 1000, "김밥": 2000, "떡볶이": 3000}
# 값 조회: 딕셔너리이름[키]
print(menu["라면"]) # 출력 결과: 1000
# 값 추가 / 수정
menu["순대"] = 2500 # 새 항목 추가
menu["라면"] = 1200 # 기존 값 수정
# 값 삭제
del menu["김밥"]
# 키 존재 여부 확인
if "떡볶이" in menu:
print("떡볶이 있음!")
# 모든 키-값 순회
for k, v in menu.items():
print(k, v)
딕셔너리 주요 메서드
| 메서드 | 설명 |
|---|---|
d.keys() | 모든 키 목록 |
d.values() | 모든 값 목록 |
d.get(키, 기본값) | 키가 없을 때 에러 대신 기본값 반환 |
d.items() | 키-값 쌍을 (키, 값) 튜플로 순회 |
6. 집합 (set) — 중복 없는 모음
집합은 순서가 없고, 중복을 허용하지 않는 자료구조입니다. 수학의 집합처럼 교집합·합집합·차집합 연산을 바로 사용할 수 있습니다.
# 중괄호 { }로 만들되, 콜론 없이 값만 넣습니다.
fruits = {"사과", "바나나", "포도", "사과"} # 중복 자동 제거
print(fruits) # {'사과', '바나나', '포도'}
# 원소 추가 / 제거
fruits.add("수박")
fruits.remove("바나나")
# 포함 여부 확인
print("사과" in fruits) # True
# 집합 연산
a = {1, 2, 3, 4, 5}
b = {4, 5, 6, 7}
print(a | b) # 합집합: {1, 2, 3, 4, 5, 6, 7}
print(a & b) # 교집합: {4, 5}
print(a - b) # 차집합: {1, 2, 3}
# 리스트 중복 제거에 활용
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
unique = sorted(list(set(numbers)))
print(unique) # [1, 2, 3, 4, 5, 6, 9]
핵심 정리 — 4가지 컨테이너 비교
| 구분 | 문자열(str) | 리스트(list) | 딕셔너리(dict) | 집합(set) |
|---|---|---|---|---|
| 작성 기호 | " " / ' ' | [ ] | { 키:값 } | { 값 } |
| 순서 보장 | O | O | O (3.7+) | X |
| 중복 허용 | O | O | 키 불가 | X |
| 값 수정 | 불가 | 가능 | 가능 | 가능 |
| 주요 용도 | 텍스트 처리 | 순서 있는 목록 | 키-값 매핑 | 중복 제거·집합 연산 |
이번 주 대회 바로가기
1반 | 2반 | 3반 | 4반 | 5반 | 6반 | 7반 | 8반 | 9반
※ 대회 링크는 선생님이 별도 공지합니다.