Skip to content
This page has been auto-translated and may contain errors.View in English

튜플과 집합

여러분은 이미 리스트를 알고 있습니다. Python에는 리스트로 해결할 수 없는 문제들을 풀어주는 두 가지 컬렉션 타입이 더 있습니다. 튜플은 절대 변하지 않는 고정된 값들의 묶음을 담습니다. 집합은 고유한 값만 담으며, 컬렉션이 아무리 커져도 멤버십을 즉시 확인할 수 있게 해줍니다.

Python의 컬렉션 도구 모음에는 네 가지 타입이 있습니다. 리스트와 딕셔너리는 대부분의 일반적인 경우를 처리합니다. 튜플과 집합은 특정한 경우를 해결합니다: 불변성이 자산이 되는 고정된 레코드, 그리고 O(1) 멤버십 테스트가 우선순위인 고유 값 컬렉션입니다.

listdict 외에도 Python은 tuple(불변 고정 길이 시퀀스)과 set(해시 테이블 기반의 고유한 해시 가능 객체들의 순서 없는 컬렉션)을 제공합니다. 각각은 선택하기 전에 알아둘 가치가 있는 고유한 메모리 모델, 해시 가능성 특성, 그리고 성능 프로필을 가집니다.

튜플

튜플은 생성한 후에는 변경할 수 없는 순서가 있는 값들의 묶음입니다. 괄호는 튜플을 정의하지만, 선택 사항입니다. 실제로 튜플을 만드는 것은 쉼표입니다. 단일 항목 튜플에는 끝에 쉼표가 필요합니다.

튜플은 불변 시퀀스입니다. 괄호가 아니라 쉼표가 튜플을 만듭니다. 불변성은 모든 요소가 해시 가능할 때 튜플을 해시 가능하게 만들어, 리스트로는 채울 수 없는 사용 사례를 열어줍니다: 딕셔너리 키, 집합 멤버, 그리고 고정 구조 레코드입니다.

tuple은 고정 크기 C 배열로 뒷받침되는 불변 시퀀스입니다. __hash__는 모든 요소가 해시 가능할 때 요소의 해시로부터 계산되어, 튜플을 딕셔너리 키와 집합 멤버로 유효하게 만듭니다. __getitem__은 정수와 슬라이스를 지원합니다; __setitem__은 구현되어 있지 않으므로, 모든 변경 시도는 TypeError를 발생시킵니다. 단일 항목 형태 (42,)는 끝의 쉼표가 필요합니다; 없으면 괄호는 단순히 그룹화일 뿐입니다.

python
point      = (10, 20)
rgb        = (255, 128, 0)
dimensions = (1920, 1080)
single     = (42,)            # 단일 항목 튜플에는 끝의 쉼표가 필요
also_tuple = 42, 99           # 괄호는 선택 사항; 쉼표가 튜플을 만듭니다

인덱스로 접근하는 것은 리스트와 정확히 같은 방식으로 작동합니다. 항목을 변경하려고 하면 TypeError가 발생합니다:

인덱싱, 슬라이싱, 음수 인덱스 모두 리스트와 동일하게 작동합니다. 인덱스를 통한 할당 시도는 모두 TypeError를 발생시킵니다; 이것은 제한이 아니라 의도된 것입니다.

정수와 slice 객체로 __getitem__을 사용하면 list와 동일한 클램핑 규칙을 따릅니다. __setitem__은 없습니다: 튜플 타입은 이를 등록하지 않으므로, TypeError는 파싱 시점이 아닌 런타임에 발생합니다.

python
point = (10, 20)
point[0]    # 10
point[1]    # 20
point[-1]   # 20

point[0] = 99    # TypeError: 'tuple' object does not support item assignment

튜플을 언제 사용할까

함께 속하며 변하지 않을 작은 관련 값들의 묶음이 있을 때 튜플을 사용하세요. 좌표 (x, y), 색상 (r, g, b), 이름-점수 쌍 ("민수", 87). 고정된 구조는 코드를 읽는 사람에게 이 그룹이 단일 단위로 취급된다는 신호를 보냅니다.

