메타클래스를 사용하면 클래스 생성을 다양한 방법으로 커스텀화할 수 있지만, 여전히 메타클래스로 처리할 수 없는 경우가 있다.
#전달 인자, 반환 값, 예외 출력
#디버깅 데코레이터 정의
"""
아래의 식을 활용하면, 모든 메서드를 @trace_func 데코레이터를 써서 재정의
불필요한 중복으로 인해서 가독성 저하및 실수가 쉬워짐
"""
from functools import wraps
def trace_func(func):
if hasattr(func, 'tracing'): #단 한 번만 데코레이터 적용
return func
@wraps(func)
def wrapper(*args, **kwargs):
result = None
try:
result = func(*args, **kwargs)
return result
except Exception as e:
result = e
raise
finally:
print(f'{func.__name__}({args!r}, {kwargs!r}) ->'
f'{result!r}')
wrapper.tracing = True
return wrapper
#해결 방법: 메타클래스를 사용해서 클래스에 속한 모든 메서드 자동으로 감싸기
import types
trace_types = (
types.MethodType,
types.FunctionType,
types.BuiltinFunctionType,
types.BuiltinMethodType,
types.MethodDescriptorType,
types.ClassMethodDescriptorType)
class TraceMeta(type):
def __new__(meta, name, bases, class_dict):
klass = super().__new__(meta, name, bases, class_dict)
for key in dir(klass):
value = getattr(klass, key)
if isinstance(value, trace_types):
wrapped = trace_func(value)
setattr(klass, key,wrapped)
return klass
#TraceMeta는 OhterMeta가 상속하지 않으므로 오류 발생
#메타클래스 상속 활용해도 코드 변경을 못한다.
# 클래스 데코레이션으로 위와 같은 문제
#클래스 선언 앞에 @기호와 데코레이터 함수 적기
#데코레이터 함수는 인자로 받은 클래스를 적절히 변경하여 재생성
#코드도 짧다
def my_class_decorator(klass):
klass.extra_param = '안녕'
return klass
@my_class_decorator
class Myclass:
pass
def trace(klass):
for key in dir(klass):
value = getattr(klass, key)
if isinstance(value, trace_types):
wrapped = trace_func(value)
setattr(klass, key, wrapped)
return klass
#위의 데코레이터를 우리가 만든 dict 하위 클래스 적용하면
#메타클래스 쓴 것과 같은 결과
@trace
class TraceDict(dict):
pass
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass #키 오류가 발생할 것을 예상
__new__((<class '__main__.TraceDict'>, [('안녕', 1)]), {}) ->{}
__getitem__(({'안녕': 1, '거기': 2}, '안녕'), {}) ->1
__getitem__(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) ->KeyError('존재하지 않음')
#데코레이션을 적용할 클래스에 이미 메타 클래스가 있어도 데코레이터 사용 가능
#이런 식으로 클래스 합성 및 확장이 일어남
class OtherMeta(type):
pass
@trace
class TraceDict(dict, metaclass = OtherMeta):
pass
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass
코드를 입력하세요
new((<class 'main.TraceDict'>, [('안녕', 1)]), {}) ->{}
getitem(({'안녕': 1, '거기': 2}, '안녕'), {}) ->1
getitem(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) ->KeyError('존재하지 않음')
<br>
### Summary
- 클래스 데코레이터는 class 인스턴스를 파라미터로 받아서 이 클래스를 변경한 클래스나 새로운 클래스를 반환해주는 간단한 함수
- 준비 코드를 최소화하면서 클래스 내부의 모든 메서드나 애트리뷰트를 변경하고 싶을 때 클래스 데코레이터가 유용
- 메타클래스는 서로 쉽게 합성할 수 없지만, 여러 클래스 데코레이터를 충돌없이 사용해서 똑같은 클래스를 확장