Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 362 – 함수 시그니처 객체

Author:
Brett Cannon <brett at python.org>, Jiwon Seo <seojiwon at gmail.com>, Yury Selivanov <yury at edgedb.com>, Larry Hastings <larry at hastings.org>
Status:
Final
Type:
Standards Track
Created:
21-Aug-2006
Python-Version:
3.3
Post-History:
04-Jun-2012
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

Python은 함수와 메서드에 대한 인트로스펙션을 포함하여 강력한 인트로스펙션 기능을 항상 지원해 왔습니다(이 PEP의 나머지 부분에서 “function”은 함수와 메서드를 모두 가리킵니다). 함수 객체를 검사하면 함수의 시그니처를 완전히 재구성할 수 있습니다. 안타깝게도 이 정보는 불편한 방식으로 저장되며, 깊이 중첩된 대여섯 개의 속성에 분산되어 있습니다.

이 PEP는 함수 시그니처를 위한 새로운 표현을 제안합니다. 새로운 표현에는 함수와 해당 매개변수에 필요한 모든 정보가 포함되며, 인트로스펙션을 쉽고 직관적으로 수행할 수 있습니다.

그러나 이 객체는 기존 함수 메타데이터를 대체하지 않습니다. 기존 함수 메타데이터는 Python 자체가 해당 함수를 실행하는 데 사용합니다. 새로운 메타데이터 객체는 Python 프로그래머가 함수 인트로스펙션을 더 쉽게 수행하도록 하는 용도로만 사용됩니다.

시그니처 객체

시그니처 객체는 함수의 호출 시그니처와 반환 어노테이션을 나타냅니다. 함수가 허용하는 각 매개변수에 대해 parameters 컬렉션에 Parameter Object를 저장합니다.

시그니처 객체에는 다음과 같은 공개 속성과 메서드가 있습니다.

  • return_annotation : object
    함수의 “return” 어노테이션입니다. 함수에 “return” 어노테이션이 없으면 이 속성은 Signature.empty로 설정됩니다.
  • parameters : OrderedDict
    매개변수 이름과 해당 Parameter 객체의 순서가 있는 매핑입니다.
  • bind(*args, **kwargs) -> BoundArguments
    위치 인자와 키워드 인자에서 매개변수로의 매핑을 생성합니다. 전달된 인자가 시그니처와 일치하지 않으면 TypeError를 발생시킵니다.
  • bind_partial(*args, **kwargs) -> BoundArguments
    bind()와 동일하게 작동하지만 일부 필수 인자를 생략할 수 있습니다(functools.partial의 동작을 모방합니다). 전달된 인자가 시그니처와 일치하지 않으면 TypeError를 발생시킵니다.
  • replace(parameters=<optional>, *, return_annotation=<optional>) -> Signature
    replace를 호출한 인스턴스를 기반으로 새로운 시그니처 인스턴스를 생성합니다. 서로 다른 parameters및/또는 return_annotation을 전달하여 기본 시그니처의 해당 속성을 재정의할 수 있습니다. 복사된 Signature에서 return_annotation을 제거하려면 Signature.empty를 전달하십시오.

    ‘=<optional>’ 표기법은 인자가 선택 사항임을 의미합니다. 이 표기법은 이 PEP의 나머지 부분에도 적용됩니다.

시그니처 객체는 변경할 수 없습니다. 수정된 복사본을 만들려면 Signature.replace()를 사용하십시오.

>>> def foo() -> None:
...     pass
>>> sig = signature(foo)

>>> new_sig = sig.replace(return_annotation="new return annotation")
>>> new_sig is not sig
True
>>> new_sig.return_annotation != sig.return_annotation
True
>>> new_sig.parameters == sig.parameters
True

>>> new_sig = new_sig.replace(return_annotation=new_sig.empty)
>>> new_sig.return_annotation is Signature.empty
True

Signature 클래스를 인스턴스화하는 방법은 두 가지입니다:

  • Signature(parameters=<optional>, *, return_annotation=Signature.empty)
    기본 Signature 생성자입니다. 선택적 Parameter 객체 시퀀스와 선택적 return_annotation을 받습니다. 매개변수 시퀀스는 이름이 중복된 매개변수가 없고 매개변수가 올바른 순서로 배치되었는지 검증합니다. 즉, 위치 전용 매개변수가 먼저 오고, 그다음 위치 또는 키워드 매개변수가 오는 식입니다.
  • Signature.from_function(function)
    전달된 함수의 시그니처를 반영하는 Signature 객체를 반환합니다.

Signature의 동일성을 테스트할 수 있습니다. 매개변수가 같고, 위치 매개변수와 위치 전용 매개변수가 같은 순서로 나타나며, 반환 어노테이션이 같을 때 두 시그니처는 동일합니다.

