환영합니다, Rolling Ress의 카루입니다.
Python 첫 글 마지막에 Decoration에 대해 쓰겠다고 했는데, 몇 년이 지나서야 쓰네요. 이번에는 파이썬의 강력한 기능 중 하나인 데코레이터에 대해 설명하며, 사전지식으로 펑터와 클로저에 대해서도 다루겠습니다.
Functor(함수객체, 펑터)
함수객체란 객체를 함수처럼 쓸 수 있게 만든 것입니다. 일반적인 함수와 달리 클래스의 내부 변수를 통해 상태를 저장할 수 있는 점이 큰 차이점입니다. C++에서 operator()을 이용하여 함수객체를 정의했다면, 파이썬에서는 동일한 기능을 수행하는 __call__()을 정의하여 함수객체를 만들 수 있습니다.
체이닝에 대해 알아보겠습니다. 합성함수 h(g(f(x)))가 있을 때, 실제 함수의 실행 순서는 f, g, h입니다. 조금 더 직관적으로 표기할 수는 없을까요? 우리가 읽는 순서대로 f-g-h를 순서대로 표기하는 방법이 있습니다.
class Chain:
def __init__(self, v): self.v = v
def __call__(self, f): return Chain(f(self.v))
def __repr__(self): return str(self.v)
# 내장 메서드(str.strip, str.upper)와 람다를 한 줄로 체이닝
result = Chain(" python ")(str.strip)(str.upper)(lambda x: x + " IS FUN!")
print(result) # 출력: PYTHON IS FUN!
코드가 좀 난해하죠. 그렇지만 이해하면 쉽습니다.
1. Chain(" python ")이 호출되어 Chain 타입의 이름 없는 임시객체 A가 생성되고, 내부 v에는 " python "이 저장됩니다.
2. A(str.strip)을 통해 임시객체 A는 str.strip(v)를 생성합니다. 이때 Chain 타입의 이름 없는 임시객체 B가 생성되고, 내부 v에는 "python"이 저장됩니다.
3. B(str.strip)을 통해 임시객체 B는 str.upper(v)를 생성합니다. 이때 Chain 타입의 이름 없는 임시객체 C가 생성되고, 내부 v에는 "PYTHON"이 저장됩니다.
4. C(lambda x: x + " IS FUN!")을 통해 임시객체 C는 람다식이 적용된 v값을 생성합니다. 이때 Chain 타입의 이름 없는 임시객체 D가 생성되고, 내부 v에는 "PYTHON IS FUN!"이 저장됩니다.
5. 임시객체 D가 result에 저장됩니다.
6. D의 __repr__ 함수가 실행되어 v를 str형으로 반환하여 출력합니다.
만약 펑터가 없었다면 아래와 같이 써야 했겠죠.
raw_data = " python "
# 주의: 괄호 안쪽부터 시작해 오른쪽에서 왼쪽으로 거꾸로 읽어야 합니다.
result = (lambda x: x + " IS FUN!")(str.upper(str.strip(raw_data)))
print(result) # PYTHON IS FUN!
경우에 따라 이게 더 편할 수도 있겠지만, 코드가 길어지면 이쪽이 더 난해해집니다.
Closure(클로저)
클로저는 상태를 가지는 함수라고 이해하면 쉽습니다. 쉽게 말해, 생성될 당시의 상황을 기억하는 함수입니다. 사실 비슷한 개념을 방금 봤죠. 함수객체도 멤버 변수를 이용해 상태를 기억할 수 있었으니까요. 펑터의 편리한 버전이 클로저라고 보시면 됩니다.
def adder(a):
def wrapper(b):
return a + b # a는 adder의 변수
return wrapper # wrapper 함수는 객체이므로, 반환할 수 있다.
add2 = adder(2)
print(add2(2)) # 2 + 2 = 4
add5 = adder(5)
print(add5(3)) # 5 + 3 = 8
# print(add5(10))의 결과는 얼마일까요?
adder 함수는 단지 내부의 wrapper 함수를 반환합니다. add2 = adder(2)를 통해 add2에는 a = 2인 wrapper 함수가 통으로 들어가게 됩니다. 무언가 이상하지 않나요? adder 함수는 이미 종료되었는데, wrapper 함수는 add2에 남아 계속 살아있고 adder함수의 지역변수인 a는 2의 값을 계속 갖고 있습니다. 이게 바로 클로저입니다. 함수가 생성될 당시의 상황(a = 2)을 기억하는 함수죠.
Decorator
이제 데코레이터를 봅시다. 함수의 시작과 끝, 혹은 둘 중 하나에 특정한 작업을 넣고 싶다고 가정합니다. 여기서는 시작과 끝에 "시작", "끝"을 출력하겠습니다.
def trace(f):
def wrapper():
print(f.__name__, ' 시작')
f()
print(f.__name__, ' 끝')
return wrapper
def hello():
print('< hello >')
def world():
print('< world >')
trace_hello = trace(hello)
trace_hello()
trace_world = trace(world)
trace_world()
여기서 클로저가 보입니다. 이번에는 변수를 저장하지 않고, 매개변수인 함수 f를 상태로 저장했습니다. trace에 함수를 담아 객체를 생성하면, trace 객체는 이 함수를 실행할 준비가 됩니다.

