Python의 커링과 그것이 짜증나는 이유

13815 단어 pythonfunctional

커링이란?



Curring은 개발자가 함수에 인수를 부분적으로 전달할 수 있도록 하는 기능적 기술(또는 기능)입니다. 예를 살펴보겠습니다.

예시



일부 인코딩을 사용하여 decodestr로 변환하는 함수bytes가 있다고 상상해 보십시오.

Yep, I know that there is built-in str.decode method, but for simplicity and some non-imaginary add example let's look at this one



def decode(encoding: str, s: str) -> bytes:
    return s.decode(encoding)


이 함수는 항상 decode("UTF-8", some_str)처럼 실행됩니다. 대부분 하나의 인코딩(특히 "UTF-8")을 사용하므로 이"UTF-8" 인수는 어디에나 있을 것입니다. 어느 날 그것을 리팩토링하고 다른 함수를 작성하기로 결정할 때마다 작성하는 데 지쳤습니다.

def decode_utf_8(s: str) -> bytes:
    return decode("UTF-8", s)


실제로는 지름길일 뿐이지만 무의식적으로 커링을 사용했습니다(그러나 실제 함수형 언어가 사용하는 방식은 아님)! Curring은 왼쪽 인수를 전달할 때 대기하는 다른 함수를 얻기 위해 일부 인수를 함수에 전달하는 것입니다.

따라서 함수에 3개의 인수가 있고 2개만 전달하면 예외를 발생시키는 대신 실제로 하나의 왼쪽 인수만 허용하는 함수가 생성됩니다.

파이썬이 커링을 지원한다면?



Python이 기본적으로 이 기능을 지원한다면 다음과 같이 작성할 수 있습니다.

# this does not work!!!
decode_utf_8 = decode("UTF-8")  # type: Callable[[str], bytes]


나에게는 멋져 보이지만 솔직히 말해서 이전 버전과의 호환성 문제로 인해 이것이 실제로 Python3.X(언젠가는 Python4에서 가능할 수도 있지만 누가 알겠습니까?)에서 현실이 될 가능성은 없습니다.

그러나 현재 Python에서 함수를 커링하는 몇 가지 합법적인 방법이 있습니다.

옵션 1: functools.partial


functools.partial를 사용하면 다음과 같이 decode_utf_8를 작성할 수 있습니다.

from functools import partial

decode_utf_8 = partial(decode, "UTF-8")

decode_utf_8("hello")  # b"hello"


옵션 2: toolz.curry(더 나은 것)



또 다른 옵션은 훌륭한 패키지에서 제공됩니다 toolz .

from toolz import curry

@curry
def decode(encoding: str, s: str) -> bytes:
    return s.decode(encoding)

decode_utf_8 = decode("UTF-8")


이 옵션은 커링이 하나의 데코레이터로 기본적으로 지원되는 것처럼 보이기 때문에 훨씬 더 좋아 보입니다.

근데 왜 짜증나?



Python은 동적으로 입력되는 언어이며 partial 또는 @curry를 사용한 후에는 실제로 코드 편집기에 대한 유형 힌트를 얻을 수 없습니다. 유형 힌트를 작성하고 mypy와 같은 정적 유형 검사기를 사용하여 코드를 깨끗하게 유지하고 유형을 입력하려는 모든 시도는 헛된 것입니다. 예를 들어 VS 코드는 실제 decode_utf_8 대신 partial 유형이 curry 또는 Callable[[str], bytes]임을 알려줍니다.

이것은 실제로 함수를 실행하기 위해 전달해야 하는 인수를 스스로 기억해야 하기 때문에 사용하기 어렵습니다. 또한 선택적 인수로 수행할 작업이 항상 명확한 것은 아닙니다. 임의의 인수가 있는 함수도 당연히 커링할 수 없습니다.

해킹이 있습니까?



글쎄, 나는 하나를 찾고 있었고 다음과 같은 간단한 해결 방법을 만들었습니다.

from typing import (
    Callable,
    Concatenate,
    ParamSpec,
    TypeVar,
)

P = ParamSpec("P")
A1 = TypeVar("A1")
AResult = TypeVar("AResult")


def hof1(func: Callable[Concatenate[A1, P], AResult]):
    """Separate first argument from other."""

    def _wrapper(arg_1: A1) -> Callable[P, AResult]:
        def _func(*args: P.args, **kwargs: P.kwargs) -> AResult:
            return func(arg_1, *args, **kwargs)

        return _func

    return _wrapper

# also hof2 and hof3 decorator for separating first 2 and 3
# arguments correspondingly

@hof1
def decode(encoding: str, s: str) -> bytes:
    return s.decode(encoding)

decode_utf_8 = decode("UTF-8")
# but you can't do this:
decode("UTF-8", "hello")  # raises error


이 데코레이터를 사용하면 먼저 전달할 인수와 실제로 함수를 실행하기 위해 전달할 인수를 미리 알아야 합니다. 이것은 심각한 제한이지만 쉬운 구문보다 더 중요하다고 생각하는 유형 힌트를 저장하는 데 도움이 됩니다.

더 광범위한 예:

from toolz import pipe


@hof1
def encode(encoding: str, b: bytes) -> str:
    return b.encode(encoding)


@hof1
def split(sep: str, s: str) -> Iterable[str]:
    return s.split(sep)

@hof1
def cmap(
   mapper: Callable[[X], Y], iterable: Iterable[X]
) -> Iterable[Y]:
    return map(mapper, iterable)

query = pipe(
    b"name=John&age=32",
    encode("UTF-8"),
    split("&"),
    cmap(split("=")),
    dict,
)

print(query)  # {"name": "John", "age": "32"}

# same without hof1
query = pipe(
    b"name=John&age=32",
    lambda b: b.encode("UTF-8"),
    lambda s: s.split("&"),
    lambda ss: map(lambda s: s.split("="), ss),
    dict,
)


커링 및 그러한 해결 방법에 대해 어떻게 생각하십니까? 의견에 생각과 질문을 공유하십시오!

좋은 웹페이지 즐겨찾기