Post

[AI Assignment - Python] πŸŽ€ λ°μ½”λ ˆμ΄ν„° μ™„μ „ 정볡 β€” timer, retry, validate_types 직접 κ΅¬ν˜„

[AI Assignment - Python] πŸŽ€ λ°μ½”λ ˆμ΄ν„° μ™„μ „ 정볡 β€” timer, retry, validate_types 직접 κ΅¬ν˜„

🎯 과제: μ‹€λ¬΄ν˜• λ°μ½”λ ˆμ΄ν„° 3개 κ΅¬ν˜„

  1. @timer β€” ν•¨μˆ˜ μ‹€ν–‰ μ‹œκ°„ μΈ‘μ •
  2. @retry(max_attempts=3) β€” μ‹€νŒ¨ μ‹œ μžλ™ μž¬μ‹œλ„
  3. @validate_types β€” νƒ€μž… 힌트 기반 λŸ°νƒ€μž„ νƒ€μž… 검증

λͺ¨λ“  λ°μ½”λ ˆμ΄ν„°μ— functools.wrapsλ₯Ό μ μš©ν•˜λŠ” 것이 μš”κ΅¬μ‚¬ν•­μ΄μ—ˆμŠ΅λ‹ˆλ‹€.


πŸ“ λ°μ½”λ ˆμ΄ν„° κΈ°λ³Έ ꡬ쑰

κ³Όμ œμ— λ“€μ–΄κ°€κΈ° 전에 λ¨Όμ € κΈ°λ³Έ ꡬ쑰λ₯Ό μ •λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€.

1
2
3
4
5
6
7
8
9
10
def my_decorator(func):
    def wrapper(*args, **kwargs):
        # ν•¨μˆ˜ μ‹€ν–‰ 전에 ν•  일
        result = func(*args, **kwargs)
        # ν•¨μˆ˜ μ‹€ν–‰ 후에 ν•  일
        return result
    return wrapper

# @my_decoratorλŠ” 사싀 이것과 κ°™μŠ΅λ‹ˆλ‹€:
# say_hello = my_decorator(say_hello)

인자λ₯Ό λ°›λŠ” λ°μ½”λ ˆμ΄ν„°λŠ” ν•œ κ²Ή 더 κ°μŒ‰λ‹ˆλ‹€:

1
2
3
4
5
6
def repeat(n: int):           # ← λ°μ½”λ ˆμ΄ν„° νŒ©ν† λ¦¬
    def decorator(func):      # ← μ‹€μ œ λ°μ½”λ ˆμ΄ν„°
        def wrapper(*args, **kwargs):
            ...
        return wrapper
    return decorator

1️⃣ @timer

1μ°¨ μ‹œλ„

1
2
3
4
5
6
7
8
9
10
def timer(func):
    def running_check(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        if result == 'done':
            text = f"좜λ ₯: slow_function μ‹€ν–‰ μ‹œκ°„: {end_time - start_time:.2f}초"
        return text
    print(running_check().__repr__())
    return running_check

지적받은 점 3κ°€μ§€

if result == 'done' ν•˜λ“œμ½”λ”©: λ°μ½”λ ˆμ΄ν„°λŠ” μ–΄λ–€ ν•¨μˆ˜λ“  κ°μŒ€ 수 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€. 'done'을 λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜μ—μ„œλ§Œ μž‘λ™ν•˜λ©΄ λ²”μš©μ„±μ΄ μ—†μŠ΅λ‹ˆλ‹€.

textλ₯Ό λ°˜ν™˜ν•˜κ³  resultλ₯Ό μ†Œμ‹€: μ›λž˜ ν•¨μˆ˜μ˜ λ°˜ν™˜κ°’μ„ λŒλ €μ€˜μ•Ό ν•˜λŠ”λ°, 좜λ ₯ λ¬Έμžμ—΄μ„ λ°˜ν™˜ν•˜κ³  μžˆμ—ˆμŠ΅λ‹ˆλ‹€. ifλ₯Ό 타지 μ•ŠμœΌλ©΄ textκ°€ λ―Έμ •μ˜λ˜μ–΄ NameError도 λ°œμƒν•©λ‹ˆλ‹€.

λ°μ½”λ ˆμ΄ν„° μ•ˆμ—μ„œ 직접 μ‹€ν–‰: print(running_check()) 이 쀄 λ•Œλ¬Έμ— @timerκ°€ λΆ™λŠ” μˆœκ°„ ν•¨μˆ˜κ°€ λ°”λ‘œ μ‹€ν–‰λ©λ‹ˆλ‹€. λ°μ½”λ ˆμ΄ν„°λŠ” 감싼 ν•¨μˆ˜λ₯Ό λ°˜ν™˜λ§Œ ν•΄μ•Ό ν•©λ‹ˆλ‹€.

1
2
3
4
5
6
# μ‹€ν–‰ 타이밍 이해
@timer                    # ← 이 μ‹œμ μ— timer(slow_function) μ‹€ν–‰
def slow_function():      #    β†’ wrapperλ₯Ό λ°˜ν™˜λ§Œ ν•΄μ•Ό 함
    ...

slow_function()           # ← 이 μ‹œμ μ— wrapper() μ‹€ν–‰

2μ°¨ μ‹œλ„ (μ™„μ„±)

1
2
3
4
5
6
7
8
9
def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} μ‹€ν–‰ μ‹œκ°„: {elapsed:.2f}초")
        return result
    return wrapper

