Decorator Operator (Draft)

ilotoki·2023년 12월 13일
0

Decorator operator는 그 이름처럼 decorator를 inline에서 사용할 수 있도록 합니다.
syntax는 다음과 같습니다.

value @func_or_decorator

기존 데코레이터와 비교하자면 다음과 같습니다.

@decorator
def func():
    ...

# Using decorator operator
def func():
    ...
func = `func @decorator`  # or func @= decorator

이는 기존 데코레이터와는 다르게 value가 함수일 필요는 없습니다(함수여도 좋습니다).
핵심적으로 value @func_or_decoratorfunc_or_decorator(value)와 같습니다.\

함수의 값을 가진 뒤 그 값으로 대체하고 싶은 경우 @=를 사용할 수 있습니다. 이때 __imatmul__을 이용한 특별한 처리가 필요하지 않습니다.

val = map(str, 10 @range)
val @= list  # same with `val = list(val)`
print(val)
# output: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

Usages

여러 함수를 겹쳐 사용할때

Flat is better then nested. 하지만 파이썬을 사용하다 보면 여러 함수가 겹쳐서 사용되는 경우가 많습니다.
이 경우 데코레이터 연산자를 사용하는 것이 직관적인 대안이 됩니다.

def joiner(delimiter: str):
    return lambda iterable: delimiter.join(str(i) for i in iterable)

print(joiner("|")(list(range(10))))

# Using decorator operator:
10 @range @list @joiner("|") @print
# output: 0|1|2|3|4|5|6|7|8|9

함수의 인자와 변수가 겹치는 경우

var = func(var) 형태가 지속적으로 반복되는 경우에도 상당히 유용하게 사용이 가능합니다.

# from https://discuss.python.org/t/apply-operator-f-x-means-f-x/32834/7?u=ilotoki0804
import numpy as np

arr = np.array(...)
arr = np.mean(arr)
arr = np.sqrt(arr)
arr = np.log(arr)

# Using decorator operator:
arr = np.array(...)
arr @= np.mean  # same with arr = arr @np.mean
arr @= np.sqrt
arr @= np.log

복잡한 함수 실행

복잡한 함수 실행에서 괄호를 상당 부분 걷어내 괄호의 늪 사이에서 길을 잃을 확률을 줄일 수 있습니다.

# from https://discuss.python.org/t/apply-operator-f-x-means-f-x/32834/19?u=ilotoki0804
result = collections.OrderedDict(
    map(
        lambda x: (x[0],x),
        self.get_buy_operations_with_adjusted(sorted(items))
    )
)

# Using decorator operator:
result = map(
    lambda x: (x[0], x),
    items @sorted @self.get_buy_operations_with_adjusted
) @collections.OrderedDict

pipelining

# from https://discuss.python.org/t/apply-operator-f-x-means-f-x/32834/6?u=ilotoki0804
result = data
result @= filterpipe(lambda age: age >= 25)
result @= selectpipe("name", "score")
result @= arrangepipe("score", order="desc")

post-define decorating

함수가 선언되는 장소와 데코레이팅을 적용할 장소가 다르거나, 데코레이터를 한 번만 적용하고 샆은 경우에도 사용이 가능합니다.

# from https://stackoverflow.com/questions/28245450/statement-decorators
import requests

def retry(count: int):
    def inner(f):
        def wrapper(*args, **kwargs):
            for i in range(count):
                try:
                    return f(*args, **kwargs)
                except Exception:
                    if i + 1 >= count:
                        raise
        return wrapper
    return inner

# post-define decorating
requests.get @= retry(3)
res = requests.get(unstable_source)

# instant decorating (discouraged)
res = (requests.get @retry(3))(unstable_source)

Broadcasting

numpy.arraypandas.Series 등에서는 연산자에 대해 broadcasting을 사용하고 있습니다.
단점이라면 함수에 대해서는 broadcasting이 적용되지 않는다는 점인데, decorator operator는 연산자이기 때문에 overloading이 될 수 있습니다.

callable과 array는 완전히 구분되는 타입이기 때문에 이 기능은 matrix multiplication과 명백하게 구분 가능합니다. 좋은 네이밍과 함께라면 IDE의 도움 없이도 쉽게 둘을 구분할 수 있습니다.

import numpy as np
arr = np.array(10 @range @list)
print(arr - 1)  # [-1  0  1  2  3  4  5  6  7  8] (broadcasted)
print(str(arr))  # [0 1 2 3 4 5 6 7 8 9] (Not broadcasted)
print(arr @str)  # will be ['0' '1' '2' '3' '4' '5' '6' '7' '8' '9'], same with np.array([str(i) for i in arr])

물론 이를 지원하기 위해서는 해당 라이브러리 개발자들과의 합의가 필요합니다.

특징

구현이 매우 쉬움

전체 구현은 다음과 같습니다.

from typing import Callable

class DecoratorOperatorMeta(type):
    __matmul__ = lambda cls, other: other(cls)
    __rmatmul__ = lambda cls, other: cls(other)


class pipeline[T, U, V](metaclass=DecoratorOperatorMeta):
    """A decorator for implementing decorator operator on Python."""

    def __init__(self, f: Callable[[U], T]) -> None:
        self.func = f

    def __call__(self, *args, **kwargs) -> T:
        return self.func(*args, **kwargs)

    def __matmul__(self, other: Callable[[Callable[[U], T]], V]) -> V:
        """__matmul__ is not needed if decorator operator is being default.

        pipeline_func @no_pipeline_func
        ==> no_pipeline_func(pipeline_func)
        """
        return other(self.func)

    def __rmatmul__(self, other: U) -> T:
        """
        argument @pipeline_func
        ==> pipeline_func(argument)
        """
        return self.func(other)

