[7/20] 문자열
글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.
이 포스트는 Strings를 한글로 요약 정리한 글입니다.
문자열 (Strings)
문자열은 정수나 소수, 불 표현식과는 다르다. 이 장에서는 문자열을 구성하는 문자에 접근하는 방법을 배우고, 줄리아가 제공하는 문자열 도우미 함수 중 일부에 대해서 알아볼 것이다.
캐릭터 (Characters)
영어를 사용하는 사람들은 알파벳, 숫자 및 일반적인 문장 부호와 같은 문자에 익숙하다. 이런 문자들은 표준화되어 ASCII 표준에 따라 0부터 127사이의 정수 값으로 일대일 배치한다. 영어가 아닌 한국어나 중국어, 아랍어와 같아느 언어들도 ASCII 표준에 포함되어 있다.
유니 코드 표준은 복잡하게 구성된 문자들도 정확히 표현할 수 있도록 고안되었기 때문에 일반적으로 주요한 표준으로 사용되고 있으며, 전 세계적으로 모든 문자들에 고유 번호를 제공한다.
Char
값은 단일 문자를 나타내며, 작은 따옴표로 묶는다.
1 |
|
위의 바나나와 같은 이모티곤 또한 유니코드 표준에 포함되어 있다. (:banana: TAB)
문자열은 시퀀스이다. (A String Is a Sequence)
문자열은 일련의 문자이다. 대괄호 연산자를 사용하여 한 번에 하나씩 문자에 엑세스 할 수 있다.
1 |
|
두 번째 명령문은 fruit
에서 첫 번째 문자를 선택하여 letter
라는 변수에 할당한다.
위와 같이 대괄호[]를 내부에 있는 표현식을 인덱스(index)라고 부른다. 인덱스는 문자열 중에서 원하는 문자가 어느 위치에 있는지를 나타낸다.
줄리아는 모든 인덱싱을 1부터 시작한다. 즉, 첫 번째 문자는 인덱스 1로 찾을 수 있으며, 마지막 문자는 인덱스 end
에서 찾을 수 있다.
1 |
|
또한 인덱스는 변수나 연산자가 포함된 표현식들도 사용될 수 있다.
1 |
|
그러나 인덱스 값이 정수가 아니라면 오류 메시지가 뜬다.
1 |
|
length
length
는 내장 함수로 문자열 안에 있는 문자들의 개수를 반환한다.
1 |
|
위의 예시를 보면 length
는 이모티콘 3개와 그 사이의 띄어쓰기를 포함하여 총 길이를 5
라고 반환한다.
문자열의 마지막 문자를 얻고 싶다면, 아래의 코드를 입력하면 된다.
1 |
|
하지만 기대했던 것과는 다른 결과가 나온다. 문자열은 다양한 UTF-8 인코딩을 사용하여 인코딩되었다. UTF-8은 가변적으로 폭이 변하는 인코딩 (variable-width encoding)으로 모든 문자가 동일한 바이트 수로 인코딩되지 않는다.
sizeof()
는 문자의 바이트 수를 보여준다.
1 |
|
위 이모티콘은 4바이트로 인코딩되었고 문자열 인덱싱은 바이트 기반이기 때문에 fruits
의 5번째 요소는 띄어쓰기인 SPACE
이다.
이것은 UTF-8 문자열에서 모든 문자들은 정확히 몇 바이트로 인덱싱 되었는지 확인해야 한다는 것을 보여준다. 만약 눈으로 파악한 인덱스를 사용한다면, 잘못된 결과를 도출할 수 있다.
1 |
|
fruits
예시에서는 🍌
가 4바이트이기 때문에 인덱스 2,3,4는 반환되지 않으며, 그 다음 유효한 인덱스는 5이다. 다음 유효한 인덱스를 찾고 싶다면 nextind(fruits, 1)
을 입력하면 된다. 이후 5 다음으로 유효한 인덱스를 찾고 싶다면 nextind(fruits, 5)
를 실행하면 된다.
순회 (Traversal)
많은 계산들은 한 번에 한 문자씩 문자열을 처리한다. 이때 컴퓨터는 하나의 문자를 선택하여 해당 명령들을 수행한 이후, 다른 문자를 선택하여 이를 반복한다. 이것을 순회(Traversal)이라고 하며, 순회를 작성하는 방법은 while
루프를 사용하는 것이다.
1 |
|
위의 코드는 변수 index
에 fruits
의 첫 번째 인덱스를 할당한다. 그 후 while
루프 조건으로 넘어간다. 조건은 index
가 fruits
의 인덱스 총 길이보다 작으면 true
을 도출하여 루프를 실행하거나, 크면 false
를 도출하여 루프를 종료한다.
firstindex()
는 인수의 첫 번째 인덱스를 반환한다. 키워드 global
는 기존에 정의했던 변수에 새로운 값을 재할당한다.
문자열 슬라이스 (String Slices)
문자열의 일부를 슬라이스(Slices)라고 부른다. 슬라이스는 문자를 축출하는 것과 비슷하다.
1 |
|
연산자 [n:m]
은 인덱스 n
번째 바이트부터 m
번째 바이트까지의 문자들을 반환한다. 그렇기 때문에 인덱스 위치를 잘 파악하고 있어야 한다.
키워드 end
는 마지막 문자를 반환한다.
1 |
|
만약에 첫 번째 인덱스의 수가 두 번째 인덱스의 수보다 크다면 빈 문자(empty string)를 반환한다.
1 |
|
빈 문자(empty string)는 어떤 캐릭터도 포함하지 않고 길이도 0이지만, 다른 문자열과 동일하게 사용할 수 있다.
문자열은 변경 불가능 (Strings Are Immutable)
문자열을 변경하기 위해서 변수 옆에 연산자[]
를 사용해보면 다음과 같은 결과를 얻을 수 있다.
1 |
|
위와 같은 오류가 발생하는 이유는 기존에 있던 문자열을 변경할 수 없기 때문이다. 가장 좋은 방법은 기존에 있던 문자열을 새로운 문자열로 재할당하는 것이다.
1 |
|
이 예시에서는 새로운 첫 글자를 greeting
애 연결한 후 재할당한다. 원래 문자열 내부를 변경하는 것은 아니다.
문자 보간법 (String Interpolation)
위의 예시처럼 곱셈 연산자인 *
을 사용하여 연결하면 복잡한 변경이 요구될 때 번거로울 수 있다. 줄리아는 $
을 사용하여 문자열 보간을 허용한다. 보간(Interpolation)이란 쉽게 설명하면 사이에 채워넣는 것을 의미한다.
1 |
|
위의 방법은 *
을 사용하는 것보다 더 편리하고 읽기도 쉽다.
$
다음에 작성되는 모든 표현식들은 문자열로 간주된다. 따라서 괄호를 사용하여 모든 표현식을 문자열로 보간할 수 있다.
1 |
|
검색 (Searching)
아래의 함수는 어떤 기능을 할까?
1 |
|
find()
는 []
연산자의 역수와 같다. 인덱스를 받아서 해당 문자를 축출하는 대신에 해당 문자를 받아서 인덱스를 축출한다. 문자를 찾지 못하면 함수는 -1을 반환한다.
위의 while
루프는 word[index] == letter
인 경우 루프를 벗어나 값을 바로 반환한다.
만약 문자열에서 찾으려는 문자를 찾지 못하면, -1을 반환한다.
위와 같이 일련의 문자열에서 특정 문자에 대한 인덱스를 반환해주는 계산 방식을 검색(Searching)이라고 한다.
루핑과 카운팅 (Looping and Counting)
아래의 코드는 해당 단어에서 a
가 몇 번 있는지 세어서 반환해준다.
1 |
|
이 프로그램은 카운터(counter)라 불리는 다른 계산 방식이다. 변수 counter
는 0으로 초기화되고, a
가 나타날 때마다 1씩 증가한다. 루프에서 빠져나가면, counter
가 가진 결과를 반환한다.
문자열 라이브러리 (String Library)
줄리아는 물자열에서 사용할 수 있는 유용한 함수들을 제공한다. 예를 들어서 uppercase()
는 문자열들을 인수로 받아서 대문자로 변경해준다.
1 |
|
또한 우리가 작성했던 find()
와 유사한 함수인 findfirst()
도 제공한다.
1 |
|
사실은 findfirst()
가 우리가 만든 함수보다 더 일반적이다. 이 함수는 캐릭터뿐만 아니라 문자열의 일부도 찾아준다.
1 |
|
findfirst()
는 문자열의 첫 번째부터 특정 문자들을 찾는다. 만약 특정 지점부터 문자를 찾고 싶다면 findnext()
를 사용하면 된다. findnext()
는 세 번째 인수에 특정 지점 인덱스를 추가하면 된다.
1 |
|
∈
연산자
연산자 ∈(\in TAB)
은 불 연산자로서, 특정 문자가 문자열에 속했는지 여부를 보여준다.
1 |
|
예로, 아래의 함수는 단어1과 단어2에 모두 들어 있는 문자들만 출력한다.
1 |
|
변수 이름을 잘 선택하면, 코드를 자연어처럼 읽을 수 있다. "첫 번째 단어의 문자가 두 번째 단어에 속해있다면 문자를 인쇄하라"
아래의 코드는 oranges
와 apples
를 비교한 예시이다.
1 |
|
문자열 컴포지션 (String Comparison)
관계 연산자들은 문자열에서 작동한다. 다음은 두 문자가 동일한지 ==
연산자를 사용한 예시이다.
1 |
|
다른 관계 연산자들도 알파벳순으로 단어를 넣을 때 사용할 수 있다. 참고로 줄리아는 대문자와 소문자를 구별하며, 알파벳 순서는 대문자가 소문자보다 앞의 글자로 인식된다.
1 |
|
Tip 위의 문제를 해결하는 가장 보편적인 방법은 모든 문자열을 소문자와 같은 기준 형식으로 변경하는 것이다.
디버깅
시퀀스에서 인덱스를 사용해 값을 순회하면 순회의 시작과 끝을 얻기가 어렵다. 다음은 두 단어를 비교하고 한 단어가 다른 단어와 반대인 경우 true
를 반환하는 함수를 볼 것이다. 하지만 이 함수는 두 가지의 오류를 가지고 있다.
1 |
|
위 함수의 첫 번째 줄인 if
문은 두 단어의 길이가 똑같지 않으면 즉시 false
를 반환한다. 아무것도 반환되지 않는다면 두 단어의 길이가 같다고 가정할 수 있다.
i
와 j
는 인덱스이며, i
는 word1
에서 앞에서 뒤로 이동하며, j
는 word2
뒤에서 앞으로 이동한다. i
와 j
가 일치하지 않는 순간 while
문은 false
를 반환한다. 모든 글자가 일치하여 전체 루프를 통과하면 함수는 true
를 반환한다.
여기서 사용된 lastindex()
는 문자열의 마지막 인덱스를 반환하고 prevind()
는 이후 사용될 문자의 인덱스를 미리 반환하는 것이다.
지금부터는 "pots"와 "stop" 두 단어를 사용하여 함수를 테스트 할 것이다.
1
2julia> isreverse("pots", "stop")
false
분명히 반대로 쓰여진 단어인데도 함수는 false
를 반환했다. 이런 오류들을 디버깅하기 위해서 첫 번째로 할당된 인덱스들을 인쇄해볼 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function isreverse(word1, word2)
if length(word1) != length(word2)
return false
end
i = firstindex(word1)
j = lastindex(word2)
while j >= 0
j = prevind(word2, j)
@show i j
if word1[i] != word2[j]
return false
end
i = nextind(word1, i)
end
true
end
위의 코드로 프로그램을 다시 작동해보면, 아래와 같은 정보를 얻을 수 있다.
1
2
3
4julia> isreverse("pots", "stop")
i = 1
j = 3
false
첫 번째 루프에서 j
의 값은 4이어야 하는데, 3이 나왔다. 이것은 j = prevind(word2, j)
를 while
루프 맨 끝으로 옮겨야 한다는 것을 의미한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function isreverse(word1, word2)
if length(word1) != length(word2)
return false
end
i = firstindex(word1)
j = lastindex(word2)
while j >= 0
@show i j
if word1[i] != word2[j]
return false
end
i = nextind(word1, i)
j = prevind(word2, j)
end
true
end
위 코드를 작동시키면 결과는 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13julia> isreverse("pots", "stop")
i = 1
j = 4
i = 2
j = 3
i = 3
j = 2
i = 4
j = 1
i = 5
j = 0
ERROR: BoundsError: attempt to access "pots"
at index [5]
이번에는 BoundsError
가 발생하였다. i
의 값이 5가 되고 문자열 "pots"의 범위를 벗어난다.
Tip 위의 오류를 해결한 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function isreverse(word1, word2)
if length(word1) != length(word2)
return false
end
i = firstindex(word1)
j = lastindex(word2)
while j >= 0
@show i j
if word1[i] != word2[j]
return false
end
i = nextind(word1, i)
j = prevind(word2, j)
end
true
end