Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

Python 딕셔너리로 계산하기: min(), max(), zip()으로 최솟값·최댓값 찾고 정렬하는 방법

문제 상황

데이터가 담긴 딕셔너리(dictionary)를 대상으로 최솟값 구하기, 최댓값 구하기, 정렬하기 같은 다양한 계산을 수행하고 싶은 경우가 있습니다. 이 글에서는 실제 예제를 통해 딕셔너리 계산의 여러 접근 방식과 각 방식의 장단점을 살펴보겠습니다.

해결 방법

먼저 테니스 선수 이름과 각 선수가 획득한 그랜드슬램 타이틀 수를 담은 딕셔너리를 만들어 보겠습니다.

PlayerTitles = {
    'Federer': 20,
    'Nadal': 20,
    'Djokovic': 17,
    'Murray': 3,
    'Theim': 1,
    'Zverev': 0
}

1. 단순히 min() 호출하기

선수 이름과 그랜드슬램 타이틀 수가 담긴 딕셔너리가 준비되었습니다. 이제 가장 적은 타이틀을 가진 선수를 찾아보겠습니다.

#type(PlayerTitles)
print(f"Output \n*** The minimum value in the dictionary is {min(PlayerTitles)} ")

출력 결과

*** The minimum value in the dictionary is Djokovic

2. values() 메서드로 값 기준 계산하기

이 결과는 아마 원하는 것이 아닐 겁니다. 사실 우리는 딕셔너리의 키(key)가 아니라 값(value)을 대상으로 계산하려는 것이기 때문입니다. 딕셔너리의 values() 메서드를 사용해 이 문제를 해결해 보겠습니다.

print(f"Output \n*** The minimum value in the dictionary is {min(PlayerTitles.values())} ")

출력 결과

*** The minimum value in the dictionary is 0

3. 값에 대응하는 키가 필요한 경우

하지만 안타깝게도 이 역시 정확히 원하는 결과가 아닌 경우가 많습니다. 예를 들어, 최솟값 자체보다는 그 값에 대응하는 키, 즉 타이틀 수가 가장 적은 선수의 이름이 궁금할 수 있습니다.

4. key 함수로 최솟값의 키 얻기

min()과 max()에 key 함수를 함께 전달하면 최솟값이나 최댓값에 해당하는 키를 얻을 수 있습니다.

print(f"Output \n***{min(PlayerTitles, key=lambda k: PlayerTitles[k])} ")

출력 결과

***Zverev

5. 최솟값 자체를 얻으려면 추가 조회 필요

다만 이 방식으로 최솟값 자체를 얻으려면 한 번 더 조회(lookup)를 수행해야 하는 번거로움이 있습니다.

min_titles = PlayerTitles[min(PlayerTitles, key=lambda k: PlayerTitles[k])]
print(f"Output \n***{min_titles} ")

출력 결과

***0

6. zip()으로 (값, 키) 쌍 만들기 — 가장 우아한 해결책

zip()을 활용한 해결책은 딕셔너리를 (값, 키) 쌍의 시퀀스로 "뒤집어(inverting)" 문제를 해결합니다. 이렇게 만든 튜플끼리 비교를 수행하면 값 요소가 먼저 비교되고, 값이 같을 경우에만 키가 비교됩니다.

이 방식은 정확히 우리가 원하는 동작을 제공하며, 단 한 문장으로 딕셔너리 내용에 대한 축소(reduction) 연산과 정렬을 손쉽게 수행할 수 있게 해줍니다.

min_titles = min(zip(PlayerTitles.values(), PlayerTitles.keys()))
max_titles = max(zip(PlayerTitles.values(), PlayerTitles.keys()))

print(f"Output \n***{min_titles , max_titles} ")

출력 결과

***((0, 'Zverev'), (20, 'Nadal'))

7. zip() + sorted()로 데이터 순위 매기기

같은 방식으로 데이터의 순위를 매기고 싶다면 zip()과 sorted()를 함께 사용하면 됩니다.

titles_sorted = sorted(zip(PlayerTitles.values(), PlayerTitles.keys()))
print(f"Output \n***{titles_sorted} ")

출력 결과

***[(0, 'Zverev'), (1, 'Theim'), (3, 'Murray'), (17, 'Djokovic'), (20, 'Federer'), (20, 'Nadal')]

8. 주의: zip() 이터레이터는 한 번만 소비 가능

이런 계산을 수행할 때 한 가지 주의할 점이 있습니다. zip()이 생성하는 이터레이터(iterator)는 단 한 번만 소비(consume)할 수 있다는 것입니다.

titles_and_players = zip(PlayerTitles.values(), PlayerTitles.keys())
print(f"Output \n***{min(titles_and_players)} ")

출력 결과

***(0, 'Zverev')

9. 같은 이터레이터를 다시 호출하면 빈 시퀀스(empty sequence) 관련 예외가 발생하게 됩니다. 따라서 zip()의 결과를 여러 번 사용해야 한다면 미리 list()로 변환해 두는 것이 좋습니다.

10. 값이 중복될 때의 동작

마지막으로 기억해야 할 점은, (값, 키) 쌍을 대상으로 하는 계산에서 여러 항목이 같은 값을 가질 경우 키가 최종 결과를 결정한다는 것입니다.

예를 들어 min()이나 max() 같은 계산에서 중복된 값이 존재하면, 가장 작거나 큰 키를 가진 항목이 반환됩니다. 바로 이 때문에 최대 타이틀을 가진 선수를 찾으면 Federer(20)와 Nadal(20) 중 알파벳 순서가 뒤인 Nadal 하나만 결과로 나오는 것입니다(7번 단계 출력 참고).

정리

딕셔너리 계산에는 세 가지 접근 방식이 있습니다. 첫째, min()/max()에 key 함수(lambda)를 전달하는 방법은 키나 값을 개별적으로 구할 때 유용하지만 값과 키를 동시에 얻으려면 추가 조회가 필요합니다. 둘째, zip()으로 (값, 키) 쌍을 만드는 방법은 한 번의 호출로 값과 키를 모두 얻을 수 있어 가장 깔끔하지만, 이터레이터가 일회성이라는 점과 값 중복 시 키가 결과를 좌우한다는 점을 유의해야 합니다. 셋째, 정렬이 필요하다면 zip()과 sorted()를 조합하면 됩니다. 상황에 맞는 방법을 선택해 활용하시기 바랍니다.