일급 함수 (First-class Function)
파이썬에서 함수는 일급 객체(first-class object)로 작동합니다.
일급 객체는 다른 값(숫자, 문자열 등)과 동일하게 취급할 수 있는 대상을 말합니다.
즉, 숫자를 변수에 담고 다른 함수에 전달하고 반환할 수 있듯이, 함수도 똑같이 할 수 있다는 의미라고 이해하면 됩니다.
함수를 변수에 할당
함수를 호출하지 않고 이름만 쓰면 함수 객체 자체를 가리킵니다.
이 함수 객체를 변수에 담을 수 있습니다.
def greet():
print("hello")
say_hi = greet
say_hi() # hello
위 코드를 통해 greet와 say_hi는 이제 같은 함수 객체를 가리키고 있습니다.
greet()처럼 괄호가 붙으면 함수를 실행하는 것이고, greet처럼 괄호 없이 쓰면 함수 자체를 참조하는 것으로 코드가 동작합니다.
이와 같은 차이를 구분하는 것이 핵심입니다.
함수를 인자로 전달
함수를 다른 함수의 인자로도 넘길 수 있습니다.
받는 쪽에서는 그 함수를 내부적으로 호출하거나 다른 방식으로 활용할 수 있습니다.
def run(func):
func()
run(greet) # hello
run(greet)에서 greet는 함수 객체입니다.
run 함수는 전달받은 함수를 내부에서 func()로 호출하는 함수입니다.
이처럼 함수를 인자로 받는 함수를 고차 함수(higher-order function)라고 부릅니다.
파이썬의 map(), filter(), sorted() 등이 모두 이 방식으로 동작하고 있다고 합니다.
함수를 반환값으로 사용
함수가 다른 함수를 만들어서 반환할 수 있습니다.
def get_func():
def inner():
print("hello")
return inner
f = get_func()
f() # hello
get_func()를 호출하면 inner 함수 객체가 반환됩니다.
반환된 함수를 f에 담아서 나중에 호출할 수 있습니다.
위와 같은 패턴이 데코레이터의 핵심 구조라고 이해할 수 있습니다.
클로저 (Closure)
클로저는 내부 함수가 자신이 정의된 외부 함수의 변수를 기억하는 함수입니다.
외부 함수가 이미 실행을 마치고 종료된 이후에도 내부 함수가 그 변수에 계속 접근할 수 있습니다.
일반적으로 함수가 종료되면 그 안에서 만든 변수도 사라집니다.
그런데 내부 함수가 외부 함수의 변수를 참조하고 있으면, 외부 함수가 종료된 뒤에도 그 변수가 메모리에서 사라지지 않고 내부 함수와 함께 유지됩니다.
이 상태를 클로저라고 합니다.
def outer(message):
def inner():
print(message)
return inner
f = outer("hello")
f() # hello
다시 설명하면, outer("hello")가 반환된 시점에 outer 함수는 종료됐습니다.
그런데 반환된 inner는 여전히 message 변수를 기억하고 있습니다.
inner가 message를 여전히 잡아서 갖고 있기 때문입니다.
데코레이터는 이와 같은 클로저 구조를 그대로 활용합니다.
원본 함수를 외부 함수의 인자로 받아서 내부 함수가 그 함수를 기억하고, 기억한 함수를 실행하기 전후에 원하는 동작을 추가하는 방식으로 동작합니다.
데코레이터 기초와 @ 문법
지금까지의 개념이 모아서 정리하면 데코레이터가 됩니다.
데코레이터는 함수를 인자로 받아서, 그 함수에 기능을 추가한 새로운 함수를 반환하는 함수입니다.
원본 함수의 코드를 직접 수정하지 않고 동작을 확장할 수 있다는 것이 핵심입니다.
@ 없이 직접 감싸기
@ 문법을 쓰기 전에 데코레이터가 어떻게 동작하는지 확인해보겠습니다.
def my_decorator(func):
def wrapper():
print("함수 실행 전")
func()
print("함수 실행 후")
return wrapper
def say_hello():
print("hello")
say_hello = my_decorator(say_hello)
say_hello()
# 함수 실행 전
# hello
# 함수 실행 후
my_decorator는 함수를 받아서 wrapper를 반환합니다.
wrapper는 클로저로, 전달받은 func를 기억하고 있습니다.
say_hello = my_decorator(say_hello) 이후로 say_hello는 더 이상 원본 함수가 아니라 wrapper를 가리키고 있습니다.
@ 문법
say_hello = my_decorator(say_hello) 이 과정을 @로 간결하게 표현할 수 있습니다.
@my_decorator
def say_hello():
print("hello")
@my_decorator는 say_hello = my_decorator(say_hello)와 완전히 동일합니다.
@ 문법은 새로운 기능이 아니라 기존 코드를 더 읽기 쉽게 만들어주는 표기법입니다.
*args, **kwargs를 활용한 범용 데코레이터
위의 wrapper()는 인자를 받지 않기 때문에 인자가 없는 함수에만 적용할 수 있습니다.
어떤 함수에든 적용하려면 wrapper가 인자를 유연하게 받아서 원본 함수로 그대로 전달해야 합니다.
이럴 경우에 *args와 **kwargs를 사용합니다.
어떤 인자가 몇 개 오든 상관없이 모두 받아서 그대로 넘겨주는 방식입니다.
또한 원본 함수의 반환값도 wrapper가 그대로 돌려줘야 합니다.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("함수 실행 전")
result = func(*args, **kwargs)
print("함수 실행 후")
return result
return wrapper
이 패턴이 데코레이터를 작성할 때 기본 형태입니다.
실용적으로 쓸 데코레이터라면 항상 이 구조로 작성하는 것이 좋습니다.
functools.wraps
데코레이터를 적용하면 원본 함수가 wrapper로 교체됩니다.
이 과정에서 함수의 이름(__name__)이나 독스트링(__doc__) 같은 메타데이터도 wrapper의 것으로 바뀝니다.
디버깅이나 문서화 도구를 사용할 때 혼란이 생길 수 있습니다.
@my_decorator
def say_hello():
"""인사 함수"""
print("hello")
print(say_hello.__name__) # wrapper ← 원본 이름이 사라짐
print(say_hello.__doc__) # None
functools.wraps는 이 문제를 해결해줍니다.
wrapper에 @wraps(func)를 붙이면 원본 함수의 메타데이터를 wrapper에 복사해서 보존합니다.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
데코레이터를 작성할 때 @wraps(func)를 빠뜨리면 나중에 의도치 않은 문제가 생길 수 있어서, 거의 항상 함께 작성하는 것이 관례입니다.
인자를 받는 데코레이터
지금까지의 데코레이터는 함수 자체만 받았습니다.
그런데 데코레이터 자체에 설정값을 전달하고 싶은 경우가 있습니다.
예를 들어 함수를 몇 번 반복할지, 어떤 로그 레벨로 기록할지 등을 외부에서 지정하고 싶을 때입니다.
이를 위해서는 함수를 한 겹 더 감싸야 합니다.
구조는 밑과 같습니다.
가장 바깥 함수가 데코레이터의 인자를 받고, 그 안의 함수가 원본 함수를 받고, 가장 안쪽 함수가 실제로 실행됩니다.
from functools import wraps
def repeat(n):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def say_hello():
print("hello")
@repeat(3)은 내부적으로 say_hello = repeat(3)(say_hello)와 동일합니다.
repeat(3)이 먼저 실행되어 decorator를 반환하고, 그 decorator가 say_hello를 받아서 wrapper로 감싸는 순서입니다.
처음 보면 복잡하게 느껴질 수 있는데, 결국 데코레이터를 반환하는 함수를 하나 더 만드는 것입니다.
실용 예시
실행 시간 측정
함수 실행에 걸리는 시간을 측정할 때 사용합니다. 분석 파이프라인에서 어느 단계가 병목인지 파악할 때 유용합니다.
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 실행 시간: {time.time() - start:.4f}초")
return result
return wrapper
입력값 검증
함수 본문 안에서 입력값을 검증하는 코드를 반복 작성하는 대신, 데코레이터로 분리하면 함수 본문을 핵심 로직에만 집중시킬 수 있습니다.
def validate_positive(func):
@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if isinstance(arg, (int, float)) and arg < 0:
raise ValueError(f"음수는 허용되지 않습니다: {arg}")
return func(*args, **kwargs)
return wrapper
'BI&Programming-Tools > Python' 카테고리의 다른 글
| 생물정보학에서의 프로그래밍, 환경 세팅 (0) | 2026.07.06 |
|---|---|
| [파이썬] 리스트 컴프리헨션(List Comprehension) (0) | 2026.06.27 |
| [Python TDD] Mocking (0) | 2026.06.24 |
| [Python TDD] pytest (0) | 2026.06.24 |
| [Python TDD] unittest (0) | 2026.06.24 |
