Decorator로 함수 실행시간 기록하기
Decorator로 함수 실행시간 로깅하고, 이메일 알람보내기
데이터 전처리나 학습을 시켜놓고 며칠 뒤 결과를 확인하는 경우가 많은데, 종종 중간에 작업이 실패하기도 하고 생각보다 빨리 끝나있을 때도 있다.
만약 돌려놓은 함수가 끝났는지, 끝났다면 성공적으로 끝났는지 실패했는지 바로 알 수 있다면, 다음 작업을 이어서 수행하거나 디버깅후 재실행 할 수 있을 것이다.
이번 글에서는 함수의 실행시간을 기록하고, 나아가 이메일 알림까지 보내주는 데코레이터를 만들어본다.
1단계, 함수 실행시간을 기록해주는 아주 간단한 데코레이터를 만들어보자.
from datetime import datetime
from functools import wraps
def timed(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = datetime.now()
result = func(*args, **kwargs)
end = datetime.now()
print(f'Success. {end-start} taken for {func.__name__}')
return result
return wrapper
@timed
def foo():
print('Hello World')
>>> foo()
Hello World
Success. 0:00:00.000023 taken for foo
decorator를 사용할 때에는 functools.wraps 으로 wrapper를 감싸지 않으면 함수의 메타정보를 잃어버리게되므로 주의한다.
def nothing(func):
def func_name_will_be_overwritten(*args, **kwargs):
return func(*args, **kwargs)
return func_name_will_be_overwritten
@nothing
def foo():
print('Hello World')
>>> print(foo.__name__)
func_name_will_be_overwritten
2단계, 별도의 로거를 파라메터로 받아 로깅한다.
파라메터를 별도로 받는 decorator를 작성하려면 한겹 더 감싸준다.
def timed(logger=None, level=10):
log = print
if isinstance(logger, logging.getLoggerClass()):
log = lambda m: logger.log(level, m)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = datetime.now()
result = func(*args, **kwargs)
end = datetime.now()
log(f'Success. {end-start} taken for {func.__name__}')
return result
return wrapper
return decorator
@timed(logger)
def foo():
print('Hello World')
>>> foo()
Hello World
2018-07-08 16:02:23,926 DEBUG 4:test.py - Success. 0:00:00.000025 taken for foo
이렇게 별도의 파라메터를 받는 데코레이터는 안타깝게도 항상 () 가 필요하다. 기본 파라메터를 사용할 때엔 괄호 없이 사용하고 싶다면 아래와 같이 편법을 사용하는 수 밖에 없다.
def decorator_with_optional_parameter(param=None):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(param)
return func(*args, **kwargs)
return wrapper
if callable(param): # params가 callable이면 ()없이 쓰여서 함수가 바로 들어왔다고 가정한다.
return decorator(param)
else:
return decorator
@decorator_with_optional_parameter()
def foo():
print('Hello')
@decorator_with_optional_parameter
def bar():
print('World')
>>> foo()
None
Hello
>>> bar()
<function bar at 0x10778ea60>
World
3단계, caller, parameter와 같은 좀 더 많은 정보를 기록한다.
from inspect import getframeinfo, currentframe
def timed(logger=None, level=10):
log = print
if isinstance(logger, logging.getLoggerClass()):
log = lambda m: logger.log(level, m)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 파라메터 정보를 출력하기 위한 정보를 구성한다.
parameters = [repr(x) for x in args]
if kwargs:
parameters.append(repr(kwargs))
funcname = f"{func.__name__}({', '.join(parameters)})"
# caller 정보를 출력한다.
caller = getframeinfo(currentframe().f_back)
if caller:
log(f'{funcname} is called by {caller.function} in {caller.lineno}:{caller.filename}')
else:
log(f'{funcname} is called')
# 시간을 기록하고 함수를 실행한다.
start = datetime.now()
result = func(*args, **kwargs)
end = datetime.now()
log(f'Success. {end-start} taken for {funcname}')
return result
return wrapper
return decorator
@timed()
def foo(name):
print(f'Hello World {name}')
>>> foo('John')
foo('John') is called by <module> in 40:test.py
Hello World John
Success. 0:00:00.000013 taken for foo('John')
4단계, 함수 성공/실패시 이메일을 보내도록 한다.
import traceback
def timed(logger=None, level=10, notify=False):
log = print
if isinstance(logger, logging.getLoggerClass()):
log = lambda m: logger.log(level, m)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 파라메터 정보를 출력하기 위한 정보를 구성한다.
parameters = [repr(x) for x in args]
if kwargs:
parameters.append(repr(kwargs))
funcname = f"{func.__name__}({', '.join(parameters)})"
# caller 정보를 출력한다.
caller = getframeinfo(currentframe().f_back)
if caller:
log(f'{funcname} is called by {caller.function} in {caller.lineno}:{caller.filename}')
else:
log(f'{funcname} is called')
# 시간을 기록하고 함수를 실행한다.
start = datetime.now()
# 함수의 성패에 따라 적절한 메일을 보낸다.
try:
result = func(*args, **kwargs)
except Exception as e:
end = datetime.now()
msg = f'Fail. {end-start} taken for {funcname}: '
log(msg, e)
send_email(msg, traceback.format_exc())
raise e
end = datetime.now()
msg = f'Success. {end-start} taken for {funcname}'
log(msg)
send_email(msg, str(result))
return result
return wrapper
return decorator
@timed()
def foo(name):
print(f'Hello World {name}')
이메일을 보내는 함수는 대충 이렇게 작성한다.
import smtplib
from email.mime.text import MIMEText
def send_email(sender, receivers, subject, content):
try:
msg = MIMEText(content.encode('utf-8'), 'html', _charset='utf-8')
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = ', '.join(receivers)
s = smtplib.SMTP('localhost')
s.sendmail(sender, receivers, msg.as_string())
s.quit()
print('Successfully sent the mail to %s' % msg['To'])
except Exception as e:
print('Fail to send the mail.', e)
>>> foo()
foo() is called by <module> in 71:test.py
Fail. 0:00:00.000012 taken for foo(): foo() missing 1 required positional argument: 'name'
Successfully sent the mail to receiver1, receiver2
Traceback (most recent call last):
File "test.py", line 71, in <module>
foo()
File "test.py", line 57, in wrapper
raise e
File "test.py", line 51, in wrapper
result = func(*args, **kwargs)
TypeError: foo() missing 1 required positional argument: 'name'