처음부터 차근차근 파이썬 자세히보기

Python-기본/Python-데이터 타입 및 자료 구조

Python-컬렉션 데이터 타입(1. 셋)

윤빵빵영 2021. 1. 5. 09:15

컬렉션 자료구조는 시퀀스 자료구조와 달리 데이터를 서로 연관시키지 않고 모아두는 컨테이너와 같습니다. 컬렉션 자료구조는 시퀀스 자료구조에서 사용할 수 있었던 속성 중 세 가지 속성을 지닙니다.

파이썬 내장 컬렉션 타입의 속성
1. 멤버십 연산 in
2. 크기 함수 len(col)
3. 반복성(iterable)

즉, 시퀀스 자료구조의 인덱싱/슬라이싱을 제외한 나머지 속성을 사용할 수 있습니다.

파이썬 내장 컬렉션 자료형에는 셋(집합, set)과 딕셔너리(dictionary)가 있습니다. 이번 글에서는 파이썬의 셋에 대한 기본적인 운용을 다뤄보도록 하겠습니다.


목차

1. 셋 선언
2. 자료형 변환 set( )
3. 컬렉션 데이터 타입 속성-셋
  (1). 멤버십 연산 in
  (2). 크기 함수 len(seq)
  (3). 반복성(iterable)
4. 셋 메서드와 연산자
  (1) add( )
  (2) update( )
  (3) union( )
  (4) intersection( )
  (5) difference( )
  (6) symmetric_difference( )
  (7) clear( )
  (8) discard( ), remove( ), pop( )
  (9) issubset( )


파이썬의 셋은 반복 가능(iterable)하고 가변(mutable)이며 중복 요소가 없고 정렬되지 않은 컬렉션 자료형입니다. 앞서 언급했듯 인덱스 연산은 할 수 없으며 멤버십 테스트 및 중복 항목 제거에 주로 사용되는 자료구조입니다.

셋의 운용과 관련해서 삽입 시간복잡도는 (정렬되지 않았으므로) O(1)이고 수학에서 익히 다뤘듯 합집합과 교집합 연산이 가능합니다. 합잡합의 시간 복잡도는 O(m+n), 교집합의 경우 두 셋 중 더 작은 셋에 대해서만 계산하므로 시간 복잡도는 O(n)입니다.

 

1. 셋 선언

셋은 일반정으로 중괄호 { }를 이용하여 선언합니다. 

even_number = {0, 2, 4, 6, 8}
print(even_number)
print(type(even_number))

결과)

{0, 2, 4, 6, 8}
<class 'set'>


주의해야될 부분은 비어 있는 셋을 선언할 때는 { }로 하면 안됩니다. { }은 비어있는 딕셔너리(dictionary)로 다음 글에서 다룰 자료형이 됩니다. 비어 있는 셋을 선언할 때는 set( ) 함수를 이용해야 합니다.

empty_dictionary = { }
empty_set = set( )
print(empty_dictionary)
print(type(empty_dictionary))
print(empty_set)
print(type(empty_set))

결과)

{}
<class 'dict'>
set()
<class 'set'>

 

2. 자료형 변환 set( )

반복 가능한 자료형은 셋 자료형으로 변환할 수 있습니다. 문자열, 튜플, 리스트, 심지어 다음 글에서 다룰 딕셔너리까지 셋 자료형으로 변환할 수 있으며, 이때 중복된 값은 버려집니다.

lett = 'letters'
lett_set = set(lett)
print(lett_set)

결과)

{'r', 'e', 's', 't', 'l'}


위의 결과에서 중복된 문자인 e와 t는 하나만 들어있다는 것에 주목하시면 됩니다. 

name_list = ['전영근', '이명현', '김태영', '윤영', '윤영', '강정규', '홍진우', '전현우']
name_set = set(name_list)
print(name_set)

결과)

{'이명현', '김태영', '윤영', '강정규', '전현우', '홍진우', '전영근'}


다음 글에서 다룰 딕셔너리도 셋으로 변환 가능합니다. 이때 딕셔너리의  키만 수집됩니다.

