이번에는 문자열을 응용하여 회문을 판별하는 방법과 N-gram을 만드는 방법에 대해서 알아보자
회문(palindrome)은 순서를 거꾸로 읽어도 제대로 읽은 것과 같은 단어와 문장을 말한다. 예를 들어 "level", "SOS", "rotator", "nurses run"과 같은 단어와 문장이 있다.
그럼 문자열이 회문인지 판별하기 위해선 어떻게 할까? 회문은 첫 글자와 마지막 글자가 같다. 그리고 안쪽으로 한 글자 씩 좁혔을 때 글자가 같아서 서로 같으면 회문이다.
word = input('단어를 입력하세요: ')
is_palindrome = True # 회문 판별값을 저장할 변수, 초깃값은 True
for i in range(len(word) // 2): # 0부터 문자열 길이의 절반만큼 반복
if word[i] != word[-1 - i]: # 왼쪽 문자와 오른쪽 문자를 비교하여 문자가 다르면
is_palindrome = False # 회문이 아님
break
print(is_palindrome) # 회문 판별값 출력
실행 결과
단어를 입력하세요: level (입력)
True
실행 결과
단어를 입력하세요: hello (입력)
False
여기서 핵심인 개념은 문자열 길이이다. 판별을 할 때 문자열 길이를 기준으로 절반으로 나누어 왼쪽 문자와 오른쪽 문자가 같은지 검사한다.
반복문으로 문자열의 각 문자를 일일이 비교하려니까 다소 번거롭다. 이 보다 더 간단한 방법이 있다.
회문은 시퀀스 객체의 슬라이스를 활용하면 간단하게 판별할 수 있다.
word = input('단어를 입력하세요: ')
print(word == word[::-1]) # 원래 문자열과 반대로 뒤집은 문자열을 비교
실행 결과
단어를 입력하세요: level (입력)
True
단어를 입력하세요: hello (입력)
False
정말 간단하게 word = word[::-1]
로 간단하게 끝났다. word[::-1]
은 문자열 전체에서 인덱스가 1 씩 감소하면서 요소를 가져오기 때문에 문자열이 뒤집혀지게 된다.
이 방법 외에도 다양한 방법으로 회문을 판별할 수 있는데 반복 가능한 객체의 요소 순서를 반대로 뒤집는 reversed
를 사용해도 된다.
>>> word = 'level'
>>> list(word) == list(reversed(word))
True
reversed
를 활용하면 문자열이 반대로 뒤집히고 리스트에 넣으면 요소 순서가 반대로 된 리스트를 구할 수 있을 것이다.
>>> list(word)
['l', 'e', 'v', 'e', 'l']
>>> list(reversed(word))
['l', 'e', 'v', 'e', 'l']
이 두 리스트를 ==
로 비교하면 회문인지를 판별할 수 있다.
join
메서드를 사용하여 회문을 판별할 수 있다.
>>> word = 'level'
>>> word == ''.join(reversed(word))
True
join
은 구분자 문자열과 문자열 리스트 요소를 연결한다. 여기서는 구분자 문자열이 ''
로 빈 문자열이기 때문에 문자 순서가 반대로 된 문자열을 얻을 수 있는 것이다.
>>> word
'level'
>>> ''.join(reversed(word))
'level'
그렇기 때문에 ==
로 비교하여 회문인지를 판별할 수 있다.
N-gram은 문자열에서 N 개의 연속된 요소를 추출하는 방법이다. 만약 'Hello' 문자열을 2-gram으로 추출하면 다음과 같이 된다.
He
el
ll
lo
즉 문자열을 처음부터 끝까지 한 글자씩 이동하면서 2글자를 추출한다. 3-gram 은 3글자, 4-gram 은 4글자이다.
'Hello' 를 2-gram으로 출력해보자.
text = 'Hello'
for i in range(len(text) - 1): # 2-gram이므로 문자열의 끝에서 한 글자 앞까지만 반복함
print(text[i], text[i + 1], sep='') # 현재 문자와 그다음 문자 출력
실행 결과
He
el
ll
lo
생각보다 매우 간단하다.
만약 3-gram 이라면 어떻게 될까? 반복 횟수는 range(len(text) - 2)
가 되고 print(text[i], text[i + 1], text[i + 2], sep='')
가 될 것이다.
text = 'hello'
two_gram = zip(text, text[1:])
for i in two_gram:
print(i[0], i[1], sep='')
실행 결과
He
el
ll
lo
지금까지 zip
함수는 리스트 두 개로 딕셔너리를 만들 때 사용했는데 zip
은 반복 가능한 객체의 각 요소들을 튜플로 묶어준다.
>>> text = 'hello'
>>> list(zip(text, text[1:]))
[('h', 'e'), ('e', 'l'), ('l', 'l'), ('l', 'o')]
zip(text, text[1:])
는 text
와 text[1:]
의 각 요소를 묶어서 튜플로 만든다. 따라서 위와 같이 두 리스트가 묶인 것을 볼 수 있다. 이렇게 묶인 것들을 반복문으로 출력하면된다.
앞서 zip
을 통해서 N-gram 을 만들 때 일일히 [1:], [2:] 와 같이 슬라이스를 해줘야하니 상당히 번거롭다. N-gram 의 숫자가 늘어나면 그만큼 슬라이스도 여러 개 입력해줘야 한다. 그렇다면 이 과정을 코드로 만들 수 없을까? 다음과 같이 리스트 표현식을 사용하면 된다.
>>> text = 'hello'
>>> [text[i:] for i in range(3)]
['hello', 'ello', 'llo']
위의 소스 코드를 통해서 text[0:], text[1:], text[2:]
를 구했다. 즉, 3-gram에 필요한 슬라이스이다.
이제 이 ['hello', 'ello', 'llo']
리스트를 zip
에 넣어보자.
>>> list(zip(['hello', 'ello', 'llo']))
[('hello',), ('ello',), ('llo',)]
결과를 보면 우리가 원했던 3-gram 이 아니다. 왜 그럴까? zip
은 반복 가능한 객체를 콤마로 구분하여 넣어줘야 한다. 하지만 우리가 넘긴 ['hello', 'ello', 'llo']
는 요소 3개가 들어있는 1개의 리스트이기 때문이다.
zip
에 각 요소를 콤마로 구분해서 넣어주려면 리스트 앞에 *
를 붙여줘야 한다.
>>> list(zip(*['hello', 'ello', 'llo']))
[('h', 'e', 'l'), ('e', 'l', 'l'), ('l', 'l', 'o')]
이제 3-gram 리스트가 만들어졌다.
리스트에 *
을 붙이는 방법을 리스트 언패킹(list unpacking)이라고 하는데 이 부분은 나중에 뒤에서 보자
지금까지 회문 판별과 N-gram 에 대해서 보았다. 여기서 인덱스로 문자열을 다루는 방법과 reversed와 zip 을 활용한 방법에 대해서 눈여겨 보아두자.