HOWTO · Python

Python에서 문자별로 두 문자열 비교

파이썬에서 두 개의 문자열을 문자별로 비교하는 방법을 배웁니다.

이 페이지의 내용

단순한 동등 비교만으로는 부족할 때가 있습니다. 두 문자열의 몇 개 위치에서 같은 문자가 있는지, 또는 정확히 어디에서 달라지기 시작하는지 알아야 할 수 있습니다. Python에는 위치별 일치 개수를 반환하는 단일 내장 기능이 없지만, zip()과 제너레이터 표현식을 사용하면 한 줄로 구현할 수 있습니다.

이 기사에서는 먼저 zip()을 사용하는 방법을 살펴보고, 위치 자체가 중요할 때 사용하는 인덱스 루프, 길이가 다른 문자열과 문자열이 아닌 입력을 처리하는 방법, 일치 개수가 == 및 해밍 거리와 어떻게 관련되는지, 그리고 비교에 드는 비용을 설명합니다.

zip()으로 두 문자열을 문자별로 비교

두 문자열을 위치별로 비교하는 가장 짧은 방법은 zip()으로 문자를 짝지은 뒤 같은 쌍의 개수를 세는 것입니다. zip(first, second)는 (first[0], second[0]), (first[1], second[1]) 같은 튜플을 순서대로 생성하다가 더 짧은 문자열이 끝나는 즉시 멈추므로, 결과는 항상 공통 접두사 범위에서 센 개수입니다.

이 쌍들을 제너레이터 표현식을 통해 sum()에 전달하면 각 True 비교가 1로 계산됩니다. 다음 예제를 실행해 보세요:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

def count_matching_positions(first, second):
    return sum(a == b for a, b in zip(first, second))


print(count_matching_positions("apple", "apricot"))
print(count_matching_positions("homophones", "homonyms"))
print(count_matching_positions("apple", "applepie"))
2
4
5

"apple"과 "apricot"은 처음 두 위치에서만 일치하고, "homophones"와 "homonyms"는 zip()이 생성하는 여덟 개 위치 중 네 곳에서 일치하며, "apple"은 "applepie"와 공유하는 접두사의 다섯 문자 모두와 일치합니다. 마지막 결과가 중요합니다. 다섯 개 위치가 일치한다고 해서 두 문자열이 같다는 뜻은 아니며, zip()이 비교한 모든 항목이 일치했다는 뜻일 뿐입니다.

여기서 “문자"가 무엇을 의미하는지에 대해 한 가지 주의할 점이 있습니다. Python은 문자열을 독자가 하나의 글자로 인식하는 단위가 아니라 유니코드 코드 포인트 단위로 순회합니다. e 뒤에 결합용 악센트가 붙은 형태로 표기된 "é"는 두 개의 코드 포인트이므로, 시각적으로 똑같아 보이는 두 문자열도 unicodedata.normalize()로 정규화하기 전에는 서로 다른 개수를 만들 수 있습니다.

위치가 필요할 때 인덱스 루프 사용하기

zip()은 문자가 일치하는지는 알려주지만 인덱스는 버립니다. 첫 번째 불일치를 보고하거나 비교 범위를 일부 슬라이스로 제한하는 등 어떤 위치가 다른지 알아야 할 때는 더 짧은 길이를 기준으로 range()를 사용해 루프를 실행하세요:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

def compare_with_positions(first, second):
    size = min(len(first), len(second))
    matches = 0
    for i in range(size):
        if first[i] == second[i]:
            matches += 1
        else:
            print(f"position {i}: {first[i]!r} versus {second[i]!r}")
    return matches


print("matches:", compare_with_positions("homophones", "homonyms"))
position 4: 'p' versus 'n'
position 5: 'h' versus 'y'
position 6: 'o' versus 'm'
position 7: 'n' versus 's'
matches: 4

인덱스를 명시적으로 제어하고 싶다면 while 루프로도 같은 작업을 할 수 있습니다. i = 0으로 초기화하고 while i < size 조건으로 반복한 뒤 i를 직접 증가시키면 됩니다. enumerate(zip(first, second))는 수동 카운터 없이 (index, (char1, char2)) 튜플을 제공하는 세 번째 방법입니다.

출력에 표시되는 위치 보고 문구와 matches: 4 레이블은 원문 그대로 영어로 유지됩니다. 이 예제는 네 개 위치의 문자가 일치했음을 보여 줍니다.

길이가 다른 문자열과 문자열이 아닌 입력 처리하기

zip() 예제들은 하나의 입력 계약을 공유합니다. 두 인수는 모두 문자열이어야 하며, 비교는 공통 접두사만 다룹니다. 이로 인한 두 가지 결과는 의식적으로 처리할 필요가 있습니다.

첫째, zip()은 더 긴 문자열의 남은 뒷부분을 전혀 검사하지 않습니다. 동등 비교에서처럼 남은 꼬리 부분도 결과에 반영해야 한다면 길이를 직접 확인하거나, 더 짧은 입력을 채워 모든 위치를 방문하도록 itertools.zip_longest(first, second, fillvalue=...)로 문자열을 짝지으세요.

둘째, None이나 다른 문자열이 아닌 값을 전달하는 것은 우연히 일치 항목을 찾지 못한 비교가 아니라 프로그래밍 오류입니다. count_matching_positions("apple", None)에 대해 0을 반환하면 잘못된 호출과 실제 결과를 혼동하게 됩니다. 대신 호출자가 문제를 알 수 있도록 입력을 거부하세요:

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

def count_matching_positions(first, second):
    if not isinstance(first, str) or not isinstance(second, str):
        raise TypeError("expected two strings")
    return sum(a == b for a, b in zip(first, second))


try:
    count_matching_positions("apple", None)
except TypeError as exc:
    print("TypeError:", exc)

print(count_matching_positions("apple", "applepie"))
TypeError: expected two strings
5

len(first) != len(second)도 거부해야 하는지는 작업에 따라 달라집니다. 공통 접두사 개수 세기는 그 자체로 타당하지만, 동등 비교 방식의 검사는 길이가 다른 문자열을 서로 다른 문자열로 취급해야 합니다.

출력의 TypeError: expected two strings 메시지는 원문 그대로 유지되는 예제의 일부이므로 영어로 표시됩니다. 이 메시지는 입력 계약을 위반했음을 호출자에게 알려 줍니다.

일치 개수 세기, 동등성 검사 또는 해밍 거리 계산

위치별 일치 개수는 문자열 비교와는 다른 질문에 답하며, 잘못 선택하면 미묘한 버그가 발생합니다.

  • first == second는 전체 문자열의 동등성을 검사합니다. 모든 위치가 일치하고 길이도 같아야 합니다. "apple" == "applepie"는 공통 접두사가 완전히 일치하더라도 False입니다.
  • all(a == b for a, b in zip(first, second))는 비슷해 보이지만 공통 접두사가 같다는 것만 증명하며, "apple"와 "applepie"에 대해 True를 반환합니다. 실제 동등성 검사로 만들려면 len(first) == len(second)를 추가하거나 간단히 ==를 사용하세요.
  • 해밍 거리는 반대 방향으로, 서로 다른 위치의 개수를 셉니다. 같은 길이의 문자열에 대해서만 정의됩니다. 입력 길이가 같다면 정확히 len(first)에서 일치 개수를 뺀 값입니다.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

def hamming_distance(first, second):
    if len(first) != len(second):
        raise ValueError("Hamming distance needs strings of equal length")
    return sum(a != b for a, b in zip(first, second))


first, second = "apple", "applepie"
print(first == second)
print(sum(a == b for a, b in zip(first, second)))
print(hamming_distance("karolin", "kathrin"))
False
5
3

"karolin"과 "kathrin"은 위치 2, 3, 4에서 다르므로 해밍 거리는 3입니다. 두 문자열의 길이가 같기 때문에 이 값은 7 - 4와도 같습니다.

시간 복잡도와 공간 복잡도 이해하기

zip() 개수 세기와 인덱스 루프는 모두 최대 min(len(first), len(second))개의 문자 쌍만 검사하므로 실행 시간은 O(min(len(first), len(second)))입니다. 즉 더 짧은 입력에 대해 선형입니다. 최악의 경우는 두 문자열이 공통 접두사 전체에서 일치할 때이며, 첫 번째 불일치에서 멈추는 루프는 더 일찍 끝날 수 있습니다.

보조 공간은 O(1)입니다. zip()과 제너레이터 표현식은 한 번에 하나의 쌍만 보관하는 지연 반복자이고, 카운터도 하나의 정수일 뿐입니다. list(zip(first, second))로 쌍을 실체화하면 아무런 이점 없이 O(min(len(first), len(second))) 공간이 들므로 제너레이터 형태를 유지하세요.

참고로 first == second도 최악의 경우에는 선형이지만, 차이가 발견되는 즉시 비교를 멈출 수 있습니다.