복잡해 보일지도 모르겠지만, 핵심적으로는 callable의 구현에 다음의 딱 한 줄만 추가하면 됩니다.

__rmatmul__ = lambda self, other: self(other)

파이썬에는 이미 @ 연산자에 대한 구현이 있기에 parser에 추가적인 변화를 줄 필요도 없고 새로운 magic method를 구현할 필요도, 심지어 연산자 순서를 재지정해야 할 필요도 없습니다.

이해하기 쉬움

데코레이터의 연장이기에 데코레이터를 이해하고 있다면 바로 이해할 수 있습니다.

일반 함수 표기와의 적절한 믹싱

위에서는 용례를 보여주기 위해 조금 과도하게 이 연산자를 이용했습니다. 하지만 이 연산자는 일반 함수 표기와도 잘 어울립니다. 자세한 내용은 More Than One Way To Do It를 참고하세요.

"|".join(map(str, list(range(100))))

# Using decorator operator
map(str, 10 @range @list) @"|".join @print
# output: 0|1|2|3|4|5|6|7|8|9

decorator statement (traditional decorator) will be more explainable

데코레이터는 클로져와 같은 파이썬의 기능을 아주 잘 이해해야 적용이 가능한 상당한 고급 기능입니다.

하지만 decorator operator는 함수를 처음 배운 초보에게도 쉽게 이해시킬 수 있기 때문에 나중에 데코레이터 선언문을 배울 때 심리적 저항감이나 이해에 필요한 관문을 줄일 수 있습니다.

@deco
def hello():
    ...

# above code is just syntactic sugar for following code:
def hello():
    ...
hello = hello @deco

change implementation of original decorator

큰 변화는 아닙니다만 기존 데코레이터에 대한 구현은 func = deco(func)에서 func @= deco로 변경되어야 합니다.

표기

decorator operator는 다음과 같은 두 가지의 표기법이 존재할 수 있습니다.

func @decorator: 뒤 연산자와 연산자가 붙어 있음

  • 데코레이터에 대해 알고 있는 사용자에게는 직관적인 이해를 도울 수 있습니다.
  • matrix multiply(arr1 @ arr2)로 사용되는 경우와 효과적으로 구별할 수 있습니다.
  • 스타일 가이드라인에 예외가 있는 이유를 설명해야 합니다(단점). (Python - Why does decorator operator have different rule compared to rest of other operators? - Stack Overflow)

func @ decorator: 연산자와 피연산자가 띄어져 있음

  • 일반적인 다른 연산자와 같은 사용법입니다.
  • 일관적입니다.

More Than One Way To Do It

제가 가장 걱정하는 문제는 이것입니다.

데코레이터 연산자는 일반적인 함수는 할 수 없는 것을 제시하지는 않습니다. 일종의 func(val)을 위한 syntactic suger입니다.

또한 이는 파이썬의 look and feel를 본질적으로 변경하는 중요한 변화가 될 수도 있습니다.

PEP 584에서는 다음과 같이 설명합니다.

The “Zen of Python” merely expresses a preference for “only one obvious way”:

There should be one-- and preferably only one --obvious way to do it.

The emphasis here is that there should be an obvious way to do “it”.

저는 PEP 584의 주장에 따라 스타일 가이드라인을 통해 데코레이터 연산자가 the Zen of Python을 위반하지 않고 구현될 수 있으리라 감히 생각합니다.

Style Guideline

이는 제가 제안하는 스타일 가이드라인입니다.

decorator should be used...

  • When it have needs one positional parameter.
  • When output is meaningful(unlike print). 단, 일부 파이프라이닝에서는 제외

Decorator operator shoud not be used...

  • With functools.partial only for using decorator operator.
  • When original decorator can be used.

Examples:

  • All decorators
  • Many iterable-based functions (list, iter, sum, max) and many other.
  • Many inner functions (operator.itemgetter, operator.attrgetter)

시도해보기

구현이 매우 쉬움 파트의 코드를 복사한 뒤 사용할 callable에 callable @= pipeline(클래스의 경우에는 metaclass를 이용할 수도 있음.)을 붙이면 데코레이터 연산자를 체험해볼 수 있습니다. Inner function일 경우 내부 함수도 pipeline decorator를 붙이는 것을 잊지 마세요.

str   @= pipeline
range @= pipeline
len   @= pipeline
print @= pipeline
list  @= pipeline
repr  @= pipeline

10 @range @list @repr @print
# output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

map(repr, 10 @range @list) @list
# output: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

more considerations

아래의 두 문법은 제가 파이썬이 각 잡고 관련 기능을 추가하기 시작했을 때 유용할 수 있는 몇 가지 기능들입니다.
하지만 위의 style guideline을 따른다면 굳이 아래의 기능들을 구현하지 않고도 decorator operator를 유용하게 사용할 수 있을 것입니다.

syntactic suger for partial function

functools.partial(map, str)
map?(str)

stardecorator

map(*(str, range(100)))
(str, range(100)) @@map

추가

이 기능은 함수형 프로그래밍 언어들과 깊은 관계가 있다고 확신합니다. 하지만 제가 함수형 프로그래밍이나 파이프라이닝에 대해 전문가는 아닙니다. 관련해서 참고할 만한 사항이 있다면 알려주세요.
감사합니다 :)

0개의 댓글