상위 목록: 하위 목록: 작성 날짜: 읽는 데 8 분 소요

함수 주석(Function Annotations)

함수 주석(Function Annotations)은 함수의 매개변수와 반환값에 주석(Annotations)을 작성합니다.

#으로 작성하는 일반 주석과 달리, 함수 주석은 함수 객체에 정보로 저장되며 코드에서 다시 읽어 사용할 수 있습니다.

단, 주석이므로 강제성은 없어 무시하고 사용할 수 있습니다. Python 인터프리터는 주석에 작성된 형식을 검사하지 않습니다.

함수 주석에는 주로 형식(Type)을 작성하며, 이를 타입 힌트(Type Hint)라 부릅니다.

Python은 변수에 형식을 선언하지 않는 언어이므로, def area(width, height)만 보고는 width에 정수를 넣어야 하는지 문자열을 넣어야 하는지 알기 어렵습니다. 함수 주석은 이러한 정보를 함수 정의에 직접 기록해, 함수를 사용하는 사람이 설명 문서를 찾지 않아도 의도를 알 수 있게 합니다.

또한 IDE의 자동 완성, mypy 같은 정적 검사 도구, 문서 생성 도구가 이 주석을 읽어 잘못된 형식의 값을 전달하는 실수를 실행 전에 찾아 줍니다.

이번 강좌에서는 함수 주석을 작성하는 방법, 주석이 저장되는 위치, 그리고 Python 3.14에서 바뀐 주석의 평가 시점과 이를 다루는 annotationlib 모듈을 다룹니다.

  • Tip : typing 모듈, list[int], int | None 같은 타입 힌트 문법은 제 49강에서 자세히 다룹니다. 이번 강좌에서는 주석이 저장되고 평가되는 방식을 중심으로 설명합니다.



함수 주석 작성

함수 주석은 매개변수 이름 뒤의 콜론(:)과, 매개변수 목록을 닫는 괄호 뒤의 화살표(->)로 작성합니다.

def func(a: str, b: float = 3.5) -> int:
    return a + b


value = func(3)
print(value)
print(func.__annotations__)
결과
6.5
{‘a’: <class ‘str’>, ‘b’: <class ‘float’>, ‘return’: <class ‘int’>}

함수에 직접 주석을 작성할 수 있습니다.

콜론(:)을 사용해 매개변수에 할당되어야 하는 형식을 할당합니다.

->(화살표)를 사용해 반환값에 할당되어야 하는 형식을 할당합니다.

매개변수에 기본값을 할당하는 경우, 변수: 주석 = 기본값의 형태로 할당합니다.

매개변수에 할당된 형식을 맞추지 않아도 되며, 반환되는 값의 형식이 변환되어 반환되지 않습니다.

예제는 a에 str이 아닌 3을 전달했고, int가 아닌 6.5를 반환했지만 오류가 발생하지 않습니다.

func(3)은 주석과 관계없이 3 + 3.5를 그대로 계산해 6.5를 반환합니다. 주석이 int라고 해서 결과가 6으로 바뀌지도 않습니다.

작성된 주석은 함수의 __annotations__ 속성에 사전 형태로 저장됩니다. 반환값의 주석은 'return' 키로 저장됩니다.

return은 Python의 예약어이므로 매개변수 이름으로 사용할 수 없습니다. 그러므로 반환값의 주석을 'return' 키에 저장해도 매개변수의 주석과 겹치지 않습니다.

  • Tip : PEP 8에서는 a: str처럼 콜론 뒤에만 공백을 두고, 주석과 기본값을 함께 작성할 때는 b: float = 3.5처럼 = 앞뒤에 공백을 두는 형태를 권장합니다. 주석이 없는 기본값은 b=3.5처럼 공백 없이 작성합니다.


def total(*args: int, scale: float = 1.0, **kwargs: str) -> float:
    return sum(args) * scale


print(total(1, 2, 3, scale=0.5))
print(total.__annotations__)


def greet(name, times: int = 1):
    return name * times


print(greet.__annotations__)
print(greet.__defaults__)
결과
3.0
{‘args’: <class ‘int’>, ‘scale’: <class ‘float’>, ‘kwargs’: <class ‘str’>, ‘return’: <class ‘float’>}
{‘times’: <class ‘int’>}
(1,)

*args와 **kwargs에도 주석을 작성할 수 있습니다.

*args: int는 args 전체가 정수라는 의미가 아니라, 전달되는 각각의 위치 인자가 정수라는 의미입니다. args 자체는 항상 튜플입니다. 마찬가지로 **kwargs: str은 각 키워드 인자의 값이 문자열이라는 의미입니다.

