Python

어노테이션 모범 사례

파이썬 3.10 이상 버전에서 객체의 어노테이션 딕셔너리 접근하기

파이썬 3.10에서 표준 라이브러리에 새로운 함수인 inspect.get_annotations() 가 추가되었습니다. 파이썬 3.10부터 3.13 버전까지 어노테이션을 지원하는 모든 객체의 어노테이션 딕셔너리에 접근할 때는 이 함수를 호출하는 것이 최선의 방법입니다. 또한, 이 함수는 문자열로 처리된(stringized) 어노테이션을

파이썬 3.14에서는 어노테이션 처리를 위한 새로운 annotationlib 모듈이 제공됩니다. 여기에는 inspect.get_annotations() 를 대체하는 annotationlib.get_annotations() 함수가 포함되어 있습니다.

어떠한 이유로 inspect.get_annotations() 를 사용할 수 없는 경우, __annotations__ 데이터 멤버에 직접 접근할 수도 있습니다. 이에 대한 모범 사례도 파이썬 3.10에서 변경되었습니다. 파이썬 3.10부터 o.__annotations__ 는 파이썬 함수, 클래스, 모듈의 경우 항상 작동함이 보장됩니다. 검사하려는 객체가 이 세 가지 중 하나라는 것이 확실하다면, 단순히 o.__annotations__ 를 사용하여 해당 객체의 어노테이션 딕셔너리를 가져올 수 있습니다.

그러나 다른 유형의 콜러블(예를 들어 functools.partial() 에 의해 생성된 호출 가능 객체 등)은 __annotations__ 속성이 정의되어 있지 않을 수 있습니다. 성격이 불분명한 객체의 __annotations__ 에 접근할 때 파이썬 3.10 이상 버전의 모범 사례는 세 개의 인자를 가진 getattr() 을 호출하는 것입니다(예: getattr(o, '__annotations__', None)).

파이썬 3.10 이전 버전에서는 어노테이션을 정의하지 않은 클래스에 접근할 때 부모 클래스가 어노테이션을 가지고 있으면 부모의 __annotations__ 를 반환했습니다. 파이썬 3.10 이후 버전부터는 자식 클래스의 어노테이션이 빈 딕셔너리로 반환됩니다.

파이썬 3.9 이하 버전에서 객체의 어노테이션 딕셔너리 접근하기

파이썬 3.9 이하 버전에서 객체의 어노테이션 딕셔너리에 접근하는 것은 최신 버전보다 훨씬 복잡합니다. 이는 해당 버전들의 설계상 결함으로, 특히 클래스 어노테이션과 관련된 문제입니다.

inspect.get_annotations() 을 사용하지 않는 경우를 제외하면, 다른 객체(함수, 기타 콜러블 및 모듈)의 어노테이션 딕셔너리에 접근하는 최선의 방법은 3.10 버전과 동일합니다. 즉, 세 개의 인자를 가진 getattr() 을 사용하여 해당 객체의 __annotations__ 속성에 접근해야 합니다.

불행히도 이는 클래스의 경우 모범 사례가 아닙니다. 클래스에서 __annotations__ 는 선택 사항이며 클래스는 베이스 클래스로부터 속성을 상속받을 수 있기 때문에, 특정 클래스의 __annotations__ 속성에 접근할 때 의도치 않게 베이즈 클래스 의 어노테이션 딕셔너리가 반환될 수 있습니다. 예시:

class Base:
    a: int = 3
    b: str = 'abc'

class Derived(Base):
    pass

print(Derived.__annotations__)

이 코드는 Derived 가 아닌 Base 의 어노테이션 딕셔너리를 출력합니다.

조사하려는 객체가 클래스인 경우(isinstance(o, type)), 코드를 분리하여 처리해야 합니다. 이 경우 파이썬 3.9 이전 버전의 구현 세부 사항을 활용하는 것이 모범 사례입니다. 즉, 클래스에 어노테이션이 정의되어 있다면 해당 내용은 클래스의 __dict__ 딕셔너리에 저장됩니다. 클래스에 어노테이션이 정의되었는지 여부를 알 수 없으므로, 클래스 딕셔너리에서 get() 메서드를 호출하는 것이 좋습니다.

종합하자면, 파이썬 3.9 이하 버전에서 임의의 객체에 대한 __annotations__ 속성에 안전하게 접근하는 예시 코드는 다음과 같습니다:

if isinstance(o, type):
    ann = o.__dict__.get('__annotations__', None)
else:
    ann = getattr(o, '__annotations__', None)

이 코드를 실행한 후, ann 은 딕셔너리 또는 None 이어야 합니다. 추가로 검사하기 전에 isinstance() 를 사용하여 ann 의 타입을 다시 한번 확인하는 것을 권장합니다.

일부 특이하거나 형식이 잘못된 타입 객체는 __dict__ 속성을 가지고 있지 않을 수 있으므로, 추가적인 안전성을 위해 getattr`를 사용하여 :attr:()!__dict__`에 접근하는 것이 좋을 수도 있습니다.

문자열화된 어노테이션 수동 역직렬화

어노테이션이 ‘문자열화’될 수 있는 상황에서 해당 문자열을 평가하여 파이썬 값을 생성해야 한다면, inspect.get_annotations() 를 호출하여 작업을 수행하는 것이 가장 좋습니다.

파이썬 3.9 이하 버전을 사용하거나 어떤 이유로 inspect.get_annotations() 를 사용할 수 없는 경우, 해당 기능을 직접 구현해야 합니다. 현재 파이썬 버전에서 inspect.get_annotations() 의 구현 방식을 검토하고 유사한 접근 방식을 따를 것을 권장합니다.

