파이썬은 처음이라 – 연속열은 처음이라

이번 시간에는 지난 번에 살짝 언급만 하고 넘어갔던 튜플에 대해서 기본적인 내용을 다루겠다. 튜플은 파고 들자면 제법 묵직해질 수 있는 토픽이기는 하지만, 튜플을 활용하는 화려한(?) 기법들은 개인적으로 중급 이상의 과정에 어울린다고 생각하기 때문에 여기서는 간단한 개념과 기본적인 사용법에 대해서만 설명하고자 한다. 튜플은 여러가지 측면에서 리스트와 비슷한 점이 많고, 실제로 리스트와 크게 구분없이 쓰이는 경향이 있는 것도 사실이다. 리스트와 튜플의 공통적인 특성과 이런 특성을 가지는 타입들을 부르는 말인 연속열에 대해서 이야기해보고자 한다.

튜플은 처음이라

튜플은 여러 개의 값을 묶어서 마치 하나의 값처럼 다룰 수 있는 데이터 형식이다. 리스트와 마찬가지로 원소가 될 수 있는 값의 유형에는 제한이 없으며, 각각의 원소는 0부터 시작하는 정수 인덱스로 참조할 수 있다. 튜플은 소괄호((  )) 로 둘러싸인 콤마로 구분된 값으로 정의되는 것이 정석이다. 하지만 몇 가지 예외적인 방법으로 정의하는 것도 가능하다.

a = (1, "A")  # 튜플 a는 정수 1과 문자열 "A"의 쌍으로 묶여 있다. 

## 바인딩 구문의 우변에 위치하는 경우 컴마를 생략할 수 있다.
b = 2, True, None  

이렇게 만들어진 튜플의 각 원소는 리스트와 같은 방식으로 원소 및 부분열을 참조할 수 있다.

a = (1, 2, 3, 4, 5)
a[1] ## -> 2
a[::2] ## -> (1, 3, 5)

리스트와 튜플의 가장 큰 차이점은 리스트는 가변적인 값의 집합인 반면, 튜플은 하나로 단단히 묶여진 값의 순서쌍이며 개별 원소를 추가/삭제/변형하는 것은 허용되지 않는다는 것이다.

a[1] = 20
## TypeError : 'tuple' object does not support item assignment

그러한 특성을 제외하면 튜플은 리스트와 거의 똑같이 사용할 수 있다. 튜플은 FOR 구문을 통해서 개별 원소를 순회할 수 있고, 리스트 축약의 베이스로도 사용될 수 있다.

튜플 분해하기

사실 튜플은 바인딩시에 분해하는 것이 가능하다. 바인딩 구문의 좌변에도 튜플 문법을 쓸 수 있고, 이는 우변에 있는 튜플의 각 원소에 매칭된다. 예를 들어 아래와 같은 식으로 특정한 튜플의 구성요소는 정수 인덱스가 아니라 각각의 위치에 맞는 값으로 분해된다.

a = (1, 2, 3)
x, y, z = a
## x->1, y->2, z->3

## 위 문법은 아래의 동작과 일치
x = a[0]
y = a[1]
z = a[2]

## 사실, 좌변이 튜플일 때, 우변은 리스트여도 됩니다.
x, y, *z = [1, 2, 3, 4, 5]
## x->1, y->2, z->[3,4,5]

이 문법은 튜플을 조금 더 고급지게 다룰 때 다시 이야기할테니 눈여겨봐두고 다음으로 넘어가자.

연속열은 처음이라

튜플과 리스트는 튜플에서는 원소를 추가/변경할 수 없다는 사실을 제외하면 이 두 타입의 행동은 매우 비슷하고, 실제로 대부분의 파이썬 코드에서 튜플과 리스트는 서로 혼용해서 쓸 수 있다. 이것이 가능한 이유는 튜플과 리스트의 구조가 매우 닮아있기 때문인데, 바로 각각의 원소가 자신의 순서를 가진채로 줄을 지어 있는 연속열이기 때문이다. 파이썬에서는 리스트와 튜플외에도 이러한 연속열이  또 하나 있는데, 바로 문자열이다.

파이썬에서는 이러한 연속열이라고 묶을 수 있는 타입들을 실제로 연속열(Sequence)이라고 부르면서 거의 같은 것으로 취급한다. 세 연속열의 차이점은 이러하다.

  • 리스트는 어떤 값이든 원소로 가질 수 있으며, 변경이 가능하다.
  • 튜플은 어떤 값이든 원소로 가질 수 있으나, 변경이 불가능하다.
  • 문자열은 낱개의 문자만을 원소로 가질 수 있으며, 변경은 불가능하다.

