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

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

튜플은 처음이라

튜플은 여러 개의 값을 묶어서 마치 하나의 값처럼 다룰 수 있는 데이터 형식이다. 리스트와 마찬가지로 원소가 될 수 있는 값의 유형에는 제한이 없으며, 각각의 원소는 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을 사용한다면 이름? 이라고 입력하는 것으로 더 쉽게 확인이 가능하다. 

(Swift) 시퀀스와 관련된 Swift 표준 함수들

Swift 기본함수 중에는 Sequence를 만드는 함수들이 제법 있다. 이러한 함수 중에서 가장 많이 사용할 법한 함수로 우선 stride()를 들 수 있다. 이 함수는 파라미터가 다른 두 가지 버전이 있는데 하나는 stride(from:to:by:) 이고 다른 하나는 stride(from:through:by:)이다. 첫번째 버전은 to: 뒤의 경계를 포함하지 않으며, 두 번째 버전은 ClosedRange처럼 뒤쪽 경계값을 포함한다.

이 함수들은 주로 for 문과 같이 쓰이면서 주어진 범위 내에서 step을 달리하여 건너뛰는 값들을 하나씩 얻을 수 있게 한다. 보통 정수범위의 이터레이션에 쓰이는데, 다음과 같이 10~20사이에서 0.2 씩 뛰면서 반복할 수도 있다.

for a in stride(from:10.0, to:20.0, by:0.2) { print(a) }

혹은 거꾸로 내려가는 값들을 표현하기에도 좋다.

for a in stride(from:5.0, through:0.0, by:-0.5) { print(a) }
// 5.0, 4.5, 4.0, ... , 0.5, 0.0

이는 정수가 아닌 실수값1 등에 대해서 특정 범위에서 반복문을 만들 때, 혹은 특정 범위에서 임의의 step을 이용해서 시퀀스를 만들 때 사용한다. 이 함수들의 리턴형은 Sequence를 따르는 StrideTo, StrideThrough 제네릭 구조체 타입이다.2 (배열이 아니다.)

repeatElement(_:count:)

다음으로는 repeatElement(_:count:) 가 있다. 이는 특정 원소가 지정된 개수만큼 반복되는 시퀀스를 생성한다. 이 때의 리턴타입은 제네릭 구조체인 Repeated이며 이는 Collection의 일종이다.

Repeated : 모든 원소가 동일한 콜렉션3

sequence()

다음은 이름 그대로 시퀀스를 만들 것 처럼 생긴 sequence() 함수인데 여기에도 두 가지 버전이 존재한다. 그 중 하나는 sequence(first:next:)인데 시작값과, 어떤 값으로부터 다음 값을 만드는 함수를 이용해서 무한 수열을 생성할 수 있다. (이는 시퀀스의 원리상 정의와 비슷하다. 첫 원소와, 특정 원소에서의 다음원소를 계산할 수 있으면 정해진 길이만큼의 수열을 순차적-sequential-으로 구할 수 있다.) 어떤 원소로부터 그 다음 원소는 현재 원소 값을 기준으로 만들 수 있으므로, 등차수열이나 등비수열을 만들 때 유용하게 쓸 수 있다4

let _3n_plus1 = sequence(first:1){ $0 + 3 }
for x in _3n_plus1.prefix(10) { print(x) }
// 1, 4, 7, 10, 13, 16, 19, 22, 25, 28

두 번째 sequence함수는 sequence(state:)로 앞의 함수와 비슷하게 생겼는데, 값이 아닌 계산에 필요한 팩터들을 하나의 상태에 저장해두고 이를 계속 변경하면서 다음항을 계산할 수 있다. 일례로 피보나치 수열을 다음과 같이 계산할 수 있다. 5

var s = (a: 0, b: 1)
let fib = sequence(state: s) { (state: inout (a: Int, b: Int)) -> Int? in 
    defer {state = (state.b, state.a + state.b)}
    return state.a
}
for f in fib.prefix(10) { print(f) }
// 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

위에서 보이는 두 번째 버전의 타입 시그니처를 눈여겨봐두자.

func sequence<State, Element>(state: State, next: @escaping (inout State) -> Element?) -> UnfoldSequence<Element>

여기서 리턴타입은 UnfoldSequence이다. 이는 원소들이 특정 원소나 변경가능한 상태값에 대해서 클로저를 반복 적용하여 생성되는 시퀀스로 설명된다.6 이 수열의 수학적 특성 상 모든 원소는 한 번에 계산되어 생성되는 것이 아니라 매번 계산해야 하기 때문이다.

시퀀스의 각 원소들은 느긋하게 계산되며, 시퀀스의 길이는 무한수열이 될 수 있다. 6

하지만 중요한 것은 이 수열이 항상 ‘느긋한’ 것은 아니라는 것이다. 예를 들어 다음 코드는 무한루프가 되며 제대로 실행되지 않는다.

