지식/Python

[python] 시간, 타입 체크 데코레이터

ZeroAct 2023. 2. 26.
728x90
반응형

개요

파이썬에는 데코레이터라(decorator)는 것이 있다.

직역하면 꾸미다 라는 뜻인데 말그대로 함수를 꾸미는 것이다.

 

만약 함수의 시간을 체크 하고 싶다고 해보자.

그럼 이렇게 할 것 이다.

import time
import random

def func():
    time.sleep(random.random() * 1)

start = time.time()
func()
ellapsed_time = time.time() - start

print(f"Ellapsed Time: {ellapsed_time:.2f} seconds")
# Ellapsed Time: 0.55 seconds

시간을 재는 곳마다 저런 구조로 코드를 짜야한다면 매우 더러울 것이다.

시간을 재는 함수를 짜보자.

def func():
    time.sleep(random.random() * 1)

def get_ellapsed_time(func):
    start = time.time()
    func()
    ellapsed_time = time.time() - start
    print(f"Ellapsed Time: {ellapsed_time:.2f} seconds")

get_ellapsed_time(func)

func 함수 자체를 get_ellapsed_time 에 넘겨서 위와 같이 짤 수 있다.

이정도도 사실 충분하지만 코드가 이쁘지는 않다.

시간을 재는 행위가 코드 구조에 영향을 미치는 것이 맘에 들지 않는다.

(우리는 불편함을 느껴야한다)

 

시간을 재면서 func 의 작업을 하는 함수를 만들어 보자.

def func():
    time.sleep(random.random() * 1)

def get_time_func(func):

    def time_func():
        start = time.time()
        func()
        ellapsed_time = time.time() - start
        print(f"Ellapsed Time: {ellapsed_time:.2f} seconds")
    
    return time_func

time_func = get_time_func(func)
time_func()

get_time_func 으로 func 를 감싸면 func 의 작업도 수행하면서 시간을 재는 time_func 를 만들 수 있다.

 

이제 main 코드에서 시간을 재더라도 코드 구조가 바뀌지 않는다.

의식의 흐름처럼 왔지만

time_func = get_time_func(func)

해당 코드는

@get_time_func
def func():
    time.sleep(random.random() * 1)

func()

@get_time_func 을 함수 정의 위에 붙여주는 것으로 같은 기능을 할 수 있다.

이것이 데코레이터 이다.

 

데코레이터에는 중요한 작업을 맡기지 않는다.

예시로 보여준 시간 체크 같이 주요 로직에 직접적 관련이 없는 작업에 주로 사용하는 것이 좋은 것 같다.

(@classmethod 의 경우 인자 값 자체가 바뀌긴한다.)

 

장점으로는 메인 소스코드의 로직이 전혀 바뀌지 않는다는 것이다.

타입 체크 (type check)

파이썬은 타입에 관대하지만 가면 갈수록 타입이 중요하다.

타입을 강제화 하는 데코레이터를 만들어보자

def add_int(a: int, b: int) -> int:
    return a + b

이 함수는 오직 int 만을 위한 함수이다.

float 이 들어오기를 바라지 않는다.

그렇다면 인자의 type 이 int 가 아닌 경우 오류를 raise 해주는 장치가 필요하다.

def add_int(a: int, b: int) -> int:
    if not isinstance(a, int):
        raise TypeError("a 는 정수여야 합니다.")
    
    if not isinstance(b, int):
        raise TypeError("b 는 정수여야 합니다.")

    return a + b

그런데.. int만 받아야하는 함수가 100개라면 어떨까.

타입체크를 위해 추가된 4줄(공백제외)의 소스코드가 400줄이 되어 버릴 것이다.

 

데코레이터를 사용하자.

def only_int(func):
    def inner(a, b):
        if not isinstance(a, int):
            raise TypeError("a 는 정수여야 합니다.")
        
        if not isinstance(b, int):
            raise TypeError("b 는 정수여야 합니다.")
        
        return func(a, b)
    return inner

@only_int
def add_int(a: int, b: int) -> int:
    return a + b

print(add_int(1, 2))
# 3

print(add_int(1, 2.))
"""
Traceback (most recent call last):
  File ".\6.py", line 18, in <module>
    print(add_int(1, 2.))
  File ".\6.py", line 8, in inner
    raise TypeError("b 는 정수여야 합니다.")
TypeError: b 는 정수여야 합니다.
"""

고급 타입 체크 (advanced type check)

float 만 받고 싶다면 어떻게 해야할까

only_float 데코레이터를 만들어서 사용하면 되겠지..?

str 만 받고 싶다면?

타입도 지정할 수 있으면 좋을 것 같다.

 

다음과 같이 하면 된다.

def type_check(t: type):
    def inner1(func):
        def inner2(*args):
            for arg in args:
                if not isinstance(arg, t):
                    raise TypeError(f"argument 는 {t} 타입 이여야 합니다.")
            return func(*args)
        return inner2
    return inner1

@type_check(int)
def add_int(a: int, b: int) -> int:
    return a + b

print(add_int(1, 2.))
"""
Traceback (most recent call last):
  File ".\7.py", line 18, in <module>
    print(add_int(1, 2.))
  File ".\7.py", line 7, in inner2
    raise TypeError(f"argument 는 {t} 타입 이여야 합니다.")
TypeError: argument 는 <class 'int'> 타입 이여야 합니다.
"""

 

728x90
반응형

'지식 > Python' 카테고리의 다른 글

[Database] Transaction  (0) 2023.08.24
[python] Factory Pattern  (0) 2023.02.26

댓글