Tip: 핡심 νŒ¨ν„΄ β€” wrapperλŠ” resultλ₯Ό λ°˜ν™˜, λ°μ½”λ ˆμ΄ν„°λŠ” wrapperλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.


2️⃣ @retry

1μ°¨ μ‹œλ„

1
2
3
4
5
6
7
8
def retry(max_attempts: int):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(max_attempts):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

μž¬μ‹œλ„ λ‘œμ§μ„ λ°μ½”λ ˆμ΄ν„° μ•ˆμ΄ μ•„λ‹ˆλΌ __main__에 λ”°λ‘œ μž‘μ„±ν–ˆμŠ΅λ‹ˆλ‹€:

1
2
3
4
5
6
7
8
# __main__μ—μ„œ 직접 μž¬μ‹œλ„ β€” λ°μ½”λ ˆμ΄ν„°μ˜ μ˜λ―Έκ°€ 없어짐
for i in range(max_attempts):
    try:
        check = unstable_api_call()
        if check['status'] == 'ok':
            break
    except Exception as e:
        print(f"μ‹œλ„ {i+1}/{max_attempts} μ‹€νŒ¨: {e}")

⚠️ λ°μ½”λ ˆμ΄ν„°μ˜ κ°€μΉ˜λŠ” ν˜ΈμΆœν•˜λŠ” μͺ½μ΄ μž¬μ‹œλ„ λ‘œμ§μ„ λͺ°λΌλ„ 되게 λ§Œλ“œλŠ” κ²ƒμž…λ‹ˆλ‹€. try/except + for 루프가 wrapper μ•ˆμ— μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

2μ°¨ μ‹œλ„ β€” return vs raise 문제

1
2
3
4
5
6
7
8
9
10
11
def wrapper(*args, **kwargs):
    last_exception = None
    for i in range(max_attempts):
        try:
            result = func(*args, **kwargs)
            print(f"{func.__name__} μ‹œλ„ {i+1}/{max_attempts} 성곡")
            return result
        except Exception as e:
            last_exception = e
            print(f"{func.__name__} μ‹œλ„ {i+1}/{max_attempts} μ‹€νŒ¨: {e}")
    return last_exception   # ← 문제!

3번 λ‹€ μ‹€νŒ¨ν•˜λ©΄:

1
2
result = μ„œλ²„ 응닡 μ—†μŒ
type = <class 'ConnectionError'>   ← μ˜ˆμ™Έ 객체가 정상 λ°˜ν™˜κ°’μ²˜λŸΌ λŒμ•„μ˜΄

return last_exception은 μ˜ˆμ™Έλ₯Ό 정상 λ°˜ν™˜κ°’μœΌλ‘œ λŒλ €λ³΄λƒ…λ‹ˆλ‹€. ν˜ΈμΆœν•˜λŠ” μͺ½μ—μ„œ μ—λŸ¬κ°€ λ°œμƒν–ˆλŠ”μ§€ μ•Œ μˆ˜κ°€ μ—†μŠ΅λ‹ˆλ‹€.

3μ°¨ μ‹œλ„ β€” λ°”κΉ₯ try/except μΆ”κ°€ (μ—­μ‹œ μ‹€νŒ¨)

1
2
3
4
5
6
7
8
9
10
11
12
def wrapper(*args, **kwargs):
    last_exception = None
    try:
        for i in range(max_attempts):
            try:
                result = func(*args, **kwargs)
                return result
            except Exception as e:
                last_exception = e
                break
    except Exception as e:
        raise last_exception

