본문 바로가기

파이썬 코딩의 기술

파이썬코딩의기술 - BW51 : 합성 가능한 클래스 확장이 필요하면 메타클래스보다는 클래스 데코레이터를 사용하라

메타클래스(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)을 사용해 작업을 병렬로 수행할 수 있음

- 하지만, 동시성 파이썬 코드가 실제 병렬적으로 실행되게 만드는 것은 매우 어려움

- 여러가지 상황에서 파이썬을 어떻게 사용하면 가장 좋을지 알아둬야함