반응형

Intro


안녕하세요, 오늘은 decorator에 대한 마지막 정리 글입니다. 앞선 글들을 통해 우리는 decorator란 무엇인지, 어떤 기능을 하는지, 그리고 @property, @staticmethod, @classmethod와 같은 기본 decorator들에 대해서 알게되었습니다.

이번 글은 decorator 정리에 대한 마지막 글로, 제가 프로그래밍하면서 마주쳤던 그 외 decorator인 dataclass와 functools에 대해서 정리하겠습니다.

@dataclass


@dataclass는 class를 선언할 때 class위에 붙여주는 decorator입니다. 이 decorator가 붙은 class는 dataclass가 되고, dataclass의 instance는 아래의 기능들을 수행할 수 있게 됩니다.

@dataclass를 사용하기 위해서는 dataclass 함수를 import 해줘야 합니다.

from dataclass import dataclass
  1. intance를 출력하면 갖고 있는 필드 값 나열 (repr)
  2. hashable해서 dict나 set에 추가 가능
  3. 대소관계 비교
  4. 필드 값 변경 방지

위의 기능을 무조건 구현하는 것은 아니고, @dataclass decorator의 인수를 조절해서 원하는 기능만 추가할 수 있습니다.
인수의 종류 및 기본 값은 아래와 같습니다.

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

  • init: __init__이 구현되어 있지 않은 경우에 instance를 생성할 때 받은 값들을 자동으로 instance의 필드 값으로 매핑해주는 __init__함수를 추가해줍니다.
  • repr: __repr__을 추가해줍니다. 이를 추가하면 instance를 print했을 때, instance가 갖고있는 필드 값들을 알 수 있습니다.
  • eq: __eq__를 추가해줍니다. 이를 추가하면 instance가 갖고있는 필드 값들이 모두 동일하면 instance 비교 연산 (==)에서 True를 반환합니다.
  • order: __lt__, __le__, __gt__, __ge__을 추가해줍니다. 이를 추가하면 instance사이의 필드 값에 기반한 대소관계 비교가 가능해집니다.
  • unsafe_hash: __hash__를 추가해줍니다. instance를 hashable하게 만들어줘서 dict나 set의 key로 사용될 수 있습니다.
  • frozen: instance의 필드를 불변하게 만듭니다.

이렇듯, @dataclass는 우리가 원하는 기능을 구현하기 위해서 번거롭게 반복해야 하는 단순한 로직들을, 손쉽게 추가할 수 있도록 돕습니다.

주의) 넘겨주는 인자의 조합, 또는 기존 class에 추가하고자 하는 기능을 담당하는 매직 메소드 (__~~~__)가 이미 추가되어있을 시, 에러가 발생할 수 있습니다. 이에 대해서는 python의 dataclass구현 코드에 적힌 doc string을 참고하셔서 미리 예방하는 것을 추천드립니다.

@dataclass와 관련된 code test는 이미 너무 잘 정리된 블로그 글을 발견해서, 해당 URL을 남기는 것으로 대체하도록 하겠습니다.
[파이썬] 데이터 클래스 사용법 (dataclasses 모듈)

functools


functools는 함수에 여러 기능을 부여해줄 수 있는 모듈입니다. 오늘은 functools가 제공해주는 여러 기능 중 cached, cached_property에 대해서 알아보겠습니다.

@cache decorator는 함수가 이전에 받았던 인수에 대한 결과값을 caching하는 것 입니다. 메모리는 좀 소모되겠지만, 중복 연산을 피할 수 있습니다.

피보나치 함수는 잘못 구현하면 내부에서 무수히 많은 recursive call이 발생해서 결과 값을 얻지 못할 수 있습니다. 그래서 이전에는 중복 연산이 발생하지 않도록 임의로 로직을 구현했지만, @cache를 사용하면 쉽게 해결할 수 있습니다.

아래는 예시입니다.

from functools import cache
import time

@cache
def fibonacci_cached(n: int) -> int:
    if n in [0, 1]:
        return 1
    else:
        return fibonacci_cached(n-1) + fibonacci_cached(n-2)

def fibonacci_uncached(n: int) -> int:
    if n in [0, 1]:
        return 1
    else:
        return fibonacci_uncached(n-1) + fibonacci_uncached(n-2)

num = 40
print(f"start fibonacci{num} w/ cache")
start = time.time()
print(fibonacci_cached(40))
end = time.time()
print(f"result: {end - start} sec\n")

print(f"start fibonacci{num} w/o cache")
start = time.time()
print(fibonacci_uncached(40))
end = time.time()
print(f"result: {end - start} sec")

결과는 아래와 같습니다. @cache가 붙은 fibonacci_cached()의 실행 결과가 훨씬 빠름을 알 수 있습니다.

start fibonacci40 w/ cache
165580141
result: 0.0 sec

start fibonacci40 w/o cache
165580141
result: 48.55391979217529 sec

@cached_property decorator는 @cache와 달리 이전의 결과 값들을 모두 저장하지 않습니다. @cached_property를 통해 caching 되는 것은 @cached_property가 붙은 함수의 이름과 결과값이, instance의 새로운 필드와 필드 값으로 caching됩니다. 그리고 역시 호출할 때 마다 새롭게 실행되지 않고, caching된 값을 반환합니다.

아래는 예시입니다.

from functools import cached_property
import time

import numpy as np

class DataSet:
    def __init__(self, sequence_of_numbers: np.ndarray):
        self._data = tuple(sequence_of_numbers)

    @property
    def stdev_property(self) -> np.float64:
        return np.std(self._data)

    @cached_property
    def stdev_cached_property(self):
        return np.std(self._data)

dataset = DataSet(np.random.rand(1000))

print('Before stdev_property')
print(dataset.__dict__.keys())
print('\nAfter stdev_property')
dataset.stdev_property
print(dataset.__dict__.keys())
print('---------------------------------------')
print('\nBefore stdev_cached_property')
print(dataset.__dict__.keys())
print('\nAfter stdev_cached_property')
dataset.stdev_cached_property
print(dataset.__dict__.keys())

print('---------------------------------------')
print('\nstdev_cached_property value before _data changed')
print(dataset.stdev_cached_property)
dataset._data = np.random.rand(100)
print('\nstdev_cached_property value After _data changed')
print(dataset.stdev_cached_property)

결과는 아래와 같습니다. @cached_property가 붙은 stdev_cached_property()의 실행 결과 instance의 새로운 필드로 추가된고, 이후에 호출하면 재 실행없이 caching된 값이 반환되는 것을 볼 수 있습니다.

Before stdev_property
dict_keys(['_data'])

After stdev_property
dict_keys(['_data'])
---------------------------------------

Before stdev_cached_property
dict_keys(['_data'])

After stdev_cached_property
dict_keys(['_data', 'stdev_cached_property'])
---------------------------------------

stdev_cached_property value before _data changed
0.289598527937108

stdev_cached_property value After _data changed
0.289598527937108

마무리


4개의 글에 거쳐서 decorator에 대해서 정리해봤습니다! 모든 decorator를 다루진 못했지만, decorator의 메카니즘에 대해서 이해할 수 있었던 계기였던 거 같습니다.

감사합니다.

References


Python 3.9 dataclass doc string
Python 3.9 - dataclass documentation
[파이썬] 데이터 클래스 사용법 (dataclasses 모듈)

반응형

+ Recent posts