프로그래밍 언어/Python

클래스를 디버깅해보자(feat.메타클래스,클래스 데코레이터)

JMDev 2023. 9. 15. 13:14

클래스를 디버깅 하기위해 구현하는 다양한 방법(함수 데코레이터, 메타클래스, 클래스 데코레이터)들을 사용해볼 수 있다.

해당 방법들을 사용하기 이전에 클래스를 디버깅하려면 어떻게 해야할까?

가장 쉬운 방법은 디버깅모드에 들어가서 break point를 찍는 방법이겠지만,

지금 구현하고 싶은 것은 해당 클래스들이 동작들 중에 생성되는 변수들을 print 찍는 것이 목표이다

 

클래스 내부에서 애트리뷰트를 변경하거나 받아갈 때 마다

디버깅할 수 있는 방법을 함수 데코레이터, 메타클래스를 활용하면서

점층적으로 리팩토링하면서 최종적으로는 클래스 데코레이터를 사용하여 리팩토링 하는 방법을 알아보자.

from functools import wraps

def trace_func(func):
    if hasattr(func, 'tracing'):  # Only decorate once
        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}')

    print(wrapper)
    wrapper.tracing = True
    return wrapper
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)
trace_dict = TraceDict([('hi', 1)])
trace_dict['there'] = 2
trace_dict['hi']
try:
    trace_dict['does not exist']
except KeyError:
    pass  # Expected
else:
    assert False

'''
__init__(({'hi': 1}, [('hi', 1)]), {}) -> None
__setitem__(({'hi': 1, 'there': 2}, 'there', 2), {}) -> None
__getitem__(({'hi': 1, 'there': 2}, 'hi'), {}) -> 1
__getitem__(({'hi': 1, 'there': 2}, 'does not exist'), {}) -> KeyError('does not exist')
'''

위 코드를 보면

클래스 내부 함수에 함수 데코레이터를 사용해 클래스를 디버깅할 수 있게 구현되어있다.

 

하지만 @trace_func 데코레이터가 중복으로 사용되고

또한 나중에 TraceDict의 부모클래스인 Dict 에 메서드가 추가될 시

수동으로 TraceDict에서 해당 메서드를 작업해야하는 상황 발생한다.

위 상황을 메타클래스로 클래스에 속한 모든 메서드를 자동 감싸는 방식으로 리팩토링이 가능하다.

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
'''
print( dir(klass) ) 하면 찍히는 값들

dir(klass) = ['__class__', '__class_getitem__', '__contains__',
 '__delattr__', '__delitem__', '__dict__', '__dir__', 
 '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
  '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', 
  '__ior__', '__iter__', '__le__', '__len__', '__lt__', '__module__',
   '__ne__', '__new__', '__or__', '__reduce__', '__reduce_ex__', '__repr__', 
   '__reversed__', '__ror__', '__setattr__', '__setitem__', '__sizeof__', '__str__', 
   '__subclasshook__', '__weakref__', 'clear', 'copy', 'fromkeys', 'get', 
   'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']

'''

지금 우리는 클래스가 생성되고, (get,set,init) 되는 과정을 디버깅하기 위해서

trace_func를 만들었고 이를 메타클래스로 리팩토링하는 코드를 위에서 확인해볼 수 있다.

dir(klass)이 반환하는 배열들을 이용하여 metaclass를 상속한 class의 value들을 가져올 수 있고,

가져온 value들이 우리가 디버깅을 원하는 값들이 반환되며,

그 반환값을 trace_func을 통해 디버깅할 수 있게 리팩토링하였다.

class TraceDict(dict, metaclass=TraceMeta):
    pass

trace_dict = TraceDict([('hi', 1)])
trace_dict['there'] = 2
trace_dict['hi']
try:
    trace_dict['does not exist']
except KeyError:
    pass  # Expected
else:
    assert False

이전에 데코레이터 방식과 동일한 결과값을 도출해낼 수 있다

하지만 이 과정속에서 TraceDict가 상속하는 Dict가 다른 metaclass를 상속하게 된다면 오류가 발생된다

# 상속과정 속 metaclass가 달라져버리면 오류가 발생한다.
try:
    class OtherMeta(type):
        pass
    
    class SimpleDict(dict, metaclass=OtherMeta):
        pass
    
    class TraceDict(SimpleDict, metaclass=TraceMeta):
        pass
except:
    print('except')
else:
    assert False

class TraceMeta(type):
    def __new__(meta, name, bases, class_dict):
        klass = type.__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 클래스를 바라보고 있어야 한다
class OtherMeta(TraceMeta):
    pass

class SimpleDict(dict, metaclass=OtherMeta):
    pass

class TraceDict(SimpleDict, metaclass=TraceMeta):
    pass

위 방식대로 진행하자면 적용 대상 클래스에 대한 제약이 많아져 문제가 발생한다.

이를 리팩토링할 수 있는 방법은 클래스 데코레이터를 사용하면된다.

클래스 데코레이터의 사용방법은 함수 데코레이터와 거의 차이가 없다.

클래스 데코레이터를 활용한 작은예제와 디버깅 클래스로 리팩토링하는 코드는 이렇다

# 클래스 데코레이터 예제코드

def add_attributes(cls):
    cls.new_attribute = "I'm a new attribute of the class"
    return cls

@add_attributes
class MyClass:
    def __init__(self, data):
        self.data = data

obj = MyClass("Hello")
print(obj.new_attribute)  # "I'm a new attribute of the class"
# 클래스 데코레이터를 활용한 디버깅 클래스 리팩토링코드
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

# Example 10
@trace
class TraceDict(dict):
    pass

trace_dict = TraceDict([('hi', 1)])
trace_dict['there'] = 2
trace_dict['hi']
try:
    trace_dict['does not exist']
except KeyError:
    pass  # Expected
else:
    assert False