greet처럼 일부 매개변수에만 주석을 작성할 수도 있습니다. 주석을 작성하지 않은 name과 반환값은 __annotations__에 포함되지 않습니다.

기본값은 주석과 별도로 __defaults__에 저장되므로, times의 기본값 1은 __annotations__가 아닌 __defaults__에서 확인할 수 있습니다.


주석은 검사되지 않습니다

함수 주석은 기록일 뿐, 실행 중에 형식을 검사하지 않습니다.

실행 중에 형식을 반드시 확인해야 한다면, __annotations__를 읽어 직접 검사하는 코드를 작성할 수 있습니다.


import inspect


def check_types(func, *args, **kwargs):
    bound = inspect.signature(func).bind(*args, **kwargs)
    for name, value in bound.arguments.items():
        expected = func.__annotations__.get(name)
        if expected is not None and not isinstance(value, expected):
            print(f"{name}: {expected.__name__} 필요, {type(value).__name__} 전달")
            return False
    return True


def area(width: int, height: int) -> int:
    return width * height


print(check_types(area, 3, 4))
print(check_types(area, 3, height="4"))
결과
True
height: int 필요, str 전달
False

inspect.signature(func).bind()는 전달한 인자를 어느 매개변수에 들어가는지 짝지어 줍니다. check_types(area, 3, height="4")라면 {'width': 3, 'height': '4'}가 됩니다.

반복문에서 매개변수 이름으로 __annotations__의 형식을 꺼내고, isinstance()로 실제 값과 비교합니다. height에 문자열 "4"가 전달되었으므로 메시지를 출력하고 False를 반환합니다.

이처럼 주석을 실제로 활용하는 것은 주석을 읽는 코드의 역할이며, 인터프리터 자체는 아무것도 검사하지 않습니다. 실무에서는 직접 검사하기보다 mypy 같은 정적 검사 도구로 실행 전에 확인하는 경우가 많습니다.

  • Tip : 예제의 isinstance() 검사는 int, str 같은 단순한 형식에서만 동작합니다. list[int]처럼 제 49강의 제네릭 형식은 isinstance()로 검사할 수 없습니다.



클래스 속성의 주석

class Point:
    x: int
    y: int = 0


print(Point.__annotations__)
print(Point.y)
print(hasattr(Point, "x"))
결과
{‘x’: <class ‘int’>, ‘y’: <class ‘int’>}
0
False

함수 주석과 같은 문법으로 변수에도 주석을 작성할 수 있습니다. 클래스 본문에 작성한 변수 주석은 클래스의 __annotations__에 저장됩니다.

주의할 점은 x: int처럼 값 없이 주석만 작성하면 변수가 만들어지지 않는다는 것입니다. __annotations__에는 x가 있지만, hasattr(Point, "x")는 False입니다.

y: int = 0처럼 값을 함께 할당해야 Point.y로 사용할 수 있는 클래스 속성이 만들어집니다.

이 형태는 제 50강의 데이터 클래스(Data Class)에서 속성의 목록과 형식을 선언할 때 사용됩니다.



주석의 지연 평가(Deferred Evaluation)

주석에 작성한 int, str은 문자열이 아니라 실제로 계산되는 Python 식입니다. 그러므로 주석에 함수 호출을 작성하면 그 함수가 실제로 실행됩니다.

이 식이 언제 계산되는지가 Python 3.14에서 크게 바뀌었습니다.


def check():
    print("주석 평가")
    return int


def func(x: check()) -> None:
    pass


print("함수 정의 완료")
print(func.__annotations__)
print(func.__annotations__)
결과
함수 정의 완료
주석 평가
{‘x’: <class ‘int’>, ‘return’: None}
{‘x’: <class ‘int’>, ‘return’: None}

Python 3.14 이상부터 주석은 함수를 정의할 때 계산되지 않고, 주석이 처음 필요할 때 계산됩니다. 이를 지연 평가(Deferred Evaluation)라 하며, PEP 649와 PEP 749에서 정의되었습니다.

예제에서 check()는 함수를 정의할 때가 아닌, func.__annotations__에 처음 접근할 때 실행됩니다.

한 번 계산된 결과는 저장되므로, 두 번째로 접근할 때는 check()가 다시 실행되지 않습니다.

Python 3.13 이하에서는 함수를 정의하는 순간 주석이 계산되어, 주석 평가가 함수 정의 완료보다 먼저 출력됩니다.

결과를 순서대로 읽으면, def func 문을 실행해도 아무것도 출력되지 않고 함수 정의 완료가 먼저 출력됩니다. 첫 번째 func.__annotations__에서 비로소 check()가 실행되어 주석 평가가 출력되고, 두 번째 접근에서는 저장된 사전만 출력됩니다.