튜플은 고정된 구조를 전달합니다: 위치가 의미를 가지고 그룹이 하나의 단위로 취급되는 값들의 묶음입니다. 해시 가능성 덕분에 리스트로는 불가능한 딕셔너리 키로 유효합니다. 튜플이 신호하는 계약은: 이 값들은 함께 속하며 변경되어서는 안 된다는 것입니다.

튜플은 고정 아리티 레코드를 위한 관용적인 타입입니다. 해시 가능성 덕분에 Hashable이 필요한 어디서든 사용 가능합니다: 딕셔너리 키, 집합 멤버, functools.lru_cache 호출 시그니처. 의미론적 계약은 리스트와 다릅니다: 튜플은 위치가 의미를 가지는 이종 레코드를 나타내고, 리스트는 길이와 순서가 변할 수 있는 동종 시퀀스를 나타냅니다.

python
locations = {}
locations[(40, -74)] = "서울"   # 튜플을 딕셔너리 키로, 작동함
locations[[40, -74]] = "서울"   # 리스트를 딕셔너리 키로, TypeError

언패킹

언패킹은 튜플에서 값을 꺼내어 각각을 한 줄에 자기 이름에 할당합니다. 이름의 수는 값의 수와 일치해야 합니다. 남은 항목들을 리스트로 캡처하려면 *를 사용하세요.

언패킹은 모든 이터러블에서 작동합니다: 튜플, 리스트, 문자열. 스타가 붙은 타겟이 가변 길이 슬라이스를 잡지 않는 한, 타겟 이름의 수는 이터러블의 길이와 일치해야 합니다. 불일치는 ValueError를 발생시킵니다. 언패킹은 함수에서 여러 반환 값을 소비하는 관용적인 방식입니다.

언패킹은 오른쪽에서 __iter__를 호출하고 각 산출된 값을 해당 타겟 이름에 바인딩합니다. 스타가 붙은 타겟(*rest)은 남은 항목들을 list로 모읍니다. 불일치는 런타임에 ValueError를 발생시킵니다. 확장된 언패킹은 for 헤더 안에서도 작동합니다: for x, y in list_of_pairs는 각 반복 항목을 언패킹합니다.

python
point = (10, 20)
x, y  = point

print(x)   # 10
print(y)   # 20

first, *rest = [1, 2, 3, 4, 5]
# first = 1, rest = [2, 3, 4, 5]

head, *middle, tail = [1, 2, 3, 4, 5]
# head = 1, middle = [2, 3, 4], tail = 5

명명된 튜플

명명된 튜플은 각 위치에 이름이 있는 튜플입니다. point[0]이 x 좌표라는 것을 기억하는 대신, point.x라고 쓸 수 있습니다. 값들은 여전히 불변입니다; 단지 숫자 위치 대신 읽기 쉬운 속성 이름을 얻을 뿐입니다.

namedtuple은 튜플처럼 정확히 동작하지만 명명된 속성 접근을 추가하는 클래스를 생성합니다. 전체 클래스보다 가볍고, 불변이며, 자기 문서화됩니다. 일반 튜플의 위치 접근이 이해되기 위해 주석이 필요한 경우에 사용하세요.

collections.namedtuple은 클래스 팩토리입니다: 컴파일된 명명된 속성 접근을 가진 tuple 서브클래스를 런타임에 생성합니다. 생성된 클래스에는 _asdict(), _replace(), _fields가 포함됩니다. 메모리 사용량은 일반 튜플과 동일합니다. 더 많은 제어(기본값, 타입 어노테이션, 선택적 가변성)를 위해서는 dataclasses.dataclass가 현대적 대안입니다; 타입 어노테이션된 튜플의 경우 typing.NamedTuple이 관용적입니다.

명명된 튜플 임포트

namedtuple은 Python 표준 라이브러리에 있지만 임포트가 필요합니다. from collections import namedtuple 줄은 이 코스에서 첫 번째 임포트입니다. 임포트는 모듈 챕터에서 완전히 다룹니다.