name_dictionary = {'icp': '윤영', '302': '김현수', '302': '명경규'}
name_set = set(name_dictionary)
print(name_set)

결과)

{'302', 'icp'}

 

3. 컬렉션 데이터 타입 속성-셋

  (1). 멤버십 연산 in

객체 in 셋은 앞의 해당 객체가 셋 내에서 검색되는지를 확인합니다. 검색이 된다면 True를, 검색이 되지 않는다면 False를 반환합니다.

icp = {'이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
print('윤영' in icp)

결과)

True


멤버십 연산은 셋을 이용하는 가장 일반적인 사용처입니다. 

drinks = {
'martini' : {'vodka', 'vermouth'},
'black russian' : {'vodka', 'kahlua'},
'white russian' : {'cream', 'kahlua', 'vodka'},
'manhattan' : {'rye', 'vermouth', 'bitters'},
'screwdriver' : {'orange juice', 'vodka'}
}
for name, contents in drinks.items( ):
    if 'vodka' in contents:
        print(name) 

결과)

martini
black russian
white russian
screwdriver


drinks는 key와 value로 구성되어 있는 딕셔너리 객체입니다. key에는 칵테일 이름이, value에는 해당 칵테일에 사용되는 재료들이 셋의 자료형으로 들어와 있는 상태입니다. 

for name, contents in drinks.items( ):

이 부분은 name과 contents를 drinks 딕셔너리의 key와 value에 각각 할당하는 언패킹입니다. 즉, 첫 번째 순서로

name = 'martini', contents = {'vodka', 'vermouth'}

가 할당됩니다. 다음으로 조건문 

if 'vodka' in contents:

를 판단하며 이때 문자열 'vodka'가 contents에서 검색되는 지를 확인합니다. 확인되면

print(name)

name인 'martini'를 출력합니다. for 문이 가장 앞에 있으므로 그 다음 반복을 시행합니다. 이와 같이 딕셔너리와 셋을 이용하면 특정 값을 갖고 있는 객체가 어떤 것인지를 검색할 수 있습니다.

 

  (2). 크기 함수 len(seq)

크기 함수 len(col)는 컬렉션의 길이를 정수로 반환합니다. 

name_set = {'전영근', '이명현', '김태영', '윤영', '윤영', '강정규', '홍진우', '전현우'}
print(len(name_set))

결과)

7

 

  (3). 반복성(iterable)

셋을 이용하여 for이나 while과 같은 구문을 이용한 반복 처리가 가능합니다. 

name_set = {'전영근', '이명현', '김태영', '윤영', '윤영', '강정규', '홍진우', '전현우'}
for name in name_set:

    print(name)

결과)

전영근
홍진우
전현우
김태영
강정규
이명현
윤영


셋은 위치가 지정되어 있지 않은 배열이기 때문에 위의 소스코드를 시행할 때마다 다른 결과를 얻게 됩니다.

 

4. 셋 메서드와 연산자

add( ), update( ), union( ), intersection( ), difference( ), clear( ), discard( ), remove( ), pop( )

  (1) add( )

A.add(x): 셋 A에 객체 x가 없을 경우 추가합니다.

icp = {'이명현', '김태영', '윤영', '강정규', '홍진우'}
print(id(icp))
icp.add('전현우')
print(icp)
print(id(icp))

결과)

1870707821256
{'홍진우', '강정규', '윤영', '전현우', '김태영', '이명현'}
1870707821256


셋은 가변 자료형이기 때문에 add( ) 메서드를 이용한 자료 삽입 이후에도 id( ) 함수의 결과가 같은 것을 확인할 수 있습니다. 즉, add( ) 메서드는 사용되는 셋 그 자체에 자료를 추가합니다.

 

  (2) update( )

A.update(B): B를 A에 추가합니다. 합집합 연산으로 A |= B와같이 |= 연산자를 이용할 수 있습니다.

icp = {'이명현', '김태영', '윤영', '강정규', '홍진우'}
mipro = {'전영근', '김현수', '명경규', '김태영'}
print(id(icp))
icp.update(mipro)
print(icp)
print(id(icp))

