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의 나머지 부분에서 “함수”는 함수와 메서드를 모두 가리킵니다). 함수 객체를 살펴보면 함수의 시그니처를 완전히 재구성할 수 있습니다. 안타깝게도 이 정보는 불편한 방식으로 저장되어 있으며, 깊이 중첩된 여섯 개 남짓한 속성에 흩어져 있습니다.

이 PEP는 함수 시그니처를 위한 새로운 표현 방식을 제안합니다. 새로운 표현 방식은 함수와 그 매개변수에 대해 필요한 모든 정보를 담고 있으며, 인트로스펙션을 쉽고 간단하게 만들어 줍니다.

하지만 이 객체는 Python 자체가 함수를 실행할 때 사용하는 기존 함수 메타데이터를 대체하지 않습니다. 새 메타데이터 객체의 목적은 오로지 Python 프로그래머가 함수 인트로스펙션을 더 쉽게 할 수 있도록 하는 것입니다.

Signature 객체

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

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

  • 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가 호출된 인스턴스를 바탕으로 새로운 Signature 인스턴스를 만듭니다. 다른 parameters 및/또는 return_annotation을 전달하여 기반 시그니처의 해당 속성을 재정의할 수 있습니다. 복사된 Signature에서 return_annotation을 제거하려면 Signature.empty를 전달하십시오.

    ‘=<optional>’ 표기법은 해당 인자가 선택 사항이라는 뜻입니다. 이 표기법은 이 PEP의 나머지 부분에도 적용됩니다.

Signature 객체는 불변입니다. 수정된 복사본을 만들려면 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(매개변수) 객체를 인스턴스화합니다. name과 kind는 필수이고, annotation과 default는 선택 사항입니다.

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

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와 함께 사용해야 합니다.

args와 kwargs 프로퍼티를 사용하여 함수를 호출할 수 있습니다:

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 객체를 생성하여 반환합니다. (classmethod와 staticmethod도 지원됩니다. 둘 다 디스크립터이므로 전자는 바운드 메서드를 반환하고 후자는 감싸고 있는 함수를 반환합니다.)
  • 객체가 functools.partial의 인스턴스이면, 그 partial.func 속성으로부터 새 Signature를 구성하고, 이미 바인딩된 partial.args와 partial.kwargs를 반영합니다
  • 객체가 클래스 또는 메타클래스인 경우:
    • 객체의 타입에 MRO에 정의된 __call__ 메서드가 있으면 그 메서드에 대한 Signature를 반환합니다
    • 객체에 MRO에 정의된 __new__ 메서드가 있으면 그 메서드에 대한 Signature 객체를 반환합니다
    • 객체에 MRO에 정의된 __init__ 메서드가 있으면 그 메서드에 대한 Signature 객체를 반환합니다
  • signature(object.__call__)를 반환합니다

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

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

설계 시 고려 사항

Signature 객체의 암묵적 캐싱 없음

최초의 PEP 설계에는 inspect.signature() 함수에서 Signature 객체를 암묵적으로 캐시하는 규정이 있었습니다. 하지만 이 방식에는 다음과 같은 단점이 있습니다:

  • Signature 객체가 캐시되면 그 객체가 기술하는 함수에 가해진 변경 사항이 객체에 반영되지 않습니다. 하지만 캐싱이 필요하다면 언제든지 수동으로, 명시적으로 수행할 수 있습니다
  • __signature__ 속성은 실제 시그니처와 다른 Signature 객체를 명시적으로 설정해야 하는 경우를 위해 남겨 두는 것이 좋습니다

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

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

Signature와 매개변수의 동등성

매개변수 이름에는 의미상 중요성이 있다고 가정합니다–두 시그니처는 대응하는 매개변수가 서로 같고 이름까지 정확히 같을 때에만 같습니다. VAR_KEYWORD나 VAR_POSITIONAL 매개변수의 이름을 무시하는 등 더 느슨한 동등성 검사를 원하는 사용자는 이를 직접 구현해야 합니다.

예시

호출 가능 객체의 시그니처 시각화하기

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

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)'

어노테이션 검사기

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에 커밋되었습니다.

참고 문헌