이러한 차이를 제외하고, 즉 안에 들어있는 낱개의 원소 타입에 특화된 연산을 하거나, 연속열 자체를 변경하려는 시도를 하지 않는다면 리스트와 튜플 그리고 문자열은 마치 같은 타입처럼 행동할 수 있다. 즉 리스트 축약의 베이스이거나 FOR 문에서 순회할 집합으로 사용할 수 있다.

## 리스트를 이용한 리스트 축약
[ x * 2 for x in [1,2,3]]
#=> [2,4,6]

## 튜플을 그대로 사용할 수 있다.
[x*2 for x in (1, 2, 3)]
#=> [2,4,6]

## 문자열을 써도 된다. 단, (* 2)한 결과는 다르다.
[x*2 for x in "123"]
#-> ["11", "22", "33"]

연속열들의 공통적인 특징은 다음과 같은 것들이 있다.

  • + 연산자를 사용하면 두 연속열을 붙일 수 있다. (이 때 두 연속열을 같은 타입이어야 한다.)
  • * 연산자로 정수를 곱해서 연속열을 반복, 확장할 수 있다.
  • in 연산자를 사용한 멤버십 테스트가 가능하다.
  • seq[i] 와 같이 정수 인덱스를 통해서 개별 원소를 액세스할 수 있다. 인덱스가 음수인 경우에 뒤쪽에서부터 액세스한다.
  • seq[start:end], seq[start:end:step]의 문법으로 슬라이싱할 수 있다.
  • 바인딩 구문에 사용될 때, 좌변에서 튜플 문법을 써서 분해하는 것이 가능하다.

반복가능한 건 또 처음이라

연속열들은 공통적으로 FOR구문에 사용될 수 있다는 특징을 가지고 있다. (그리고 동시에 축약(comprehension)구문에도 사용할 수 있다.) 파이썬에는 비록 연속열은 아니지만 반복가능한 몇 가지 타입들이 더 존재한다. 아직 자세히 살펴보지 않은 그룹형식인 사전과 세트(set)가 그러하다. 그 외에도 지금까지 자주 등장했던 녀석이 하나 있는데, FOR문에서 예시로 많이 쓰인 range() 함수가 그러하다.

range()함수의 도움말을 읽어보면1 다음과 같은 내용을 확인할 수 있다. (아래 예시는 iPython을 이용해서 확인한 내용이다.)

In [1]: range?
Init signature: range(self, /, *args, **kwargs)
Docstring:
range(stop) -> range object
range(start, stop[, step]) -> range object

Return an object that produces a sequence of integers from start (inclusive)
to stop (exclusive) by step.  range(i, j) produces i, i+1, i+2, ..., j-1.
start defaults to 0, and stop is omitted!  range(4) produces 0, 1, 2, 3.
These are exactly the valid indices for a list of 4 elements.
When step is given, it specifies the increment (or decrement).
Type:           type

range()함수는 리스트나 튜플이 아닌 range 객체라는 것을 반환하고, 이 객체는 정수의 연속열을 생성할 수 있는 객체라고 설명하고 있다. 사실 파이썬에서는 이러한 반복가능한 특성을 가진 객체가 은근히 많고, 또 뒤에서 중요하게 다뤄질 제너레이터라는 개념을 배우고 나면 간단하고 편리하게 이러한 반복가능한 값을 만들어서 사용할 수 있다. 이에 대한 자세한 내용은 제너레이터에 대한 이해를 필요로 하니 다음으로 미루기로 하자.

다만 여기에서 반복가능 객체들은 마술 상자 같은 곳에서 정해진 순서에 따라서 값을 뿅뿅 만들어서 하나씩 내놓을 수 있는 능력을 가진 값들이라고 보면 된다. 다만 리스트, 튜플, 문자열은 그 때 그 때 마다 필요한 값을 만드는 것이 아니라, 모든 값을 한 번에 다 만들어서 펼쳐놓고 앞에서부터 하나씩 골라서 쓰는 개념으로 이해하고 있으면 되겠다.

연재 초반에 리스트가 파이썬에서 매우 중요한 타입이라고 언급했었는데 조금씩 그 중요성에 대한 감이 잡히는지 모르겠다. 리스트는 단일 값이 아닌 집합으로서 기능하면서, 연속열과 반복가능이라는 특성으로 이어지면서 FOR문과 그외 다른 유사한 타입들과의 성질을 많이 공유하고 있다. 따라서 이에 대해 이해하면 덤으로 그외 여러 타입들에 대해서 쉽게 친숙해 질 수 있고, 그러한 과정에서 지금은 말로 표현하기 어려운 어떤 언어 자체의 디자인 방향성 같은 것을 체득하게 될 것이다.

