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

튜플과 세트

리스트는 이미 배웠습니다. 파이썬에는 리스트로 해결할 수 없는 문제를 위한 컬렉션 타입이 두 가지 더 있습니다. 튜플은 절대 바뀌지 않는 고정된 값들의 묶음을 담습니다. 세트는 오직 고유한 값만 담고, 컬렉션이 얼마나 커지든 특정 값이 들어있는지 즉시 확인할 수 있게 해줍니다.

파이썬의 컬렉션 도구 모음에는 네 가지 타입이 있습니다. 리스트딕셔너리가 대부분의 일반적인 상황을 처리합니다. 튜플과 세트는 좀 더 구체적인 상황을 해결합니다. 불변성이 장점으로 작용하는 고정된 레코드, 그리고 O(1) 멤버십 확인이 중요한 고유 값 컬렉션입니다.

listdict 외에도 파이썬은 tuple(생성 후 바꿀 수 없는 고정 길이 시퀀스)과 set(해시 테이블 기반의 고유 값들로 이루어진 순서 없는 컬렉션이며, dict를 빠르게 만드는 것과 동일한 내용 기반 조회 구조)을 제공합니다. 아래의 모든 결정을 좌우하는 구분 기준은 이것입니다: 튜플은 해시 가능(내용을 하나의 숫자로 축약할 수 있어서 딕셔너리의 키나 세트의 멤버로 쓸 수 있음)하지만, 리스트는 그렇지 않습니다. 습관적으로 손이 가는 쪽이 아니라, 컬렉션에 무엇을 시킬지에 따라 선택하십시오.

튜플

튜플은 생성 후 바꿀 수 없는 순서 있는 값들의 묶음입니다. 괄호가 튜플을 정의하지만 괄호는 있어도 없어도 됩니다. 실제로 튜플을 만드는 것은 쉼표입니다. 항목이 하나뿐인 튜플은 뒤에 쉼표를 붙여야 합니다.

튜플은 불변 시퀀스입니다. 괄호가 아니라 쉼표가 튜플을 만듭니다. 불변성 덕분에 모든 요소가 해시 가능할 때 튜플 자체도 해시 가능해지는데, 이는 리스트로는 채울 수 없는 용도를 열어줍니다. 딕셔너리 키, 세트 멤버, 그리고 고정된 구조의 레코드입니다.

tuple은 한번 만들어지면 바꿀 수 없는 고정 길이 시퀀스입니다. 내부의 어떤 것도 움직이지 않으므로, 모든 요소가 해시 가능하기만 하면 파이썬은 이를 위한 하나의 해시(내용에서 파생된 숫자)를 계산할 수 있습니다. 바로 이 점 때문에 튜플은 리스트가 할 수 없는 일, 즉 딕셔너리 키나 세트 멤버 역할을 할 수 있습니다. 인덱싱과 슬라이싱은 리스트와 완전히 동일하게 동작하지만, 항목 대입은 없습니다. 그래서 point[0] = 99는 실행되는 순간 TypeError를 발생시킵니다. 항목이 하나뿐인 형태 (42,)는 뒤에 쉼표가 필요합니다. 쉼표가 없으면 괄호는 그저 묶는 역할만 하고, 결과로 그냥 값 42만 돌아옵니다.

python
point = (10, 20)
rgb = (255, 128, 0)
dimensions = (1920, 1080)
single = (42,)            # 항목이 하나뿐인 튜플은 뒤에 쉼표가 필요합니다
also_tuple = 42, 99       # 괄호는 선택 사항이며, 쉼표가 튜플을 만듭니다

인덱스로 접근하는 방식은 리스트와 완전히 같습니다. 항목을 바꾸려 하면 TypeError가 발생합니다:

인덱싱, 슬라이싱, 음수 인덱스는 모두 리스트와 동일하게 동작합니다. 인덱스를 통한 대입은 모두 TypeError를 발생시키며, 이는 의도된 동작이지 한계가 아닙니다.

