[7/20] 문자열

글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.

이 포스트는 Strings를 한글로 요약 정리한 글입니다.

문자열 (Strings)

문자열은 정수나 소수, 불 표현식과는 다르다. 이 장에서는 문자열을 구성하는 문자에 접근하는 방법을 배우고, 줄리아가 제공하는 문자열 도우미 함수 중 일부에 대해서 알아볼 것이다.

캐릭터 (Characters)

영어를 사용하는 사람들은 알파벳, 숫자 및 일반적인 문장 부호와 같은 문자에 익숙하다. 이런 문자들은 표준화되어 ASCII 표준에 따라 0부터 127사이의 정수 값으로 일대일 배치한다. 영어가 아닌 한국어나 중국어, 아랍어와 같아느 언어들도 ASCII 표준에 포함되어 있다.

유니 코드 표준은 복잡하게 구성된 문자들도 정확히 표현할 수 있도록 고안되었기 때문에 일반적으로 주요한 표준으로 사용되고 있으며, 전 세계적으로 모든 문자들에 고유 번호를 제공한다.

Char 값은 단일 문자를 나타내며, 작은 따옴표로 묶는다.

1
2
3
4
5
6
julia> 'x'
'x': ASCII/Unicode U+0078 (category Ll: Letter, lowercase)
julia> '🍌'
'🍌': Unicode U+01f34c (category So: Symbol, other)
julia> typeof('x')
Char

위의 바나나와 같은 이모티곤 또한 유니코드 표준에 포함되어 있다. (:banana: TAB)

문자열은 시퀀스이다. (A String Is a Sequence)

문자열은 일련의 문자이다. 대괄호 연산자를 사용하여 한 번에 하나씩 문자에 엑세스 할 수 있다.

1
2
3
4
julia> fruit = "banana"
"banana"
julia> letter = fruit[1]
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)

두 번째 명령문은 fruit에서 첫 번째 문자를 선택하여 letter라는 변수에 할당한다.

위와 같이 대괄호[]를 내부에 있는 표현식을 인덱스(index)라고 부른다. 인덱스는 문자열 중에서 원하는 문자가 어느 위치에 있는지를 나타낸다.

줄리아는 모든 인덱싱을 1부터 시작한다. 즉, 첫 번째 문자는 인덱스 1로 찾을 수 있으며, 마지막 문자는 인덱스 end에서 찾을 수 있다.

1
2
julia> fruit[end]
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)

또한 인덱스는 변수나 연산자가 포함된 표현식들도 사용될 수 있다.

1
2
3
4
5
6
julia> i = 1
1
julia> fruit[i+1]
'a': ASCII/Unicode U+0061 (category Ll: Letter, lowercase)
julia> fruit[end-1]
'n': ASCII/Unicode U+006e (category Ll: Letter, lowercase)

그러나 인덱스 값이 정수가 아니라면 오류 메시지가 뜬다.

1
2
julia> letter = fruit[1.5]
ERROR: MethodError: no method matching getindex(::String, ::Float64)

length

length는 내장 함수로 문자열 안에 있는 문자들의 개수를 반환한다.

1
2
3
4
julia> fruits = "🍌 🍎 🍐"
"🍌 🍎 🍐"
julia> len = length(fruits)
5

위의 예시를 보면 length는 이모티콘 3개와 그 사이의 띄어쓰기를 포함하여 총 길이를 5라고 반환한다.

문자열의 마지막 문자를 얻고 싶다면, 아래의 코드를 입력하면 된다.

1
2
julia> last = fruits[len]
' ': ASCII/Unicode U+0020 (category Zs: Separator, space)

하지만 기대했던 것과는 다른 결과가 나온다. 문자열은 다양한 UTF-8 인코딩을 사용하여 인코딩되었다. UTF-8은 가변적으로 폭이 변하는 인코딩 (variable-width encoding)으로 모든 문자가 동일한 바이트 수로 인코딩되지 않는다.

sizeof()는 문자의 바이트 수를 보여준다.

1
2
julia> sizeof("🍌")
4

위 이모티콘은 4바이트로 인코딩되었고 문자열 인덱싱은 바이트 기반이기 때문에 fruits의 5번째 요소는 띄어쓰기인 SPACE 이다.

이것은 UTF-8 문자열에서 모든 문자들은 정확히 몇 바이트로 인덱싱 되었는지 확인해야 한다는 것을 보여준다. 만약 눈으로 파악한 인덱스를 사용한다면, 잘못된 결과를 도출할 수 있다.

1
2
julia> fruits[2]
ERROR: StringIndexError("🍌 🍎 🍐", 2)

fruits예시에서는 🍌가 4바이트이기 때문에 인덱스 2,3,4는 반환되지 않으며, 그 다음 유효한 인덱스는 5이다. 다음 유효한 인덱스를 찾고 싶다면 nextind(fruits, 1)을 입력하면 된다. 이후 5 다음으로 유효한 인덱스를 찾고 싶다면 nextind(fruits, 5)를 실행하면 된다.

