[Python] 정렬 마스터하기: 주요 함수와 모듈 소개
기본 key 함수를 사용한 정렬부터 여러 기준으로 정렬하기, 비교 함수, operator 모듈, 사용자 정의 클래스를 활용한 정렬 방법까지 소개
![[Python] 정렬 마스터하기: 주요 함수와 모듈 소개](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708053698604%2F9d1cadf3-414f-4eb6-9242-b3a1095e338f.png&w=3840&q=75)
최근 프로그래머스에서 Python을 사용해 알고리즘 문제를 풀다가 정렬이 필요한 문제를 만났다. Python에서는 key함수를 이용해서 정렬을 하는데 다른 언어처럼 비교 함수를 구현해 정렬할 수 있는지 궁금해져서 공부를 하게 됐다. 이 글에서는 Python에서 정렬하는 방법을 알아본다.
Key 함수로 리스트 정렬하기
Python에서 가장 간단한 정렬 방법 중 하나는 key 함수를 사용하는 것이다. key 함수는 인자 하나를 받아 정렬에 사용할 기준 값을 반환한다.
key 함수, 또는 collation 함수는 min(), max(), sorted(), list.sort(), heapq.merge(), heapq.nlargest(), heapq.nsmallest(), itertools.groupby() 등 정렬이 필요한 다양한 함수에서 활용된다.
예제: 대소문자 구분 없는 정렬
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로 사용하면 대소문자를 구분하지 않고 알파벳 순으로 정렬한다. 반면, 기본 정렬은 대소문자를 구분해 정렬한다.
예제: 튜플의 특정 값 기준 정렬
# 학생 정보 (이름, 성적, 나이) 튜플 리스트
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 함수를 사용해 정렬 기준을 정의한다.
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 정렬이 안정적이라는 점을 활용해 한 번에 여러 조건을 적용하는 것이 코드를 간결하게 유지하고 성능 측면에서도 유리하다.
# 성적 기준 내림차순 정렬 (단계 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")과 같이 사용할 수 있다.
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)를 사용해 성적 오름차순, 나이 오름차순으로 정렬한다.
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) 형태의 직접 비교 방식 대신 키 함수 사용을 권장한다. 비교 함수를 사용해야 하는 상황이라면 이 함수로 비교 함수를 키 함수로 변환할 수 있다.
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한 코드를 만들 수 있다.
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 릴리즈 문서를 열어보니 type statement가 추가되어 generic 타입과 type aliases를 좀 더 자연스럽게 할 수 있게 됐고, f-strings 사용 시 불편했던 "{""}"이 이제는 오류가 나지 않는다고 해서 반가웠다. 언어도 끊임없이 발전하니 언어에 대한 공부도 게을리하지 말아야겠다고 생각이 들었다.
다음에는 최근에 많이 쓰고 있는 Dart나 Python의 새로운 기능들이나 어떻게 업데이트가 되어왔는지 한 번 훑어보는 글을 작성해봐야겠다.


