문자열과 반복문

Think Python의 8장 내용을 요약 및 수정한 내용입니다.

문자열은 여러 개의 문자열들의 나열이라는 의미에서 모음(collection) 자료형의 값이라 부르기도 한다고 앞서 설명하였다. 즉, 여러 개의 값(문자)을 하나로 묶여 있는 것이 문자열이다.

그런데 필요할 때는 항목으로 사용된 각각의 문자를 확인하여 활용할 수 있어야 한다. 인덱싱과 슬라이싱도 이런 용도로 사용되는 도구이지만, 문자열의 길이가 엄청 길어지면 인덱스와 슬라이싱 구간을 수동으로 지정하기 어려운 경우가 발생한다. 즉, 자동화 기능이 요구된다.

whilefor 반복 명령문이 바로 자동화 기능을 지원한다. 특히, 문자열, 리스트, 튜플, 어레이, 사전 등의 자료형과 함께 사용할 때 두 반복문의 기능을 극대화할 수 있다.

참조: 반복 명령문을 지원하지 않는 프로그래밍 언어는 용도가 매우 제한적이며, 그런 프로그래밍 언어는 거의 사용되지 않는다.

문자열과 while 반복문

문자열과 while 반복문을 함께 사용하려면 인덱스를 활용해야 한다.

예를 들어, 문자열 'banana'에 사용된 항목들을 하나씩 차례대로 출력하려면 다음과 같이 한다.

In [13]:
fruit = 'banana'

idx = 0
while idx < len(fruit):
    letter = fruit[idx]
    print(letter)
    idx = idx + 1
    
print("최종 index =", idx)
b
a
n
a
n
a
최종 index = 6

위 코드 설명:

  • while 반복의 조건인 idx < len(fruit)가 거짓이 될 때까지 idx의 값이 0, 1, 2 등으로 변한다.
  • idx가 0, 1, ..., 5의 값을 가지면 해당 인덱스가 가리키는 위치의 문자를 출력한다.
  • idx에 할당된 값이 len(fruit)인 6이 되는 순간에 while 반복문을 벗어난다. 따라서 위 프로그램의 실행이 종료될 때 idx가 가리키는 값은 banana 문자열의 길이닌 6이 된다.

예제

주어진 문자열에 포함된 문자들을 역순으로 인쇄하는 프로그램은 다음과 같다.

In [14]:
fruit = 'apple'

idx = 1
while idx < len(fruit) + 1:
    letter = fruit[-idx]
    print(letter)
    idx = idx + 1
e
l
p
p
a

그런데 print 함수가 출력할 때마다 줄바꿈이 사용된다. 줄바꿈이 발생하지 않도록 하려면 다음과 같이 print 함수의 옵션 인자 설정 하나를 바꿔야 한다.

In [15]:
fruit = 'apple'

idx = 1
while idx < len(fruit) + 1:
    letter = fruit[-idx]
    print(letter, end='')
    idx = idx + 1
elppa

파이썬에서 특정 함수는 옵션 매개변수를 여러 개 가질 수 있다. print 함수도 여러 개의 옵션 매개변수를 가지면, end 가 그중에 하나이다. end는 출력을 마친 후 실행하는 추가 옵션을 담당하며 기본 옵션값은 줄바꿈('\n') 이다. 위 프로그램에서는 end 옵션 매개변수에 할당된 인자를 아무 것도 하지 않는 것을 의미하는 빈 문자열로 지정한 것이다.

예를 들어, 항목들을 쉼표(콤마)로 구분하고 싶다면 다음가 같이 한다.

In [16]:
fruit = 'apple'

idx = 1
while idx < len(fruit) + 1:
    letter = fruit[-idx]
    print(letter, end=', ')
    idx = idx + 1
e, l, p, p, a, 

print 함수의 기타 옵션 매개변수 중에 sep도 중요하다. sep 옵션 매개변수에 대한 설명은 코딩도장: 여러 값 출력하기를 참조하면 좋다.

위 프로그램은 슬라이싱을 사용하면 매우 간단하게 해결된다. 스텝이 음수이면 역순으로 슬라이싱을 적용한다.

In [17]:
fruit[::-1]
Out[17]:
'elppa'

주의: 항목들 사이에 쉼표 등을 삽입하는 기능은 슬라이싱이 제공하지 않는다.

