문자열 안에 포함된 숫자를 기준으로 정렬하고 싶을 때 사용하는 방식을 자연 정렬(Natural Sort) 또는 휴먼 정렬(Human Sort)이라고 부릅니다.
자연 정렬이 필요한 이유
예를 들어 다음과 같은 리스트가 있다고 가정해 보겠습니다.
['Hello1', 'Hello12', 'Hello29', 'Hello2', 'Hello17', 'Hello25']
사람이 기대하는 정렬 결과는 아래와 같습니다.
['Hello1', 'Hello2', 'Hello12', 'Hello17', 'Hello25', 'Hello29']
하지만 파이썬의 기본 문자열 정렬은 사전순(ASCII 순서)으로 비교하기 때문에 다음과 같은 결과가 나옵니다.
['Hello1', 'Hello12', 'Hello17', 'Hello2', 'Hello25', 'Hello29']
'Hello12'가 'Hello2'보다 앞에 오는 것은 사람이 생각하는 숫자 크기 순서와 다르기 때문에, 단순히 sort()만으로는 원하는 결과를 얻을 수 없습니다.
해결 방법: sort()의 key 매개변수 활용
이 문제는 sort() 함수가 제공하는 key 매개변수를 사용하면 해결할 수 있습니다. key는 리스트의 각 항목에서 비교 기준값을 계산하는 함수입니다.
여기서는 정규표현식(regular expression)을 이용해 문자열에서 숫자 부분을 추출하고, 텍스트와 숫자를 함께 고려하여 정렬합니다.
예제 코드
import re
def atoi(text):
# 숫자로만 이루어진 부분은 int로 변환, 그 외에는 그대로 반환
return int(text) if text.isdigit() else text
def natural_keys(text):
# 문자열을 숫자와 비숫자로 분리한 뒤 각 조각을 변환
return [atoi(c) for c in re.split(r'(\d+)', text)]
my_list = ['Hello1', 'Hello12', 'Hello29', 'Hello2', 'Hello17', 'Hello25']
my_list.sort(key=natural_keys)
print(my_list)실행 결과
['Hello1', 'Hello2', 'Hello12', 'Hello17', 'Hello25', 'Hello29']
동작 원리 살펴보기
re.split(r'(\d+)', text)는 캡처 그룹을 사용하므로, 문자열을 숫자 부분과 텍스트 부분으로 나눌 때 구분자까지 결과에 포함됩니다. 예를 들어 'Hello25'는 ['Hello', '25', '']로 분리되고, 각 요소가 atoi()를 통해 ['Hello', 25, '']로 변환됩니다.
파이썬은 리스트끼리 비교할 때 요소를 하나씩 순서대로 비교하므로, 먼저 'Hello'라는 공통 접두사가 같은지 확인한 뒤 숫자 값인 25와 2 등을 실제 숫자로 비교하게 됩니다. 덕분에 'Hello2'가 'Hello12'보다 작다고 판단하여 사람이 기대하는 순서대로 정렬이 이루어집니다.
마무리
파일명 정렬, 버전 번호 정렬처럼 문자열 속 숫자가 중요한 경우에는 자연 정렬이 필수적입니다. 위의 natural_keys 패턴은 재사용성이 높으니 유틸리티 함수로 만들어 두면 다양한 상황에서 유용하게 활용할 수 있습니다.