콘텐츠로 건너뛰기
Home » Python101 : 다른 집합 유형들

Python101 : 다른 집합 유형들

지난 글에서 리스트의 특징에 대해서 언급할 때나, 리스트 축약에 대해서 이야기하면서 “연속열”이나 “반복가능”이라는 단어를 사용한 적이 있다. 사실 파이썬에 연속열이나 반복객체 혹은 반복가능객체는 이제는 거의 공식적으로 사용되는 개념이다. 리스트는 이런 “연속열”과 “반복가능”에 공통적으로 속하는 유형의 데이터 타입인 것이다. 이번 글에서는 그럼 리스트 외에 어떤 다른 집합형 데이터 타입이 있는지 알아보고 그 면면을 좀 살펴보기로 하겠다.

값의 타입

일단 값의 타입을 확인하는 방법을 알아보자. 사실 여기서 말하는 타입들은 아주 “대표적인” 파이썬 타입들이며, 사실 파이썬에는 엄청나게 많은 데이터 유형이 존재한다. (사실 어떤 데이터의 타입을 표현하는 값 자체도 Type 이라는 타입을 가진다.) 파이썬에서 특정한 값의 타입을 확인하는 방법으로는 type() 함수가 있다. 이 함수는 인자로 전달된 값의 타입값 (타입을 나나태는 Type 객체의 값)을 리턴한다. 한편, 특정한 객체가 특정한 타입인지를 확인하고 싶다면 type(a) 를 사용하기 보다는 isintance(a, b) 를 사용하면 된다. 이 함수는 객체 a 가 타입 b에 속하는지를 검사하는 함수이다.

튜플

튜플(tuple)은 변경 불가능한 리스트로 생각할 수 있다. 원소들은 순서대로 나열되어 정수 인덱스 및 슬라이싱을 사용하여 참조할 수 있지만, 한 번 생성된 튜플은 원소를 추가/제거/변경하는 것이 허용되지 않는다.

  • 튜플은 괄호안에 각 원소를 콤마로 구분하여 생성한다 : (1, 2)
  • 원소가 1개인 튜플을 만들 때에는 콤마를 반드시 입력해야 한다. : (1,) ( (1)이라고 쓰면 그냥 1과 같음)
  • 빈 튜플 : (, )
  • 연속열이나 반복가능 객체를 tuple() 함수에 전달하여 생성할 수 있다. : tuple(range(3)) == (0, 1, 2)

튜플은 변경불가능(immutable)한 집합이다. 따라서 제공되는 메소드 역시 t.index(a), t.count(a) 정도로 단순하다. 또 원소의 추가에 따른 메모리 재할당등의 동작이 필요없기 때문에, 변경될 필요가 없는 배열과 같은 데이터가 필요하다면 리스트보다는 튜플을 사용하는 것이 권장된다. 튜플 그 자체는 변경 불가능하지만, 튜플의 각 원소가 변경 가능하다면 그에 대한 조작은 허용된다.

리스트 축약 문법을 괄호(( ... ))로 둘러싸서 표현하는 것은 튜플을 생성하지 않는다. 대신 이는 제너레이터 객체이다.

집합 (Set)

리스트가 원소들을 순서대로 줄지어 놓은 것에 비해 set는 이 원소들 사이에 딱히 순서를 부여하지는 않는다. 1 파이썬 3.8부터 “순서가 없는 집합”들의 키 관련 구현이 조금 달라지면서 원래는 순서가 없던 set나 dict의 원소들의 순서가 유지되도록 하는 쪽이 성능에 더 도움이 되는 것으로 알려져 그 이후 버전의 파이썬에서 set, dict의 원소들은 for 문 등으로 순회시에 추가된 순서를 기억하게 된다. 따라서 정수 인덱스를 사용해서 각각의 원소를 참조할 수 없다.

  • 같은 값의 원소가 중복으로 포함될 수 없다.
  • 특정 원소가 있는지 검사하는 것이 빠르다. (해시에 의해 체크되므로 O_1(n)이다.)
  • 인덱스를 통해 개별 원소를 액세스하지 못한다.
  • 합집합, 교집합, 차집합 등 수학의 집합 연산을 지원한다.

집합은 각각의 원소에 대해 정수 인덱스가 아닌 해시값을 통해 액세스한다. 따라서 특정 원소의 포함 여부를 검사하는 연산(조금 있는 척해서 ‘멤버십 검사’라고 함)이 매우 빠르다. 그런데 이 때 사용되는 해시 연산은 리스트나 사전, 집합과 같은 변경 가능한 객체에는 적용되지 않는다. 따라서 이들 타입의 객체들은 리스트나 튜플의 원소는 될 수 있지만 집합의 원소는 될 수 없다.

