` 1

파이썬 문법 중 하나인 데코레이터(Decorator)는 wrapping 방식을 통해 함수를 유연하게 적용하여, 특정 기능을 편리하게 사용하게 해줍니다.

아래처럼 특수기호 @를 붙여서 사용하는 방식이 데코레이터입니다.

@tf.function
def train_step(args):
    pass

글에서는 데코레이터의 개념, 동작방식이 아닌 몇 가지 유용하게 사용되는 또는 사용될만한 데코레이터를 알아보도록 하겠습니다.


@retry

@retry 데코레이터는 특정 예외가 발생한 경우, 설정한 횟수만큼 재시도하는 데코레이터입니다.

일반적인 상황에서는 잘 사용되진 않지만, 네트워크, DB 통신에서 유용하게 사용되는 함수입니다.

retrying 라이브러리나 직접 구현하여 사용하는 방법이 있지만, tenacity 라이브러리가 유명하게 사용됩니다. 아래 코드는 tenacity 공식 문서에 나와있는 예제입니다.

  - tenacity 공식 문서: https://tenacity.readthedocs.io/en/latest/

import random
from tenacity import retry

@retry
def do_something_unreliable():
    if random.randint(0, 10) > 1:
        raise IOError("Broken sauce, everything is hosed!!!111one")
    else:
        return "Awesome sauce!"

print(do_something_unreliable())

성공할때까지의 시도, wait 횟수 등도 parameter를 통해 조절할 수 있게 제공해줍니다.

@classmethod

@classmethod는 아래 @staticmethod와 함께 보면 좋을 것 같습니다.

어느정도 자바/C 언어를 경험한 분들이라면 클래스 또는 정적 변수에 익숙할 것입니다. 그 개념 그대로 파이썬에서도 클래스 변수, 클래스 메소드를 사용할 수 있습니다.

단일 클래스나 상속받은 클래스의 변수를 조정할 때 유용하게 사용됩니다.

암묵적으로 @classmethod 데코레이터를 사용하면 첫 번째 인자로 cls 파라미터를 사용합니다. 인스턴스 메소드라 불리우는 self와 비슷한 개념입니다. 우리는 cls 파라미터를 통해 클래스 변수에 접근할 수 있게 됩니다.

위에서 언급하였듯이 @classmethod를 통해 클래스 변수를 조정할 수 있고, upgrade_os 메소드처럼 새로운 생성자를 만들어 줄 수도 있습니다.

class Computer:
    os = 'Linux' # 클래스 변수
    
class Personal_Computer(Computer):
    def __init__(self, c_id, pos):
        self.c_id = c_id
        self.partial_os = pos
        
    @classmethod
    def change_os(cls, this_os):
        if cls.os != this_os:
            cls.os = this_os
        else:
            print(f'{cls.os} Already up-to-date!')
            
    @classmethod
    def upgrade_os(cls, c_id, new_os):
        return cls(c_id, new_os)
            
computer_1 = Personal_Computer('2021', 'window11')
computer_2 = Personal_Computer('2020', 'window10')

# 변경 전, Linux -> Window
print(f'os change Before: {computer_1.os}, {computer_2.os}')
Personal_Computer.change_os(this_os = 'Window')

# 변경 후
print(f'os change After: {computer_1.os}, {computer_2.os}', end = '\n\n')

# upgrade os
print(f'os upgrade Before: {computer_2.partial_os}')
upgraded_computer_2 = computer_2.upgrade_os(computer_1.c_id, 'window11')
print(f'os change After: {upgraded_computer_2.partial_os}')

@staticmethod

정적 메소드입니다. 이 데코레이터를 사용하면 아래 코드처럼 객체 생성없이 바로 클래스를 통해 접근하여 사용이 가능합니다. 유용해보이지만, 많이 사용하지 않는 기능입니다.

class Computer:
    os = 'Linux' # 클래스 변수
    
class Personal_Computer(Computer):      
    ..생략..
    
    @staticmethod
    def print_os():
        print(Computer.os)

Personal_Computer.print_os() # Linux

@property

@property 데코레이터는 getter/setter를 떠올리면 쉽다. 아래 코드처럼 사용 가능합니다.

@property는 getter, @name.setter는 setter 역할을 합니다.

class ERP:
    def __init__(self):
        self._salary = 100
    
    @property
    def salary(self):
        return self._salary
    
    @salary.setter
    def salary(self, value):
        if self._salary < 500:
            self._salary = value
        else:
            raise ValueError('No!')
        
erp = ERP()
print(erp.salary) # 100
erp.salry = 200
print(erp.salry) # 200

물론 파이썬 특성상 이렇게 하지않아도 변수에 자유롭게 접근하거나 새로운 값을 할당할 수 있습니다.
다만 파이썬에서는 언더바('_')를 활용해서 private/protected 의미를 부여하는데요.

'_name' 은 private, '__name'은 protected를 암묵적으로 의미합니다.

언더바를 사용하면 우리가 흔히 사용하는 기능인 객체 메소드 참조(예를 들면 Tab키를 눌러서 어떤 함수가 확인하는)가 되지 않습니다. 참조하려면 언더바를 명시적으로 붙여주어야 합니다.

뿐만 아니라 @name.setter의 기능은 당연히 setter와 같지만, 위의 예제처럼 변수 할당에 조건을 편리하게 걸 수 있다는 장점이 있습니다.

@dataclass

Python 3.7이상 버전부터 사용할 수 있는 @dataclass 데코레이터는 __init____eq____repr__ 등 함수를 자동으로 등록해주는 기능입니다.

from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int
        
person1 = Person('kim', 10)
person2 = Person('kim', 10)

print(person1.name, person1.age)
print(person1 == person2)

위처럼 별도의 함수를 정의해주지 않아도 바로 사용할 수 있습니다.

@lru_cache

@lru_cache는 메모라이제이션(memorization) 기능을 의미합니다.

이 데코레이터를 사용하면 함수의 반환값을 max_size 크기까지 값을 저장할 수 있습니다.

from functools import lru_cache
import urllib

@lru_cache(maxsize = 32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = 'https://www.python.org/dev/peps/pep-%04d/' % num
    try:
        with urllib.request.urlopen(resource) as s:
            return s.read()
    except urllib.error.HTTPError:
        return 'Not Found'
    
id_check = []
for i in [8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991]:
    pep = get_pep(i)
    
    if i == 8:
        id_check.append(pep)
    
print(get_pep.cache_info()) # 총 hits 3 -> 320 2번, 8 1번 hit
assert id_check[0] is id_check[1] # id 비교, @lru_cache 사용안할 시 에러 발생!

@lru_cache 기능으로 인해 id_check에 들어가는 객체 id가 동일해서(캐시를 이용하기 때문) AssertionError가 발생하지 않습니다