튜플에서 읽는 것, 즉 인덱스, 음수 인덱스, 슬라이스는 리스트와 완전히 동일하게 동작합니다. 다른 점은 쓰기입니다. 항목 대입이 없으므로 point[0] = 99는 그 줄이 실행되는 순간 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
Juno튜플 튜플은 만든 후에는 바꿀 수 없는 순서 있는 값들의 묶음입니다. 이를 만드는 것은 괄호가 아니라 쉼표이기 때문에, (42,)가 그 외로운 뒤쪽 쉼표를 필요로 하는 것입니다. 항목을 다시 대입해 보면 TypeError가 나오는데, 그 바꿀 수 없다는 안정감이야말로 튜플을 선택하는 이유입니다.
Juno튜플 튜플은 불변 시퀀스이고, 이를 만드는 것은 괄호가 아니라 쉼표입니다. 그 불변성이 바로 장점입니다. 내용이 해시 가능하면 튜플도 해시 가능해져서, 리스트로는 할 수 없는 딕셔너리 키나 세트 멤버 역할을 할 수 있습니다. 항목 대입은 의도적으로 TypeError를 발생시킵니다.
Juno튜플 튜플을 읽는 것은 리스트와 같고, 없는 건 쓰기입니다. 그래서 point[0] = 99는 실행 시점에 TypeError를 발생시킵니다. 이 점을 활용하십시오. 튜플은 이후 어디서도 다시 쓰이지 않을 것이라는 보장이 필요할 때 넘기는 값입니다. 항목이 하나뿐인 (42,) 함정은 누구나 한 번씩은 걸립니다.

튜플을 언제 사용할까

서로 관련된 작은 값들의 묶음이 있고 그 값들이 바뀌지 않을 때 튜플을 사용하십시오. 좌표 (x, y), 색상 (r, g, b), 이름과 점수 쌍 ("민준", 87) 같은 경우입니다. 이렇게 고정된 구조는 코드를 읽는 누구에게나 이 묶음이 하나의 단위로 취급된다는 것을 알려줍니다.

튜플은 고정된 구조를 나타냅니다. 위치 자체가 의미를 가지고 묶음 전체가 하나의 단위로 취급되는 경우입니다. 해시 가능하다는 점 덕분에 딕셔너리 키로도 유효한데, 이는 리스트로는 할 수 없는 일입니다. 튜플이 나타내는 약속은 이렇습니다. 이 값들은 함께 다녀야 하며 바뀌어서는 안 됩니다.

고정된 형태의 레코드, 즉 필드 개수가 정해져 있고 각 위치가 특정한 의미를 가지는 경우에 튜플을 사용하십시오. 튜플은 해시 가능(내용이 하나의 숫자로 축약됨)하기 때문에 해시 가능한 값이 필요한 어디에든 들어갈 수 있습니다. 딕셔너리 키, 세트 멤버, 또는 functools.lru_cache가 캐싱하는 인자(함수를 호출했던 인자를 키로 삼아 함수의 결과를 기억하는 것) 같은 곳입니다. 의미상의 신호도 리스트와 다릅니다. 튜플은 "이 필드들은 함께 다니고 각 위치는 의미가 있다"(좌표, 색상)라고 말하는 반면, 리스트는 "길이가 바뀔 수 있는 비슷한 것들의 나열"이라고 말합니다.

Juno튜플을 언제 사용할까 각 자리가 의미를 가지는 고정된 작은 묶음이 있을 때 튜플을 쓰십시오. 좌표, (r, g, b) 색상, 이름과 점수 쌍 같은 것들입니다. 튜플이 해시 가능하기 때문에 딕셔너리 키로도 쓸 수 있습니다. 리스트는 그렇게 할 수 없는데, 처음 시도해 보는 사람들이 여기서 걸려 넘어집니다.
Juno튜플을 언제 사용할까 튜플은 고정된 구조를 나타냅니다. 위치가 의미를 가지고 묶음 전체가 하나의 단위로 함께 다닙니다. 이 점이 "길이가 바뀔 수 있는 시퀀스"라고 말하는 리스트와의 차이입니다. 그리고 튜플이 해시 가능하기 때문에, 리스트가 TypeError를 내는 딕셔너리 키 자리에서도 튜플은 잘 동작합니다.
Juno튜플을 언제 사용할까 고정된 형태의 레코드에는 튜플, 길이가 변하는 비슷한 것들의 나열에는 리스트를 쓰십시오. 그 대가로 얻는 것이 해시 가능성입니다. 튜플은 딕셔너리 키, 세트, 또는 lru_cache 키 자리에 들어갈 수 있는데, 이는 모두 리스트가 갈 수 없는 자리입니다. 그 약속을 주석이 아니라 타입 자체가 전달하게 하십시오.
python
locations = {}
locations[(40, -74)] = "뉴욕"   # 튜플을 딕셔너리 키로 사용, 동작함
locations[[40, -74]] = "뉴욕"   # 리스트를 딕셔너리 키로 사용, TypeError

