최빈값을 찾거나 점수순으로 정렬할 때, 첫 번째 기준만으로 결과가 하나로 결정되지 않을 수 있다. 이때 반복문을 먼저 작성하면 “먼저 만난 값”이나 “마지막에 만난 값”이 별다른 근거 없이 답이 되기 쉽다.
오늘 정리할 기준은 동률 처리와 출력 순서를 요구사항의 일부로 보는 것이다.
입력이 [4, 2, 4, 2, 7]이면 4와 2가 각각 두 번 등장한다.
| 동률 규칙 | 기대 결과 |
|---|---|
| 최빈값 중 가장 작은 값 | 2 |
| 최빈값 중 먼저 등장한 값 | 4 |
| 모든 최빈값을 오름차순으로 | [2, 4] |
세 결과는 서로 모순되지 않는다. 서로 다른 질문의 답이기 때문이다. 문제에서 규칙을 정했다면 그대로 구현하고, 직접 설계하는 함수라면 호출자가 알 수 있도록 규칙을 명시해야 한다.
records = [("B", 90), ("A", 90), ("C", 80)]
by_score = sorted(records, key=lambda row: -row[1])
by_score_and_name = sorted(records, key=lambda row: (-row[1], row[0]))
print(by_score)
print(by_score_and_name)
[('B', 90), ('A', 90), ('C', 80)]
[('A', 90), ('B', 90), ('C', 80)]
첫 정렬은 동점자의 입력 순서를 유지한다. 두 번째 정렬은 이름을 다음 기준으로 사용한다. 입력 순서가 곧 업무상 우선순위가 아니라면, 안정 정렬이라는 성질만으로 원하는 결과를 얻었다고 판단할 수 없다.
def rank_records(records):
return sorted(records, key=lambda row: (-row[1], row[0]))
first = [("B", 90), ("A", 90), ("C", 80)]
second = [("C", 80), ("A", 90), ("B", 90)]
assert rank_records(first) == rank_records(second)
이 예제는 이름이 서로 다른 데이터다. 점수와 이름으로 순서가 결정되므로 입력 순서를 바꿔도 같은 결과가 나와야 한다. 반대로 “먼저 접수한 항목 우선”이 조건이라면 순서를 바꾸었을 때 결과가 달라지는 것이 정상일 수 있다. 테스트의 성질도 요구사항에서 도출해야 한다.
ORDER BY score DESC만 사용하면 동점 행 사이의 순서는 정해지지 않는다. 동점자를 고유한 학생 번호순으로 보여주려면 ORDER BY score DESC, student_id ASC처럼 끝까지 구분 가능한 기준을 추가한다.
특히 상위 일부 행만 조회할 때는 경계에 동점자가 있는지 확인해야 한다. “정해진 개수만 반환”과 “경계 점수의 동점자도 모두 포함”은 서로 다른 요구다. 정렬 기준을 추가하는 것만으로 후자의 포함 규칙까지 해결되지는 않는다.
코드가 반복해서 같은 값을 반환하는 것과 명세에 맞는 값을 반환하는 것은 다르다. 우연한 순서에 기대지 않고 선택 규칙을 코드와 테스트 양쪽에 드러내는 것이 이번 정리의 핵심이다.