Python & AI Tutorials Logo
Python 프로그래밍

15. 튜플과 range: 단순한 불변 시퀀스

14장에서는 Python의 다재다능한 가변 시퀀스 타입인 리스트(list)를 살펴보았습니다. 이제는 또 다른 중요한 시퀀스 타입 두 가지인 튜플(tuple)range(range) 를 살펴보겠습니다. 리스트는 시간이 지나며 변하는 컬렉션을 저장하는 데 뛰어나지만, 튜플은 데이터를 수정으로부터 보호하는 불변(immutable) 시퀀스를 제공하고, range는 숫자 시퀀스를 메모리 효율적으로 표현하는 방법을 제공합니다.

각 시퀀스 타입을 언제 사용해야 하는지 이해하면 프로그램이 더 효율적이고, 더 안전하며, 의도가 더 명확해집니다. 이 장을 마치면 튜플과 range를 효과적으로 다루는 방법을 알게 되고, 모든 Python 시퀀스 타입 전반에 걸쳐 동작하는 공통 연산도 이해하게 됩니다.

15.1) 튜플 만들기와 사용하기(쉼표의 중요성)

튜플(tuple) 은 항목의 순서가 있는, 불변(immutable) 시퀀스입니다. 리스트처럼 튜플도 어떤 타입의 데이터든 담을 수 있고 요소의 순서를 유지합니다. 하지만 리스트와 달리, 튜플을 한 번 만들면 그 내용을 수정할 수 없습니다.

괄호로 튜플 만들기

튜플을 만드는 가장 일반적인 방법은 쉼표로 구분된 값을 괄호로 감싸는 것입니다.

python
# 학생 시험 점수 튜플
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)에서 그룹화하기 위해 사용됩니다.

python
# 아래는 모두 같은 튜플을 만듭니다
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에게 그룹화된 표현식이 아니라 튜플을 원한다는 뜻을 알려줍니다.

괄호가 필요한 경우

쉼표가 튜플을 만들지만, 모호함을 피하기 위해 특정 상황에서는 괄호가 필요해집니다.

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개인 튜플에서 끝의 쉼표가 필요하다는 규칙은 초보자를 종종 당황하게 합니다.

python
# 흔한 실수: 쉼표를 빼먹기
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) 연산을 지원합니다.

python
# 학생 정보 튜플
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장에서 리스트로 배운 모든 인덱싱과 슬라이싱 기법은 튜플에서도 동일하게 동작합니다. 핵심 차이는 튜플은 생성 후에 수정할 수 없다는 점입니다.

여러 항목

단일 항목

빈 튜플

튜플 생성

문법 선택

(item1, item2, ...)

(item,) - 쉼표 필요

()

15.2) 튜플 패킹과 언패킹

튜플의 가장 강력하고 우아한 기능 중 하나는 여러 값을 함께 패킹(packing)하고, 이를 별도의 변수로 언패킹(unpacking)할 수 있다는 점입니다. 이 기능은 Python 코드를 놀라울 정도로 간결하고 읽기 쉽게 만들어 줍니다.

튜플 패킹

튜플 패킹(tuple packing) 은 여러 값을 쉼표로 구분해 함께 놓음으로써 튜플을 생성할 때 발생합니다.

python
# 값을 튜플로 패킹
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) 은 반대 과정으로, 튜플에서 값을 꺼내어 별도의 변수에 대입합니다.

python
# 기본 언패킹
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를 발생시킵니다.

python
# 이것은 에러를 발생시킵니다
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)

튜플 언패킹으로 변수 값 교환하기

튜플 언패킹을 사용하면 임시 변수를 쓰지 않고도 변수 값을 우아하게 교환할 수 있습니다.

python
# 임시 변수를 사용한 전통적인 교환
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) 을 제공합니다.

python
# "나머지" 변수를 사용한 언패킹
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) 로 요소들을 담는다는 점에 주목하세요. 담을 요소가 없다면 별 변수는 빈 리스트가 됩니다.