집합은 {1, 2, 3} 처럼 중괄호를 사용한 리터럴을 사용하거나, set() 함수에 연속열/반복가능 객체를 전달하여 생성할 수 있다. 중복된 원소는 1개만 저장된다.

  • a.difference(b) : 집합 a 에서 집합 b와 공통인 부분을 제외한 차집합을 구한다. a - b 와 같다.
  • a.union(b) : 집합 a, b의 합집합을 구한다. a + b 와 같다.
  • a.intersection(b) : 집합 a, b의 교집합을 구한다.
  • a.symmetric_difference(b) : 집합 a, b의 대칭 차집합을 구한다 (a - b) + (b - a) 와 같다.
  • 교집합, 차집합, 대칭차집합을 구하는 메소드에는 _update 접미사가 붙은 메소드들이 있다. 이 메소드들은 원래 집합을 업데이트하며, None을 리턴한다.

참고로 빈 중괄호를 사용하는 것은 빈 집합이 아닌 빈 사전을 생성한다. 빈 집합을 만들려면 set() 함수를 인자 없이 호출하면 된다. (다른 집합 타입들도 마찬가지로 인자를 주지 않고 생성함수를 호출하면 빈 객체가 만들어진다.)

사전

사전은 정수 인덱스 대신 사용자가 정한 별도의 키를 통해서 데이터를 저장하고 찾는 집합으로, 다른 언어에서는 해시맵/해시테이블이라고도 부르는 자료 구조이다. 집합과 마찬가지로 이 때 키로 사용되는 값은 해시가 가능해야 하므로 사전, 리스트, 집합 타입의 객체는 사전의 키가 될 수 없다. (단, 사전의 값으로는 사용될 수 있다.) 메뉴 이름과 가격처럼 짝지을 수 있는 데이터를 저장하고 관리하는데 유용하게 사용된다.

사전은 집합과 같이 중괄호를 사용하여 만든다. 집합과 차이점은 키와 값을 각각 콜론으로 연결하는 것이다. ({'a': 100, 'b': 200}) 그 외에 생성함수 dict()를 사용하여 만들 수 있다.

  • 사전 리터럴을 통한 생성 : {'a': 1, 'b': 2}
  • “키=값”의 형태로 인자를 전달하여 생성: dict(a=1, b=2)
  • 키의 연속열과 값의 연속열을 각각 전달하여 생성 : dict(('a', 'b'), (1, 2))
  • (key, value) 의 연속열을 전달하여 생성 : dict([('a', 1), ('b', 2)])
  • 사전 축약 문법을 사용하여 생성

사전 축약 문법

리스트 축약과 비슷하게 사전 축약 문법을 사용하여 사전을 생성할 수 있다. 키와 값의 연속열을 각각 가지고 있다면, 이 둘을 짝지어서 { key:value for ... } 의 형태로 작성할 수 있다.

# 키, 값의 데이터가 각각 있을 때
keys = ['apple', 'banana', 'cherry', 'orange']
values = (100, 200, 500, 1000)
d = {key: value for (key, value) in zip(keys, values)}
# ==> d = dict(keys, values)

# 사전의 키-값을 서로 바꾼 사전 만들기
d2 = {value: key for (key, value) in d.items()}

사전에서 사용할 수 있는 메소드들을 몇 가지 소개한다.

  • d.get(key, defafult=None) : 사전 d 에서 key에 대응하는 값을 찾는다. 해당 키가 존재하지 않으면 default 값이 리턴된다. default 값은 생략할 수 있으며, 생략된 경우 None이다.
  • d.setdefault(key, default=None) : d.get()과 비슷한 동작을 한다. 대신 키에 해당하는 값이 없을 때, (key: default) 의 쌍을 사전에 추가한 후 default를 리턴하는 점이 다르다.
  • d.keys() : 사전의 키들을 반복가능 객체 형태로 리턴한다. 이 리턴값은 리스트가 아니기 때문에 정수 인덱스로 원소를 참조할 수 없지만 list() 함수를 통해서 리스트로 변환 가능하다. 주로 사전의 각 키에 대해서 for 루프를 돌 때 사용한다.
  • d.values() : 사전의 값들을 반복가능 객체로 리턴한다. 사전의 값들에 대해서 for 루프를 돌 때 사용한다.
  • d.items() : 사전의 각각의 키-값 쌍을 튜플로 묶어서 반복가능 객체 형태로 리턴한다. for 루프에서 사전의 각 키와 값을 모두 사용할 때 사용한다.
  • d.pop(key, default) : 리스트의 pop() 과 비슷하게 특정 키에 대해서 그 키-값 쌍을 제거하면서 값을 리턴한다. 키가 없는 경우 default 값을 리턴한다. 만약 default 를 주는 것을 생략하면 KeyError 예외가 발생한다.
  • d.popitem() : 리스트의 pop()에 좀 더 가깝다. 마지막으로 추가된 키와 값을 제거하면서 (key, value) 튜플의 형태로 리턴한다.

