# [Python] 정렬 마스터하기: 주요 함수와 모듈 소개

최근 프로그래머스에서 Python을 사용해 알고리즘 문제를 풀다가 정렬이 필요한 문제를 만났다. Python에서는 key함수를 이용해서 정렬을 하는데 다른 언어처럼 비교 함수를 구현해 정렬할 수 있는지 궁금해져서 공부를 하게 됐다. 이 글에서는 Python에서 정렬하는 방법을 알아본다.

[프로그래머스 - 가장 큰 수](https://school.programmers.co.kr/learn/courses/30/lessons/42746#)
## Key 함수로 리스트 정렬하기

Python에서 가장 간단한 정렬 방법 중 하나는 key 함수를 사용하는 것이다. key 함수는 인자 하나를 받아 정렬에 사용할 기준 값을 반환한다.

key 함수, 또는 collation 함수는 `min()`, `max()`, `sorted()`, `list.sort()`, `heapq.merge()`, `heapq.nlargest()`, `heapq.nsmallest()`, `itertools.groupby()` 등 정렬이 필요한 다양한 함수에서 활용된다.
### 예제: 대소문자 구분 없는 정렬
```python
words = ["apple", "Banana", "cherry", "Date"]
sorted_case_insensitive = sorted(words, key=str.lower)
sorted_default = sorted(words)

print(sorted_case_insensitive)
print(sorted_default)

# 출력 결과:
# ['apple', 'Banana', 'cherry', 'Date']
# ['Banana', 'Date', 'apple', 'cherry']

```

`str.lower` 함수를 key로 사용하면 대소문자를 구분하지 않고 알파벳 순으로 정렬한다. 반면, 기본 정렬은 대소문자를 구분해 정렬한다.
### 예제: 튜플의 특정 값 기준 정렬
```python
# 학생 정보 (이름, 성적, 나이) 튜플 리스트
students = [
    ('John', 'A', 15),
    ('Dave', 'B', 15),
    ('Alice', 'A', 12),
    ('Carol', 'B', 12),
]
sorted_students_by_age = sorted(students, key=lambda student: student[2])

print(sorted_students_by_age)

# 출력 결과:
# [('Alice', 'A', 12), ('Carol', 'B', 12), ('John', 'A', 15), ('Dave', 'B', 15)]
```

나이를 기준으로 정렬하고 싶다면 key 함수에 나이에 해당하는 값을 반환하는 함수를 지정해 정렬하면 된다. 이 예제에서는 익명 함수를 사용해서 나이를 반환하는 함수를 구현했다.

### 예제: 여러 기준으로 정렬하기

Python 정렬 알고리즘은 기본적으로 안정적(stable)이어서, 같은 키 값을 가진 요소들 사이의 원래 순서를 유지한다. 여러 조건을 사용해 정렬할 때는 `key` 함수를 통해 반환되는 값을 튜플로 구성해 이를 쉽게 달성할 수 있다.

다음 예제에서는 학생 정보를 담은 튜플 리스트를 성적(내림차순)과 나이(오름차순)를 기준으로 정렬한다. 이를 위해 `lambda` 함수를 사용해 정렬 기준을 정의한다.

```python
students = [('Carol', 'B', 12), ('Alice', 'A', 12), ('Dave', 'B', 15), ('John', 'A', 15)]
sorted_students = sorted(students, key=lambda student: (-ord(student[1]), student[2]))
print(sorted_students)
# 출력 결과:
# [('Alice', 'A', 12), ('John', 'A', 15), ('Carol', 'B', 12), ('Dave', 'B', 15)]
```

위 예제에서 `lambda` 함수는 각 학생(`student`)에 대해 성적의 알파벳(`student[1]`)을 ASCII 코드로 변환해 내림차순 정렬 기준으로 하고, 같은 성적을 가진 학생들 사이에서는 나이(`student[2]`)를 오름차순 정렬 기준으로 한다.

또 다른 방법으로 성적을 기준으로 내림차순 정렬한 후, 이후에 나이를 기준으로 오름차순 정렬하는 방식도 가능하다. 하지만, Python 정렬이 안정적이라는 점을 활용해 한 번에 여러 조건을 적용하는 것이 코드를 간결하게 유지하고 성능 측면에서도 유리하다.

```python
# 성적 기준 내림차순 정렬 (단계 1)
sorted_by_grade = sorted(students, key=lambda student: -ord(student[1]))
# 나이 기준 오름차순으로 다시 정렬 (단계 2)
sorted_by_grade_then_age = sorted(sorted_by_grade, key=lambda student: student[2])
print(sorted_by_grade_then_age)
# 출력 결과:
# [('Alice', 'A', 12), ('John', 'A', 15), ('Carol', 'B', 12), ('Dave', 'B', 15)]
```

### operator 모듈을 활용하여 리스트 정렬하기

함수를 임의로 정의할 수 있지만, operator 모듈의 사전 정의된 기능을 활용할 수도 있다. 예를 들어, 리스트의 인덱스1에 위치한 아이템을 가져오는 lambda 함수 대신 `operator.itemgetter(1)`를 사용할 수 있고, dictionary의 key나 클래스 멤버는 `operator.attrgetter("firstname")`와 같이 접근할 수 있다.

여러 기준으로 정렬하기를 원한다면 `operator.itermgetter(1, 2)` 또는 `operator.attrgetter("firstname", "lastname")`과 같이 사용할 수 있다.

```python
import operator

sorted_students = sorted(students, key=operator.itemgetter(1, 2))
print(sorted_students)
# 출력 결과:
# [('Alice', 'A', 12), ('Carol', 'A', 15), ('Dave', 'B', 12), ('Bob', 'B', 15)]
```

`operator.itemgetter(1, 2)`를 사용해 성적 오름차순, 나이 오름차순으로 정렬한다.

```python
from operator import attrgetter

class Person:
    def __init__(self, firstname, lastname):Ï
        self.firstname = firstname
        self.lastname = lastname

    def __repr__(self):
        return f"Person({self.firstname}, {self.lastname})"

persons = [
    Person('John', 'Doe'),
    Person('Jane', 'Doe'),
    Person('Alice', 'Cooper'),
    Person('Bob', 'Barker')
]

sorted_persons = sorted(persons, key=attrgetter('lastname', 'firstname'))
Ï
for person in sorted_persons:
    print(person)

# 출력 결과:
# Person(Bob, Barker)
# Person(Alice, Cooper)
# Person(Jane, Doe)
# Person(John, Doe)
```

클래스나 dictionary의 경우 `operator.attrgetter(...)`로 속성 기준 정렬을 한다.

### `functools.cmp_to_key`로 리스트 정렬하기

`functools.cmp_to_key` 함수는 사용자 정의 비교 함수를 키 함수로 변환해 사용한다. Python 3에서는 `cmp(a, b)` 형태의 직접 비교 방식 대신 키 함수 사용을 권장한다. 비교 함수를 사용해야 하는 상황이라면 이 함수로 비교 함수를 키 함수로 변환할 수 있다.

```python
from functools import cmp_to_key

def compare_items(x, y):
    return len(x) - len(y)

key_function = cmp_to_key(compare_items)

str_list = ['banana', 'apple', 'cherry', 'date']
sorted_str_list = sorted(str_list, key=key_function)

print(sorted_str_list)  

# 출력 결과:
# ['date', 'apple', 'banana', 'cherry']
```

`compare_items` 함수는 문자열 길이를 비교한다. `cmp_to_key`로 이를 키 함수로 변환하고, `sorted`의 `key` 인자로 전달해 문자열 길이 기준으로 정렬한다.

## 정렬을 지원하는 클래스 만들기

복잡한 정렬 로직이 필요한 경우 사용자 정의 클래스를 만들어 정렬할 수 있도록 하는 것이 유지보수와 가독성 측면에서 유리하다.

Python에서는 매직 메소드를 이용하여 클래스 인스턴스 간 비교 연산자를 직접 정의할 수 있고 이렇게 구현한 비교 연산자들은 `sorted`, `list.sort()`와 같은 내장 함수와도 함께 사용할 수 있어 더 깨끗하고 Pythonnic한 코드를 만들 수 있다.

```python
class Person:
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname

    def __repr__(self):
        return f"Person('{self.firstname}', '{self.lastname}')"

    # 비교 매직 메소드 추가
    def __lt__(self, other):
        return (self.lastname, self.firstname) < (other.lastname, other.firstname)
    
persons = [
    Person('John', 'Doe'),
    Person('Jane', 'Doe'),
    Person('Alice', 'Cooper'),
    Person('Bob', 'Barker')
]

# 성(lastname)과 이름(firstname) 순으로 정렬
sorted_persons = sorted(persons)

for person in sorted_persons:
    print(person)

# 출력 결과:
# Person('Bob', 'Barker')
# Person('Alice', 'Cooper')
# Person('Jane', 'Doe')
# Person('John', 'Doe')
```

## 결론

이번 포스팅을 작성하면서 python 정렬 방식을 확실하게 정리한 것 뿐만 아니라 operator 모듈을 활용하는 방법이나 여러 정렬 기준을 한 번에 적용하여 정렬하는 방법까지 알게 됐다. Python을 꽤 오래 사용했었는데 관성적으로 사용하는 것들만 사용하던 것을 반성하는 시간이 됐다. 

오랜만에 어떤 기능들이 추가됐는지 궁금하여 [Python 3.12 릴리즈 문서](https://docs.python.org/ko/3/whatsnew/3.12.html)를 열어보니 type statement가 추가되어 generic 타입과 type aliases를 좀 더 자연스럽게 할 수 있게 됐고, f-strings 사용 시 불편했던 `"{""}"`이 이제는 오류가 나지 않는다고 해서 반가웠다. 언어도 끊임없이 발전하니 언어에 대한 공부도 게을리하지 말아야겠다고 생각이 들었다.

다음에는 최근에 많이 쓰고 있는 Dart나 Python의 새로운 기능들이나 어떻게 업데이트가 되어왔는지 한 번 훑어보는 글을 작성해봐야겠다.

## References
- [Python3 Docs - operator module](https://docs.python.org/ko/3/library/operator.html)
- [Python3 Docs - key function](https://docs.python.org/3/glossary.html#term-key-function)
- [Python3 Docs - functools.cmp_to_key](https://docs.python.org/3/library/functools.html#functools.cmp_to_key)
- [Python Magic Methods 비교 연산자](https://supermemi.tistory.com/entry/Python-3-Magic-Methods-%EB%8B%A4%EB%A3%A8%EA%B8%B0-2%ED%8E%B8-%EB%B9%84%EA%B5%90-%EC%97%B0%EC%82%B0%EC%9E%90-eq-ne-lt-gt-le-ge)
- [Python3 Docs - 정렬 How To](https://docs.python.org/ko/3/howto/sorting.html)