언패킹

언패킹은 튜플에서 값들을 꺼내 한 줄로 각각 자신만의 이름에 대입합니다. 이름의 개수는 값의 개수와 일치해야 합니다. 나머지 항목들을 리스트로 모으고 싶다면 *를 사용하십시오.

언패킹은 튜플, 리스트, 문자열 등 이터러블이라면 무엇이든 동작합니다. 대상 이름의 개수는 이터러블의 길이와 일치해야 하지만, 별표가 붙은 대상이 가변 길이 부분을 받아내는 경우는 예외입니다. 개수가 맞지 않으면 ValueError가 발생합니다. 언패킹은 함수의 여러 반환값을 받아내는 파이썬다운 방법입니다.

언패킹은 튜플뿐 아니라 이터러블(반복할 수 있는 것)이라면 무엇이든 동작하며, 우변을 따라가면서 각 값을 순서대로 대상 이름에 묶습니다. 별표가 붙은 대상(*rest)은 나머지 항목들을 list로 흡수하므로, 개수가 정확히 일치할 필요가 없어집니다. 그냥 개수가 맞지 않으면 실행 시점에 ValueError가 발생합니다. 가장 자주 쓰게 되는 형태는 for 문의 헤더입니다. for name, score in pairs는 반복할 때마다 각 항목을 언패킹하는데, 각 쌍을 일일이 인덱싱하는 것보다 훨씬 깔끔하게 읽힙니다.

Juno언패킹 언패킹은 튜플이나 리스트에서 각 값을 꺼내 한 줄로 자기만의 이름에 넘겨줍니다. x, y = point처럼요. 이름의 개수는 값의 개수와 맞아야 하지만, *rest를 추가하면 나머지를 다 모아줍니다. point[0], point[1]을 여기저기 쓰던 걸 멈춘 날, 저에게는 이게 확 이해가 됐습니다.
Juno언패킹 언패킹은 각 값을 한 줄로 자기만의 이름에 대입하고, *rest 같은 별표 대상은 가변 길이 부분을 받아냅니다. 개수가 맞지 않으면 ValueError가 발생합니다. 함수의 여러 반환값을 결과를 인덱싱하는 대신 깔끔하게 받아내는 방법입니다.
Juno언패킹 언패킹은 어떤 이터러블에서든 동작하고 왼쪽부터 순서대로 묶습니다. *rest가 나머지를 흡수하고, 단순한 개수 불일치는 ValueError를 냅니다. 가장 쓸모 있는 자리는 for 헤더입니다. for name, score in 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를 공짜로 얻습니다. 그보다 더 필요한 것, 즉 기본값이나 타입 애너테이션이 필요하다면, 애너테이션이 필요한 경우는 typing.NamedTuple이 처리하고, 메서드나 선택적 가변성이 필요해지면 dataclasses.dataclass가 현대적인 선택입니다.

Juno네임드 튜플 네임드 튜플은 모든 위치에 이름을 붙여줘서, point[0]이 x라는 걸 기억하는 대신 point.x라고 쓸 수 있습니다. 다른 모든 면에서는 여전히 완전히 불변이고 평범한 튜플처럼 동작합니다. 이름이 있으면 나중에 이 코드를 읽는 당신 자신이 고마워할 겁니다.
Juno네임드 튜플namedtuple은 이름으로 접근할 수 있는 튜플 형태의 클래스를 만들어 줍니다. 불변이고 스스로 문서화되며, 완전한 클래스보다 가볍습니다. 일반 튜플의 위치를 설명하려면 주석이 필요해질 것 같은 순간이 바로 이걸 쓸 때입니다. 그보다 더 필요하면 dataclass가 현대적인 다음 단계입니다.
Juno네임드 튜플namedtuple은 같은 메모리 비용으로 이름 붙은 필드를 가진 튜플 서브클래스를 주고, _replace()_asdict()도 공짜로 딸려 옵니다. 기본값, 메서드, 제대로 된 타입 힌트가 필요해지면 팩토리와 씨름하지 말고 typing.NamedTuple이나 dataclass로 넘어가십시오.