문자열

문자열도 각각의 낱자 문자가 순서대로 모여있는 집합이라 할 수 있다. 순서가 있기 때문에 특정 글자나 부분열을 인덱스, 슬라이싱을 통해서 액세스할 수 있다. 문자열은 튜플과 같이 변경 불가능하기 때문에 s[3] = 'c' 와 같이 특정 위치의 문자를 변경할 수는 없다. 문자열은 집합의 특성 외에도 문자열 이라는 데이터 타입을 처리하는데 필요한 고유한 메소드가 많이 있는데, 이 부분은 문자열에 대해 다루는 부분에서 보다 자세히 다루도록 하겠다.

  • 문자열의 낱자, 부분열에 대해서는 인덱스를 사용한 subscription이 가능하다. : s[3], s[2:8]
  • s.index(c) : 문자 c가 처음 등장하는 인덱스를 찾는다.
  • s.index(c, a) : 인덱스 a 이후로 문자 c가 처음 등장하는 인덱스를 찾는다.
  • s.find(cs) / s.find(cs, a) : 낱자가 아닌 문자열 cs가 나타나는 인덱스를 찾는다.
  • s.count(c) : 문자열 내에 포함된 문자 c의 개수를 구한다.

연속열

연속열(Sequence)은 어떤 원소들이 정해진 순서대로 줄지어 있는 집합을 말한다. 리스트, 튜플, 문자열과 같은 타입들이 연속열에 속한다. 연속열의 공통된 특징은 그 내부의 각 원소들이 정수로 구분할 수 있는 순서를 갖는다는 점이며, 따라서 다음과 같은 동작을 할 수 있다는 것을 의미한다.

  • 정수 인덱스를 사용하여 특정한 원소를 참조할 수 있다.
  • 슬라이싱을 사용하여 부분열을 구할 수 있다.
  • 특정한 값이 어디쯤에 있는지 그 인덱스를 구할 수 있다.
  • s * int 와 같이 정수값을 곱해서 반복해서 늘린 사본을 만들 수 있다.
  • e in s 를 사용해서 특정한 원소가 포함되었는지 여부를 검사할 수 있다.
  • + 연산자를 사용해서 타입이 같은 두 개의 연속열을 더할 수 있다. (리스트 + 문자열처럼 타입이 다른 연속열을 더할 수는 없다.)

반복 가능

반복 가능한 객체는 파이썬 공식 문서에서 iterable 라는 표기로 종종 사용된다. 연속열처럼 특정한 한 타입이라기 보다는 어떤 공통된 특징을 공유하는 타입들을 묶어서 말한다. 이들은 공통된 특징으로 인해 비슷한 동작을 할 수 있고, 이런 비슷한 동작을 수행하게 하는 함수나 메소드는 보통 이름이 같도록 만들어져 있다.

반복 가능 객체는 이터레이터(iterator, 반복자)를 생성할 수 있는 객체이며, 내부적으로 __iter__() 메소드를 가지고 있다. 이 특징은 어떤 집합에서 각각의 원소에 대해 어떤 작업을 반복할 수 있도록, 순회할 수 있다는 것을 의미한다.

  • for 루프 및 리스트 축약 문법에 사용될 수 있다.
  • list(), tuple(), dict(), set() 함수에 전달하여 해당 타입 값을 생성할 수 있다.
  • sum(), max(), min() 함수에 전달하여 합계, 최대값, 최소값을 구할 수 있다.
  • sorted() 함수에 전달하여 정렬된 리스트로 만들 수 있다.
  • any(), all() 함수를 사용해서 특정 조건을 만족하는 1개의 원소가 있는지 혹은 전체 원소가 조건을 만족하는지를 검사할 수 있다.

모든 반복 가능 객체가 연속열일수는 없지만, 연속열에서는 인덱스를 1씩 늘려나가면서 모든 원소를 순회할 수 있으므로 모든 연속열은 반복가능하다고 말할 수 있다. 또한 앞으로 많이 보게 될 range(), map(), filter() 함수의 결과나 사전의 .keys(), .values() 메소드의 리턴 값 역시 리스트가 아닌 반복 가능 객체이다.

반복 가능 객체는 연속열이 아니기 때문에 m[3] 과 같은 subscription을 사용할 수 없다. 리스트나 튜플로 만들어도 될 것 같은 값들을 굳이 이런 별도의 유형으로 만드는 것은 불편해 보일 수 있는데, 다 이렇게 하는 이유가 있다. 그럼 그 이유가 무엇인지에 대해서는 다음 시간에 알아보도록 하고 오늘은 이쯤에서 마무리하도록 하겠다.