5.4.1 순서와 중복이 없는 원소의 집합

집합(set)은 시퀀스와 매핑처럼 데이터를 담을 수 있는 도구이지만, 데이터의 저장보다는 어떤 원소가 어떤 집합에 포함되는지를 검사하기 위한 도구다. 어떤 원소가 집합에 포함되어 있는지 검사하는 문제의 예로는 “어떤 이메일 주소가 메일링 리스트에 등록되어 있는가”, “어떤 파일의 확장자가 사용가능한 확장자인가” 등을 들 수 있다.

집합은 수학의 집합 이론을 흉내낸 도구다. 합집합, 교집합, 차집합 같은 수학의 집합 연산을 파이썬의 집합에도 적용할 수 있다. ‘한국의 모든 남성’과 ‘20세 이하인 사람’의 교집합을 구할 때처럼, 집합 연산은 프로그래밍에도 유용하다.

집합은 저장한 데이터의 순서를 관리하지 않고, 데이터를 중복 저장하지 않는다. 순서를 통해 원소를 관리하고 중복 데이터도 허용하는 시퀀스나 키의 중복은 금지되지만 값의 중복은 허용하는 매핑과 구별된다. 원소의 순서와 중복 원소는 어떤 원소가 집합에 포함되는지를 검사하는 데는 불필요한 정보다.

집합의 특징을 요약해 보자.

  • 저장한 데이터에 순서가 없다.
  • 중복 데이터를 저장하지 않는다.
  • 어떤 원소가 집합에 포함되었는지 검사하는 데 사용된다.
  • 수학의 집합 연산을 적용할 수 있다.

5.4.2 집합 표현하기

집합을 표현할 때는 중괄호({}) 를 사용한다. 여러 개의 원소는 콤마(,)로 구분한다. {1, 2, 3, 4}와 같은 식이다. 대괄호를 사용하는 리스트나 소괄호를 사용하는 튜플과 매우 흡사하다. 대괄호와 소괄호 대신 중괄호를 사용할 뿐이다. 중괄호는 {키1: 값1, 키2: 값2}와 같이 사전을 표현할 때도 사용된다. 그렇지만 사전은 콜론(:)으로 표현된 키-값 쌍을 담으므로 집합과 구별이 가능하다.

그런데 공집합(원소가 없는 집합)은 중괄호로 표현할 수 없다. 빈 사전({})은 키-값 쌍이 들어있지 않아 초보 파이썬 사용자는 공집합으로 혼동하기 쉽다. 그러나 파이썬에서 {}은 공집합이 아니라 빈 사전을 나타낸 것으로 약속되어 있다. 공집합은 중괄호가 아니라 set()으로 나타낸다.

  • 원소가 하나 이상인 집합: {원소1, 원소2, 원소3, ...}
  • 공집합: set()

다음은 이 정의에 따라 몇 가지 집합을 표현해 본 것이다.

코드 5-59 집합 표현하기

# 정수의 집합
{0, -127, 97, 1789}

# 이메일 주소의 집합
{'bakyeono@gmail.com', 'i@bakyeono.net'}

# 공집합
set()

연습문제

연습문제 5-13 집합 정의하기

다음 세 집합을 파이썬 집합으로 각각 정의하라.

* 한 주의 모든 요일
* 여러분이 학교나 직장에 가는 요일
* 여러분이 휴식을 취하는 요일

5.4.3 시퀀스를 집합으로 변환하기

시퀀스는 순서와 중복이 있다는 점을 제외하면 집합과 형태가 유사하다. 따라서 집합을 시퀀스로 변환하거나 반대로 집합을 시퀀스로 변환할 수 있다. 단, 시퀀스를 집합으로 변환할 때는 원소의 순서와 중복된 원소가 사라진다는 점에 유의해야 한다.

시퀀스를 집합으로 변환할 때는 set() 함수를 사용한다.

코드 5-60 시퀀스를 집합으로 변환하기

>>> set([6, 1, 1, 2, 3, 3, 1, 5, 5, 4])      # 리스트를 집합으로
{1, 2, 3, 4, 5, 6}