python
from collections import namedtuple

Point  = namedtuple("Point",  ["x", "y"])
Player = namedtuple("Player", ["name", "score", "level"])

p = Point(10, 20)
p.x    # 10
p.y    # 20

minsu = Player("민수", 87, 5)
minsu.name    # "민수"
minsu.score   # 87

집합

집합은 보장된 순서가 없는 고유 값들의 컬렉션입니다. 같은 값을 두 번 추가해도 아무 일도 일어나지 않습니다: 집합은 각 항목의 사본을 하나만 유지합니다. 항목이 있는 집합에는 중괄호를 사용하고, 빈 집합을 만들려면 set()을 사용하세요.

set은 중복을 자동으로 거부하는 순서 없는 컬렉션입니다. 멤버십 테스트는 크기에 관계없이 O(1)이며, 이는 큰 컬렉션에서 값이 존재하는지 확인해야 할 때마다 올바른 도구가 됩니다. 참고: {}는 빈 집합이 아니라 빈 딕셔너리를 만듭니다; 빈 집합에는 set()을 사용하세요.

set은 해시 테이블 기반의 고유한 해시 가능 객체들의 컬렉션입니다. 멤버십 테스트, 삽입, 삭제는 모두 평균 O(1)입니다. 반복 순서는 내부 해시 위치를 반영하며 실행 간에 안정적이지 않습니다. 해시 가능 객체만 멤버가 될 수 있습니다: int, str, tuple은 가능; list, dict, set은 불가능. {}는 집합이 아니라 빈 딕셔너리 리터럴로 파싱됩니다.

python
tags     = {"python", "beginner", "tutorial"}
numbers  = {1, 2, 3, 4, 5}
empty    = set()    # {}가 아님 (그것은 빈 딕셔너리)

같은 값을 두 번 추가해도 집합은 변경되지 않습니다:

python
tags.add("python")   # tags는 변경되지 않음, "python"이 이미 있음

집합을 언제 사용할까

집합은 세 가지에 적합한 도구입니다: 리스트에서 중복 제거하기, 큰 컬렉션에서 무언가가 있는지 빠르게 확인하기, 그리고 두 그룹을 비교하여 공유하거나 다른 점을 찾기.

세 가지 뚜렷한 사용 사례가 집합 사용을 이끕니다: 중복 제거(삽입 시 자동), O(1) 멤버십 테스트(list의 O(n) 대비), 그리고 집합 대수(|, &, -, ^). 컬렉션이 크고 멤버십을 자주 확인할 때, 성능 차이는 상당합니다.

세 가지 정형화된 집합 사용 사례는 해시 테이블 속성과 직접적으로 매핑됩니다: 고유성(삽입 시 중복 거부), 평균 O(1) __contains__(해시 조회), 그리고 집합 대수(던더 메서드를 호출하는 비트 스타일 연산자). O(1) 멤버십 테스트는 실용적으로 가장 중요합니다: 10,000개 항목의 집합에서 in은 10개 집합에서만큼 빠릅니다.

python
# 리스트에서 중복 제거
raw    = ["cat", "dog", "cat", "bird", "dog", "cat"]
unique = list(set(raw))   # ["cat", "dog", "bird"] (순서는 보장되지 않음)
python
# 빠른 멤버십 확인
valid_codes = {"USD", "EUR", "GBP", "JPY"}
code        = "EUR"

if code in valid_codes:    # 수천 개의 코드가 있어도 즉시 조회
    print("Valid")

집합 연산

집합은 수학에서 배운 것과 동일한 연산을 지원합니다: 합집합(두 집합 중 하나에 있는 모든 것), 교집합(두 집합이 공유하는 것만), 그리고 차집합(한쪽에 있고 다른 쪽에는 없는 것). Python은 이를 위해 연산자 기호를 사용하며, 각각은 메서드 등가물을 가집니다.

Python의 집합 연산자는 수학적 표기법을 반영합니다: 합집합 |, 교집합 &, 차집합 -, 대칭차 ^. 각 연산자는 메서드 형태(.union(), .intersection() 등)를 가지며, 이는 집합뿐만 아니라 모든 이터러블을 받습니다.