python
# 담을 것이 없을 때
a, b, *rest = (10, 20)
print(rest)  # Output: []
 
# 언패킹에서 별은 하나만 허용됩니다
# first, *middle, *end = (1, 2, 3, 4)  # SyntaxError: multiple starred expressions

밑줄로 값 무시하기

때로는 튜플에서 일부 값만 필요합니다. 관례적으로 Python 프로그래머들은 무시하려는 값에 대해 변수 이름으로 밑줄 _를 사용합니다.

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

밑줄은 단지 일반 변수 이름이지만, 이를 사용하면 다른 프로그래머(그리고 자기 자신)에게 해당 값들을 의도적으로 무시하고 있다는 신호가 됩니다.

패킹과 언패킹의 실용 예시

python
# 계산 결과로 여러 값을 반환
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) 입니다. 한 번 만들면 튜플의 내용은 바꿀 수 없습니다. 요소를 추가하거나 제거하거나 수정할 수 없습니다. 이런 불변성은 제한처럼 보일 수 있지만, 중요한 이점을 제공합니다.

실전에서 불변성이 의미하는 것

python
# 튜플 만들기
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 deletion

Python이 튜플은 item assignment를 지원하지 않는다고 말할 때, 그것은 튜플의 어떤 위치에 저장된 값도 바꿀 수 없다는 뜻입니다. 튜플의 구조는 생성 시점에 고정됩니다.

가변 리스트와 불변 튜플 비교

python
# 리스트는 가변입니다 - 변경할 수 있습니다
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. 데이터 무결성과 안전성

함수에 튜플을 전달하면, 그 함수가 실수로 데이터를 수정할 수 없다는 것을 알 수 있습니다.

python
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) 해야 합니다. 즉, 절대 변하지 않는 해시 값을 가져야 합니다. 튜플 같은 불변 객체는 딕셔너리 키가 될 수 있지만, 리스트 같은 가변 객체는 될 수 없습니다.

python
# 튜플은 딕셔너리 키가 될 수 있습니다
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. 의도를 표현하기

리스트 대신 튜플을 사용하면 이 데이터는 변하지 않아야 한다는 뜻을 다른 프로그래머(그리고 자기 자신)에게 전달합니다.

python
# 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()가 객체가 사용하는 메모리 크기를 알려준다는 것만 알아두세요.

python
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 seconds

15.4) 불변성의 함정: 튜플에 가변 항목이 들어 있는 경우

튜플 자체는 불변이지만, 리스트나 딕셔너리 같은 가변 객체를 포함할 수 있습니다. 이는 미묘하지만 중요한 차이를 만들어냅니다. 튜플의 구조는 고정이지만, 그 안에 들어 있는 가변 객체의 내용은 여전히 바뀔 수 있습니다.

차이를 이해하기

python
# 리스트를 포함하는 튜플
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에, 하나는 리스트 객체에 대한 참조입니다. 튜플의 구조(즉, 어떤 객체를 참조하는지)는 바뀔 수 없습니다. 하지만 리스트 객체 자체는 가변이므로 그 내용은 바뀔 수 있습니다.

차이를 시각화하기

python
# 튜플 구조는 고정입니다
data = ("Python", [1, 2, 3])
 
# 이것은 튜플이 참조하는 대상을 바꾸려는 시도입니다 - 허용되지 않습니다
# data[1] = [4, 5, 6]  # TypeError
 
# 이것은 튜플이 참조하는 리스트를 수정하는 것입니다 - 허용됩니다
data[1].append(4)
print(data)  # Output: ('Python', [1, 2, 3, 4])
 
# 튜플은 여전히 같은 리스트 객체를 참조합니다
# 리스트의 내용만 바뀌었고, 튜플이 가리키는 리스트 자체는 바뀌지 않았습니다