>>> set(('사과', '토마토', '바나나', '감'))  # 튜플을 집합으로
{'감', '사과', '바나나', '토마토'}

위 코드에서 확인할 수 있듯, 시퀀스를 집합으로 변환하면 중복된 원소는 하나만 남고, 원소의 순서는 유지되지 않는다.

레인지를 정의한 후 집합으로 변환하면, ‘0 이상 1만 미만의 모든 짝수’와 같은 일정 범위의 규칙적인 수의 집합을 쉽게 생성할 수 있다.

코드 5-61 레인지를 이용해 집합 정의하기

>>> 짝수 = set(range(0, 10000, 2))
>>> 홀수 = set(range(1, 10000, 2))

집합의 원소가 될 수 있는 데이터

집합은 수, 문자열, 불리언, 튜플, 레인지 등의 불변 데이터를 원소로 가질 수 있다. 하지만 리스트나 사전 같은 가변 데이터, 그리고 가변 데이터를 담은 튜플은 집합의 원소가 될 수 없다.

시퀀스에서 중복된 원소 제거하기

집합에 중복된 원소가 없다는 점을 이용해, 시퀀스의 중복 원소를 제거할 때 집합 변환을 활용할 수 있다.

코드 5-62 리스트에서 중복 원소 제거하기

>>> list(set([6, 1, 1, 2, 3, 3, 1, 5, 5, 4]))
[1, 2, 3, 4, 5, 6]

이 방법을 사용할 때는 중복 원소뿐 아니라 원소의 순서라는 간접적인 정보 또한 사라진다는 점에 유의해야 한다.

연습문제

연습문제 5-14 중복 원소 제거하기

0 이상 1000 미만의, 3의 배수 또는 4의 배수는 모두 몇 개인지 계산하라.

힌트: 3의 배수의 시퀀스와 4의 배수의 시퀀스를 각각 구하라.

힌트: 두 시퀀스를 합한 후 중복을 제거하라.

5.4.4 집합 연산

집합에는 원소 검사, 합집합, 교집합, 여집합, 차집합 부분집합 검사 등의 수학의 집합 연산을 수행할 수 있다. 집합에서 사용할 수 있는 연산을 IDLE의 대화식 셸에 코드를 입력하며 학습해 볼 것이다. 연습에 사용할 집합부터 정의해 두자.

코드 5-63 집합의 원소 개수 세기

>>> 들짐승 = {'사자', '박쥐', '늑대', '곰'}
>>> 날짐승 = {'독수리', '매', '박쥐'}
>>> 바다생물 = {'참치', '상어', '문어 괴물'}

원소 개수 세기

len() 함수는 집합의 원소 개수를 셀 때도 사용된다.

코드 5-64 집합의 원소 개수 세기

>>> len(들짐승)
4

원소 검사하기

원소를 포함하는지 확인하는 것은 집합의 핵심 기능이다. 시퀀스나 매핑과 마찬가지로 innot in 연산을 통해 원소를 검사할 수 있다.

코드 5-65 집합의 원소 검사하기

>>> '늑대' in 들짐승
True

>>> '곰' not in 들짐승
False

참고로 집합은 인덱싱 연산을 지원하지 않는다. 따라서 들짐승[0] 이나 들짐승['늑대'] 같은 표현은 오류다.

집합의 원소 검사 기능이 시퀀스보다 좋은 이유

원소를 검사하는 기능은 시퀀스에도 있다. 예를 들어, '늑대' in list(들짐승)과 같이 리스트에서 원소를 검사해도 집합에서 검사한 것과 동일한 결과를 얻을 수 있다.

그런데 검사해야 하는 데이터의 양이 클 때는 집합이 유리하다. 시퀀스의 원소 검사 기능은 원소를 처음부터 끝까지 순서대로 확인하면서 찾는다. 시퀀스가 클수록 검사 비용이 커진다. 반면, 집합은 해시 알고리즘을 사용하여 언제나 원소 검사를 단 번에 수행해 낸다.