μ•ˆμͺ½ exceptκ°€ 이미 μ˜ˆμ™Έλ₯Ό μ²˜λ¦¬ν•˜κΈ° λ•Œλ¬Έμ— λ°”κΉ₯ exceptκΉŒμ§€ μ˜ˆμ™Έκ°€ μ „νŒŒλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 결과적으둜 None이 λ°˜ν™˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

4μ°¨ μ‹œλ„ (μ™„μ„±)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def retry(max_attempts: int):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for i in range(max_attempts):
                try:
                    result = func(*args, **kwargs)
                    print(f"{func.__name__} μ‹œλ„ {i+1}/{max_attempts} 성곡")
                    return result
                except Exception as e:
                    last_exception = e
                    print(f"{func.__name__} μ‹œλ„ {i+1}/{max_attempts} μ‹€νŒ¨: {e}")
            raise last_exception
        return wrapper
    return decorator

Tip: μ„±κ³΅ν•˜λ©΄ return으둜 ν•¨μˆ˜κ°€ μ¦‰μ‹œ μ’…λ£Œλ©λ‹ˆλ‹€. for문이 λκΉŒμ§€ λ‹€ λŒμ•˜λ‹€λŠ” 것 μžμ²΄κ°€ μ „λΆ€ μ‹€νŒ¨λ₯Ό μ˜λ―Έν•˜λ―€λ‘œ, forλ¬Έ λ°”λ‘œ 뒀에 raise만 λ†“μœΌλ©΄ λ©λ‹ˆλ‹€.


3️⃣ @validate_types

1μ°¨ μ‹œλ„ β€” int ν•˜λ“œμ½”λ”©