결과)

2186256583368
{'홍진우', '윤영', '김현수', '이명현', '김태영', '명경규', '전영근', '강정규'}
2186256583368


icp에 mipro를 추가하여 icp의 원소가 달라졌어도 icp의 id( ) 값은 변하지 않았음에 주목해야 합니다. 즉, icp 셋 그 자체에 mipro의 원소들을 추가하는 것입니다. 또한 중복된 값은 자동적으로 제거됨을 확인할 수 있습니다.

icp = {'이명현', '김태영', '윤영', '강정규', '홍진우'}
mipro = {'전영근', '김현수', '명경규', '김태영'}
icp |= mipro
print(icp)

결과)

{'홍진우', '김현수', '김태영', '강정규', '윤영', '전영근', '이명현', '명경규'}

 

  (3) union( )

A.union(B): update( ) 메서드와 같지만 연산 결과를 복사본으로 반환합니다. A | B와 동일합니다.

icp = {'이명현', '김태영', '윤영', '강정규', '홍진우'}
mipro = {'전영근', '김현수', '명경규', '김태영'}
print(id(icp))
new_group = icp.union(mipro)
print(icp)
print(id(icp))
print(new_group)
print(id(new_group))

결과)

2108623489736
{'홍진우', '윤영', '이명현', '김태영', '강정규'}
2108623489736
{'홍진우', '명경규', '김현수', '윤영', '이명현', '김태영', '강정규', '전영근'}
2108625178184


icp.union(mipro) 메서드를 실행하더라도 icp 셋에는 변화가 없으며, 새로 할당된 변수인 new_group이 합집합 결과임을 알 수 있습니다. 또한 icp와 new_group의 id( ) 값이 다르다는 것에 주목해야 합니다.

 

  (4) intersection( )

A.intersection(B): A와 B의 교집합의 복사본을 반환합니다. A & B와 동일합니다.

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
group302 = {'전영근', '김태영', '윤영', '김현수', '명경규'}
print(id(icp))
cross_group = icp.intersection(group302)
print(icp)
print(id(icp))
print(cross_group)
print(id(cross_group))

결과)

1190209325768
{'전영근', '김태영', '홍진우', '윤영', '전현우', '이명현', '강정규'}
1190209325768
{'전영근', '김태영', '윤영'}
1190211014216

 

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
group302 = {'전영근', '김태영', '윤영', '김현수', '명경규'}
cross_group = icp & group302
print(cross_group)

결과)

{'전영근', '윤영', '김태영'}

 

  (5) difference( )

A.difference(B): A와 B의 차집합의 복사본을 반환합니다. A - B와 동일합니다.

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
group302 = {'전영근', '김태영', '윤영', '김현수', '명경규'}
print(id(icp))
diff_group = icp.difference(group302)
print(icp)
print(id(icp))
print(diff_group)
print(id(diff_group))

결과)

1779400313544
{'윤영', '강정규', '홍진우', '전현우', '전영근', '김태영', '이명현'}
1779400313544
{'이명현', '전현우', '강정규', '홍진우'}
1779402001992

 

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
group302 = {'전영근', '김태영', '윤영', '김현수', '명경규'}
diff_group = icp - group302
print(diff_group)

결과)

{'홍진우', '전현우', '강정규', '이명현'}

 

  (6) symmetric_difference( )

A.symmetric_difference(B): A와 B의 각 원소 중 중복(교집합)을 제외한 집합의 복사본을 반환합니다. A ^ B와 동일합니다.

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
group302 = {'전영근', '김태영', '윤영', '김현수', '명경규'}
print(id(icp))
clear_group = icp.symmetric_difference(group302)
print(icp)
print(id(icp))
print(clear_group)
print(id(clear_group))

결과)

1771451321032
{'전영근', '강정규', '이명현', '윤영', '김태영', '전현우', '홍진우'}
1771451321032
{'이명현', '강정규', '명경규', '전현우', '홍진우', '김현수'}
1771453009480

 

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
group302 = {'전영근', '김태영', '윤영', '김현수', '명경규'}
clear_group = icp ^ group302
print(clear_group)

