파이썬 기초 강좌 #11 세트 (Set)

16 분 소요

안녕하세요. 스쿨오브웹의 이상희입니다. 초등학교 때 산수시간에 집합에 대해서 배운 것을 기억하시나요? 학교를 졸업하신지 얼마 되지 않았다면 선명하게 기억을 하고 계실테고, 학교를 졸업하신지 오래 되셨다면 기억이 가물가물하실텐데요, 아무리 기억이 잘 나지않아도 분명히 합집합, 교집합, 여집합, 차집합과 같은 여러가지 집합의 형태를 배우셨을겁니다. 파이썬에도 이런 집합과 같은 데이터스트럭쳐를 제공하는데, 그것이 세트입니다. 그럼 지금부터 세트에 대해서 공부해보도록 하겠습니다.

먼저 세트 오브젝트를 생성하는 방법을 알아보겠습니다. 세트를 생성하는 방법은 2가지가 있습니다.

첫번째 방법은 세트 컨스트럭터를 사용하는 방법입니다. 세트 컨스트럭터는 시퀀스 타입의 오브젝트를 하나만 인자로 받으며, 만약 인자를 받지 않으면 빈 세트 오브젝트를 리턴합니다.

리스트를 컨스트럭터의 인자로 전달하여 세트를 생성한 뒤, 타입 함수를 사용하여 오브젝트 타입을 확인해보겠습니다.

>>> set_1 = set([1, 2, 3])
>>> print(set_1)
{1, 2, 3}
>>> print(type(set_1))
<class 'set'>

세트 오브젝트가 생성된 것을 확인하였습니다.

이 번에는 튜플을 인자로 전달하여 생성해보겠습니다.

>>> set_2 = set((1, 2, 3))
>>> print(set_2)
{1, 2, 3}
>>> print(type(set_2))
<class 'set'>

리스트를 사용했을 때와 같이 세트 오브젝트가 생성되었습니다.

이 번에는 스트링 오브젝트와 레인지 오브젝트를 인자로 전달해보겠습니다.

>>> set_3 = set('string')
>>> print(set_3)
{'s', 't', 'n', 'r', 'i', 'g'}
>>> print(type(set_3))
<class 'set'>
>>> set_4 = set(range(10))
>>> print(set_4)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> print(type(set_4))
<class 'set'>

스트링과 레이지 오브젝트를 사용하여 각각의 세트가 생성되었습니다.

이 번에는 컨스트럭터를 인자 없이 실행하여 빈 세트를 생성해보겠습니다.

>>> set_5 = set()
>>> print(set_5)
set()
>>> print(type(set_5))
<class 'set'>

빈 세트가 생성되었습니다.

앞에서도 설명 드렸듯이 세트 컨스트럭터는 리스트, 튜플, 스트링, 레인지 등과 같은 시퀀스 타입의 오브젝트를 하나만 인자로 받습니다. 만약에 시퀀스 타입이 아닌 오브젝트를 전달하면 어떻게 되는지 확인해보도록하겠습니다.

시퀀스 오브젝트가 아닌 각각의 숫자 1, 2, 3 을 인자로 전달해 보겠습니다.

