37. 내장 함수와 유용한 도구
Python은 어떤 모듈도 import할 필요 없이 항상 사용할 수 있는 풍부한 내장 함수 모음을 제공합니다. 이 함수들은 일상적인 Python 프로그래밍의 기반을 이루며, 데이터, 시퀀스, 컬렉션을 효율적으로 다루는 데 도움을 줍니다. 이 장에서는 Python에서 가장 유용한 내장 도구들을 살펴보고, 이를 활용해 더 깔끔하고 표현력이 좋은 코드를 작성하는 방법을 배웁니다.
Python의 타입 시스템 이해하기
특정 내장 함수로 들어가기 전에, Python이 데이터 타입을 어떻게 구성하는지 이해하는 것이 도움이 됩니다. 이 지식은 어떤 연산이 어떤 타입에서 동작하는지 예측하고, 오류 메시지가 발생했을 때 이를 이해하는 데 도움이 됩니다.
Python의 데이터 타입은 서로 보완적인 두 관점에서 이해할 수 있습니다:
타입 계층(Type Hierarchy): 타입들이 어떻게 연결되는가
이는 Python이 타입을 “무엇인지(ARE)”에 따라 패밀리로 묶어 구성하는 방식을 보여줍니다.
기능 기반 관점(Capability-Based View): 타입이 무엇을 할 수 있는가
내장 함수에서는, 타입이 무엇인지보다 타입이 무엇을 할 수 있는지가 더 중요합니다:
핵심 기능(capabilities):
- Iterable(이터러블):
for반복문(loop)에서 사용할 수 있음 →sum(),any(),all(),sorted()와 함께 동작 - Collection(컬렉션):
len()이 가능한 이터러블 →len()및in연산자와 함께 동작 - Sequence(시퀀스): 인덱싱이 가능한 컬렉션 →
[index]와 슬라이싱[start:end]지원
이것이 중요한 이유
내장 함수는 특정 기능을 요구합니다:
| 함수(Function) | 요구 사항(Requires) | 동작 대상(Works With) |
|---|---|---|
len() | 컬렉션(Collection) | str, list, dict, set, tuple |
sum() | 숫자의 이터러블(Iterable) | list, tuple, set, range, generator |
sorted() | 이터러블(Iterable) | str, list, dict, set, tuple |
[index] | 시퀀스(Sequence) | str, list, tuple, range |
이 범주를 이해하면 다음을 할 수 있습니다:
- 어떤 함수가 어떤 타입에서 동작하는지 예측하기
- "object is not iterable" 같은 오류 메시지 이해하기
- 인덱싱(
[0])이 가능한 경우와 반복(for)만 가능한 경우를 구분하기
37.1) 자주 쓰는 내장 함수(len, sum, min, max, abs, round)
Python에서 가장 자주 사용하는 내장 함수는 반복문이나 복잡한 로직을 직접 작성하지 않고도 데이터에 대한 일반적인 연산을 수행하도록 도와줍니다. 이 함수들은 최적화되어 있고, 가독성이 좋으며, Pythonic 코드의 기반을 이룹니다.
37.1.1) len()으로 길이 측정하기
len() 함수는 컬렉션의 항목 개수를 반환합니다. 문자열, 리스트(list), 튜플(tuple), 딕셔너리(dictionary), 셋(set) 및 기타 모든 컬렉션 타입에서 동작합니다.
# 문자열의 문자 수 세기
message = "Hello, World!"
print(len(message)) # Output: 13
# 리스트의 요소 수 세기
scores = [85, 92, 78, 90, 88]
print(len(scores)) # Output: 5
# 딕셔너리의 키-값 쌍 개수 세기
student = {"name": "Bob", "age": 21, "major": "CS"}
print(len(student)) # Output: 3
# 셋의 고유 항목 수 세기
unique_ids = {101, 102, 103, 101, 102} # 중복 제거됨
print(len(unique_ids)) # Output: 3len() 함수는 처리하기 전에 데이터의 크기를 알아야 할 때 특히 유용합니다:
# 크기에 따라 데이터 처리하기
data = [12, 45, 23, 67, 89, 34]
if len(data) < 5:
print("Not enough data for analysis")
else:
print(f"Analyzing {len(data)} data points") # Output: Analyzing 6 data points
average = sum(data) / len(data)
print(f"Average: {average}") # Output: Average: 45.037.1.2) sum()으로 합계 계산하기
sum() 함수는 이터러블(iterable)의 모든 숫자를 더합니다. 값을 누적하기 위해 반복문을 작성하는 것보다 훨씬 깔끔합니다.
# 숫자 리스트 합계
prices = [19.99, 24.50, 15.75, 32.00]
total = sum(prices)
print(f"Total: ${total}") # Output: Total: $92.24
# 튜플 합계
daily_steps = (8500, 10200, 7800, 9500, 11000)
weekly_total = sum(daily_steps)
print(f"Total steps this week: {weekly_total}") # Output: Total steps this week: 47000
# range 합계
total_1_to_100 = sum(range(1, 101))
print(total_1_to_100) # Output: 5050sum()과 len()을 결합해 평균을 계산하는 실용적인 예시는 다음과 같습니다:
# 평균 시험 점수 계산하기
test_scores = [88, 92, 79, 85, 90, 87]
total_score = sum(test_scores)
num_tests = len(test_scores)
average_score = total_score / num_tests
print(f"Average score: {average_score:.1f}") # Output: Average score: 86.8중요한 제한: sum()은 숫자에만 동작합니다. 문자열을 이어 붙이거나 리스트를 결합하는 데 사용할 수 없습니다:
# 이는 TypeError를 발생시킵니다
words = ["Hello", " ", "World"]
# sentence = sum(words) # TypeError: unsupported operand type(s)37.1.3) min()과 max()로 최솟값/최댓값 찾기
min()과 max() 함수는 이터러블에서 가장 작은 값과 가장 큰 값을 찾습니다. 숫자, 문자열, 그리고 비교 가능한 모든 객체에서 동작합니다.
# 최솟값과 최댓값 숫자 찾기
temperatures = [72, 68, 75, 70, 73, 69]
coldest = min(temperatures)
warmest = max(temperatures)
print(f"Temperature range: {coldest}°F to {warmest}°F")
# Output: Temperature range: 68°F to 75°F
# 문자열 최솟값/최댓값 찾기(알파벳 순)
names = ["Zoe", "Alice", "Bob", "Charlie"]
first_alphabetically = min(names)
last_alphabetically = max(names)
print(f"First: {first_alphabetically}, Last: {last_alphabetically}")
# Output: First: Alice, Last: Zoe컬렉션 대신 여러 인자를 직접 전달할 수도 있습니다:
# 개별 값 비교하기
lowest = min(45, 23, 67, 12, 89)
highest = max(45, 23, 67, 12, 89)
print(f"Lowest: {lowest}, Highest: {highest}")
# Output: Lowest: 12, Highest: 89
# 몇 개의 특정 값만 비교할 때 유용함
price1 = 19.99
price2 = 24.50
price3 = 15.75
cheapest = min(price1, price2, price3)
print(f"Cheapest option: ${cheapest}") # Output: Cheapest option: $15.7537.1.4) abs()로 절댓값 구하기
abs() 함수는 숫자의 절댓값, 즉 0으로부터의 거리(항상 양수)를 반환합니다. 방향은 중요하지 않고 크기만 중요할 때 유용합니다.
# 음수의 절댓값
print(abs(-42)) # Output: 42
print(abs(-3.14)) # Output: 3.14
# 양수의 절댓값(변화 없음)
print(abs(42)) # Output: 42
print(abs(3.14)) # Output: 3.14
# 0의 절댓값
print(abs(0)) # Output: 0방향은 중요하지 않은 차이를 계산하는 것이 흔한 사용 사례입니다:
# 기온 변화량 계산(크기만)
morning_temp = 65
evening_temp = 72
temperature_change = abs(evening_temp - morning_temp)
print(f"Temperature changed by {temperature_change}°F")
# Output: Temperature changed by 7°F37.1.5) round()로 숫자 반올림하기
round() 함수는 숫자를 지정한 소수점 자리수로 반올림합니다. 두 번째 인자가 없으면 가장 가까운 정수로 반올림합니다.
# 가장 가까운 정수로 반올림
print(round(3.7)) # Output: 4
print(round(3.2)) # Output: 3
print(round(3.5)) # Output: 4
print(round(4.5)) # Output: 4 (가장 가까운 짝수로 반올림)
# 특정 소수점 자리로 반올림
price = 19.876
print(round(price, 2)) # Output: 19.88 (소수점 2자리)
print(round(price, 1)) # Output: 19.9 (소수점 1자리)
# 음수 자릿수로 반올림(십의 자리, 백의 자리 등으로 반올림)
population = 1234567
print(round(population, -3)) # Output: 1235000 (천 단위)
print(round(population, -4)) # Output: 1230000 (만 단위)중간값에 대한 참고사항: 두 정수의 정확히 중간에 있는 숫자를 반올림할 때 Python은 특별한 규칙을 사용합니다. 예를 들어, 2.5는 정확히 2와 3 사이입니다. 3으로 올림될 것으로 예상할 수 있지만, Python은 짝수인 쪽으로 반올림합니다—이 경우 2입니다.
이를 "은행가 반올림(banker's rounding)"이라고 합니다. 이는 IEEE 754 표준의 일부이며 많은 반올림 작업에 걸쳐 편향을 줄이는 데 도움이 됩니다.
# 중간값은 가장 가까운 짝수로 반올림
print(round(0.5)) # Output: 0 (0은 짝수)
print(round(1.5)) # Output: 2 (2는 짝수)
print(round(2.5)) # Output: 2 (2는 짝수)
print(round(3.5)) # Output: 4 (4는 짝수)
print(round(4.5)) # Output: 4 (4는 짝수)37.2) enumerate()로 시퀀스 열거하기
시퀀스를 반복할 때는 요소와 그 위치가 둘 다 필요한 경우가 많습니다. enumerate() 함수는 둘 다 제공하므로, 수동 카운터 변수를 사용할 필요가 없습니다.
37.2.1) 수동 카운터의 문제점
enumerate()를 배우기 전에는, 프로그래머들이 위치를 추적하기 위해 카운터 변수를 자주 사용했습니다:
# 수동 카운터 방식(동작하지만 이상적이지 않음)
fruits = ["apple", "banana", "cherry", "date"]
index = 0
for fruit in fruits:
print(f"{index}: {fruit}")
index += 1
# Output:
# 0: apple
# 1: banana
# 2: cherry
# 3: date이 접근에는 몇 가지 단점이 있습니다:
- 관리해야 할 변수가 추가로 생김(
index) - 카운터 증가를 잊기 쉬움
37.2.2) enumerate()로 위치와 값을 함께 사용하기
enumerate() 함수는 이 문제를 우아하게 해결합니다. 이터러블을 받아 (인덱스, 요소) 쌍을 반환합니다:
# enumerate() 사용 - 더 깔끔하고 더 Pythonic
fruits = ["apple", "banana", "cherry", "date"]
for index, fruit in enumerate(fruits):
print(f"{index}: {fruit}")
# Output:
# 0: apple
# 1: banana
# 2: cherry
# 3: date문법 for index, fruit in enumerate(fruits)는 튜플 언패킹(tuple unpacking)(15장에서 배운 내용)을 사용합니다. 각 반복에서 enumerate()는 (0, "apple") 같은 튜플을 제공하며, 이것이 변수 index와 fruit로 언패킹됩니다.
다음은 enumerate()가 실제로 만들어내는 값입니다:
# enumerate의 출력 직접 보기
fruits = ["apple", "banana", "cherry"]
enumerated = list(enumerate(fruits))
print(enumerated)
# Output: [(0, 'apple'), (1, 'banana'), (2, 'cherry')]37.2.3) 다른 숫자에서 열거 시작하기
기본적으로 enumerate()는 0부터 카운트합니다. start 파라미터로 다른 시작 숫자를 지정할 수 있습니다:
# 1부터 카운트 시작하기(표시에 유용)
tasks = ["Write code", "Test code", "Deploy code"]
for number, task in enumerate(tasks, start=1):
print(f"Step {number}: {task}")
# Output:
# Step 1: Write code
# Step 2: Test code
# Step 3: Deploy code사용자에게 번호 매긴 리스트를 보여줄 때 특히 유용합니다. 사용자는 보통 1부터 시작하는 카운트를 기대하기 때문입니다:
# 번호가 있는 메뉴 옵션
menu_items = ["New Game", "Load Game", "Settings", "Quit"]
print("Main Menu:")
for number, item in enumerate(menu_items, start=1):
print(f"{number}. {item}")
# Output:
# Main Menu:
# 1. New Game
# 2. Load Game
# 3. Settings
# 4. Quit37.2.4) 문자열과 다른 이터러블에서의 enumerate()
enumerate() 함수는 리스트뿐 아니라 어떤 이터러블에서도 동작합니다:
# 문자열의 문자를 열거하기
word = "Python"
for position, letter in enumerate(word):
print(f"Letter {position}: {letter}")
# Output:
# Letter 0: P
# Letter 1: y
# Letter 2: t
# Letter 3: h
# Letter 4: o
# Letter 5: n
# 튜플 열거하기
coordinates = (10, 20, 30, 40)
for index, value in enumerate(coordinates):
print(f"Coordinate {index}: {value}")
# Output:
# Coordinate 0: 10
# Coordinate 1: 20
# Coordinate 2: 30
# Coordinate 3: 40enumerate() 함수는 코드를 더 읽기 쉽고 오류 가능성을 줄여 줍니다. 반복문에서 위치와 값이 모두 필요하다면, 수동으로 카운터를 관리하는 대신 enumerate()를 사용하세요.
37.3) zip()으로 시퀀스 결합하기
zip() 함수는 여러 이터러블을 요소 단위로 결합하여, 대응되는 요소들의 쌍(또는 튜플)을 만듭니다. 이는 서로 관련된 데이터를 별도의 시퀀스에 저장해 두고 동시에 처리해야 할 때 매우 유용합니다.
37.3.1) zip()의 동작 방식 이해하기
zip() 함수는 두 개 이상의 이터러블을 받아, 각 입력 이터러블에서 하나씩 요소를 담은 튜플들의 이터레이터(iterator)를 반환합니다:
# 두 리스트 결합하기
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
combined = list(zip(names, ages))
print(combined)
# Output: [('Alice', 25), ('Bob', 30), ('Charlie', 35)]"zip"이라는 이름은 옷의 지퍼에서 왔습니다. 서로 분리된 두 면을 요소별로 결합해 하나의 연결된 구조로 만듭니다.
다음은 zip()이 요소를 어떻게 짝지어 주는지 시각적으로 나타낸 것입니다:
37.3.2) 반복문에서 zip() 사용하기
zip()의 가장 일반적인 사용법은 for 반복문(loop)에서 여러 시퀀스를 동시에 순회해야 할 때입니다:
# 병렬 데이터 처리하기
students = ["Alice", "Bob", "Charlie", "Diana"]
scores = [92, 85, 88, 95]
for student, score in zip(students, scores):
print(f"{student} scored {score}")
# Output:
# Alice scored 92
# Bob scored 85
# Charlie scored 88
# Diana scored 95이는 인덱스를 사용하는 것보다 훨씬 깔끔합니다:
# zip() 없이 - 더 복잡하고 인덱스 오류 가능성이 큼
students = ["Alice", "Bob", "Charlie", "Diana"]
scores = [92, 85, 88, 95]
for i in range(len(students)):
print(f"{students[i]} scored {scores[i]}")
# 같은 출력이지만 코드가 더 많고 인덱스 오류 가능성이 있음37.3.3) 길이가 다른 시퀀스 처리하기
입력 시퀀스의 길이가 다르면, zip()은 가장 짧은 시퀀스가 소진되는 시점에서 멈춥니다:
# 길이가 다른 시퀀스
names = ["Alice", "Bob", "Charlie", "Diana"]
ages = [25, 30] # 나이 2개뿐
for name, age in zip(names, ages):
print(f"{name} is {age} years old")
# Output:
# Alice is 25 years old
# Bob is 30 years old
# (Charlie and Diana are not processed)이 동작은 오류를 막아주지만, 주의하지 않으면 조용한 데이터 손실로 이어질 수 있습니다. 시퀀스 길이가 기대한 대로인지 항상 확인하세요:
# 길이 불일치 확인하기
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30]
if len(names) != len(ages):
print(f"Warning: {len(names)} names but {len(ages)} ages")
print("Only processing the first", min(len(names), len(ages)), "entries")
# Output: Warning: 3 names but 2 ages
# Output: Only processing the first 2 entries
# zip()으로 계속 - 가장 짧은 곳에서 멈춤
for name, age in zip(names, ages):
print(f"{name} is {age} years old")37.3.4) 두 개보다 많은 시퀀스 zip()하기
zip() 함수는 임의의 개수의 이터러블을 결합할 수 있습니다:
# 세 시퀀스 결합하기
products = ["Laptop", "Mouse", "Keyboard"]
prices = [999.99, 24.99, 79.99]
quantities = [5, 20, 15]
print("Inventory Report:")
for product, price, quantity in zip(products, prices, quantities):
total_value = price * quantity
print(f"{product}: ${price} × {quantity} = ${total_value:.2f}")
# Output:
# Inventory Report:
# Laptop: $999.99 × 5 = $4999.95
# Mouse: $24.99 × 20 = $499.80
# Keyboard: $79.99 × 15 = $1199.8537.3.5) zip()으로 딕셔너리 만들기
강력한 패턴 중 하나는 zip()을 사용해 키 시퀀스와 값 시퀀스로부터 딕셔너리를 만드는 것입니다:
# 두 리스트로 딕셔너리 만들기
keys = ["name", "age", "city"]
values = ["Alice", 25, "Boston"]
person = dict(zip(keys, values))
print(person)
# Output: {'name': 'Alice', 'age': 25, 'city': 'Boston'}37.4) any()와 all()로 불리언 집계하기
any()와 all() 함수는 전체 이터러블에 걸쳐 조건을 테스트하여 단일 불리언(boolean) 결과를 반환합니다. 여러 조건을 기반으로 한 검증과 의사 결정을 위한 강력한 도구입니다.
37.4.1) any() 이해하기: 하나라도 True이면 True
any() 함수는 이터러블 안에 truthy(True로 평가되는) 요소가 하나라도 있으면 True를 반환합니다. 모든 요소가 falsy면 False를 반환합니다:
# any() 기본 예시
print(any([True, False, False])) # Output: True (at least one True)
print(any([False, False, False])) # Output: False (all False)
print(any([False, True, True])) # Output: True (multiple True values)
# 빈 이터러블
print(any([])) # Output: False (no elements to be True)any() 함수는 Python의 truthiness 규칙(7장에서 배운 내용)을 사용합니다. 0이 아닌 숫자, 비어 있지 않은 문자열, 비어 있지 않은 컬렉션은 truthy입니다:
# 다양한 truthy/falsy 값에서의 any()
print(any([0, 0, 1])) # Output: True (1 is truthy)
print(any([0, 0, 0])) # Output: False (all zeros are falsy)
print(any(["", "", "text"])) # Output: True ("text" is truthy)
print(any(["", "", ""])) # Output: False (empty strings are falsy)37.4.2) any()의 실용적인 사용법
예시: 하나라도 조건을 만족하는지 확인하기
# 낙제 점수(60 미만)가 하나라도 있는지 확인하기
scores = [75, 82, 55, 90, 88]
has_failing_grade = any(score < 60 for score in scores)
if has_failing_grade:
print("Warning: At least one failing grade")
# Output: Warning: At least one failing grade
else:
print("All grades are passing")37.4.3) all() 이해하기: 모두 True일 때만 True
all() 함수는 이터러블의 모든 요소가 truthy일 때만 True를 반환합니다. 요소 중 하나라도 falsy이면 False를 반환합니다:
# all() 기본 예시
print(all([True, True, True])) # Output: True (all True)
print(all([True, False, True])) # Output: False (one False)
print(all([True, True, False])) # Output: False (one False)
# 빈 이터러블
print(all([])) # Output: True (vacuous truth - no False elements)빈 이터러블에서의 동작은 다소 놀라울 수 있습니다: all([])은 True를 반환합니다. 이를 공허한 참(vacuous truth)이라고 합니다. “모든 요소가 True다”라는 문장은, 반박할 요소가 하나도 없을 때 기술적으로 참이기 때문입니다.
# 다양한 truthy/falsy 값에서의 all()
print(all([1, 2, 3])) # Output: True (all non-zero)
print(all([1, 0, 3])) # Output: False (0 is falsy)
print(all(["a", "b", "c"])) # Output: True (all non-empty)
print(all(["a", "", "c"])) # Output: False (empty string is falsy)37.4.4) all()의 실용적인 사용법
예시: 모든 조건이 만족되는지 검증하기
# 모든 점수가 합격(60 이상)인지 확인하기
scores = [75, 82, 68, 90, 88]
all_passing = all(score >= 60 for score in scores)
if all_passing:
print("Congratulations! All grades are passing")
# Output: Congratulations! All grades are passing
else:
print("Some grades need improvement")37.4.5) any()와 all()의 단락 평가(Short-Circuit Evaluation)
any()와 all()은 둘 다 단락 평가(short-circuit evaluation)(9장에서 배운 내용)를 사용합니다. 결과가 결정되는 즉시 검사를 중단합니다:
# 호출될 때 출력하는 함수 (실행을 보여주기 위해)
def is_positive(n):
print(f"Checking {n}")
return n > 0
# any()는 첫 True에서 멈춤
print("Testing any():")
numbers = [0, 0, 1, 2, 3]
result = any(is_positive(n) for n in numbers)
# Output:
# Testing any():
# Checking 0
# Checking 0
# Checking 1
# (여기서 멈춤 - 2나 3은 확인 안 함)
print(f"Result: {result}") # Output: Result: True
print("\nTesting all():")
numbers = [1, 2, 0, 3, 4]
result = all(is_positive(n) for n in numbers)
# Output:
# Testing all():
# Checking 1
# Checking 2
# Checking 0
# (여기서 멈춤 - 3이나 4는 확인 안 함)
print(f"Result: {result}") # Output: Result: False이 덕분에 any()와 all()은 효율적입니다. 결과가 결정된 뒤에는 요소를 확인하는 데 시간을 낭비하지 않습니다.
37.5) sorted()와 사용자 정의 키로 정렬하기
sorted() 함수는 어떤 이터러블이든 정렬된 새 리스트를 만듭니다. .sort() 메서드(리스트에서만 동작하며 제자리에서 수정함)와 달리, sorted()는 어떤 이터러블에서도 동작하고 항상 새 리스트를 반환합니다.
37.5.1) sorted()로 기본 정렬하기
sorted() 함수는 기본적으로 요소를 오름차순으로 정렬합니다:
# 숫자 정렬하기
numbers = [42, 17, 93, 8, 55]
sorted_numbers = sorted(numbers)
print(sorted_numbers) # Output: [8, 17, 42, 55, 93]
# 원본 리스트는 변하지 않음
print(numbers) # Output: [42, 17, 93, 8, 55]
# 문자열 정렬하기(알파벳 순)
names = ["Charlie", "Alice", "Bob", "Diana"]
sorted_names = sorted(names)
print(sorted_names) # Output: ['Alice', 'Bob', 'Charlie', 'Diana']sorted() 함수는 리스트뿐 아니라 어떤 이터러블에서도 동작합니다:
# 튜플 정렬하기(리스트를 반환)
coordinates = (5, 2, 8, 1, 9)
sorted_coords = sorted(coordinates)
print(sorted_coords) # Output: [1, 2, 5, 8, 9]
# 문자열 정렬하기(문자 리스트 반환)
word = "python"
sorted_letters = sorted(word)
print(sorted_letters) # Output: ['h', 'n', 'o', 'p', 't', 'y']
# 셋 정렬하기(정렬된 리스트 반환)
unique_numbers = {5, 8, 2, 1}
sorted_unique = sorted(unique_numbers)
print(sorted_unique) # Output: [1, 2, 5, 8]37.5.2) 역순 정렬
reverse=True 파라미터를 사용하면 내림차순으로 정렬합니다:
# 숫자를 내림차순으로 정렬
scores = [85, 92, 78, 95, 88]
highest_first = sorted(scores, reverse=True)
print(highest_first) # Output: [95, 92, 88, 85, 78]
# 문자열을 내림차순으로 정렬(역알파벳 순)
names = ["Charlie", "Alice", "Bob", "Diana"]
reverse_alpha = sorted(names, reverse=True)
print(reverse_alpha) # Output: ['Diana', 'Charlie', 'Bob', 'Alice']37.5.3) key 매개변수 이해하기
key 매개변수는 sorted()를 진정으로 강력하게 만드는 부분입니다. Python이 정렬 중에 요소를 비교하는 방식을 변환합니다.
key 매개변수란 무엇인가?
key 매개변수는 함수를 받습니다. Python은 각 요소에 대해 이 함수를 호출하여 "비교 키"를 추출한 다음, 원래 요소 대신 이러한 키를 기반으로 정렬합니다.
단계별 작동 방식:
- Python이 각 요소에 대해 키 함수를 호출합니다
- Python이 모든 키를 수집합니다
- Python이 이러한 키를 비교하여 정렬합니다
- Python이 새로운 순서로 원래 요소를 반환합니다
# 예제: 길이로 정렬
words = ["python", "is", "awesome"]
# 단계 1: Python이 각 단어에 len() 호출
# len("python") → 6
# len("is") → 2
# len("awesome") → 7
# 단계 2: Python이 이러한 키를 가짐: [6, 2, 7]
# 단계 3: Python이 키를 정렬: [2, 6, 7]
# 단계 4: Python이 해당 순서로 단어 반환: ["is", "python", "awesome"]
result = sorted(words, key=len)
print(result) # Output: ['is', 'python', 'awesome']키 함수 시각화:
키 함수가 될 수 있는 것은?
키 함수는 다음을 만족해야 합니다:
- 하나의 인자를 받음 (정렬되는 요소)
- Python이 비교할 수 있는 값을 반환 (숫자, 문자열, 튜플 등)
# 내장 함수가 잘 작동함
sorted(numbers, key=abs) # 절댓값으로 정렬
sorted(words, key=len) # 길이로 정렬
sorted(names, key=str.lower) # 대소문자 구분 없이 정렬
# 자신의 함수
def first_letter(word):
return word[0]
sorted(words, key=first_letter) # 첫 글자로 정렬
# Lambda 함수 (Chapter 23)
sorted(words, key=lambda w: w[-1]) # 마지막 글자로 정렬중요: 키 함수는 요소당 한 번 호출됨
# 키 함수가 언제 호출되는지 시연
def show_key(word):
print(f"Getting key for: {word}")
return len(word)
words = ["cat", "elephant", "dog"]
result = sorted(words, key=show_key)
# Output:
# Getting key for: cat
# Getting key for: elephant
# Getting key for: dog
print(result) # Output: ['cat', 'dog', 'elephant']중요: 키 함수는 요소당 한 번 호출됨
show_key가 각 단어에 대해 정확히 한 번 호출되며, 비교 중에 반복적으로 호출되지 않는다는 점에 주목하세요. Python은 효율적입니다—먼저 모든 키를 추출하고 캐시한 다음, 캐시된 키를 사용하여 정렬합니다.
key를 "어떤 측면을 비교해야 하나?"에 대한 답으로 생각하세요:
key=len→ "길이로 비교"key=abs→ "절댓값으로 비교"key=str.lower→ "모두 소문자인 것처럼 비교"key=lambda x: x[1]→ "두 번째 요소로 비교"
key 매개변수는 요소의 모든 속성으로 정렬할 수 있게 하여, sorted()를 매우 다재다능하게 만듭니다.
37.5.4) 내장 함수를 키로 사용해 정렬하기
Python의 내장 함수는 훌륭한 key 함수가 됩니다:
# 절댓값 기준으로 정렬하기
numbers = [-5, 2, -8, 1, -3, 7]
sorted_by_magnitude = sorted(numbers, key=abs)
print(sorted_by_magnitude) # Output: [1, 2, -3, -5, 7, -8]
# 대소문자 무시하고 문자열 정렬하기
names = ["alice", "Bob", "CHARLIE", "diana"]
sorted_case_insensitive = sorted(names, key=str.lower)
print(sorted_case_insensitive) # Output: ['alice', 'Bob', 'CHARLIE', 'diana']37.5.5) 복잡한 데이터 구조 정렬하기
튜플 리스트나 리스트의 리스트를 정렬할 때는 인덱싱을 사용해 어떤 요소를 기준으로 정렬할지 지정할 수 있습니다:
# 두 번째 요소 기준으로 튜플 정렬하기
students = [
("Alice", 92),
("Bob", 85),
("Charlie", 88),
("Diana", 95)
]
# 점수(두 번째 요소) 기준으로 정렬
by_score = sorted(students, key=lambda student: student[1])
print(by_score)
# Output: [('Bob', 85), ('Charlie', 88), ('Alice', 92), ('Diana', 95)]
# 점수 내림차순 정렬
by_score_desc = sorted(students, key=lambda student: student[1], reverse=True)
print(by_score_desc)
# Output: [('Diana', 95), ('Alice', 92), ('Charlie', 88), ('Bob', 85)]참고: 여기서는 lambda를 사용했습니다(23장에서 배운 내용). lambda는 작은 익명 함수입니다. 표현식 lambda student: student[1]는 학생 튜플을 받아 두 번째 요소(점수)를 반환하는 함수를 만듭니다.
37.5.6) 다단계 정렬(Multi-Level Sorting)
키 함수에서 튜플을 반환하여 여러 기준으로 정렬할 수 있습니다. Python은 튜플을 왼쪽에서 오른쪽으로 요소별로 비교합니다:
튜플 비교 작동 방식:
Python이 두 튜플을 비교할 때 다음 규칙을 따릅니다:
- 첫 번째 요소를 비교합니다. 다르면 비교가 완료됩니다.
- 첫 번째 요소가 같으면 두 번째 요소를 비교합니다.
- 두 번째 요소가 같으면 세 번째 요소를 비교합니다.
- 차이를 찾거나 요소가 다 떨어질 때까지 계속합니다.
# 튜플 비교 예제
print((1, 'a') < (2, 'z')) # Output: True (1 < 2이므로 즉시 True)
print((1, 'z') < (1, 'a')) # Output: False (1 == 1이므로 'z' < 'a' 비교)
print((1, 'a') < (1, 'a')) # Output: False (두 튜플이 같음)
print((1, 2, 9) < (1, 3, 1)) # Output: True (1 == 1, 그 다음 2 < 3)이것이 튜플을 다단계 정렬에 완벽하게 만듭니다—Python이 "첫 번째 기준 비교, 그 다음 두 번째, 그 다음 세 번째" 로직을 자동으로 처리합니다:
# 여러 기준으로 정렬하기
students = [
("Alice", "Smith", 92),
("Bob", "Jones", 85),
("Alice", "Brown", 88),
("Charlie", "Smith", 85)
]
# 이름(First name) 기준, 다음으로 성(Last name) 기준
by_name = sorted(students, key=lambda s: (s[0], s[1]))
print("By name:")
for student in by_name:
print(f" {student}")
# Output:
# By name:
# ('Alice', 'Brown', 88)
# ('Alice', 'Smith', 92)
# ('Bob', 'Jones', 85)
# ('Charlie', 'Smith', 85)
# 점수 내림차순, 그 다음 이름 오름차순으로 정렬
by_score_then_name = sorted(students, key=lambda s: (-s[2], s[0]))
print("\nBy score (high to low), then name:")
for student in by_score_then_name:
print(f" {student}")
# Output:
# By score (high to low), then name:
# ('Alice', 'Smith', 92)
# ('Alice', 'Brown', 88)
# ('Bob', 'Jones', 85)
# ('Charlie', 'Smith', 85)참고: 한 기준은 내림차순, 다른 기준은 오름차순으로 정렬하려면 숫자 값을 음수로 만듭니다(-s[2]). 음수로 만들면 숫자의 정렬 순서가 반대가 되기 때문입니다. 위 예제에서 -s[2]는 점수를 높은 것부터 낮은 것으로 정렬하고, s[0]는 이름을 A부터 Z로 정렬합니다.
37.5.7) 복잡한 키를 위한 도우미 함수 사용하기
정렬 로직이 복잡해지면, 도우미 함수를 정의하는 것이 코드를 더 읽기 쉽고 유지 관리하기 쉽게 만듭니다. 그런 다음 키 함수 내에서 이 도우미 함수를 사용할 수 있습니다.
예제: 확장자로 파일 정렬하기
파일을 확장자(.csv, .jpg, .pdf 등)로 그룹화하고, 각 그룹 내에서 파일 이름을 알파벳순으로 정렬하고 싶다고 가정해봅시다. 키 함수는 파일 확장자를 추출해야 하는데, 이는 문자열 조작이 필요합니다.
# 확장자, 그 다음 이름으로 파일 정렬
files = [
"report.pdf",
"data.csv",
"image.jpg",
"notes.txt",
"backup.csv",
"photo.jpg"
]
# 정렬을 위해 확장자 추출
def get_extension(filename):
"""파일 이름에서 파일 확장자를 추출합니다."""
return filename.split(".")[-1] # "."로 분할하고 마지막 부분 가져오기
# 키에서 도우미 함수 사용
sorted_files = sorted(files, key=lambda f: (get_extension(f), f))
print("Files sorted by extension, then name:")
for file in sorted_files:
print(f" {file}")
# Output:
# Files sorted by extension, then name:
# backup.csv # csv 파일이 먼저 (알파벳순)
# data.csv # csv 파일이 먼저 (알파벳순)
# image.jpg # jpg 파일이 다음
# photo.jpg # jpg 파일이 다음
# report.pdf # pdf 파일이 다음
# notes.txt # txt 파일이 마지막작동 방식:
- 키 함수
lambda f: (get_extension(f), f)는 각 파일 이름에 대해 튜플을 반환합니다 - "report.pdf"의 경우
("pdf", "report.pdf")를 반환합니다 - "data.csv"의 경우
("csv", "data.csv")를 반환합니다 - Python은 튜플의 첫 번째 요소(확장자)로 정렬한 다음 두 번째 요소(전체 파일 이름)로 정렬합니다
- 이렇게 하면 파일이 확장자별로 그룹화되고 각 그룹 내에서 알파벳순으로 정렬됩니다
왜 도우미 함수를 사용하나요?
가독성을 비교해봅시다:
# 도우미 함수 없이 - 이해하기 어려움
sorted_files = sorted(files, key=lambda f: (f.split(".")[-1], f))
# 도우미 함수 사용 - 의도가 더 명확함
sorted_files = sorted(files, key=lambda f: (get_extension(f), f))도우미 함수는 코드를 자체 문서화합니다. get_extension(f)를 읽는 사람은 즉시 무슨 일이 일어나고 있는지 이해하는 반면, f.split(".")[-1]은 머릿속으로 해석이 필요합니다.
37.5.8) sorted() vs .sort(): 각각 언제 사용할까
Python에는 정렬을 위한 두 가지 방법이 있습니다:
sorted()- 새로 정렬된 리스트를 반환하는 함수.sort()- 제자리에서 정렬하는 리스트 메서드
# sorted() - 새 리스트 생성, 원본은 그대로
numbers = [3, 1, 4, 1, 5]
sorted_numbers = sorted(numbers)
print(f"Original: {numbers}") # Output: Original: [3, 1, 4, 1, 5]
print(f"Sorted: {sorted_numbers}") # Output: Sorted: [1, 1, 3, 4, 5]
# .sort() - 리스트를 제자리에서 수정, None 반환
numbers = [3, 1, 4, 1, 5]
result = numbers.sort()
print(f"Modified: {numbers}") # Output: Modified: [1, 1, 3, 4, 5]
print(f"Return value: {result}") # Output: Return value: Nonesorted()를 사용할 때:
- 원래 순서를 유지해야 할 때
- 리스트가 아닌 것을 정렬할 때(튜플, 문자열, 셋 등)
- 한 표현식에서 정렬하고 대입하고 싶을 때
.sort()를 사용할 때:
- 리스트가 있고 원래 순서가 필요 없을 때
- 메모리를 절약하고 싶을 때(새 리스트를 만들지 않음)
- 큰 리스트를 효율적으로 제자리에서 정렬할 때
sorted() 함수는 Python에서 가장 다재다능한 도구 중 하나입니다. key 파라미터와 결합하면, 단순한 숫자 정렬부터 중첩 데이터 구조의 복잡한 다중 기준 정렬까지 거의 모든 정렬 요구를 처리할 수 있습니다.
이 장에서는 Python의 필수 내장 함수와 도구를 익혔습니다. 다음을 배웠습니다:
- Python의 타입 계층을 이해하고, 어떤 연산이 어떤 타입에서 동작하는지 예측하기
len(),sum(),min(),max(),abs(),round()같은 기본 함수를 일반적인 작업에 사용하기enumerate()를 사용해 위치 정보를 포함하여 반복하기zip()으로 병렬 시퀀스를 동시에 처리하기any()와all()로 컬렉션 전반에 걸쳐 의사결정하기sorted()와 사용자 정의 key 함수로 유연하게 데이터 정렬하기
이 도구들은 관용적(idiomatic) Python 코드의 기반을 이룹니다. 효율적이고 읽기 쉬우며, 엣지 케이스를 올바르게 처리합니다. 프로그래밍을 계속하다 보면 이러한 함수들을 끊임없이 사용하게 될 것입니다. 이들은 Python 코드를 우아하고 표현력 있게 만드는 빌딩 블록입니다.
다음 장에서는 데코레이터(decorator)를 살펴봅니다. 데코레이터는 23장에서 배운 일급 함수(first-class function) 개념을 바탕으로, 강력한 방식으로 함수의 동작을 수정하고 확장할 수 있게 해줍니다.