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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 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 객체를 인스턴스화합니다.
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 객체를 반환합니다.
- 객체의 타입에 MRO에서 정의된
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에 커밋되었습니다.
참고 문헌
Copyright
This document has been placed in the public domain.