들어가기 전
- 이 글은 이펙티브 파이썬을 공부하며 작성한 글입니다.
- 코드 : https://github.com/chickenchickenlove/effective-python/tree/master/item51
요약
- 클래스 데코레이터는 class 인스턴스를 파라미터로 받아서 이 클래스를 변경한 클래스나 새로운 클래스를 반환해주는 간단한 함수다.
- 준비 코드를 최소화하면서 클래스 내부의 모든 메서드나 어트리뷰트를 변경하고 싶을 때, 클레스 데코레이터가 유용하다.
- 메타 클래스는 서로 쉽게 합성할 수 없지만, 여러 클래스 데코레이터를 충돌 없이 사용해 똑같은 클래스를 확장할 수 있다.
- 클래스 전체적으로 데코레이터 사용하는 방법
- 클래스 함수마다 필요한 데코레이터 붙이기
- 가독성에 문제 있음 / 데코레이터 실수로 놓칠 수 있음 / 부모 클래스에 메서드가 추가되면, 데코레이터 누락됨.
- 메타 클래스를 이용해 데코레이터 붙이기
- 상속 구조에서 부모 / 자식 클래스가 각각 메타 클래스를 사용하는 경우 메타 클래스 Conflict 발생해서 확장이 제한됨.
- 클래스 데코레이터 이용해서 붙이기
- 위에서 이야기한 모든 단점들을 상쇄시킴.
- 클래스 함수마다 필요한 데코레이터 붙이기
Item 51. 합성 가능한 클래스 확장이 필요하면 메타클래스보다는 클래스 데코레이터를 사용하라.
클래스의 모든 메서드에 데코레이터를 적용하고 싶은 경우가 있다. 이럴 때는 아래 세 가지 방법을 고려해 볼 수 있다.
- 모든 메서드에 데코레이터 선언하기
- 메타 클래스를 이용해 데코레이터 붙이기
- 클래스 데코레이터를 이용해 데코레이터 붙이기
각 방법에는 장/단점이 존재할 것이지만 단점을 위주로 살펴보면, '클래스 데코레이터를 이용해 데코레이터 붙이기'가 가장 좋은 방법이다.
모든 메서드에 데코레이터 선언
- 데코레이터를 여기저기 많이 붙이니 코드 가독성이 떨어짐.
- 특정 함수에 데코레이터 붙이는 것을 까먹을 수 있음.
- 부모 클래스에 메서드가 추가되는 경우, 누락됨.
메타 클래스를 이용해 데코레이터 붙이는 것
- 부모 클래스, 자식 클래스가 서로 다른 메타 클래스를 사용하는 경우 Meta Class Conflict 발생.
- Meta 클래스 상속을 통해 해결할 수 있지만, 라이브러리 코드인 경우 불가능함.
이런 문제점들이 존재하는데, 클래스 데코레이터를 사용하면 이 모든 문제를 해결할 수 있게 된다. 따라서 클래스 차원에서 공통적으로 추가되어야 하는 데코레이터가 있다면, 클래스 데코레이터를 사용하도록 한다.
def my_class_decorator(klass):
print('데코레이터 펑션 호출')
klass.extra_param = '안녕'
return klass
@my_class_decorator
class MyClass:
print('클래스 호출')
pass
print('시작 전')
print(MyClass)
print(MyClass.extra_param)
클래스 데코레이터의 사용 예시는 위 코드를 참고하면 된다.
- Import 시점에 클래스 블록쪽이 먼저 호출된다.
- 클래스 블록이 호출되고 난 후에, 데코레이터 함수가 호출된다. 이 때, 클래스 전체가 전달된다.
- 데코레이터 함수에서 클래스를 반환해주면 된다.
클래스 호출
데코레이터 펑션 호출
시작 전
<class '__main__.MyClass'>
안녕
실행 결과는 다음과 같다. 클래스 호출 → 데코레이터 펑션 → ... 순으로 진행된다.
코드1. 데코레이터를 일일이 붙여주기
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}) -> {result!r}')
wrapper.tracing = True
return wrapper
class TraceDict(dict):
@trace_func
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@trace_func
def __getitem__(self, *args, **kwargs):
return super().__getitem__(*args, **kwargs)
@trace_func
def __setitem__(self, *args, **kwargs):
super().__setitem__(*args, **kwargs)
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass
위 코드의 설명은 다음과 같다.
- trace_func 라는 데코레이터를 선언한다. 이 함수는 디버깅용으로 사용된다.
- 파이썬 딕셔너리를 상속한 TraceDict를 선언한다. 이 때, 3개의 함수에 데코레이터를 추가했다.
- trace_dict를 호출했을 때, 디버깅이 적절히 이루어지는 것을 확인하고자 한다.
__init__(({'안녕': 1}, [('안녕', 1)]), {}) -> None
__setitem__(({'안녕': 1, '거기': 2}, '거기', 2), {}) -> None
__getitem__(({'안녕': 1, '거기': 2}, '안녕'), {}) -> 1
__getitem__(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) -> KeyError('존재하지 않음')
위 코드를 실행해보면, 로깅이 적절하게 남겨진 것을 확인할 수 있다. 그런데 이 때 어떤 문제점이 존재할까?
- 함수에 데코레이터를 빼먹고 안달 수도 있음.
- 데코레이터를 추가해야 할 함수가 많아지면 가독성이 떨어짐.
- 파이썬 dict 클래스에 새로운 메서드가 추가되는 경우, 데코레이터가 적용되지 않음.
이런 단점들이 존재하기 때문에 함수마다 데코레이터를 추가해주는 것은 간단한 대응책은 될 수 있지만, 좋은 대응책이 될 수는 없다.
코드2. 메타 클래스로 데코레이터 넣기
import types
from functools import wraps
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
def trace_func(func):
...
class TraceDict(dict, metaclass=TraceMeta):
pass
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass
위 코드는 메타 클래스를 이용해 데코레이터를 추가하는 방법이다.
- trace_types를 이용해 데코레이터를 추가할 타입을 선언한다.
- TraceDict 클래스 Import가 완료되면, TraceMeta.__new__()가 호출됨.
- TraceMeta.__new__()에 TraceDict의 클래스 딕셔너리(어트리뷰트 관련)가 전달되고, 여기서 trace_types에 맞는 인자들이 있으면 데코레이터를 한번 감싼다.
메타 클래스의 new() 메서드로 넘겨지는 인자 중 클래스 딕셔너리가 있는데, 클래스 딕셔너리는 부모의 메서드 / 어트리뷰트들이 항상 포함된다. 따라서 부모 클래스에 새로운 메서드가 추가되더라도 데코레이터를 누락없이 추가할 수 있게 된다.
__new__((<class '__main__.TraceDict'>, [('안녕', 1)]), {}) -> {}
__getitem__(({'안녕': 1, '거기': 2}, '안녕'), {}) -> 1
__getitem__(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) -> KeyError('존재하지 않음')
위 코드의 실행 결과는 다음과 같다.
- 함수에 데코레이터를 빼먹고 안달 수도 있음.
- 데코레이터를 추가해야 할 함수가 많아지면 가독성이 떨어짐.
- 파이썬 dict 클래스에 새로운 메서드가 추가되는 경우, 데코레이터가 적용되지 않음.
위에서 언급했던 이 문제들이 모두 해결되는 것이다. 그러나 메타 클래스를 이용한 데코레이터 추가에도 단점은 존재한다. 부모 클래스가 이미 메타 클래스를 사용하는 경우, 확장하기 어려워진다는 것이다.
class TraceMeta(type):
pass
class OtherMeta(type):
pass
class SimpleDict(dict, metaclass=OtherMeta):
pass
class TraceDict(SimpleDict, metaclass=TraceMeta):
pass
>>>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
메타 클래스를 이용해 데코레이터를 추가할 때의 명확한 한계는 다음과 같다.
- TraceMeta, OtherMeta가 메타 클래스로 존재함.
- SimpleDict는 OtherMeta를 메타 클래스로 사용.
- TraceDict는 SimpleDict를 상속받고, TraceMeta를 메타 클래스로 사용
이 경우, 위에서 볼 수 있듯이 실행 결과에서 'metaclass conflict'를 볼 수 있다.
class OtherMeta(TraceMeta):
pass
class SimpleDict(dict, metaclass=OtherMeta):
pass
class TraceDict(SimpleDict, metaclass=TraceMeta):
pass
OtherMeta 클래스가 TraceMeta 클래스를 상속받도록 해서 문제를 해결할 수는 있다. 그러나 해결할 수 없는 부분은 만약 이런 코드들이 특정 라이브러리의 코드라면, 개발자가 직접 수정할 수 없는 부분이기 때문에 확장에 한계가 있다는 것이다.
코드3. 클래스 데코레이터 이용하기
...
trace_types = (
types.MethodType,
types.FunctionType,
types.BuiltinFunctionType,
types.BuiltinMethodType,
types.MethodDescriptorType,
types.ClassMethodDescriptorType)
def trace_func(func):
...
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
@trace
class TraceDict(dict):
pass
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass
- 앞서 이야기했던 클래스 데코레이터 기능을 이용해 클래스 전체에 데코레이터를 적용한다.
- 클래스 메서드 중 어떤 메서드에 데코레이터를 적용할 지는 if isinstance(..) 문을 이용해서 처리한다.
클래스는 내부적으로 클래스 딕셔너리를 가지고 있고, 클래스 딕셔너리에서는 어트리뷰트와 메서드 등을 조회할 수 있다. trace_types에 명시된 타입들에만 데코레이터를 적용한다. 그리고 setattr()을 이용해 클래스 딕셔너리에 데코레이터로 한번 감싼 함수를 다시 배열하도록 한다. 이렇게 코드를 작성했을 때는 앞에서 이야기 했던 모든 단점을 극복할 수 있다.
- 가독성 문제 없음.
- 실수로 빼먹지 않을 수 있음.
- 부모 클래스에 새로운 메서드가 추가되어도 trace_types에 명시된 타입과 일치한다면, 놓치지 않고 업데이트 됨.
__new__((<class '__main__.TraceDict'>, [('안녕', 1)]), {}) -> {}
__getitem__(({'안녕': 1, '거기': 2}, '안녕'), {}) -> 1
__getitem__(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) -> KeyError('존재하지 않음')
위 코드의 실행 결과를 살펴보면 다음과 같다.
'프로그래밍 언어 > 파이썬' 카테고리의 다른 글
파이썬 3.11에 추가된 것들 살펴보기 (0) | 2024.02.17 |
---|---|
Effective Python Item 41. 기능을 합성할 때는 믹스인 클래스를 사용하라 (0) | 2024.02.16 |
Effective Python Item 47. 지연 계산 어트리뷰트가 필요하면 __getattr__, __getattribute__, __setattr__을 사용하라 (0) | 2024.02.12 |
Effective Python Item 39. @classmethod를 통해 클래스 다형성을 이용해라. (0) | 2024.02.05 |
Effective Python Item 38. 간단한 인터페이스의 경우 클래스 대신 함수를 받아라. (0) | 2024.02.04 |