namedtuple은 표준 라이브러리에 있으므로 먼저 임포트해야 합니다: 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

alice = Player("민지", 87, 5)
alice.name    # "민지"
alice.score   # 87

세트

세트는 순서가 보장되지 않는 고유한 값들의 컬렉션입니다. 같은 값을 두 번 추가해도 아무 일도 일어나지 않습니다. 세트는 각 항목의 복사본을 하나만 유지합니다. 항목이 있는 세트를 만들려면 중괄호를 쓰고, 빈 세트를 만들려면 set()을 쓰십시오.

set은 중복을 자동으로 걸러내는 순서 없는 컬렉션입니다. 멤버십 확인은 크기와 관계없이 O(1)이므로, 큰 컬렉션에서 어떤 값이 존재하는지 확인해야 할 때 항상 적합한 도구입니다. 참고: {}는 빈 딕셔너리를 만들고 빈 세트를 만들지 않습니다. 빈 세트에는 set()을 사용하십시오.

set은 고유하고 해시 가능한 값들을 담으며, 딕셔너리와 같은 내용 기반 조회 구조인 해시 테이블을 기반으로 만들어집니다. 이 덕분에 평균 O(1)의 멤버십 확인, 삽입, 삭제를 얻습니다(세트가 커져도 비용이 그 크기에 따라 늘어나지 않고 그대로 유지됩니다). 이에 따라 계획해 두어야 할 결과가 두 가지 있습니다. 해시 가능한 값만 들어갈 수 있어서 int, str, tuple은 괜찮지만 list, dict, set은 안 되고, 순회 순서는 내부 해시 위치를 따르므로 안정적이라고 절대 믿어서는 안 됩니다. 문법적으로 걸리기 쉬운 함정이 하나 있습니다. {}는 빈 세트가 아니라 빈 딕셔너리이므로, set()을 사용하십시오.

Juno세트 세트는 오직 고유한 값만 유지하기 때문에, 이미 들어있는 것을 추가해도 아무 일도 일어나지 않습니다. 에러도 없고 중복도 없습니다. 항목들과 함께 중괄호로 만들되, 빈 세트를 만들 때는 set()을 쓰십시오. {}는 사실 빈 딕셔너리이기 때문입니다. 이 마지막 부분에서 거의 모두가 걸려 넘어집니다.
Juno세트 세트는 중복을 자동으로 걸러내고, 크기가 얼마든 O(1)로 멤버십을 확인합니다. 그래서 큰 컬렉션에서 "이게 여기 있나?"를 물을 때 적합한 도구가 됩니다. 빈 세트 함정을 조심하세요. {}는 딕셔너리이므로 set()을 쓰세요.
Juno세트 세트는 고유하고 해시 가능한 값들의 해시 테이블입니다. O(1)의 멤버십, 삽입, 삭제를 얻지만, 그 대가로 해시 가능한 항목만 들어갈 수 있고 순회 순서는 절대 안정적이지 않습니다. 그 순서에 의지하지 말고, {}가 딕셔너리라는 것을 기억해서 빈 세트가 필요할 때는 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) 멤버십(세트가 커져도 in의 비용이 그대로 유지됨), 그리고 집합 연산(|, &, -, ^)입니다. 실제 코드에서 진짜 효과를 보는 것은 멤버십 확인입니다. 10,000개 항목을 가진 세트에서의 in은 10개짜리 세트에서와 똑같이 빠르지만, 같은 확인을 리스트에서 하면 O(n)이라서 크기에 비례해 느려집니다. x in some_list를 큰 리스트에 대해 반복하고 있다는 것을 발견했다면, 그 리스트를 한 번 세트로 바꿔주는 것이 보통 해결책입니다.

