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의 나머지 부분에서 “함수”는 함수와 메서드를 모두 가리킵니다). 함수 객체를 살펴보면 함수의 시그니처를 완전히 재구성할 수 있습니다. 안타깝게도 이 정보는 불편한 방식으로 저장되어 있으며, 깊이 중첩된 여섯 개 남짓한 속성에 흩어져 있습니다.
이 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 객체를 반환합니다
- 객체의 타입에 MRO에 정의된
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에 커밋되었습니다.
참고 문헌
Copyright
This document has been placed in the public domain.