이렇게 생각해 보세요. 튜플은 상자들이 한 줄로 늘어선 것과 같고, 각 상자에는 어떤 객체를 가리키는 참조가 들어 있습니다. 상자 자체는 잠겨 있어(불변) 위치를 바꿀 수 없지만, 상자 안에 가변 객체를 가리키는 참조가 있다면 그 객체는 여전히 바뀔 수 있습니다.

딕셔너리를 포함하는 튜플

같은 원칙이 튜플 내부의 딕셔너리에도 적용됩니다.

python
# 딕셔너리를 포함하는 튜플
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)할 때만 딕셔너리 키로 사용할 수 있습니다. 튜플 자체는 불변이지만, 가변 객체(리스트 등)를 포함한 튜플은 아예 해시가 불가능하므로 딕셔너리 키로 사용할 수 없습니다.

python
# 이것은 동작하지만 위험합니다
tuple_with_list = ("key", [1, 2, 3])
# data = {tuple_with_list: "value"}  # TypeError: unhashable type: 'list'

딕셔너리 키로는 완전히 불변인 객체(문자열, 숫자, frozenset, 다른 튜플 등)만 포함하는 튜플만 사용하세요.

완전히 불변인 튜플 만들기

완전히 불변인 튜플이 필요하다면, 내용물도 모두 불변이어야 합니다.

python
# 완전히 불변인 튜플 - 불변 타입만 포함
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

가변 내용이 의도적인 경우

때로는 가변 내용을 가진 튜플이 실제로 필요할 수도 있습니다. 예를 들어, 레코드 구조는 고정이지만 특정 필드는 바뀌어야 할 때입니다.

python
# 신원은 고정이고 성적은 변하는 학생 레코드
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) 리스트 대신 튜플을 사용해야 할 때

튜플과 리스트 중 무엇을 선택할지는 중요한 설계 결정입니다. 둘 다 시퀀스이지만, 서로 다른 목적을 갖고 서로 다른 의도를 전달합니다.

고정된, 이질적인 데이터에는 튜플을 사용하세요

튜플은 보통 서로 다른 타입을 포함하는, 하나의 논리적 엔티티를 표현하는 고정된 개수의 항목에 가장 잘 맞습니다.

python
# 학생 레코드: 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)

각 튜플은 각 요소의 위치가 특정 의미를 갖는 완전한 "레코드"를 나타냅니다. 첫 번째 요소는 항상 이름, 두 번째는 항상 나이, 이런 식입니다.

동질적인 컬렉션에는 리스트를 사용하세요

리스트는 비슷한 항목들의 개수가 가변적이고, 추가/삭제/재정렬할 수도 있는 경우에 가장 적합합니다.

python
# 장보기 목록 - 같은 타입의 항목(문자열)
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"])  # 여러 사용자 추가

리스트는 항목 개수가 바뀔 수 있고, 각 항목이 같은 역할을 하는 컬렉션을 위한 것입니다.

함수 반환값에는 튜플을 사용하세요

함수가 관련된 여러 값을 반환할 때, 튜플은 자연스러운 선택입니다.

python
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)가 필요할 때 튜플은 필수입니다.

python
# 과목과 학기별 학생 성적
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

리스트는 가변이라 딕셔너리 키가 될 수 없지만, 튜플은 될 수 있습니다.

불변 설정에는 튜플을 사용하세요

절대 바뀌면 안 되는 설정 데이터가 있을 때, 튜플은 그런 의도를 표현해 줍니다.

python
# 변하지 않아야 하는 애플리케이션 설정
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"
)

결정 가이드

python
# 다음 경우에 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 객체를 만듭니다.

python
# 인자 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 객체 자체를 출력하면 모든 값을 표시하지 않습니다.

python
r = range(5)
print(r)  # Output: range(0, 5)
print(type(r))  # Output: <class 'range'>

range 객체가 동작하는 방식

