[Python] 정렬 마스터하기: 주요 함수와 모듈 소개
기본 key 함수를 사용한 정렬부터 여러 기준으로 정렬하기, 비교 함수, operator 모듈, 사용자 정의 클래스를 활용한 정렬 방법까지 소개
최근 프로그래머스에서 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의 새로운 기능들이나 어떻게 업데이트가 되어왔는지 한 번 훑어보는 글을 작성해봐야겠다.