Juno세트를 언제 사용할까 세트가 잘하는 세 가지 일이 있습니다. 리스트에서 중복을 걷어내는 것, 큰 컬렉션에 무언가가 있는지 빠르게 확인하는 것, 그리고 두 그룹을 비교해서 공통점이나 차이점을 찾는 것입니다. 이 중 하나를 하고 있다면 세트가 아마 맞는 도구일 겁니다.
Juno세트를 언제 사용할까 세 가지 이유: 삽입 시 중복 제거, 리스트의 O(n)과 비교되는 O(1) 멤버십, 그리고 |, &, -, ^를 이용한 집합 연산입니다. 컬렉션이 클수록, in을 자주 확인할수록 세트가 더 앞서 나갑니다.
Juno세트를 언제 사용할까 고유성, O(1) in, 그리고 집합 연산, 모두 해시 테이블에서 자연스럽게 나오는 것들입니다. 실용적으로 이득이 되는 것은 멤버십입니다. 반복되는 x in big_list는 매번 O(n)이므로, 그 리스트를 한 번 세트로 바꾸는 것이 보통의 해법입니다.
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")

집합 연산

세트는 수학에서 배운 것과 같은 연산을 지원합니다. 합집합(둘 중 어느 세트에든 있는 것), 교집합(양쪽 세트가 공유하는 것만), 차집합(한쪽에는 있지만 다른 쪽에는 없는 것)입니다. 파이썬은 이를 위해 연산자 기호를 쓰고, 각각에는 대응하는 메서드도 있습니다.

파이썬의 세트 연산자는 수학 표기법을 그대로 반영합니다. 합집합에는 |, 교집합에는 &, 차집합에는 -, 대칭차집합에는 ^입니다. 각 연산자에는 메서드 형태(.union(), .intersection() 등)도 있는데, 세트뿐 아니라 어떤 이터러블도 받아들입니다.

연산자 형태(|, &, -, ^)는 양쪽 모두 세트여야 하며, 리스트처럼 그렇지 않은 것이 오면 TypeError를 발생시킵니다. 메서드 형태(.union(), .intersection() 등)는 더 느슨합니다. 어떤 이터러블도 받아들여서 알아서 변환해 주기 때문에, a | [1, 2]는 실패하지만 a.union([1, 2])는 동작합니다. 연산이 문제를 일으켰을 때 기억해야 할 차이가 바로 이것입니다. 또한 새 세트를 반환하는 대신 왼쪽 세트를 직접 갱신하는 제자리 연산 형태(|=, &=, -=, ^=)도 있는데, 이는 .update(), .intersection_update() 등을 호출하는 것과 같습니다.

Juno집합 연산 수학 수업에서 배운 네 가지 연산입니다. |는 합집합(어느 쪽에든 있음), &는 교집합(양쪽 모두에 있음), -는 차집합(한쪽에만 있고 다른 쪽에는 없음), ^는 대칭차집합(한쪽에만 있고 양쪽 모두에는 없음)입니다. 기호보다 단어가 더 편하다면 .union() 같은 풀어 쓴 메서드도 각각 있습니다.
Juno집합 연산| 합집합, & 교집합, - 차집합, ^ 대칭차집합입니다. 메서드 형태(.union() 등)도 같은 일을 하지만, 연산자로는 안 되는 것, 즉 세트가 아닌 이터러블도 받아들입니다.
Juno집합 연산 연산자는 양쪽이 다 세트여야 하고 그렇지 않으면 TypeError를 냅니다. 메서드 형태는 어떤 이터러블도 받아들이므로, a | [1, 2]는 안 되지만 a.union([1, 2])는 됩니다. 제자리 연산 형태(|=, &=)는 새 세트를 반환하는 대신 그 자리에서 갱신합니다.
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를 발생시키는 반면, .discard()는 존재 여부가 불확실할 때 안전한 선택입니다. .pop()은 임의의 원소를 제거하는데, 세트에는 순서가 없으므로 "마지막" 원소가 아닙니다.

