튜플과 세트
리스트는 이미 배웠습니다. 파이썬에는 리스트로 해결할 수 없는 문제를 위한 컬렉션 타입이 두 가지 더 있습니다. 튜플은 절대 바뀌지 않는 고정된 값들의 묶음을 담습니다. 세트는 오직 고유한 값만 담고, 컬렉션이 얼마나 커지든 특정 값이 들어있는지 즉시 확인할 수 있게 해줍니다.
튜플
튜플은 생성 후 바꿀 수 없는 순서 있는 값들의 묶음입니다. 괄호가 튜플을 정의하지만 괄호는 있어도 없어도 됩니다. 실제로 튜플을 만드는 것은 쉼표입니다. 항목이 하나뿐인 튜플은 뒤에 쉼표를 붙여야 합니다.
point = (10, 20)
rgb = (255, 128, 0)
dimensions = (1920, 1080)
single = (42,) # 항목이 하나뿐인 튜플은 뒤에 쉼표가 필요합니다
also_tuple = 42, 99 # 괄호는 선택 사항이며, 쉼표가 튜플을 만듭니다인덱스로 접근하는 방식은 리스트와 완전히 같습니다. 항목을 바꾸려 하면 TypeError가 발생합니다:
point = (10, 20)
point[0] # 10
point[1] # 20
point[-1] # 20
point[0] = 99 # TypeError: 'tuple' object does not support item assignment(42,)가 그 외로운 뒤쪽 쉼표를 필요로 하는 것입니다. 항목을 다시 대입해 보면 TypeError가 나오는데, 그 바꿀 수 없다는 안정감이야말로 튜플을 선택하는 이유입니다. 튜플을 언제 사용할까
서로 관련된 작은 값들의 묶음이 있고 그 값들이 바뀌지 않을 때 튜플을 사용하십시오. 좌표 (x, y), 색상 (r, g, b), 이름과 점수 쌍 ("민준", 87) 같은 경우입니다. 이렇게 고정된 구조는 코드를 읽는 누구에게나 이 묶음이 하나의 단위로 취급된다는 것을 알려줍니다.
(r, g, b) 색상, 이름과 점수 쌍 같은 것들입니다. 튜플이 해시 가능하기 때문에 딕셔너리 키로도 쓸 수 있습니다. 리스트는 그렇게 할 수 없는데, 처음 시도해 보는 사람들이 여기서 걸려 넘어집니다. locations = {}
locations[(40, -74)] = "뉴욕" # 튜플을 딕셔너리 키로 사용, 동작함
locations[[40, -74]] = "뉴욕" # 리스트를 딕셔너리 키로 사용, TypeError언패킹
언패킹은 튜플에서 값들을 꺼내 한 줄로 각각 자신만의 이름에 대입합니다. 이름의 개수는 값의 개수와 일치해야 합니다. 나머지 항목들을 리스트로 모으고 싶다면 *를 사용하십시오.
x, y = point처럼요. 이름의 개수는 값의 개수와 맞아야 하지만, *rest를 추가하면 나머지를 다 모아줍니다. point[0], point[1]을 여기저기 쓰던 걸 멈춘 날, 저에게는 이게 확 이해가 됐습니다. 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라고 쓸 수 있습니다. 값은 여전히 불변이고, 숫자 위치 대신 읽기 쉬운 속성 이름을 얻는 셈입니다.
point[0]이 x라는 걸 기억하는 대신 point.x라고 쓸 수 있습니다. 다른 모든 면에서는 여전히 완전히 불변이고 평범한 튜플처럼 동작합니다. 이름이 있으면 나중에 이 코드를 읽는 당신 자신이 고마워할 겁니다. namedtuple은 표준 라이브러리에 있으므로 먼저 임포트해야 합니다: from collections import namedtuple. 임포트는 모듈 장에서 자세히 다룹니다.
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
alice = Player("민지", 87, 5)
alice.name # "민지"
alice.score # 87세트
세트는 순서가 보장되지 않는 고유한 값들의 컬렉션입니다. 같은 값을 두 번 추가해도 아무 일도 일어나지 않습니다. 세트는 각 항목의 복사본을 하나만 유지합니다. 항목이 있는 세트를 만들려면 중괄호를 쓰고, 빈 세트를 만들려면 set()을 쓰십시오.
set()을 쓰십시오. {}는 사실 빈 딕셔너리이기 때문입니다. 이 마지막 부분에서 거의 모두가 걸려 넘어집니다. tags = {"python", "beginner", "tutorial"}
numbers = {1, 2, 3, 4, 5}
empty = set() # {}가 아님 (그건 빈 딕셔너리입니다)같은 값을 두 번 추가해도 세트는 바뀌지 않습니다:
tags.add("python") # tags는 그대로입니다. "python"은 이미 들어있습니다세트를 언제 사용할까
세트가 딱 맞는 세 가지 상황이 있습니다. 리스트에서 중복을 제거할 때, 큰 컬렉션에 무언가가 있는지 빠르게 확인할 때, 그리고 두 그룹을 비교해서 공통점이나 차이점을 찾을 때입니다.
# 리스트에서 중복 제거
raw = ["cat", "dog", "cat", "bird", "dog", "cat"]
unique = list(set(raw)) # ["cat", "dog", "bird"] (순서는 보장되지 않음)# 빠른 멤버십 확인
valid_codes = {"USD", "EUR", "GBP", "JPY"}
code = "EUR"
if code in valid_codes: # 수천 개의 코드가 있어도 즉시 조회됨
print("Valid")집합 연산
세트는 수학에서 배운 것과 같은 연산을 지원합니다. 합집합(둘 중 어느 세트에든 있는 것), 교집합(양쪽 세트가 공유하는 것만), 차집합(한쪽에는 있지만 다른 쪽에는 없는 것)입니다. 파이썬은 이를 위해 연산자 기호를 쓰고, 각각에는 대응하는 메서드도 있습니다.
|는 합집합(어느 쪽에든 있음), &는 교집합(양쪽 모두에 있음), -는 차집합(한쪽에만 있고 다른 쪽에는 없음), ^는 대칭차집합(한쪽에만 있고 양쪽 모두에는 없음)입니다. 기호보다 단어가 더 편하다면 .union() 같은 풀어 쓴 메서드도 각각 있습니다. 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()는 항목 하나를 넣고, .update()는 리스트나 다른 이터러블에서 여러 개를 한꺼번에 추가합니다. 구분해서 기억해야 할 짝은 삭제입니다. .remove()는 항목이 없으면 에러를 내고, .discard()는 어깨를 한번 들썩이고 그냥 넘어갑니다. 세트에 있는지 확실하지 않을 때는 .discard()가 뜻하지 않은 에러를 막아줍니다. tags = {"python", "beginner"}
tags.add("tutorial") # 항목 하나 추가
tags.update(["web", "api"]) # 어떤 이터러블에서든 여러 항목을 추가
tags.remove("beginner") # 삭제, 없으면 KeyError 발생
tags.discard("missing") # 삭제, 없어도 에러 없음
tags.pop() # 임의의 항목을 제거하고 반환
tags.clear() # 전체 삭제항목이 존재하는지 확실하지 않을 때는 .discard()를 사용하십시오.
프로즌 세트
프로즌 세트는 생성 후 수정할 수 없는 세트입니다. 이를 쓰는 주된 이유는, 프로즌 세트가 해시 가능해서 딕셔너리 키로 쓰거나 다른 세트 안에 저장할 수 있다는 점입니다.
valid_statuses = frozenset({"active", "paused", "deleted"})
valid_statuses.add("archived") # AttributeError, frozenset은 불변입니다알맞은 컬렉션 고르기
네 가지 타입, 각각 명확한 역할이 있습니다. 데이터에 무엇을 해야 하는지 물어보면 대개 알맞은 선택이 뒤따라옵니다.
| list | tuple | set | dict | |
|---|---|---|---|---|
| 순서 | 있음 | 있음 | 없음 | 있음 (삽입 순서) |
| 가변성 | 가변 | 불변 | 가변 | 가변 |
| 중복 | 허용 | 허용 | 불허 | 불허 (키) |
| 접근 방식 | 인덱스 | 인덱스 | 해당 없음 | 키 |
| 사용 시점 | 순서 있고 바뀌는 시퀀스 | 고정된 레코드 | 고유한 값, 빠른 멤버십 | 키-값 조회 |
빠른 결정 규칙:
- 이름으로 무언가를 찾아봐야 한다? → dict
- 순서가 있고 수정할 컬렉션이 필요하다? → list
- 서로 관련된 값들의 고정된 묶음이 있다? → tuple
- 고유한 값이나 빠른 멤버십 확인이 필요하다? → set
실전에서
튜플로 고정된 레코드를 저장하고 세트로 고유 값을 추적하기:
home = (51.5074, -0.1278) # 위도, 경도
office = (51.5155, -0.0922)
home_lat, home_lon = home
print(f"Home: {home_lat}, {home_lon}")
# 세트로 고유 방문자를 추적
visitors = set()
visitors.add("alice")
visitors.add("bob")
visitors.add("alice") # 이미 세트에 있으므로 조용히 무시됨
visitors.add("carol")
print(f"Unique visitors: {len(visitors)}")
print(f"alice visited: {'alice' in visitors}")
print(f"dave visited: {'dave' in visitors}")
