python clean code chapter5
Python Clean Code
5. 데코레이터를 사용한 코드 개선
5-1. 파이썬의 데코레이터
함수와 메서드를 쉽게 수정하기 위해서, 예를 들어, original이라는 함수가 있고 그 기능을 약간 수정한 modifier라고 하는 함수가 있는 경우 다음과 같이 작성한다.
def original(...):
...
original = modifier(original)
함수에 변형을 할 때마다 modifier함수를 사용하여 함수를 호출한 다음 함수를 처음 정의한 것과 같은 이름으로 재할당해야 했다.
이는 혼란스럽고 오류가 발생하기 쉽고 번거롭다. 그래서 새로운 구문이 추가 되었다.
@modifier
def original(...):
...
데코레이터 이후에 나오는 것을 데코레이터의 첫 번째 파라미터로 하고 데코레이터의 결과 값을 반환하게 하는 문법적 설탕일 뿐이다.
문법적 설탕(syntax sugar): 어떤 언어에서 동일한 기능이지만, 타이핑의 수고를 덜어주기 위해 또는 읽기 쉽게 하기 위해 다른 표현으로 코딩할 수 있게 해주는 기능
파이썬에서는 modifier를 데코레이터, original을 데코레이팅된 함수 또는 래핑된 객체라 한다.
- 데코레이터 디자인 패턴과 혼동하면 안된다.
함수 데코레이터
def retry(operation):
@wraps(operation)
def wrapped(*arg,**kwargs):
last_rasied = None
RETRIES_LIMIT = 3
for _ in range(RETRIES_LIMIT):
try:
return operation(*args,**kwargs)
except:
...
raise last_raised
return wrapped
@retry
def run_operation(task):
return task.run()
@retry는 실제로 파이썬에서 run_operation = retry(run_operation)을 실행하게 해주는 문법적 설탕일 뿐이다.
클래스 데코레이터
함수에 적용한 것처럼 클래스에도 데코레이터를 사용헐 수 있다.
유일할 차이점은 데코레이터 함수의 파라미터롤 함수가 아닌 클래스를 받는다는 점이다.
어떤 개발자들은 클래스 데코레이터가 복잡하고 가독성을 떨어뜨릴 수 있다고 말할 수 있다.
클래스에서 정의한 속성과 메서드를 데코레이터 안에서 완전히 다른 용도로 변경할 수 있기 때문이다.
중첩 함수의 데코레이터
RETRIES_LIMIT = 3
def with_retry(retries_limit=RETRIES_LIMIT, allowed_exceptions=None):
allowed_exceptions or (ControlledException,)
def retry(operation):
@wraps(operation)
def wrapped(*args, **kwargs):
last_raised = None
for _ in range(retries_limit):
try:
return operation(*args,**kwargs)
except:
...
rasie lats_raised
return wrapped
return retry
@with_retry(retries_limit =5)
def run_with_custom_retries_limit(task):
return task.run()
데코레이터에 인자를 전달하기 위해 세 단계의 중첩된 함수가 필요.
데코레이터 객체
class WithRetry:
def __init__(self,retries_limit=RETRIES_LIMIT, allowed_exceptions=None):
self.retries_limit = retries_limit
self.allowed_exceptions = allowed_exceptions or (...)
def __call__(self, operation):
@wraps(operation)
def wrapped(*args,**kwargs):
last_raised = None
...
return wrapped
@withRetry(retries_limit=5)
def run_with_custom_retries_limit(task):
return task.run()
@ 연산 전에 전달된 파라미터를 사용해 데코레이터 객체를 생성한다.
__init__메서드에 정해진 로직에 따라 초기화 후 @연산이 호출된다.
데코레이터 객체는 다음 함수를 래핑 하여 __call__매직 메서드를 호출한다.
5-2.데코레이터 활용
- 파라미터 변환
- 코드 추적
- 파라미터 유효성 검사
- 재시도 로직 구현
- 일부 반복 작업을 데코레이터로 이동하여 클래스 단순화
흔한실수
def trace_decorator(function):
#@wraps(function) <-생략하는 경우 문제 functools.wraps 사용
def wrapped(*arg,**kwargs):
...
return wrapped
데코레이터 부작용 처리
데코레이터 함수가 되기 위해 필요한 조건은 가장 안쪽에 정의된 함수여야 한다는 것이다.
그렇지 않으면 임포트에 문제가 발생할 수 있다.
그럼에도 불구 하고 때로는 임포트 시에 실행하기 위해 이러한 부작용이 필요한 경우도 있고 반대의 경우도 있다.
래핑된 함수 바깥에 추가 로직을 구현하는 것은 좋지 않다.
결과를 의도하고 잘 사용하여야 한다.
어느 곳에서나 동작하는 데코레이터 만들기
데코레이터를 만들 때 일반적으로 재사용을 고려하여 함수뿐 아니라 메서드에도 동작하길 바란다.
*args, **kwargs 서명을 사용하여 데코레이터를 정의하면 모든 경우에 사용할 수 있다.
그러나 다음 2가지 이유로 함수의 서명과 비슷하게 데코레이터를 정의하는 것이 좋다.
- 원래의 함수 모양이 비슷하기 때문에 읽기가 쉽다.
- 파라미터를 받어서 뭔가를 하려면 *args, **kwargs를 사용하는 것이 불편하다.
그러나 함수에서 사용하던 데코레이터를 메서드에서 사용하려고 하면 문제가 생긴다. 메서드는 첫 번째 인자로 self를 받기 때문이다.
디스크팁터 프로토콜을 구현한 데코레이터 객체를 만들어야 한다. 예제.
from functool import wraps
from types import MethodType
class inject_db_driver:
def __init__(self, function):
self.function = function
wraps(self.function)(self)
def __call__(self,dbstring):
return self.function(DBDriver(dbstring))
def __get__(self,instance,owner):
if instance is None:
return self
return slef.__class__(MethodType(self.function,instance))
함수를 객체에 바인딩하고 데코레이터를 새로운 호출 가능 객체로 다시 생성한다.
5-3.데코레이터와 DRY원칙
데코레이터는 DRY원칙을 잘 따른다.
그러나 신중하게 설계되지 않은 데코레이터는 코드의 복잡성을 증가시킨다.
결론적으로 다음과 같은 사항을 고려했을 경우만 데코레이터 사용을 권한다.
-
처음부터 데코레이터를 만들지 않는다. 패턴이 생기고, 데코레이터에 대한 추상화가 명확해지면 그때 리팩토링을 한다.
-
데코레이터가 적어도 3회 이상 필요한 경우에만 구현한다.
-
데코레이터 코드를 최소한으로 유지한다.