메타클래스(meta-class)?
- 메타클래스를 사용하면 파이썬의 class문을 가로채서 클래스가 정의될 때마다 특별한 동작을 제공할 수 있음
- 메타 클래스의 동적으로 애트리뷰트 접근을 커스텀해주는 내장기능을 통해 간단한 클래스를 복잡한 클래스로 쉽게 변환 가능
- 새로운 하위클래스가 정의될 때마다 검증코드를 수행하는 방법 제공
(문제제기) 메타클래스를 사용하면 클래스 생성을 다양한 방법으로 커스텀화할 수 있지만,
여전히 메타클래스로 처리할 수 없는 경우가 있습니다.
- (예) 어떤 클래스의 모든 메서드를 감싸서 메서드에 전달되는 인자, 반환 값, 발생한 예외를 모두 출력하고 싶다고 하자
- (다음 코드는 디버깅 데코레이터를 정의한 코드이다)
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
- 다음과 같이 데코레이터를 새 dict 하위 클래스에 속한 여러 특별 메서드에 적용할 수 있음
class TraceDict(dict):
@trace_func
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@trace_func
def __setitem__(self, *args, **kwargs)
return super().__setitem__(*args, **kwargs)
@trace_func
def __getitem__(self, *args, **kwargs):
return super().__getitem__(*args, **kwargs)
...
- TraceDict 클래스의 인스턴스와 상호작용해보면 메서드가 잘 데코레이션됐는지 확인할 수 있음
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass # 키 오류가 발생할 것으로 예상함
>>>
_ _init__(({'안녕': 1}, [('안녕', 1)]), {}) -> None
_ _setitem__(({'안녕': 1, '거기': 2}, '거기', 2), {}) -> None
_ _getitem__(({'안녕': 1, '거기': 2}, '안녕'), {}) -> 1
_ _getitem__(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) -> KeyError('존재하지 않음')
- (문제점) 꾸미려는 모든 메서드를 @trace_func 데코레이터를 써서 재정의해야 한다는 것입니다. -> (가독성X, 실수O)
-(해결 방법) 메타클래스를 사용해 클래스에 속한 모든 메서드를 자동으로 감싸는 것 입니다.
- (다음 코드는 새로 정의되는 타입의 모든 함수나 메서드를 trace_func 데코레이터로 감싸는 동작을 구현)
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 메타클래스를 사용해 dict 하위 클래스를 정의하고, 해당 클래스가 잘 작동하는지 확인합니다.
class TraceDict(dict, metaclass=TraceMeta):
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('존재하지 않음')
- 위 코드는 잘 동작함
- __new__ 호출도 제대로 출력함
그런데!!
상위 클래스가 메타클래스를 이미 정의한 경우, TraceMeta를 사용하면 어떤 일이 벌어질까요 ???
class OtherMeta(type):
pass
class SimpleDict(dict, metaclass=OtherMeta):
pass
class TraceDict(SimpleDict, metaclass=TraceMeta):
pass
>>>
Traceback ...
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
- (문제 - 오류) TraceMeta를 OtherMeta가 상속하지 않았으므로 오류가 발생함
- 이론적으로 메타클래스 상속을 활용해 TraceMeta를 OtherMeta가 상속하게 하면 문제를 해결할 수 있음
class TraceMeta(type):
...
class OtherMeta(TraceMeta):
pass
class SimpleDict(dict, metaclass=OtherMeta):
pass
class TraceDict(SimpleDict, metaclass=TraceMeta):
pass
trace_dict = TraceDict([('안녕', 1)])
trace_dict['거기'] = 2
trace_dict['안녕']
try:
trace_dict['존재하지 않음']
except KeyError:
pass # 키 오류가 발생할 것으로 예상함
>>>
__init_subclass__((), {}) -> None
__new__((<class '__main__.TraceDict'>, [('안녕', 1)]) -> {}
__getitem__(({'안녕': 1, '거기': 2}, '안녕'), {}) -> 1
__getitem__(({'안녕': 1, '거기': 2}, '존재하지 않음'), {}) -> KeyError('존재하지 않음') {})
하지만!!
- 라이브러리에 있는 메타클래스를 사용하는 경우, 코드를 변경할 수 없기 때문에 이 방법을 사용할 수 없음
- TraceMeta 같은 유틸리티 메타클래스를 여럿 사용하고 싶은 경우에도 사용할 수 없음
- (문제) 메타클래스를 사용하는 접근 방식은 대상 클래스에 대한 제약이 너무 많음
- (해결) 이런 문제를 해결하고자 파이썬은 클래스 데코레이터를 지원합니다.
- 클래스 선언 앞에 @기호와 데코레이터 함수를 적으면 됨
- 데코레이터 함수는 인자로 받은 클래스를 적절히 변경 및 재생성해야함
def my_class_decorator(klass):
klass.extra_param = '안녕'
return klass
@my_class_decorator
class MyClass:
pass
print(MyClass)
print(MyClass.extra_param)
>>>
<class '__main__.MyClass'>
안녕
TraceMeta.__new__ 메서드의 핵심 부분을 별도의 함수로 옮겨서
어떤 클래스에 속한 모든 메서드와 함수에 trace_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
이 데코레이터를 우리가 만든 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('존재하지 않음')
클래스를 확장하면서 합성이 가능한 방법을 찾고 있다면 클래스 데코레이터가 가장 적합한 도구 입니다.
### 기억해야할 내용 ###
- 클래스 데코레이터는 class 인스턴스를 파라미터로 받아서 이 클래스를 변경한 클래스나 새로운 클래스를
반환해주는 간단한 함수 입니다.
- 준비 코드를 최소화하면서 클래스 내부의 모든 메서드나 애트리뷰트를 변경하고 싶을 때 클래스 데코레이터가 유용합니다.
- 메타클래스는 서로 쉽게 합성할 수 없지만, 여러 클래스 데코레이터를 충돌 없이 사용해 똑같은 클래스를 확장할 수 있습니다.
## 동시성과 병렬성 ##
동시성(concurrency)이란?
컴퓨터가 같은 시간에 여러 다른 작업을 처리하는 것 처럼 보이는 것을 뜻함
- (예) CPU 코어가 하나뿐인 컴퓨터에서 운영체제는 유일한 프로세서 코어에서 실행되는 프로그램을 아주 빠르게 변경할 수 있음 -> 여러 프로그램이 번갈아가며 실행되면서 프로그램이 동시에 수행되는 것 같은 착각을 불러일으킴.
병렬성(parallelism)이란?
같은 시간에 여러 다른 작업을 실제로 처리하는 것을 뜻함
- CPU 코어가 여러 개인 컴퓨터는 여러 프로그램을 동시에 실행할 수 있다
- 각 CPU 코어는 서로 다른 프로그램의 명령어를 실행하기 때문에 각각의 프로그램이 같은 시점에 앞으로 진행될 수 있음
병렬성과 동시성의 가장 핵심적인 차이는 속도향상에 있음
- 어떤 프로그램의 서로 다른 두 실행 경로가 병렬적으로 앞으로 진행되면, 전체 작업을 수행하는데 걸리는 시간이 절반으로 줄어듬
- - -> 따라서, 실행 속도는 두 배 빨라짐
- 반대로, 수천 개의 서로 다른 실행 경로가 있는 동시성 프로그램은 겉으로 볼 때는 병렬적으로 실행되는 것처럼 보이지만,
전체 작업에 걸리는 시간을 빨라지지 않는다.
파이썬을 사용하면 다양한 스타일로 동시성 프로그램을 쉽게 작성할 수 있음
- 스레드(thread)는 상대적으로 적은 양의 동시성을 제공
- 코루틴(coroutine)은 수많은 동시성 함수를 사용할 수 있게 함
- 파이썬은 시스템콜(system call), 하위 프로세스(subprocess), C 확장(extension)을 사용해 작업을 병렬로 수행할 수 있음
- 하지만, 동시성 파이썬 코드가 실제 병렬적으로 실행되게 만드는 것은 매우 어려움
- 여러가지 상황에서 파이썬을 어떻게 사용하면 가장 좋을지 알아둬야함
'파이썬 코딩의 기술' 카테고리의 다른 글
파이썬코딩의기술-BW45 : 애트리뷰트를 리팩터링하는 대신 @property를 사용하라 (0) | 2023.03.21 |
---|---|
파이썬코딩의기술-BW44: 세터와 게터 메서드 대신 평범한 애트리뷰트를 사용하라 (0) | 2023.03.20 |
파이썬코딩의 기술 - BW50 : __set_name__으로 클래스 애트리뷰트를 표시하라 (0) | 2023.03.17 |
파이썬코딩의 기술 - BW38 : 간단한 인터페이스의 경우 클래스 대신 함수를 받아라 (0) | 2023.03.08 |
파이썬코딩의 기술 - BW 37: 내장 타입을 여러 단계로 내포시키보다는 클래스를 합성하라 (0) | 2023.03.04 |