Signature 객체 또는 그 데이터 멤버를 변경해도 함수 자체에는 영향을 주지 않습니다.

Signature는 __str__도 구현합니다:

>>> str(Signature.from_function((lambda *args: None)))
'(*args)'

>>> str(Signature())
'()'

Parameter 객체

Python의 표현력 있는 구문으로 인해 함수는 미묘한 의미상의 차이가 있는 다양한 종류의 매개변수를 받을 수 있습니다. 가능한 모든 함수 매개변수를 나타내도록 설계된 풍부한 Parameter 객체를 제안합니다.

Parameter 객체에는 다음과 같은 공개 속성 및 메서드가 있습니다:

  • name : str
    매개변수의 이름을 문자열로 나타냅니다. 유효한 Python 식별자 이름이어야 합니다. 단, POSITIONAL_ONLY 매개변수는 None으로 설정할 수 있습니다.
  • default : object
    매개변수의 기본값입니다. 매개변수에 기본값이 없으면 이 속성은 Parameter.empty로 설정됩니다.
  • annotation : object
    매개변수의 어노테이션입니다. 매개변수에 어노테이션이 없으면 이 속성은 Parameter.empty로 설정됩니다.
  • kind
    인자 값이 매개변수에 바인딩되는 방식을 설명합니다. 가능한 값은 다음과 같습니다:
    • Parameter.POSITIONAL_ONLY - 값은 위치 인자로 제공해야 합니다.

      Python에는 위치 전용 매개변수를 정의하는 명시적 구문이 없지만, 많은 내장 함수와 확장 모듈 함수, 특히 매개변수를 하나 또는 두 개만 받는 함수는 이를 허용합니다.

    • Parameter.POSITIONAL_OR_KEYWORD - 값은 키워드 인자 또는 위치 인자로 제공할 수 있습니다. 이는 Python으로 구현된 함수의 표준 바인딩 동작입니다.
    • Parameter.KEYWORD_ONLY - 값은 키워드 인자로 제공해야 합니다. 키워드 전용 매개변수는 Python 함수 정의에서 “*” 또는 “*args” 항목 뒤에 나타나는 매개변수입니다.
    • Parameter.VAR_POSITIONAL - 다른 매개변수에 바인딩되지 않은 위치 인자의 튜플입니다. 이는 Python 함수 정의에서 “*args” 매개변수에 해당합니다.
    • Parameter.VAR_KEYWORD - 다른 매개변수에 바인딩되지 않은 키워드 인자의 딕셔너리입니다. 이는 Python 함수 정의에서 “**kwargs” 매개변수에 해당합니다.

    kind 특성의 값을 설정하고 확인할 때는 항상 Parameter.* 상수를 사용하십시오.

  • replace(*, name=<optional>, kind=<optional>, default=<optional>, annotation=<optional>) -> Parameter
    replaced가 호출된 인스턴스를 기반으로 새 Parameter 인스턴스를 생성합니다. Parameter 특성을 재정의하려면 해당 인자를 전달하십시오. Parameter에서 특성을 제거하려면 Parameter.empty를 전달하십시오.

Parameter 생성자입니다:

  • Parameter(name, kind, *, annotation=Parameter.empty, default=Parameter.empty)
    Parameter 객체를 인스턴스화합니다. namekind는 필수이고 annotationdefault는 선택 사항입니다.

두 매개변수는 이름, 종류, 기본값 및 어노테이션이 같을 때 동일합니다.

Parameter 객체는 변경할 수 없습니다. Parameter 객체를 수정하는 대신 Parameter.replace()를 사용하여 다음과 같이 수정된 복사본을 생성할 수 있습니다:

>>> param = Parameter('foo', Parameter.KEYWORD_ONLY, default=42)
>>> str(param)
'foo=42'

>>> str(param.replace())
'foo=42'

>>> str(param.replace(default=Parameter.empty, annotation='spam'))
"foo:'spam'"

BoundArguments 객체입니다.

Signature.bind호출의 결과입니다. 함수의 매개변수에 대한 인자의 매핑을 보유합니다.

다음과 같은 공개 특성을 가집니다:

  • arguments : OrderedDict
    매개변수 이름을 인자 값에 매핑하는 정렬된 변경 가능한 매핑입니다. 명시적으로 바인딩된 인자만 포함합니다. bind()가 기본값에 의존한 인자는 건너뜁니다.
  • args : tuple
    위치 인자 값의 튜플입니다. ‘arguments’ 특성에서 동적으로 계산됩니다.
  • kwargs : dict
    키워드 인자 값의 딕셔너리입니다. ‘arguments’ 특성에서 동적으로 계산됩니다.

모든 인자 처리 목적에는 arguments특성을 Signature.parameters와 함께 사용해야 합니다.