>>> set(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: set expected at most 1 argument, got 3

세트는 1개의 인자만을 받는데, 3개의 인자가 전달 되었다는 에러가 발생하는 것을 볼 수 있습니다.

이번에는 int 오브젝트 하나만 인자로 전달해 보겠습니다.

>>> set(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable

int는 iterable 오브젝트가 아니라는 에러가 발생했습니다.

세트를 생성하는 두번째 방법은 컬리브레이스를 사용하는 방법입니다. 컬리브레이스를 사용하는 방식은 리스트나 튜플을 생성하는 방식과 비슷합니다.

컬리브레이스를 사용하여 세트를 생성해보겠습니다.

>>> set_6 = {1, 2, 3}
>>> print(set_6)
{1, 2, 3}
>>> print(type(set_6))
<class 'set'>

컨스트럭터를 사용했을 때와 같이 세트가 생성되었습니다. 그런데 빈 세트를 생성하기 위해서 빈 컬리브레이스를 사용하시면 어떻게 될까요? 그렇습니다. 딕셔너리 강좌에서 배웠듯이 빈 컬리브레이스를 사용하면 빈 딕셔너리가 생성됩니다.

실제로 실행을 해보겠습니다.

>>> var = {}
>>> print(var)
{}
>>> print(type(var))
<class 'dict'>

빈 컬리브레이스를 사용하니 빈 세트가 아닌 빈 딕셔너리가 생성된것을 볼 수 있습니다. 이 점 꼭 기억하시기 바랍니다.

이번에는 세트의 특징에 대해서 알아보겠습니다.

세트의 첫 번째 특징입니다. 세트는 순서가 없는 데이터 타입입니다.

세트를 하나 생성하여 확인해보겠습니다.

>>> my_set = {'one', 'two', 'three'}
>>> my_set
{'three', 'one', 'two'}

아이템의 순서가 정의한 순서와는 다르게 생성된 것을 볼 수 있습니다.

새로운 아이템 하나를 추가해보겠습니다.

>>> my_set.add('four')
>>> my_set
{'four', 'three', 'one', 'two'}

새로운 아이템이 마지막 위치가 아닌 위치에 추가된 것을 볼 수 있습니다.

세트의 두번째 특징입니다. 세트는 순서가 없는 데이터타입이기 때문에 인덱스를 사용할 수 없습니다.

인덱스를 사용해 보겠습니다.

>>> my_set[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'set' object is not subscriptable

세트 오브젝트는 인덱싱을 지원하지 않는다는 에러 메세지가 출력됐습니다.

세트의 세번째 특징입니다. 세트는 같은 값을 가진 아이템을 하나만 허용합니다.

같은 값의 숫자 두개를 사용하여 세트를 생성해보겠습니다.

>>> my_set = {1, 2, 2, 3}
>>> my_set
{1, 2, 3}

숫자 2가 하나만 남은 것을 볼 수 있습니다.

지금 생성한 세트 오브젝트는 1의 값을 가진 아이템을 가지고 있는데 새로운 1을 add 메소드를 사용하여 추가해보겠습니다.

>>> my_set.add(1)
>>> my_set
{1, 2, 3}

숫자 1은 세트안에 이미 존재하기 때문에 추가되지 않은 것을 볼 수 있습니다. 세트는 중복된 값을 제외시키기 때문에 어떤 데이터에서 유일값만을 추출하는데 유용하게 사용됩니다.

세트의 네 번째 특징입니다. 세트는 한 번 정의하면 변경이 불가능한 이뮤터블 (immutable) 타입만을 아이템으로 사용할 수 있습니다. 이뮤터블 타입에는 boolean, int, float, tuple, str, frozenset 등이 있습니다. 반대로 변경이 가능한 뮤터블 타입에는 list, set, dictionary 등이 있습니다. 그러므로 list, set, dictionary는 세트의 아이템으로 사용이 불가능합니다.

이뮤터블 타입인 튜플을 아이템으로 가진 세트를 정의해보겠습니다.

>>> {(1, 2, 3), (2, 3, 4)}
{(1, 2, 3), (2, 3, 4)}

튜플을 사용하여 문제없이 생성되는 것을 확인하였습니다.

이번에는 뮤터블 타입인 리스트를 아이템으로 사용하여 정의해보겠습니다.

>>> {[1, 2, 3], [2, 3, 4]}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

리스트는 언해셔블 타입이라는 타입에러가 발생했습니다. 언해셔블 타입은 뮤터블 타입과 같다고 생각하시면 됩니다.

마지막으로 세트 클래스가 제공하는 메소드를 살펴보도록 하겠습니다.

헬프 명령어를 사용하여 확인해보겠습니다.

>>> help(set)
Help on class set in module builtins:
 
class set(object)
|  set() -> new empty set object
|  set(iterable) -> new set object
|
|  Build an unordered collection of unique elements.
|
|  Methods defined here:
|
|  __and__(self, value, /)
|      Return self&value.
|
|  __contains__(...)
|      x.__contains__(y) <==> y in x.
|
|  __eq__(self, value, /)
|      Return self==value.
|
|  __ge__(self, value, /)
|      Return self>=value.
|
|  __getattribute__(self, name, /)
|      Return getattr(self, name).
|
|  __gt__(self, value, /)
|      Return self>value.
|
|  __iand__(self, value, /)
|      Return self&=value.
|
|  __init__(self, /, *args, **kwargs)
|      Initialize self.  See help(type(self)) for accurate signature.
|
|  __ior__(self, value, /)
|      Return self|=value.
|
|  __isub__(self, value, /)
|      Return self-=value.
|
|  __iter__(self, /)
|      Implement iter(self).
|
|  __ixor__(self, value, /)
|      Return self^=value.
|
|  __le__(self, value, /)
|      Return self<=value.
|
|  __len__(self, /)
|      Return len(self).
|
|  __lt__(self, value, /)
|      Return self<value.
|
|  __ne__(self, value, /)
|      Return self!=value.
|
|  __or__(self, value, /)
|      Return self|value.
|
|  __rand__(self, value, /)
|      Return value&self.
|
|  __reduce__(...)
|      Return state information for pickling.
|
|  __repr__(self, /)
|      Return repr(self).
|
|  __ror__(self, value, /)
|      Return value|self.
|
|  __rsub__(self, value, /)
|      Return value-self.
|
|  __rxor__(self, value, /)
|      Return value^self.
|
|  __sizeof__(...)
|      S.__sizeof__() -> size of S in memory, in bytes
|
|  __sub__(self, value, /)
|      Return self-value.
|
|  __xor__(self, value, /)
|      Return self^value.
|
|  add(...)
|      Add an element to a set.
|
|      This has no effect if the element is already present.
|
|  clear(...)
|      Remove all elements from this set.
|
|  copy(...)
|      Return a shallow copy of a set.
|
|  difference(...)
|      Return the difference of two or more sets as a new set.
|
|      (i.e. all elements that are in this set but not the others.)
|
|  difference_update(...)
|      Remove all elements of another set from this set.
|
|  discard(...)
|      Remove an element from a set if it is a member.
|
|      If the element is not a member, do nothing.
|
|  intersection(...)
|      Return the intersection of two sets as a new set.
|
|      (i.e. all elements that are in both sets.)
|
|  intersection_update(...)
|      Update a set with the intersection of itself and another.
|
|  isdisjoint(...)
|      Return True if two sets have a null intersection.
|
|  issubset(...)
|      Report whether another set contains this set.
|
|  issuperset(...)
|      Report whether this set contains another set.
|
|  pop(...)
|      Remove and return an arbitrary set element.
|      Raises KeyError if the set is empty.
|
|  remove(...)
|      Remove an element from a set; it must be a member.
|
|      If the element is not a member, raise a KeyError.
|
|  symmetric_difference(...)
|      Return the symmetric difference of two sets as a new set.
|
|      (i.e. all elements that are in exactly one of the sets.)
|
|  symmetric_difference_update(...)
|      Update a set with the symmetric difference of itself and another.
|
|  union(...)
|      Return the union of sets as a new set.
|
|      (i.e. all elements that are in either set.)
|
|  update(...)
|      Update a set with the union of itself and others.
|
|  ----------------------------------------------------------------------
|  Class methods defined here:
|
|  __class_getitem__(...) from builtins.type
|      See PEP 585
|
|  ----------------------------------------------------------------------
|  Static methods defined here:
|
|  __new__(*args, **kwargs) from builtins.type
|      Create and return a new object.  See help(type) for accurate signature.
|
|  ----------------------------------------------------------------------
|  Data and other attributes defined here:
|
|  __hash__ = None

세트 클래스는 많은 메소드를 제공하고 있는데, 이 중에서 가장 많이 쓰이는 메소드 몇개의 사용법을 배워보겠습니다.

먼저 add 메소드를 살펴보겠습니다.

>>> help(set.add)
Help on method_descriptor:
 
add(...)
Add an element to a set.
 
This has no effect if the element is already present.

add 메소드는 세트에 아이템을 추가할 때 사용하며, 앞에서 설명 드린대로 이미 존재하는 값은 중복되어 추가되지 않습니다.

빈 세트를 하나 생성한뒤 add 메소드를 사용하여 아이템을 추가해 보겠습니다.

>>> myset = set()
>>> print(myset)
set()
>>>
>>> myset.add(1)
>>> print(myset)
{1}
>>>
>>> myset.add(2)
>>> print(myset)
{1, 2}

빈 세트에 1과 2가 추가 된것을 확인하였습니다.

다음은 pop 메소드입니다.

>>> help(set.pop)
Help on method_descriptor:
 
pop(...)
Remove and return an arbitrary set element.
Raises KeyError if the set is empty.

pop 메소드는 임의의 아이템을 삭제하며, 만약 세트에 아이템이 존재하지 않으면 키에러를 발생시킵니다.

pop 메소드를 사용해 보겠습니다.

>>> myset.pop()
1
>>> print(myset)
{2}

아이템 1을 리턴하면서 세트 안에서 삭제된 것을 볼 수 있습니다.

한 번 더 실행해보겠습니다.

>>> myset.pop()
2
>>> print(myset)
set()

아이템 2가 리턴되면서 빈 세트가 남는 것을 볼 수 있습니다.

아이템이 없는 상태에서 pop 메소드를 실행해보겠습니다.

>>> myset.pop()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'pop from an empty set'

아이템이 없는 상태에서 pop 메소드를 실행하면 키 에러가 발생하는 것을 확인하였습니다.

다음은 remove 메소드에 대해서 알아보겠습니다.

>>> help(set.remove)
Help on method_descriptor:
 
remove(...)
Remove an element from a set; it must be a member.
 
    If the element is not a member, raise a KeyError.

remove 메소드도 pop 메소드와 같이 아이템을 삭제할 때 사용합니다. 하지만 팝 메소드는 인자를 받지않고 임의의 아이템을 삭제하는 반면에, remove 메소드는 삭제할 아이템을 인자로 받고, 받은 인자가 세트의 멤버 아이템이 아니면 키에러를 발생시킵니다.

세트를 다시 정의한 뒤 remove 메소드를 사용해보겠습니다.

>>> myset = {1, 2, 3}
>>> myset.remove(1)
>>> print(myset)
{2, 3}

remove 메소드를 사용하여 아이템 1이 삭제된 것을 확인하였습니다.

이번에는 아이템으로 가지고 있지 않는 값을 인자로 사용해보겠습니다.

>>> myset.remove(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 4

키에러가 발생하는 것을 확인하였습니다.

다음은 union 메소드에 대해서 알아보겠습니다.

>>> help(set.union)
Help on method_descriptor:
 
union(...)
Return the union of sets as a new set.
 
    (i.e. all elements that are in either set.)

앞에서 세트는 산수의 집합과 비슷한 데이터 타입이라고 설명드렸습니다. 두개 이상의 세트에 대하여 합집합, 교집합, 여집합과 같은 데이터를 추출해 낼 수가 있는데, 아래 그림과 같은 합집합을 추출할 때 사용되는 메소드가 유니온 메소드입니다.

합집합
합집합

먼저 유니온 메소드를 사용하기 위해서 세트 오브젝트를 두개 생성해보겠습니다.

>>> set_1 = {1, 2, 3}
>>> set_2 = {3, 4, 5}

union 메소드를 사용하여 두 세트의 합집합을 구해보겠습니다.

>>> set_1.union(set_2)
{1, 2, 3, 4, 5}

이번에는 아래 그림의 교집합과 같이 두 세트의 공통된 아이템만을 리턴하는 intersection 메소드를 사용해보겠습니다.

교집합
교집합

intersection 메소드를 사용하여 두 세트의 교집합을 출력해보겠습니다.

>>> set_1.intersection(set_2)
{3}

공통 아이템인 3만 리턴된것을 보셨습니다.

다음은 아래 그림의 차집합과 같이 하나의 세트에서 다른 세트의 아이템을 제거한 아이템만을 리턴하는 difference 메소드를 실행해보겠습니다.

차집합
차집합
>>> set_1.difference(set_2)
{1, 2}

세트 2의 아이템인 3이 제거된 1과 2만 리턴된 것을 볼 수 있습니다.

세트 클래스가 제공하는 메소드에는 지금 소개해드린 메소드 이외에도 편리한 메소드가 더 있으니 직접 사용해보시기 바랍니다. 세트는 리스트나 딕셔너리처럼 아주 자주 쓰이는 데이터 스트럭쳐는 아니지만, 필요할 때 아주 편리하게 사용되는 데이터 스트럭쳐이니 잘 익혀두시기 바랍니다.

다음 강좌에서는 프로그램을 제어하기 위해 가장 많이 쓰이는 제어문의 하나인 if 조건문에 대해서 배우도록 하겠습니다.

감사합니다.