let s = sequence(first:1, next:{ $0 + 3})
for x in s.map{ $0 + 1 }.prefix(10) {
  print(x)
}

s.map{ $0 + 1 }s의 모든 원소에 대해 eagerly 하게 적용되기 때문에 무한수열을 만들기 때문이다. 이러한 무한 수열을 취급할 때는 수열 자체를 lazy하게 만들거나 한정자를 반드시 적용한다.

let s = sequence(first:1, next:{ $0 + 3}).prefix(1000).map{ $0 + 1 }
for x in s { print(x) }

이 코드에서도 sequence()의 결과물은 느긋한 수열이지만 prefix를 하는 시점에 eagerly하게 계산되기 때문에 1000개의 값이 모두 구해진 시퀀스가 된다. 보다 나은 방법은 Sequence.lazy 프로퍼티를 이용하는 것이다. 아래 코드는 비슷하게 생겼지만, for 문을 돌면서 매 원소가 계산된다. (만약 for 루프 내에서 break 등으로 루프를 탈출한다면

let s = sequence(first:1){ $0 + 3 }.lazy.map{ $0 + 1 }.prefix(1000)
for x in s { print(x) }

이 코드에서는 lazy를 통해서 아예 LazySequence를 만들었고, 이 타입은 맵, 필터에 대해서 클로저 자체를 캡쳐하는 느긋한 수열이 만들어진다. 따라서 실제 for - in루프를 돌 때 각 시퀀스의 원소가 구해지고 그것이 다시 map을 거치는 과정을 수행한다. 따라서 수열을 생성/준비하는 시간을 들일 필요가 없으므로 안전하기도 하다.

타입을 지운 시퀀스

Swift 표준 라이브러리에는 AnySequence라는 제네릭 구조체도 만들어져 있다.

AnySequence: 타입을 지운(type-erased) 시퀀스 – AnySequence의 인스턴스는 모든 동작을 동일 Element 타입을 가진 내부의 베이스 시퀀스에게 이양하며, 내부 베이스에 관한 정보를 은닉합니다.7

레퍼런스 문서에서 상당히 앞쪽에 나오기 때문에 sequencerepeateElement 혹은 그외의 시퀀스를 생성하는 여러 메소드들과 관련이 있을 것 같지만, 그렇지는 않다.8


  1. Stridable이 아닌 것 같지만 실제로는 맞음. 
  2. Float, Double 등의 실수형 타입도 실제로는 Stridable 프로토콜을 따르고 있다. 실수값들은 실제로 “거의 연속적”이지만 이것이 가능한 이유는 Stridable은 비교가능하며 두 지점 사이의 거리를 구할 수 있기만 하면 되기 때문이다. 
  3. Repeated: Apple Developer 
  4. 그런데 등차수열이나 등비수열을 뭐 얼마나 유용하게 쓰게 될지는 모르겠다만. 
  5. 물론 이 함수는 피보나치 수열만 계산하는데 유용할 것 같다. 
  6. UnfoldSequence, Apple Developer 
  7. AnySequenceApple Developer 
  8. 내부에서 어떤 Boiler plate용으로 쓰이는 것 같음. 

(Swift) Array 완전정복 – 02. Sequence 프로토콜

Sequence

시퀀스(Sequence)는 직역하면 연속열이 될 수 있으며, 문자 그대로 개개의 원소들을 순서대로 하나씩 순회할 수 있는 타입을 의미한다. (Swift 기본 타입에 대해서는 사실상 모든 집합 타입이 이에 해당한다.) 시퀀스는 사실 Swift 문법과 밀접한 관련이 있는데, 바로 for - in 구문에 사용된다는 점이다.1 (Swift) Array 완전정복 – 02. Sequence 프로토콜 더보기

Swift의 Array 완전 정복 – 01. 생성과 조작

배열은 대부분의 프로그래밍 언어에서 가장 중요하게 다뤄지는 데이터 타입이며, 동시에 프로그래머들이 가장 많이 사용하게 되는 기본적인 자료 구조 중 하나이다. 이번 글에서는 Swift의 배열인 Array 타입에 대해 살펴보도록 하겠다.

Swift의 Array 완전 정복 – 01. 생성과 조작 더보기

오일러 프로젝트 02

오일러 프로젝트의 두 번째 문제는 4백만 이하의 피보나치 수열 중에서 짝수인 항을 모두 더한 합을 구하는 문제이다.

피보나치 수열의 각 항은 바로 앞의 항 두 개를 더한 것이 됩니다. 1과 2로 시작하는 경우 이 수열은 아래와 같습니다

. ( 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... )

 

짝수이면서 4백만 이하인 모든 항을 더하면 얼마가 됩니까?

오일러 프로젝트 02 더보기