그렇다면 시퀀스의 원소를 검사할 때 먼저 집합으로 변환해야 할까? 꼭 그렇지는 않다. 시퀀스를 집합으로 변환하는 것도 연산 비용을 발생시키기 때문이다. 검사를 수행해야 할 횟수가 많은지 등 이익과 손실을 따져봐야 할 것이다.

합집합 구하기

합집합(union)은 두 집합의 모든 원소를 합한 집합이다.

그림 5-7 A와 B의 합집합

그림 5-7 A와 B의 합집합

합집합은 union(다른집합) 메서드를 사용하여 구할 수 있다.

코드 5-66 두 집합의 합집합 구하기

>>> 짐승 = 들짐승.union(날짐승)  # 들짐승과 날짐승의 합집합
>>> 짐승
{'곰', '사자', '박쥐', '늑대', '독수리', '매'}

합집합은 | 연산자를 통해 구할 수도 있다. 여러 집합을 합할 때 이 연산자를 사용하면 메서드를 사용하는 것보다 편리하다.

코드 5-67 | 연산자로 합집합 구하기

>>> 짐승 | 들짐승 | 날짐승 | {'인간'}
{'곰', '사자', '박쥐', '늑대', '인간', '독수리', '매'}

교집합 구하기

교집합(intersection)은 두 집합이 공통으로 포함하는 모든 원소의 집합이다.

그림 5-8 A와 B의 교집합

그림 5-8 A와 B의 교집합

교집합은 intersetion(다른집합) 메서드를 사용하여 구할 수 있다.

코드 5-68 두 집합의 교집합 구하기

>>> 들짐승.intersection(날짐승)  # 들짐승과 날짐승의 교집합
{'박쥐'}

>>> 들짐승.intersection(바다생물)  # 짐승과 바다생물의 교집합
set()

교집합은 & 연산자를 통해 구할 수도 있다.

코드 5-69 & 연산자로 교집합 구하기

>>> 짐승 & 들짐승 & 날짐승
{'박쥐'}

차집합 구하기

차집합(difference set)은 한 집합에서 다른 집합에 존재하는 모든 원소를 뺀 집합이다.

그림 5-9 A에서 B를 뺀 차집합

그림 5-9 A에서 B를 뺀 차집합

차집합은 difference(다른집합) 메서드를 사용하여 구할 수 있다.

코드 5-70 차집합 구하기

>>> 짐승.difference(날짐승)  # 짐승에서 날짐승을 뺀 집합
{'곰', '사자', '늑대'}

차집합은 - 연산자를 통해 구할 수도 있다.

코드 5-71 - 연산자로 차집합 구하기

>>> 날짐승 - 들짐승  # 날짐승에서 들짐승을 뺀 집합
{'독수리', '매'}

대칭차 구하기

대칭차(symmetric difference)는 두 집합의 합집합에서 교집합을 뺀 것이다. 즉, 두 집합이 공유하지 않는 모든 원소의 집합이다.

그림 5-10 A와 B의 대칭차

그림 5-10 A와 B의 대칭차

대칭차는 symmetric_difference(다른집합) 메서드를 사용하여 구할 수 있다.

코드 5-72 두 집합의 대칭차 구하기

>>> 들짐승.symmetric_difference(날짐승)  # 들짐승과 날짐승의 대칭차
{'사자', '늑대', '곰', '독수리', '매'}

대칭차는 ^ 연산자를 통해 구할 수도 있다.

코드 5-73 ^ 연산자로 대칭차 구하기

>>> 들짐승 ^ 날짐승  # 들짐승과 날짐승의 대칭차
{'사자', '늑대', '곰', '독수리', '매'}

부분집합 검사하기

집합 B의 모든 원소가 집합 A에 포함되어 있으면, 집합 B는 집합 A의 부분집합(subset)이다.

그림 5-11 B는 A의 부분집합

그림 5-11 B는 A의 부분집합

한 집합이 다른 집합의 부분집합인지 검사할 때는 issubset(다른집합) 메서드를 사용한다.

코드 5-74 부분집합 검사하기

>>> 들짐승.issubset(짐승)    # 들짐승이 짐승의 부분집합인가?
True

>>> 들짐승.issubset(날짐승)  # 들짐승이 날짐승의 부분집합인가?
False