순회 (Traversal)

많은 계산들은 한 번에 한 문자씩 문자열을 처리한다. 이때 컴퓨터는 하나의 문자를 선택하여 해당 명령들을 수행한 이후, 다른 문자를 선택하여 이를 반복한다. 이것을 순회(Traversal)이라고 하며, 순회를 작성하는 방법은 while 루프를 사용하는 것이다.

1
2
3
4
5
6
index = firstindex(fruits)
while index <= sizeof(fruits)
letter = fruits[index]
println(letter)
global index = nextind(fruits, index)
end

위의 코드는 변수 indexfruits의 첫 번째 인덱스를 할당한다. 그 후 while루프 조건으로 넘어간다. 조건은 indexfruits의 인덱스 총 길이보다 작으면 true을 도출하여 루프를 실행하거나, 크면 false를 도출하여 루프를 종료한다.

firstindex()는 인수의 첫 번째 인덱스를 반환한다. 키워드 global는 기존에 정의했던 변수에 새로운 값을 재할당한다.

문자열 슬라이스 (String Slices)

문자열의 일부를 슬라이스(Slices)라고 부른다. 슬라이스는 문자를 축출하는 것과 비슷하다.

1
2
3
4
julia> str = "Julius Caesar";

julia> str[1:6]
"Julius"

연산자 [n:m]은 인덱스 n번째 바이트부터 m번째 바이트까지의 문자들을 반환한다. 그렇기 때문에 인덱스 위치를 잘 파악하고 있어야 한다.

키워드 end는 마지막 문자를 반환한다.

1
2
julia> str[8:end]
"Caesar"

만약에 첫 번째 인덱스의 수가 두 번째 인덱스의 수보다 크다면 빈 문자(empty string)를 반환한다.

1
2
julia> str[8:7]
""

빈 문자(empty string)는 어떤 캐릭터도 포함하지 않고 길이도 0이지만, 다른 문자열과 동일하게 사용할 수 있다.

문자열은 변경 불가능 (Strings Are Immutable)

문자열을 변경하기 위해서 변수 옆에 연산자[]를 사용해보면 다음과 같은 결과를 얻을 수 있다.

1
2
3
4
julia> greeting = "Hello, world!"
"Hello, world!"
julia> greeting[1] = 'J'
ERROR: MethodError: no method matching setindex!(::String, ::Char, ::Int64)

위와 같은 오류가 발생하는 이유는 기존에 있던 문자열을 변경할 수 없기 때문이다. 가장 좋은 방법은 기존에 있던 문자열을 새로운 문자열로 재할당하는 것이다.

1
2
julia> greeting = "J" * greeting[2:end]
"Jello, world!"

이 예시에서는 새로운 첫 글자를 greeting애 연결한 후 재할당한다. 원래 문자열 내부를 변경하는 것은 아니다.

문자 보간법 (String Interpolation)

위의 예시처럼 곱셈 연산자인 *을 사용하여 연결하면 복잡한 변경이 요구될 때 번거로울 수 있다. 줄리아는 $을 사용하여 문자열 보간을 허용한다. 보간(Interpolation)이란 쉽게 설명하면 사이에 채워넣는 것을 의미한다.

1
2
3
4
5
6
julia> greet = "Hello"
"Hello"
julia> whom = "World"
"World"
julia> "$greet, $(whom)!"
"Hello, World!"

위의 방법은 *을 사용하는 것보다 더 편리하고 읽기도 쉽다.

$다음에 작성되는 모든 표현식들은 문자열로 간주된다. 따라서 괄호를 사용하여 모든 표현식을 문자열로 보간할 수 있다.

1
2
julia> "1 + 2 = $(1 + 2)"
"1 + 2 = 3"

검색 (Searching)

아래의 함수는 어떤 기능을 할까?

1
2
3
4
5
6
7
8
9
10
function find(word, letter)
index = firstindex(word)
while index <= sizeof(word)
if word[index] == letter
return index
end
index = nextind(word, index)
end
-1
end

find()[] 연산자의 역수와 같다. 인덱스를 받아서 해당 문자를 축출하는 대신에 해당 문자를 받아서 인덱스를 축출한다. 문자를 찾지 못하면 함수는 -1을 반환한다.

위의 while 루프는 word[index] == letter인 경우 루프를 벗어나 값을 바로 반환한다.

만약 문자열에서 찾으려는 문자를 찾지 못하면, -1을 반환한다.

위와 같이 일련의 문자열에서 특정 문자에 대한 인덱스를 반환해주는 계산 방식을 검색(Searching)이라고 한다.

루핑과 카운팅 (Looping and Counting)

아래의 코드는 해당 단어에서 a가 몇 번 있는지 세어서 반환해준다.

