5.3 일급 함수와 람다 — 함수를 값으로 다루기
Python에서 함수는 일급 객체(First-class object) 입니다. 이는 함수를 변수에 할당하고, 인수로 전달하며, 반환값으로 돌려줄 수 있다는 의미입니다. 이 특성은 함수형 프로그래밍 패턴의 핵심입니다.
일급 함수(First-class Function)
# 1. 함수를 변수에 할당
def greet(name: str) -> str:
return f"안녕하세요, {name}님!"
hello = greet # 함수 객체를 변수에 할당 (호출 아님!)
print(hello("Alice")) # 안녕하세요, Alice님!
print(hello is greet) # True — 같은 함수 객체
# 2. 함수를 인수로 전달
def apply(func, value):
return func(value)
def double(x: int) -> int:
return x * 2
def square(x: int) -> int:
return x ** 2
print(apply(double, 5)) # 10
print(apply(square, 5)) # 25
print(apply(str, 42)) # "42" — 내장 함수도 전달 가능
# 3. 함수를 반환값으로 사용
def make_adder(n: int):
def adder(x: int) -> int:
return x + n
return adder # 함수를 반환
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3)) # 8
print(add10(3)) # 13
print(type(add5)) # <class 'function'>
# 4. 자료구조에 함수 저장
operations = {
"add": lambda x, y: x + y,
"sub": lambda x, y: x - y,
"mul": lambda x, y: x * y,
"div": lambda x, y: x / y if y != 0 else None,
}
for op_name, op_func in operations.items():
result = op_func(10, 3)
print(f" {op_name}(10, 3) = {result}")
lambda 문법
# 기본 문법: lambda 매개변수: 표현식
# def 함수와 달리 단일 표현식만 가능, return 키워드 없음
# 일반 함수
def multiply(x, y):
return x * y
# 동일한 람다
multiply_lambda = lambda x, y: x * y
print(multiply(3, 4)) # 12
print(multiply_lambda(3, 4)) # 12
# 람다 기본값
power = lambda base, exp=2: base ** exp
print(power(3)) # 9 (기본 exp=2)
print(power(3, 3)) # 27
# 람다의 제약
# 1. 단일 표현식만 가능 (if문, for루프, 여러 줄 불가)
# 2. 복잡한 로직은 def 사용 권장
# 3. docstring 작성 불가
# 조건 표현식은 가능
classify = lambda x: "양수" if x > 0 else ("음수" if x < 0 else "영")
print(classify(5)) # 양수
print(classify(-3)) # 음수
print(classify(0)) # 영
sorted()와 lambda
# key 인수에 lambda 전달
students = [
{"name": "Charlie", "score": 85, "age": 22},
{"name": "Alice", "score": 92, "age": 20},
{"name": "Bob", "score": 78, "age": 25},
{"name": "Diana", "score": 92, "age": 21},
]
# 점수 기준 내림차순 정렬
by_score = sorted(students, key=lambda s: s["score"], reverse=True)
for s in by_score:
print(f" {s['name']}: {s['score']}점")
# 복합 정렬: 점수 내림차순, 동점이면 이름 오름차순
by_score_name = sorted(
students,
key=lambda s: (-s["score"], s["name"])
)
for s in by_score_name:
print(f" {s['name']}: {s['score']}점")
# 문자열 정렬
words = ["banana", "Apple", "cherry", "Date"]
# 대소문자 무시 정렬
case_insensitive = sorted(words, key=lambda w: w.lower())
print(case_insensitive) # ['Apple', 'banana', 'cherry', 'Date']
# 길이 기준 정렬
by_length = sorted(words, key=len)
print(by_length) # ['Date', 'Apple', 'banana', 'cherry']
map(): 변환 적용
# map(function, iterable) — 각 원소에 함수 적용, 이터레이터 반환
numbers = [1, 2, 3, 4, 5]
# lambda로 변환
squared = list(map(lambda x: x**2, numbers))
print(squared) # [1, 4, 9, 16, 25]
# 내장 함수 사용
string_nums = ["1", "2", "3", "4", "5"]
integers = list(map(int, string_nums))
print(integers) # [1, 2, 3, 4, 5]
# 여러 이터러블 처리
a = [1, 2, 3]
b = [10, 20, 30]
sums = list(map(lambda x, y: x + y, a, b))
print(sums) # [11, 22, 33]
# map vs 컴프리헨션
# 대부분의 경우 컴프리헨션이 더 읽기 쉬움
result1 = list(map(lambda x: x**2, range(10)))
result2 = [x**2 for x in range(10)]
print(result1 == result2) # True
# 하지만 기존 함수 적용 시 map이 간결
names = ["alice", "bob", "charlie"]
capitalized = list(map(str.capitalize, names))
print(capitalized) # ['Alice', 'Bob', 'Charlie']
filter(): 조건 필터링
# filter(function, iterable) — 조건이 True인 원소만 반환
numbers = [-5, -3, -1, 0, 2, 4, 6]
positives = list(filter(lambda x: x > 0, numbers))
print(positives) # [2, 4, 6]
# None을 전달하면 falsy 값 제거
mixed = [0, 1, "", "hello", None, False, True, [], [1, 2]]
truthy = list(filter(None, mixed))
print(truthy) # [1, 'hello', True, [1, 2]]
# filter vs 컴프리헨션
evens1 = list(filter(lambda x: x % 2 == 0, range(20)))
evens2 = [x for x in range(20) if x % 2 == 0]
print(evens1 == evens2) # True
operator 모듈: 람다 대안
from operator import itemgetter, attrgetter, methodcaller, add, mul
from functools import reduce
# itemgetter: 딕셔너리/시퀀스 원소 접근
students = [
{"name": "Charlie", "score": 85},
{"name": "Alice", "score": 92},
{"name": "Bob", "score": 78},
]
# lambda 버전
by_score_lambda = sorted(students, key=lambda s: s["score"])
# operator 버전 (더 빠름)
by_score_op = sorted(students, key=itemgetter("score"))
print([s["name"] for s in by_score_op]) # ['Bob', 'Charlie', 'Alice']
# 다중 키 정렬
by_score_name = sorted(students, key=itemgetter("score", "name"))
# attrgetter: 객체 속성 접근
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
category: str
products = [
Product("Laptop", 1500000, "electronics"),
Product("Phone", 800000, "electronics"),
Product("Shirt", 30000, "clothing"),
]
# 가격순 정렬
by_price = sorted(products, key=attrgetter("price"))
print([p.name for p in by_price]) # ['Shirt', 'Phone', 'Laptop']
# 카테고리 → 가격 순 정렬
by_cat_price = sorted(products, key=attrgetter("category", "price"))
# methodcaller: 메서드 호출
words = ["hello", "WORLD", "Python"]
# 소문자로 변환
lowered = list(map(methodcaller("lower"), words))
print(lowered) # ['hello', 'world', 'python']
# 인수 있는 메서드
# methodcaller("replace", "l", "r")("hello") → "herro"
replaced = list(map(methodcaller("replace", "l", "r"), words[:2]))
print(replaced) # ['herro', 'WORLD']
# 산술 연산자
numbers = [1, 2, 3, 4, 5]
total = reduce(add, numbers) # sum(numbers)와 동일
product = reduce(mul, numbers) # 1*2*3*4*5 = 120
print(total, product)
functools.reduce()
from functools import reduce
from operator import add, mul
# reduce(func, iterable[, initial]) — 누적 연산
numbers = [1, 2, 3, 4, 5]
# 합계 (sum()과 동일)
total = reduce(add, numbers)
print(total) # 15
# 곱 (내장 함수 없음)
product = reduce(mul, numbers)
print(product) # 120
# 초기값 지정
total_with_init = reduce(add, numbers, 100)
print(total_with_init) # 115
# 커스텀 연산: 최댓값 (max()와 동일)
maximum = reduce(lambda a, b: a if a > b else b, numbers)
print(maximum) # 5
# 딕셔너리 병합
dicts = [{"a": 1}, {"b": 2}, {"c": 3}]
merged = reduce(lambda acc, d: {**acc, **d}, dicts)
print(merged) # {'a': 1, 'b': 2, 'c': 3}
# 빈 이터러블 처리: 초기값 없으면 TypeError
try:
reduce(add, [])
except TypeError as e:
print(f"오류: {e}") # reduce() of empty iterable with no initial value
result = reduce(add, [], 0) # 초기값 0
print(result) # 0
함수를 반환하는 함수 (팩토리 패턴)
# 함수 팩토리: 설정에 따라 다른 동작의 함수 생성
def make_validator(min_val: float, max_val: float, name: str = "값"):
"""범위 검사 함수를 생성"""
def validate(x: float) -> float:
if not min_val <= x <= max_val:
raise ValueError(f"{name}은 {min_val}~{max_val} 범위여야 합니다 (현재: {x})")
return x
validate.__doc__ = f"{name} 범위 검사: [{min_val}, {max_val}]"
return validate
validate_age = make_validator(0, 150, "나이")
validate_score = make_validator(0, 100, "점수")
validate_temp = make_validator(-273.15, 1000, "온도")
print(validate_age(25)) # 25
print(validate_score(87)) # 87
try:
validate_age(200)
except ValueError as e:
print(f"검증 실패: {e}")
# 포매터 팩토리
def make_formatter(prefix: str = "", suffix: str = "", width: int = 0):
def format_value(value) -> str:
text = f"{prefix}{value}{suffix}"
return text.rjust(width) if width else text
return format_value
currency = make_formatter(prefix="₩", suffix="원", width=15)
percentage = make_formatter(suffix="%", width=8)
for val in [1000, 50000, 1234567]:
print(currency(f"{val:,}"))
for pct in [10, 85.5, 100]:
print(percentage(pct))
실전 예제: 정렬 키 함수 조합
from typing import Callable, TypeVar
T = TypeVar("T")
def compose(*funcs: Callable) -> Callable:
"""여러 함수를 합성 (오른쪽에서 왼쪽으로 적용)"""
def composed(x):
result = x
for f in reversed(funcs):
result = f(result)
return result
return composed
def negate(func: Callable[[T], float]) -> Callable[[T], float]:
"""정렬 키 함수를 역순으로 만듦 (내림차순)"""
return lambda x: -func(x)
# 정렬 파이프라인
data = [
{"name": "Alice", "score": 92, "grade": "A"},
{"name": "Bob", "score": 78, "grade": "C"},
{"name": "Charlie", "score": 92, "grade": "A"},
{"name": "Diana", "score": 85, "grade": "B"},
]
# 점수 내림차순, 이름 오름차순
result = sorted(data, key=lambda x: (-x["score"], x["name"]))
for d in result:
print(f" {d['name']}: {d['score']}점")
고수 팁
1. lambda 대신 partial로 부분 적용
from functools import partial
def power(base: float, exponent: float) -> float:
return base ** exponent
# 람다 방식
square_lambda = lambda x: power(x, 2)
cube_lambda = lambda x: power(x, 3)
# partial 방식 (더 명확, 도구 지원 좋음)
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
print(square(5)) # 25.0
print(cube(5)) # 125.0
# partial의 장점: __name__ 등 메타데이터 보존
print(square.__doc__) # power의 docstring 유지 (lambda는 없음)
2. 콜백 패턴
from typing import Callable
def process_with_callback(
data: list[int],
on_success: Callable[[list[int]], None],
on_error: Callable[[Exception], None] | None = None
) -> None:
try:
result = [x * 2 for x in data]
on_success(result)
except Exception as e:
if on_error:
on_error(e)
else:
raise
process_with_callback(
[1, 2, 3],
on_success=lambda result: print(f"성공: {result}"),
on_error=lambda e: print(f"오류: {e}")
)
# 성공: [2, 4, 6]