[Python 100일 챌린지] Day 62 - 데코레이터 심화
데코레이터를 한 단계 업그레이드해봐요! 🚀
어제 배운 기본 데코레이터, 이제 설정값을 받아서 동작을 조정하거나(
@repeat(3)), 클래스로 구현하거나, 여러 개를 동시에 적용할 수 있어요! Django의@login_required, Flask의@app.route("/home")같은 실무 데코레이터들이 바로 이런 고급 기법을 쓰고 있답니다. 😊오늘 배우면 진짜 프로처럼 데코레이터를 다룰 수 있어요!
(35분 완독 ⭐⭐⭐)
🎯 오늘의 학습 목표
📚 사전 지식
- Day 61: 데코레이터 기초 - 기본 데코레이터 개념
🎯 학습 목표 1: 매개변수가 있는 데코레이터 작성하기
한 줄 설명
매개변수가 있는 데코레이터 = 설정 가능한 포장지 🎁⚙️
일반 데코레이터가 고정된 포장지라면, 매개변수가 있는 데코레이터는 “리본 3개 달기”, “리본 5개 달기” 같은 옵션을 줄 수 있어요!
1.1 왜 필요할까요?
1
2
3
4
5
6
7
8
# 같은 기능, 다른 설정
@repeat(times=3)
def say_hello():
print("Hello!")
@repeat(times=5)
def say_goodbye():
print("Goodbye!")
똑같은 반복 기능을 쓰지만, 횟수만 다르게 설정할 수 있어요! 🎯
💡 실생활 비유: 커피 주문할 때 “샷 추가”를 선택하는 것과 비슷해요. 같은 커피(함수)지만 설정(매개변수)만 달라지는 거죠!
1.2 3단계 구조 이해하기
매개변수가 있는 데코레이터는 3단계 중첩 함수로 구현해요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from functools import wraps
def repeat(times):
"""함수를 N번 반복 실행하는 데코레이터"""
# 1단계: 매개변수를 받는 함수
def decorator(func):
# 2단계: 원본 함수를 받는 함수
@wraps(func)
def wrapper(*args, **kwargs):
# 3단계: 실제 실행되는 함수
for i in range(times):
print(f"[{i+1}/{times}]", end=" ")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# 출력:
# [1/3] Hello, Alice!
# [2/3] Hello, Alice!
# [3/3] Hello, Alice!
왜 3단계일까요? 🤔
- 1단계:
times같은 설정값을 먼저 받아요 - 2단계:
func(원본 함수)를 받아요 - 3단계: 실제로 함수를 실행해요
1.3 템플릿 외워두기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from functools import wraps
def decorator_with_params(param1, param2):
"""매개변수가 있는 데코레이터 템플릿"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# param1, param2 사용 가능
print(f"매개변수: {param1}, {param2}")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@decorator_with_params("value1", "value2")
def my_function():
print("함수 실행!")
이 템플릿만 외워두면 어떤 매개변수든 받을 수 있어요! 📝
1.4 실전 예제: 조건부 실행 데코레이터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from functools import wraps
import os
def run_if_env(env_var, expected_value):
"""환경 변수가 특정 값일 때만 함수 실행"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
actual_value = os.getenv(env_var)
if actual_value == expected_value:
print(f"✅ {env_var}={actual_value}, 함수 실행")
return func(*args, **kwargs)
else:
print(f"⏭️ {env_var}={actual_value}, 함수 건너뜀")
return None
return wrapper
return decorator
@run_if_env("ENVIRONMENT", "production")
def send_email():
print("📧 이메일 전송!")
# 테스트
os.environ["ENVIRONMENT"] = "development"
send_email() # 건너뜀
os.environ["ENVIRONMENT"] = "production"
send_email() # 실행됨
실무에서 이렇게 써요! 개발 환경에서는 이메일 안 보내고, 운영 환경에서만 보내게 할 때 유용해요! 🎯
1.5 실전 예제: 속도 제한 데코레이터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from functools import wraps
import time
def rate_limit(max_calls, period):
"""일정 시간 동안 최대 호출 횟수를 제한"""
def decorator(func):
calls = []
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# 오래된 호출 기록 제거
calls[:] = [c for c in calls if now - c < period]
if len(calls) >= max_calls:
wait_time = period - (now - calls[0])
print(f"⏳ {wait_time:.1f}초 후 다시 시도하세요")
time.sleep(wait_time)
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, period=5)
def api_call():
print("🌐 API 호출!")
# 테스트
for i in range(5):
print(f"\n호출 {i+1}:")
api_call()
실무에서 이렇게 써요! API 요청을 5초에 3번으로 제한해서 서버 부하를 줄여요! 🚦
🎯 학습 목표 2: 클래스 데코레이터 이해하기
한 줄 설명
클래스 데코레이터 = 상태를 기억하는 포장지 🧠📦
함수로 만든 데코레이터는 간단하지만, 클래스로 만들면 상태(count, cache 등)를 더 쉽게 관리할 수 있어요!
2.1 클래스를 사용한 데코레이터
함수 대신 클래스로 데코레이터를 구현할 수 있어요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from functools import wraps
class CountCalls:
"""함수 호출 횟수를 세는 데코레이터 (클래스 버전)"""
def __init__(self, func):
wraps(func)(self)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"호출 횟수: {self.count}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # 호출 횟수: 1
say_hello() # 호출 횟수: 2
say_hello() # 호출 횟수: 3
print(f"총 {say_hello.count}번 호출됨")
핵심 포인트!
__init__: 함수를 받아서 저장해요__call__: 함수처럼 호출될 때 실행돼요self.count: 상태를 인스턴스 변수로 저장해요! 🔢
💡 언제 클래스를 쓸까요? 호출 횟수, 캐시, 통계 같은 상태를 기억해야 할 때 클래스가 훨씬 편해요!
2.2 매개변수가 있는 클래스 데코레이터
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from functools import wraps
class Retry:
"""재시도 데코레이터 (클래스 버전)"""
def __init__(self, max_attempts=3, delay=1):
self.max_attempts = max_attempts
self.delay = delay
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(self.max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == self.max_attempts - 1:
raise
print(f"시도 {attempt + 1} 실패, {self.delay}초 후 재시도")
import time
time.sleep(self.delay)
return wrapper
@Retry(max_attempts=3, delay=0.5)
def unreliable_function():
import random
if random.random() < 0.7:
raise Exception("일시적 오류")
return "성공!"
result = unreliable_function()
print(result)
실무에서 이렇게 써요! 네트워크 요청이나 DB 연결처럼 가끔 실패할 수 있는 작업에서 자동으로 재시도해요! 🔄
2.3 클래스에 데코레이터 적용하기
함수뿐만 아니라 클래스 전체에도 데코레이터를 적용할 수 있어요!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def singleton(cls):
"""싱글톤 패턴을 구현하는 클래스 데코레이터"""
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
print("데이터베이스 연결 초기화")
self.connection = "db_connection"
# 테스트
db1 = Database() # 데이터베이스 연결 초기화
db2 = Database() # 출력 없음 (이미 생성됨)
print(db1 is db2) # True (같은 인스턴스)
싱글톤이 뭐예요? 🤔
- 클래스의 인스턴스를 딱 하나만 만들어요
- DB 연결, 설정 관리 같은 곳에서 유용해요!
💡 실생활 비유: 나라에 대통령이 한 명인 것처럼, 프로그램에 DB 연결도 하나만 있으면 돼요!
🎯 학습 목표 3: 데코레이터 체이닝 활용하기
한 줄 설명
데코레이터 체이닝 = 여러 포장지를 겹겹이 씌우기 📦📦📦
한 함수에 여러 데코레이터를 동시에 적용할 수 있어요!
3.1 여러 데코레이터 적용하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from functools import wraps
import time
def timer(func):
"""실행 시간 측정"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"⏱️ 실행 시간: {end - start:.4f}초")
return result
return wrapper
def logger(func):
"""함수 호출 로깅"""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"📝 함수 '{func.__name__}' 호출됨")
result = func(*args, **kwargs)
print(f"📝 함수 '{func.__name__}' 완료")
return result
return wrapper
# 데코레이터 체이닝 (아래에서 위로 적용됨)
@timer
@logger
def calculate():
print("계산 중...")
time.sleep(0.5)
return 42
result = calculate()
# 출력:
# 📝 함수 'calculate' 호출됨
# 계산 중...
# 📝 함수 'calculate' 완료
# ⏱️ 실행 시간: 0.5012초
3.2 데코레이터 적용 순서 이해하기
1
2
3
4
5
6
7
8
9
# 이것은
@decorator1
@decorator2
@decorator3
def func():
pass
# 다음과 같아요
func = decorator1(decorator2(decorator3(func)))
아래에서 위로 적용돼요! 양파 껍질 벗기듯이, 가장 안쪽(decorator3)부터 바깥쪽(decorator1)으로요! 🧅
3.3 실전 예제: API 엔드포인트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from functools import wraps
def authenticate(func):
"""인증 확인"""
@wraps(func)
def wrapper(*args, **kwargs):
print("🔐 인증 확인 중...")
# 실제로는 토큰 검증 등
return func(*args, **kwargs)
return wrapper
def validate_input(func):
"""입력 검증"""
@wraps(func)
def wrapper(*args, **kwargs):
print("✅ 입력 검증 중...")
# 실제로는 데이터 검증
return func(*args, **kwargs)
return wrapper
def log_api_call(func):
"""API 호출 로깅"""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"📊 API 호출: {func.__name__}")
result = func(*args, **kwargs)
print(f"📊 API 응답: {result}")
return result
return wrapper
@log_api_call
@authenticate
@validate_input
def create_user(username, email):
"""사용자 생성 API"""
return {"username": username, "email": email, "id": 123}
# 사용
user = create_user("alice", "[email protected]")
실무에서 이렇게 써요! 웹 API에서 로깅 → 인증 → 입력 검증을 순차적으로 처리해요! 🎯
🎯 학습 목표 4: 내장 데코레이터 마스터하기
한 줄 설명
내장 데코레이터 = 파이썬이 제공하는 필수 도구 🛠️
직접 만들지 않아도, 파이썬이 미리 만들어둔 강력한 데코레이터들이 있어요!
4.1 @property - 메서드를 속성처럼!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
"""이름 가져오기"""
return self._name
@property
def age(self):
"""나이 가져오기"""
return self._age
@age.setter
def age(self, value):
"""나이 설정하기"""
if value < 0:
raise ValueError("나이는 0 이상이어야 합니다")
self._age = value
@property
def description(self):
"""계산된 속성"""
return f"{self.name}님은 {self.age}세입니다"
# 사용
person = Person("Alice", 25)
# 메서드처럼 ()를 사용하지 않아요!
print(person.name) # Alice
print(person.age) # 25
print(person.description) # Alice님은 25세입니다
# setter 사용
person.age = 26
print(person.age) # 26
person.age = -1 # ValueError 발생!
왜 @property를 쓸까요? 🤔
- 메서드인데 속성처럼 쓸 수 있어요 (괄호 안 씀)
- 값을 검증할 수 있어요 (setter)
- 계산된 값을 속성처럼 제공해요 (description)
💡 실생활 비유: 자판기에서 커피 버튼만 누르면 내부에서 자동으로 커피를 만들어주는 것처럼,
person.age만 써도 내부에서 getter 메서드가 실행돼요!
4.2 @staticmethod와 @classmethod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class MathUtils:
pi = 3.14159
def __init__(self, value):
self.value = value
# 인스턴스 메서드 (self 필요)
def add(self, n):
return self.value + n
# 정적 메서드 (self, cls 불필요)
@staticmethod
def multiply(a, b):
"""단순 계산, 클래스/인스턴스와 무관"""
return a * b
# 클래스 메서드 (cls 필요)
@classmethod
def from_string(cls, s):
"""대체 생성자"""
value = int(s)
return cls(value)
@classmethod
def get_pi(cls):
"""클래스 속성 접근"""
return cls.pi
# 사용
# 인스턴스 메서드
obj = MathUtils(10)
print(obj.add(5)) # 15
# 정적 메서드 (인스턴스 없이 호출 가능)
print(MathUtils.multiply(3, 4)) # 12
# 클래스 메서드 (대체 생성자)
obj2 = MathUtils.from_string("20")
print(obj2.value) # 20
# 클래스 메서드 (클래스 속성 접근)
print(MathUtils.get_pi()) # 3.14159
언제 뭘 쓸까요? 📚
- 인스턴스 메서드: 인스턴스 데이터(
self.value) 필요할 때 - @staticmethod: 클래스와 관계없는 유틸리티 함수
- @classmethod: 대체 생성자나 클래스 속성 접근할 때
4.3 @staticmethod vs @classmethod 비교
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Pizza:
def __init__(self, ingredients):
self.ingredients = ingredients
@staticmethod
def mix_ingredients(x, y):
"""정적 메서드: 클래스와 무관한 유틸리티 함수"""
return x + y
@classmethod
def margherita(cls):
"""클래스 메서드: 특정 타입의 인스턴스 생성"""
return cls(["모짜렐라", "토마토"])
@classmethod
def prosciutto(cls):
"""클래스 메서드: 다른 특정 타입의 인스턴스 생성"""
return cls(["모짜렐라", "토마토", "햄"])
# 사용
Pizza.mix_ingredients("치즈", "토마토") # 정적 메서드
pizza1 = Pizza.margherita() # 클래스 메서드
pizza2 = Pizza.prosciutto() # 클래스 메서드
print(pizza1.ingredients) # ['모짜렐라', '토마토']
print(pizza2.ingredients) # ['모짜렐라', '토마토', '햄']
@classmethod가 유용한 이유! 🍕
- 다양한 방법으로 인스턴스를 만들 수 있어요 (대체 생성자)
Pizza.margherita()처럼 의미가 명확해요!
4.4 @cached_property (Python 3.8+)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import cached_property
import time
class DataProcessor:
def __init__(self, data):
self.data = data
@cached_property
def processed_data(self):
"""한 번만 계산되고 캐시됨"""
print("⏳ 데이터 처리 중... (시간이 오래 걸림)")
time.sleep(1)
return [x * 2 for x in self.data]
# 사용
processor = DataProcessor([1, 2, 3, 4, 5])
# 첫 번째 접근: 계산 수행
print(processor.processed_data)
# 출력: ⏳ 데이터 처리 중... (시간이 오래 걸림)
# [2, 4, 6, 8, 10]
# 두 번째 접근: 캐시된 값 반환 (즉시)
print(processor.processed_data)
# 출력: [2, 4, 6, 8, 10]
언제 쓸까요? 🚀
- 계산이 오래 걸리지만 결과가 변하지 않을 때
- 한 번만 계산하고 결과를 저장해요!
💡 실생활 비유: 처음엔 직접 커피를 내려서 시간이 걸리지만, 다음부턴 보온병에 담아둔 커피를 바로 마시는 거예요!
💡 오늘의 핵심 요약
1. 매개변수가 있는 데코레이터
1
2
3
4
5
6
7
8
def decorator(param):
def wrapper(func):
@wraps(func)
def inner(*args, **kwargs):
# param 사용 가능
return func(*args, **kwargs)
return inner
return wrapper
2. 클래스 데코레이터
__init__: 함수나 매개변수 받기__call__: 실제 래핑된 함수 실행- 상태를 기억해야 할 때 유용해요!
3. 데코레이터 체이닝
- 여러 데코레이터 적용 가능해요
- 아래에서 위로 적용돼요!
4. 내장 데코레이터
@property: 속성처럼 사용하는 메서드@staticmethod: 클래스와 무관한 메서드@classmethod: 클래스 정보가 필요한 메서드@cached_property: 결과를 캐싱하는 속성
🧪 연습 문제
문제 1: 실행 횟수 제한 데코레이터
함수를 최대 N번까지만 실행할 수 있는 max_calls 데코레이터를 작성하세요.
1
2
3
4
5
6
7
8
@max_calls(3)
def greet():
print("Hello!")
greet() # 실행
greet() # 실행
greet() # 실행
greet() # "최대 호출 횟수 초과" 메시지
💡 힌트
단계별 힌트:
- 3단계 중첩 함수 구조를 만들어요
nonlocal키워드로 호출 횟수를 기록해요- 호출 전에 횟수를 확인하고, 초과하면 메시지만 출력해요
핵심 키워드: nonlocal count, count >= n, count += 1
✅ 정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from functools import wraps
def max_calls(n):
"""함수를 최대 n번까지만 실행"""
def decorator(func):
count = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal count
if count >= n:
print(f"❌ 최대 호출 횟수({n}번) 초과")
return None
count += 1
print(f"[{count}/{n}]", end=" ")
return func(*args, **kwargs)
return wrapper
return decorator
# 테스트
@max_calls(3)
def greet():
print("Hello!")
for _ in range(5):
greet()
출력:
1
2
3
4
5
[1/3] Hello!
[2/3] Hello!
[3/3] Hello!
❌ 최대 호출 횟수(3번) 초과
❌ 최대 호출 횟수(3번) 초과
문제 2: 계산 속성을 가진 클래스
@property를 사용하여 원의 반지름으로부터 넓이와 둘레를 계산하는 Circle 클래스를 작성하세요.
💡 힌트
단계별 힌트:
@property로area,circumference를 만들어요@radius.setter로 반지름 검증(음수 방지)을 추가해요- 원의 넓이 = π × r², 둘레 = 2 × π × r
핵심 키워드: @property, @radius.setter, math.pi
✅ 정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import math
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("반지름은 0 이상이어야 합니다")
self._radius = value
@property
def area(self):
"""원의 넓이"""
return math.pi * self._radius ** 2
@property
def circumference(self):
"""원의 둘레"""
return 2 * math.pi * self._radius
def __repr__(self):
return f"Circle(radius={self.radius})"
# 테스트
circle = Circle(5)
print(circle) # Circle(radius=5)
print(f"넓이: {circle.area:.2f}") # 넓이: 78.54
print(f"둘레: {circle.circumference:.2f}") # 둘레: 31.42
circle.radius = 10
print(f"넓이: {circle.area:.2f}") # 넓이: 314.16
출력:
1
2
3
4
Circle(radius=5)
넓이: 78.54
둘레: 31.42
넓이: 314.16
📝 오늘 배운 내용 정리
- 매개변수가 있는 데코레이터: 3단계 중첩으로 설정값을 받아요
- 클래스 데코레이터: 상태를 기억할 수 있어요
- 데코레이터 체이닝: 여러 데코레이터를 겹겹이 적용해요
- 내장 데코레이터: @property, @staticmethod, @classmethod, @cached_property
🔗 관련 자료
📚 이전 학습
Day 61: 데코레이터 기초 ⭐⭐⭐
어제는 데코레이터의 기본 개념과 함수 래핑, @wraps 사용법을 배웠어요!
📚 다음 학습
Day 63: 제너레이터 기초 ⭐⭐⭐
내일은 메모리 효율적인 데이터 처리를 위한 제너레이터를 배워요!
“고급 개념도 차근차근 배우면 어렵지 않아요!” 🚀
Day 62/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