요약하자면, 임의의 객체 o 에서 문자열화된 어노테이션을 평가하려면 다음과 같이 하십시오.

  • o 가 모듈인 경우, eval() 을 호출할 때 o.__dict__globals 로 사용하십시오.

  • o 가 클래스인 경우, eval() 을 호출할 때 sys.modules[o.__module__].__dict__globals 로, dict(vars(o))locals 로 사용하십시오.

  • ofunctools.update_wrapper(), @functools.wraps, 또는 functools.partial() 을 사용하여 래핑된 콜러블인 경우, 실제 기반 함수를 찾을 때까지 상황에 따라 o.__wrapped__ 또는 o.func 에 접근하며 반복적으로 언랩(unwrap)하십시오.

  • o 가 호출 가능한 객체(하지만 클래스는 아닌 경우)인 경우, eval() 을 호출할 때 globalso.__globals__ 를 사용합니다.

그러나 어노테이션으로 사용되는 모든 문자열이 eval() 을 통해 파이썬 값으로 변환될 수 있는 것은 아닙니다. 이론적으로 문자열은 유효한 어떤 문자열도 포함할 수 있으며, 실제로는 특별히 평가될 수 없는 문자열 값을 사용하여 어노테이션을 매기도록 요구하는 타당한 타입 힌트 사례들이 있습니다. 예:

  • 파이썬 3.10에 지원되기 전의 | 를 사용한 PEP 604 유니온 타입.

  • 실행 시점에는 필요하지 않고 typing.TYPE_CHECKING 이 참일 때만 가져오는 정의들.

eval() 이 이러한 값들을 평가하려고 시도하면 실패하고 예외를 발생시킵니다. 따라서 어노테이션을 처리하는 라이브러리 API를 설계할 때는 호출자가 명시적으로 요청한 경우에만 문자열 값을 평가하도록 하는 것이 좋습니다.

모든 파이썬 버전에서의 __annotations__ 관련 모범 사례

  • 객체의 __annotations__ 멤버에 직접 값을 할당하는 것을 피하십시오. __annotations__ 설정은 파이썬이 관리하도록 내버려 두십시오.

  • 만약 객체의 __annotations__ 멤버에 직접 할당해야 한다면, 항상 dict 객체로 설정하십시오.

  • 어떤 객체에 대해서도 __annotations__ 에 직접 접근하는 것을 피하십시오. 대신 파이썬 3.14 이상에서는 annotationlib.get_annotations() 를, 파이썬 3.10 이상에서는 inspect.get_annotations() 를 사용하십시오.

  • 만약 객체의 __annotations__ 멤버에 직접 접근하는 경우, 내부 내용을 확인하기 전에 그것이 딕셔너리인지 먼저 확인하십시오.

  • __annotations__ 딕셔너리를 수정하는 것을 피하십시오.

  • 객체의 __annotations__ 속성을 삭제하는 것을 피하십시오.

__annotations__ 의 특성(quirks)

모든 파이썬 3 버전에서, 함수 객체에 어노테이션이 정의되지 않은 경우 어노테이션 딕셔너리가 지연 생성됩니다. del fn.__annotations__ 를 사용하여 __annotations__ 속성을 삭제할 수 있지만, 그 후 fn.__annotations__ 에 접근하면 객체는 새로운 빈 딕셔너리를 생성하여 저장하고 반환합니다. 함수가 어노테이션 딕셔너리를 지연 생성하기 전에 해당 속성을 삭제하려고 하면 AttributeError 가 발생하며, del fn.__annotations__ 를 연속으로 두 번 실행하는 경우에도 항상 AttributeError 가 발생합니다.

위 단락의 모든 내용은 파이썬 3.10 이후 버전의 클래스 및 모듈 객체에도 동일하게 적용됩니다.

모든 파이썬 3 버전에서 함수 객체의 __annotations__None 으로 설정할 수 있습니다. 그러나 그 이후 해당 객체의 어노테이션에 fn.__annotations__ 로 접근하면 이 섹션의 첫 번째 단락에 명시된 대로 빈 딕셔너리가 지연 생성됩니다. 이는 파이썬 모든 버전에서 모듈과 클래스의 경우에는 해당되지 않습니다. 모듈과 클래스 객체는 __annotations__ 를 어떠한 파이썬 값으로든 설정할 수 있으며, 설정된 값을 그대로 유지합니다.

파이썬이 어노테이션을 문자열로 처리하고(from __future__ import annotations 사용 시), 여러분이 어노테이션으로 문자열을 지정하면 해당 문자열은 따옴표가 둘러싸인 상태가 됩니다. 결과적으로 어노테이션은 두 번 인용되는 셈입니다. 예:

from __future__ import annotations
def foo(a: "str"): pass

print(foo.__annotations__)

이것은 {'a': "'str'"} 를 출력합니다. 이를 반드시 ‘특성(quirk)’으로 간주해야 하는 것은 아니지만, 당황스러울 수 있으므로 언급되었습니다.

If you use a class with a custom metaclass and access __annotations__ on the class, you may observe unexpected behavior; see 749 for some examples. You can avoid these quirks by using annotationlib.get_annotations() on Python 3.14+ or inspect.get_annotations() on Python 3.10+. On earlier versions of Python, you can avoid these bugs by accessing the annotations from the class’s __dict__ (for example, cls.__dict__.get('__annotations__', None)).

일부 파이썬 버전에서 클래스의 인스턴스가 __annotations__ 속성을 가질 수도 있습니다. 그러나 이는 지원되는 기능이 아닙니다. 인스턴스의 어노테이션이 필요한 경우, type`을 사용하여 해당 클래스에 접근하십시오(예: 파이썬 3.14 이상에서 ``annotationlib.get_annotations(type(myinstance))`()).