파이썬코딩의기술 - BW14 복잡한 기준을 사용해 정렬할 때는 key 파라미터를 사용하라
list 내장 타입에는 리스트의 원소를 여러 기준에 따라 정렬할 수 있는 sort 메소드가 있습니다.
기본적으로 sort는 리스트의 내용을 원소 타입에 따른 자연스러운 순서를 사용해 오름차순으로 정렬합니다.
(예) 정수 리스트를 작은 수부터 큰 수까지 순서대로 정렬합니다.
numbers = [93, 86, 11, 68, 70]
numbers.sort()
print(numbers)
>>>
[11, 68, 70, 86, 93]
sort 메소드가 호출하는 객체 비교 특별 메서드가 정의돼 있지 않으므로 이런 타입의 객체를 정렬할 수 없습니다.
class Tool:
def __init__(self, name, weight):
self.name = name
self.weight = weight
def __repr__(self):
return f'Tool({self.name!r}, {self.weight})'
tools = [
Tool('수준계', 3.5),
Tool('해머', 1.25),
Tool('스크류드라이버', 0.5),
Tool('끝', 0.25),
]
tools.sort()
>>>
Traceback...
TypeError: '<' not supported between instaces of 'Tool' and 'Tool'
정렬에 사용하고 싶은 애트리뷰트가 객체에 들어있는 경우가 많습니다.
이런 상황을 지원하기 위해 sort에는 key라는 파라미터가 있습니다.
key는 함수여야 합니다. key 함수에는 정렬 중인 리스트의 원소가 전달됩니다.
key 함수가 반환하는 값은 원소 대신 정렬 기준으로 사용할 비교 가능한 값이어야만 합니다.
함수를 key로 사용하면 Tool 객체로 이뤄진 리스트를 이름(name)에 따라 알파벳순으로 정렬합니다.
print('미정렬:' , repr(tools))
tools.sort(key=lambda x: x.name)
print('\n정렬 : ', tools)
>>>
미정렬 : [Tool('수준계', 3.5), Tool('해머', 1.25), Tool('스크류드라이버', 0.5), Tool('끌', 0.25)]
정렬 : [Tool('끌', 0.25), Tool('수준계', 3.5), Tool('스크류드라이버', 0.5), Tool('해머', 1.25)
weight로 정렬하는 람다 함수를 만들어서 sort의 key 파라미터로 전달할 수 있습니다.
tools.sort(key=lambda x: x.weight)
print('무게순 정렬:', tools)
>>>
무게순 정렬 : [Tool('끌', 0.25), Tool('스크류드라이버', 0.5), Tool('해머', 1.25), Tool('수준계', 3.5)]
문자열 같은 기본 타입의 경우에는 정렬하기 전에 key 함수를 사용해 원소값을 변형할 수도 있습니다.
(예) lower 메소드를 사용해 리스트에 들어있는 장소 이름을 소문자로 변환함으로써 첫글자가 대문자든 소문자든
구분하지 않고 알파벳순으로 비교합니다.
- 이유 : 문자열의 자연스러운 순서에서는 대문자가 소문자보다 더 앞에 오기 때문
places = ['home', 'work', 'New York', 'Paris']
places.sort()
print('대소문자 구분:', places)
places.sort(key=lambda x: x.lower())
print('대소문자 무시:', places)
>>>
대소문자 구분: ['New York', 'Paris', 'home', 'work']
대소문자 무시: ['home', 'New York', 'Paris', 'work']
때로는 여러 기준을 사용해 정렬해야 할 수도 있습니다.
(예) 전동 공구 정보가 들어 있는 리스트가 있는데 weight로 먼저 정렬한 다음에 name으로 정렬하고 싶다면 어떻게 해야할까?
power_tools = [
Tool('드릴', 4),
Tool('원형 톱', 5),
Tool('착암기', 40),
Tool('연마기', 4),
]
쉬운 해법은 tuple을 쓰는 것입니다.
튜플은 기본적으로 비교 가능하며 자연스러운 순서가 정해져 있습니다.
이는 sort에 필요한 __lt__정의가 들어있다는 뜻이며, 이러한 특별 비교 메소드는
튜플의 각 위치를 이터레이션하면서 각 인덱스에 해당하는 원소를 한 번에 하나씩 비교하는 방식으로 구현되어있습니다.
(예) 어떤 도구가 다른 도구보다 무거울 때 어떤 일이 벌어지는지 보여줍니다.
saw = (5, '원형 톱')
jackhammer = (40, '착암기')
assert not (jackhammer < saw) # 예상한 대로 결과가 나옵니다.
비교하는 두 튜플의 첫 번째 위치에 있는 값(weight)이 서로 같으면 튜플의 비교 메서드는 두번째 위치에 있는 값을 서로 비교하고,
두 번째 위치에 있는 값도 같으면 마찬가지 방식으로 세번째 이후 위치 등에 대해 비교를 반복합니다.
drill = (4, '드릴')
sander = (4, '연마기')
assert drill[0] == sander[0] # 무게가 같다
assert drill[1] < sander[1] # 알파벳순으로 볼 때 작다
assert drill < sander # 그러므로 드릴이 더 먼저다
튜플 비교의 동작 방식을 활용해서 전동 공구 리스트를 먼저 weight로 정렬하고 그 후 name으로 정렬할 수 있습니다.
(예) 정렬에 사용할 두 애트리뷰트를 우선순위에 따라 튜플에 넣어 반환하는 key 함수를 정의합니다.
power_tools.sort(key=lambda x: (x.weight, x.name))
print(power_tools)
>>>
[Tool('드릴', 4), Tool('연마기', 4), Tool('원형 톱', 5), Tool('착암기', 40)]
튜플을 반환하는 key 함수의 한 가지 제약 사항은 모든 비교 기준의 정렬 순서가 같아야 한다는 점 입니다.
즉, 모두 오름차순이거나 모두 내림차순이어야 합니다.
- sort 메서드에 reverse 파라미터를 넘기면 튜플에 들어있는 두 기준의 정렬 순서가 똑같이 영향을 받습니다.
power_tools.sort(key=lambda x: (x.weight, x.name), reverse=True) # 모든 비교 기준을 내림차순으로 만듭니다.
print(power_tools)
>>>
[Tool('착암기', 40), Tool('원형 톱', 5), Tool('연마기', 4), Tool('드릴', 4)]
숫자 값의 경우 단항 부호 반전(-) 연산자를 사용해 정렬 방향을 혼합할 수 있습니다.
- 부호 반전 연산자(-)는 반환되는 튜플에 들어가는 값 중 하나의 부호를 반대로 만들기 때문에, 결과적으로
나머지 값의 정렬 순서는 그대로 둔 채로 반전된 값의 정렬 순서를 반대로 만듭니다.
(예) 전동 공구를 weight 기준 내림차순으로 정렬한 다음 name 기준 오름차순으로 정렬합니다.
(sander가 drill의 앞이 아니라 뒤에 온다)
power_tools.sort(key=lambda x:(-x.weight, x.name))
print(power_tools)
>>>
[Tool('착암기', 40), Tool('원형 톱',5) Tool('드릴', 4). Tool('연마기', 4)]
그러나 모든 타입에 부호 반전을 사용할 수는 없습니다.
(예) 다음 코드는 sort의 reverse 인자를 지정하고, weight 기준 오름차순으로 정렬하고, name을 반전시켜서
앞에서 본 예제와 같은 결과를 만들려고 시도하지만 실패합니다.
power_tools.sort(key=lambda x: (x.weight, -x.name), reverse=True)
>>>
Traceback ...
TypeError: bad operand type for unary -: 'str'
파이썬에서는 이를 위해 '정렬 알고리즘'을 제공합니다.
- 리스트 타입의 sort 메서드는 key 함수가 반환하는 값이 서로 같은 경우 리스트에 들어있던 원래 순서를 그대로 유지해줍니다.
- 이는 같은 리스트에 대해 서로 다른 기준으로 sort를 여러 번 호출해도 된다는 뜻 입니다.
(예) 앞에서 보여준 코드와 마찬가지로 weight 기준 내림차순, name 기준 오름차순으로 정렬하는데, sort를 두 번 호출하는
방식으로 정렬을 수행합니다.
power_tools.sort(key=lambda x: x.name) # name 기준 오름차순
power_tools.sort(key=lambda x: x.weight, reverse=True) # weight 기준 내림차순
print(power_tools)
>>>
[Tool('착암기', 40), Tool('원형 톱', 5), Tool('드릴', 4), Tool('연마기', 4)]
- 처음 sort를 호출하면 이름의 알파벳순으로 리스트가 정렬된다는 사실을 먼저 기억해야합니다.
power_tools.sort(key=lambda x: x.name)
print(power_tools)
>>>
[Tool('드릴', 4), Tool('연마기', 4), Tool('원형 톱', 5), Tool('착암기', 40)]
- 두 번째로 weight에 의해 내림차순으로 sort를 호출하면 sander와 drill이 모두 weight가 4라는 사실을 알 수 있습니다.
- 따라서 sort 메서드는 원래 리스트에 있던 것과 똑같은 순서로 두 원소를 결과 리스트에 넣습니다.
- name을 사용해 오름차순으로 정렬한 상대적인 순서가 보존됩니다.
power_tools.sort(key=lambda x: x.weight, reverse=True)
print(power_tools)
>>>
[Tool('착암기', 40), Tool('원형 톱', 5), Tool('드릴', 4), Tool('연마기', 4)]
같은 접근 방법을 사용하면 여러 다른 타입의 정렬 기준을 원하는 방향으로 서로 조합할 수 있습니다.
- 다만, 최종적으로 리스트에서 얻어내고 싶은 정렬 기준 우선순위의 역순으로 정렬을 수행해야 한다는 사실을 꼭 기억해야합니다.
- (예) weight에 의해 내림차순으로 정렬하고 그 후 name에 의해 오름차순으로 정렬된 리스트를 원했으므로,
- 먼저 name을 사용해 오름차순으로 정렬하고 그 후 weight를 사용해 내림차순으로 정렬해야 합니다.
그러나 key 함수를 사용해 tuple을 반환하게 하고 여기에 단항 부호 반전 연산을 활용하는 접근 방식이 코드가 더 적고
읽기도 쉽습니다.
꼭 필요할 때만 sort를 여러번 호출하는 방법을 사용하는 것을 권장합니다.
# 기억해야할 내용
- 리스트 타입에 들어있는 sort 메서드를 사용하면 원소 타입이 문자열, 정수, 튜플 등과 같은 내장 타입인 경우 자연스러운
순서로 리스트의 원소를 정렬할 수 있음
- 원소 타입에 특별 메서드를 통해 자연스러운 순서가 정의돼 있지 않으면 sort 메서드를 쓸 수 없음. 하지만 원소 타입에 순서 특별 메서드를 정의하는 경우는 드물다.
- sort 메서드의 key 파라미터를 사용하면 리스트의 각 원소 대신 비교에 사용할 객체를 반환하는 도우미 함수를 제공할 수 있음
- key 함수에서 튜플을 반환하면 여러 정렬 기준을 하나로 엮을 수 있음. 단항 부호 반전 연산자를 사용하면 부호를 바꿀 수 있는 타입이 정렬 기준인 경우 정렬 순서를 반대로 바꿀 수 있음
- 부호를 바꿀 수 없는 타입의 경우 여러 정렬 기준을 조합하려면 각 정렬 기준마다 reverse 값으로 정렬 순서를 지정하면서 sort 메서드를 여러 번 사용해야 함. 이때 정렬 기준의 우선순위가 점점 높아지는 순서로 sort를 호출해야함.