반환값의 주석 None은 None 값 그대로 저장됩니다. 형식이 아닌 None을 작성해도 “반환값이 없다”는 의미로 사용합니다.


class Node:
    def connect(self, other: Node) -> Node:
        return other


print(Node.connect.__annotations__)
결과
{‘other’: <class ‘__main__.Node’>, ‘return’: <class ‘__main__.Node’>}

지연 평가 덕분에 아직 정의되지 않은 이름을 주석에 사용해도 함수를 정의할 때 오류가 발생하지 않습니다.

connect 메서드를 정의하는 시점에는 Node 클래스가 아직 완성되지 않았지만, 주석에 Node를 바로 작성할 수 있습니다.

Python 3.13 이하에서는 같은 코드가 NameError를 발생시켜 other: "Node"처럼 문자열로 작성해야 했습니다.

단, 주석에 접근하는 시점에도 이름이 정의되어 있지 않다면 __annotations__에 접근할 때 NameError가 발생합니다.


def func(x: undefined_name) -> None:
    pass


print("정의 완료")
try:
    print(func.__annotations__)
except NameError as e:
    print("NameError:", e)
결과
정의 완료
NameError: name ‘undefined_name’ is not defined

undefined_name은 코드 어디에도 정의되지 않은 이름입니다.

Python 3.14에서는 함수 정의가 정상적으로 끝나 정의 완료가 출력되고, func.__annotations__에 접근하는 순간 NameError가 발생합니다. 즉, 오류가 발생하는 시점이 함수 정의에서 주석 접근으로 옮겨진 것입니다.

같은 코드를 Python 3.12에서 실행하면 첫 줄의 def 문에서 바로 NameError가 발생해 정의 완료도 출력되지 않습니다.

주석에 정의되지 않은 이름이 있을 수 있는 경우에는, 다음 절의 annotationlib를 사용하면 오류 없이 주석을 읽을 수 있습니다.


지연 평가의 동작 원리

지연 평가는 주석을 바로 계산하는 대신, 주석을 계산하는 함수를 만들어 두는 방식으로 동작합니다.

Python 3.14에서는 함수를 정의하면 주석 식을 담은 __annotate__ 함수가 함께 만들어집니다. __annotations__에 처음 접근하면 이 함수가 호출되어 사전이 만들어지고, 그 결과가 저장됩니다.

그러므로 주석을 한 번도 읽지 않는 프로그램은 주석을 계산하는 비용도 들지 않으며, 순환 참조처럼 서로를 주석에 사용하는 클래스도 순서에 상관없이 정의할 수 있습니다.



annotationlib 모듈

from annotationlib import get_annotations, Format


def connect(a: int, b: Later) -> None:
    pass


print(get_annotations(connect, format=Format.STRING))

annotations = get_annotations(connect, format=Format.FORWARDREF)
print(annotations["a"])
print(annotations["b"])

try:
    get_annotations(connect, format=Format.VALUE)
except NameError as e:
    print("NameError:", e)


class Later:
    pass


print(get_annotations(connect))
print(annotations["b"].evaluate())
결과
{‘a’: ‘int’, ‘b’: ‘Later’, ‘return’: ‘None’}
<class ‘int’>
ForwardRef(‘Later’, owner=<function connect at 0x000001FC6625C300>)
NameError: name ‘Later’ is not defined
{‘a’: <class ‘int’>, ‘b’: <class ‘__main__.Later’>, ‘return’: None}
<class ‘__main__.Later’>

Python 3.14 이상부터 주석을 읽는 표준 모듈인 annotationlib가 추가되었습니다.

get_annotations(객체, format=형식)으로 함수, 클래스, 모듈의 주석을 원하는 형식으로 가져올 수 있습니다.

Format.VALUE는 기본값으로, 주석을 실제 값으로 계산합니다. 정의되지 않은 이름이 있으면 NameError가 발생하며, __annotations__에 접근하는 것과 같습니다.

Format.FORWARDREF는 계산할 수 있는 이름은 실제 값으로, 계산할 수 없는 이름은 ForwardRef 객체로 반환합니다. ForwardRef는 나중에 evaluate() 메서드로 계산할 수 있습니다.

Format.STRING은 주석을 계산하지 않고, 소스 코드에 작성된 형태의 문자열로 반환합니다.

ForwardRef의 owner에 표시되는 메모리 주소는 실행할 때마다 달라집니다.