집합 연산자는 던더 메서드를 호출합니다: |__or__, &__and__, -__sub__, ^__xor__를 호출합니다. 연산자는 두 피연산자가 모두 집합이어야 하며, 그렇지 않으면 TypeError를 발생시킵니다. 메서드 형태는 모든 이터러블을 받아 내부적으로 변환합니다. 인플레이스 형태(|=, &=, -=, ^=)는 왼쪽 피연산자를 변경하며, .update(), .intersection_update() 등에 해당합니다.

python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b    # {1, 2, 3, 4, 5, 6}   (합집합: 어느 한쪽에 있는 모든 것)
a & b    # {3, 4}               (교집합: 둘 다에 있는 것만)
a - b    # {1, 2}               (차집합: a에 있고 b에는 없는 것)
b - a    # {5, 6}               (반대 방향 차집합)
a ^ b    # {1, 2, 5, 6}        (대칭차: 둘 중 하나에만 있는 것)

이들에는 메서드 형태도 있습니다: .union(), .intersection(), .difference(), .symmetric_difference().

집합 수정하기

집합은 가변입니다. .add()는 항목 하나를 추가합니다. .update()는 모든 리스트나 다른 이터러블에서 여러 개를 한 번에 추가합니다. .remove()는 항목을 삭제하지만 없으면 오류를 발생시킵니다. .discard()는 항목이 존재하면 조용히 삭제하고 없으면 아무것도 하지 않습니다.

.add()는 평균 O(1)입니다. .update()는 모든 이터러블을 받으며 루프에서 .add()를 호출하는 것과 동일합니다. .remove()는 미스 시 KeyError를 발생시켜 dict.__delitem__을 반영합니다. .discard()는 존재가 불확실할 때 안전한 선택입니다. .pop()은 임의의 요소를 제거합니다 — 집합은 순서가 없으므로 "마지막"이 아닙니다.

.add(x)x를 해시하고 테이블에 삽입합니다: 평균 O(1). .update(iterable)|=와 동일합니다. .remove()는 미스 시 KeyError를 발생시킵니다. .discard()는 먼저 해시 조회를 하고 미스 시 제거를 건너뜁니다. .pop()은 삽입 순서가 아니라 내부 해시 테이블 상태에 의해 결정된 임의의 요소를 제거합니다.

python
tags = {"python", "beginner"}

tags.add("tutorial")          # 항목 하나 추가
tags.update(["web", "api"])   # 모든 이터러블에서 여러 항목 추가
tags.remove("beginner")       # 제거, 없으면 KeyError 발생
tags.discard("missing")       # 제거, 없어도 오류 없음
tags.pop()                    # 임의의 항목을 제거하고 반환
tags.clear()                  # 모두 제거

항목이 존재하는지 확실하지 않을 때는 .discard()를 사용하세요.

동결 집합

동결 집합은 생성 후에 수정할 수 없는 집합입니다. 사용하는 주된 이유는: 동결 집합은 해시 가능하므로 딕셔너리 키로 사용하거나 다른 집합 안에 저장할 수 있습니다.

frozensetset의 불변 대응물입니다. 모든 읽기 연산과 집합 대수를 지원하지만 변경은 지원하지 않습니다. 불변성 덕분에 해시 가능하며, 딕셔너리 키나 다른 집합 내부의 멤버로 유효합니다.

frozenset은 요소 해시의 정렬된 축소로부터 계산된 __hash__를 구현하여, 안정적인 해시 값을 가집니다. 새 컬렉션을 반환하는 모든 집합 대수 연산자와 메서드가 지원됩니다; 변경 메서드(add, remove 등)는 정의되지 않습니다. frozenset은 런타임에 변경되어서는 안 되며 딕셔너리 키로 사용해야 할 수 있는 상수 조회 테이블에 적합한 타입입니다.

python
valid_statuses = frozenset({"active", "paused", "deleted"})
valid_statuses.add("archived")    # AttributeError, frozenset은 불변

