annotationlib — 애너테이션 조사 기능
annotationlib — 애너테이션 조사 기능 (Functionality for introspecting annotations)
annotationlib 모듈은 모듈·클래스·함수에 달린 **애너테이션(annotation)**을 들여다보는 도구를 제공해요. 애너테이션은 느리게(lazily) 평가되고 애너테이션이 만들어질 때 아직 정의되지 않은 객체에 대한 **전방 참조(forward reference)**를 자주 담고 있어요. 이 모듈은 그런 전방 참조와 기타 경계 상황에서도 애너테이션을 안정적으로 가져올 수 있는 저수준 도구들을 제공해요.
출처: Python 표준 라이브러리
본문
annotationlib 모듈은 모듈, 클래스, 함수에 달린 애너테이션을 들여다보는(introspect) 도구를 제공해요. 3.14 버전에서 추가됐어요. (소스 코드: Lib/annotationlib.py)
애너테이션은 느리게 평가되고, 애너테이션이 만들어질 때 아직 정의되지 않은 객체에 대한 전방 참조를 자주 포함해요. 이 모듈은 전방 참조와 다른 경계 상황이 있어도 애너테이션을 안정적으로 가져올 수 있는 저수준 도구 세트를 제공해요.
이 모듈은 애너테이션을 **세 가지 주요 형식(Format)**으로 가져오는 걸 지원해요. 각각 서로 다른 사용 사례에 잘 맞죠.
- VALUE — 애너테이션을 평가해서 그 값을 반환해요. 다루기 가장 직관적이지만, 예를 들어 애너테이션이 정의되지 않은 이름을 참조하면 오류를 발생시킬 수 있어요.
- FORWARDREF — 해석할 수 없는 애너테이션에 대해
ForwardRef객체를 반환해요. 평가하지 않고 애너테이션을 들여다볼 수 있게 해 주죠. 해결되지 않은 전방 참조가 있을 수 있는 애너테이션을 다룰 때 유용해요. - STRING — 애너테이션을 문자열로 반환해요. 소스 파일에 나타나는 모습과 비슷하죠. 애너테이션을 읽기 좋게 표시하고 싶은 문서 생성기 등에 유용해요.
get_annotations() 함수가 애너테이션을 가져오는 주 진입점이에요. 함수, 클래스, 모듈을 주면 요청한 형식의 애너테이션 사전을 반환해요. 이 모듈은 또한 애너테이션을 평가하는 데 쓰는 annotate 함수를 직접 다루는 기능(예: get_annotate_from_class_namespace(), call_annotate_function())과, evaluate 함수를 다루는 call_evaluate_function()을 제공해요.
주의: 이 모듈의 대부분 기능은 임의의 코드를 실행할 수 있어요. 보안 섹션을 꼭 확인하세요.
더 알아보기
- PEP 649 — Python에서 애너테이션이 작동하는 현재 모델을 제안했어요.
- PEP 749 — PEP 649의 여러 측면을 확장하고
annotationlib모듈을 소개했어요.- Annotations Best Practices — 애너테이션을 다루는 모범 사례.
- typing-extensions — 이전 Python 버전에서 동작하는
get_annotations()의 백포트를 제공해요.
애너테이션 의미론 (Annotation semantics)
애너테이션 평가 방식은 Python 3의 역사를 거치며 바뀌었고, 현재도 future import에 의존해요. 지금까지 세 가지 실행 모델이 있었어요.
- Stock 의미론(Python 3.0~3.13의 기본값; PEP 3107, PEP 526 참고) — 애너테이션을 소스 코드에서 만나는 즉시 적극적으로(eagerly) 평가해요.
- 문자열화된 애너테이션(Python 3.7 이상에서
from __future__ import annotations와 함께 사용; PEP 563 참고) — 애너테이션을 문자열로만 저장해요. - 지연 평가(Deferred evaluation)(Python 3.14 이상의 기본값; PEP 649, PEP 749 참고) — 애너테이션은 접근할 때만 느리게 평가돼요.
다음 프로그램을 예로 들어 볼게요.
def func(a: Cls) -> None:
print(a)
class Cls: pass
print(func.__annotations__)
이건 이렇게 동작해요.
- stock 의미론(Python 3.13 이하):
func가 정의되는 줄에서NameError를 던져요. 그 시점에Cls는 정의되지 않은 이름이니까요. - 문자열화된 애너테이션(
from __future__ import annotations사용 시):{'a': 'Cls', 'return': 'None'}을 출력해요. - 지연 평가(Python 3.14 이상):
{'a': <class 'Cls'>, 'return': None}을 출력해요.
함수 애너테이션이 Python 3.0(PEP 3107)에서 처음 도입됐을 때는 구현이 가장 단순하고 명확한 방식이라 stock 의미론이 쓰였어요. 변수 애너테이션이 Python 3.6(PEP 526)에 도입될 때도 같은 실행 모델이 사용됐죠. 하지만 애너테이션을 타입 힌트로 쓸 때 문제가 생겼어요. 애너테이션을 만날 때 아직 정의되지 않은 이름을 참조할 필요가 생기는 경우가 있거든요. 게다가 모듈 import 시점에 애너테이션을 실행하는 건 성능 문제도 있었어요. 그래서 Python 3.7에서 PEP 563이 from __future__ import annotations 문법으로 애너테이션을 문자열로 저장하는 기능을 소개했죠. 당시 계획은 언젠가 이 동작을 기본값으로 만들려는 거였는데, 문제가 생겼어요. 런타임에 애너테이션을 들여다보는 사람들에게 문자열화된 애너테이션은 처리하기 더 어렵거든요. 대안 제안인 PEP 649가 세 번째 실행 모델인 지연 평가를 소개했고 Python 3.14에서 구현됐어요. from __future__ import annotations가 있으면 여전히 문자열화된 애너테이션이 쓰이지만, 이 동작은 결국 제거될 거예요.
클래스 (Classes)
class annotationlib.Format
애너테이션이 반환될 수 있는 형식을 설명하는 IntEnum이에요. enum의 멤버나 그에 해당하는 정수 값을 get_annotations()와 이 모듈의 다른 함수, 그리고 __annotate__ 함수에 넘길 수 있어요.
VALUE = 1— 값은 애너테이션 표현식의 평가 결과예요.VALUE_WITH_FAKE_GLOBALS = 2— annotate 함수가 가짜 전역(fake globals)이 있는 특별한 환경에서 평가되고 있음을 알리는 특수 값이에요. 이 값을 받으면 annotate 함수는Format.VALUE형식과 같은 값을 반환하거나, 이 환경에서의 실행을 지원하지 않는다는 뜻으로NotImplementedError를 발생시켜야 해요. 이 형식은 내부적으로만 쓰이고 이 모듈의 함수에 넘기면 안 돼요.FORWARDREF = 3— 정의된 값은 실제 애너테이션 값(Format.VALUE형식대로), 정의되지 않은 값은ForwardRef프록시예요. 실제 객체들은ForwardRef프록시 객체에 대한 참조를 포함할 수 있어요.STRING = 4— 값은 애너테이션이 소스 코드에 나타나는 그대로의 텍스트 문자열이에요. 공백 정규화와 상수값 최적화 같은 수정은 포함될 수 있어요. 이 문자열들의 정확한 값은 향후 Python 버전에서 바뀔 수 있어요.
3.14 버전에서 추가.
class annotationlib.ForwardRef
애너테이션의 전방 참조를 위한 프록시 객체예요.
FORWARDREF 형식을 사용할 때 애너테이션이 해결할 수 없는 이름을 포함하면 이 클래스의 인스턴스가 반환돼요. 예를 들어 클래스가 정의되기 전에 참조되는 전방 참조가 애너테이션에 쓰였을 때 발생할 수 있어요.
-
__forward_arg__—ForwardRef를 생성하도록 평가된 코드를 담은 문자열이에요. 원래 소스와 정확히 같지 않을 수 있어요. -
evaluate(*, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE)— 전방 참조를 평가해서 그 값을 반환해요.format이VALUE(기본값)이면, 전방 참조가 해결할 수 없는 이름을 가리킬 때NameError같은 예외를 던질 수 있어요. 이 메서드의 인자들로 원래는 정의되지 않을 이름에 대한 바인딩을 제공할 수 있어요.format이FORWARDREF면 예외를 던지지 않고ForwardRef인스턴스를 반환할 수 있어요. 예를 들어 전방 참조 객체가list[undefined]코드를 담고 있고undefined가 정의되지 않은 이름이라면,FORWARDREF형식으로 평가하면list[ForwardRef('undefined')]를 반환해요.format이STRING이면__forward_arg__를 반환해요.owner매개변수는 이 메서드에 스코프 정보를 넘기는 기본 메커니즘이에요.ForwardRef의 owner는 그ForwardRef가 파생된 애너테이션을 담고 있는 객체, 즉 모듈 객체, 타입 객체, 함수 객체예요.globals,locals,type_params매개변수는ForwardRef가 평가될 때 사용할 수 있는 이름에 더 정밀하게 영향을 주는 메커니즘이에요.globals와locals는eval()에 전달돼, 이름이 평가되는 전역·지역 네임스페이스를 나타내요.type_params는 제네릭 클래스와 함수의 네이티브 문법으로 만들어진 객체와 관련돼요. 전방 참조가 평가되는 동안 스코프에 있는 타입 매개변수의 튜플이에요. 예를 들어 제네릭 클래스C의 클래스 네임스페이스에 있는 애너테이션에서 가져온ForwardRef를 평가한다면,type_params는C.__type_params__로 설정해야 해요.get_annotations()가 반환한ForwardRef인스턴스는 자신이 유래한 스코프에 대한 정보의 참조를 유지해요. 그래서 추가 인자 없이 이 메서드를 호출해도 그런 객체를 평가할 수 있어요. 다른 방식으로 만든ForwardRef인스턴스는 스코프에 대한 정보가 없을 수 있으니, 성공적으로 평가하려면 이 메서드에 인자를 넘겨야 할 수 있어요.owner,globals,locals,type_params가 모두 주어지지 않고ForwardRef가 자기 기원에 대한 정보도 담고 있지 않으면, 빈globals와locals사전이 사용돼요.3.14 버전에서 추가.
함수 (Functions)
annotationlib.annotations_to_string(annotations)
런타임 값을 담은 애너테이션 사전을, 문자열만 담은 사전으로 변환해요. 값이 이미 문자열이 아니면 type_repr()을 사용해 변환해요. STRING 형식을 지원하지만 애너테이션을 만드는 코드에 접근할 수 없는, 사용자가 제공한 annotate 함수를 위한 헬퍼예요. 예를 들어 함수형 문법으로 만든 typing.TypedDict 클래스의 STRING 형식을 구현하는 데 쓰여요.
>>> from typing import TypedDict
>>> Movie = TypedDict("movie", {"name": str, "year": int})
>>> get_annotations(Movie, format=Format.STRING)
{'name': 'str', 'year': 'int'}
3.14 버전에서 추가.
annotationlib.call_annotate_function(annotate, format, *, owner=None)
주어진 format(Format enum의 멤버)으로 annotate 함수 annotate를 호출하고, 함수가 만들어낸 애너테이션 사전을 반환해요.
이 헬퍼 함수가 필요한 이유는, 컴파일러가 함수·클래스·모듈용으로 생성한 annotate 함수는 직접 호출하면 VALUE 형식만 지원하기 때문이에요. 다른 형식을 지원하기 위해 이 함수는 annotate 함수를 특별한 환경에서 호출해서 다른 형식으로 애너테이션을 만들 수 있게 해요. 클래스가 구성되는 동안 애너테이션을 부분적으로 평가해야 하는 기능을 구현할 때 유용한 빌딩 블록이에요.
owner는 애너테이션 함수를 소유한 객체(보통 함수, 클래스, 모듈)예요. 주어지면 FORWARDREF 형식에서 더 많은 정보를 담은 ForwardRef 객체를 만드는 데 사용돼요.
더 알아보기 — 이 함수가 쓰는 구현 기법에 대한 설명은 PEP 649 에 있어요.
3.14 버전에서 추가.
annotationlib.call_evaluate_function(evaluate, format, *, owner=None)
주어진 format(Format enum의 멤버)으로 evaluate 함수 evaluate를 호출하고 함수가 만들어낸 값을 반환해요. call_annotate_function()과 비슷하지만, 후자는 항상 문자열을 애너테이션에 매핑하는 사전을 반환하는 반면 이 함수는 단일 값을 반환해요.
이 함수는 타입 별칭과 타입 매개변수와 관련된 지연 평가 요소를 위해 생성된 evaluate 함수와 함께 쓰도록 설계됐어요.
typing.TypeAliasType.evaluate_value()— 타입 별칭의 값typing.TypeVar.evaluate_bound()— 타입 변수의 boundtyping.TypeVar.evaluate_constraints()— 타입 변수의 constraintstyping.TypeVar.evaluate_default()— 타입 변수의 기본값typing.ParamSpec.evaluate_default()— 파라미터 명세의 기본값typing.TypeVarTuple.evaluate_default()— 타입 변수 튜플의 기본값
owner는 evaluate 함수를 소유한 객체, 예를 들어 타입 별칭이나 타입 변수 객체예요. format은 값이 반환되는 형식을 제어해요.
>>> type Alias = undefined
>>> call_evaluate_function(Alias.evaluate_value, Format.VALUE)
Traceback (most recent call last):
...
NameError: name 'undefined' is not defined
>>> call_evaluate_function(Alias.evaluate_value, Format.FORWARDREF)
ForwardRef('undefined')
>>> call_evaluate_function(Alias.evaluate_value, Format.STRING)
'undefined'
3.14 버전에서 추가.
annotationlib.get_annotate_from_class_namespace(namespace)
클래스 네임스페이스 사전 namespace에서 annotate 함수를 가져와요. 네임스페이스에 annotate 함수가 없으면 None을 반환해요. 주로 클래스가 완전히 만들어지기 전(예: 메타클래스에서)에 유용해요. 클래스가 존재한 뒤에는 cls.__annotate__로 annotate 함수를 가져올 수 있어요. 메타클래스에서 이 함수를 쓰는 예시는 아래에 있어요.
3.14 버전에서 추가.
annotationlib.get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE)
객체의 애너테이션 사전을 계산해요.
obj는 callable, 클래스, 모듈, 혹은 __annotate__ 또는 __annotations__ 속성을 가진 다른 객체일 수 있어요. 다른 객체를 넘기면 TypeError가 발생해요.
format 매개변수는 애너테이션이 반환되는 형식을 제어하고, Format enum의 멤버나 그에 해당하는 정수여야 해요. 각 형식은 이렇게 작동해요.
- VALUE:
object.__annotations__를 먼저 시도하고, 존재하지 않으면object.__annotate__함수를 호출해요(존재한다면). - FORWARDREF:
object.__annotations__가 존재하고 성공적으로 평가될 수 있으면 그것을 사용하고, 아니면object.__annotate__함수를 호출해요. 그것도 존재하지 않으면object.__annotations__를 다시 시도하고, 접근 중 발생한 오류를 다시 발생시켜요.object.__annotate__를 호출할 때는 먼저FORWARDREF로 호출해요. 구현되지 않았다면VALUE_WITH_FAKE_GLOBALS가 지원되는지 확인하고 가짜 전역 환경에서 그것을 사용해요. 두 형식 모두 지원되지 않으면VALUE를 사용하는 것으로 넘어가요.VALUE가 실패하면 이 호출의 오류가 발생해요. - STRING:
object.__annotate__가 존재하면 먼저 호출하고, 아니면object.__annotations__를 사용해annotations_to_string()으로 문자열화해요.object.__annotate__를 호출할 때는 먼저STRING으로 호출해요. 구현되지 않았다면VALUE_WITH_FAKE_GLOBALS가 지원되는지 확인하고 가짜 전역 환경에서 그것을 사용해요. 두 형식 모두 지원되지 않으면VALUE를 사용하고 결과를annotations_to_string()으로 변환해요.VALUE가 실패하면 이 호출의 오류가 발생해요.
dict를 반환해요. get_annotations()는 호출할 때마다 새 dict를 반환해요. 같은 객체에 두 번 호출하면 서로 다르지만 동등한 dict 두 개를 반환하는 거죠.
이 함수는 여러 세부 사항을 자동으로 처리해줘요.
eval_str이 참이면str타입의 값이eval()로 언스트링화(un-stringize)돼요. 문자열화된 애너테이션(from __future__ import annotations)과 함께 쓰려는 용도예요.Format.VALUE이외의 형식에서eval_str을 참으로 설정하는 것은 오류예요.obj에 애너테이션 사전이 없으면 빈 dict를 반환해요. (함수와 메서드는 항상 애너테이션 사전을 가지고, 클래스·모듈·다른 종류의 callable은 없을 수 있어요.)- 클래스의 상속된 애너테이션과 메타클래스의 애너테이션은 무시해요. 클래스가 자기 자신의 애너테이션 사전이 없으면 빈 dict를 반환해요.
- 객체 멤버와 dict 값에 대한 모든 접근은 안전을 위해
getattr()과dict.get()을 사용해요.
eval_str은 str 타입의 값이 eval() 호출 결과로 대체되는지 여부를 제어해요.
eval_str이 참이면str타입의 값에eval()을 호출해요. (get_annotations()는 예외를 잡지 않아요.eval()이 예외를 발생시키면get_annotations()호출을 지나 스택을 풀어요.)eval_str이 거짓(기본값)이면str타입의 값은 그대로예요.
globals와 locals는 eval()에 전달돼요. 자세한 내용은 eval() 문서를 보세요. globals나 locals가 None이면, 이 함수는 type(obj)에 따라 문맥별 기본값으로 그 값을 대체할 수 있어요.
obj가 모듈이면globals는 기본적으로obj.__dict__예요.obj가 클래스이면globals는 기본적으로sys.modules[obj.__module__].__dict__,locals는 기본적으로obj클래스 네임스페이스예요.obj가 callable이면globals는 기본적으로obj.__globals__예요. 다만obj가 래핑된 함수(functools.update_wrapper()사용)나functools.partial객체라면, 래핑되지 않은 함수를 찾을 때까지 언랩해요.
어떤 객체의 애너테이션 사전에 접근할 때 get_annotations()를 호출하는 것이 모범 사례예요. 애너테이션 모범 사례에 대한 자세한 내용은 Annotations Best Practices 를 보세요.
>>> def f(a: int, b: str) -> float:
... pass
>>> get_annotations(f)
{'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}
3.14 버전에서 추가.
annotationlib.type_repr(value)
임의의 Python 값을 STRING 형식에 쓰기 적합한 형식으로 변환해요. 대부분의 객체에 repr()을 호출하지만, 타입 객체 같은 일부 객체는 특별히 처리해요. STRING 형식을 지원하지만 애너테이션을 만드는 코드에 접근할 수 없는, 사용자가 제공한 annotate 함수를 위한 헬퍼예요. 애너테이션에서 흔히 만나는 값을 담은 다른 객체에 사용자 친화적인 문자열 표현을 제공하는 데도 쓸 수 있어요.
3.14 버전에서 추가.
레시피 (Recipes)
메타클래스에서 애너테이션 사용하기
메타클래스는 클래스 생성 중에 클래스 본문의 애너테이션을 검사하거나 수정하고 싶을 수 있어요. 그러려면 클래스 네임스페이스 사전에서 애너테이션을 가져와야 해요. from __future__ import annotations로 만든 클래스는 애너테이션이 사전의 __annotations__ 키에 있어요. 애너테이션이 있는 다른 클래스는 get_annotate_from_class_namespace()로 annotate 함수를, call_annotate_function()으로 그것을 호출해 애너테이션을 가져올 수 있어요. 보통 FORWARDREF 형식을 쓰는 게 가장 좋아요. 클래스가 만들어질 때 아직 해결할 수 없는 이름을 애너테이션이 참조할 수 있게 해 주거든요.
애너테이션을 수정하려면 원래 annotate 함수를 호출하고 필요한 조정을 한 다음 결과를 반환하는 래퍼 annotate 함수를 만드는 게 가장 좋아요.
아래는 클래스에서 모든 typing.ClassVar 애너테이션을 걸러내 별도 속성에 넣는 메타클래스 예시예요.
import annotationlib
import typing
class ClassVarSeparator(type):
def __new__(mcls, name, bases, ns):
if "__annotations__" in ns: # from __future__ import annotations
annotations = ns["__annotations__"]
classvar_keys = {
key for key, value in annotations.items()
# Use string comparison for simplicity; a more robust solution
# could use annotationlib.ForwardRef.evaluate
if value.startswith("ClassVar")
}
classvars = {key: annotations[key] for key in classvar_keys}
ns["__annotations__"] = {
key: value for key, value in annotations.items()
if key not in classvar_keys
}
wrapped_annotate = None
elif annotate := annotationlib.get_annotate_from_class_namespace(ns):
annotations = annotationlib.call_annotate_function(
annotate, format=annotationlib.Format.FORWARDREF
)
classvar_keys = {
key for key, value in annotations.items()
if typing.get_origin(value) is typing.ClassVar
}
classvars = {key: annotations[key] for key in classvar_keys}
def wrapped_annotate(format):
annos = annotationlib.call_annotate_function(annotate, format, owner=typ)
return {key: value for key, value in annos.items() if key not in classvar_keys}
else: # no annotations
classvars = {}
wrapped_annotate = None
typ = super().__new__(mcls, name, bases, ns)
if wrapped_annotate is not None:
# Wrap the original __annotate__ with a wrapper that removes ClassVars
typ.__annotate__ = wrapped_annotate
typ.classvars = classvars # Store the ClassVars in a separate attribute
return typ
커스텀 callable annotate 함수 만들기
커스텀 annotate 함수는 함수·클래스·모듈을 위해 자동 생성되는 것 같은 리터럴 함수일 수 있어요. 또는 클래스가 제공하는 캡슐화를 활용하고 싶을 수도 있는데, 그 경우 어떤 callable이든 annotate 함수로 쓸 수 있어요.
VALUE, STRING, FORWARDREF 형식을 직접 제공하려면, annotate 함수는 다음 속성을 제공해야 해요.
- 시그니처가
__call__(format, /) -> dict인 callable__call__— 지원하는 형식으로 호출될 때NotImplementedError를 발생시키지 않아요.
VALUE_WITH_FAKE_GLOBALS 형식(직접 지원하지 않을 때 STRING이나 FORWARDREF를 자동 생성하는 데 사용)을 제공하려면 annotate 함수는 다음 속성을 제공해야 해요.
- 시그니처가
__call__(format, /) -> dict인 callable__call__—VALUE_WITH_FAKE_GLOBALS로 호출될 때NotImplementedError를 발생시키지 않아요. - annotate 함수의 컴파일된 코드를 담은 code 객체
__code__. - 선택:
__code__가 나타내는 함수가 위치 기본값을 쓰면, 함수의 위치 기본값의 튜플__kwdefaults__. - 선택:
__code__가 나타내는 함수가 키워드 기본값을 쓰면, 함수의 키워드 기본값의 dict__defaults__. - 선택: 기타 모든 함수 속성.
class Annotate:
called_formats = []
def __call__(self, format=None, /, *, _self=None):
# When called with fake globals, `_self` will be the
# actual self value, and `self` will be the format.
if _self is not None:
self, format = _self, self
self.called_formats.append(format)
if format <= 2: # VALUE or VALUE_WITH_FAKE_GLOBALS
return {"x": MyType}
raise NotImplementedError
__code__ = __call__.__code__
__defaults__ = (None,)
__kwdefaults__ = property(lambda self: dict(_self=self))
__globals__ = {}
__builtins__ = {}
__closure__ = None
이것은 이렇게 호출할 수 있어요.
>>> from annotationlib import call_annotate_function, Format
>>> call_annotate_function(Annotate(), format=Format.STRING)
{'x': 'MyType'}
또는 객체의 annotate 함수로 쓸 수 있어요.
>>> from annotationlib import get_annotations, Format
>>> class C:
... pass
>>> C.__annotate__ = Annotate()
>>> get_annotations(Annotate(), format=Format.STRING)
{'x': 'MyType'}
STRING 형식의 한계
STRING 형식은 애너테이션의 소스 코드를 근사하려고 하지만, 사용되는 구현 전략 때문에 정확한 소스 코드를 항상 복구할 수는 없어요.
첫째, 문자열화기(stringifier)는 컴파일된 코드에 없는 정보 — 주석, 공백, 괄호, 컴파일러가 단순화하는 연산 — 는 당연히 복구할 수 없어요.
둘째, 문자열화기는 어떤 스코프에서 이름을 찾는 거의 모든 연산을 가로챌 수 있지만, 완전히 상수에 대해 동작하는 연산은 가로챌 수 없어요. 결과적으로 신뢰할 수 없는 코드에 STRING 형식을 요청하는 건 안전하지 않아요. Python은 충분히 강력해서 전역이나 내장에 접근할 수 없어도 임의 코드 실행을 달성할 수 있거든요. 예를 들면:
>>> def f(x: (1).__class__.__base__.__subclasses__()[-1].__init__.__builtins__["print"]("Hello world")): pass
...
>>> annotationlib.get_annotations(f, format=annotationlib.Format.STRING)
Hello world
{'x': 'None'}
참고: 이 특정 예시는 (글을 쓰는 시점에) 동작하지만, 구현 세부 사항에 의존하므로 미래에 동작한다는 보장은 없어요.
ast 모듈로 표현되는 Python의 다양한 표현식 중 일부는 지원돼요. 즉 STRING 형식이 일반적으로 원본 소스 코드를 복구할 수 있다는 뜻이에요. 나머지는 지원되지 않아 잘못된 출력이나 오류가 발생할 수 있어요.
지원되는 것(가끔 주의점 포함):
ast.BinOp, ast.UnaryOp (ast.Invert(~), ast.UAdd(+), ast.USub(-)는 지원, ast.Not(not)은 미지원), ast.Dict(** 언패킹 사용 시 제외), ast.Set, ast.Compare (ast.Eq와 ast.NotEq 지원, ast.Lt, ast.LtE, ast.Gt, ast.GtE는 지원하되 피연산자가 뒤집힐 수 있음, ast.Is, ast.IsNot, ast.In, ast.NotIn은 미지원), ast.Call(** 언패킹 사용 시 제외), ast.Constant(정확한 표현은 아님 — 예: 문자열의 이스케이프 시퀀스는 사라지고, 16진수는 10진수로 변환), ast.Attribute(값이 상수가 아닐 때), ast.Subscript(값이 상수가 아닐 때), ast.Starred(* 언패킹), ast.Name, ast.List, ast.Tuple, ast.Slice.
지원되지 않지만 문자열화기가 만나면 정보성 있는 오류를 던지는 것:
ast.FormattedValue(f-strings; !r 같은 변환 지정자를 쓰면 오류가 감지되지 않음), ast.JoinedStr(f-strings).
지원되지 않고 잘못된 출력을 내는 것:
ast.BoolOp(and와 or), ast.IfExp, ast.Lambda, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp.
애너테이션 스코프에서 허용되지 않아 관련이 없는 것:
ast.NamedExpr(:=), ast.Await, ast.Yield, ast.YieldFrom.
FORWARDREF 형식의 한계
FORWARDREF 형식은 가능한 한 실제 값을 만들려고 하고, 해결할 수 없는 것은 ForwardRef 객체로 대체해요. STRING 형식과 대체로 같은 한계의 영향을 받아요. 리터럴에 대한 연산을 수행하거나 지원되지 않는 표현식 타입을 쓰는 애너테이션은 FORWARDREF 형식으로 평가할 때 예외를 발생시킬 수 있어요.
지원되지 않는 표현식에서의 동작 예시를 몇 개 볼게요.
>>> from annotationlib import get_annotations, Format
>>> def zerodiv(x: 1 / 0): ...
>>> get_annotations(zerodiv, format=Format.STRING)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>> get_annotations(zerodiv, format=Format.FORWARDREF)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
>>> def ifexp(x: 1 if y else 0): ...
>>> get_annotations(ifexp, format=Format.STRING)
{'x': '1'}
애너테이션을 들여다볼 때의 보안 의미
이 모듈의 많은 기능은 애너테이션과 관련된 코드를 실행하는 것을 포함하고, 그 코드는 임의의 일을 할 수 있어요. 예를 들어 get_annotations()는 임의의 annotate 함수를 호출할 수 있고, ForwardRef.evaluate()는 임의의 문자열에 eval()을 호출할 수 있어요. 애너테이션에 담긴 코드는 임의의 시스템 호출을 하거나, 무한 루프에 빠지거나, 그 밖의 어떤 연산도 수행할 수 있어요. 이는 __annotations__ 속성에 대한 어떤 접근에도, 그리고 typing.get_type_hints()처럼 애너테이션을 다루는 typing 모듈의 여러 함수에도 마찬가지로 해당돼요.
이로부터 발생하는 어떤 보안 문제도, 신뢰할 수 없는 애너테이션을 포함할 수 있는 코드를 import한 직후에 적용돼요. import는 언제나 임의의 연산을 수행하게 할 수 있으니까요. 하지만 신뢰할 수 없는 출처의 문자열이나 다른 입력을 받아 애너테이션 조사 API 중 하나에 넘기는 것은 안전하지 않아요. 예를 들어 __annotations__ 사전을 편집하거나 ForwardRef 객체를 직접 만드는 식으로요.