argskwargs속성을 사용하여 함수를 호출할 수 있습니다:

def test(a, *, b):
    ...

sig = signature(test)
ba = sig.bind(10, b=20)
test(*ba.args, **ba.kwargs)

*args 또는 **kwargs의 일부로 전달될 수 있는 인자는 BoundArguments.args특성에만 포함됩니다. 다음 예를 살펴보십시오.

def test(a=1, b=2, c=3):
    pass

sig = signature(test)
ba = sig.bind(a=10, c=13)

>>> ba.args
(10,)

>>> ba.kwargs:
{'c': 13}

구현

이 구현은 inspect 모듈에 새 함수 signature()를 추가합니다. 이 함수는 호출 가능 객체에 대한 Signature를 가져오는 데 권장되는 방법입니다.

이 함수는 다음 알고리즘을 구현합니다.

  • 객체가 호출 가능 객체가 아니면 TypeError를 발생시킵니다.
  • 객체에 __signature__ 특성이 있고 그 값이 None이 아니면 해당 값을 반환합니다.
  • 객체에 __wrapped__ 특성이 있으면 signature(object.__wrapped__)를 반환합니다.
  • 객체가 FunctionType의 인스턴스이면 해당 객체에 대한 새 Signature를 생성하여 반환합니다.
  • 객체가 바운드 메서드이면 첫 번째 매개변수(일반적으로 self 또는 cls)를 제거한 새 Signature 객체를 생성하여 반환합니다. (classmethodstaticmethod도 지원합니다. 둘 다 디스크립터이므로 전자는 바운드 메서드를 반환하고 후자는 래핑된 함수를 반환합니다.)
  • 객체가 functools.partial의 인스턴스이면 해당 객체의 partial.func 특성에서 새 Signature를 생성하고 이미 바인딩된 partial.argspartial.kwargs를 반영합니다.
  • 객체가 클래스 또는 메타클래스이면 다음을 수행합니다.
    • 객체의 타입에 MRO에서 정의된 __call__ 메서드가 있으면 해당 메서드에 대한 Signature를 반환합니다.
    • 객체에 MRO에서 정의된 __new__ 메서드가 있으면 해당 메서드에 대한 Signature 객체를 반환합니다.
    • 객체에 MRO에서 정의된 __init__ 메서드가 있으면 해당 메서드에 대한 Signature 객체를 반환합니다.
  • signature(object.__call__)을 반환합니다.

Signature 객체는 지연 방식으로 생성되며 자동으로 캐시되지 않는다는 점에 유의하십시오. 그러나 사용자는 __signature__ 특성에 Signature를 저장하여 수동으로 캐시할 수 있습니다.

Python 3.3용 구현은 [1]에서 확인할 수 있습니다. 이 패치를 추적하는 Python 이슈는 [2]입니다.

설계 고려 사항

Signature 객체의 암시적 캐싱 없음

최초의 PEP 설계에는 inspect.signature() 함수에서 Signature 객체를 암시적으로 캐시하는 방안이 포함되어 있었습니다. 그러나 여기에는 다음과 같은 단점이 있습니다.

  • Signature 객체가 캐시되면 해당 객체가 설명하는 함수의 변경 사항이 그 객체에 반영되지 않습니다. 그러나 캐싱이 필요하다면 언제든 수동으로 명시적으로 수행할 수 있습니다.
  • 실제 객체와 다른 Signature 객체를 명시적으로 설정해야 하는 경우를 위해 __signature__ 특성을 따로 남겨 두는 편이 좋습니다.

일부 함수는 인트로스펙션할 수 없을 수 있습니다.

Python의 특정 구현에서는 일부 함수가 인트로스펙션이 불가능할 수 있습니다. 예를 들어 CPython에서는 C로 정의된 내장 함수가 인자에 대한 메타데이터를 전혀 제공하지 않습니다. 이러한 함수들에 대한 지원을 추가하는 것은 이 PEP의 범위를 벗어납니다.

Signature와 Parameter의 동등성

매개변수 이름에 의미론적 중요성이 있다고 가정합니다–두 시그니처는 대응하는 매개변수들이 동등하고 정확히 같은 이름을 가질 때만 같습니다. 좀 더 느슨한 동등성 테스트를 원하는 사용자, 예를 들어 VAR_KEYWORD나 VAR_POSITIONAL 매개변수의 이름을 무시하고자 하는 사용자는 직접 구현해야 합니다.

예제

호출 가능 객체의 Signature 시각화

몇 가지 클래스와 함수를 정의해 봅시다:

from inspect import signature
from functools import partial, wraps


class FooMeta(type):
    def __new__(mcls, name, bases, dct, *, bar:bool=False):
        return super().__new__(mcls, name, bases, dct)

    def __init__(cls, name, bases, dct, **kwargs):
        return super().__init__(name, bases, dct)


