리스트에 데이터를 담는 법은 4편에서 끝냈습니다. 그런데 담아만 놓으면 뭐 하나요. 판매량 순으로 줄을 세우고, 가격이 싼 것부터 보여주고, 같은 반 안에서는 키 순서로 다시 줄을 세워야 비로소 "데이터"가 "정보"가 됩니다. 실무 코드에서 정렬이 안 들어가는 프로그램을 거의 본 적이 없어요. 쇼핑몰의 "낮은 가격순", 게임의 랭킹 보드, 메일함의 최신순 정렬까지 전부 오늘 배울 두 개의 도구, sort()와 sorted()로 만들어집니다.
sort()와sorted()의 차이 — 그리고 초보자의 80%가 한 번은 밟는None함정key매개변수 하나로 글자 수 순, 가격순, 성적순 등 어떤 기준으로든 정렬하기- 람다(lambda)와 다중 기준 정렬 — "반 오름차순, 같은 반이면 키 내림차순" 같은 요구사항 처리
- 파이썬 정렬이 보장하는 안정 정렬이 실무에서 왜 고마운 기능인지
1. sort()와 sorted() — 이름은 비슷한데 성격이 다르다
편의점 야간 알바를 한다고 상상해 봅시다. 손님 대기 시간을 분 단위로 기록해 둔 리스트가 있어요. 짧은 순서로 줄을 세우고 싶다면 리스트의 sort() 메서드를 부르면 됩니다.
wait_times = [12, 3, 27, 8, 15] wait_times.sort() print(wait_times) # [3, 8, 12, 15, 27]
간단하죠. 그런데 여기에 파이썬 입문자라면 반드시 한 번은 밟고 지나가는 지뢰가 하나 묻혀 있습니다. sort()는 원본 리스트를 직접 바꾸고, 아무것도 돌려주지 않습니다. 정확히는 None을 돌려줘요.
menu = ["라떼", "아메리카노", "콜드브루"] result = menu.sort() print(result) # None ← !!! print(menu) # ['라떼', '아메리카노', '콜드브루']
나는 처음 파이썬을 배울 때 이것 때문에 30분을 날렸습니다. result에 정렬된 리스트가 들어 있을 거라 굳게 믿고 다음 줄에서 result[0]을 꺼내려다 TypeError 폭탄을 맞았죠. 파이썬이 일부러 이렇게 설계한 겁니다. "원본을 바꾸는 메서드는 값을 돌려주지 않는다"는 일관된 철학이에요. 원본은 그대로 두고 정렬된 새 리스트를 받고 싶다면 내장 함수 sorted()를 씁니다.
orders = [4500, 6000, 3000, 5500] cheap_first = sorted(orders) print(cheap_first) # [3000, 4500, 5500, 6000] print(orders) # [4500, 6000, 3000, 5500] ← 원본은 무사
| 구분 | list.sort() | sorted() |
|---|---|---|
| 원본 | 직접 바꿈 (제자리 정렬) | 건드리지 않음 |
| 반환값 | None | 정렬된 새 리스트 |
| 쓸 수 있는 대상 | 리스트 전용 | 튜플, 문자열, 딕셔너리 등 반복 가능한 모든 것 |
| 메모리 | 절약 (복사본 없음) | 새 리스트만큼 추가 사용 |
뭘 써야 할지 고민된다면 기준은 하나입니다. 원본이 다시 필요한가? 필요하면 sorted(), 필요 없으면 sort(). 실무에서는 원본을 보존하는 습관이 버그를 줄여 주기 때문에 저는 애매하면 sorted() 쪽을 택합니다.
lst = lst.sort()이 한 줄이면 멀쩡한 리스트가
None으로 증발합니다. sort()는 반환값이 None이니까요. 이후 lst[0]을 시도하는 순간 TypeError: 'NoneType' object is not subscriptable가 터집니다. 에러 메시지에 NoneType이 보이면 "아, 어딘가에서 sort()의 반환값을 받았구나"부터 의심하세요.
2. 내림차순 — reverse=True 한 마디면 끝
시험 점수를 높은 순으로 줄 세우고 싶을 때, 일단 오름차순으로 정렬한 다음 뒤집어야 하나? 그럴 필요 없습니다. 두 함수 모두 reverse=True 옵션을 받아요.
scores = [88, 95, 73, 100, 64] print(sorted(scores, reverse=True)) # [100, 95, 88, 73, 64]
헷갈리기 쉬운 게 하나 있는데, 리스트에는 reverse()라는 별개의 메서드도 있습니다. 이건 정렬이 아니라 단순히 순서만 뒤집는 거예요. [1, 3, 2]에 reverse()를 쓰면 [2, 3, 1]이 됩니다. 정렬 옵션 reverse=True와는 완전히 다른 물건이니 이름만 보고 혼동하지 마세요.
3. key — 정렬 기준을 통째로 갈아끼우는 매개변수
여기서부터가 오늘의 본론입니다. 솔직히 sort()까지는 누구나 합니다. 실력 차이는 key에서 갈려요.
key에는 함수를 넘깁니다. 파이썬은 정렬 직전에 각 요소를 그 함수에 한 번씩 통과시키고, 함수가 돌려준 값을 기준으로 줄을 세웁니다. 예를 들어 채팅방 닉네임을 글자 수 순으로 정렬하고 싶다면, 글자 수를 재는 함수 len을 그대로 넘기면 됩니다.
nicknames = ["밤톨이", "달려라하니", "곰", "치즈냥이"] print(sorted(nicknames, key=len)) # ['곰', '밤톨이', '치즈냥이', '달려라하니']
주의할 점: key=len()이 아니라 key=len입니다. 괄호를 붙이면 "len을 지금 실행한 결과"가 되어 버려요. 우리는 함수 자체를 건네주고, 실행은 파이썬에게 맡기는 겁니다. 함수를 값처럼 주고받는 이 개념은 6부(함수 편)에서 제대로 다룹니다.
영어 단어 정렬에서도 key가 활약합니다. 파이썬 기본 정렬은 대문자를 소문자보다 앞에 둡니다(내부적으로 문자 코드 값을 비교하기 때문). 그래서 사전 순서를 기대하면 결과가 어긋나요.
words = ["banana", "Cherry", "apple"] print(sorted(words)) # ['Cherry', 'apple', 'banana'] print(sorted(words, key=str.lower)) # ['apple', 'banana', 'Cherry']
key=str.lower는 "비교할 때만 전부 소문자로 바꿔서 비교해 달라"는 뜻입니다. 원본 데이터는 바뀌지 않는다는 점이 포인트예요. key 함수는 어디까지나 줄 세우기용 기준값만 만들 뿐입니다.
4. 람다와 함께 — 진짜 데이터를 정렬해 보자
실무 데이터는 숫자 하나짜리 리스트가 아닙니다. 동네 빵집의 오늘 판매 기록이 (빵 이름, 판매 개수) 튜플로 쌓여 있다고 합시다. 많이 팔린 순서로 보고 싶어요.
sales = [("크림빵", 12), ("소금빵", 31), ("단팥빵", 7)]
sales.sort(key=lambda item: item[1], reverse=True)
print(sales)
# [('소금빵', 31), ('크림빵', 12), ('단팥빵', 7)]
lambda item: item[1]은 "각 튜플에서 1번 인덱스(판매 개수)를 꺼내 달라"는 일회용 함수입니다. 람다 문법이 낯설다면 지금은 lambda 입력: 기준값 패턴 하나만 외워 두세요. 어차피 정렬에서 쓰는 람다의 9할은 이 모양입니다.
딕셔너리가 담긴 리스트도 똑같습니다. 중고거래 앱에 올라온 판매글을 가격순으로 줄 세워 볼게요.
used_items = [
{"name": "자전거", "price": 80000},
{"name": "모니터", "price": 45000},
{"name": "키보드", "price": 15000},
]
by_price = sorted(used_items, key=lambda d: d["price"])
print([d["name"] for d in by_price])
# ['키보드', '모니터', '자전거']
인덱스나 키로 꺼내기만 하는 단순한 경우라면 표준 라이브러리
operator.itemgetter가 람다보다 살짝 빠르고 의도도 분명합니다. from operator import itemgetter 후에 sorted(sales, key=itemgetter(1))처럼 씁니다. 외울 필요까지는 없고, 남의 코드에서 만났을 때 "아 그거"라고 알아보면 충분합니다.
5. 다중 기준 정렬 — "반은 오름차순, 키는 내림차순"
체육대회 입장 순서를 짠다고 합시다. 1반부터 입장하되, 같은 반 안에서는 키 큰 학생이 앞에 섭니다. 기준이 두 개죠. 이럴 때 key 함수가 튜플을 돌려주게 하면 됩니다. 파이썬은 튜플을 비교할 때 첫 요소부터 차례로 비교하니까, 첫 기준이 같으면 자동으로 두 번째 기준으로 넘어갑니다.
# (이름, 반, 키)
students = [("지우", 2, 165), ("하준", 1, 172),
("서연", 1, 158), ("민호", 2, 180)]
students.sort(key=lambda s: (s[1], -s[2])) # 반 오름차순, 키 내림차순
print(students)
# [('하준', 1, 172), ('서연', 1, 158), ('민호', 2, 180), ('지우', 2, 165)]
키 내림차순을 -s[2]처럼 부호를 뒤집어 처리한 게 보이시나요? 숫자 기준일 때만 통하는 트릭이지만 정말 자주 씁니다. 문자열처럼 부호를 못 붙이는 기준이 섞이면 정렬을 두 번 나눠서 하는데, 그게 가능한 이유가 바로 다음 절의 안정 정렬입니다.
6. 안정 정렬 — 파이썬이 조용히 지켜주는 약속
파이썬의 정렬은 안정 정렬(stable sort)입니다. 기준값이 같은 요소끼리는 원래 순서를 그대로 유지한다는 뜻이에요.
data = [("귤", 3000), ("배", 3000), ("감", 1000)]
print(sorted(data, key=lambda x: x[1]))
# [('감', 1000), ('귤', 3000), ('배', 3000)] ← 귤·배 순서 유지
귤과 배는 둘 다 3,000원이지만, 정렬 후에도 입력 순서(귤 → 배)가 흔들리지 않았습니다. 별것 아닌 듯하지만 이 보증 덕분에 "이름순으로 먼저 정렬하고, 그다음 가격순으로 다시 정렬"하면 가격이 같은 상품끼리는 이름순이 자동으로 유지됩니다. 정렬을 겹겹이 쌓을 수 있는 거죠. 내부적으로는 Timsort라는 알고리즘이 일하는데, 파이썬 개발자 Tim Peters가 만든 거라 이름이 그렇습니다. 현실 데이터에 흔한 "이미 부분적으로 정렬된 구간"을 영리하게 활용해서, 운이 좋으면 거의 다 정렬된 리스트를 눈 깜짝할 새 처리합니다.
원본을 지키려면 sorted(), 바꿔도 되면 sort(). 기준을 바꾸고 싶으면 key에 함수를. 기준이 여러 개면 key가 튜플을 돌려주게. 이 네 문장이 오늘의 전부다.
7. 정렬에서 자주 터지는 에러와 함정
마지막으로, 정렬하다 만나는 대표적인 사고 두 가지를 미리 겪어 봅시다.
sorted([3, "1", 2]) # TypeError: '<' not supported between instances of 'str' and 'int'
숫자와 문자열은 누가 큰지 비교할 수 없으니 파이썬이 정직하게 거부합니다. 엑셀이나 CSV에서 읽어 온 데이터에 숫자와 문자가 섞여 있을 때 단골로 만나는 에러예요. 그리고 더 교묘한 함정이 하나 있습니다.
print(sorted(["10", "9", "100", "2"])) # ['10', '100', '2', '9'] ← 어딘가 이상하다? print(sorted(["10", "9", "100", "2"], key=int)) # ['2', '9', '10', '100'] ← 이게 원하던 결과
"10"이 "9"보다 앞에 오는 이유는 문자열 비교가 첫 글자부터 한 자씩 이뤄지기 때문입니다. '1'이 '9'보다 코드값이 작으니 "100"도 "2"보다 앞이 되죠. 파일에서 읽은 데이터는 전부 문자열이라는 사실을 잊으면 정렬 결과가 통째로 어긋납니다.
key=int 한 마디로 해결됩니다.
덤으로 하나 더. min()과 max()도 key를 받습니다. 10km 기록(분)에서 가장 빠른 러너를 찾을 때 정렬까지 할 필요 없이 min(runners, key=lambda r: r[1]) 한 줄이면 끝나요. "1등 하나만 필요한데 전체를 정렬하고 있다면" 신호입니다. min/max로 바꾸세요.
8. 연습문제
손으로 풀어 보고 정답을 확인하세요. 답을 보기 전에 머리로 결과를 예측하는 게 핵심입니다.
- 카페 주문 금액
[4500, 6000, 3000, 5500]을 원본을 유지한 채 비싼 순으로 정렬한 새 리스트를 만드세요. - 튜플 리스트
[("포도", 7), ("사과", 7), ("딸기", 2)]를 개수 오름차순으로 정렬하면 포도와 사과의 순서는 어떻게 될까요? 이유까지 설명해 보세요. names = ["bob", "Alice", "carol"]을 대소문자 구분 없이 사전순으로 정렬하세요.
정답과 해설
# 1번
prices = [4500, 6000, 3000, 5500]
expensive_first = sorted(prices, reverse=True)
# [6000, 5500, 4500, 3000] — sorted()라서 원본 보존
# 2번: [('딸기', 2), ('포도', 7), ('사과', 7)]
# 개수가 같은 포도·사과는 안정 정렬 덕분에 입력 순서 유지
# 3번
names = ["bob", "Alice", "carol"]
print(sorted(names, key=str.lower))
# ['Alice', 'bob', 'carol']
더 공부할 거리
- 파이썬 공식 정렬 HOW TO (한국어) — key, 안정 정렬, 다중 기준까지 공식 문서가 의외로 친절합니다.
- sorted() 내장 함수 레퍼런스
- 점프 투 파이썬 — 리스트 자료형
- Real Python — How to Use sorted() and .sort() (영어)
다음 편 예고
6편에서는 리스트의 사촌, 튜플(tuple)을 다룹니다. "바꿀 수 없는 리스트가 왜 필요하지?"라는 당연한 의문부터, 함수가 값을 여러 개 돌려줄 때 일어나는 패킹/언패킹의 비밀까지. 리스트만 쓰던 분들이 튜플을 익히면 코드가 한 단계 단단해집니다. 내일 만나요.
'AI > Coding' 카테고리의 다른 글
| 파이썬 입문 4편 — 리스트(list) 완전 정복: 추가·삭제·정렬부터 복사 함정까지 (0) | 2026.06.11 |
|---|---|
| 파이썬 입문 3편 — 문자열 완전 정복: 인덱싱·슬라이싱부터 f-string·인코딩까지 (1) | 2026.06.11 |
| 파이썬 입문 2편 — 숫자형 완전 정복: 정수·실수, 두 가지 나눗셈과 부동소수점 오차까지 (0) | 2026.06.11 |
| 파이썬 입문 1편 — 프로그래밍이 처음이어도 괜찮습니다: 설치부터 첫 코드, 변수·자료형까지 (0) | 2026.06.10 |
| 코딩의 덧셈·나눗셈·몫·나머지 완전 기초: 초보자를 위한 산술 연산자 설명 (0) | 2026.06.09 |