15. 튜플과 range: 단순한 불변 시퀀스
14장에서는 Python의 다재다능한 가변 시퀀스 타입인 리스트(list)를 살펴보았습니다. 이제는 또 다른 중요한 시퀀스 타입 두 가지인 튜플(tuple) 과 range(range) 를 살펴보겠습니다. 리스트는 시간이 지나며 변하는 컬렉션을 저장하는 데 뛰어나지만, 튜플은 데이터를 수정으로부터 보호하는 불변(immutable) 시퀀스를 제공하고, range는 숫자 시퀀스를 메모리 효율적으로 표현하는 방법을 제공합니다.
각 시퀀스 타입을 언제 사용해야 하는지 이해하면 프로그램이 더 효율적이고, 더 안전하며, 의도가 더 명확해집니다. 이 장을 마치면 튜플과 range를 효과적으로 다루는 방법을 알게 되고, 모든 Python 시퀀스 타입 전반에 걸쳐 동작하는 공통 연산도 이해하게 됩니다.
15.1) 튜플 만들기와 사용하기(쉼표의 중요성)
튜플(tuple) 은 항목의 순서가 있는, 불변(immutable) 시퀀스입니다. 리스트처럼 튜플도 어떤 타입의 데이터든 담을 수 있고 요소의 순서를 유지합니다. 하지만 리스트와 달리, 튜플을 한 번 만들면 그 내용을 수정할 수 없습니다.
괄호로 튜플 만들기
튜플을 만드는 가장 일반적인 방법은 쉼표로 구분된 값을 괄호로 감싸는 것입니다.
# 학생 시험 점수 튜플
scores = (85, 92, 78, 95)
print(scores) # Output: (85, 92, 78, 95)
print(type(scores)) # Output: <class 'tuple'>
# 여러 데이터 타입이 섞인 튜플
student_info = ("Alice", 20, "Computer Science", 3.8)
print(student_info) # Output: ('Alice', 20, 'Computer Science', 3.8)
# 빈 튜플
empty = ()
print(empty) # Output: ()
print(len(empty)) # Output: 0튜플은 리터럴 문법으로 괄호 ()를 사용하고, 리스트는 대괄호 []를 사용합니다. 이런 시각적 구분은 지금 다루는 타입이 무엇인지 즉시 알아차리는 데 도움이 됩니다.
튜플을 만드는 것은 괄호가 아니라 쉼표입니다
여기에는 많은 초보자가 놀라는 중요한 디테일이 있습니다. 실제로 튜플을 만드는 것은 괄호가 아니라 쉼표입니다. 괄호는 종종 선택 사항이며, 주로 튜플을 더 눈에 띄게 하거나 식(expression)에서 그룹화하기 위해 사용됩니다.
# 아래는 모두 같은 튜플을 만듭니다
coordinates_1 = (10, 20)
coordinates_2 = 10, 20 # 괄호가 필요 없습니다!
print(coordinates_1) # Output: (10, 20)
print(coordinates_2) # Output: (10, 20)
print(coordinates_1 == coordinates_2) # Output: True
# 중요한 것은 쉼표입니다
x = (42) # 이것은 괄호로 감싼 정수 42일 뿐입니다
y = (42,) # 이것은 요소가 1개인 튜플입니다
print(type(x)) # Output: <class 'int'>
print(type(y)) # Output: <class 'tuple'>
print(y) # Output: (42,)(42)의 괄호는 수학 표현식에서처럼 단지 그룹화 괄호입니다. 요소가 하나인 튜플을 만들려면 끝에 쉼표를 반드시 포함해야 합니다: (42,). 이 쉼표가 Python에게 그룹화된 표현식이 아니라 튜플을 원한다는 뜻을 알려줍니다.
괄호가 필요한 경우
쉼표가 튜플을 만들지만, 모호함을 피하기 위해 특정 상황에서는 괄호가 필요해집니다.
# 괄호가 없으면 혼란스러울 수 있습니다
def get_dimensions():
return 1920, 1080 # 튜플을 반환합니다
width, height = get_dimensions()
print(f"Screen: {width}x{height}") # Output: Screen: 1920x1080
# 튜플을 함수 인자로 전달할 때는 괄호가 필요합니다
print((1, 2, 3)) # Output: (1, 2, 3)
# 괄호가 없으면 Python은 세 개의 별도 인자로 인식합니다
# 복잡한 표현식에서는 괄호가 필요합니다
result = (10, 20) + (30, 40) # 튜플 연결(concatenation)
print(result) # Output: (10, 20, 30, 40)요소가 1개인 튜플 만들기
요소가 1개인 튜플에서 끝의 쉼표가 필요하다는 규칙은 초보자를 종종 당황하게 합니다.
# 흔한 실수: 쉼표를 빼먹기
not_a_tuple = ("Python")
print(type(not_a_tuple)) # Output: <class 'str'>
print(not_a_tuple) # Output: Python
# 올바른 방법: 끝의 쉼표를 포함
is_a_tuple = ("Python",)
print(type(is_a_tuple)) # Output: <class 'tuple'>
print(is_a_tuple) # Output: ('Python',)
# 괄호 없이도 쉼표는 동작합니다
also_a_tuple = "Python",
print(type(also_a_tuple)) # Output: <class 'tuple'>
print(also_a_tuple) # Output: ('Python',)왜 Python은 이렇게 어색해 보이는 문법을 요구할까요? 괄호는 Python에서 이미 또 다른 의미가 있기 때문입니다. 즉, 표현식을 그룹화합니다. 쉼표가 없으면 Python은 (42)가 그룹화된 숫자인지, (42)가 튜플인지 구분할 방법이 없습니다.
튜플 요소에 접근하기
튜플은 리스트와 동일한 인덱싱(indexing) 및 슬라이싱(slicing) 연산을 지원합니다.
# 학생 정보 튜플
student = ("Bob", 22, "Physics", 3.6)
# 개별 요소 접근(0부터 시작)
name = student[0]
age = student[1]
major = student[2]
gpa = student[3]
print(f"{name} is {age} years old") # Output: Bob is 22 years old
print(f"Major: {major}, GPA: {gpa}") # Output: Major: Physics, GPA: 3.6
# 음수 인덱싱도 동작합니다
last_item = student[-1]
print(f"Last item: {last_item}") # Output: Last item: 3.6
# 슬라이싱은 새 튜플을 추출합니다
first_two = student[:2]
print(first_two) # Output: ('Bob', 22)
print(type(first_two)) # Output: <class 'tuple'>14장에서 리스트로 배운 모든 인덱싱과 슬라이싱 기법은 튜플에서도 동일하게 동작합니다. 핵심 차이는 튜플은 생성 후에 수정할 수 없다는 점입니다.
15.2) 튜플 패킹과 언패킹
튜플의 가장 강력하고 우아한 기능 중 하나는 여러 값을 함께 패킹(packing)하고, 이를 별도의 변수로 언패킹(unpacking)할 수 있다는 점입니다. 이 기능은 Python 코드를 놀라울 정도로 간결하고 읽기 쉽게 만들어 줍니다.
튜플 패킹
튜플 패킹(tuple packing) 은 여러 값을 쉼표로 구분해 함께 놓음으로써 튜플을 생성할 때 발생합니다.
# 값을 튜플로 패킹
coordinates = 10, 20, 30
print(coordinates) # Output: (10, 20, 30)
# 서로 다른 타입을 패킹
user_data = "Alice", 25, "alice@example.com"
print(user_data) # Output: ('Alice', 25, 'alice@example.com')
# 함수 반환값 패킹
def get_statistics(numbers):
total = sum(numbers)
count = len(numbers)
average = total / count
return total, count, average # 세 값을 튜플로 패킹합니다
stats = get_statistics([85, 90, 78, 92, 88])
print(stats) # Output: (433, 5, 86.6)함수가 쉼표로 구분된 여러 값을 반환하면, Python은 이를 자동으로 튜플로 패킹합니다. 이것이 함수가 여러 값을 반환하는 것처럼 보이는 이유입니다. 실제로는 그 값들을 담고 있는 하나의 튜플을 반환하는 것입니다.
튜플 언패킹
튜플 언패킹(tuple unpacking) 은 반대 과정으로, 튜플에서 값을 꺼내어 별도의 변수에 대입합니다.
# 기본 언패킹
point = (100, 200)
x, y = point
print(f"x = {x}, y = {y}") # Output: x = 100, y = 200
# 언패킹은 튜플뿐 아니라 모든 시퀀스에서 동작합니다
name, age, email = ["Bob", 30, "bob@example.com"]
print(f"{name} is {age} years old") # Output: Bob is 30 years old
# 함수의 반환값을 바로 언패킹
total, count, average = get_statistics([95, 88, 92, 85])
print(f"Average of {count} scores: {average}") # Output: Average of 4 scores: 90.0왼쪽의 변수 개수는 시퀀스의 요소 개수와 일치해야 합니다. 일치하지 않으면 Python은 ValueError를 발생시킵니다.
# 이것은 에러를 발생시킵니다
coordinates = (10, 20, 30)
# x, y = coordinates # ValueError: too many values to unpack (expected 2)
# 이것도 에러를 발생시킵니다
point = (5, 10)
# x, y, z = point # ValueError: not enough values to unpack (expected 3, got 2)튜플 언패킹으로 변수 값 교환하기
튜플 언패킹을 사용하면 임시 변수를 쓰지 않고도 변수 값을 우아하게 교환할 수 있습니다.
# 임시 변수를 사용한 전통적인 교환
a = 10
b = 20
temp = a
a = b
b = temp
print(f"a = {a}, b = {b}") # Output: a = 20, b = 10
# 튜플 언패킹을 사용한 Python식 우아한 교환
x = 100
y = 200
x, y = y, x # 한 줄로 교환!
print(f"x = {x}, y = {y}") # Output: x = 200, y = 100
# 두 개보다 많은 변수 교환
first = "A"
second = "B"
third = "C"
first, second, third = third, first, second
print(first, second, third) # Output: C A B이것은 어떻게 동작할까요? Python은 먼저 오른쪽을 평가하여 (y, x) 튜플을 만들고, 그 다음 왼쪽 변수들에 언패킹합니다. 이 과정은 한 단계로 일어나므로 임시 변수가 필요 없습니다.
별(star) 연산자를 이용한 확장 언패킹
Python은 * 연산자를 사용해 여러 요소를 한꺼번에 담는 확장 언패킹(extended unpacking) 을 제공합니다.
# "나머지" 변수를 사용한 언패킹
scores = (95, 88, 92, 85, 90, 87)
first, second, *rest = scores
print(f"Top two: {first}, {second}") # Output: Top two: 95, 88
print(f"Others: {rest}") # Output: Others: [92, 85, 90, 87]
print(type(rest)) # Output: <class 'list'>
# 별 표시는 어디에나 올 수 있습니다
numbers = (1, 2, 3, 4, 5)
first, *middle, last = numbers
print(f"First: {first}") # Output: First: 1
print(f"Middle: {middle}") # Output: Middle: [2, 3, 4]
print(f"Last: {last}") # Output: Last: 5
# 앞부분을 담기
*beginning, second_last, last = numbers
print(f"Beginning: {beginning}") # Output: Beginning: [1, 2, 3]
print(f"Last two: {second_last}, {last}") # Output: Last two: 4, 5별이 붙은 변수는, 튜플에서 언패킹하더라도 항상 리스트(list) 로 요소들을 담는다는 점에 주목하세요. 담을 요소가 없다면 별 변수는 빈 리스트가 됩니다.
# 담을 것이 없을 때
a, b, *rest = (10, 20)
print(rest) # Output: []
# 언패킹에서 별은 하나만 허용됩니다
# first, *middle, *end = (1, 2, 3, 4) # SyntaxError: multiple starred expressions밑줄로 값 무시하기
때로는 튜플에서 일부 값만 필요합니다. 관례적으로 Python 프로그래머들은 무시하려는 값에 대해 변수 이름으로 밑줄 _를 사용합니다.
# 날짜 문자열 파싱
date_string = "2024-03-15"
year, month, day = date_string.split("-")
print(f"Month: {month}") # Output: Month: 03
# 월만 신경 쓴다면
_, month, _ = date_string.split("-")
print(f"Month: {month}") # Output: Month: 03
# 확장 언패킹과 함께 사용
data = ("Alice", 25, "Engineer", "New York", "alice@example.com")
name, age, *_, email = data
print(f"{name} ({age}): {email}") # Output: Alice (25): alice@example.com밑줄은 단지 일반 변수 이름이지만, 이를 사용하면 다른 프로그래머(그리고 자기 자신)에게 해당 값들을 의도적으로 무시하고 있다는 신호가 됩니다.
패킹과 언패킹의 실용 예시
# 계산 결과로 여러 값을 반환
def calculate_rectangle_properties(width, height):
"""직사각형의 넓이와 둘레를 계산합니다."""
area = width * height
perimeter = 2 * (width + height)
return area, perimeter # 패킹
# 결과 언패킹
rect_area, rect_perimeter = calculate_rectangle_properties(5, 3)
print(f"Area: {rect_area}, Perimeter: {rect_perimeter}") # Output: Area: 15, Perimeter: 16
# 언패킹으로 반복하기
students = [
("Alice", 85),
("Bob", 92),
("Carol", 78)
]
for name, score in students: # 반복문에서 언패킹
print(f"{name}: {score}")
# Output:
# Alice: 85
# Bob: 92
# Carol: 78튜플 패킹과 언패킹은 Python 코드를 더 읽기 쉽고 표현력 있게 만듭니다. 인덱스로 튜플 요소에 접근하는 것(student[0], student[1]) 대신, 의미 있는 변수 이름으로 언패킹해 사용할 수 있습니다.
15.3) 튜플은 불변입니다: 그게 유용한 경우
튜플의 정의적 특징은 불변성(immutability) 입니다. 한 번 만들면 튜플의 내용은 바꿀 수 없습니다. 요소를 추가하거나 제거하거나 수정할 수 없습니다. 이런 불변성은 제한처럼 보일 수 있지만, 중요한 이점을 제공합니다.
실전에서 불변성이 의미하는 것
# 튜플 만들기
coordinates = (10, 20, 30)
print(coordinates) # Output: (10, 20, 30)
# 수정 시도는 에러를 발생시킵니다
# coordinates[0] = 15 # TypeError: 'tuple' object does not support item assignment
# 요소 추가 시도는 에러를 발생시킵니다
# coordinates.append(40) # AttributeError: 'tuple' object has no attribute 'append'
# 요소 제거 시도는 에러를 발생시킵니다
# del coordinates[1] # TypeError: 'tuple' object doesn't support item deletionPython이 튜플은 item assignment를 지원하지 않는다고 말할 때, 그것은 튜플의 어떤 위치에 저장된 값도 바꿀 수 없다는 뜻입니다. 튜플의 구조는 생성 시점에 고정됩니다.
가변 리스트와 불변 튜플 비교
# 리스트는 가변입니다 - 변경할 수 있습니다
shopping_list = ["milk", "bread", "eggs"]
shopping_list[1] = "butter" # 요소 수정
shopping_list.append("cheese") # 요소 추가
print(shopping_list) # Output: ['milk', 'butter', 'eggs', 'cheese']
# 튜플은 불변입니다 - 변경할 수 없습니다
product_dimensions = (10, 20, 5) # width, height, depth in cm
# product_dimensions[0] = 12 # TypeError: cannot modify
# product_dimensions.append(3) # AttributeError: no append method
# 튜플을 "변경"하려면 새 튜플을 만들어야 합니다
new_dimensions = (12, 20, 5) # 완전히 새로운 튜플 생성
print(new_dimensions) # Output: (12, 20, 5)불변성이 유용한 이유
불변성은 몇 가지 실용적인 이점을 제공합니다.
1. 데이터 무결성과 안전성
함수에 튜플을 전달하면, 그 함수가 실수로 데이터를 수정할 수 없다는 것을 알 수 있습니다.
def calculate_distance(point1, point2):
"""2D 두 점 사이의 거리를 계산합니다."""
x1, y1 = point1
x2, y2 = point2
dx = x2 - x1
dy = y2 - y1
# 원한다 하더라도 입력 튜플은 수정할 수 없습니다
return (dx**2 + dy**2) ** 0.5
start = (0, 0)
end = (3, 4)
distance = calculate_distance(start, end)
print(f"Distance: {distance}") # Output: Distance: 5.0
print(f"Start point unchanged: {start}") # Output: Start point unchanged: (0, 0)리스트라면 함수가 데이터를 수정할 가능성을 걱정해야 합니다. 튜플은 그렇지 않다는 보장이 있습니다.
2. 튜플을 딕셔너리 키로 사용하기
17장에서 더 자세히 살펴보겠지만, 딕셔너리 키는 해시 가능(hashable) 해야 합니다. 즉, 절대 변하지 않는 해시 값을 가져야 합니다. 튜플 같은 불변 객체는 딕셔너리 키가 될 수 있지만, 리스트 같은 가변 객체는 될 수 없습니다.
# 튜플은 딕셔너리 키가 될 수 있습니다
locations = {
(0, 0): "Origin",
(10, 20): "Point A",
(30, 40): "Point B"
}
print(locations[(10, 20)]) # Output: Point A
# 리스트는 딕셔너리 키가 될 수 없습니다
# locations_bad = {
# [0, 0]: "Origin" # TypeError: unhashable type: 'list'
# }3. 의도를 표현하기
리스트 대신 튜플을 사용하면 이 데이터는 변하지 않아야 한다는 뜻을 다른 프로그래머(그리고 자기 자신)에게 전달합니다.
# RGB 색상 값 - 절대 변하면 안 됩니다
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
# 데이터베이스 연결 파라미터 - 고정된 설정
DB_CONFIG = ("localhost", 5432, "myapp", "production")
# 지리 좌표 - 위치는 변하지 않습니다
EIFFEL_TOWER = (48.8584, 2.2945) # latitude, longitude코드에서 튜플을 보면 이 데이터는 일정하게 유지되어야 한다는 것을 즉시 알 수 있습니다. 리스트를 보면 수정될 수 있음을 알 수 있습니다.
4. 성능 이점
튜플은 불변이기 때문에 Python은 리스트에서 할 수 없는 방식으로 최적화할 수 있습니다. 27장에서 sys 모듈을 배우겠지만, 지금은 sys.getsizeof()가 객체가 사용하는 메모리 크기를 알려준다는 것만 알아두세요.
import sys
# 튜플은 동등한 리스트보다 메모리를 덜 사용합니다
tuple_data = (1, 2, 3, 4, 5)
list_data = [1, 2, 3, 4, 5]
print(f"Tuple size: {sys.getsizeof(tuple_data)} bytes") # Output: Tuple size: 80 bytes (may vary by Python version)
print(f"List size: {sys.getsizeof(list_data)} bytes") # Output: List size: 104 bytes (may vary by Python version)
# 튜플 생성이 더 빠릅니다
import timeit
tuple_time = timeit.timeit("(1, 2, 3, 4, 5)", number=1000000)
list_time = timeit.timeit("[1, 2, 3, 4, 5]", number=1000000)
print(f"Tuple creation: {tuple_time:.4f} seconds")
print(f"List creation: {list_time:.4f} seconds")
# Example output: Tuple creation: 0.0055 seconds, List creation: 0.0292 seconds15.4) 불변성의 함정: 튜플에 가변 항목이 들어 있는 경우
튜플 자체는 불변이지만, 리스트나 딕셔너리 같은 가변 객체를 포함할 수 있습니다. 이는 미묘하지만 중요한 차이를 만들어냅니다. 튜플의 구조는 고정이지만, 그 안에 들어 있는 가변 객체의 내용은 여전히 바뀔 수 있습니다.
차이를 이해하기
# 리스트를 포함하는 튜플
student_data = ("Alice", 20, [85, 90, 78]) # name, age, scores
print(student_data) # Output: ('Alice', 20, [85, 90, 78])
# 튜플 요소를 재할당할 수는 없습니다
# student_data[0] = "Bob" # TypeError: 'tuple' object does not support item assignment
# 하지만 튜플 안의 리스트는 수정할 수 있습니다
student_data[2].append(92) # 새 점수 추가
print(student_data) # Output: ('Alice', 20, [85, 90, 78, 92])
student_data[2][0] = 88 # 기존 점수 수정
print(student_data) # Output: ('Alice', 20, [88, 90, 78, 92])여기서 무슨 일이 벌어지고 있을까요? 튜플은 세 개의 참조(reference)를 저장합니다. 하나는 문자열 "Alice"에, 하나는 정수 20에, 하나는 리스트 객체에 대한 참조입니다. 튜플의 구조(즉, 어떤 객체를 참조하는지)는 바뀔 수 없습니다. 하지만 리스트 객체 자체는 가변이므로 그 내용은 바뀔 수 있습니다.
차이를 시각화하기
# 튜플 구조는 고정입니다
data = ("Python", [1, 2, 3])
# 이것은 튜플이 참조하는 대상을 바꾸려는 시도입니다 - 허용되지 않습니다
# data[1] = [4, 5, 6] # TypeError
# 이것은 튜플이 참조하는 리스트를 수정하는 것입니다 - 허용됩니다
data[1].append(4)
print(data) # Output: ('Python', [1, 2, 3, 4])
# 튜플은 여전히 같은 리스트 객체를 참조합니다
# 리스트의 내용만 바뀌었고, 튜플이 가리키는 리스트 자체는 바뀌지 않았습니다이렇게 생각해 보세요. 튜플은 상자들이 한 줄로 늘어선 것과 같고, 각 상자에는 어떤 객체를 가리키는 참조가 들어 있습니다. 상자 자체는 잠겨 있어(불변) 위치를 바꿀 수 없지만, 상자 안에 가변 객체를 가리키는 참조가 있다면 그 객체는 여전히 바뀔 수 있습니다.
딕셔너리를 포함하는 튜플
같은 원칙이 튜플 내부의 딕셔너리에도 적용됩니다.
# 딕셔너리를 포함하는 튜플
user_profile = ("alice", {"email": "alice@example.com", "age": 25})
print(user_profile) # Output: ('alice', {'email': 'alice@example.com', 'age': 25})
# 튜플이 참조하는 딕셔너리 자체를 바꿀 수는 없습니다
# user_profile[1] = {"email": "newemail@example.com"} # TypeError
# 하지만 딕셔너리 자체는 수정할 수 있습니다
user_profile[1]["age"] = 26
user_profile[1]["city"] = "New York"
print(user_profile) # Output: ('alice', {'email': 'alice@example.com', 'age': 26, 'city': 'New York'})딕셔너리 키에서 이것이 중요한 이유
튜플은 모든 요소가 해시 가능(hashable)할 때만 딕셔너리 키로 사용할 수 있습니다. 튜플 자체는 불변이지만, 가변 객체(리스트 등)를 포함한 튜플은 아예 해시가 불가능하므로 딕셔너리 키로 사용할 수 없습니다.
# 이것은 동작하지만 위험합니다
tuple_with_list = ("key", [1, 2, 3])
# data = {tuple_with_list: "value"} # TypeError: unhashable type: 'list'딕셔너리 키로는 완전히 불변인 객체(문자열, 숫자, frozenset, 다른 튜플 등)만 포함하는 튜플만 사용하세요.
완전히 불변인 튜플 만들기
완전히 불변인 튜플이 필요하다면, 내용물도 모두 불변이어야 합니다.
# 완전히 불변인 튜플 - 불변 타입만 포함
point_3d = (10, 20, 30) # 모두 정수
rgb_color = (255, 128, 0) # 모두 정수
coordinates = ((10, 20), (30, 40)) # 튜플의 튜플
# 이것들은 딕셔너리 키로 안전합니다
color_names = {
(255, 0, 0): "Red",
(0, 255, 0): "Green",
(0, 0, 255): "Blue"
}
# 중첩 튜플도 불변입니다
nested = ((1, 2), (3, 4))
# nested[0][0] = 5 # TypeError: 'tuple' object does not support item assignment가변 내용이 의도적인 경우
때로는 가변 내용을 가진 튜플이 실제로 필요할 수도 있습니다. 예를 들어, 레코드 구조는 고정이지만 특정 필드는 바뀌어야 할 때입니다.
# 신원은 고정이고 성적은 변하는 학생 레코드
def create_student(name, student_id):
"""빈 성적 리스트를 가진 학생 레코드를 생성합니다."""
return (name, student_id, []) # name과 ID는 고정, grades는 변경 가능
student = create_student("Alice", "S12345")
print(student) # Output: ('Alice', 'S12345', [])
# 학생의 신원은 고정입니다
print(f"Student: {student[0]} (ID: {student[1]})") # Output: Student: Alice (ID: S12345)
# 하지만 성적은 쌓이는 대로 추가할 수 있습니다
student[2].append(85)
student[2].append(92)
student[2].append(78)
print(f"Grades: {student[2]}") # Output: Grades: [85, 92, 78]
# 튜플 구조는 name과 ID를 실수로 바꾸지 못하게 보호하면서
# 성적 리스트는 커질 수 있도록 허용합니다이 패턴은 어떤 데이터는 보호하면서 다른 데이터는 바뀌도록 허용하고 싶을 때 유용합니다. 다만 튜플의 불변성과 내용물의 가변성 사이의 차이를 반드시 인지해야 합니다.
15.5) 리스트 대신 튜플을 사용해야 할 때
튜플과 리스트 중 무엇을 선택할지는 중요한 설계 결정입니다. 둘 다 시퀀스이지만, 서로 다른 목적을 갖고 서로 다른 의도를 전달합니다.
고정된, 이질적인 데이터에는 튜플을 사용하세요
튜플은 보통 서로 다른 타입을 포함하는, 하나의 논리적 엔티티를 표현하는 고정된 개수의 항목에 가장 잘 맞습니다.
# 학생 레코드: name, age, major, GPA
student = ("Alice", 20, "Computer Science", 3.8)
# 지리 좌표: latitude, longitude
location = (40.7128, -74.0060) # New York City
# RGB 색상: red, green, blue
color = (255, 128, 0)
# 데이터베이스 연결: host, port, database, username
db_connection = ("localhost", 5432, "myapp", "admin")
# 날짜: year, month, day
date = (2024, 3, 15)각 튜플은 각 요소의 위치가 특정 의미를 갖는 완전한 "레코드"를 나타냅니다. 첫 번째 요소는 항상 이름, 두 번째는 항상 나이, 이런 식입니다.
동질적인 컬렉션에는 리스트를 사용하세요
리스트는 비슷한 항목들의 개수가 가변적이고, 추가/삭제/재정렬할 수도 있는 경우에 가장 적합합니다.
# 장보기 목록 - 같은 타입의 항목(문자열)
shopping_list = ["milk", "bread", "eggs", "butter"]
shopping_list.append("cheese") # 필요하면 항목 추가
shopping_list.remove("bread") # 항목 제거
# 시험 점수 - 같은 타입의 항목(숫자)
test_scores = [85, 92, 78, 95, 88]
test_scores.append(90) # 새 점수 추가
test_scores.sort() # 점수 재정렬
# 사용자 이름 - 같은 타입의 항목(문자열)
active_users = ["alice", "bob", "carol"]
active_users.extend(["dave", "eve"]) # 여러 사용자 추가리스트는 항목 개수가 바뀔 수 있고, 각 항목이 같은 역할을 하는 컬렉션을 위한 것입니다.
함수 반환값에는 튜플을 사용하세요
함수가 관련된 여러 값을 반환할 때, 튜플은 자연스러운 선택입니다.
def get_user_info(user_id):
"""데이터베이스에서 사용자 정보를 가져옵니다."""
# 데이터베이스 조회 시뮬레이션
return "Alice", "alice@example.com", 25, "New York"
# 반환된 튜플 언패킹
name, email, age, city = get_user_info(101)
print(f"{name} from {city}") # Output: Alice from New York
def calculate_statistics(numbers):
"""숫자 목록의 최솟값, 최댓값, 평균을 계산합니다."""
if not numbers:
return None, None, None
minimum = min(numbers)
maximum = max(numbers)
average = sum(numbers) / len(numbers)
return minimum, maximum, average
# 결과 언패킹
min_val, max_val, avg_val = calculate_statistics([85, 92, 78, 95, 88])
print(f"Range: {min_val} to {max_val}, Average: {avg_val}")
# Output: Range: 78 to 95, Average: 87.6튜플을 반환하면 이 값들이 서로 관련되어 있고 함께 고려되어야 한다는 것이 분명해집니다.
딕셔너리 키에는 튜플을 사용하세요
딕셔너리에서 복합 키(composite keys)가 필요할 때 튜플은 필수입니다.
# 과목과 학기별 학생 성적
grades = {
("CS101", "Fall2023"): 85,
("CS101", "Spring2024"): 90,
("MATH201", "Fall2023"): 88,
("MATH201", "Spring2024"): 92
}
# 특정 성적 조회
course = "CS101"
semester = "Spring2024"
grade = grades[(course, semester)]
print(f"Grade in {course} ({semester}): {grade}") # Output: Grade in CS101 (Spring2024): 90
# 격자 좌표를 딕셔너리 키로 사용
grid = {
(0, 0): "Start",
(5, 3): "Obstacle",
(10, 10): "Goal"
}
position = (5, 3)
if position in grid:
print(f"At {position}: {grid[position]}") # Output: At (5, 3): Obstacle리스트는 가변이라 딕셔너리 키가 될 수 없지만, 튜플은 될 수 있습니다.
불변 설정에는 튜플을 사용하세요
절대 바뀌면 안 되는 설정 데이터가 있을 때, 튜플은 그런 의도를 표현해 줍니다.
# 변하지 않아야 하는 애플리케이션 설정
APP_CONFIG = (
"MyApp", # Application name
"1.0.0", # Version
"production", # Environment
True, # Debug mode
8080 # Port
)
# UI용 컬러 팔레트 - 이 색들은 고정입니다
COLOR_PALETTE = (
(255, 0, 0), # Primary red
(0, 128, 255), # Primary blue
(255, 255, 255), # White
(0, 0, 0) # Black
)
# API 엔드포인트 - 이 URL들은 바뀌지 않습니다
API_ENDPOINTS = (
"https://api.example.com/users",
"https://api.example.com/products",
"https://api.example.com/orders"
)결정 가이드
# 다음 경우에 TUPLES를 사용:
# 1. 데이터가 고정 구조를 가진 하나의 레코드를 나타낼 때
employee = ("E001", "Alice", "Engineering", 75000)
# 2. 함수에서 여러 값을 반환할 때
def divide_with_remainder(a, b):
return a // b, a % b
# 3. 딕셔너리 키로 사용해야 할 때
cache = {(5, 10): 50, (3, 7): 21}
# 4. 데이터가 수정되면 안 될 때
SCREEN_RESOLUTION = (1920, 1080)
# 다음 경우에 LISTS를 사용:
# 1. 바뀔 수 있는 유사 항목들의 컬렉션일 때
tasks = ["Write code", "Test code", "Deploy code"]
tasks.append("Document code")
# 2. 항목을 추가/삭제/재정렬해야 할 때
scores = [85, 90, 78]
scores.sort()
scores.append(92)
# 3. 모든 항목이 같은 목적을 가질 때
usernames = ["alice", "bob", "carol"]
# 4. 컬렉션 크기를 미리 알 수 없을 때
results = []
for i in range(10):
results.append(i * 2)15.6) range 객체를 깊이 있게 이해하기
튜플과 리스트를 언제 사용해야 하는지 이해했으니, Python의 세 번째 불변 시퀀스 타입인 range를 살펴봅시다. range(range) 타입은 숫자의 불변 시퀀스를 나타냅니다. 모든 요소를 메모리에 저장하는 리스트나 튜플과 달리, range 객체는 필요할 때 숫자를 생성하므로, 큰 시퀀스를 표현하는 데 매우 메모리 효율적입니다.
range 객체 만들기
range() 함수는 세 가지 형태로 range 객체를 만듭니다.
# 인자 1개: range(stop)
# 0부터 stop 직전까지 숫자를 생성합니다
numbers = range(5)
print(list(numbers)) # Output: [0, 1, 2, 3, 4]
# 인자 2개: range(start, stop)
# start부터 stop 직전까지 숫자를 생성합니다
numbers = range(2, 7)
print(list(numbers)) # Output: [2, 3, 4, 5, 6]
# 인자 3개: range(start, stop, step)
# start부터 stop 직전까지 step만큼 증가시키며 숫자를 생성합니다
numbers = range(0, 10, 2)
print(list(numbers)) # Output: [0, 2, 4, 6, 8]
# 음수 step으로 역으로 카운트
numbers = range(10, 0, -1)
print(list(numbers)) # Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]range의 내용을 보려면 list()로 리스트로 변환한다는 점에 주목하세요. range 객체 자체를 출력하면 모든 값을 표시하지 않습니다.
r = range(5)
print(r) # Output: range(0, 5)
print(type(r)) # Output: <class 'range'>range 객체가 동작하는 방식
range 객체는 모든 값을 메모리에 저장하지 않습니다. 대신 필요할 때 각 값을 계산합니다.
import sys
# 100만 개 숫자를 나타내는 range
large_range = range(1000000)
print(f"Range size: {sys.getsizeof(large_range)} bytes") # Output: Range size: 48 bytes (may vary by Python version)
# 100만 개 숫자를 담은 리스트
large_list = list(range(1000000))
print(f"List size: {sys.getsizeof(large_list)} bytes") # Output: List size: 8000056 bytes (approximately 8MB)
# range는 작고, 리스트는 큽니다!range 객체는 start, stop, step 세 값만 저장합니다. 그리고 시퀀스에서 어떤 숫자가 필요해질 때 그 숫자를 계산합니다. 이 덕분에 range는 큰 시퀀스에서도 매우 효율적입니다.
for 반복문에서 range 사용하기
12장에서 배웠듯이, range는 보통 for 반복문(loop)과 함께 가장 많이 사용됩니다.
# 0부터 4까지 세기
for i in range(5):
print(f"Count: {i}")
# Output:
# Count: 0
# Count: 1
# Count: 2
# Count: 3
# Count: 4
# 1부터 10까지 세기
for i in range(1, 11):
print(i, end=" ")
print() # Output: 1 2 3 4 5 6 7 8 9 10
# 2씩 증가하며 세기
for i in range(0, 20, 2):
print(i, end=" ")
print() # Output: 0 2 4 6 8 10 12 14 16 18
# 거꾸로 세기
for i in range(5, 0, -1):
print(f"T-minus {i}")
# Output:
# T-minus 5
# T-minus 4
# T-minus 3
# T-minus 2
# T-minus 1range 객체의 인덱싱과 슬라이싱
range 객체는 다른 시퀀스처럼 인덱싱과 슬라이싱을 지원합니다.
# range 만들기
numbers = range(10, 50, 5) # 10, 15, 20, 25, 30, 35, 40, 45
# 인덱싱
print(numbers[0]) # Output: 10
print(numbers[3]) # Output: 25
print(numbers[-1]) # Output: 45
# 슬라이싱은 새 range를 반환합니다
subset = numbers[2:5]
print(subset) # Output: range(20, 35, 5)
print(list(subset)) # Output: [20, 25, 30]
# 길이
print(len(numbers)) # Output: 8포함 여부 확인하기
in 연산자로 숫자가 range에 포함되는지 확인할 수 있습니다.
# 0부터 20까지의 짝수
evens = range(0, 21, 2)
print(10 in evens) # Output: True
print(15 in evens) # Output: False
print(20 in evens) # Output: True
# 매우 효율적입니다 - Python은 모든 숫자를 생성하지 않습니다
# 해당 숫자가 시퀀스에 속하는지 계산합니다
large_range = range(0, 1000000, 3)
print(999999 in large_range) # Output: True (instant, no iteration needed)Python은 모든 숫자를 생성하지 않고도 수학적으로 포함 여부를 판단할 수 있으므로, 매우 큰 range에서도 이 연산은 극도로 빠릅니다.
빈 range와 역방향 range
# 빈 range - stop이 start와 같습니다
empty = range(5, 5)
print(list(empty)) # Output: []
print(len(empty)) # Output: 0
# 빈 range - 주어진 step으로 stop에 도달할 수 없습니다
impossible = range(1, 10, -1) # 음수 step으로는 증가할 수 없습니다
print(list(impossible)) # Output: []
# 역방향 range
backwards = range(10, 0, -1)
print(list(backwards)) # Output: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
# 음수 범위 역방향
negative_range = range(-5, -15, -2)
print(list(negative_range)) # Output: [-5, -7, -9, -11, -13]range와 리스트를 언제 사용할까
# 다음 경우에 range를 사용:
# 1. 반복(iteration)을 위한 숫자 시퀀스가 필요할 때
for i in range(100):
# 무언가를 100번 처리
pass
# 2. 시퀀스의 인덱스가 필요할 때
items = ["a", "b", "c", "d"]
for i in range(len(items)):
print(f"Index {i}: {items[i]}")
# 3. 큰 시퀀스에서 메모리 효율이 중요할 때
# 최소한의 메모리를 사용합니다
for i in range(1000000):
if i % 100000 == 0:
print(i)
# 다음 경우에 리스트를 사용:
# 1. 실제 값을 저장해야 할 때
squares = [1, 3, 5, 7, 10]
# 2. 시퀀스를 수정해야 할 때
numbers = list(range(5))
numbers[2] = 100 # 값 수정
numbers.append(200) # 값 추가
# 3. 서로 다른 연산으로 시퀀스를 여러 번 사용해야 할 때
data = list(range(10))
print(sum(data))
print(max(data))
print(sorted(data, reverse=True))range 객체는 Python의 효율성을 보여주는 완벽한 예입니다. 모든 요소를 저장하는 메모리 비용 없이 시퀀스의 장점을 모두 제공합니다.
15.7) 리스트, 튜플, range 사이 변환하기
Python에서는 서로 다른 시퀀스 타입 간 변환이 쉽습니다. 이런 변환을 이해하면 상황에 맞는 타입을 선택하고, 필요에 따라 데이터를 변환할 수 있습니다.
리스트로 변환하기
list() 함수는 어떤 시퀀스든 리스트로 변환합니다.
# 튜플 -> 리스트
student_tuple = ("Alice", 20, "CS")
student_list = list(student_tuple)
print(student_list) # Output: ['Alice', 20, 'CS']
print(type(student_list)) # Output: <class 'list'>
# 이제 수정할 수 있습니다
student_list[1] = 21
student_list.append(3.8)
print(student_list) # Output: ['Alice', 21, 'CS', 3.8]
# range -> 리스트
numbers = range(5)
numbers_list = list(numbers)
print(numbers_list) # Output: [0, 1, 2, 3, 4]
# 문자열 -> 리스트(각 문자가 요소가 됩니다)
text = "Python"
chars = list(text)
print(chars) # Output: ['P', 'y', 't', 'h', 'o', 'n']리스트로 변환하는 것은 시퀀스를 수정해야 하거나 append(), sort(), remove() 같은 리스트 전용 메서드를 사용해야 할 때 유용합니다.
튜플로 변환하기
tuple() 함수는 어떤 시퀀스든 튜플로 변환합니다.
# 리스트 -> 튜플
scores_list = [85, 90, 78, 92]
scores_tuple = tuple(scores_list)
print(scores_tuple) # Output: (85, 90, 78, 92)
print(type(scores_tuple)) # Output: <class 'tuple'>
# 이제 불변입니다
# scores_tuple[0] = 88 # TypeError: 'tuple' object does not support item assignment
# range -> 튜플
numbers = range(1, 6)
numbers_tuple = tuple(numbers)
print(numbers_tuple) # Output: (1, 2, 3, 4, 5)
# 문자열 -> 튜플
text = "Hi"
chars_tuple = tuple(text)
print(chars_tuple) # Output: ('H', 'i')튜플로 변환하는 것은 데이터가 수정되지 않도록 보호하고 싶거나, 시퀀스를 딕셔너리 키로 사용해야 할 때 유용합니다.
15.8) 문자열, 리스트, 튜플, range에 공통인 시퀀스 연산
Python의 시퀀스 타입인 문자열, 리스트, 튜플, range는 많은 공통 연산을 공유합니다. 이런 공통 연산을 이해하면 어떤 시퀀스 타입이든 효율적으로 다룰 수 있습니다.
길이, 최솟값, 최댓값
모든 시퀀스는 len(), min(), max() 함수를 지원합니다.
# 문자열
text = "Python"
print(len(text)) # Output: 6
print(min(text)) # Output: P (유니코드 값 기준 가장 작은 문자)
print(max(text)) # Output: y (유니코드 값 기준 가장 큰 문자)
# 리스트
numbers = [45, 12, 78, 23, 56]
print(len(numbers)) # Output: 5
print(min(numbers)) # Output: 12
print(max(numbers)) # Output: 78
# 튜플
scores = (85, 92, 78, 95, 88)
print(len(scores)) # Output: 5
print(min(scores)) # Output: 78
print(max(scores)) # Output: 95
# range
nums = range(10, 50, 5)
print(len(nums)) # Output: 8
print(min(nums)) # Output: 10
print(max(nums)) # Output: 45min()과 max()가 동작하려면 요소들끼리 비교 가능해야 합니다. 문자열과 숫자가 섞인 리스트의 최솟값은 구할 수 없습니다.
mixed = [1, "hello", 3]
# print(min(mixed)) # TypeError: '<' not supported between instances of 'str' and 'int'인덱싱과 음수 인덱싱
모든 시퀀스는 양수 및 음수 인덱스를 사용한 인덱싱을 지원합니다.
# 양수 인덱싱(0부터 시작)
text = "Python"
numbers = [10, 20, 30, 40, 50]
coords = (5, 10, 15)
values = range(0, 100, 10)
print(text[0]) # Output: P
print(numbers[2]) # Output: 30
print(coords[1]) # Output: 10
print(values[3]) # Output: 30
# 음수 인덱싱(끝에서부터)
print(text[-1]) # Output: n (마지막 문자)
print(numbers[-2]) # Output: 40 (끝에서 두 번째)
print(coords[-3]) # Output: 5 (끝에서 세 번째, 즉 첫 번째)
print(values[-1]) # Output: 90 (range의 마지막 값)음수 인덱스는 끝에서부터 셉니다. -1은 마지막 요소, -2는 끝에서 두 번째 요소처럼 동작합니다.
in과 not in으로 포함 여부 테스트하기
모든 시퀀스는 포함 여부 테스트를 지원합니다.
# 문자열 - 부분 문자열(substring) 확인
text = "Python Programming"
print("Python" in text) # Output: True
print("Java" in text) # Output: False
print("gram" in text) # Output: True (부분 문자열)
print("PYTHON" not in text) # Output: True (대소문자 구분)
# 리스트
fruits = ["apple", "banana", "cherry", "date"]
print("banana" in fruits) # Output: True
print("grape" in fruits) # Output: False
print("apple" not in fruits) # Output: False
# 튜플
coordinates = (10, 20, 30, 40)
print(20 in coordinates) # Output: True
print(25 in coordinates) # Output: False
print(50 not in coordinates) # Output: True
# range - 매우 효율적이며 반복이 필요 없습니다
numbers = range(0, 100, 2) # Even numbers 0 to 98
print(50 in numbers) # Output: True
print(51 in numbers) # Output: False (odd number)
print(100 in numbers) # Output: False (stop은 포함되지 않습니다)range의 경우 Python은 모든 요소를 확인하지 않고도 수학적으로 포함 여부를 판단할 수 있으므로, 매우 큰 range에서도 극도로 빠릅니다.
연결과 반복
문자열, 리스트, 튜플은 +로 연결(concatenation)하고 *로 반복(repetition)할 수 있습니다.
# +로 연결
text1 = "Hello"
text2 = " World"
print(text1 + text2) # Output: Hello World
list1 = [1, 2, 3]
list2 = [4, 5, 6]
print(list1 + list2) # Output: [1, 2, 3, 4, 5, 6]
tuple1 = (10, 20)
tuple2 = (30, 40)
print(tuple1 + tuple2) # Output: (10, 20, 30, 40)
# *로 반복
print("Ha" * 3) # Output: HaHaHa
print([0] * 5) # Output: [0, 0, 0, 0, 0]
print((1, 2) * 3) # Output: (1, 2, 1, 2, 1, 2)중요: range는 연결이나 반복을 지원하지 않습니다.
r1 = range(5)
r2 = range(5, 10)
# combined = r1 + r2 # TypeError: unsupported operand type(s) for +: 'range' and 'range'
# range를 결합하려면 먼저 리스트나 튜플로 변환해야 합니다
combined = list(r1) + list(r2)
print(combined) # Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]등장 횟수 세기
count() 메서드는 특정 요소가 몇 번 등장하는지 반환합니다.
# 문자열 - 부분 문자열 등장 횟수 세기
text = "Mississippi"
print(text.count("s")) # Output: 4
print(text.count("ss")) # Output: 2
print(text.count("i")) # Output: 4
# 리스트
numbers = [1, 2, 3, 2, 4, 2, 5]
print(numbers.count(2)) # Output: 3
print(numbers.count(6)) # Output: 0
# 튜플
grades = (85, 90, 85, 92, 85, 88)
print(grades.count(85)) # Output: 3
print(grades.count(95)) # Output: 0
# range에는 count() 메서드가 없지만, 먼저 변환할 수 있습니다
nums = range(0, 20, 2)
nums_list = list(nums)
print(nums_list.count(10)) # Output: 1요소의 인덱스 찾기
index() 메서드는 첫 번째로 등장하는 요소의 위치를 반환합니다.
# 문자열
text = "Python Programming"
print(text.index("P")) # Output: 0 (첫 번째 P)
print(text.index("Pro")) # Output: 7 (부분 문자열 위치)
# print(text.index("Java")) # ValueError: substring not found
# 리스트
fruits = ["apple", "banana", "cherry", "banana"]
print(fruits.index("banana")) # Output: 1 (첫 번째 등장)
print(fruits.index("cherry")) # Output: 2
# print(fruits.index("grape")) # ValueError: 'grape' is not in list
# 튜플
coordinates = (10, 20, 30, 20, 40)
print(coordinates.index(20)) # Output: 1 (첫 번째 등장)
print(coordinates.index(40)) # Output: 4
# range에는 index() 메서드가 없지만, 먼저 변환할 수 있습니다
nums = range(10, 50, 5)
nums_list = list(nums)
print(nums_list.index(25)) # Output: 3요소를 찾지 못하면 index()는 ValueError를 발생시킵니다. 이를 피하려면 먼저 in으로 확인하세요.
fruits = ["apple", "banana", "cherry"]
search_fruit = "grape"
if search_fruit in fruits:
position = fruits.index(search_fruit)
print(f"{search_fruit} found at position {position}")
else:
print(f"{search_fruit} not found")
# Output: grape not foundfor 반복문으로 순회하기
모든 시퀀스는 for 반복문으로 순회(iteration)할 수 있습니다.
# 문자열 - 문자 단위로 순회
for char in "Python":
print(char, end=" ")
print() # Output: P y t h o n
# 리스트
for fruit in ["apple", "banana", "cherry"]:
print(f"I like {fruit}")
# Output:
# I like apple
# I like banana
# I like cherry
# 튜플
for score in (85, 90, 78):
print(f"Score: {score}")
# Output:
# Score: 85
# Score: 90
# Score: 78
# range
for i in range(1, 6):
print(f"Count: {i}")
# Output:
# Count: 1
# Count: 2
# Count: 3
# Count: 4
# Count: 5비교 연산
시퀀스는 ==, !=, <, >, <=, >=로 비교할 수 있습니다.
# 동등 비교
print([1, 2, 3] == [1, 2, 3]) # Output: True
print((1, 2, 3) == (1, 2, 3)) # Output: True
print("abc" == "abc") # Output: True
# 부등 비교
print([1, 2, 3] != [1, 2, 4]) # Output: True
print((1, 2) != (1, 2)) # Output: False
# 사전식(lexicographic) 비교(요소별로 비교)
print([1, 2, 3] < [1, 2, 4]) # Output: True (3 < 4)
print([1, 2, 3] < [1, 3, 0]) # Output: True (2 < 3)
print("apple" < "banana") # Output: True (알파벳 순)
print((1, 2) < (1, 2, 3)) # Output: True (앞이 같으면 더 짧은 쪽이 더 작습니다)
# 서로 다른 타입 비교
print([1, 2, 3] == (1, 2, 3)) # Output: False (타입이 다릅니다)비교는 왼쪽에서 오른쪽으로 요소별로 진행됩니다. 처음으로 다른 요소가 나오는 지점의 비교 결과가 전체 결과를 결정합니다.
이런 공통 연산을 이해하면 어떤 시퀀스 타입에서도 동작하는 코드를 작성할 수 있어, 프로그램이 더 유연하고 재사용 가능해집니다.
15.9) 모든 시퀀스 타입에서의 고급 슬라이싱
슬라이싱(slicing)은 시퀀스를 다루는 Python의 가장 강력한 기능 중 하나입니다. 14장에서 기본 슬라이싱을 소개했지만, 모든 시퀀스 타입에서 동작하는 고급 슬라이싱 기법도 있습니다.
기본 슬라이싱 복습
슬라이싱은 sequence[start:stop:step] 문법으로 시퀀스의 일부를 추출합니다.
# 문자열 기본 슬라이싱
text = "Python Programming"
print(text[0:6]) # Output: Python
print(text[7:18]) # Output: Programming
print(text[7:]) # Output: Programming (인덱스 7부터 끝까지)
print(text[:6]) # Output: Python (처음부터 인덱스 6까지)
# 리스트 기본 슬라이싱
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[2:7]) # Output: [2, 3, 4, 5, 6]
print(numbers[:5]) # Output: [0, 1, 2, 3, 4]
print(numbers[5:]) # Output: [5, 6, 7, 8, 9]
# 튜플 기본 슬라이싱
coordinates = (10, 20, 30, 40, 50, 60)
print(coordinates[1:4]) # Output: (20, 30, 40)
print(coordinates[:3]) # Output: (10, 20, 30)
print(coordinates[3:]) # Output: (40, 50, 60)
# range 기본 슬라이싱
nums = range(0, 100, 10)
print(list(nums[2:5])) # Output: [20, 30, 40]기억하세요. start는 포함(inclusive), stop은 제외(exclusive)이며, 결과는 항상 원래 시퀀스와 같은 타입입니다.
슬라이싱에서 step 사용하기
선택적 세 번째 인자인 step은 몇 개의 요소를 건너뛸지 제어합니다.
# 두 번째 요소마다
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::2]) # Output: [0, 2, 4, 6, 8]
print(numbers[1::2]) # Output: [1, 3, 5, 7, 9]
# 세 번째 요소마다
text = "abcdefghijklmnop"
print(text[::3]) # Output: adgjmp
# start와 stop을 포함한 step
print(numbers[2:8:2]) # Output: [2, 4, 6]
print(text[1:10:2]) # Output: bdfhj음수 step: 시퀀스 뒤집기
음수 step은 슬라이싱 방향을 반대로 바꿉니다.
# 전체 시퀀스 뒤집기
text = "Python"
print(text[::-1]) # Output: nohtyP
numbers = [1, 2, 3, 4, 5]
print(numbers[::-1]) # Output: [5, 4, 3, 2, 1]
coordinates = (10, 20, 30, 40)
print(coordinates[::-1]) # Output: (40, 30, 20, 10)
# step을 두고 뒤집기
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(numbers[::-2]) # Output: [9, 7, 5, 3, 1] (두 개마다, 역방향)
# 일부만 뒤집기
text = "Python Programming"
print(text[7:18][::-1]) # Output: gnimmargorP ("Programming" 뒤집기)음수 step을 사용할 때 start와 stop은 다르게 동작합니다.
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 음수 step에서는 start가 stop보다 커야 합니다
print(numbers[7:2:-1]) # Output: [7, 6, 5, 4, 3] (7부터 3까지 내려감)
print(numbers[8:3:-2]) # Output: [8, 6, 4] (8부터 4까지 내려감, step -2)
# 음수 step에서 start/stop 생략
print(numbers[:5:-1]) # Output: [9, 8, 7, 6] (끝에서 6까지 내려감)
print(numbers[5::-1]) # Output: [5, 4, 3, 2, 1, 0] (5에서 시작까지 내려감)슬라이싱에서 음수 인덱스 사용하기
start와 stop 위치에 음수 인덱스를 사용할 수 있습니다.
text = "Python Programming"
# 마지막 11글자
print(text[-11:]) # Output: Programming
# 마지막 11글자를 제외한 나머지
print(text[:-11]) # Output: Python
# -15부터 -5까지
print(text[-15:-5]) # Output: hon Progra
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 마지막 5개 요소
print(numbers[-5:]) # Output: [5, 6, 7, 8, 9]
# 마지막 3개를 제외한 나머지
print(numbers[:-3]) # Output: [0, 1, 2, 3, 4, 5, 6]
# -7부터 -2까지
print(numbers[-7:-2]) # Output: [3, 4, 5, 6, 7]range에서의 슬라이싱
range를 슬라이싱하면 새로운 range 객체가 반환됩니다.
# range 슬라이싱
numbers = range(0, 100, 5) # 0, 5, 10, 15, ..., 95
print(numbers) # Output: range(0, 100, 5)
# 슬라이스는 새 range를 반환합니다
subset = numbers[5:10]
print(subset) # Output: range(25, 50, 5)
print(list(subset)) # Output: [25, 30, 35, 40, 45]
# step 포함
every_other = numbers[::2]
print(list(every_other)) # Output: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
# 음수 step
reversed_range = numbers[::-1]
print(list(reversed_range)) # Output: [95, 90, 85, ..., 5, 0]빈 슬라이스와 경계 사례
numbers = [1, 2, 3, 4, 5]
# 빈 슬라이스(양수 step에서 start >= stop)
print(numbers[3:3]) # Output: []
print(numbers[5:10]) # Output: [] (stop이 길이를 넘어감)
print(numbers[10:20]) # Output: [] (둘 다 길이를 넘어감)
# 시퀀스 범위를 넘어가는 슬라이스는 안전합니다
print(numbers[-100:100]) # Output: [1, 2, 3, 4, 5] (전체 시퀀스)
print(numbers[2:100]) # Output: [3, 4, 5] (2부터 끝까지)
# 음수 step에서 호환되지 않는 start/stop
print(numbers[2:7:-1]) # Output: [] (음수 step으로 앞으로 갈 수 없음)
# step은 0이 될 수 없습니다
# print(numbers[::0]) # ValueError: slice step cannot be zero복사를 위한 슬라이싱
슬라이싱은 새 시퀀스를 생성하므로, 복사하는 방법이 됩니다.
# 슬라이싱으로 복사
original = [1, 2, 3, 4, 5]
copy = original[:] # 처음부터 끝까지 슬라이싱
print(copy) # Output: [1, 2, 3, 4, 5]
# 복사본을 수정해도 원본에는 영향이 없습니다
copy[0] = 100
print(f"Original: {original}") # Output: Original: [1, 2, 3, 4, 5]
print(f"Copy: {copy}") # Output: Copy: [100, 2, 3, 4, 5]
# 튜플에서도 동작합니다(새 튜플 생성)
original_tuple = (1, 2, 3, 4, 5)
copy_tuple = original_tuple[:]
print(copy_tuple) # Output: (1, 2, 3, 4, 5)
# 문자열에서도
text = "Python"
text_copy = text[:]
print(text_copy) # Output: Python하지만 14장에서 봤듯이, 이것은 얕은 복사(shallow copy) 를 생성한다는 점을 기억하세요.
# 얕은 복사의 한계
original = [[1, 2], [3, 4]]
copy = original[:]
# 중첩 리스트를 수정하면 둘 다 영향을 받습니다
copy[0][0] = 100
print(f"Original: {original}") # Output: Original: [[100, 2], [3, 4]]
print(f"Copy: {copy}") # Output: Copy: [[100, 2], [3, 4]]튜플과 range는 Python 시퀀스 도구 상자에서 필수적인 도구입니다. 튜플은 실수로 수정되는 것을 막아주는 불변의 구조화된 데이터를 제공하고, 딕셔너리 키로 사용할 수 있게 해 줍니다. range는 숫자 시퀀스를 메모리 효율적으로 표현하므로, 반복문과 큰 시퀀스에 완벽합니다. 각 타입을 언제 사용해야 하는지, 그리고 서로 어떻게 변환하는지 이해하면 코드가 더 효율적이고, 더 안전하며, 의도가 더 명확해집니다.
모든 시퀀스 타입에 공통인 연산(인덱싱, 슬라이싱, 순회, 포함 여부 테스트)은 어떤 시퀀스든 직관적으로 다룰 수 있게 해 주는 일관된 인터페이스를 이룹니다. 고급 슬라이싱 기법은 시퀀스 데이터를 추출하고 다루는 강력하고 표현력 있는 방법을 제공합니다.
Python으로 프로그래밍을 계속하다 보면, 상황에 맞는 시퀀스 타입을 자연스럽게 선택하게 될 것입니다. 변하는 컬렉션에는 리스트, 고정 레코드에는 튜플, 숫자 시퀀스에는 range, 텍스트에는 문자열을 사용하게 됩니다. 이 장은 그런 선택을 자신 있게 할 수 있도록, 그리고 각 타입을 효과적으로 사용할 수 있도록 필요한 지식을 제공했습니다.