결과)

{'강정규', '이명현', '홍진우', '명경규', '김현수', '전현우'}

 

  (7) clear( )

A.clear( ): 셋 A의 모든 항목을 제거합니다.

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
print(id(icp))
icp.clear( )
print(icp)
print(id(icp))

결과)

1864805059272
set()
1864805059272


clear( ) 메서드는 셋 자체에서 삭제 작업을 하므로 위의 소스코드에서 icp 내 모든 값이 삭제되며, id( ) 값도 변하지 않습니다.

 

  (8) discard( ), remove( ), pop( )

A.discard(x): A의 항목 x를 제거하며 반환값은 없습니다. 

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
print(id(icp))
order = icp.discard('윤영')
print(order)
print(icp)
print(id(icp))
icp.discard('김현수')
print(icp)

결과)

1639175555784
None
{'이명현', '김태영', '전현우', '강정규', '홍진우', '전영근'}
1639175555784
{'이명현', '김태영', '전현우', '강정규', '홍진우', '전영근'}


discard( ) 메서드는 대상이 되는 셋 자체에서 시행되는 메서드이므로 icp.discard('윤영')이 실행된 결과를 order에 할당한 뒤 그 결과를 보기 위해 order를 출력하면 None을 얻을 수 있습니다. 반환값이 없다는 것은 이를 두고 한 말입니다. 앞에서 셋 자체에 시행되는 메서드들은 모두 이와 같습니다. 연산 결과를 복사본으로 반환하는 메서드들은 결과 그 자체가 새로운 변수에 할당됩니다. 또한 셋 내에 해당 항목이 없어도(icp.discard('김현수')) 시행 가능한 메서드임에 주목해야 합니다.

A.remove(x): A.discard(x)와 같지만 항목 x가 없을 경우 KeyError 예외를 발생시킵니다.

icp = {'전영근', '이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
print(id(icp))
order = icp.remove('윤영')
print(order)
print(icp)
print(id(icp))
icp.remove('김현수')
print(icp)

결과)

icp.remove('김현수')
KeyError: '김현수'
2940453259976
None
{'전영근', '김태영', '홍진우', '전현우', '이명현', '강정규'}
2940453259976


A.pop( ): A에서 한 항목을 무작위로 제거하고 그 항목을 반환합니다. 셋이 비어있으면 KeyError 예외를 발생시킵니다.

name_set = {'윤영', '김현수', '명경규'}
name_set.pop(
)
print(name_set)
name_set.pop( )
print(name_set)

name_set.pop( )
print(name_set)

name_set.pop( )
print(name_set)

결과)

{'김현수', '명경규'}
{'명경규'}
set()


  (9) issubset( )

A.issubset(B): A가 B의 부분집합(subset)인지를 판별합니다. 맞으면 True, 아니면 False를 반환합니다. A <= B와 동일합니다.

icp = {'이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
group302 = {'전영근', '김태영', '윤영', '김현수', '명경규'}
both_group = {'김태영', '윤영'}
print(group302.issubset(icp)
)
print(both_group.issubset(icp))

결과)

False
True

 

icp = {'이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
group302 = {'전영근', '김태영', '윤영', '김현수', '명경규'}
both_group = {'김태영', '윤영'}
print(group302 <= icp
)
print(both_group <= icp)

결과)

False 
True

 

관련 연산자로 A < B, A >= B, A > B가 있습니다.

A < B: A가 B의 진부분집합임을 판별합니다. 진부분집합이란, 부분집합이되 동일하지 않은 집합을 말합니다.

icp = {'이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
some_mipro = {'이명현', '김태영', '윤영', '강정규', '홍진우', '전현우'}
both_group = {'김태영', '윤영'}
print(some_mipro < icp
)
print(both_group < icp)

결과)

False 
True


A >= B: A <= B의 반대입니다. B가 A의 부분집합임을 판별합니다.

A > B: A < B의 반대입니다. B가 A의 진부분집합임을 판별합니다.