결과를 한 줄씩 읽으면 다음과 같습니다.

  1. Format.STRING은 아무것도 계산하지 않으므로, Later가 정의되지 않았어도 모든 주석을 문자열로 반환합니다.
  2. Format.FORWARDREF에서 a는 계산할 수 있으므로 int 클래스가, b는 아직 정의되지 않았으므로 ForwardRef('Later', ...)가 반환됩니다.
  3. Format.VALUE는 Later를 계산할 수 없어 NameError가 발생합니다.
  4. Later 클래스를 정의한 뒤에는 기본 형식으로도 모든 주석을 계산할 수 있습니다.
  5. 앞에서 저장해 둔 ForwardRef도 evaluate()를 호출하면 이제 Later 클래스로 계산됩니다.


형식 계산 여부 정의되지 않은 이름 사용하는 경우
Format.VALUE 계산 NameError 발생 모든 이름이 정의된 뒤 실제 형식이 필요할 때
Format.FORWARDREF 가능한 것만 계산 ForwardRef로 반환 일부 이름이 아직 없을 수 있을 때
Format.STRING 계산하지 않음 문자열로 반환 주석을 화면에 표시하거나 문서를 만들 때
  • Tip : 기존의 inspect.get_annotations()도 Python 3.14에서 format 매개변수를 지원하며, annotationlib.get_annotations()와 같은 함수입니다.

  • Tip : 함수 객체에는 주석을 계산하는 __annotate__ 함수가 생성되며, __annotations__와 get_annotations()는 내부적으로 이 함수를 호출합니다. 일반적으로는 __annotate__를 직접 호출하지 않고 get_annotations()를 사용합니다.



from __future__ import annotations

from __future__ import annotations


def func(a: str, b: float = 3.5) -> int:
    return a + b


print(func.__annotations__)
결과
{‘a’: ‘str’, ‘b’: ‘float’, ‘return’: ‘int’}

from __future__ import annotations를 작성하면, 모든 주석이 계산되지 않고 문자열로 저장됩니다. (PEP 563)

Python 3.7부터 사용할 수 있었으며, 지연 평가가 도입되기 전에 정의되지 않은 이름을 주석에 쓰기 위해 주로 사용했습니다.

Python 3.14에서도 사용할 수 있지만, 주석이 문자열로 남으므로 실제 값이 필요하면 get_annotations(func, eval_str=True)처럼 다시 계산해야 합니다.

Python 3.14 이상만 지원하는 코드라면, 기본 동작인 지연 평가만으로도 대부분의 경우를 처리할 수 있습니다.

결과에서 <class 'str'>이 아닌 'str'처럼 따옴표가 붙은 문자열이 저장된 것을 확인할 수 있습니다. 이 방식은 주석을 아예 계산하지 않으므로, 주석을 읽는 쪽에서 다시 계산해야 한다는 단점이 있습니다.

지연 평가와 비교하면, 두 방식 모두 정의 시점에 주석을 계산하지 않지만 from __future__ import annotations는 __annotations__에 문자열을, 3.14의 지연 평가는 처음 접근할 때 계산한 실제 값을 저장한다는 차이가 있습니다.


import sys
from __future__ import annotations
결과
SyntaxError: from __future__ imports must occur at the beginning of the file

from __future__ import 문은 반드시 파일의 맨 앞에 작성해야 합니다.

앞에는 모듈 설명 문자열(docstring), # 주석, 빈 줄, 다른 from __future__ import 문만 올 수 있습니다.

import sys처럼 다른 코드가 먼저 작성되면, 실행 전에 SyntaxError가 발생합니다.

__future__ 문은 파일 전체의 컴파일 방식을 바꾸는 지시이므로, 다른 코드가 실행되기 전에 컴파일러가 먼저 알 수 있도록 파일의 맨 앞에 있어야 합니다.



정리

이름 설명 반환값/특징
매개변수: 주석 매개변수 주석 기본값과 함께 b: float = 3.5
-> 주석 반환값 주석 'return' 키로 저장
함수.__annotations__ 주석 사전 주석이 없는 매개변수는 제외, 형식을 검사하지 않음
지연 평가 (Python 3.14 이상) 주석을 처음 접근할 때 계산 정의되지 않은 이름도 정의 시점에는 오류 없음
annotationlib.get_annotations() 주석을 원하는 형식으로 읽기 (Python 3.14 이상) VALUE, FORWARDREF, STRING
from __future__ import annotations 모든 주석을 문자열로 저장 (Python 3.7 이상) 파일 맨 앞에 작성, 실제 값은 다시 계산 필요

함수 주석은 함수의 의도를 기록하고 도구가 읽을 수 있게 하는 정보이며, 인터프리터는 이 정보로 값을 검사하거나 변환하지 않습니다.

Python 3.14 이상에서는 주석이 필요할 때 계산되므로, 아직 정의되지 않은 클래스도 문자열 없이 주석에 바로 사용할 수 있습니다.

댓글 남기기