2.10. 집합#
집합(set) 은 고유한 항목들의 순서 없는 모음입니다. 이미 존재하는 값을 추가해도 아무 효과가 없으며, 반복할 때 각 값은 정확히 한 번만 나옵니다. 집합은 멤버십과 중복 제거가 중요하고 순서는 중요하지 않을 때 적합한 도구입니다.
2.10.1. 집합 만들기#
비어 있지 않은 집합에는 중괄호를, 빈 집합에는 set() 을 사용하세요:
colours = {"red", "green", "blue"}
empty = set()
중괄호는 dict 리터럴처럼 보입니다. {} 그 자체는 빈 집합이 아니라 빈 딕셔너리 인데, 이는 Python의 역사적 우연 중 하나입니다. 빈 경우에는 set() 을 사용하세요.
set() 은 어떤 이터러블로부터든 집합을 만들기도 하는데, 이는 시퀀스에서 중복을 없애는 표준적인 방법입니다:
nums = [1, 2, 2, 3, 1, 4]
unique = set(nums)
print(unique)
출력:
{1, 2, 3, 4}
출력 순서는 달라질 수 있습니다. 집합은 어떤 특정한 순서로 반복한다고 보장하지 않습니다.
2.10.2. 집합 대 딕셔너리#
집합과 딕셔너리는 모두 고유한 항목을 해시 테이블에 저장합니다. 각 항목이 무엇을 함께 지니는가가 차이점입니다:
둘 사이의 선택은 각 항목 옆에 딸린 값 이 의미가 있는지에 관한 것입니다:
각 항목 옆에 어떤 값도 딸려 있지 않을 때, 즉 항목이 존재하는지만 신경 쓰거나 고유 항목들의 그룹을 합집합/교집합으로 결합할 때는 집합 을 선택하세요.
각 항목이 조회로 가져오려는 데이터와 짝지어져 있을 때, 즉 설정 맵, 캐시, 이름으로 키를 매긴 카운터 등에는 딕셔너리 를 선택하세요.
두 타입은 표면적인 구문을 많이 공유하는데, 바로 여기서 대부분의 혼란이 생깁니다. 차이점을 한 블록에 정리하면:
set | dict | |
|---|---|---|
담는 것 | 고유한 항목 | 고유한 키, 각각 값을 가짐 |
내용이 있는 리터럴 |
|
|
빈 리터럴 |
|
|
멤버십 검사 |
|
|
값 가져오기 | 해당 없음 |
|
항목 추가 |
|
|
반복 | 항목을 내놓음 | 키를 내놓음 (쌍은 |
내용이 있는 리터럴과 빈 리터럴 사이의 비대칭성은 짚고 넘어갈 만한 함정입니다:
항목이 들어 있는 중괄호 –
{1, 2, 3}– 는 집합 리터럴이고, 키-값 쌍이 들어 있는 중괄호 –{"a": 1}– 는 딕셔너리 리터럴입니다. 파서는 안에 무엇이 있는지로 둘을 구분합니다.안에 아무것도 없는 중괄호 –
{}– 는 빈 집합이 아니라 빈 딕셔너리입니다. 딕셔너리가 먼저 나왔기에 빈 리터럴은 딕셔너리의 것입니다. 빈 집합은 중괄호 리터럴이 아예 없으며 반드시set()으로 써야 합니다.
딕셔너리의 키만 읽힐 때 흔히 쓰는 패턴은 집합으로 바꾸는 것입니다. 의도가 분명해지고 쓰이지 않는 값을 메모리에서 덜어냅니다.
2.10.3. 추가와 제거#
set.add()– 항목 하나를 삽입합니다.set.discard()– 항목이 있으면 제거하고, 없으면 아무것도 하지 않습니다.set.remove()– 항목을 제거합니다. 없으면KeyError를 발생시킵니다.set.clear()– 집합을 비웁니다.
s = {1, 2, 3}
s.add(4)
s.discard(99) # silent: 99 not in s
s.remove(2)
print(s)
출력:
{1, 3, 4}
2.10.4. 멤버십#
in 연산자는 멤버십을 검사합니다. 집합에서는 크기와 상관없이 대략 상수 시간이 걸리는데, 이것이 “이 값이 거기 있는가” 만 물어보면 될 때 list 대신 집합을 선택하는 주된 이유입니다:
if "red" in colours:
print("colour is allowed")
같은 내용을 담은 list 라면 매번 처음부터 훑게 되는데, 항목이 열 개일 때는 괜찮지만 만 개일 때는 느립니다.
2.10.5. 집합 연산#
두 집합은 일반적인 수학 연산으로 결합할 수 있습니다. 각각 연산자 형태와 메서드 형태가 모두 있습니다:
a | b또는a.union(b)– 두 집합 중 어느 쪽에든 있는 모든 것.a & b또는a.intersection(b)– 두 집합 모두에 나타나는 것만.a - b또는a.difference(b)–a에는 있지만b에는 없는 것.a ^ b또는a.symmetric_difference(b)– 한쪽에는 있지만 둘 다에는 없는 것.
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a | b)
print(a & b)
print(a - b)
print(a ^ b)
출력:
{1, 2, 3, 4, 5, 6}
{3, 4}
{1, 2}
{1, 2, 5, 6}
연산자 형태는 읽기 전용이고, 메서드 형태는 오른쪽에 다른 집합뿐 아니라 어떤 이터러블이든 받습니다(a.union([5, 6])). 문맥에서 더 잘 읽히는 쪽을 고르세요.
2.10.6. 집합에 들어갈 수 있는 것#
집합 요소는 해시 가능 해야 하는데, 이는 dict 키와 같은 제약입니다. int, float, str, bool, bytes, 그리고 tuple (내용물이 그 자체로 해시 가능할 때) 은 모두 됩니다. list 와 dict 는 안 됩니다. 이를 추가하려 하면 TypeError 가 발생합니다.
2.10.7. frozenset#
일반 set 은 가변적입니다. add / remove / discard 를 호출할 때마다 객체가 제자리에서 변합니다. 그 가변성 때문에 집합은 해시 가능 자격을 잃으며, 따라서 집합은 dict 키나 다른 집합의 멤버로 사용할 수 없습니다.
frozenset 은 불변 대응물입니다. set 과 동일한 조회와 연산자(in, |, &, -, ^)를 갖지만, add / remove 가 없고 변경하는 메서드도 없습니다. 내용이 결코 변할 수 없으므로 frozenset 의 해시는 잘 정의됩니다. 따라서 해시 가능 합니다:
primary = frozenset({"red", "green", "blue"})
secondary = frozenset({"yellow", "purple", "orange"})
palettes = {
primary: "RGB",
secondary: "mixed",
}
print(palettes[primary])
출력:
RGB
어떤 이터러블로부터든 frozenset을 만드세요. 빈 경우에는 frozenset() 을, 기존 집합의 불변 스냅샷을 취하려면 frozenset(some_set) 을 사용합니다:
snapshot = frozenset(s) # immutable copy of s
s.add("new") # snapshot does not change
이를 사용할 흔한 이유는 두 가지입니다: