파이썬 숫자 순서바꾸기 - paisseon susja sunseobakkugi

Python에서 리스트 등을 오름차순 혹은 내림차순으로 정렬하기 위해서 sort() 메소드나 sorted() 함수를 사용한다. 이번 포스팅에서 0로 메워지지 않은 예를 들면, 0001, 0002....가 아닌 1, 2...으로 되어 있는 문자열 형태의 숫자는 어떻게 정렬할 수 있는지 알아보고자 한다.
(참고로 문자열 숫자를 별다른 과정을 거치지 않고 정렬하기 위해서는 0001, 0002와 같이 앞을 0으로 메워주면 간단하게 제대로 정렬되지 않는 문제가 해결되지만 경우에 따라 적용하지 못할 수 있기 때문에 그 방법에 대해 알아봤다. )

  • sort()와 sorted()
  • 0로 메워지지 않은 숫자 문자열의 주의점
  • 인수 key에 int()나 float()를 지정
  • 정규표현으로 문자열 안의 숫자를 추출

- 문자열안에 숫자가 1개인 경우
- 문자열안의 숫자가 여러 개인 경우
- 문자열 안에 숫자가 아닌 요소도 있는 경우

sort()와 sorted()


sort()는 리스트형의 메소드로, 원래 리스트 자체가 정렬된다.

l = [10, 1, 5]

l.sort()
print(l)
# [1, 5, 10]

sorted()는 내장 함수로, 정렬된 새로운 리스트가 생성된다. 원래의 리스트는 변경되지 않는다.

l = [10, 1, 5]

print(sorted(l))
# [1, 5, 10]

print(l)
# [10, 1, 5]

기본적으로는 오름차순이다. 내림차순으로 하고 싶은 경우 인수 reverse를 True로 지정할 필요가 있다. 예시 코드는 sorted()이지만 sort()도 마찬가지이다.

print(sorted(l, reverse=True))
# [10, 5, 1]

앞서 말했듯, 0로 메워진 숫자의 문자열 리스트의 경우 특별히 문제없이 정렬된다. 또한 아래의 샘플 코드에서는 sorted()를 사용하고 있지만, sort()의 경우도 동일하다.

l = ['10', '01', '05']

print(sorted(l))
# ['01', '05', '10']

0로 메워지지 않은 숫자의 문자열 리스트의 경우, 숫자로써가 아닌 문자열을 사전순으로 나열하므로 아래와 같은 결과가 출력되어 버리고 만다. 아래의 예와 같이 "10"은 "5"보다 작은 것 처럼 정렬된다.

l = ['10', '1', '5']

print(sorted(l))
# ['1', '10', '5']

인수 key에 int()나 float()를 지정


sort()나 sorted()에서는 인수 key에 함수를 지정하면 그 함수가 적용된 결과를 바탕으로 정렬된다. 인수 key에 문자열을 숫자로 변환하는 int()나 float()를 지정하는 것으로 숫자의 크기 순으로 제대로 정렬된다. 함수를 인수로 지정할 때에 ()를 작성하면 에러가 발생하므로 주의할 필요가 있다.

l = ['10', '1', '5']

print(sorted(l, key=int))
# ['1', '5', '10']

print(sorted(l, key=float))
# ['1', '5', '10']

정수의 문자열은 int()이든 float()이든 둘 다 상관없이 변환되지만, 소수점이 있는 수의 경우 당연히 float()를 사용해야한다.

l = ['10.0', '1.0', '5.0']

print(sorted(l, key=float))
# ['1.0', '5.0', '10.0']

sort()에서도 똑같이 인수 key를 지정할 수 있다.

l = ['10', '1', '5']

l.sort(key=int)
print(l)
# ['1', '5', '10']

지금까지의 결과들로 알 수 있듯, key로 지정한 함수는 어디까지나 정렬의 비교를 위한 것이므로 결과는 제대로 문자열이다.
int형이나 float형의 결과를 원하는 경우 리스트 내포 표기로 변환한 리스트를 정렬하면 된다.

l = ['10', '1', '5']

print([int(s) for s in l])
# [10, 1, 5]

print(sorted([int(s) for s in l]))
# [1, 5, 10]

정규표현으로 문자열 안의 숫자를 추출


숫자뿐인 문자열의 경우 바로 앞에서 봤던 int()나 float()를 지정하면 되지만 아래와 같이 문자열 안에 숫자가 포함되어 있는 경우 주의가 필요하다. 이럴 때에는 정규 표현 모듈 re를 사용하여 문자열 안의 숫자 부분을 추출한 후 숫자로 변환해야할 필요가 있다.

l = ['file10.txt', 'file1.txt', 'file5.txt']

문자열 안의 숫자가 1개인 경우

search()로 match오브젝트를 얻어내고, group()메소드로 매치된 부분을 문자열로써 추출한다.
정규표현의 패턴으로써 \d+를 사용한다. \d는 숫자, +는 1문자 이상의 반복을 의미한다. 따라서 \d+는 한 개의 문자 이상의 연속된 숫자에 매치된다는 의미가 된다.

import re

s = 'file5.txt'

print(re.search(r'\d+', s).group())
# 5

여기에서는 백슬래시를 그대로 쓸수 있도록 raw 문자열을 사용하고 있다. 문자열로부터 반환된 숫자를 변환하는 경우 int()나 float()를 사용한다.

