14. 리스트(list): 항목의 순서 있는 컬렉션
지금까지 이 책에서는 개별 데이터 조각, 즉 단일 숫자, 문자열, 불리언 값을 다뤄 왔습니다. 하지만 실제 프로그램에서는 종종 관련된 항목들의 컬렉션을 다뤄야 합니다—학생 이름 목록, 온도 측정값의 연속, 상품 가격의 모음, 또는 사용자 명령의 시퀀스처럼 말입니다. Python의 리스트(list) 는 순서 있는 데이터 컬렉션을 저장하고 다루기 위한 기본 도구입니다.
리스트는 특정한 순서로 여러 항목을 담을 수 있는 시퀀스(sequence) 입니다. 문자열(문자만 담을 수 있음)과 달리, 리스트는 숫자, 문자열, 불리언, 심지어 다른 리스트까지 어떤 타입의 데이터도 담을 수 있습니다. 또한 리스트는 가변(mutable) 이라서 생성 후에도 내용물을 바꿀 수 있는데—항목을 추가하거나, 제거하거나, 기존 항목을 수정할 수 있습니다.
이 장에서는 리스트를 만드는 방법, 요소에 접근하는 방법, 수정하는 방법, 그리고 실용적인 프로그래밍 문제를 해결하는 데 리스트를 사용하는 방법을 살펴보겠습니다. 마지막에는 리스트가 Python에서 가장 강력하고 가장 자주 쓰이는 자료구조 중 하나인 이유를 이해하게 될 것입니다.
14.1) 리스트 만들기와 요소 접근하기
14.1.1) 대괄호로 리스트 만들기
리스트를 만드는 가장 일반적인 방법은 항목들을 대괄호(square brackets) [] 로 감싸고, 각 항목을 쉼표로 구분하는 것입니다. 간단한 예시는 다음과 같습니다:
# 학생 이름 리스트
students = ["Alice", "Bob", "Charlie", "Diana"]
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']Python이 리스트를 어떻게 표시하는지 확인해 보세요. 대괄호를 보여 주고 각 문자열에 따옴표를 붙입니다. 이것이 리스트의 표현(representation), 즉 Python이 내부에 무엇이 있는지 보여 주는 방식입니다.
리스트에는 어떤 타입의 데이터도 담을 수 있습니다. 다음은 시험 점수 리스트입니다:
# 정수 점수 리스트
scores = [85, 92, 78, 95, 88]
print(scores) # Output: [85, 92, 78, 95, 88]실무에서는 덜 흔하지만, 같은 리스트에 서로 다른 타입을 섞어 넣는 것도 가능합니다:
# 혼합 타입 리스트(덜 흔하지만 유효함)
mixed_data = ["Alice", 25, True, 3.14]
print(mixed_data) # Output: ['Alice', 25, True, 3.14]빈 리스트(empty list) 는 항목이 하나도 없으며, 대괄호만으로 만들 수 있습니다:
# 빈 리스트
empty = []
print(empty) # Output: []
print(len(empty)) # Output: 0문자열에 사용해 왔던 len() 함수는 리스트에도 동작합니다—리스트에 들어 있는 항목 수를 반환합니다.
14.1.2) 리스트의 순서와 위치 이해하기
리스트는 항목을 추가한 순서(order) 를 유지합니다. 처음 넣은 항목은 첫 번째로 남고, 두 번째는 두 번째로 남는 식입니다. 이 순서가 중요한 이유는 위치(position) (또는 인덱스(index))로 특정 항목에 접근할 수 있게 해 주기 때문입니다.
Python은 0부터 시작하는 인덱싱(zero-based indexing) 을 사용합니다. 첫 번째 항목은 위치 0, 두 번째 항목은 위치 1, 이런 식입니다. 처음에는 낯설 수 있지만 많은 프로그래밍 언어에서 사용하는 관례입니다.
실제로 어떻게 동작하는지 살펴봅시다:
students = ["Alice", "Bob", "Charlie", "Diana"]
# 첫 번째 학생에 접근(인덱스 0)
first_student = students[0]
print(first_student) # Output: Alice
# 세 번째 학생에 접근(인덱스 2)
third_student = students[2]
print(third_student) # Output: Charlie세 번째 학생을 얻기 위해 인덱스 3이 아니라 2를 사용한다는 점에 주목하세요. 이는 0부터 세기 때문입니다.
14.1.3) 양수 인덱스로 요소 접근하기
리스트 요소에 접근하려면 리스트 이름 뒤에 대괄호로 인덱스를 씁니다: list_name[index]. 인덱스는 유효 범위(0부터 len(list) - 1까지) 안의 정수여야 합니다.
다음은 상품 가격을 다루는 실용적인 예시입니다:
# 달러 단위 상품 가격
prices = [19.99, 24.50, 15.75, 32.00, 8.99]
# 특정 가격에 접근
first_price = prices[0]
last_index = len(prices) - 1 # 마지막 유효 인덱스 계산
last_price = prices[last_index]
print(f"First product costs: ${first_price}") # Output: First product costs: $19.99
print(f"Last product costs: ${last_price}") # Output: Last product costs: $8.99마지막 인덱스로 len(prices) - 1을 왜 사용할까요? 리스트에 항목이 5개라면 인덱스는 0, 1, 2, 3, 4이므로 마지막 유효 인덱스는 항상 길이보다 1 작기 때문입니다.
인덱스를 식(expression)과 계산에 사용할 수도 있습니다:
scores = [85, 92, 78, 95, 88]
# 처음 세 점수의 평균 계산
first_three_average = (scores[0] + scores[1] + scores[2]) / 3
print(f"Average of first three: {first_three_average}") # Output: Average of first three: 85.014.1.4) 음수 인덱스: 끝에서부터 세기
Python은 편리한 기능을 제공합니다. 음수 인덱스(negative indices) 를 사용하면 리스트의 끝에서부터 항목에 접근할 수 있습니다. 인덱스 -1은 마지막 항목, -2는 뒤에서 두 번째 항목을 가리키는 식입니다.
students = ["Alice", "Bob", "Charlie", "Diana"]
# 끝에서부터 접근
last_student = students[-1]
second_to_last = students[-2]
print(last_student) # Output: Diana
print(second_to_last) # Output: Charlie이는 마지막 항목이 필요하지만 len(list) - 1을 계산하고 싶지 않을 때 특히 유용합니다:
prices = [19.99, 24.50, 15.75, 32.00, 8.99]
# 아래 두 접근 방식은 동일합니다
last_price_method1 = prices[len(prices) - 1]
last_price_method2 = prices[-1]
print(last_price_method1) # Output: 8.99
print(last_price_method2) # Output: 8.99양수 인덱스와 음수 인덱스가 같은 항목을 어떻게 가리키는지 확인해 봅시다:
14.1.5) 유효하지 않은 인덱스를 사용하면 어떻게 될까
존재하지 않는 인덱스에 접근하려고 하면 Python은 IndexError 를 발생시킵니다:
students = ["Alice", "Bob", "Charlie"]
# WARNING: This list has indices 0, 1, 2 (or -3, -2, -1) - for demonstration only
# Trying to access index 3 causes an error
# PROBLEM: Index 3 doesn't exist in a 3-item list
# print(students[3]) # IndexError: list index out of range이 오류는 그 자리에 없는 항목을 요청했다는 사실을 Python이 알려 주는 방식입니다.
14.2) 리스트 인덱싱과 슬라이싱
14.2.1) 리스트 슬라이싱 기초 이해하기
5장에서 배웠듯이 문자열을 슬라이스할 수 있는 것처럼, 리스트도 슬라이싱(slice) 해서 일부를 추출할 수 있습니다. 슬라이스는 원래 리스트 요소의 일부를 담은 새로운 리스트를 만듭니다. 문법은 list[start:stop]이며, start는 슬라이스가 시작되는 인덱스(포함), stop은 끝나는 인덱스(미포함)입니다.
numbers = [10, 20, 30, 40, 50, 60, 70]
# 인덱스 1부터 인덱스 4 전까지(인덱스 4는 포함하지 않음)의 요소 가져오기
subset = numbers[1:4]
print(subset) # Output: [20, 30, 40]슬라이스 [1:4]는 인덱스 1, 2, 3을 포함하지만 인덱스 4 전에 멈춥니다. 이 “stop은 미포함” 규칙은 문자열 슬라이싱과 동일합니다.
학생 이름으로 실용적인 예시를 살펴봅시다:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
# 처음 세 학생 가져오기
first_three = students[0:3]
print(first_three) # Output: ['Alice', 'Bob', 'Charlie']
# 인덱스 2부터 4까지의 학생 가져오기
middle_group = students[2:5]
print(middle_group) # Output: ['Charlie', 'Diana', 'Eve']14.2.2) 슬라이스에서 start 또는 stop 생략하기
start 인덱스를 생략하면 처음부터 슬라이스할 수 있고, stop 인덱스를 생략하면 끝까지 슬라이스할 수 있습니다:
scores = [85, 92, 78, 95, 88, 91, 87]
# 처음부터 인덱스 3 전까지
first_few = scores[:3]
print(first_few) # Output: [85, 92, 78]
# 인덱스 4부터 끝까지
last_few = scores[4:]
print(last_few) # Output: [88, 91, 87]
# 전체 리스트(처음부터 끝까지)
all_scores = scores[:]
print(all_scores) # Output: [85, 92, 78, 95, 88, 91, 87]슬라이스 [:]는 전체 리스트의 복사본(copy) 을 만듭니다. 원본을 수정하지 않고 복제본으로 작업하고 싶을 때 유용하며—이 부분은 14.6절에서 더 살펴보겠습니다.
14.2.3) 슬라이스에서 음수 인덱스 사용하기
음수 인덱스는 단일 요소 접근에서와 마찬가지로 슬라이스에서도 동작합니다. 이는 끝에서부터 항목을 얻을 때 특히 유용합니다:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank"]
# 마지막 세 학생 가져오기
last_three = students[-3:]
print(last_three) # Output: ['Diana', 'Eve', 'Frank']
# 마지막 두 학생을 제외한 모든 학생 가져오기
all_but_last_two = students[:-2]
print(all_but_last_two) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']
# 뒤에서 세 번째부터 뒤에서 두 번째 전까지
middle_from_end = students[-3:-1]
print(middle_from_end) # Output: ['Diana', 'Eve']14.2.4) step 값을 사용한 슬라이싱
세 번째 매개변수를 추가하여 스텝(step) (항목 사이에서 몇 개의 인덱스를 건너뛸지)를 제어할 수 있습니다. 전체 문법은 list[start:stop:step]입니다:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 인덱스 0부터 시작해서 두 번째 숫자마다
evens = numbers[0:10:2]
print(evens) # Output: [0, 2, 4, 6, 8]
# 인덱스 1부터 시작해서 세 번째 숫자마다
every_third = numbers[1:10:3]
print(every_third) # Output: [1, 4, 7]음수 스텝(negative step) 을 사용해 리스트를 뒤집을 수도 있습니다:
numbers = [1, 2, 3, 4, 5]
# 리스트 뒤집기
reversed_numbers = numbers[::-1]
print(reversed_numbers) # Output: [5, 4, 3, 2, 1]슬라이스 [::-1]는 “끝에서 시작해 처음으로 가되, 1씩 뒤로 이동하라”는 뜻입니다. 이는 시퀀스를 뒤집는 흔한 Python 관용구입니다.
14.2.5) 슬라이스는 IndexError를 절대 발생시키지 않는다
단일 요소에 접근하는 것과 달리, 슬라이싱은 매우 관대합니다. 리스트 범위를 벗어난 인덱스를 지정하면 Python이 알아서 범위에 맞게 조정합니다:
numbers = [10, 20, 30, 40, 50]
# 존재하는 것보다 더 많이 요청하기
extended_slice = numbers[2:100]
print(extended_slice) # Output: [30, 40, 50]
# 끝을 넘어서 시작하기
empty_slice = numbers[10:20]
print(empty_slice) # Output: []이 동작은 슬라이싱할 때 정확한 경계를 걱정하지 않아도 된다는 점에서 유용합니다—Python이 경계 상황을 자연스럽게 처리해 줍니다.
14.3) 리스트 수정과 자주 쓰는 리스트 메서드
14.3.1) 리스트는 가변이다: 요소 바꾸기
불변(immutable)인 문자열과 달리, 리스트는 가변(mutable) 이라서 생성 후에도 내용물을 바꿀 수 있습니다. 특정 인덱스에 새 값을 대입하여 개별 요소를 수정할 수 있습니다:
# 가격 리스트로 시작하기
prices = [19.99, 24.50, 15.75, 32.00]
print(prices) # Output: [19.99, 24.5, 15.75, 32.0]
# 두 번째 가격(인덱스 1) 업데이트
prices[1] = 22.99
print(prices) # Output: [19.99, 22.99, 15.75, 32.0]
# 음수 인덱싱으로 마지막 가격 업데이트
prices[-1] = 29.99
print(prices) # Output: [19.99, 22.99, 15.75, 29.99]이 가변성은 강력합니다—새 리스트를 만들지 않고 제자리(in place)에서 데이터를 업데이트할 수 있다는 뜻이기 때문입니다. 하지만 의도치 않은 변경에 주의해야 한다는 뜻이기도 하며, 이는 14.6절에서 다루겠습니다.
14.3.2) append()로 요소 추가하기
append() 메서드는 리스트의 끝 에 단일 항목을 추가합니다. 이는 가장 자주 사용되는 리스트 연산 중 하나입니다:
# 빈 장바구니로 시작하기
cart = []
print(cart) # Output: []
# 항목을 하나씩 추가하기
cart.append("Milk")
print(cart) # Output: ['Milk']
cart.append("Bread")
print(cart) # Output: ['Milk', 'Bread']
cart.append("Eggs")
print(cart) # Output: ['Milk', 'Bread', 'Eggs']append()는 리스트를 제자리(in place) 에서 수정한다는 점에 주목하세요—새 리스트를 반환하지 않습니다. 이 메서드는 None을 반환하므로, 결과를 대입할 필요가 없습니다:
scores = [85, 92, 78]
result = scores.append(95)
print(scores) # Output: [85, 92, 78, 95]
print(result) # Output: None14.3.3) insert()로 특정 위치에 요소 삽입하기
append()가 항상 끝에 추가하는 반면, insert()를 사용하면 어느 위치든 항목을 추가할 수 있습니다. 문법은 list.insert(index, item)입니다:
students = ["Alice", "Charlie", "Diana"]
print(students) # Output: ['Alice', 'Charlie', 'Diana']
# 인덱스 1에 "Bob" 삽입(Alice와 Charlie 사이)
students.insert(1, "Bob")
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']어떤 인덱스에 삽입하면 그 위치에 있던 항목(그리고 그 뒤의 모든 항목)은 오른쪽으로 밀립니다:
numbers = [10, 20, 30, 40]
print(numbers) # Output: [10, 20, 30, 40]
# 인덱스 2에 25 삽입
numbers.insert(2, 25)
print(numbers) # Output: [10, 20, 25, 30, 40]인덱스 0을 사용하면 맨 앞에 삽입할 수 있습니다:
priorities = ["Medium", "Low"]
priorities.insert(0, "High")
print(priorities) # Output: ['High', 'Medium', 'Low']리스트 길이를 넘어서는 인덱스를 지정하면, insert()는 항목을 끝에 추가합니다(append()처럼 동작합니다):
items = [1, 2, 3]
items.insert(100, 4)
print(items) # Output: [1, 2, 3, 4]14.3.4) remove()로 요소 제거하기
remove() 메서드는 리스트에서 특정 값의 첫 번째 등장(first occurrence) 을 제거합니다:
fruits = ["apple", "banana", "cherry", "banana", "date"]
print(fruits) # Output: ['apple', 'banana', 'cherry', 'banana', 'date']
# 첫 번째 "banana" 제거
fruits.remove("banana")
print(fruits) # Output: ['apple', 'cherry', 'banana', 'date']첫 번째 "banana"만 제거되었고, 두 번째는 남아 있다는 점에 주목하세요. 존재하지 않는 값을 제거하려고 하면 Python은 ValueError 를 발생시킵니다:
numbers = [10, 20, 30]
# WARNING: Attempting to remove non-existent value - for demonstration only
# PROBLEM: 40 is not in the list
# numbers.remove(40) # ValueError: list.remove(x): x not in list이 오류를 피하려면 제거하기 전에 항목이 존재하는지 확인할 수 있습니다:
cart = ["Milk", "Bread", "Eggs"]
item_to_remove = "Butter"
if item_to_remove in cart:
cart.remove(item_to_remove)
print(f"Removed {item_to_remove}")
else:
print(f"{item_to_remove} not in cart")
# Output: Butter not in cart14.3.5) pop()으로 요소 제거 및 반환하기
pop() 메서드는 특정 인덱스의 항목을 제거하고 그 값을 반환(return) 합니다. 인덱스를 지정하지 않으면 마지막 항목을 제거하고 반환합니다:
scores = [85, 92, 78, 95, 88]
# 마지막 점수 제거 및 가져오기
last_score = scores.pop()
print(f"Removed: {last_score}") # Output: Removed: 88
print(scores) # Output: [85, 92, 78, 95]
# 인덱스 1의 점수 제거 및 가져오기
second_score = scores.pop(1)
print(f"Removed: {second_score}") # Output: Removed: 92
print(scores) # Output: [85, 78, 95]이는 리스트에서 항목을 한 번에 하나씩 처리해야 할 때 유용합니다:
tasks = ["Write code", "Test code", "Deploy code"]
while len(tasks) > 0:
current_task = tasks.pop(0) # 앞에서 제거
print(f"Working on: {current_task}")
# Output:
# Working on: Write code
# Working on: Test code
# Working on: Deploy code
print(tasks) # Output: []14.3.6) extend()로 리스트 확장하기
extend() 메서드는 다른 리스트(또는 어떤 iterable이든)의 모든 항목을 현재 리스트의 끝에 추가합니다:
primary_colors = ["red", "blue", "yellow"]
secondary_colors = ["green", "orange", "purple"]
# secondary_colors의 모든 색을 primary_colors에 추가
primary_colors.extend(secondary_colors)
print(primary_colors)
# Output: ['red', 'blue', 'yellow', 'green', 'orange', 'purple']이는 전체 리스트를 단일 요소로 추가하는 append()와는 다릅니다:
colors1 = ["red", "blue"]
colors2 = ["green", "orange"]
# append 사용(리스트를 하나의 요소로 추가)
colors1.append(colors2)
print(colors1) # Output: ['red', 'blue', ['green', 'orange']]
# extend 사용(각 요소를 개별적으로 추가)
colors3 = ["red", "blue"]
colors3.extend(colors2)
print(colors3) # Output: ['red', 'blue', 'green', 'orange']14.3.7) sort()와 sorted()로 리스트 정렬하기
Python은 리스트를 정렬하는 두 가지 방법을 제공합니다. sort() 메서드는 리스트를 제자리(in place) 에서 정렬합니다(원본을 수정함):
scores = [78, 95, 85, 92, 88]
scores.sort()
print(scores) # Output: [78, 85, 88, 92, 95]내림차순으로 정렬하려면 reverse 매개변수를 사용합니다:
scores = [78, 95, 85, 92, 88]
scores.sort(reverse=True)
print(scores) # Output: [95, 92, 88, 85, 78]sorted() 함수(38장에서 더 자세히 다룹니다)는 원본을 수정하지 않고 새로운 정렬된 리스트(new sorted list) 를 만듭니다:
original = [78, 95, 85, 92, 88]
sorted_scores = sorted(original)
print(original) # Output: [78, 95, 85, 92, 88]
print(sorted_scores) # Output: [78, 85, 88, 92, 95]문자열에도 알파벳 순서를 사용하여 정렬이 동작합니다:
names = ["Charlie", "Alice", "Diana", "Bob"]
names.sort()
print(names) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']14.3.8) reverse()로 리스트 뒤집기
reverse() 메서드는 리스트를 제자리(in place) 에서 뒤집습니다:
numbers = [1, 2, 3, 4, 5]
numbers.reverse()
print(numbers) # Output: [5, 4, 3, 2, 1]이는 역순 정렬과는 다릅니다—reverse()는 현재의 순서가 무엇이든 그냥 뒤집기만 합니다:
mixed = [3, 1, 4, 1, 5]
mixed.reverse()
print(mixed) # Output: [5, 1, 4, 1, 3]또한 슬라이싱으로 list[::-1]를 사용해 리스트를 뒤집을 수도 있다는 점을 기억하세요. 차이점은 슬라이싱은 새 리스트를 만드는 반면, reverse()는 원본을 수정한다는 것입니다.
14.3.9) index()와 count()로 요소 찾기
index() 메서드는 값이 처음 등장하는 위치를 반환합니다:
students = ["Alice", "Bob", "Charlie", "Diana", "Bob"]
# "Charlie"가 어디에 있는지 찾기
position = students.index("Charlie")
print(f"Charlie is at index {position}") # Output: Charlie is at index 2
# 첫 번째 "Bob" 찾기
bob_position = students.index("Bob")
print(f"Bob is at index {bob_position}") # Output: Bob is at index 1값이 존재하지 않으면 index()는 ValueError 를 발생시킵니다:
students = ["Alice", "Bob", "Charlie"]
# WARNING: Attempting to find non-existent value - for demonstration only
# PROBLEM: 'Eve' is not in the list
# position = students.index("Eve") # ValueError: 'Eve' is not in listcount() 메서드는 값이 몇 번 등장하는지 반환합니다:
numbers = [1, 2, 3, 2, 4, 2, 5]
twos = numbers.count(2)
print(f"The number 2 appears {twos} times") # Output: The number 2 appears 3 times
# 항목이 없으면 count는 0을 반환할 수 있습니다
sixes = numbers.count(6)
print(f"The number 6 appears {sixes} times") # Output: The number 6 appears 0 times14.3.10) clear()로 모든 요소 지우기
clear() 메서드는 리스트의 모든 항목을 제거하여 빈 리스트로 만듭니다:
cart = ["Milk", "Bread", "Eggs", "Butter"]
print(cart) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']
cart.clear()
print(cart) # Output: []
print(len(cart)) # Output: 0이는 빈 리스트를 대입하는 것과 동일하지만, clear()는 의도를 더 명확하게 드러냅니다.
14.4) del로 리스트 요소 삭제하기
14.4.1) del로 인덱스 기준 요소 제거하기
del 문은 특정 인덱스의 리스트 요소를 삭제할 수 있습니다:
students = ["Alice", "Bob", "Charlie", "Diana", "Eve"]
print(students) # Output: ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve']
# 인덱스 2의 요소 삭제
del students[2]
print(students) # Output: ['Alice', 'Bob', 'Diana', 'Eve']pop()과 달리 del은 제거된 값을 반환하지 않습니다—그냥 삭제만 합니다. 제거한 값을 사용할 필요가 없을 때 유용합니다:
scores = [85, 92, 78, 95, 88]
# 가장 낮은 점수 제거(인덱스 2에 있음)
del scores[2]
print(scores) # Output: [85, 92, 95, 88]del에서도 음수 인덱스를 사용할 수 있습니다:
tasks = ["Task 1", "Task 2", "Task 3", "Task 4"]
# 마지막 작업 삭제
del tasks[-1]
print(tasks) # Output: ['Task 1', 'Task 2', 'Task 3']14.4.2) del로 슬라이스 삭제하기
del 문은 슬라이스 전체를 한 번에 제거할 수도 있습니다:
numbers = [10, 20, 30, 40, 50, 60, 70]
print(numbers) # Output: [10, 20, 30, 40, 50, 60, 70]
# 인덱스 2부터 4까지 요소 삭제(인덱스 2, 3, 4)
del numbers[2:5]
print(numbers) # Output: [10, 20, 60, 70]이는 특정 구간의 요소를 제거할 때 특히 유용합니다:
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 처음 세 요소 제거
del data[:3]
print(data) # Output: [4, 5, 6, 7, 8, 9, 10]
# 마지막 두 요소 제거
del data[-2:]
print(data) # Output: [4, 5, 6, 7, 8]스텝 슬라이싱을 사용해 한 칸씩 건너뛰며 삭제하는 것도 가능합니다:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 두 번째 요소마다 삭제
del numbers[::2]
print(numbers) # Output: [1, 3, 5, 7, 9]14.4.3) del, remove(), pop() 비교하기
각 삭제 방법을 언제 사용하면 좋은지 정리해 봅시다:
# 비교를 위한 예시 리스트
items = ["apple", "banana", "cherry", "date", "elderberry"]
# 삭제할 값을 알고 있을 때는 remove() 사용
items_copy1 = items.copy()
items_copy1.remove("cherry") # 첫 번째 "cherry" 제거
print(items_copy1) # Output: ['apple', 'banana', 'date', 'elderberry']
# 인덱스를 알고 있고 값이 필요할 때는 pop() 사용
items_copy2 = items.copy()
removed_item = items_copy2.pop(2) # 인덱스 2의 항목 제거 및 반환
print(f"Removed: {removed_item}") # Output: Removed: cherry
print(items_copy2) # Output: ['apple', 'banana', 'date', 'elderberry']
# 인덱스를 알고 있지만 값이 필요 없을 때는 del 사용
items_copy3 = items.copy()
del items_copy3[2] # 인덱스 2의 항목을 그냥 제거
print(items_copy3) # Output: ['apple', 'banana', 'date', 'elderberry']14.5) for 반복문(loop)으로 리스트 순회하기
14.5.1) 기본 리스트 순회
리스트에서 가장 흔한 작업 중 하나는 각 항목을 순서대로 처리하는 것입니다. 12장에서 배운 for 반복문(loop)은 이에 딱 맞습니다:
students = ["Alice", "Bob", "Charlie", "Diana"]
# 각 학생 처리
for student in students:
print(f"Hello, {student}!")
# Output:
# Hello, Alice!
# Hello, Bob!
# Hello, Charlie!
# Hello, Diana!반복 변수(이 경우 student)는 리스트의 각 값을 순서대로 하나씩 갖게 됩니다. 이 변수는 의미 있는 어떤 이름으로든 지정할 수 있습니다:
scores = [85, 92, 78, 95, 88]
# 각 점수의 등급 계산 및 표시
for score in scores:
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
else:
grade = "C"
print(f"Score {score} is a {grade}")
# Output:
# Score 85 is a B
# Score 92 is a A
# Score 78 is a C
# Score 95 is a A
# Score 88 is a B14.5.2) 여러 리스트에서 대응되는 항목 처리하기
때로는 서로 관련된 데이터가 별도의 리스트에 저장되어 있고, 이를 함께 처리해야 할 때가 있습니다. 38장에서 zip() 함수를 자세히 배우겠지만, 대응되는 항목들을 처리하는 데 어떻게 도움이 되는지 간단히 미리 보겠습니다:
# zip()은 38장에서 배우겠지만, 지금은 간단한 예시를 보겠습니다
students = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
# 대응되는 쌍 처리
for student, score in zip(students, scores):
print(f"{student} scored {score}")
# Output:
# Alice scored 85
# Bob scored 92
# Charlie scored 78zip() 함수는 여러 리스트의 요소들을 짝지어 주며, 관련 데이터가 별도의 리스트에 있을 때 유용합니다. 이 기능과 다른 반복 도구들은 38장에서 깊이 있게 살펴보겠습니다.
14.6) 리스트 복사와 공유 참조 피하기
14.6.1) 리스트 참조 이해하기
리스트를 변수에 대입할 때, Python은 리스트의 복사본을 만드는 것이 아니라 메모리에서 동일한 리스트 객체를 가리키는 참조(reference) 를 만듭니다. 즉 여러 변수가 같은 리스트를 참조할 수 있습니다:
original = [1, 2, 3]
reference = original # 두 변수는 같은 리스트를 가리킵니다
# 한 변수를 통해 수정하면 다른 변수에도 영향을 줍니다
reference.append(4)
print(original) # Output: [1, 2, 3, 4]
print(reference) # Output: [1, 2, 3, 4]이 동작은 reference가 독립적인 복사본일 것이라고 기대하면 놀라울 수 있습니다. 왜 이것이 중요한지 살펴봅시다:
# 시나리오: 장바구니의 변경 사항을 추적하고 싶습니다
cart = ["Milk", "Bread"]
backup = cart # 원래 상태를 저장하려고 함
# 항목 추가
cart.append("Eggs")
cart.append("Butter")
# "backup" 확인
print(backup) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']백업도 같이 바뀌었습니다! 이는 backup과 cart가 같은 리스트 객체에 대한 두 개의 이름이기 때문입니다.
14.6.2) 슬라이싱으로 독립적인 복사본 만들기
진짜 독립적인 복사본을 만들려면 [:] 슬라이싱을 사용하세요:
original = [1, 2, 3]
copy = original[:] # 같은 내용을 가진 새 리스트 생성
# 복사본을 수정해도 원본에는 영향을 주지 않습니다
copy.append(4)
print(original) # Output: [1, 2, 3]
print(copy) # Output: [1, 2, 3, 4]이제 장바구니 예시를 고쳐 봅시다:
cart = ["Milk", "Bread"]
backup = cart[:] # 독립적인 복사본 생성
# cart에 항목 추가
cart.append("Eggs")
cart.append("Butter")
# backup은 변하지 않습니다
print(cart) # Output: ['Milk', 'Bread', 'Eggs', 'Butter']
print(backup) # Output: ['Milk', 'Bread']14.6.3) copy() 메서드로 복사본 만들기
리스트에는 [:]와 같은 일을 하는 copy() 메서드도 있습니다:
original = [10, 20, 30]
copy = original.copy()
copy.append(40)
print(original) # Output: [10, 20, 30]
print(copy) # Output: [10, 20, 30, 40][:]와 copy()는 모두 얕은 복사(shallow copies) 를 만들며, 이에 대해서는 다음에서 논의하겠습니다.
14.6.4) 얕은 복사의 한계
[:]와 copy()는 둘 다 얕은 복사(shallow copies) 를 만듭니다. 이는 리스트의 구조는 복사하지만, 리스트 안에 다른 가변 객체(다른 리스트 등)가 들어 있으면 그 내부 객체들은 여전히 공유된다는 뜻입니다:
# 리스트를 포함하는 리스트
original = [[1, 2], [3, 4], [5, 6]]
copy = original[:]
# 바깥 리스트 구조 수정은 독립적입니다
copy.append([7, 8])
print(original) # Output: [[1, 2], [3, 4], [5, 6]]
print(copy) # Output: [[1, 2], [3, 4], [5, 6], [7, 8]]
# 하지만 내부 리스트를 수정하면 둘 다 영향을 받습니다!
copy[0].append(99)
print(original) # Output: [[1, 2, 99], [3, 4], [5, 6]]
print(copy) # Output: [[1, 2, 99], [3, 4], [5, 6], [7, 8]]왜 이런 일이 발생할까요? 얕은 복사는 새 바깥 리스트를 만들지만 내부 리스트들은 여전히 공유 참조이기 때문입니다:
중첩 구조에서는 깊은 복사(deep copy) 가 필요하며, 이는 이후 장에서 copy 모듈을 다룰 때 배우게 됩니다. 지금은 얕은 복사가 불변 항목(숫자, 문자열, 튜플)의 리스트에서는 완벽하게 동작하지만, 중첩된 가변 구조에서는 주의가 필요하다는 점을 알아 두세요.
14.6.5) 공유 참조가 유용할 때
때로는 여러 변수가 같은 리스트를 참조하도록 원할 수도 있습니다. 이는 코드의 여러 부분에서 하나의 리스트를 수정해야 할 때 유용합니다:
# 리스트를 제자리에서 수정하는 함수
def add_bonus_points(scores, bonus):
for i in range(len(scores)):
scores[i] = scores[i] + bonus
# 원본 리스트가 수정됩니다
student_scores = [85, 92, 78]
add_bonus_points(student_scores, 5)
print(student_scores) # Output: [90, 97, 83]이는 함수가 복사본이 아니라 원본 리스트에 대한 참조를 받기 때문에 가능한 일입니다. 이 부분은 Part V에서 함수를 자세히 공부할 때 더 살펴보겠습니다.
14.7) 리스트를 순회할 때 enumerate() 사용하기
14.7.1) 인덱스와 값이 모두 필요한 경우
리스트를 순회할 때 인덱스와 값이 모두 필요한 경우가 있습니다. 한 가지 방법은 range(len(list))를 사용하는 것입니다:
students = ["Alice", "Bob", "Charlie", "Diana"]
for i in range(len(students)):
print(f"Student {i}: {students[i]}")
# Output:
# Student 0: Alice
# Student 1: Bob
# Student 2: Charlie
# Student 3: Diana이 방법도 동작하지만, 그다지 우아하지 않습니다. 각 값에 접근하기 위해 students[i]를 써야 하며, 값 자체를 직접 순회하는 것보다 가독성이 떨어집니다.
14.7.2) enumerate()로 더 깔끔한 코드 작성하기
enumerate() 함수는 더 나은 해결책을 제공합니다. 각 항목에 대해 인덱스와 값을 모두 반환합니다:
students = ["Alice", "Bob", "Charlie", "Diana"]
for index, student in enumerate(students):
print(f"Student {index}: {student}")
# Output:
# Student 0: Alice
# Student 1: Bob
# Student 2: Charlie
# Student 3: Dianafor index, value in enumerate(list) 문법은 enumerate()가 만들어 내는 각 쌍을 언패킹(unpack)합니다. 이는 range(len())을 사용하는 것보다 훨씬 읽기 쉽습니다.
14.7.3) enumerate()를 다른 숫자에서 시작하기
기본적으로 enumerate()는 0부터 카운트합니다. start 매개변수로 시작 숫자를 지정할 수 있습니다:
students = ["Alice", "Bob", "Charlie", "Diana"]
# 0 대신 1부터 카운트 시작
for position, student in enumerate(students, start=1):
print(f"Position {position}: {student}")
# Output:
# Position 1: Alice
# Position 2: Bob
# Position 3: Charlie
# Position 4: Diana이는 프로그래머 친화적인 인덱싱(0부터 시작) 대신 사람이 보기 좋은 번호(1부터 시작)를 표시하고 싶을 때 유용합니다.
enumerate() 실용 예시
다음은 번호가 매겨진 메뉴를 표시하는 실용 예시입니다:
menu_items = ["New Game", "Load Game", "Settings", "Quit"]
print("Main Menu:")
for number, item in enumerate(menu_items, start=1):
print(f"{number}. {item}")
# Output:
# Main Menu:
# 1. New Game
# 2. Load Game
# 3. Settings
# 4. Quit14.7.4) enumerate()로 리스트 수정하기
enumerate()는 위치에 따라 리스트 요소를 수정해야 할 때 사용할 수 있습니다:
# 점수에 위치 기반 보너스 추가
scores = [85, 92, 78, 95, 88]
for index, score in enumerate(scores):
# 첫 번째 학생은 보너스 5점, 두 번째는 4점, 이런 식입니다
bonus = 5 - index
if bonus > 0:
scores[index] = score + bonus
print(scores) # Output: [90, 96, 81, 97, 89]14.8) 흔한 리스트 패턴: 검색, 필터링, 데이터 집계
14.8.1) 리스트에서 항목 찾기
가장 흔한 작업 중 하나는 리스트에 특정 항목이 들어 있는지 확인하는 것입니다. 7장에서 배운 in 연산자를 사용하면 간단합니다:
students = ["Alice", "Bob", "Charlie", "Diana"]
# 학생이 리스트에 있는지 확인
if "Charlie" in students:
print("Charlie is enrolled") # Output: Charlie is enrolled
if "Eve" not in students:
print("Eve is not enrolled") # Output: Eve is not enrolled항목의 위치를 찾으려면 index() 메서드를 사용하면 되는데(14.3.9절에서 다룸), 먼저 항목이 존재하는지 확인해야 한다는 점을 기억하세요:
scores = [85, 92, 78, 95, 88]
target_score = 95
if target_score in scores:
position = scores.index(target_score)
print(f"Score {target_score} found at index {position}")
# Output: Score 95 found at index 3
else:
print(f"Score {target_score} not found")14.8.2) 최대값과 최소값 찾기
Python 내장 함수 max()와 min()은 리스트에서도 동작합니다:
scores = [85, 92, 78, 95, 88, 91, 76]
highest_score = max(scores)
lowest_score = min(scores)
print(f"Highest score: {highest_score}") # Output: Highest score: 95
print(f"Lowest score: {lowest_score}") # Output: Lowest score: 7614.8.3) 집계 계산: 합계, 평균, 개수
합계와 평균을 구하는 것은 기본적인 리스트 연산입니다:
scores = [85, 92, 78, 95, 88, 91, 76, 89]
# 합계와 평균 계산
total = sum(scores)
count = len(scores)
average = total / count
print(f"Total: {total}") # Output: Total: 694
print(f"Count: {count}") # Output: Count: 8
print(f"Average: {average:.2f}") # Output: Average: 86.75다음은 장바구니 합계를 계산하는 실용 예시입니다:
cart_items = ["Milk", "Bread", "Eggs", "Butter", "Cheese"]
prices = [3.99, 2.49, 4.99, 5.49, 6.99]
# 총 비용 계산
total_cost = sum(prices)
item_count = len(cart_items)
print(f"Items in cart: {item_count}")
print(f"Total cost: ${total_cost:.2f}")
# Output:
# Items in cart: 5
# Total cost: $23.9514.9) 조건문에서의 리스트 가변성과 참/거짓 판정
14.9.1) 실제에서 리스트 가변성 이해하기
이 장 전체에서 리스트가 가변(mutable) 이라는 점—즉 생성 후에도 변경될 수 있다는 점—을 보았습니다. 이 가변성은 데이터 컬렉션을 저장하고 조작하는 데 리스트가 매우 강력해지는 이유입니다. 이제 포괄적인 예제로 이해를 정리해 봅시다:
# 빈 작업 리스트로 시작
tasks = []
print(f"Initial tasks: {tasks}") # Output: Initial tasks: []
# 작업 추가
tasks.append("Write code")
tasks.append("Test code")
tasks.append("Deploy code")
print(f"After adding: {tasks}")
# Output: After adding: ['Write code', 'Test code', 'Deploy code']
# 긴급 작업을 맨 앞에 삽입
tasks.insert(0, "Review requirements")
print(f"After inserting: {tasks}")
# Output: After inserting: ['Review requirements', 'Write code', 'Test code', 'Deploy code']
# 첫 작업 완료 처리 및 제거
completed = tasks.pop(0)
print(f"Completed: {completed}") # Output: Completed: Review requirements
print(f"Remaining: {tasks}")
# Output: Remaining: ['Write code', 'Test code', 'Deploy code']
# 작업 수정
tasks[1] = "Test code thoroughly"
print(f"After modifying: {tasks}")
# Output: After modifying: ['Write code', 'Test code thoroughly', 'Deploy code']14.9.2) 가변성 vs 불변성: 리스트 vs 문자열
가변 리스트와 불변 문자열의 차이를 이해하는 것은 중요합니다. 문자열에서는 연산이 원본을 수정하는 것이 아니라 새 문자열을 만듭니다:
# 문자열은 불변입니다
text = "hello"
text.upper() # 새 문자열을 만들며, 원본은 바꾸지 않습니다
print(text) # Output: hello (unchanged)
# 문자열을 "변경"하려면 재대입해야 합니다
text = text.upper()
print(text) # Output: HELLO
# 리스트는 가변입니다
numbers = [1, 2, 3]
numbers.append(4) # 리스트를 제자리에서 수정합니다
print(numbers) # Output: [1, 2, 3, 4] (changed)이 차이는 이러한 타입들을 다루는 방식에 영향을 줍니다:
# 문자열 연산은 재대입이 필요합니다
name = "alice"
name = name.capitalize() # 변경을 보려면 재대입해야 합니다
print(name) # Output: Alice
# 리스트 연산은 제자리에서 수정됩니다
scores = [85, 92, 78]
scores.append(95) # 재대입이 필요 없습니다
print(scores) # Output: [85, 92, 78, 95]14.9.3) 불리언 문맥에서 리스트 사용하기
리스트에는 참/거짓 판정(truthiness) 이 있습니다. 빈 리스트는 False로 간주되고, 비어 있지 않은 리스트는 True로 간주됩니다. 이는 조건문에서 유용합니다:
# 빈 리스트는 falsy입니다
empty_cart = []
if empty_cart:
print("Cart has items")
else:
print("Cart is empty") # Output: Cart is empty
# 비어 있지 않은 리스트는 truthy입니다
cart_with_items = ["Milk", "Bread"]
if cart_with_items:
print("Cart has items") # Output: Cart has items이 패턴은 처리 전에 리스트에 요소가 있는지 확인할 때 흔히 사용됩니다:
students = ["Alice", "Bob", "Charlie"]
if students:
print(f"We have {len(students)} students")
for student in students:
print(f" - {student}")
else:
print("No students enrolled")
# Output:
# We have 3 students
# - Alice
# - Bob
# - Charlie14.9.4) 실용 패턴: 빌 때까지 처리하기
리스트의 참/거짓 판정은 리스트가 빌 때까지 항목을 처리하는 유용한 패턴을 가능하게 합니다:
# 작업이 남아 있지 않을 때까지 처리
tasks = ["Task 1", "Task 2", "Task 3"]
while tasks: # 리스트가 비어 있지 않은 동안 계속
current_task = tasks.pop(0)
print(f"Processing: {current_task}")
print("All tasks completed!")
# Output:
# Processing: Task 1
# Processing: Task 2
# Processing: Task 3
# All tasks completed!14.9.5) 빈 리스트 확인하기: 명시적 vs 암시적
리스트가 비어 있는지 확인하는 방법은 두 가지가 있습니다:
items = []
# 암시적 검사(Pythonic)
if not items:
print("List is empty") # Output: List is empty
# 명시적 검사(이것도 유효함)
if len(items) == 0:
print("List is empty") # Output: List is empty암시적 검사(if not items:)는 더 간결하고 어떤 컬렉션 타입에도 동작하기 때문에 일반적으로 Python에서는 이를 선호합니다. 하지만 두 방법 모두 올바르며 실제 코드에서도 둘 다 볼 수 있습니다.
14.9.6) 가변성과 함수 동작
리스트를 함수(Part V에서 자세히 다룹니다)에 전달하면 함수는 같은 리스트 객체에 대한 참조를 받습니다. 즉 함수가 원본 리스트를 수정할 수 있습니다:
def add_item(shopping_list, item):
shopping_list.append(item)
print(f"Added {item}")
# 원본 리스트가 수정됩니다
cart = ["Milk", "Bread"]
print(f"Before: {cart}") # Output: Before: ['Milk', 'Bread']
add_item(cart, "Eggs") # Output: Added Eggs
print(f"After: {cart}") # Output: After: ['Milk', 'Bread', 'Eggs']이 동작은 문자열과 숫자처럼 불변인 타입과는 다르며, 불변 타입에서는 함수가 원래 값을 바꿀 수 없습니다. 이 구분을 이해하는 것은 올바른 프로그램을 작성하는 데 매우 중요합니다.
리스트는 Python에서 가장 기본적이면서도 다재다능한 자료구조 중 하나입니다. 리스트는 순서가 있고 가변인 컬렉션을 제공하며, 필요에 따라 커지거나 작아질 수 있어 관련된 데이터의 시퀀스를 저장하고 처리하는 데 완벽합니다. 여러분은 이 장에서 리스트를 생성하는 방법, 인덱싱과 슬라이싱으로 요소에 접근하는 방법, 다양한 메서드로 수정하는 방법, 효율적으로 순회하는 방법, 그리고 가변이라는 성질을 이해하는 방법을 배웠습니다.
우리가 살펴본 패턴—검색, 필터링, 집계, 데이터 변환—은 Python에서 컬렉션을 다루는 기초가 됩니다. 학습을 계속하면 리스트 컴프리헨션(35장)과 고급 반복 기법(36~37장) 등 리스트를 다루는 훨씬 더 강력한 방법을 발견하게 될 것입니다. 하지만 이 장에서 익힌 기본기는 Python 프로그래밍 여정 전반에서 큰 도움이 될 것입니다.