예제: 문자 검색함수

인터넷 검색에서 가장 중요한 요소는 원하는 단어 또는 문장을 포함한 웹사이트, 문서 등을 찾는 일이다. 여기서는 문자열에 특정 문자가 포함되어 있는지 여부를 판단하는 findChar 함수를 기존 문자열 메서드를 전혀 사용하지 않으면서 구현해 본다.

아래 정의된 findChar 함수는 입력된 문자열에 특정 문자가 포함되어 있으면 그 문자가 가장 먼저 나타나는 곳의 인덱스를 찾아주고, 그렇지 않으면 -1을 내준다.

In [18]:
def findChar(word, letter):
    idx = 0
    while idx < len(word):
        if word[idx] == letter:
            return idx
        idx = idx + 1
    return -1
In [19]:
findChar('banana', 'a')
Out[19]:
1
In [20]:
findChar('banana', 'n')
Out[20]:
2
In [21]:
findChar('banana', 'o')
Out[21]:
-1

문자열과 for 반복 명령문

파이썬에서 문자열과 같은 모음 자료형은 while 반복문 보다 for 반복문과 사용하는 게 보다 효율적이다. 이유는 for 반복문 자체가 자동으로 인덱스를 1씩 키워주는 기능을 갖고 있기 때문이다.

주의: C와 자바는 for 반복문이 이런 기능을 제공하지 않는다.

따라서 앞서 while 반복문에서 인덱스가 1씩 커지는 것을 지원하는 idx 변수가 필요없게 된다. 예를 들어, 아래 코드는 fruit에 할당된 문자열의 각 항목을 차례대로 출력한다.

In [22]:
for letter in fruit:
    print(letter)
a
p
p
l
e
  • letter 변수는 처음에 a를 가리킨다.
  • 다음 과정을 letter 변수가 더 이상 가리킬 값(문자)가 없을 때까지 위 과정을 반복한다.
    • for 반복문의 본체를 실행한다.
    • letter 변수가 가리키는 값을 오른편에 위치한 값으로 대체한다.

예제

for 반복 명령문을 활용하여 특정 단어들을 생성하는 예제를 다룬다. 구체적으로, JKLMNOPQ에 포함된 문자를 하나씩 꺼내어 ack라는 문자열의 맨 앞에 붙힌 단어들을 생성한다.

In [23]:
# 접두사
prefixes = 'JKLMNOPQ'

#접미사
suffix = 'ack'

for letter in prefixes:
    print(letter + suffix)
Jack
Kack
Lack
Mack
Nack
Oack
Pack
Qack

예제

appleorange 두 단어에 공동으로 포함된 알파벳을 출력하는 방법은 다음과 같다.

In [24]:
word1 = 'apple'
word2 = 'orange'

for letter in word1:
    if letter in word2:
        print(letter)
a
e

두 개의 문자열에 공동으로 사용된 문자들을 모두 출력하는 함수 inBoth를 구현하려면 word1word2가 임의의 문자열을 가리킬 수 있도록 하면 된다. 즉, 두 변수를 매개변수로 사용하는 함수를 아래와 같이 선언하면 된다.

In [25]:
def inBoth(word1, word2):
    for letter in word1:
        if letter in word2:
            print(letter)
In [26]:
inBoth('apple', 'orange')
a
e

예제: 집계하기

다음 프로그램은 문자열에서 문자 'a'가 나타나는 횟수를 집계(카운팅, counting)할 수 있다.

In [27]:
word = 'orange'
count = 0
for letter in word:
    if letter == 'a':
        count = count + 1
print(word + '에서' + " 알파벳 a가", count, '번 사용되었어요.')
orange에서 알파벳 a가 1 번 사용되었어요.

주의: print 함수의 인자들을 의도적으로 복잡하게 사용하였다.

디버깅 연습: 인덱스 활용

아래의 is_reverse 함수는 인자로 입력된 두 문자열이 서로 뒤집어진 관계인지를 판단하는 기능을 갖도록 정의되었다.

In [28]:
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를 리턴하고 실행을 종료한다.
  • 두 단어의 길이가 같을 경우, 두 단어를 탐색하기 위해 두 개의 인덱스 지수인 ij 선언.
  • while 명령문
    • iword1를 순방향으로 탐색
    • jword2를 역방향으로 탐색
    • 만약 두 글자가 다른 경우를 발견하면, 즉시 False를 리턴하고 실행 종료.
  • while 명령문이 끝난 경우
    • 두 단어가 서로 뒤집힌 관계인을 의미
    • 따라서 True 리턴하고 실행 종료.

그런데 위 함수는 잘못 정의되어 있다. is_reverse 함수의 정의가 문법상으로는 하자가 없지만 실행을 하다보면 오류가 발생한다. 이와 같은 오류를 실행시간 오류(runtime error) 라 부른다.

In [29]:
is_reverse('pots', 'stop')
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-29-d368e6954e78> in <module>
----> 1 is_reverse('pots', 'stop')

<ipython-input-28-762d949ae797> in is_reverse(word1, word2)
      7 
      8     while j > 0:
----> 9         if word1[i] != word2[j]:
     10             return False
     11         i = i+1

IndexError: string index out of range

이런 종류의 오류를 디버깅할 때 오류를 발생시킨 명령문 바로 앞줄에서 인덱스의 값을 인쇄해보는 것이다.

In [30]:
def is_reverse(word1, word2):
    if len(word1) != len(word2):
        return False

    i = 0
    j = len(word2)

    while j > 0:
        print('i =', i, 'j = ', j)                 # print 명령문 추가
        if word1[i] != word2[j]:
            return False
        i = i+1
        j = j-1

    return True

다시 실행하면 좀 더 많은 정보를 얻는다.

In [31]:
is_reverse('pots', 'stop')
i = 0 j =  4
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-31-d368e6954e78> in <module>
----> 1 is_reverse('pots', 'stop')

<ipython-input-30-4b4176bb5753> in is_reverse(word1, word2)
      8     while j > 0:
      9         print('i =', i, 'j = ', j)                 # print 명령문 추가
---> 10         if word1[i] != word2[j]:
     11             return False
     12         i = i+1

IndexError: string index out of range

1차 수정

while 명령문을 시작하면서 ij에 할당된 인덱스의 값을 출력한다. 그런데 j의 값은 4인데, 문자열 'pots'의 범위를 벗어나기 때문에 오류가 발생한다. 마지막 문자의 지수는 문자열의 길이에서 1을 빼야 하므로, j의 초기값은 len(word2)-1이어야 한다.

In [32]:
def is_reverse(word1, word2):
    if len(word1) != len(word2):
        return False

    i = 0
    j = len(word2) - 1                   # 1차 수정

    while j > 0:
        print('i =', i, 'j = ', j)       # print 명령문 추가
        if word1[i] != word2[j]:
            return False
        i = i+1
        j = j-1

    return True
In [33]:
is_reverse('pots', 'stop')
i = 0 j =  3
i = 1 j =  2
i = 2 j =  1
Out[33]:
True

이번에는 답은 맞았지만 while 순환이 세 번만 실행된 것으로 확인된다. 실제로 아래의 경우에는 오답을 리턴한다.

In [34]:
is_reverse('pota', 'stop')
i = 0 j =  3
i = 1 j =  2
i = 2 j =  1
Out[34]:
True

2차 수정

while 순환이 세 번만 실행되기에 pota의 마지막 문자인 astop의 첫째 문자인 s의 비교는 실행되지 않기 때문이다.

즉, j의 값이 0인 경우를 다루지 않는다. 따라서 코드를 아래와 같이 추가 수정해야 한다.

In [35]:
def is_reverse(word1, word2):
    if len(word1) != len(word2):
        return False

    i = 0
    j = len(word2) - 1                   # 1차 수정

    while j >= 0:                        # 2차 수정
        print('i =', i, 'j = ', j)       # print 명령문 추가
        if word1[i] != word2[j]:
            return False
        i = i+1
        j = j-1

    return True
In [36]:
is_reverse('pota', 'stop')
i = 0 j =  3
i = 1 j =  2
i = 2 j =  1
i = 3 j =  0
Out[36]:
False

이제 is_reverse 함수가 제대로 작동함을 확인하였다. 따라서 디버깅을 위해 추가한 print 명령문을 이제는 삭제하는 게 좋다.

In [37]:
def is_reverse(word1, word2):
    if len(word1) != len(word2):
        return False

    i = 0
    j = len(word2) - 1

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

    return True
In [38]:
is_reverse('pots', 'stop')
Out[38]:
True
In [39]:
is_reverse('pota', 'stop')
Out[39]:
False

연습문제

  1. 아래 코드는 앞서 살펴본 findChar 검색함수이다.

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

    아래 조건을 만족시키도록 findChar 함수를 수정하라.

    • 인자를 세 개 받는다. 예를 들어, wordletter 두 개의 매개변수와 더불어 position이라는 매개변수를 하나 더 사용하도록 한다.
    • position 매개변수는 정수를 입력값으로 기대한다.
    • position을 통해 전달된 정수는 탐색을 시작할 위치를 나타낸다. 즉, 앞서 정의된 findChar 함수는 position이 0인 특수한 경우가 되도록 한다.

  2. 문자열의 find 메서드에 대응하는 find 함수를 정의하라. 즉, 아래 조건을 만족하는 find 함수를 정의해야 한다.

    • word1, word2, position 세 개의 인자를 받는다.
    • word1, word2는 문자열을 입력값으로 기대한다.
    • word1word2를 부분문자열로 포함할 경우 부분문자열의 시작위치를 리턴하고, 포함하지 않을 경우 -1을 리턴한다.
    • position 매개변수는 정수를 입력값으로 기대한다.
    • position을 통해 전달된 정수는 탐색을 시작할 위치를 나타낸다.

      (힌트) 문자열의 find 메서드 활용 가능.

  3. 아래는 집계(counting)에 사용된 코드이다.
    word = 'orange'
     count = 0
     for letter in word:
         if letter == 'a':
             count = count + 1
     print(count)
    
    1. 위 코드를 이용하여 문자열과 문자를 인자로 받는 counting라는 이름의 함수를 선언하라. 즉, counting('orange', 'a') 형식으로 호출되면 orange 문자열에서 a 문자가 몇 번 나타나는지를 확인해주어야 한다.
    2. 앞서 find 함수의 경우처럼 counting 함수를 인자를 하나 더 받아서 특정 인덱스 이후 지정한 문자가 몇 번 출현하는지를 집계하도록 수정하라.

  4. 영어 단어 중에서 radar, noon 등과 같이 앞으로 읽으나 뒤로 읽으나 스펠링이 동일한 단어를 회문(palindrome)이라 한다. 문자열이 회문이면 True를, 아니면 False를 리턴하는 함수 is_palindrome을 구현하라. 즉, 아래 코드에서 pass 부분을 적당한 코드로 구현하라.
    def is_palindrome(word)
         pass
    
  5. 앞선 문제에서 다룬 is_palindrome 함수를 스텝을 이용하여 구현하라.

  6. 다음 함수들은 모두 문자열이 소문자를 포함하고 있는지 여부를 조사하도록 구현되었지만 일부는 제대로 작동하지 않는다. 각각의 함수마다, 함수가 실제로 무엇을 하는지 설명하라. (인자는 문자열이 들어오는 경우를 다룬다.)

    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
    
  7. ROT13은 단어의 각 글자를 13자리만큼 회전 시키는 방식의 간단한 암호 법이다. 글자를 회전한다는 것은 알파벳 상의 위치를 이동한다는 뜻이다. 예를 들어, 'A' 를 3만큼 이동하면 'D'가, 'Z' 를 1만 큼 이동하면 'A'가 된다.

    문자열과 정수를 매개변수로 받아들여서 원래 문자열을 요청한 양만큼 "회전"시킨 문자열을 리턴하는 함수 rotate_word를 구현하라. 예를 들어, 'cheer' 를 7만큼 회전시키면 'jolly' 이고 'melon' 을 -10만큼 회전시키면 'cubed'가 되어야 한다. #### 힌트: 문자를 숫자 코드로 형변환하는 ord 내장함수와 숫자코드를 문자로 형변환 시키는 chr 내장함수를 이용할 수 있다. 아래 사이트에 가면 모범답안이 있다. 하지만 먼저 스스로 해결하도록 노력해 보아야 한다.

    http://thinkpython.com/code/rotate.py

    내장함수란? 파이썬에서 이미 정의된 함수를 의미한다.