1
2
3
4
5
6
7
8
word = "banana"
counter = 0
for letter in word
if letter == 'a'
global counter = counter + 1
end
end
println(counter)

이 프로그램은 카운터(counter)라 불리는 다른 계산 방식이다. 변수 counter는 0으로 초기화되고, a가 나타날 때마다 1씩 증가한다. 루프에서 빠져나가면, counter가 가진 결과를 반환한다.

문자열 라이브러리 (String Library)

줄리아는 물자열에서 사용할 수 있는 유용한 함수들을 제공한다. 예를 들어서 uppercase()는 문자열들을 인수로 받아서 대문자로 변경해준다.

1
2
julia> uppercase("Hello, World!")
"HELLO, WORLD!"

또한 우리가 작성했던 find()와 유사한 함수인 findfirst()도 제공한다.

1
2
julia> findfirst("a", "banana")
2:2

사실은 findfirst()가 우리가 만든 함수보다 더 일반적이다. 이 함수는 캐릭터뿐만 아니라 문자열의 일부도 찾아준다.

1
2
julia> findfirst("na", "banana")
3:4

findfirst()는 문자열의 첫 번째부터 특정 문자들을 찾는다. 만약 특정 지점부터 문자를 찾고 싶다면 findnext()를 사용하면 된다. findnext()는 세 번째 인수에 특정 지점 인덱스를 추가하면 된다.

1
2
julia> findnext("na", "banana", 4)
5:6

연산자

연산자 ∈(\in TAB)은 불 연산자로서, 특정 문자가 문자열에 속했는지 여부를 보여준다.

1
2
julia> 'a'"banana"    # 'a' in "banana"
true

예로, 아래의 함수는 단어1과 단어2에 모두 들어 있는 문자들만 출력한다.

1
2
3
4
5
6
7
function inboth(word1, word2)
for letter in word1
if letter ∈ word2
print(letter, " ")
end
end
end

변수 이름을 잘 선택하면, 코드를 자연어처럼 읽을 수 있다. "첫 번째 단어의 문자가 두 번째 단어에 속해있다면 문자를 인쇄하라"

아래의 코드는 orangesapples를 비교한 예시이다.

1
2
julia> inboth("apples", "oranges")
a e s

문자열 컴포지션 (String Comparison)

관계 연산자들은 문자열에서 작동한다. 다음은 두 문자가 동일한지 == 연산자를 사용한 예시이다.

1
2
3
4
word = "Pineapple"
if word == "banana"
println("All right, bananas.")
end

다른 관계 연산자들도 알파벳순으로 단어를 넣을 때 사용할 수 있다. 참고로 줄리아는 대문자와 소문자를 구별하며, 알파벳 순서는 대문자가 소문자보다 앞의 글자로 인식된다.

1
Your word, Pineapple, comes before banana.

Tip 위의 문제를 해결하는 가장 보편적인 방법은 모든 문자열을 소문자와 같은 기준 형식으로 변경하는 것이다.

디버깅

시퀀스에서 인덱스를 사용해 값을 순회하면 순회의 시작과 끝을 얻기가 어렵다. 다음은 두 단어를 비교하고 한 단어가 다른 단어와 반대인 경우 true를 반환하는 함수를 볼 것이다. 하지만 이 함수는 두 가지의 오류를 가지고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function isreverse(word1, word2)
if length(word1) != length(word2)
return false
end
i = firstindex(word1)
j = lastindex(word2)
while j >= 0
j = prevind(word2, j)
if word1[i] != word2[j]
return false
end
i = nextind(word1, i)
end
true
end

위 함수의 첫 번째 줄인 if문은 두 단어의 길이가 똑같지 않으면 즉시 false를 반환한다. 아무것도 반환되지 않는다면 두 단어의 길이가 같다고 가정할 수 있다.

ij는 인덱스이며, iword1에서 앞에서 뒤로 이동하며, jword2 뒤에서 앞으로 이동한다. ij가 일치하지 않는 순간 while문은 false를 반환한다. 모든 글자가 일치하여 전체 루프를 통과하면 함수는 true를 반환한다.

여기서 사용된 lastindex()는 문자열의 마지막 인덱스를 반환하고 prevind()는 이후 사용될 문자의 인덱스를 미리 반환하는 것이다.

지금부터는 "pots"와 "stop" 두 단어를 사용하여 함수를 테스트 할 것이다.

1
2
julia> isreverse("pots", "stop")
false

분명히 반대로 쓰여진 단어인데도 함수는 false를 반환했다. 이런 오류들을 디버깅하기 위해서 첫 번째로 할당된 인덱스들을 인쇄해볼 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function 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
4
julia> 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
16
function 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
13
julia> 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
16
function 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


[7/20] 문자열
https://dev-bearabbit.github.io/ko/ThinkJulia/Think-Julia-Chapter-7/
Author
Jess
Posted on
2020년 3월 5일
Licensed under