고급 함수 기법 (고차 함수, 람다, 클로저, 데코레이터, 제너레이터) 총정리
앞서 파이썬 중급 과정의 파일 입출력과 예외 처리 기법을 정복했습니다. 이번 글에서는 함수를 단순한 코드 블록이 아닌 일급 객체(First-Class Citizen)로 다루어 코드의 유연성과 재사용성을 극대화하는 고급 함수 기법의 모든 것을 파헤쳐 보겠습니다.
1. 일급 함수 (First-Class Functions)의 이해
파이썬에서 함수는 일급 객체(First-Class Citizen)로 취급됩니다. 이는 함수가 일반적인 변수, 정수, 문자열 등과 완전히 동등하게 다루어짐을 의미합니다.
-
변수에 할당 가능: 함수 자체를 다른 변수에 담을 수 있습니다.
-
인자로 전달 가능: 함수를 다른 함수의 입력값(인자)으로 넘길 수 있습니다.
-
반환값으로 사용 가능: 함수가 실행된 결과로 또 다른 함수를 리턴할 수 있습니다.
-
자료구조 저장 가능: 리스트나 딕셔너리 내부에 함수를 저장해 관리할 수 있습니다.
def greet(name):
return f"Hello, {name}!"
# 함수를 변수에 할당하여 호출
say_hello = greet
print(say_hello("Alice")) # 출력: Hello, Alice!
2. 고차 함수 (Higher-Order Functions)와 내장 함수
고차 함수란 함수를 인자로 받거나 함수를 반환하는 함수를 의미합니다. 파이썬은 데이터 처리를 위해 강력한 내장 고차 함수들을 제공합니다.
2.1 대표 내장 고차 함수
-
map(func, iterable): 반복 가능한 객체의 각 요소에 지정된 함수를 적용하여 매핑합니다. -
filter(func, iterable): 함수의 조건 결과가 참(True)인 요소들만 골라냅니다. -
reduce(func, iterable):functools모듈에 있으며, 요소들을 누적하여 단일 값으로 축소합니다. -
sorted(iterable, key=func): 정렬 기준을key인자에 함수 형태로 전달하여 정렬합니다.
from functools import reduce
numbers = [1, 2, 3, 4, 5]
# 1. map: 각 요소를 제곱
squared = list(map(lambda x: x * x, numbers)) # [1, 4, 9, 16, 25]
# 2. filter: 짝수만 추출
evens = list(filter(lambda x: x % 2 == 0, numbers)) # [2, 4]
# 3. reduce: 모든 요소의 곱 계산
product = reduce(lambda x, y: x * y, numbers) # 120
3. 간결한 익명 함수: 람다(Lambda) 표현식
람다 함수는 이름이 없는(Anonymous) 일회성 함수로, lambda 키워드를 사용해 한 줄로 간결하게 정의합니다.
# 기본 문법: lambda 매개변수: 표현식
add = lambda x, y: x + y
print(add(3, 5)) # 출력: 8
# 정렬 기준 커스텀에 활용
points = [(2, 3), (1, 1), (4, 2)]
points_sorted = sorted(points, key=lambda p: p[1]) # y 좌표 기준 정렬
4. 상태를 기억하는 클로저 (Closures)
클로저는 내부 함수가 자신을 둘러싼 외부 함수의 지역 변수를, 외부 함수의 실행이 끝난 뒤에도 계속해서 기억하고 접근할 수 있는 현상(또는 그러한 함수)을 말합니다.
def make_adder(n):
def adder(x):
return x + n # 외부 함수의 변수 n을 기억
return adder
add_5 = make_adder(5)
print(add_5(10)) # 출력: 15 (외부 함수가 종료되었어도 n=5를 기억함)
5. 함수의 마법사: 데코레이터(Decorators)와 @wraps
데코레이터는 기존 함수나 클래스의 코드를 직접 수정하지 않고, 그 전후에 새로운 기능을 추가하거나 래핑할 수 있게 해주는 파이썬의 강력한 구문 설탕(Syntactic Sugar)입니다.
5.1 기본 구현 구조와 @ 문법
대부분의 데코레이터는 함수를 인자로 받아 내부 래퍼(wrapper) 함수를 반환하는 구조를 가집니다. 가변 인자 *args, **kwargs를 사용하여 어떤 형태의 함수 인자도 수용할 수 있어야 합니다.
from functools import wraps
def log_decorator(func):
@wraps(func) # 원래 함수의 메타데이터(__name__, __doc__ 등)를 보존
def wrapper(*args, **kwargs):
print(f"--- '{func.__name__}' 함수 실행 시작 ---")
result = func(*args, **kwargs)
print(f"--- '{func.__name__}' 함수 실행 종료 ---")
return result
return wrapper
@log_decorator
def add(a, b):
"""두 수를 더합니다."""
return a + b
add(3, 4)
주요 실무 활용 용도
-
실행 시간 측정: 성능 최적화를 위한 타이머 기능 구현
-
로깅(Logging): 함수 호출 내역 및 인자 기록
-
캐싱(Memoization):
@functools.lru_cache를 통한 중복 연산 방지 -
권한 검사(Authorization): 특정 유저 권한 확인 후 로직 분기
6. 재귀 함수와 메모이제이션(Memoization)
재귀 함수는 함수가 자기 자신을 다시 호출하여 문제를 분할하고 해결하는 기법입니다. 단, 무한 호출을 막기 위한 기본 단계(Base Case)가 반드시 필요합니다.
def factorial(n):
if n <= 1: # 기본 단계
return 1
return n * factorial(n - 1) # 재귀 단계
메모이제이션을 통한 성능 최적화
재귀 함수는 중복 연산이 많아질 경우 심각한 성능 저하나 스택 오버플로우(RecursionError)를 유발할 수 있습니다. 이 때 파이썬의 @lru_cache 데코레이터를 적용하면 계산된 결과를 캐시에 저장해 두어 지수 시간 복잡도를 선형 시간으로 극적으로 개선할 수 있습니다.
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
7. 메모리 효율적인 제너레이터(Generators)와 yield
일반적인 함수는 return을 만나면 값을 반환하고 종료되지만, 제너레이터는 yield 키워드를 사용하여 값을 반환한 뒤 함수 상태를 일시 중단하고 대기합니다.
-
지연 평가(Lazy Evaluation): 요청이 들어올 때(
next()호출 시)에만 메모리에서 값을 하나씩 계산하므로 대용량 데이터 처리 시 메모리 효율성이 압도적으로 뛰어납니다.
def count_up_to(max_num):
n = 1
while n <= max_num:
yield n # 값을 반환하고 실행을 일시 중단
n += 1
# 제너레이터 객체 생성
gen = count_up_to(3)
print(next(gen)) # 출력: 1
print(next(gen)) # 출력: 2