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
이를 사용할 흔한 이유는 두 가지입니다: