Python 3.0에서 도입된 함수 어노테이션(Function Annotations)은 함수의 매개변수와 반환값에 임의의 메타데이터를 추가할 수 있는 기능입니다. PEP-3107을 통해 Python 3부터 공식적으로 채택되었으며, 그 핵심 목적은 함수 매개변수와 반환값에 메타데이터를 연결하는 표준적인 방법을 제공하는 것이었습니다.
함수 어노테이션의 기본 개념
함수 어노테이션을 이해하기 위해 알아두어야 할 핵심 사항은 다음과 같습니다.
함수 어노테이션은 매개변수와 반환값 모두에 대해 완전히 선택 사항(optional)입니다.
컴파일 시점에 함수의 여러 요소를 임의의 Python 표현식과 연결할 수 있는 방법을 제공합니다.
PEP-3107은 내장 타입을 포함하여 어떤 종류의 표준 의미론(semantic)도 정의하지 않습니다. 이러한 역할은 모두 서드파티 라이브러리에 맡겨져 있습니다.
어노테이션 작성 문법
단순 매개변수의 어노테이션
매개변수 어노테이션은 다음 형태로 작성합니다.
def foo(x: expression, y: expression = 20):
...
가변 인자(*args, **kwargs)에 대한 어노테이션은 다음과 같습니다.
def foo(*args: expression, **kwargs: expression):
...
중첩된 매개변수의 경우 어노테이션은 항상 매개변수 이름 바로 뒤에 위치하며, 닫는 괄호까지 이어지지 않습니다. 또한 중첩 매개변수의 모든 요소에 어노테이션을 붙일 필요는 없습니다.
def foo(x1, y1: expression), (x2: expression, y2: expression)=(None, None)):
....
중요한 점은 Python이 어노테이션 자체에 어떤 의미론도 부여하지 않는다는 것입니다. Python은 메타데이터를 연결하고 손쉽게 접근할 수 있게 해주는 문법적 지원만 제공할 뿐이며, 어노테이션 작성은 강제되지 않습니다.
>>> def func(x:'annotating x', y: 'annotating y', z: int) -> float: print(x + y + z)
위 예제에서 func() 함수는 x, y, z 세 개의 매개변수를 받아 그 합을 출력합니다. 첫 번째 인자 x는 문자열 'annotating x'로, 두 번째 인자 y는 'annotating y'로 어노테이션이 지정되었고, 세 번째 인자 z에는 int 타입이 붙었습니다. 반환값에는 float 타입이 어노테이션으로 지정되었으며, 이때 반환값 어노테이션에는 -> 문법을 사용합니다.
실행 결과
>>> func(2,3,-4)
1
>>> func('Function','-','Annotation')
Function-Annotation
위 코드에서 func()를 두 번 호출했습니다. 한 번은 정수 인자로, 다른 한 번은 문자열 인자로 호출했는데, 두 경우 모두 func()는 올바르게 동작하고 어노테이션은 단순히 무시됩니다. 즉, 어노테이션은 함수의 실행에 아무런 영향을 주지 않습니다.
함수 어노테이션 접근하기
모든 어노테이션은 __annotations__라는 딕셔너리에 저장되며, 이 딕셔너리 자체가 함수의 속성(attribute)입니다.
>>> def func(x:'annotating x', y: 'annotating y', z: int) -> float: print(x + y + z)
>>> func.__annotations__
{'x': 'annotating x', 'y': 'annotating y', 'z': <class 'int'>, 'return': <class 'float'>}
앞선 예제에서 확인할 수 있듯이, 어노테이션은 엄밀히 말해 타입 선언이 아닙니다. 물론 그런 용도로 활용할 수 있고, 다른 언어의 타입 지정 문법과 비슷하게 보일 수도 있습니다.
>>> def func(a: 'python', b: {'category: ' 'language'}) -> 'yep':
pass
>>> func.__annotations__
{'a': 'python', 'b': {'category: language'}, 'return': 'yep'}
>>>
어노테이션은 임의의 표현식이므로 __annotations__ 딕셔너리에 어떤 값이든 저장할 수 있습니다. Python 인터프리터 입장에서는 값을 저장한다는 사실 외에 특별한 의미가 없지만, 실무에서는 매개변수와 반환값의 타입을 명시하는 것이 함수 어노테이션의 가장 일반적인 용도입니다.
@no_type_check 데코레이터
어노테이션을 타입 선언으로 간주하는 도구를 사용하면서도, 어노테이션을 다른 목적으로 활용하고 싶은 경우가 있을 수 있습니다. 이럴 때 표준 라이브러리의 @no_type_check 데코레이터를 사용하면 해당 함수를 타입 검사 대상에서 제외할 수 있습니다.
>>> from typing import no_type_check
>>> @no_type_check
def func(a: 'python', b: {'category: ' 'language'}) -> 'yep':
pass
>>>
일반적으로는 이 데코레이터가 필요하지 않습니다. 어노테이션을 활용하는 대부분의 도구는 자신에게 해당하는 어노테이션을 스스로 식별하는 방법을 갖추고 있기 때문입니다. 이 데코레이터는 동작이 모호해질 수 있는 예외적인 상황을 보호하기 위한 안전장치라고 이해하면 됩니다.
데코레이터의 입력값으로서의 어노테이션
어노테이션은 데코레이터와 매우 잘 조합됩니다. 어노테이션 값은 데코레이터에 입력을 전달하는 좋은 수단이 되고, 데코레이터가 생성하는 래퍼(wrapper)는 어노테이션에 실질적인 의미를 부여하는 코드를 배치하기에 적합한 위치이기 때문입니다.
from functools import wraps
def adapted(func):
@wraps(func)
def wrapper(**kwargs):
final_args = {}
for name, value in kwargs.items():
adapt = func.__annotations__.get(name)
if adapt is not None:
final_args[name] = adapt(value)
else:
final_args[name] = value
result = func(**final_args)
adapt = func.__annotations__.get('return')
if adapt is not None:
return adapt(result)
return result
return wrapper
@adapted
def func(a: int, b: repr) -> str:
return a
위의 adapted 데코레이터는 원래 함수를 래퍼로 감쌉니다. 이 래퍼는 키워드 인자만 받아들이므로, 원래 함수가 위치 인자를 지원하더라도 호출 시에는 반드시 인자의 이름을 지정해야 합니다.
함수가 래퍼로 감싸지면, 래퍼는 함수의 매개변수 어노테이션에서 변환 어댑터(adapter)를 찾아 실제 함수에 인자를 전달하기 전에 적용합니다.
함수가 값을 반환한 후에는 래퍼가 반환값 어댑터가 있는지 확인하고, 존재한다면 최종 반환 직전에 반환값에 적용합니다.
여기서 일어나는 일의 의미를 곱씹어 보면 꽤 놀랍습니다. 사실 우리는 "함수에 매개변수를 전달한다", "함수가 값을 반환한다"라는 행위의 의미 자체를 재정의한 것입니다.
키워드 인자 자동 저장 활용
때로는 메서드의 매개변수 중 일부에 대해 self의 속성에 값을 할당하는 것 외에 어떠한 처리도 필요하지 않은 경우가 있습니다. 데코레이터와 어노테이션을 활용하면 이 과정을 자동화할 수 있을까요? 물론 가능합니다.
from functools import wraps
def store_args(func):
@wraps(func)
def wrapper(self, **kwargs):
for name, value in kwargs.items():
attrib = func.__annotations__.get(name)
if attrib is True:
attrib = name
if isinstance(attrib, str):
setattr(self, attrib, value)
return func(self, **kwargs)
return wrapper
class A:
@store_args
def __init__(self, first: True, second: 'example'):
pass
a = A(first=5, second=6)
assert a.first == 5
assert a.example == 6
store_args 데코레이터는 __init__ 메서드의 매개변수 어노테이션을 검사합니다. 어노테이션이 True라면 매개변수 이름과 동일한 이름의 속성에 값을 저장하고, 어노테이션이 문자열이라면 그 문자열을 속성 이름으로 사용합니다. 그 결과 first=5는 a.first에 저장되고, second=6은 어노테이션 'example'에 따라 a.example에 저장됩니다.
마무리
함수 어노테이션은 그 자체로는 단순한 메타데이터이지만, 타입 힌트(PEP 484 이후 mypy 같은 정적 타입 검사 도구), 문서화, 데코레이터 기반 프레임워크 등 현대 Python 생태계 전반에서 폭넓게 활용되는 강력한 기반 기술입니다. 어노테이션의 본질을 정확히 이해해 두면 더 견고하고 유지보수하기 쉬운 코드를 작성하는 데 큰 도움이 됩니다.