흔한생각 (recording)/Dynamo

파이썬 함수 데코레이터를 이용해서 함수 범용성 높이기(리스트 레벨구조 기능 추가)

hnanmal 2022. 5. 27. 01:58
300x250

다이나모 파이썬 스크립트에서 리스트 레벨 구조 기능 추가하기(feat. Python decorator function)

파이썬은 동적 타입을 지원하기 때문에 함수 인자들의 자료형을 그렇게 신경 쓰면서 함수를 디자인하지 않아도 된다는 장점(?) 이 있습니다.

그래서 프로그래밍을 처음 접했을 때 진입장벽이 낮고, 배우기 쉽습니다. 게다가 타입에 신경쓰지 않는 상황이라면 상대적으로 규모가 작은 코딩인 경우가 많으니, 소규모의 스크립팅 목적으로도 파이썬이 참 적당합니다.

그래서 비주얼 프로그래밍인 다이나모에서도 파이썬 스크립트 노드가 자주 활용됩니다.

그런데 단일개체를 입력받아서 결과를 반환하는 함수를 만들고 나면,
리스트와 같은 다중 항목들을 처리하기 위해서는 만든 함수를 가지고 for 문을 구성해야 하는데 이게 좀 귀찮습니다.


파이썬의 데코레이터

그런데 파이썬에서 제공하는 기능 중에는 함수의 데코레이터라는 것이 있습니다.
데코레이터라는 것을 제대로 설명하려면 말이 길어지겠지만, 간단히 설명하자면,

어떤 함수를 수정하지 않고도 그 함수에서 추가적인 기능을 구현하고자 할 때 사용하는 것


이라고 할 수 있습니다.
말만 들어서는 당연히 감이 잘 안 오실 테니, 예를 들어볼게요.


  • 단일 객체
  • 1차원 리스트
  • 다중 리스트


이렇게 함수의 재료가 될 인자를 3가지 방식으로 미리 준비합시다.


위 그림처럼 파이썬 스크립트로 함수 test_function1을 만들고, 인자로 받은 숫자에 2를 곱해 반환하는 함수로 작성해봅시다.
단일 개체인 3을 연결해주면, 6이라는 결괏값이 잘 나오는 걸 볼 수 있네요.


그런데 바로 밑에 있는 [1, 2, 3, 4, 5]라는 리스트를 연결해주면, 결괏값이 요지경이 됩니다.
각 원소의 값을 2배하는 것이 아니고, 리스트에 있는 원소들을 순차적으로 2번 반복한 새로운 리스트를 반환해 주죠? 원하는 바가 절대 아닙니다..


복잡한 다중구조의 리스트를 연결하면 결과물은 더더욱 미궁 속으로 빠지게 됩니다.
물론 이 문제를 해결하기 위해 test_function의 코드를 수정하는 것도 가능합니다.


자 이렇게 수정된 내용의 test_function2를 사용하면, 함수 인자로 리스트가 들어왔을 때 결괏값을 의도대로 반환해 줍니다.

하지만, 이번에는 단일개체를 인자로 집어넣으면 함수가 다시 오류를 토해내게 되는군요. 예외 처리하려면 함수의 내용을 또 고쳐야 합니다..

데코레이터로 구현하는 함수인자 처리


이제 decorator를 한번 써볼까요?


위 그림의 적색 상자처럼 iterize라는 함수를 하나 선언해보겠습니다..

코드의 내용을 간략히 설명하자면,

  1. 먼저 함수 내부에서 또다시 wrapper라는 함수를 정의합니다.
  2. 그리고 추가 기능을 구현할 대상함수자체를 인자로 받아들여서,
  3. 대상 함수가 품은 인자의 타입이 리스트이면 각 원소를 인자로 갖는 wrapper함수를 다시 호출해주고,
  4. 인자의 타입이 단일 개체라면 대상 함수를 해당 인자에 적용한 결과를 반환하도록 구성되어 있습니다.


말이 복잡하죠? 재귀함수의 형태로 구성되어서 설명이 좀 깔끔하지 않습니다.

재귀 함수에 대해서, 그리고 파이썬에서의 재귀 함수에 대해서는 할 말이 많으니, 나중에 또 따로 포스팅하겠습니다..

지금은 그냥 “어떤 대상 함수를 집어넣어 작동하는 iterize라는 함수를 만들었고, iterize함수는 대상 함수가 가지고 온 값이 리스트라면, 그 리스트를 파헤치도록 만들어져 있다.”라고 이해하세요.


기존 함수에 데코레이터를 적용해주면 행동방식이 바뀐다


데코레이터의 준비물을 만들었으니, 이제 아까 만들었던 test_function1 함수에 데코레이터를 적용해 봅시다. 적용하는 법은 간단합니다.



위 그림처럼 test_function1 함수의 선언부 바로 위에다가 제가 뭔가를 적어놓았죠?
@뒤에 데코레이터 함수의 이름을 적으면 모든 것이 끝납니다.
이제 함수 개조가 끝난 거예요!

@iterize라고 적은 한 줄이, 이제부터 test_function 함수의 작동방식을 살짝 바꾸어 줍니다.
처음과는 다르게 리스트를 함수에 전달해도, 각각의 원소에 2를 곱해서 돌려주고 있죠?


함수 내부를 직접 수정한 test_function2 함수와 다른 점은, 이제부터 test_function1 함수는 함수 인자로 단일 개체인 3을 전달해도 오류가 발생하지 않고, 정확히 2배인 6을 반환해 준다는 점입니다.

신기하지 않나요?


중첩되고 중첩되어 내부구조가 복잡한 3번 리스트의 경우에도 각각의 원소에 2를 곱하고, 기존 리스트의 구조는 유지한 채로 결괏값을 반환해 주고 있습니다!!




























레빗 객체들도 이 방식으로 컨트롤될까?


단순히 숫자들로 이루어진 리스트 말고, 레빗 객체로 이루어진 리스트로도 한번 해볼까요?



먼저 레빗 샘플 파일 하나 열어주고요.



위 그림처럼 코드를 한번 작성해줍시다.
샘플 프로젝트에 배치된 모든 창문을 가져온 뒤에, Geometry를 뽑아보겠습니다.
단일 객체를 인자로 주었을 때, 당연히 잘 작동됩니다.




이번에는 창문들로 이루어진 리스트를 전달했을 때의 결과입니다.
인자의 자료형이 달라졌지만 함수는 훌륭하게 제 역할을 계속 수행하고 있군요!

def iterize(function):
    def wrapper(args):
        if isinstance(args, list) or isinstance(args, set) or isinstance(args, tuple) or isinstance(args, dict):
            return map(wrapper, args)
        else:
            return function(args)
    return wrapper


@iterize
def 사용자정의함수(x):
    x어쩌고 저쩌고~~


실습에 사용한 코드는 위와 같습니다.

다만 매번 복사해서 붙여 넣기가 귀찮다면, 다이나모 파이썬 노드에 템플릿 기능으로 해당 코드를 처음부터 가지고 노드가 생성되게 할 수 있습니다.
파이썬 노드 템플릿 기능 설정은 하기 링크 참고하세요.

https://primer.dynamobim.org/ko/10_Custom-Nodes/10-6_Python-Templates.html


요약하자면,
데코레이터를 위 코드로 선언해주시고,
자신이 선언한 함수 위에 @iterize 라는 문구만 붙이면, 리스트 구조에 관계없이 동작하는 함수를 만들 수 있습니다~!!

반응형