셋(Set)의 기본과 활용: 중복 없는 데이터의 집합
1. 서론: 유일한 데이터의 효율적인 관리
지금까지 우리는 리스트(List), 튜플(Tuple), 딕셔너리(Dictionary)와 같은 파이썬의 핵심 자료 구조들을 배웠습니다. 이들은 각각 순서, 변경 가능성, 키-값 쌍이라는 고유한 특징을 가지고 데이터를 관리합니다. 하지만 때로는 데이터의 순서나 중복 여부가 중요하지 않고, 오직 ‘유일한(Unique)’ 요소들만을 효율적으로 관리하고 싶을 때가 있습니다. 예를 들어, 웹사이트 방문자들의 고유 IP 주소 목록을 저장하거나, 특정 이벤트에 참여한 사람들의 중복 없는 명단을 만들거나, 두 집합 간의 공통 요소나 차이점을 찾아야 할 때가 그렇습니다. 이때 필요한 것이 바로 ‘셋(Set)’입니다. 셋은 수학의 집합 개념과 유사하며, 중복된 요소를 허용하지 않고 순서가 없는 자료형입니다. 이 챕터에서는 파이썬 셋의 기본적인 개념과 생성 방법, 그리고 셋이 가지는 중요한 특징들과 집합 연산(합집합, 교집합, 차집합 등)에 대해 깊이 있게 알아보겠습니다. 셋을 마스터하는 것은 여러분의 파이썬 데이터 관리 능력을 한 단계 더 끌어올리고, 유일한 데이터를 효율적으로 다룰 수 있는 기반을 마련하는 핵심적인 단계가 될 것입니다.
2. 셋(Set)이란 무엇인가?
셋은 중복된 요소를 허용하지 않고, 순서가 없는(unordered) 데이터의 집합입니다. 파이썬에서 셋은 중괄호({})로 묶고, 각 요소는 쉼표(,)로 구분하여 생성합니다. (단, 빈 셋을 만들 때는 set() 함수를 사용해야 합니다. {}는 빈 딕셔너리를 의미합니다.)
# 셋 생성 예시
my_set = {1, 2, 3, 2, 1} # 중복된 1과 2는 자동으로 제거됩니다.
print(my_set) # 출력: {1, 2, 3} (순서는 다를 수 있습니다)
print(type(my_set)) # 출력: <class 'set'>
# 빈 셋 생성
empty_set = set()
print(empty_set) # 출력: set()
print(type(empty_set)) # 출력: <class 'set'>
# 주의: {}는 빈 딕셔너리를 생성합니다.
empty_dict = {}
print(type(empty_dict)) # 출력: <class 'dict'>
2.1. 셋의 특징
- 중복 불허 (Unique Elements): 셋의 가장 중요한 특징입니다. 셋은 중복된 요소를 저장하지 않습니다. 요소를 추가할 때 이미 존재하는 요소라면 추가되지 않습니다.
- 순서 없음 (Unordered): 셋의 요소들은 순서를 가지지 않습니다. 따라서 인덱싱이나 슬라이싱을 사용할 수 없습니다. 요소를 출력할 때마다 순서가 다를 수 있습니다.
- 변경 가능 (Mutable): 셋 자체는 요소를 추가하거나 삭제할 수 있어 변경 가능합니다. 하지만 셋의 요소는 변경 불가능한(Immutable) 자료형(숫자, 문자열, 튜플 등)만 포함할 수 있습니다. 리스트나 딕셔너리처럼 변경 가능한 자료형은 셋의 요소로 포함할 수 없습니다.
- 빠른 검색: 셋은 내부적으로 해시 테이블(Hash Table)을 사용하여 요소를 저장하므로, 특정 요소가 셋에 포함되어 있는지(
in연산자)를 매우 빠르게 확인할 수 있습니다.
3. 셋 생성 방법
셋을 생성하는 방법은 다양합니다.
3.1. 중괄호 {}를 이용한 생성
초기 값을 가진 셋을 생성할 때 사용합니다. 중복된 값은 자동으로 제거됩니다.
numbers = {1, 2, 3, 2, 4}
print(numbers) # 출력: {1, 2, 3, 4} (순서는 다를 수 있음)
fruits = {"apple", "banana", "cherry"}
print(fruits) # 출력: {'apple', 'banana', 'cherry'}
# 다양한 자료형이 섞인 셋 (단, 요소는 불변이어야 함)
mixed_set = {10, 3.14, "Hello", True, (1, 2)}
print(mixed_set) # 출력: {True, 10, 3.14, 'Hello', (1, 2)} (순서는 다를 수 있음)
# 리스트는 셋의 요소가 될 수 없습니다. (TypeError 발생)
# invalid_set = {1, [2, 3]} # TypeError: unhashable type: 'list'
3.2. set() 함수를 이용한 변환
set() 함수는 문자열, 리스트, 튜플 등 다른 반복 가능한(iterable) 객체를 셋으로 변환할 때 사용합니다. 이 과정에서 중복된 요소는 자동으로 제거됩니다.
# 리스트를 셋으로 변환 (중복 제거)
my_list = [1, 2, 2, 3, 4, 4, 5]
set_from_list = set(my_list)
print(f"리스트 {my_list}을 셋으로: {set_from_list}")
# 출력: 리스트 [1, 2, 2, 3, 4, 4, 5]을 셋으로: {1, 2, 3, 4, 5}
# 문자열을 셋으로 변환 (중복 문자 제거, 순서 없음)
text = "programming"
set_from_str = set(text)
print(f"문자열 \"{text}\"을 셋으로: {set_from_str}")
# 출력: 문자열 "programming"을 셋으로: {'g', 'i', 'o', 'p', 'r', 'a', 'm', 'n'} (순서는 다를 수 있음)
# 튜플을 셋으로 변환
my_tuple = (10, 20, 20, 30)
set_from_tuple = set(my_tuple)
print(f"튜플 {my_tuple}을 셋으로: {set_from_tuple}")
# 출력: 튜플 (10, 20, 20, 30)을 셋으로: {10, 20, 30}
4. 셋에 요소 추가 및 삭제하기
셋은 변경 가능한 자료형이므로, 요소를 추가하거나 삭제할 수 있습니다.
4.1. add(): 요소 추가
add(element) 메서드는 셋에 새로운 요소를 추가합니다. 이미 존재하는 요소라면 아무런 변화가 없습니다.
my_set = {1, 2, 3}
print(f"원본 셋: {my_set}") # 출력: 원본 셋: {1, 2, 3}
my_set.add(4)
print(f"add 후 (새 요소): {my_set}") # 출력: add 후 (새 요소): {1, 2, 3, 4}
my_set.add(2) # 이미 존재하는 요소는 추가되지 않습니다.
print(f"add 후 (기존 요소): {my_set}") # 출력: add 후 (기존 요소): {1, 2, 3, 4}
4.2. remove(): 특정 요소 삭제
remove(element) 메서드는 셋에서 특정 요소를 삭제합니다. 만약 해당 요소가 셋에 없으면 KeyError가 아닌 KeyError와 유사한 KeyError가 발생합니다.
my_set = {1, 2, 3, 4}
print(f"원본 셋: {my_set}") # 출력: 원본 셋: {1, 2, 3, 4}
my_set.remove(3)
print(f"remove 후: {my_set}") # 출력: remove 후: {1, 2, 4}
# my_set.remove(5) # KeyError: 5 (존재하지 않는 요소 삭제 시도)
4.3. discard(): 특정 요소 삭제 (오류 없음)
discard(element) 메서드는 remove()와 유사하게 특정 요소를 삭제하지만, 해당 요소가 셋에 없어도 오류를 발생시키지 않습니다.
my_set = {1, 2, 3, 4}
print(f"원본 셋: {my_set}") # 출력: 원본 셋: {1, 2, 3, 4}
my_set.discard(2)
print(f"discard 후 (존재하는 요소): {my_set}") # 출력: discard 후 (존재하는 요소): {1, 3, 4}
my_set.discard(5) # 존재하지 않는 요소여도 오류 없음
print(f"discard 후 (존재하지 않는 요소): {my_set}") # 출력: discard 후 (존재하지 않는 요소): {1, 3, 4}
4.4. pop(): 임의의 요소 삭제 및 반환
pop() 메서드는 셋에서 임의의 요소를 삭제하고 그 요소를 반환합니다. 셋은 순서가 없으므로 어떤 요소가 삭제될지 예측할 수 없습니다. 셋이 비어있으면 KeyError가 발생합니다.
my_set = {1, 2, 3}
print(f"원본 셋: {my_set}") # 출력: 원본 셋: {1, 2, 3}
popped_element = my_set.pop()
print(f"pop 후: {my_set}, 삭제된 요소: {popped_element}")
# 출력: pop 후: {2, 3}, 삭제된 요소: 1 (또는 다른 요소)
4.5. clear(): 셋의 모든 요소 삭제
clear() 메서드는 셋의 모든 요소를 삭제하여 빈 셋으로 만듭니다. 셋 객체 자체는 남아있습니다.
my_set = {1, 2, 3}
my_set.clear()
print(f"clear 후: {my_set}") # 출력: clear 후: set()
5. 셋의 집합 연산 (Set Operations)
셋은 수학의 집합 개념을 기반으로 하므로, 다양한 집합 연산을 지원합니다. 이는 두 개 이상의 셋 간의 관계를 분석할 때 매우 강력한 도구가 됩니다.
set_a = {1, 2, 3, 4, 5}
set_b = {4, 5, 6, 7, 8}
5.1. 합집합 (Union, | 또는 union())
두 셋의 모든 요소를 포함하는 새로운 셋을 반환합니다.
union_set = set_a | set_b
print(f"합집합 (|): {union_set}") # 출력: 합집합 (|): {1, 2, 3, 4, 5, 6, 7, 8}
union_set_method = set_a.union(set_b)
print(f"합집합 (union()): {union_set_method}") # 출력: 합집합 (union()): {1, 2, 3, 4, 5, 6, 7, 8}
5.2. 교집합 (Intersection, & 또는 intersection())
두 셋에 공통으로 존재하는 요소를 포함하는 새로운 셋을 반환합니다.
intersection_set = set_a & set_b
print(f"교집합 (&): {intersection_set}") # 출력: 교집합 (&): {4, 5}
intersection_set_method = set_a.intersection(set_b)
print(f"교집합 (intersection()): {intersection_set_method}") # 출력: 교집합 (intersection()): {4, 5}
5.3. 차집합 (Difference, - 또는 difference())
한 셋에는 존재하지만 다른 셋에는 존재하지 않는 요소를 포함하는 새로운 셋을 반환합니다.
difference_set_ab = set_a - set_b
print(f"차집합 (A - B): {difference_set_ab}") # 출력: 차집합 (A - B): {1, 2, 3}
difference_set_ba = set_b - set_a
print(f"차집합 (B - A): {difference_set_ba}") # 출력: 차집합 (B - A): {6, 7, 8}
difference_set_method = set_a.difference(set_b)
print(f"차집합 (difference()): {difference_set_method}") # 출력: 차집합 (difference()): {1, 2, 3}
5.4. 대칭 차집합 (Symmetric Difference, ^ 또는 symmetric_difference())
두 셋 중 한 쪽에만 존재하는 요소를 포함하는 새로운 셋을 반환합니다. (합집합 – 교집합)
symmetric_difference_set = set_a ^ set_b
print(f"대칭 차집합 (^): {symmetric_difference_set}") # 출력: 대칭 차집합 (^): {1, 2, 3, 6, 7, 8}
symmetric_difference_set_method = set_a.symmetric_difference(set_b)
print(f"대칭 차집합 (symmetric_difference()): {symmetric_difference_set_method}")
# 출력: 대칭 차집합 (symmetric_difference()): {1, 2, 3, 6, 7, 8}
5.5. 부분집합/상위집합 확인
issubset(): 현재 셋이 다른 셋의 부분집합인지 확인합니다.issuperset(): 현재 셋이 다른 셋의 상위집합인지 확인합니다.isdisjoint(): 두 셋이 공통된 요소를 가지고 있지 않은지 확인합니다.
set_x = {1, 2}
set_y = {1, 2, 3, 4}
print(f"set_x가 set_y의 부분집합인가? {set_x.issubset(set_y)}") # 출력: True
print(f"set_y가 set_x의 상위집합인가? {set_y.issuperset(set_x)}") # 출력: True
set_c = {1, 2}
set_d = {3, 4}
print(f"set_c와 set_d는 서로소인가? {set_c.isdisjoint(set_d)}") # 출력: True
6. frozenset: 변경 불가능한 셋
파이썬은 변경 불가능한(Immutable) 셋인 frozenset도 제공합니다. frozenset은 일반 셋과 달리 요소를 추가하거나 삭제할 수 없으며, 딕셔너리의 키로 사용될 수 있습니다.
my_frozenset = frozenset([1, 2, 3])
print(my_frozenset) # 출력: frozenset({1, 2, 3})
# my_frozenset.add(4) # AttributeError: 'frozenset' object has no attribute 'add'
# frozenset은 딕셔너리의 키로 사용 가능
dict_with_frozenset_key = {frozenset({1, 2}): "Value A"}
print(dict_with_frozenset_key) # 출력: {frozenset({1, 2}): 'Value A'}
7. 결론: 유일한 데이터와 집합 연산의 효율성
이 챕터를 통해 여러분은 파이썬 셋의 기본적인 개념과 생성 방법, 그리고 셋이 가지는 중요한 특징들에 대해 깊이 있게 학습했습니다. 셋이 중복된 요소를 허용하지 않고 순서가 없으며, 변경 가능하지만 요소는 불변이어야 한다는 점을 이해했습니다. 또한, add(), remove(), discard(), pop(), clear()와 같은 메서드를 사용하여 셋의 요소를 조작하는 방법과, 합집합, 교집합, 차집합, 대칭 차집합 등 강력한 집합 연산을 수행하는 방법을 살펴보았습니다.
셋은 데이터에서 중복을 제거하거나, 특정 요소의 존재 여부를 빠르게 확인하거나, 두 데이터 집합 간의 관계를 분석할 때 매우 효율적인 자료 구조입니다. 예를 들어, 웹사이트 방문자들의 고유 IP 주소를 관리하거나, 두 리스트 간의 공통 요소를 찾는 등의 작업에서 셋은 빛을 발합니다.
이제 여러분은 파이썬의 핵심 자료 구조인 리스트, 튜플, 딕셔너리, 그리고 셋의 기본 개념과 조작 방법을 모두 이해했습니다. 다음 챕터에서는 이들 자료 구조를 언제 어떤 상황에서 사용해야 하는지, 즉 ‘자료 구조 선택 가이드’에 대해 알아보겠습니다. 오늘 배운 셋의 개념과 집합 연산을 활용하여 다양한 데이터를 직접 다루어 보면서, 여러분의 파이썬 실력을 더욱 단단하게 다지세요!
