[11/20] 튜플
글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.
이 포스트는 Tuples를 한글로 요약 정리한 글입니다.
튜플 (Tuples)
이번 장에서는 또다른 내장된 데이터 타입인 튜플(Tuples)에 대해서 알아보고 배열과 딕셔너리, 튜플이 어떻게 같이 작동하는지 공부할 것이다. 또한 유용한 기능인 '수집과 분산 연산자(gather and scatter operators.)'와 '가변 인수 배열(variable-length argument arrays)' 등을 살펴볼 것이다.
튜플은 불변이다
튜플은 일련의 값들이다. 값은 어떤 데이터 타입도 될 수 있으며, 또한 정수로 인덱싱되기 때문에 배열과 유사한 점이 많다. 중요한 차이점은 튜플은 요소를 변경할 수 없으며 각각의 요소들은 고유한 데이터 타입을 가진다.
문법적으로 튜플은 쉼표로 구분된 값 목록이다.
1 |
|
하지만 필요하진 않더라도 괄호를 사용해서 튜플을 작성하는 것이 일반적이다.
1 |
|
하나의 요소로 튜플을 만들기 위해서는 마지막에 쉼표를 넣어주어야 한다.
1 |
|
WARNING 쉼표 없는 괄호 안의 값은 튜플이 아니다.
1
2
3
4julia> t2 = ('a')
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
julia> typeof(t2)
Char
튜플을 만드는 다른 방법은 내장 함수를 이용하는 것이다. 인수가 없는 상태인 tuple()
을 입력하면 빈 튜플이 생성된다.
1 |
|
여러 개의 인수들을 입력한다면, 인수들이 값인 튜플을 생성한다.
1 |
|
tuple
은 내장 함수의 이름이기 때문에 변수의 이름으로는 사용하지 않는 것이 좋다.
대부분의 배열 연산자들은 튜플에서 작동한다. 대괄호 연산자는 해당 인덱스의 요소를 반환한다.
1 |
|
슬라이스 연산자 또한 요소들의 특정 범위를 선택한다.
1 |
|
그러나 튜플의 한 요소를 수정하려고 시도하면, 아래의 오류 메시지가 나타난다.
1 |
|
튜플은 변하지 않기 때문에 요소들을 수정할 수 없다.
관계 연산자들도 튜플이나 기타 시퀀스에서 작동한다. 줄리아는 각 시퀀스에서 첫 번째 요소를 비교하며 시작한다. 동일하면 다음 요소로 진행되며 다른 요소를 찾을 때까지 계속한다. 서브시퀀스는 고려되지 않는다. (실제로 큰 경우에도)
1 |
|
첫 번째 예시에서는 뒤의 튜플의 모든 요소가 앞의 튜플의 요소보다 모두 크기 때문에 true
를 반환했다. 두 번째 예시는 세 번째 요소가 뒤의 튜플보다 앞의 튜플이 더 크지만 앞의 두 개의 요소가 모두 뒤의 튜플이 더 크기 때문에 true
를 반환했다. 즉, 하나라도 큰 요소가 있다면 true
이고 아니라면 false
를 반환한다.
튜플 할당
이것은 두 변수의 값을 바꿀 때 유용하다. 기존 할당에서는 임시 변수를 사용해야 한다. 예를 들어 a
와 b
를 바꾸려고 한다면, 아래와 같이 중간에 임시 변수가 필요하다.
1 |
|
위의 방식은 번거롭다. 튜플은 훨씬 간단한 방법으로 변경한다.
1 |
|
왼쪽은 튜플의 변수들이며, 오른쪽은 튜플의 표현식이다. 각 값들은 해당 변수에 할당되며, 오른쪽의 모든 표현식은 할당 전에 평가된다.
왼쪽에 있는 변수의 수는 오른쪽 값의 수보다 작으면 작동하지만, 초과된 값은 사라진다.
1 |
|
일반적으로 오른쪽은 문자열, 배열, 튜플 등 어떤 종류의 시퀀스도 입력될 수 있다. 예를 들어 이메일 주소를 사용자 이름과 도메인으로 나누려면 다음과 같이 작성할 수 있다.
1 |
|
split()
으로부터 반환된 두 값은 배열의 두 요소가 된다. 첫 번째 요소는 uname
에 할당되며, 두 번째는 domain
에 할당된다.
1 |
|
반환 값으로서의 튜플
엄격하게 말하자면, 함수는 오직 한 개의 값만 반환하지만 그러나 그 한 개의 값이 튜플이라면 여러 개의 값을 반환하는 것과 같은 효과를 가진다. 예를 들어 두 정수의 나눗셈에서 몫과 나머지를 구하려 할 때, x ÷ y
을 계산한 후 x % y
를 계산하는 것은 비효율적이다. 두 개의 수식을 한번에 계산하는 것이 더 낫다.
내장 함수인 divrem()
은 두 인수를 가져가 몫과 나머지인 두 개의 값을 가진 튜플로 반환한다. 이후 결과를 튜플로 저장할 수 있다.
1 |
|
또는 요소들을 각각 저장하기 위해서 튜플 할당을 이용할 수 있다.
1 |
|
아래의 함수는 튜플을 결과로 반환하는 예시이다.
1 |
|
minimum()
과 maximum()
은 시퀀스에서 가장 작은 수와 큰 수를 찾아주는 내장된 함수이다. minmax()
은 두 개의 값을 튜플로 반환한다. 같은 결과를 주는 내장 함수인 extrema()
도 효과적이다.
가변 인수 튜플 (Variable-length Argument Tuples)
함수들은 가변적인 개수의 인수를 받아들일 수 있다. ...
로 끝나는 매개 변수 이름은 인수를 튜플로 수집(gather)한다. 예를 들어 printall()
은 여러 인수들을 사용하여 출력한다.
1 |
|
수집 매개 변수는 어떤 이름도 사용할 수 있지만 관례 상으로 args
를 사용된다. 아래의 코드는 함수가 어떻게 작동하는지 보여준다.
1 |
|
위에서 본 수집을 보완해주는 것이 분산(scatter)이다. 만약 값의 시퀀스가 있고 여러 인수로 함수에 값을 전달하려는 경우 ...
연산자를 사용하면 된다. 예를 들어 divrem()
은 정확히 2개의 인수를 받으며, 2개의 요소를 가진 튜플을 넣어도 작동하지 않는다.
1 |
|
그러나 튜플에 ...
연산자를 사용하면 작동한다.
1 |
|
많은 내장 함수들이 가변 인수 튜플을 사용한다. 예를 들어, min()
과 max()
은 여러 인수들을 가져올 수 있다.
1 |
|
하지만 sum()
은 작동하지 않는다.
1 |
|
배열과 튜플
zip()
은 두 개 이상의 시퀀스들을 가져온 후 시퀀스들을 요소로 가진 튜플로 반환하는 내장 함수이다. 함수의 이름은 두 줄의 치아를 연결하고 끼워 넣는 지퍼에서 가져왔다.
아래의 예시는 문자열과 배열을 zip()
의 인수로 입력한 결과이다.
1 |
|
위의 결과는 페어 단위로 정리할 수 있는 짚(zip) 객체이다. zip()
은 일반적으로 for
루프에서 사용된다.
1 |
|
짚 객체는 일종의 이터레이터(iterator)이며 시퀀스를 반복하는 객체이다. 이터레이터는 몇 가지 측면에서 배열과 비슷하지만, 배열과 달리 인덱스를 사용하여 요소를 선택할 수 없다.
만약 배열 연산자나 함수를 사용하고 싶다면, 짚 객체를 이용하여 배열을 만들면 된다.
1 |
|
위의 결과는 튜플의 배열이다. 이 예시에서 각각의 튜플은 문자열에서 온 문자와 배열에서 온 요소들을 포함한다.
만약 시퀀스가 다른 길이라면, 더 짧은 시퀀스를 기준으로 결과가 출력된다.
1 |
|
튜플의 배열을 순회하기 위해서는 for
루프에서 튜플 할당을 사용할 수 있다.
1 |
|
루프가 실행될 때마다 줄리아는 배열에서 다음 튜플을 선택하고 그 요소들을 letter와 number로 할당한다. for
루프에서 (letter, number)
와 같이 괄호를 사용하는 것은 의무적이다.
만약 zip()
, for
루프, 튜플 할당을 모두 결합하면, 동시에 두 개 이상의 시퀀스를 순회하는데 유용한 관용구를 얻는다. 예를 들어, hasmatch()
는 t1
과 t2
2개의 시퀀스를 가져가서 특정 인덱스 i
가 t1[i] == t2[i]
라면 true
를 반환한다.
1 |
|
시퀀스의 요소와 인덱스들을 순회해야 한다면, 내장 함수인 enumerate()
를 사용할 수 있다.
1 |
|
enumerate()
의 결과는 enumerate 객체이며, 일련의 쌍을 반복한다. 각 쌍에는 인덱스와 시퀀스 요소가 포함되어 있다.
딕셔너리와 튜플
딕셔너리는 키-값 페어를 반복하는 이터레이터(iterator)로 사용할 수 있다. for
루프에서 사용한다면 다음과 같다.
1 |
|
딕셔너리와 동일하게 아이템들은 특정한 순서가 없다.
또한 튜플 배열을 사용하여 새 딕셔너리로 초기화할 수 있다.
1 |
|
Dict()
과 zip()
을 결합하면 딕셔너리를 만드는 간결한 방법이 된다.
1 |
|
이것은 튜플을 딕셔너리의 키로 사용하는 일반적인 방법이다. 예를 들어 전화 번호부는 성과 이름 쌍에서 전화 번호로 매핑될 수 있다. 우리가 이름 쌍과 전화 번호를 알고 있다면 아래와 같이 작성할 수 있다.
1 |
|
위의 예시에서 대활호 안에 있는 표현식은 튜플이다. 즉, 이 딕셔너리를 순회하기 위해서는 튜플 할당을 사용할 수 있다.
1 |
|
이 루프는 directory
에서 튜플인 키-값 페어를 순회한다. 각 튜플의 키 요소를 last
와 first
에, 값을 숫자에 할당한 다음 이름과 해당 전화번호를 출력한다.
시퀀스의 시퀀스 (Sequences of Sequences)
지금까지는 튜플 배열(arrays of tuples)에 중점을 두었지만, 이번 장의 모든 예제들은 배열의 배열(arrays of arrays), 튜플의 튜플(tuples of tuples), 배열 튜플(tuples of arrays)에서도 작동한다. 앞과 같이 가능한 조합을 열거하지 않기 위해서 시퀀스의 시퀀스로 이야기 하는 것이 훨씬 편하다.
많은 상황에서 문자열, 배열 및 튜플 등 서로 다른 종류의 시퀀스들을 바꿔서 사용할 수 있다. 만약 바꾼다면 어떤 것을 선택해야 할까?
확실한 것부터 시작하자면, 문자열은 요소들이 문자로 이루어져야 하기 때문에 다른 시퀀스보다 제한적이다. 또한 문자열은 불변이다. 만약 문자열에서 문자를 변경하는 함수가 필요한 경우, 새 문자열을 만드는 것 대신 문자 배열을 사용할 수 있다.
배열은 대부분 변경이 가능하기 때문에 튜플보다는 일반적으로 사용된다. 그러나 튜플이 더 적합한 몇 가지의 경우가 있다.
- 반환 구문과 같은 일부 코드에서는 배열보다 튜플을 만드는 것이 문법 상 더 간단하다.
- 시퀀스를 함수에 인수로 전달하는 경우 튜플을 사용하면 에일리어싱으로 인한 오류의 가능성이 줄어든다.
- 성능상의 이유로 컴파일러가 튜플 타입을 전문화할 수 있다.
튜플은 변경할 수 없으므로 배열을 수정하는 sort!()
나 reverse!()
와 같은 함수를 제공하지 않는다. 그러나 줄리아는 배열의 요소들을 가져와 정렬된 상태의 새로운 배열을 반환하는 내장 함수 sort()
를 제공하며, 시퀀스를 가져와 역순으로 반환하는 reverse()
도 제공한다.
디버깅
배열, 딕셔너리 그리고 튜플은 데이터 구조의 예이다. 이 장에서는 튜플 배열 또는 튜플을 키로 가진 딕셔너리와 같은 복합 데이터 구조를 보기 시작하였다. 복합 데이터 구조는 유용하지만 형태 오류(shape errors)가 나기 쉽다. 즉, 데이터 구조가 잘못된 데이터 타입이나 크기 또는 구조를 가지는 것이다. 예를 들어 함수는 하나의 정수가 있는 배열을 기대하는데 개발자가 정수 하나만 입력한다면, 그 함수는 작동하지 않는다.
줄리아는 시퀀스의 요소에 데이터 타입을 첨부할 수 있다. 이 작업을 수행하는 방법은 다중 디스패치 파트에서 자세하게 설명할 것이다. 데이터 타입을 지정하면 많은 형태 오류가 제거된다.