class Foo(metaclass=FooMeta):
    def __init__(self, spam:int=42):
        self.spam = spam

    def __call__(self, a, b, *, c) -> tuple:
        return a, b, c

    @classmethod
    def spam(cls, a):
        return a


def shared_vars(*shared_args):
    """Decorator factory that defines shared variables that are
       passed to every invocation of the function"""

    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            full_args = shared_args + args
            return f(*full_args, **kwargs)

        # Override signature
        sig = signature(f)
        sig = sig.replace(tuple(sig.parameters.values())[1:])
        wrapper.__signature__ = sig

        return wrapper
    return decorator


@shared_vars({})
def example(_state, a, b, c):
    return _state, a, b, c


def format_signature(obj):
    return str(signature(obj))

이제 python REPL에서:

>>> format_signature(FooMeta)
'(name, bases, dct, *, bar:bool=False)'

>>> format_signature(Foo)
'(spam:int=42)'

>>> format_signature(Foo.__call__)
'(self, a, b, *, c) -> tuple'

>>> format_signature(Foo().__call__)
'(a, b, *, c) -> tuple'

>>> format_signature(Foo.spam)
'(a)'

>>> format_signature(partial(Foo().__call__, 1, c=3))
'(b, *, c=3) -> tuple'

>>> format_signature(partial(partial(Foo().__call__, 1, c=3), 2, c=20))
'(*, c=20) -> tuple'

>>> format_signature(example)
'(a, b, c)'

>>> format_signature(partial(example, 1, 2))
'(c)'

>>> format_signature(partial(partial(example, 1, b=2), c=3))
'(b=2, c=3)'

Annotation Checker

import inspect
import functools

def checktypes(func):
    '''Decorator to verify arguments and return types

    Example:

        >>> @checktypes
        ... def test(a:int, b:str) -> int:
        ...     return int(a * b)

        >>> test(10, '1')
        1111111111

        >>> test(10, 1)
        Traceback (most recent call last):
          ...
        ValueError: foo: wrong type of 'b' argument, 'str' expected, got 'int'
    '''

    sig = inspect.signature(func)

    types = {}
    for param in sig.parameters.values():
        # Iterate through function's parameters and build the list of
        # arguments types
        type_ = param.annotation
        if type_ is param.empty or not inspect.isclass(type_):
            # Missing annotation or not a type, skip it
            continue

        types[param.name] = type_

        # If the argument has a type specified, let's check that its
        # default value (if present) conforms with the type.
        if param.default is not param.empty and not isinstance(param.default, type_):
            raise ValueError("{func}: wrong type of a default value for {arg!r}". \
                             format(func=func.__qualname__, arg=param.name))

    def check_type(sig, arg_name, arg_type, arg_value):
        # Internal function that encapsulates arguments type checking
        if not isinstance(arg_value, arg_type):
            raise ValueError("{func}: wrong type of {arg!r} argument, " \
                             "{exp!r} expected, got {got!r}". \
                             format(func=func.__qualname__, arg=arg_name,
                                    exp=arg_type.__name__, got=type(arg_value).__name__))

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Let's bind the arguments
        ba = sig.bind(*args, **kwargs)
        for arg_name, arg in ba.arguments.items():
            # And iterate through the bound arguments
            try:
                type_ = types[arg_name]
            except KeyError:
                continue
            else:
                # OK, we have a type for the argument, lets get the corresponding
                # parameter description from the signature object
                param = sig.parameters[arg_name]
                if param.kind == param.VAR_POSITIONAL:
                    # If this parameter is a variable-argument parameter,
                    # then we need to check each of its values
                    for value in arg:
                        check_type(sig, arg_name, type_, value)
                elif param.kind == param.VAR_KEYWORD:
                    # If this parameter is a variable-keyword-argument parameter:
                    for subname, value in arg.items():
                        check_type(sig, arg_name + ':' + subname, type_, value)
                else:
                    # And, finally, if this parameter a regular one:
                    check_type(sig, arg_name, type_, arg)

        result = func(*ba.args, **ba.kwargs)

        # The last bit - let's check that the result is correct
        return_type = sig.return_annotation
        if (return_type is not sig._empty and
                isinstance(return_type, type) and
                not isinstance(result, return_type)):

            raise ValueError('{func}: wrong return type, {exp} expected, got {got}'. \
                             format(func=func.__qualname__, exp=return_type.__name__,
                                    got=type(result).__name__))
        return result

    return wrapper

승인

PEP 362는 2012년 6월 22일 금요일 Guido에 의해 승인되었습니다 [3] . 참조 구현은 그날 늦게 trunk에 커밋되었습니다.

참고 문헌