부분집합은 비교 연산자(<=)를 통해 구할 수도 있다.

코드 5-75 부분집합 검사하기

>>> 들짐승 <= 짐승
True

>>> 들짐승 <= 날짐승
False

부분집합의 반대인 상위집합(superset)이라는 개념도 있다. 상위집합을 검사할 때는 issuperset() 메서드 또는 >= 연산자를 사용할 수 있다. 상위집합보다는 부분집합이 더 일반적으로 사용되는 개념이므로, 특별한 이유가 없다면 부분집합을 사용하자.

서로소 집합 검사하기

집합 A와 집합 B에 공통된 원소가 하나도 없으면, 두 집합은 서로소 집합(disjoint set)이다.

그림 5-12 A와 B는 서로소 집합

그림 5-12 A와 B는 서로소 집합

서로소 집합을 검사하려면 isdisjoint(다른집합) 메서드를 사용한다.

코드 5-76 서로소 집합 검사하기

>>> 들짐승.isdisjoint(날짐승)
False

>>> 들짐승.isdisjoint(바다생물)
True

원소 변경하기

집합은 수정 가능한 데이터다. 이미 정의한 집합에 원소를 추가하거나 삭제할 수 있다. 다음과 같은 메서드를 사용한다.

  • add(데이터): 데이터를 원소로 추가
  • discard(원소): 원소 제거
  • remove(원소): 원소 제거 (원소가 없으면 오류)
  • pop(): 아무 원소나 꺼낸다 (집합이 비어있으면 오류)
  • clear(): 모든 원소를 제거

대화식 셸에서 메서드를 사용해 보자.

코드 5-77 집합 원소 수정하기

>>> 들짐승.add('인간')      # 들짐승에 인간 추가
>>> 들짐승
{'사자', '늑대', '곰', '인간', '박쥐'}

>>> 들짐승.discard('인간')  # 인간 제거
>>> 들짐승
{'사자', '늑대', '곰', '박쥐'}

>>> 들짐승.remove('곰')     # 곰 제거
>>> 들짐승
{'사자', '늑대', '박쥐'}

>>> 들짐승.pop()            # 아무 원소나 꺼내기
'사자'

>>> 들짐승
{'늑대', '박쥐'}

>>> 들짐승.clear()          # 모든 원소 제거
>>> 들짐승
set()

이상으로 집합으로 수행할 수 있는 다양한 연산을 알아보았다. 수학의 집합론은 프로그래밍에서도 자주 활용되는 개념이다. 어떤 문제를 해결하는 데 집합 연산이 필요하다면 파이썬의 집합 연산을 활용해 계산할 수 있다.

연습문제

연습문제 5-15 일하는 날

is_working_day() 함수를 정의하라. 이 함수는 요일을 입력받아 그 요일이 여러분이 쉬는 날이면 True, 아니면 False를 반환한다.

연습문제 5-16 집합 계산 1

0 이상 1000 미만의, 3과 4의 공배수는 모두 몇 개인지 계산하라.

연습문제 5-17 집합 계산 2

0 이상 1000 미만의, 3의 배수이되 4의 배수는 아닌 수는 모두 몇 개인지 구하라.

5.4.5 프로즌셋

집합은 리스트, 사전처럼 수정이 가능한 데이터다. 한 번 집합을 정의한 뒤에도 원소를 추가, 삭제할 수 있다.

리스트에 튜플이 있는 것처럼, 파이썬은 수정이 불가능한 집합도 제공한다. 프로즌셋(frozenset)이 그것이다. 프로즌셋은 집합과 사용 방법이 거의 동일하고 연산과 메서드도 공유한다. 단, 내용을 수정하는 연산과 메서드는 지원하지 않는다.

프로즌셋을 정의하는 방법은 다음과 같다.

  • 공집합: frozenset()
  • 집합을 변환: frozenset({원소 1, 원소 2, 원소 3, ...})
  • 시퀀스를 변환: frozenset([원소 1, 원소 2, 원소 3, ...])

프로즌셋의 연산과 메서드는 집합과 거의 동일하므로 설명을 생략한다.