이 장에서는 List의 정렬에 대한 이야기를 하려고 합니다.
리스트의 정렬에는 두가지 방법이 있습니다.
sorted 는 파이썬의 내장함수(built-in function)입니다.
즉, 파이썬의 Sequential data structures에서 모두 작동한다는 의미입니다.
sorted 함수는 정렬된 리스트를 반환합니다.
원본 객체는 변경되지 않습니다.
파라미터를 입력함으로써 사용자 기준에 따라 정렬 가능합니다.
list_a = [4,6,55,34,12,7]
list_b = sorted(list_a)
print(list_a)
print(list_b)
[4, 6, 55, 34, 12, 7]
[4, 6, 7, 12, 34, 55]
list_a = [4,6,55,34,12,7]
list_b = list_a.sort()
print(list_a)
print(list_b)
[4, 6, 7, 12, 34, 55]
None # sort 메소드는 return이 없어, list_b는 none 값이 할당돼있다.
sorted 와 sort 는 파라미터가 동일하므로 앞으로 소개할 내용을 두 함수 혹은 메소드에 사용할 수 있다는 점을 기억하세요.
sort 는 기본적으로 리스트 내의 객체를 자연스러운 순서(natural order)로 오름차순 정렬합니다.
여기서 자연스러운 순서란 숫자에서는 대소, 문자열에서는 선후 관계에 따른 순서 등을 말합니다. 직관적으로 순서를 확인할 수 있다는 말이죠.
이는 내장 데이터 타입에서는 잘 작동합니다. int나 float, string 같은 경우에는 말이죠.
하지만 사용자 정의 클래스에는 어떨까요? 객체 비교 특별 메서드가 정의되어 있지 않다면 sort는 작동하지 않고 에러가 발생합니다.
그럼 비교 특별 메소드를 정의하면 해결할 수 있습니다.
하지만 우리가 class를 정의해서 사용하는 경우, 대부분은 하나의 객체에 여러 특성을 포함하기 위함입니다. 그럼 이 여러 특성에 대해 각각 정렬을 지원해야할 경우가 있을 겁니다.
즉, 비교 특별 메소드를 정의하여 자연스러운 순서를 정의해주는 것은 클래스 정렬에서 큰 의미가 없다는 것입니다.
아래 예시를 보시죠.
class Student:
def __init__(self, name, year, id):
self.name = name
self.year = year
self.id = id
def __repr__(self):
return f'Student[{self.name}, {self.year}, {self.id}]'
이런 학생의 이름, 학년, 학번을 가지는 자료형이 필요하여 Student 클래스를 정의하였습니다.
만약 비교 특별 메소드를 정의한다면 (이름, 학년, 학번) 중, 어떤 특성을 비교 기준으로 하시겠나요?
대답이 쉽지 않죠. 왜냐하면 학생들을 이름순으로, 학년순으로, 학번순으로 정렬해야할 경우가 모두 있을만 하니까요.
그렇기 때문에 클래스 비교를 위해 비교 특별 메소드를 정의하는 것은 좋은 선택지는 아닙니다.
좋은 선택지는 key parameter를 활용하는 것입니다.
sort 의 key parameter는 '함수'를 값으로 받습니다.
def로 새로운 함수를 선언해도 되겠지만, 보다 간편한 lambda 함수를 활용하는 편이 좋습니다.
예시를 봅시다.
cf) lambda 함수 : https://wikidocs.net/22804
studnets = [Student('Jeon', 4, 201829160),
Student('Joe', 1, 202325860),
Student('Kim', 3, 202021468),
Student('Jeong', 2, 202218489)]
students 리스트에 4명의 학생을 Student 클래스로 정의하여 담았습니다.
name, age, id 3가지 attribute 중, name으로 정렬해보겠습니다.
studnets.sort(key=lambda x: x.name)
print(studnets)
>> output
[Student[Jeon, 4, 201829160],
Student[Jeong, 2, 202218489],
Student[Joe, 1, 202325860],
Student[Kim, 3, 202021468]]
만약 year를 기준으로 정렬하고 싶다면, name 대신 year를 넣으면 됩니다.
아주 간단하게 원하는 기준으로 정렬할 수 있습니다.
간단하게 멤버변수를 가져오는 정도의 동작을 함수로 선언하였지만, String의 대/소문자를 전환하여 정렬하는 등의 동작도 가능합니다.
말 그대로 함수이니까요.
1순위와 2순위를 정하고 1순위가 같은 경우 2순위로 정렬해야하는 경우가 많이 발생합니다.
예를 들면, 장학금 선정에서 1순위-학점, 2순위-소득분위 등으로 정하고 학점이 동점이면 소득분위를 기준으로 우선순위를 부여하는 경우.
이런 정렬은 Tuple 을 이용하여 구현할 수 있습니다.
Tuple은 비교가능하며 자연스러운 순서가 정해져 있습니다. tuple에서의 비교는 각 인자에 대해 이터레이션하면서 비교합니다. 즉 튜플의 첫번째 요소부터 비교하고, 만약 같다면 두번째 요소를 비교하는 식으로 작동합니다.
이 tuple을 lambda 함수에 적용합니다.
이름, 학년이 같은 학생이 존재할 때, 학년, 이름, 학번 순으로 정렬하고 싶습니다.
studnets.append(Student('Jeong', 2, 20221897))
studnets.sort(key=lambda x: (x.year, x.name , x.id))
print(studnets)
[Student[Joe, 1, 202325860],
Student[Jeong, 2, 202218297],
Student[Jeong, 2, 202218489],
Student[Kim, 3, 202021468],
Student[Jeon, 4, 201829160]]
lambda 함수의 return을 tuple 값을 반환하면 다수의 기준으로 정렬이 가능합니다.
그런데, 이런 경우가 있을 수도 있습니다. 학년은 4학년부터 내림차순으로, 학번은 오름차순으로 정렬하고 싶어요. 이런 경우에는 reverse parameter로는 불가능합니다.
튜플과 단항(unary) 부호 반전 연산자(-)를 활용하여 가능합니다.
studnets.append(Student('Jeong', 2, 20221897))
studnets.sort(key=lambda x: (-x.year, x.name , x.id))
print(studnets)
[Student[Jeon, 4, 201829160],
Student[Kim, 3, 202021468],
Student[Jeong, 2, 20221897],
Student[Jeong, 2, 202218297],
Student[Joe, 1, 202325860]]
이는 튜플 값의 year 위치의 요소를 음수로 만들어 절대값이 클수록 값이 작아지도록 하여 전체 정렬은 오름차순이지만 우리가 원하는대로 정렬한 것입니다.
하지만 이 방법은 numeric data에만 유효하다는 한계가 있습니다. 위 작동방식을 보면 음수화가 가능한 데이터에만 작동할 수 있습니다.
string data인 name tag에 마이너스를 붙이면 에러가 방생합니다.
이런 경우에는, 두번의 sort를 실행하여 원하는 정렬을 얻습니다.
학년 순으로 내림차순 정렬하고, 같은 학년이면 이름 순으로 오름차순하여 정렬하고 싶다면,
students.sort(key=lambda x: x.name, reverse=False) students.sort(key=lambda x: x.year, reverse=True)
얻어내고 싶은 정렬 기준 우선순위의 역순으로 정렬을 수행해야 한다는 점이 포인트입니다.