즉, wrapper 함수에서 f()의 앞뒤에 특정한 동작을 일으키는 문장을 작성하면 함수 실행 전후에 해당 코드를 실행할 수 있습니다. 하지만 모든 함수를 이렇게 객체로 만들어서 사용하기는 귀찮죠. 그럴 때 @을 활용한 데코레이터 문법을 씁니다.
def trace(f):
def wrapper():
print(f.__name__, ' 시작')
f()
print(f.__name__, ' 끝')
return wrapper
@trace
def hello():
print('< hello >')
@trace
def world():
print('< world >')
hello()
world()
데코레이터로 실행할 함수 위에 @<데코레이터이름>을 작성해주면 끝입니다. 실행 결과는 위와 동일합니다. 이렇게 하면 기존 hello, world 함수의 동작을 수정하지 않으면서 새로운 기능을 추가할 수 있습니다.
import time
# 실행 시간을 측정하는 데코레이터
def timer(func):
# *args, **kwargs를 넣으면 어떤 인자를 가진 함수든 모두 받을 수 있습니다.
def wrapper(*args, **kwargs):
start_time = time.time() # 시작 시간 기록
result = func(*args, **kwargs) # 원래 함수 실행
end_time = time.time() # 종료 시간 기록
print(f"[{func.__name__}] 실행 시간: {end_time - start_time:.4f}초")
return result # 원래 함수의 반환값을 그대로 돌려줌
return wrapper
@timer
def count_numbers(limit):
total = 0
for i in range(limit):
total += i
return total
@timer
def make_sleep():
time.sleep(1)
print("충분히 쉬었습니다.")
# 함수 실행
count_numbers(10000000)
make_sleep()
위처럼 작성하면 timer 데코레이터를 이용해 count_numbers, make_sleep 함수의 실행 시간을 측정하여 표시할 수 있습니다. 기능이 필요 없다면 단순히 @timer를 제거하면 원래 함수의 구현대로 사용할 수 있습니다. 반대로 새로운 함수를 만들었는데, 시간 측정을 하고 싶다면? 그 함수 위에 @timer를 추가해주면 됩니다.
파이썬에서는 static 메서드를 정의할 수 있는 키워드가 없는데, 데코레이터를 이용하여 동일하게 구현할 수 있습니다. 아래는 파이썬의 내장 데코레이터 @staticmethod의 사용법입니다.
class MathUtils:
@staticmethod
def add(a, b):
# self를 받지 않습니다! 클래스의 상태와 무관하게 작동합니다.
return a + b
@staticmethod
def is_even(number):
return number % 2 == 0
# 객체를 생성(MathUtils())하지 않고, 클래스 이름에서 바로 꺼내 씁니다.
print(MathUtils.add(10, 20)) # 출력: 30
print(MathUtils.is_even(4)) # 출력: True
이 경우, 마치 @staticmethod가 키워드처럼 동작한다고 볼 수도 있습니다. 그렇게 이해해도 문제 없으며, 오히려 그 편이 더 직관적입니다.
class Person:
def __init__(self, name, age):
self.name = name
self._age = age # 실제 데이터는 '_age'에 숨겨둡니다.
# 1. Getter
@property
def age(self):
print("Getter 호출: 나이를 조회합니다.")
return self._age
# 2. Setter
@age.setter
def age(self, value):
print(f"Setter 호출: 나이를 {value}(으)로 변경 시도합니다.")
if value < 0:
raise ValueError("오류: 나이는 음수가 될 수 없습니다!")
self._age = value
user = Person("홍길동", 20)
# 값 읽기 (Getter 작동)
print(user.age) # 출력: Getter 호출: 나이를 조회합니다. -> 20
# 값 쓰기 (Setter 작동 - 정상적인 값)
user.age = 25 # 출력: Setter 호출: 나이를 25(으)로 변경 시도합니다.
print(user.age) # 25
# 값 쓰기 (Setter 작동 - 잘못된 값 필터링)
# user.age = -5 # ValueError: 오류: 나이는 음수가 될 수 없습니다! 발생
C#에서의 프로퍼티와 같은 사용도 가능합니다. 단, @property만 사용하면 읽기 전용이 됩니다. @프로퍼티이름.setter를 사용해야 값을 수정할 수 있습니다. 프로퍼티를 사용하면 그냥 변수를 사용할 때에 비해 입출력에 조건을 걸 수 있고, 여러 변수를 조합하여 새로운 읽기 전용 프로퍼티를 만드는 등 유연한 사용이 가능해집니다.
'Python' 카테고리의 다른 글
| Python 고급 문법 3: with 블럭과 @contextmanager (0) | 2026.04.22 |
|---|---|
| Python 고급 문법 2: 이터레이터(iterator)와 제너레이터(generator) (0) | 2026.04.22 |
| 파이썬 코딩 효율을 높여주는 10가지 문법들 (ft. Pythonic한 코드란?) (0) | 2024.09.24 |
| 파이썬을 활용한 네트워크 분석 (고양국제고 진로특강) (0) | 2022.04.10 |
| 파이썬 코딩 효율을 크게 높여주는 7가지 팁들 (0) | 2022.02.16 |