1
2
3
4
5
6
7
8
def validate_types(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        for arg in args:
            if not isinstance(arg, int):
                raise TypeError(f"μΈμžλŠ” μ •μˆ˜(int)μ—¬μ•Ό ν•©λ‹ˆλ‹€. 받은 νƒ€μž…: {type(arg)}")
        return func(*args, **kwargs)
    return wrapper

λ™μž‘μ€ ν•˜μ§€λ§Œ, λͺ¨λ“  μΈμžκ°€ intμΈμ§€λ§Œ κ²€μ‚¬ν•©λ‹ˆλ‹€. get_type_hints와 inspectλ₯Ό import 해놓고도 μ•ˆ μ“°κ³  μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이러면 greet(name: str, count: int) 같은 ν•¨μˆ˜μ—μ„œ str을 넣어도 μ—λŸ¬κ°€ λ‚©λ‹ˆλ‹€.

2μ°¨ μ‹œλ„ β€” μ—λŸ¬ λ©”μ‹œμ§€ 문제

1
2
3
for param_name, value in zip(params, args):
    if not isinstance(value, hints[param_name]):
        raise TypeError(f"μΈμžλŠ” μ •μˆ˜(int)μ—¬μ•Ό ν•©λ‹ˆλ‹€. 받은 νƒ€μž…: {type(param_name)}")

λ‘œμ§μ€ λ§žμ•˜μ§€λ§Œ μ—λŸ¬ λ©”μ‹œμ§€μ— 두 κ°€μ§€ λ¬Έμ œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€:

  • "μ •μˆ˜(int)"κ°€ ν•˜λ“œμ½”λ”© β€” str이어야 ν•˜λŠ” νŒŒλΌλ―Έν„°μ—λ„ β€œμ •μˆ˜(int)μ—¬μ•Ό ν•©λ‹ˆλ‹€β€κ°€ 좜λ ₯됨
  • type(param_name) β€” param_name은 "a" 같은 λ¬Έμžμ—΄μ΄λ―€λ‘œ 항상 <class 'str'>이 λ‚˜μ˜΄. type(value)λ₯Ό 써야 함

3μ°¨ μ‹œλ„ (μ™„μ„±)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def validate_types(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        hints = get_type_hints(func)
        sig = inspect.signature(func)
        params = list(sig.parameters.keys())

        for param_name, value in zip(params, args):
            if not isinstance(value, hints[param_name]):
                raise TypeError(
                    f"'{param_name}'의 νƒ€μž…μ΄ {hints[param_name].__name__}μ—¬μ•Ό ν•˜μ§€λ§Œ "
                    f"{type(value).__name__}이 전달됨"
                )
        return func(*args, **kwargs)
    return wrapper

βœ… μ‹€ν–‰ κ²°κ³Ό

1
2
3
4
5
6
7
8
9
10
11
12
=== timer ===
slow_function μ‹€ν–‰ μ‹œκ°„: 1.50초

=== retry ===
unstable_api_call μ‹œλ„ 1/3 μ‹€νŒ¨: μ„œλ²„ 응닡 μ—†μŒ
unstable_api_call μ‹œλ„ 2/3 성곡

=== validate_types ===
add(1, 2)          β†’ 3
add("1", "2")      β†’ TypeError: 'a'의 νƒ€μž…μ΄ intμ—¬μ•Ό ν•˜μ§€λ§Œ str이 전달됨
greet(123, 3)      β†’ TypeError: 'name'의 νƒ€μž…μ΄ strμ—¬μ•Ό ν•˜μ§€λ§Œ int이 전달됨
greet("hello", 3)  β†’ hellohellohello

πŸ” functools.wrapsλŠ” μ™œ ν•„μš”ν•œκ°€

μ²˜μŒμ—λŠ” @wraps 없이도 func.__name__이 잘 λ‚˜μ™€μ„œ ν•„μš” μ—†λ‹€κ³  μƒκ°ν–ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ 그건 λ°μ½”λ ˆμ΄ν„° λ‚΄λΆ€μ—μ„œ func을 직접 μ°Έμ‘°ν•˜κ³  있기 λ•Œλ¬Έμ΄μ—ˆμŠ΅λ‹ˆλ‹€. λ¬Έμ œλŠ” λ°”κΉ₯μ—μ„œ λ³Ό λ•Œμž…λ‹ˆλ‹€:

1
2
3
4
5
6
7
8
9
10
11
12
@timer
def slow_function():
    """느린 ν•¨μˆ˜μž…λ‹ˆλ‹€"""
    ...

# @wraps 없이
print(slow_function.__name__)    # β†’ "wrapper"     ← μ›λž˜ 이름 μ†Œμ‹€
print(slow_function.__doc__)     # β†’ None           ← docstring μ†Œμ‹€

# @wraps 있으면
print(slow_function.__name__)    # β†’ "slow_function"
print(slow_function.__doc__)     # β†’ "느린 ν•¨μˆ˜μž…λ‹ˆλ‹€"

⚠️ FastAPI 같은 ν”„λ ˆμž„μ›Œν¬κ°€ ν•¨μˆ˜μ˜ __name__κ³Ό __doc__을 μ½μ–΄μ„œ API λ¬Έμ„œλ₯Ό μžλ™ μƒμ„±ν•˜λŠ”λ°, @wrapsκ°€ μ—†μœΌλ©΄ λͺ¨λ“  μ—”λ“œν¬μΈνŠΈ 이름이 β€œwrapperβ€λ‘œ ν‘œμ‹œλ©λ‹ˆλ‹€.


πŸ’‘ Python νƒ€μž… νžŒνŠΈλŠ” λŸ°νƒ€μž„μ— κ°•μ œλ˜μ§€ μ•ŠλŠ”λ‹€

과제 쀑에 β€œνƒ€μž… 힌트λ₯Ό λ„£μœΌλ©΄ add("1", 2)μ—μ„œ λ°”λ‘œ μ—λŸ¬κ°€ λ‚˜μ§€ μ•Šλ‚˜μš”?β€λΌλŠ” 의문이 μƒκ²ΌμŠ΅λ‹ˆλ‹€. 닡은 μ•„λ‹™λ‹ˆλ‹€.

1
2
3
4
5
def add(a: int, b: int) -> int:
    return a + b

add("hello", "world")   # β†’ "helloworld" (μ—λŸ¬ μ—†μŒ!)
add(1.5, 2.3)            # β†’ 3.8 (int μ•„λ‹Œλ° μ—λŸ¬ μ—†μŒ!)

Python의 νƒ€μž… νžŒνŠΈλŠ” κ·Έλƒ₯ β€œλ©”λͺ¨β€μž…λ‹ˆλ‹€. μ‹€μ œλ‘œ νƒ€μž…μ„ κ°•μ œν•˜λ €λ©΄ mypy 같은 정적 뢄석 도ꡬλ₯Ό μ“°κ±°λ‚˜, 이번 과제의 @validate_types처럼 λŸ°νƒ€μž„μ— 직접 체크해야 ν•©λ‹ˆλ‹€. FastAPIκ°€ λ‚΄λΆ€μ μœΌλ‘œ 이런 λŸ°νƒ€μž„ 검증을 ν•΄μ£ΌλŠ” λŒ€ν‘œμ μΈ μ˜ˆμž…λ‹ˆλ‹€.


πŸ€” λ°μ½”λ ˆμ΄ν„°κ°€ μ™œ ν•„μš”ν•œκ°€

과제λ₯Ό ν•˜λ©΄μ„œ β€œκ΅³μ΄ λ°μ½”λ ˆμ΄ν„°κ°€ ν•„μš”ν•œκ°€?β€λΌλŠ” 의문이 λ“€μ—ˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ @retry 없이 API 호좜 3개λ₯Ό λ§Œλ“ λ‹€κ³  κ°€μ •ν•˜λ©΄ 닡이 λ‚˜μ˜΅λ‹ˆλ‹€:

1
2
3
4
5
6
7
8
9
10
# λ°μ½”λ ˆμ΄ν„° 없이 β€” μž¬μ‹œλ„ 둜직이 맀번 반볡
def call_openai():
    for i in range(3):
        try: return openai.chat(...)
        except: ...

def call_anthropic():
    for i in range(3):              # ← λ˜‘κ°™μ€ μ½”λ“œ
        try: return anthropic.messages(...)
        except: ...
1
2
3
4
5
6
7
8
# λ°μ½”λ ˆμ΄ν„°λ‘œ β€” λΉ„μ¦ˆλ‹ˆμŠ€ 둜직만 λ‚¨μŒ
@retry(max_attempts=3)
def call_openai():
    return openai.chat(...)

@retry(max_attempts=3)
def call_anthropic():
    return anthropic.messages(...)

λ°μ½”λ ˆμ΄ν„°μ˜ κ°€μΉ˜λŠ” νš‘λ‹¨ κ΄€μ‹¬μ‚¬μ˜ λΆ„λ¦¬μž…λ‹ˆλ‹€. λ‘œκΉ…, 인증, μž¬μ‹œλ„, 캐싱, νƒ€μž… 검증 같은 건 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 μ•„λ‹Œλ° μ—¬κΈ°μ €κΈ°μ„œ ν•„μš”ν•©λ‹ˆλ‹€. 이걸 ν•¨μˆ˜λ§ˆλ‹€ λ°˜λ³΅ν•˜λŠ” λŒ€μ‹  @ ν•œ μ€„λ‘œ λΆ™μ΄λŠ” κ²ƒμž…λ‹ˆλ‹€.


πŸ“ 이번 κ³Όμ œμ—μ„œ 배운 것

λ°μ½”λ ˆμ΄ν„° κΈ°λ³Έ νŒ¨ν„΄: wrapperλŠ” resultλ₯Ό λ°˜ν™˜, λ°μ½”λ ˆμ΄ν„°λŠ” wrapperλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. λ°μ½”λ ˆμ΄ν„° μ•ˆμ—μ„œ ν•¨μˆ˜λ₯Ό 직접 μ‹€ν–‰ν•˜λ©΄ μ•ˆ λ©λ‹ˆλ‹€.

인자 μžˆλŠ” λ°μ½”λ ˆμ΄ν„°: νŒ©ν† λ¦¬ β†’ λ°μ½”λ ˆμ΄ν„° β†’ wrapper의 3쀑 쀑첩 κ΅¬μ‘°μž…λ‹ˆλ‹€. ν‚€μ›Œλ“œ 인자둜 ν˜ΈμΆœν•  λ•ŒλŠ” 이름이 μΌμΉ˜ν•΄μ•Ό ν•˜κ³ , μœ„μΉ˜ 인자둜 ν˜ΈμΆœν•˜λ©΄ μƒκ΄€μ—†μŠ΅λ‹ˆλ‹€.

return vs raise: μ˜ˆμ™Έλ₯Ό returnν•˜λ©΄ 정상 λ°˜ν™˜κ°’μ²˜λŸΌ λŒμ•„κ°‘λ‹ˆλ‹€. μ˜ˆμ™ΈλŠ” λ°˜λ“œμ‹œ raise둜 λ°œμƒμ‹œμΌœμ•Ό ν˜ΈμΆœν•˜λŠ” μͺ½μ—μ„œ 인지할 수 μžˆμŠ΅λ‹ˆλ‹€.

@wraps의 μ—­ν• : μ›λž˜ ν•¨μˆ˜μ˜ __name__, __doc__ λ“± 메타데이터λ₯Ό λ³΄μ‘΄ν•©λ‹ˆλ‹€. FastAPI 같은 ν”„λ ˆμž„μ›Œν¬μ˜ μžλ™ λ¬Έμ„œ 생성에 영ν–₯을 μ€λ‹ˆλ‹€.

νƒ€μž… νžŒνŠΈλŠ” λ©”λͺ¨μΌ 뿐: Python은 λŸ°νƒ€μž„μ— νƒ€μž…μ„ κ°•μ œν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. get_type_hints()와 inspect.signature()둜 직접 읽어와야 ν•©λ‹ˆλ‹€.

This post is licensed under CC BY 4.0 by the author.