.add(x)는 평균 O(1)로 항목 하나를 삽입합니다. .update(iterable)은 여러 항목을 추가하며 |=와 같습니다. 사람들이 걸리는 짝은 .remove().discard()입니다. 둘 다 항목을 삭제하지만, .remove()는 항목이 없으면 KeyError를 발생시키고 .discard()는 조용히 아무 일도 하지 않습니다. 존재 여부가 불확실할 때는 .discard()를 쓰십시오. 그러면 항목이 없다는 것이 감싸야 할 예외가 되지 않습니다. .pop()은 어떤 원소를 반환하며 제거하지만 어떤 것인지는 정의되어 있지 않습니다. 세트에는 순서가 없으므로 .pop()을 "마지막" 항목을 가져오는 것으로 취급해서는 안 됩니다.

Juno세트 수정하기.add()는 항목 하나를 넣고, .update()는 리스트나 다른 이터러블에서 여러 개를 한꺼번에 추가합니다. 구분해서 기억해야 할 짝은 삭제입니다. .remove()는 항목이 없으면 에러를 내고, .discard()는 어깨를 한번 들썩이고 그냥 넘어갑니다. 세트에 있는지 확실하지 않을 때는 .discard()가 뜻하지 않은 에러를 막아줍니다.
Juno세트 수정하기 하나를 넣을 때는 .add(), 어떤 이터러블에서 여러 개를 넣을 때는 .update()입니다. .remove()는 항목이 없으면 KeyError를 냅니다. .discard()는 그것을 무시하는 안전한 버전입니다. 그리고 세트에는 순서가 없으므로 .pop()은 "마지막" 원소가 아니라 임의의 원소를 꺼냅니다.
Juno세트 수정하기.add()는 O(1)이고, .update()|=입니다. 함정은 .remove()KeyError를 내는 반면 .discard()는 조용히 넘어간다는 점입니다. 존재 여부가 불확실할 때는 .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은 생성 후 바꿀 수 없는 set입니다. 내부의 어떤 것도 움직이지 않으므로 안정적인 해시를 얻고 그 자체가 해시 가능해져서, 평범한 set으로는 할 수 없는 일인 딕셔너리 키나 다른 세트의 멤버가 될 수 있습니다. 읽거나 새 컬렉션을 반환하는 것들(멤버십 확인, 집합 연산자)은 모두 그대로 동작하고, 변경 메서드(add, remove 등)는 사라집니다. 이것이 딱 맞는 경우는, 프로그램 실행 중에 절대 바뀌면 안 되고 딕셔너리나 세트 안에 들어가야 할 수도 있는 상수 조회 테이블입니다.

Juno프로즌 세트 프로즌 세트는 만들어진 후에는 바꿀 수 없는 세트입니다. 바로 이 잠긴 상태 덕분에 해시 가능해져서, 평범한 세트와 달리 딕셔너리 키로 쓰거나 다른 세트 안에 넣을 수 있습니다. 절대 바뀌지 않아야 할 고정된 허용 값 묶음이 있을 때 이걸 쓰십시오.
Juno프로즌 세트frozenset은 불변의 set입니다. 모든 읽기와 집합 연산은 있지만 변경은 없습니다. 불변성 덕분에 해시 가능해져서, 평범한 set이 에러를 내는 자리인 딕셔너리 키나 다른 세트의 멤버로 동작합니다. 상수 조회 테이블에 잘 맞습니다.
Juno프로즌 세트frozenset은 안정적인 해시를 가지므로 set이 갈 수 없는 곳, 즉 딕셔너리 키나 다른 세트의 멤버 자리에 갈 수 있습니다. 읽기와 집합 연산은 남아 있고 변경 메서드는 사라집니다. 실행 중에 절대 바뀌면 안 되는 상수 조회 테이블이 이것의 깔끔한 활용처입니다.
python
valid_statuses = frozenset({"active", "paused", "deleted"})
valid_statuses.add("archived")    # AttributeError, frozenset은 불변입니다

알맞은 컬렉션 고르기

네 가지 타입, 각각 명확한 역할이 있습니다. 데이터에 무엇을 해야 하는지 물어보면 대개 알맞은 선택이 뒤따라옵니다.

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