range 객체는 모든 값을 메모리에 저장하지 않습니다. 대신 필요할 때 각 값을 계산합니다.

python
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)과 함께 가장 많이 사용됩니다.

python
# 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 1

range 객체의 인덱싱과 슬라이싱

range 객체는 다른 시퀀스처럼 인덱싱과 슬라이싱을 지원합니다.

python
# 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에 포함되는지 확인할 수 있습니다.

python
# 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

python
# 빈 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와 리스트를 언제 사용할까

python
# 다음 경우에 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() 함수는 어떤 시퀀스든 리스트로 변환합니다.

python
# 튜플 -> 리스트
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() 함수는 어떤 시퀀스든 튜플로 변환합니다.

python
# 리스트 -> 튜플
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')

튜플로 변환하는 것은 데이터가 수정되지 않도록 보호하고 싶거나, 시퀀스를 딕셔너리 키로 사용해야 할 때 유용합니다.

list

tuple

tuple

list

Range

List

Tuple

15.8) 문자열, 리스트, 튜플, range에 공통인 시퀀스 연산

Python의 시퀀스 타입인 문자열, 리스트, 튜플, range는 많은 공통 연산을 공유합니다. 이런 공통 연산을 이해하면 어떤 시퀀스 타입이든 효율적으로 다룰 수 있습니다.

길이, 최솟값, 최댓값

모든 시퀀스는 len(), min(), max() 함수를 지원합니다.

python
# 문자열
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: 45

min()max()가 동작하려면 요소들끼리 비교 가능해야 합니다. 문자열과 숫자가 섞인 리스트의 최솟값은 구할 수 없습니다.

python
mixed = [1, "hello", 3]
# print(min(mixed))  # TypeError: '<' not supported between instances of 'str' and 'int'

인덱싱과 음수 인덱싱

모든 시퀀스는 양수 및 음수 인덱스를 사용한 인덱싱을 지원합니다.

python
# 양수 인덱싱(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으로 포함 여부 테스트하기

모든 시퀀스는 포함 여부 테스트를 지원합니다.

python
# 문자열 - 부분 문자열(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)할 수 있습니다.

python
# +로 연결
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는 연결이나 반복을 지원하지 않습니다.

python
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() 메서드는 특정 요소가 몇 번 등장하는지 반환합니다.

python
# 문자열 - 부분 문자열 등장 횟수 세기
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() 메서드는 첫 번째로 등장하는 요소의 위치를 반환합니다.

python
# 문자열
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으로 확인하세요.

python
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 found

for 반복문으로 순회하기

모든 시퀀스는 for 반복문으로 순회(iteration)할 수 있습니다.

python
# 문자열 - 문자 단위로 순회
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

비교 연산

시퀀스는 ==, !=, <, >, <=, >=로 비교할 수 있습니다.

python
# 동등 비교
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] 문법으로 시퀀스의 일부를 추출합니다.

python
# 문자열 기본 슬라이싱
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은 몇 개의 요소를 건너뛸지 제어합니다.

python
# 두 번째 요소마다
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은 슬라이싱 방향을 반대로 바꿉니다.

python
# 전체 시퀀스 뒤집기
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은 다르게 동작합니다.

python
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 위치에 음수 인덱스를 사용할 수 있습니다.

python
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 객체가 반환됩니다.

python
# 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]

빈 슬라이스와 경계 사례

python
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

복사를 위한 슬라이싱

슬라이싱은 새 시퀀스를 생성하므로, 복사하는 방법이 됩니다.

python
# 슬라이싱으로 복사
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) 를 생성한다는 점을 기억하세요.

python
# 얕은 복사의 한계
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, 텍스트에는 문자열을 사용하게 됩니다. 이 장은 그런 선택을 자신 있게 할 수 있도록, 그리고 각 타입을 효과적으로 사용할 수 있도록 필요한 지식을 제공했습니다.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai