문자열

[strings]

문자열은 시퀀스

문자열은 문자들의 시퀀스(sequence)입니다. 대괄호 연산자를 써서 한번에 한 문자씩 다룰 수 있습니다:

>>> fruit = 'banana'
>>> letter = fruit[1]

두번째 문장은 fruit 에서 1번 문자를 선택해서 letter에 대입합니다.

대괄호 안의 표현식을 지수(index)라고 부릅니다. 지수는 시퀀스에서 어떤 문자를 원하는지를 가리킵니다.

하지만 여러분인 기대하는 것을 얻지 못합니다:

>>> print letter
a

대부분의 사람들에게, 'banana' 의 첫번째 글자는 a 가 아니라 b입니다. 그러나, 컴퓨터 과학자들에게, 지수는 문자열의 처음으로부터의 오프셋(offset)이고, 첫 번째 문자의 오프셋은 0입니다.

>>> letter = fruit[0]
>>> print letter
b

그래서 b는 'banana'의 0번 문자, a는 1번 문자, n은 2번 문자입니다.

지수로 변수와 연산자를 포함하는 임의의 표현식을 사용할 수 있습니다만, 지수의 값은 정수가 되어야만 합니다. 그렇지 않으면 이렇게 됩니다:

>>> letter = fruit[1.5]
TypeError: string indices must be integers

len

len은 문자열에 있는 문자의 개수를 돌려주는 내장함수입니다:

>>> fruit = 'banana'
>>> len(fruit)
6

문자열의 마지막 문자를 얻기 위해, 이렇게 하고 싶을 수 있습니다: To get the last letter of a string, you might be tempted to try something like this:

>>> length = len(fruit)
>>> last = fruit[length]
IndexError: string index out of range

IndexError의 이유는 ’banana’ 에 지수 6인 문자는 없기 때문입니다. 0부터 세기 시작하기 때문에, 여섯 개의 문자는 0번부터 5번까지 입니다. 마지막 문자를 얻기 위해서는, length 에서 1을 빼야만 합니다:

>>> last = fruit[length-1]
>>> print last
a

대신에, 음의 지수를 이용할 수 있는데, 문자열의 끝에서부터 세게 됩니다. 표현식 fruit[-1] 은 마지막 문자를 주고, fruit[-2]는 끝에서 두 번째 문자를 주는 식입니다.

for 순환 탐색

[for]

많은 종류의 계산들이 한번에 한 문자씩 문자열을 처리합니다. 종종 처음에서 시작해서, 차례대로 각 문자들은 선택한 후, 그 것으로 뭔가를 하고 끝까지 반복합니다. 이런 유형의 처리를 탐색(traversal)이라고 합니다. 탐색을 쓰는 한가지 방법은 while 순환을 이용하는 것입니다:

index = 0
while index < len(fruit):
    letter = fruit[index]
    print letter
    index = index + 1

이 순환은 문자열을 탐색하면서 한 줄에 한 글자씩 인쇄합니다. 순환 조건이 index < len(fruit) 이기 때문에, index 가 문자열의 길이와 같아질 때, 조건이 거짓이 되고, 순환의 바디는 실행되지 않게 됩니다. 참조되는 마지막 문자는 지수 len(fruit)-1 에 있는 것인데, 문자열의 마지막 문자입니다.

[연습 8.1.]

문자열을 인자로 받아서 한 줄에 하나씩 문자를 역순으로 인쇄하는 함수를 작성하세요.

탐색을 쓰는 또 하나의 방법은 for 순환을 사용하는 것입니다:

for char in fruit:
    print char

매 반복마다, 문자열의 다음 문자가 변수 char에 대입됩니다. 아무런 문자도 남지 않을 때까지 순환은 반복됩니다.

다음 예는 알파벳순서를 만드는데 접합(문자열 더하기)과 for 순환을 사용하는 법을 보여줍니다. 로버트 맥클로스키(Robert McCloskey) 의 책 Make Way for Ducklings 에서, 새끼오리들의 이름은 Jack, Kack, Lack, Mack, Nack, Ouack, Pack, Quack 입니다. 이 순환은 이 이름들을 순서대로 출력합니다:

prefixes = 'JKLMNOPQ'
suffix = 'ack'

for letter in prefixes:
    print letter + suffix

출력은:

Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack

물론, “Ouack” 과 “Quack”의 철자가 틀렸으니 아주 맞는 것은 아닙니다.

[연습 8.2.]

Modify the program to fix this error.

문자열 슬라이스

[slice]

문자열의 일부분을 슬라이스(slice)라고 부릅니다. 슬라이스를 선택하는 방법은 문자를 선택하는 것과 비슷합니다:

>>> s = 'Monty Python'
>>> print s[0:5]
Monty
>>> print s[6:12]
Python

연산자 은 “n번” 에서 “m번” 문자까지 가는 문자열의 일부를 돌려주는데, n번 문자는 포함하고, m번 문자는 포함하지 않습니다. 이런 행동이 상식에는 반하지만, 그림 [fig.banana]에서처럼, 문자 사이를 가리키는 지수를 상상해보면 도움이 될 수 있습니다.

banana

[fig.banana]

(콜론 앞에 있는) 첫 번째 지수를 생략하면, 슬라이스는 문자열의 처음에서 시작합니다. 두 번째 지수를 생략하면, 슬라이스는 문자열의 끝까지 갑니다:

>>> fruit = 'banana'
>>> fruit[:3]
'ban'
>>> fruit[3:]
'ana'

첫 번째 지수가 두 번째보다 크거나 같으면 결과는 ,두 개의 따옴표로 표시되는, 빈 문자열이 됩니다:

>>> fruit = 'banana'
>>> fruit[3:3]
''

빈 문자열은 문자를 포함하지 않고 길이가 0인 것을 제외하고는 다른 문자열들과 동일합니다.

[연습 8.3.]

fruit가 문자열일 때, fruit[:]가 뜻하는 것은 무엇입니까?

문자열은 수정불가

문자열에 있는 문자를 바꾸려는 목적으로, 대입문의 좌변에서 연산자를 사용하고 싶을 수 있습니다. 예를 들어:

>>> greeting = 'Hello, world!'
>>> greeting[0] = 'J'
TypeError: object does not support item assignment

이 경우에 “객체(object)”는 문자열이고, “항목(item)”은 여러분이 대입하려고 하는 문자입니다. 지금 당장은, 객체는 값과 같습니다만 나중에 정의를 개선할 것입니다. 항목은 시퀀스에 있는 값 중의 하나입니다.

오류는 문자열이 수정불가하기 때문에 발생하는데, 이미 존재하는 문자열을 변경할 수 없다는 뜻입니다. 여러분이 할 수 있는 최선은 원본의 변종인 새 문자열을 만드는 것입니다:

>>> greeting = 'Hello, world!'
>>> new_greeting = 'J' + greeting[1:]
>>> print new_greeting
Jello, world!

이 예는 새로운 첫 문자에 greeting의 슬라이스를 접합합니다. 원 문자열에는 어떤 영향도 주지 않습니다.

검색

[find]

다음 함수가 하는 일은 무엇일까요?

def find(word, letter):
    index = 0
    while index < len(word):
        if word[index] == letter:
            return index
        index = index + 1
    return -1

어떤 면에서, find는 의 역입니다. 지수를 받아서 대응하는 문자를 뽑아내는 대신, 문자를 받아서 문자가 등장하는 곳의 지수를 찾습니다. 문자가 발견되지 않으면, 함수는 -1을 돌려줍니다.

이 것이 순환 내에서 return 문을 사용한 첫 번째 예입니다. 만약 word[index] == letter 면, 함수는 순환을 멈추고 즉시 복귀합니다.

만약 문자가 문자열에 등장하지 않으면, 프로그램은 순환을 정상적으로 마치고 -1을 돌려줍니다.

이런 유형의 계산—시퀀스를 탐색하다가 찾는 것을 발견하면 돌려주는 것—을 검색이라고 합니다.

[연습 8.4.]

find를 수정해서, word 내의 지수를 세 번째 매개변수로 받아들여, 어디서부터 검색을 시작할지를 지정할 수 있도록 하세요.

순환과 세기

[counter]

다음 프로그램은 문자열에서 a가 나타나는 횟수를 셉니다:

word = 'banana'
count = 0
for letter in word:
    if letter == 'a':
        count = count + 1
print count

이 프로그램은 계수기(counter)라고 불리는 다른 하나의 계산 유형을 예시합니다. 변수 count는 0으로 초기화된 다음 a가 발견될 때마다 1씩 증가합니다. 순환이 종료할 때, count는 결과를 갖게 됩니다—a의 총 개수.

[연습 8.5.]

이 코드를 count라는 이름의 함수로 캡슐화하고, 문자열과 문자를 인자로 받아들이도록 일반화하세요.

[연습 8.6.]

문자열을 탐색하는 대신, 앞 절에서 나온 인자 세 개 버전의 find를 사용하도록 이 함수를 다시 작성하세요.

문자열 메쏘드

메쏘드(method)는 함수와 비슷합니다—인자를 받아들이고 값을 돌려줍니다—. 하지만 문법이 다릅니다. 예를 들어, 메쏘드 upper는 문자열을 받아들여서 모두 대문자로 된 새 문자열을 돌려줍니다:

함수 문법 upper(word) 대신에, 메쏘드 문법 word.upper()를 사용합니다.

>>> word = 'banana'
>>> new_word = word.upper()
>>> print new_word
BANANA

이런 형식의 점 표기법은 메쏘드의 이름, upper,와 메쏘드를 적용할 문자열의 이름, word,을 지정합니다. 빈 괄호는 이 메쏘드가 아무런 인자도 받아들이지 않음을 가리킵니다.

메쏘드를 실행하는 것을 호출(invocation)이라고 부릅니다; 이 경우에, word에 대해 upper를 호출한다고 말합니다.

알고 보면, 우리가 작성한 함수와 아주 비슷한 find 라는 이름의 문자열 메쏘드가 있습니다:

>>> word = 'banana'
>>> index = word.find('a')
>>> print index
1

이 예에서, 우리는 word에 대해 find를 호출하고, 우리가 찾는 문자를 매개변수로 전달했습니다.

사실, find 메쏘드는 우리 함수보다 더 일반적입니다; 문자뿐만 아니라 부분 문자열을 찾을 수 있습니다.

>>> word.find('na')
2

검색을 시작할 지수를 두 번째 인자로 받을 수 있습니다:

>>> word.find('na', 3)
4

그리고 세 번째 인자는 멈춰야 할 지수를 가리킵니다:

>>> name = 'bob'
>>> name.find('b', 1, 2)
-1

이 검색이 실패하는 이유는 b가 1 에서 2까지의 지수 범위(2는 포함하지 않음)에 나타나지 않기 때문입니다.

[연습 8.7.]

count라는 이름의 문자열 메쏘드가 있는데, 앞의 연습에서 나온 함수와 유사합니다. 이 메쏘드에 관한 설명서를 읽고, 'banana'에 등장하는 a를 세는 호출을 작성하세요.

[연습 8.8.]

http://docs.python.org/2/library/stdtypes.html#string-methods에서 문자열 메쏘드에 관한 설명서를 읽으세요. 어떻게 동작하는지 정확히 이해하기 위해, 그 중 몇 가지로 실험해 보고 싶을 겁니다 strip 과 replace는 특히 유용합니다.

설명서는 헛갈릴 수 있는 문법을 사용합니다. 예를 들어, find(sub[, start[, end]]) 에서, 대괄호는 생략 가능한 인자를 나타냅니다. 그래서, sub는 필수고, start는 선택사항입니다. 그리고, 만약 start 를 포함시킨다면, end가 선택사항이 됩니다.

in 연산자

[inboth]

in은 두 개의 문자열을 받아들여서, 첫 번째가 두 번째에 부분 문자열로 등장하면 True를 돌려주는 논리 연산자입니다:

>>> 'a' in 'banana'
True
>>> 'seed' in 'banana'
False

예를 들어, 다음 함수는 word2에도 동시에 등장하는 word1의 모든 문자들을 인쇄합니다:

def in_both(word1, word2):
    for letter in word1:
        if letter in word2:
            print letter

잘 선택된 변수 명을 사용할 때, 파이썬은 때로 영어처럼 읽힙니다. 여러분은 이 순환을 이렇게 읽을 수 있습니다, “for (each) letter in (the first) word, if (the) letter (appears) in (the second) word, print (the) letter.”

apples 과 oranges를 비교하면 이렇게 됩니다:

>>> in_both('apples', 'oranges')
a
e
s

문자열 비교

비교 연산자를 문자열에 사용할 수 있습니다. 두 문자열이 같은지 보기 위해:

if word == 'banana':
    print 'All right, bananas.'

다른 비교 연산자들은 단어들을 알파벳 순으로 배치하는데 유용합니다:

if word < 'banana':
    print 'Your word,' + word + ', comes before banana.'
elif word > 'banana':
    print 'Your word,' + word + ', comes after banana.'
else:
    print 'All right, bananas.'

파이썬은 대문자와 소문자를 사람들과 같은 방식으로 다루지 않습니다. 모든 대문자는 모든 소문자 앞에 옵니다, 그래서:

Your word, Pineapple, comes before banana.

이런 문제를 다루는 일반적인 방법은 비교전에 문자열들을 표준 형식, 가령 모두 소문자,으로 바꾸는 것입니다. 수류탄(Pineapple)으로 무장한 사람으로부터 스스로를 지켜야 할 때를 위해 명심해 두세요.

디버깅

시퀀스의 값들을 탐색하려고 지수를 사용할 때, 탐색의 처음과 끝에서 틀리기 쉽습니다. 두 단어를 비교해서, 한 단어가 다른 하나의 역이면 True를 돌려주기로 되어있는 함수가 있습니다만, 두 개의 오류를 갖고 있습니다:

def is_reverse(word1, word2):
    if len(word1) != len(word2):
        return False

    i = 0
    j = len(word2)

    while j > 0:
        if word1[i] != word2[j]:
            return False
        i = i+1
        j = j-1

    return True

첫 번째 if 문은 단어들의 길이가 같은지 검사합니다. 다르면 즉시 False를 돌려줄 수 있습니다. 그러고는, 함수의 남은 부분에서, 우리는 단어들의 길이가 같다고 가정할 수 있습니다. 이 것은 [guardian] 절에서 나온 파수꾼 패턴의 예입니다.

i 와 j는 지수입니다: i 는 word1를 정 방향으로 탐색하고, j는 word2를 역방향으로 탐색합니다. 만약 두 글자가 다른 경우를 발견하면, 즉시 False를 돌려줄 수 있습니다. 순환 전체를 통과해서 모든 글자들이 같음이 확인되면 True를 돌려줍니다.

이 함수를 단어 “pots” 와 “stop” 으로 검사하면, True를 돌려주리라고 기대합니다만, IndexError가 발생합니다:

>>> is_reverse('pots', 'stop')
...
  File "reverse.py", line 15, in is_reverse
    if word1[i] != word2[j]:
IndexError: string index out of range

이런 종류의 오류를 디버깅할 때, 제가 첫 번째로 하는 일은, 오류가 발생한 줄 바로 앞에서 지수의 값을 인쇄해보는 것입니다.

while j > 0:
    print i, j        # print here

    if word1[i] != word2[j]:
        return False
    i = i+1
    j = j-1

이제 프로그램을 다시 실행하면, 정보를 좀 더 얻게 됩니다:

>>> is_reverse('pots', 'stop')
0 4
...
IndexError: string index out of range

첫 번째 순환에서, j의 값은 4인데, 문자열 'pots'의 범위를 벗어납니다. 마지막 문자의 지수는 3이기 때문에 j의 초기값은 len(word2)-1이 되어야 합니다.

이 오류를 고치고 프로그램을 다시 실행하면, 이렇게 됩니다:

>>> is_reverse('pots', 'stop')
0 3
1 2
2 1
True

이번에는 올바를 답을 얻었지만, 순환이 단지 세 번만 실행된 것처럼 보이는 게 수상쩍습니다. 무슨 일이 일어나고 있는지 더 잘 파악하는데, 상태도가 유용합니다. 첫 번째 반복에서, is_reverse의 프레임이 그림 [fig.state4]에 나와있습니다.

state4

[fig.state4]

프레임에 변수들을 배치하고, i 와 j의 값이 word1 와 word2의 문자를 가리키는 것을 보이도록 점선을 추가함으로써 약간의 융통성을 발휘했습니다.

[연습 8.9.] [isreverse]

이 다이어그램으로 시작해서, 각 반복마다 i 와 j 의 값을 바꾸면서, 프로그램을 종이에서 실행해 보세요. 이 함수의 두 번째 오류를 찾아서 고치세요.

용어

객체 object:
변수가 가리킬 수 있는 것. 지금 당장은, “객체” 와 “값”을 같은 의미로 사용할 수 있다.
시퀀스 sequence:
순서가 있는 집합; 즉, 각 원소가 정수 지수를 통해 구분될 수 있는 값의 집합.
항목 item:
시퀀스에 들어있는 한 값.
지수 index:
시퀀스의 항목, 문자열의 문자 같은, 을 선택하는데 사용되는 정수 값.
슬라이스 slice:
지수의 범위로 지정되는 문자열의 일부.
빈 문자열 empty string:
문자를 포함하지 않고 길이가 0인 문자열로 두 개의 따옴표로 표시된다.
수정불가 immutable:
항목이 변경될 수 없는 시퀀스의 성질.
탐색하기 traverse:
매번 비슷한 연산을 수행하면서 시퀀스의 항목들에 대해 순환하는 것.
검색 search:
찾는 것을 발견할 때 멈추는 탐색의 한 유형.
계수기 counter:
뭔가를 세는데 사용되는 변수, 보통 0으로 초기화된 후에 증가한다.
메쏘드 method:
객체와 결합하고 점 표기법으로 호출되는 함수.
호출 invocation:
메쏘드를 실행하는 문장.

연습

[연습 8.10.]

문자열 슬라이스는 “스텝(step size)”을 지정하는 세 번째 지수를 취할 수 있습니다. 스텝은 연속된 문자 사이에 있는 공백의 수입니다. 스텝 2는 하나 건너 한 글자를, 3은 3개에 한 글자를 뜻합니다.

>>> fruit = 'banana'
>>> fruit[0:5:2]
'bnn'

스텝 -1은 단어를 역순으로 만드는데, 슬라이스 [::-1] 는 뒤집힌 문자열을 만듭니다.

이 숙어를 사용해서, 연습 [palindrome]에서 나온 is_palindrome의 한 줄짜리 버전을 작성하세요.

[연습 8.11.]

다음 함수들은 모두 문자열이 소문자를 포함하고 있는지를 조사하기로 되어있습니다만, 적어도 일부는 잘못되었습니다. 각각의 함수마다, 함수가 실제로 무엇을 하는지 설명하세요(매개변수는 문자열이라고 가정합니다).

def any_lowercase1(s):
    for c in s:
        if c.islower():
            return True
        else:
            return False

def any_lowercase2(s):
    for c in s:
        if 'c'.islower():
            return 'True'
        else:
            return 'False'

def any_lowercase3(s):
    for c in s:
        flag = c.islower()
    return flag

def any_lowercase4(s):
    flag = False
    for c in s:
        flag = flag or c.islower()
    return flag

def any_lowercase5(s):
    for c in s:
        if not c.islower():
            return False
    return True

[연습 8.12.]

[exrotate] ROT13은 단어의 각 글자를 13자리만큼 “회전” 시키는 방식의 간단한 암호 법입니다. 글자를 회전한다는 것은 알파벳 상의 위치를 이동, 필요하면 처음으로 돌아가서, 한다는 뜻인데, ’A’ 를 3만큼 이동하면 ’D’가, ’Z’ 를 1만 큼 이동하면 ’A’가 됩니다.

문자열과 정수를 매개변수로 받아들여서 원래 문자열을 요청한 양만큼 “회전”시킨 문자열을 돌려주는 함수 rotate_word를 작성하세요.

예를 들어, “cheer” 를 7만큼 회전하면 “jolly” 이고 “melon” 을 -10만큼 회전하면 “cubed”가 됩니다.

아마 내장함수 ord, 문자를 숫자 코드로 바꿉니다, 와 chr, 숫자 코드를 문자로 바꿉니다,를 사용하고 싶을 겁니다.

인터넷 상에서 잠재적으로 공격적인 농담들은 때로 ROT13으로 암호화됩니다. 쉽게 상처입지 않는다면, 찾아서 해독해보세요. 답: http://thinkpython.com/code/rotate.py.