이 선택은 절반은 성능, 절반은 의미의 문제입니다. dictset은 해싱을 통해 평균 O(1)의 조회를 주므로, 커져도 멤버십 확인이 빠르게 유지됩니다. listtuple은 인덱스로 O(1) 접근을 주지만 멤버십 확인은 O(n)이라서 in이 크기에 따라 느려집니다. 다른 축은 그 타입이 무엇을 나타내는지입니다. tuple은 불변성이 해시 가능성을 사주는 고정된 레코드이고, list는 길이가 변하는 시퀀스입니다. 딕셔너리나 세트 안에 넣을 해시 가능한 컬렉션이 필요할 때, 내장 타입 중 그렇게 할 수 있는 것은 tuplefrozenset 둘뿐입니다.

Juno알맞은 컬렉션 고르기 네 가지 타입, 네 가지 명확한 역할입니다. 순서가 있고 바꿔 나갈 것에는 리스트, 고정된 레코드에는 튜플, 고유한 값과 빠른 "여기 있나?" 확인에는 세트, 이름으로 찾아보는 데는 딕셔너리입니다. 데이터에 무엇을 해야 하는지 물어보면 대개 알맞은 것이 나옵니다.
Juno알맞은 컬렉션 고르기 데이터가 필요로 하는 것을 기준으로 고르세요. 가변성, 순서, 중복이 문제가 되는지, 그리고 어떻게 조회할지입니다. 바꿔 나갈 순서 있는 시퀀스에는 리스트, 고정된 레코드에는 튜플, 고유한 값과 O(1) 멤버십에는 세트, 키-값 조회에는 딕셔너리입니다.
Juno알맞은 컬렉션 고르기 절반은 성능, 절반은 의미입니다. dictset은 O(1) 조회를 주고, listtuple은 인덱스로는 O(1)이지만 멤버십은 O(n)입니다. tuple은 또한 "고정된 레코드"를 나타내고 해시 가능성을 사줍니다. 무언가를 중첩해 넣을 해시 가능한 컬렉션이 필요할 때, tuplefrozenset이 두 가지 선택지입니다.
listtuplesetdict
순서있음있음없음있음 (삽입 순서)
가변성가변불변가변가변
중복허용허용불허불허 (키)
접근 방식인덱스인덱스해당 없음
사용 시점순서 있고 바뀌는 시퀀스고정된 레코드고유한 값, 빠른 멤버십키-값 조회

빠른 결정 규칙:

  • 이름으로 무언가를 찾아봐야 한다? → dict
  • 순서가 있고 수정할 컬렉션이 필요하다? → list
  • 서로 관련된 값들의 고정된 묶음이 있다? → tuple
  • 고유한 값이나 빠른 멤버십 확인이 필요하다? → set

실전에서

튜플로 고정된 레코드를 저장하고 세트로 고유 값을 추적하기:

python
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}")

세트로 이미 처리된 것을 추적하고 남은 작업을 계산하기:

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"Files to process: {sorted(to_process)}")

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

print(f"Done. Total processed: {len(already_processed)}")

상수 조회 테이블에 frozenset을 사용하고 집합 연산으로 O(1) 멤버십 확인을 보여주기:

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

# frozenset에 집합 연산을 적용하면 평범한 set이 반환됨
unsafe_allowed = ALLOWED_METHODS - SAFE_METHODS
print(f"Non-safe allowed methods: {unsafe_allowed}")

# frozenset은 해시 가능하므로 세트 안에 저장할 수 있음 (평범한 set은 안 됨)
method_groups = {
    frozenset({"GET", "HEAD", "OPTIONS"}),
    frozenset({"POST", "PUT", "PATCH"}),
    frozenset({"DELETE"}),
}
print(f"Method groups: {len(method_groups)}")

method = "POST"
print(f"Allowed: {method in ALLOWED_METHODS}")
print(f"Safe:    {method in SAFE_METHODS}")

frozenset은 O(1) 조회를 유지하며 해시 가능한 타입이 필요한 어디에든 저장할 수 있습니다. 두 frozenset 객체에 집합 연산을 적용하면 평범한 set이 반환됩니다. 결과를 불변으로 유지하려면 frozenset()으로 감싸십시오.