decorator 운영 코드 예시

Kanto(칸토)·2024년 7월 7일
0

프로그래밍

목록 보기
2/5

고민 하던 구현을 쉽게 해결 해준 decorator 실전 코드를 공유합니다.

개발 상황

프로젝트 개발 중 if 문으로 이미 복잡해진 코드를 더 복잡하게 만들고 싶지 않아서 급기야 클래스를 만들려던 차였습니다. 하지만 아무래도 클래스를 만드려면 개발해야 하는 코드 양이 늘어나는 것도 있고 factory 패턴 이외에 딱히 생각나는 것도 없었기에 decorator로 방향을 바꾸게 되었습니다.

decorator를 사용하니 품을 덜 들이고 깔끔한 코드를 만드는데 도움이 된 것 같습니다.

기존 코드

처음에 가지고 있었던 코드의 모양은 if 조건에 따라서 세 가지 다른 결과를 내보내고 있었습니다. 그런데 각 if 문의 return 결과에 다른 조작을 추가로 해야 된다고 해봅시다. 즉, 'foo' 인 경우 ["a", "b", "c"] 에 더해서 ["D", "E", "F"] 와 같은 추가 리스트를 더해서 반환해야 하는 것입니다.
마찬가지로 'bar' 인 경우에도 ["x", "y", "z"]에 더해서 ["L", "M", "N"] 를 더해주는 결과를 내보내야 합니다.

즉, 처음에 클래스를 만들고 싶었던 이유는 기존 오퍼레이션에다가 의미가 조금 다른 (성격이 다른 추가적인 리스트 값을 더해줘야 하는) 오퍼레이션을 섞어야 해서 함수의 목적성이 흐릿해진다는 점이었습니다. 그리고 장기적으로는 각 분기에서 갈라지는 객체 name 의 종류에 따라서 나중에 각기 다른 operation을 다형적으로 구현해야할 가능성도 배제할 수 없었습니다.

def new_operation(_list):
    return [elem.upper() for elem in _list]

def get_tables(name):
    if name == 'foo':
        return ["a", "b", "c"] + list(map(new_operation,["d","e","f"]))
      
    elif name == 'bar':
        return ["x", "y", "z"] + list(map(new_operation,["l","m","n"]))
    
    elif name == 'elk':
        return ["p", "q", "r"] + list(map(new_operation,["s","t","u"]))

구현 예시

하지만 결론적으로는 클래스를 만들지 않고 decorator를 이용하여 new_operation을 다음과 같이 구현하였습니다.

def new_operation(func):
    def wrapper(*args, **kwargs):
        tables = func(*args, **kwargs)
        tables += list(
            map(str.upper, tables)
        )
        return tables
    return wrapper

@new_operation
def get_tables(name):
    if name == 'foo':
        return ["a", "b", "c"]
      
	elif name == 'bar':
        return ["d", "e", "f"]
    
    elif name == 'elk':
        return ["x", "y", "z"]

구현 해설

  1. new_operation을 decorator로 구현하기 위해서 wrapper 함수를 작성하였습니다. 인자로 받은 함수 func 의 return을 tables로 받은 뒤, 여기에서 원하는 operation을 진행합니다. tables += list( map(str.upper, tables) ) 그리고 최종 결과를 return 합니다.

  2. wrapper 함수의 인자로는 *args , **kwargs 를 전달하는데, 이는 함수의 인자였던 name을 받기 위함 입니다. 위 코드에서는 get_tables 함수를 호출 할 때 전달한 str 인자 (i.e. 'foo') 가 전달될 것입니다.
    프로젝트 코드에서는 get_tables에 추가 인자를 하나 더 넣어야 했는데, 그 인자는 get_tables에서 쓸 목적이 아니라 wrapper 안에서 써야 하는 용도였습니다.(i.e. new_operation을 실행할지 말지 결정하는 bool 인자) 이런 식으로 본체 함수에서 쓰지 않지만 wrapper에서 쓸 목적으로 인자를 추가하는 것이 올바른 practice 인지 모르겠습니다. 왜냐면 함수의 signiture 가시성이 떨어지기 때문입니다 (i.e .lint 검사가 실패함)

(부가정보) 3. decorator로 wrapping을 할 경우 기존 함수가 wrapper함수로 대체되어버리는 불편함이 있습니다. 기존 함수의 정보를 살리기 위해 from functools import wraps를 활용할 수 있습니다. 참고
하지만 파이썬 3.10.12(colab jupyter) 에서 테스트 해봤을 때에는 wraps를 사용하지 않고도 기존 함수의 정보를 그대로 유지할 수 있는 것으로 보입니다. 뭔가 변경이 있었을 수도 있습니다.

to be continued..

class를 활용하여 위 문제를 해결하는 방법도 실제 코드 기반으로 구현하게 되면 기록해두겠습니다.

profile
통계학으로 사람들의 행동을 이해하고 싶습니다.

0개의 댓글