고급 함수 기법 (고차 함수, 람다, 클로저, 데코레이터, 제너레이터) 총정리

앞서 파이썬 중급 과정의 파일 입출력과 예외 처리 기법을 정복했습니다. 이번 글에서는 함수를 단순한 코드 블록이 아닌 일급 객체(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

답글 남기기