print(type(re.search(r'\d+', s).group()))
# <class 'str'>

print(type(int(re.search(r'\d+', s).group())))
# <class 'int'>

이것을 무명함수(람다식)으로 sort()나 sorted() 인수 key로 지정한다.

l = ['file10.txt', 'file1.txt', 'file5.txt']

print(sorted(l))
# ['file1.txt', 'file10.txt', 'file5.txt']

print(sorted(l, key=lambda s: int(re.search(r'\d+', s).group())))
# ['file1.txt', 'file5.txt', 'file10.txt']

참고로 리스트의 요소수가 적은 경우에는 신경쓰지 않아도 되지만, compile()로 정규표현 옵션을 생성하여 사용하는 것이 효율적이다.

p = re.compile(r'\d+')
print(sorted(l, key=lambda s: int(p.search(s).group())))
# ['file1.txt', 'file5.txt', 'file10.txt']

문자열 안의 숫자가 여러 개인 경우

search()가 반환해주는 것은 맨 처음에 매치된 부분뿐이다.

s = '100file5.txt'

print(re.search(r'\d+', s).group())
# 100

findall()는 매치된 모든 부분을 리스트형식으로 반환해준다.

print(re.findall(r'\d+', s))
# ['100', '5']

print(re.findall(r'\d+', s)[1])
# 5

패턴 중의 부분을 ()으로 감싸면, groups() 메소드로 해당 부분만을 추출할 수도 있다. 예를들어, file(\d+)패턴은 fileXXX이라는 문자열의 XXX부분(숫자)를 추출할 수 있다. 해당 부분이 하나뿐이라도 튜플형으로 반환되므로 주의하자.

print(re.search(r'file(\d+)', s).groups())
# ('5',)

print(re.search(r'file(\d+)', s).groups()[0])
# 5

(\d+)\.으로 하면 XXX.이라는 문자열의 XXX부분(숫자)를 추출할 수 있다. "."앞에는 백슬래쉬가 필요하다.

print(re.search(r'(\d+)\.', s).groups()[0])
# 5

어떠한 방법을 사용해도 OK이다. findall()은 심플하지만, 숫자 부분의 갯수가 요소에 따라 다 다르면 사용할 수 없게 되므로 주의하자.

l = ['100file10.txt', '100file1.txt', '100file5.txt']

print(sorted(l, key=lambda s: int(re.findall(r'\d+', s)[1])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: int(re.search(r'file(\d+)', s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: int(re.search(r'(\d+)\.', s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

컴파일할 경우도 동일하다.

p = re.compile(r'file(\d+)')
print(sorted(l, key=lambda s: int(p.search(s).groups()[0])))
# ['100file1.txt', '100file5.txt', '100file10.txt']

문자열 안에 숫자가 없는 요소도 있는 경우

모든 요소 안에 숫자가 포함되어 있는 경우는 문제가 없지만, 그렇지 않은 경우는 별도의 케어가 필요하다. 이전과 동일한 방법을 적용하면 에러가 발생한다.

l = ['file10.txt', 'file1.txt', 'file5.txt', 'file.txt']

# print(sorted(l, key=lambda s:int(re.search(r'\d+', s).group())))
# AttributeError: 'NoneType' object has no attribute 'group'

예를 들어, 아래와 같이 함수를 정의한다. 첫 번째 인수에 문자열, 두 번째 인수는 정규표현 오브젝트, 세 번째 인수에는 매치하지 않는 경우 반환될 값을 지정한다.

def extract_num(s, p, ret=0):
    search = p.search(s)
    if search:
        return int(search.groups()[0])
    else:
        return ret

결과는 다음과 같다. groups()를 사용하고 있으므로 패턴에는 "()"가 필요하다.

p = re.compile(r'(\d+)')

print(extract_num('file10.txt', p))
# 10

print(extract_num('file.txt', p))
# 0

print(extract_num('file.txt', p, 100))
# 100

세 번째 인수는 생략이 가능하다. 이 함수를 sort()나 sorted()의 인수 key에 지정하면 된다.

print(sorted(l, key=lambda s: extract_num(s, p)))
# ['file.txt', 'file1.txt', 'file5.txt', 'file10.txt']

print(sorted(l, key=lambda s: extract_num(s, p, float('inf'))))
# ['file1.txt', 'file5.txt', 'file10.txt', 'file.txt']

숫자가 포함되어 있지 않은 요소를 오름차순의 마지막에 두길 바라는 경우, 적당히 커다란 숫자를 반환 값으로 하면 되지만, 무한대 inf로 부면 어느 값보다 크게 설정할 수 있다.
숫자가 여러개 포함되어 있는 경우는 정규표현 오브젝트를 변경하면 된다.

l = ['100file10.txt', '100file1.txt', '100file5.txt', '100file.txt']

p = re.compile(r'file(\d+)')
print(sorted(l, key=lambda s: extract_num(s, p)))
# ['100file.txt', '100file1.txt', '100file5.txt', '100file10.txt']

print(sorted(l, key=lambda s: extract_num(s, p, float('inf'))))
# ['100file1.txt', '100file5.txt', '100file10.txt', '100file.txt']

참고자료
https://note.nkmk.me/python-sort-num-str/