그러면 여기에 탄력을 더해서 리스트에 대해서 조금 더 많이 알아보는 시간을 가져야 할 것이다. 그러기 전에 파이썬의 내장 함수 몇 가지를 더 보고 진행해나가도록 하자.


  1. 파이썬 대화형 쉘모드에서나 help(이름)이라고 입력해서 도움말을 볼 수 있다. ipython을 사용한다면 이름? 이라고 입력하는 것으로 더 쉽게 확인이 가능하다. 

[Python101] Iterable(3) – 튜플

튜플(tuple)은 ‘한 벌’의 의미로 의미상으로는 가장 원시적인 배열이다. 튜플 한 번 만들어지고 나면 수정이 불가능한 집합이다. 쉽게 말해서 리스트를 ‘얼리면’ 튜플이 된다.

튜플을 만드는 법

리스트를 정의할 때는 대괄호에 원소들을 써서 생성했다. 튜플의 경우에는 괄호에 원소들을 써서 만들 수 있다. 아래의 b는 터플이다.

a = [1, 2, 3, 4]
b = (1, 2, 3, 4)

터플의 원소는 리스트와 마찬가지의 방법으로 접근할 수 있다. 터플 이름 뒤에 대괄호를 쓰고 그 속에 인덱스를 넣으면 된다.

b[2] # --> 3
b[:2] # --> (1, 2)

재밌는 사실 하나. 원소가 하나 밖에 없는 튜플을 만드려면 어떻게 해야할까? (1)이라고 쓰면 이는 수식 자체로 숫자 1과 아무런 차이가 없다. 대신 (1, ) 이라고 쓰면 이는 숫자 1을 원소로 갖는 튜플이 된다.

튜플의 메소드

튜플은 count, index의 두 개의 메소드만 제공한다. 튜플의 특정한 원소를 변경하거나, append, remove, pop, insert 등의 기능은 수행할 수 없다.

그러면 이렇게 한정적인 기능만을 수행하는 튜플을 왜 사용할까? 리스트의 경우 한 번 만들어진 이후에 변경이 가능한 점 때문에 처리하는 데 ‘속도’가 느려질 수 있다. 매우 큰 집합을 포함하는 리스트의 중간에 새로운 원소를 끼워넣거나, 중간에 있는 특정한 원소를 제거하는 것은 제아무리 빠른 컴퓨터라할지라도 꽤나 피곤한 일인 것이다. 이런 “변경 가능한 특성” 때문에 리스트는 편리한 대신 처리속도가 느리다. 튜플은 변경 가능한 특성이 없고 오직 고정되어 있는 리스트이기 때문에 원소를 검색하거나 하는 데 시간이 거의 걸리지 않는다.

사실 리스트를 이해하면 튜플은 그냥 이해하고 있는 것과 다름없다. 원소를 변경하거나 정렬하는 등의 동작을 할 수 없지만 그냥 무식하게 빠르다고 생각하면 된다.

길이를 구하거나 정렬하기 – 내장함수의 경우

튜플은 변경과 관련된 메소드를 가지고 있지 않지만, 대신 기본 내장 함수들 sorted(), len()에서는 적용할 수 있다. 즉 (2,5,7,3,4,1,9)와 같은 튜플은 sorted() 함수를 써서 ‘정렬된 리스트’를 얻을 수는 있다.

b = (2,6,3,7,4,1,9)
c = sorted(b)
print c
# --> (1, 2, 3, 4, 6, 7, 9)

튜플의 이름은 역시 자료형인 동시에 튜플로 변환하는 함수이기도 하다. tuple() 함수는 리스트를 얼리거나, 문자열을 쪼개어 터플로 만들어준다.

>>> a = 'attributes'
>>> b = range(0,10,4)
>>> ta = tuple(a)
>>> tb = tuple(b)
>>> ta
# --> ('a', 't', 't', 'r', 'i', 'b', 'u', 't', 'e', 's')
>> > tb
# --> (0, 4, 8)
>>> len(ta)
# --> 10
>>> sorted(ta)
# --> ['a', 'b', 'e', 'i', 'r', 's', 't', 't', 't', 'u']

이제 다음 시간에는 다른 자료형인 ‘사전(dictionary)’에 대해 알아보기로 하겠다. 사전형은 이름이 있는 원소들을 다루는 멋진 자료형이다.