올바른 컬렉션 선택하기

네 가지 타입, 각각 명확한 역할이 있습니다. 데이터로 무엇을 해야 하는지 묻고, 올바른 선택이 보통 명확해집니다.

컬렉션 타입 간의 선택은 어떤 연산이 중요한지와 데이터에 어떤 제약이 있는지에 관한 것입니다: 가변성, 순서, 중복 처리, 그리고 조회 전략.

컬렉션 선택은 성능과 의미론적 결정입니다. dictset은 해싱을 통해 평균 O(1) 조회를 제공합니다. listtuple은 O(1) 인덱스 접근을 제공하지만 멤버십 테스트는 O(n)입니다. tuple의 불변성은 해시 가능성을 가져옵니다. frozensettuple은 표준 라이브러리의 두 가지 해시 가능 복합 타입입니다.

listtuplesetdict
순서있음있음없음있음 (삽입 순서)
가변가능불가능가능가능
중복허용허용불가불가 (키)
접근 방법인덱스인덱스해당 없음
사용 시점순서가 있고 변경 가능한 시퀀스고정 레코드고유 값, 빠른 멤버십키-값 조회

빠른 결정 규칙:

  • 이름으로 무언가를 조회해야 하나요? → dict
  • 수정할 순서가 있는 컬렉션이 필요한가요? → list
  • 관련 값들의 고정된 묶음이 있나요? → tuple
  • 고유 값이나 빠른 멤버십 테스트가 필요한가요? → set

실전에서

튜플을 사용하여 고정된 레코드를 저장하고 집합을 사용하여 고유한 값을 추적하기:

python
home   = (37.5665, 126.9780)   # 위도, 경도
office = (37.5172, 127.0473)

home_lat, home_lon = home
print(f"집: {home_lat}, {home_lon}")

# 집합으로 고유 방문자 추적
visitors = set()
visitors.add("민수")
visitors.add("지영")
visitors.add("민수")    # 이미 집합에 있음, 조용히 무시됨
visitors.add("수진")

print(f"고유 방문자: {len(visitors)}")
print(f"민수 방문: {'민수' in visitors}")
print(f"준호 방문: {'준호' in visitors}")

집합을 사용하여 이미 처리된 것을 추적하고 남은 작업을 계산하기:

python
already_processed = {"report_jan.csv", "report_feb.csv"}
all_files         = {"report_jan.csv", "report_feb.csv", "report_mar.csv", "report_apr.csv"}

to_process = all_files - already_processed
print(f"처리할 파일: {sorted(to_process)}")

for filename in sorted(to_process):
    print(f"{filename} 처리 중...")
    already_processed.add(filename)

print(f"완료. 총 처리됨: {len(already_processed)}")

상수 조회 테이블에 frozenset을 사용하고 집합 대수를 통한 O(1) 멤버십 테스트 시연하기:

python
ALLOWED_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE"})
SAFE_METHODS    = frozenset({"GET", "HEAD", "OPTIONS"})

# frozenset에 대한 집합 대수는 일반 집합을 반환합니다
unsafe_allowed = ALLOWED_METHODS - SAFE_METHODS
print(f"안전하지 않은 허용 메서드: {unsafe_allowed}")

# frozenset은 해시 가능하므로 집합에 저장 가능 (일반 집합은 불가능)
method_groups = {
    frozenset({"GET", "HEAD", "OPTIONS"}),
    frozenset({"POST", "PUT", "PATCH"}),
    frozenset({"DELETE"}),
}
print(f"메서드 그룹: {len(method_groups)}")

method = "POST"
print(f"허용됨: {method in ALLOWED_METHODS}")
print(f"안전함: {method in SAFE_METHODS}")

frozenset은 O(1) 조회를 가지며 해시 가능 타입이 필요한 어디에나 저장될 수 있습니다. 두 frozenset 객체에 대한 집합 대수는 일반 set을 반환합니다; 불변으로 유지하려면 결과를 frozenset()으로 감싸세요.