Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 737 – 타입의 완전한 정규화된 이름을 포맷하는 C API

Author:
Victor Stinner <vstinner at python.org>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
29-Nov-2023
Python-Version:
3.13
Post-History:
29-Nov-2023
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

타입의 완전한 정규화된 이름을 포맷하는 편리한 C API를 새로 추가합니다. 타입이 구현된 방식에 따라 타입 이름을 다르게 포맷하던 방식을 더는 사용하지 않습니다.

새 C 코드에서는 오류 메시지와 __repr__() 메서드에 타입의 완전한 정규화된 이름을 사용할 것을 권장합니다. 새 C 코드에서는 타입 이름을 잘라내지 않을 것을 권장합니다.

객체의 타입과 타입의 완전한 정규화된 이름을 각각 포맷할 수 있도록 PyUnicode_FromFormat()에 %T, %#T, %N, %#N 포맷을 추가합니다.

크래시로 이어질 수 있는 빌린 참조를 피해 C 코드를 더 안전하게 만듭니다. 새 C API는 제한된 C API와 호환됩니다.

근거

표준 라이브러리

Python 표준 라이브러리에서 타입 이름이나 객체의 타입 이름을 포맷하는 것은 오류 메시지를 포맷하고 __repr__() 메서드를 구현할 때 흔히 하는 작업입니다. 타입 이름을 포맷하는 방법은 여러 가지이며, 방법마다 출력이 다릅니다.

datetime.timedelta 타입의 예:

  • 타입의 짧은 이름(type.__name__)과 타입의 정규화된 이름(type.__qualname__)은 'timedelta'입니다.
  • 타입의 모듈(type.__module__)은 'datetime'입니다.
  • 타입의 완전한 정규화된 이름은 'datetime.timedelta'입니다.
  • 타입의 표현(repr(type))에는 완전한 정규화된 이름이 들어 있습니다: <class 'datetime.timedelta'>.

Python 코드

Python에서 type.__name__은 타입의 짧은 이름을 가져오는 반면, f"{type.__module__}.{type.__qualname__}"는 타입의 “완전한 정규화된 이름”을 포맷합니다. 보통 객체 obj의 타입을 얻을 때 type(obj) 또는 obj.__class__를 사용합니다. 타입 이름을 따옴표로 감싸기도 합니다.

예시:

  • raise TypeError("str expected, not %s" % type(value).__name__)
  • raise TypeError("can't serialize %s" % self.__class__.__name__)
  • name = "%s.%s" % (obj.__module__, obj.__qualname__)

정규화된 이름은 Python 3.3에서 PEP 3155 “클래스와 함수의 정규화된 이름(Qualified name for classes and functions)”에 의해 타입에 추가되었습니다(type.__qualname__).

C 코드

C에서 타입 이름을 포맷하는 가장 흔한 방법은 타입의 PyTypeObject.tp_name 멤버를 가져오는 것입니다. 예시:

PyErr_Format(PyExc_TypeError, "globals must be a dict, not %.100s",
             Py_TYPE(globals)->tp_name);

타입의 “완전한 정규화된 이름”은 PyErr_Display(), type.__repr__() 구현, sys.unraisablehook 구현 등 몇 군데에서 사용됩니다.

Py_TYPE(obj)->tp_name을 사용하는 방식이 선호됩니다. Py_DECREF()가 필요한 PyType_GetQualName()을 호출하는 것보다 더 편리하기 때문입니다. 게다가 PyType_GetQualName()은 Python 3.11에서야 추가된 최근 함수입니다.

일부 함수는 타입 이름을 포맷하기 위해 %R (repr(type))을 사용하며, 이 경우 출력에는 타입의 완전히 정규화된 이름이 포함됩니다. 예시:

PyErr_Format(PyExc_TypeError,
             "calling %R should have returned an instance "
             "of BaseException, not %R",
             type, Py_TYPE(value));

PyTypeObject.tp_name 사용은 Python과 일관되지 않습니다

PyTypeObject.tp_name 멤버는 타입 구현 방식에 따라 다릅니다:

  • C로 작성된 정적 타입과 힙 타입: tp_name은 타입의 완전히 정규화된 이름입니다.
  • Python 클래스: tp_name은 타입의 짧은 이름(type.__name__)입니다.

따라서 Py_TYPE(obj)->tp_name을 사용하여 객체의 타입 이름을 포맷하면, 타입이 C로 구현되었는지 Python으로 구현되었는지에 따라 다른 출력이 나옵니다.

이는 코드가 Python으로 작성되든 C로 작성되든 동일하게 동작할 것을 권장하는 PEP 399 “순수 Python/C 가속기 모듈 호환성 요구 사항(Pure Python/C Accelerator Module Compatibility Requirements)”의 원칙에 어긋납니다.

예시:

$ python3.12
>>> import _datetime; c_obj = _datetime.date(1970, 1, 1)
>>> import _pydatetime; py_obj = _pydatetime.date(1970, 1, 1)
>>> my_list = list(range(3))

>>> my_list[c_obj]  # C type
TypeError: list indices must be integers or slices, not datetime.date

>>> my_list[py_obj]  # Python type
TypeError: list indices must be integers or slices, not date

오류 메시지에는 타입이 C로 구현된 경우 타입의 완전히 정규화된 이름(datetime.date)이, Python으로 구현된 경우 타입의 짧은 이름(date)이 포함됩니다.

제한된 C API

PyTypeObject 멤버는 제한된 C API에서 제외되므로, Py_TYPE(obj)->tp_name 코드는 제한된 C API에서 사용할 수 없습니다.

타입 이름은 PyType_GetName(), PyType_GetQualName(), PyType_GetModule() 함수를 사용하여 읽어야 하는데, 이 함수들은 사용하기가 덜 편리합니다.

C에서 타입 이름 잘라내기

1998년에 PyErr_Format() 함수가 추가되었을 때, 그 구현은 500바이트 크기의 고정 버퍼를 사용했습니다. 이 함수에는 다음과 같은 주석이 있었습니다:

/* Caller is responsible for limiting the format */

2001년에 이 함수는 힙에 동적 버퍼를 할당하도록 수정되었습니다. 하지만 이미 늦었습니다. %.100s 포맷을 사용하는 것처럼 타입 이름을 잘라내는 관행은 이미 습관이 되었고, 개발자들은 타입 이름을 왜 잘라내는지 잊어버렸습니다. Python에서는 타입 이름을 잘라내지 않습니다.

타입 이름을 C에서는 잘라내고 Python에서는 잘라내지 않는 것은 코드가 Python으로 작성되든 C로 작성되든 동일하게 동작할 것을 권장하는 PEP 399 “순수 Python/C 가속기 모듈 호환성 요구 사항(Pure Python/C Accelerator Module Compatibility Requirements)”의 원칙에 어긋납니다.

다음 이슈를 참조하십시오: Replace %.100s by %s in PyErr_Format(): the arbitrary limit of 500 bytes is outdated (2011).

명세

  • PyType_GetFullyQualifiedName() 함수를 추가합니다.
  • PyType_GetModuleName() 함수를 추가합니다.
  • PyUnicode_FromFormat()에 포맷을 추가합니다.
  • 새로운 C 코드에서는 오류 메시지와 __repr__() 메서드에 타입의 완전히 정규화된 이름을 사용하도록 권장합니다.
  • 새로운 C 코드에서는 타입 이름을 잘라내지 않도록 권장합니다.

PyType_GetFullyQualifiedName() 함수 추가

타입의 완전히 정규화된 이름을 얻기 위한 PyType_GetFullyQualifiedName() 함수를 추가합니다. 이 함수는 f"{type.__module__}.{type.__qualname__}"와 유사하며, type.__module__이 문자열이 아니거나 "builtins" 또는 "__main__"과 같은 경우에는 type.__qualname__과 유사합니다.

API:

PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type)

성공하면 문자열에 대한 새 참조를 반환합니다. 오류가 발생하면 예외를 일으키고 NULL을 반환합니다.

PyType_GetModuleName() 함수 추가

타입의 모듈 이름(type.__module__ 문자열)을 가져오는 PyType_GetModuleName() 함수를 추가합니다. API:

PyObject* PyType_GetModuleName(PyTypeObject *type)

성공하면 문자열에 대한 새 참조를 반환합니다. 오류가 발생하면 예외를 일으키고 NULL을 반환합니다.

PyUnicode_FromFormat()에 형식 추가

PyUnicode_FromFormat()에 다음 형식을 추가합니다:

  • %N은 타입의 정규화된 이름을 포맷하며, PyType_GetFullyQualifiedName(type)과 유사합니다. N은 type Name을 뜻합니다.
  • %T는 객체 타입의 정규화된 이름을 포맷하며, PyType_GetFullyQualifiedName(Py_TYPE(obj))와 유사합니다. T는 object Type을 뜻합니다.
  • %#N과 %#T: 대체 형식은 모듈 이름과 정규화된 이름 사이에 점 구분자(.) 대신 콜론 구분자(:)를 사용합니다.

예를 들어, tp_name을 사용하는 다음과 같은 기존 코드를:

PyErr_Format(PyExc_TypeError,
             "__format__ must return a str, not %.200s",
             Py_TYPE(result)->tp_name);

%T 형식으로 대체할 수 있습니다:

PyErr_Format(PyExc_TypeError,
             "__format__ must return a str, not %T", result);

변경된 코드의 장점:

  • 더 안전한 C 코드: 빌린 참조를 반환하는 Py_TYPE()을 사용하지 않습니다.
  • PyTypeObject.tp_name 멤버를 더 이상 명시적으로 읽지 않으므로 코드가 제한된 C API와 호환됩니다.
  • 포맷된 타입 이름이 더 이상 타입 구현에 의존하지 않습니다.
  • 타입 이름이 더 이상 잘리지 않습니다.

참고: %T 형식은 time.strftime()에서는 사용되지만 printf()에서는 사용되지 않습니다.

형식 요약

C 객체 C 타입 형식
%T %N 타입의 정규화된 이름입니다.
%#T %#N 타입의 정규화된 이름이며, 콜론 구분자를 사용합니다.

타입의 정규화된 이름 사용 권장

새 C 코드의 오류 메시지와 __repr__() 메서드에서는 타입의 정규화된 이름을 사용할 것을 권장합니다.

규모가 있는 애플리케이션에서는 특히 이름이 일반적인 경우, 서로 다른 두 모듈에 같은 짧은 이름을 가진 두 타입이 정의되어 있을 가능성이 높습니다. 완전히 정규화된 이름을 사용하면 타입을 모호하지 않게 식별하는 데 도움이 됩니다.

타입 이름을 자르지 않도록 권장

새 C 코드에서는 타입 이름을 잘라서는 안 됩니다. 예를 들어, %.100s 형식은 피해야 합니다. 대신 %s 형식(C에서는 %T 형식)을 사용하십시오.

구현

하위 호환성

이 PEP에서 제안하는 변경 사항은 하위 호환성을 유지합니다.

새 C API를 추가해도 하위 호환성에는 영향이 없습니다. 기존 C API는 변경하지 않고 그대로 둡니다. 변경되는 Python API는 없습니다.

타입의 짧은 이름을 타입의 완전히 정규화된 이름으로 바꾸는 것은 새 C 코드에서만 권장됩니다. 타입 이름을 더 이상 자르지 않는 것은 새 C 코드에서만 권장됩니다. 기존 코드는 변경하지 않고 그대로 두어야 하며, 따라서 하위 호환성이 유지됩니다. Python 코드에 대한 권장 사항은 없습니다.

기각된 아이디어

type.__fully_qualified_name__ 속성 추가

읽기 전용 속성 type.__fully_qualified_name__을 추가합니다. 이 속성은 타입의 완전한 정규화된 이름으로, f"{type.__module__}.{type.__qualname__}"와 비슷하며, type.__module__이 문자열이 아니거나 "builtins"와 같거나 "__main__"과 같으면 type.__qualname__입니다.

type.__repr__()는 변경하지 않고 그대로 두며, 모듈이 "builtins"인 경우에만 모듈을 생략합니다.

이 변경은 운영 위원회(Steering Council)에서 거부되었습니다:

우리는 이 PEP에서 제안하는 C API 변경 사항의 유용성을 인정하며, 해당 변경 사항은 그대로 수락할 가능성이 높습니다.

Python 수준의 변경 사항은 그만큼 정당성이 부족하다고 봅니다. 특히 __fully_qualified_name__의 필요성에 의문을 가지고 있습니다.

Thomas Wouters는 다음과 같이 덧붙였습니다:

C API가 하는 것과 정확히 같은 방식으로 타입을 형식화하려는 요구가 정말 있다면, 개인적으로는 type.__format__보다 유틸리티 함수가 더 합리적이라고 생각합니다. 하지만 구체적인 사용 사례가 제시된다면 SC를 설득할 수 있을 것이라고 생각합니다.

type.__format__() 메서드 추가

다음 형식을 지원하는 type.__format__() 메서드를 추가합니다:

  • N은 타입의 완전히 정규화된 이름 (type.__fully_qualified_name__)을 형식화합니다. N은 Name을 뜻합니다.
  • #N(대체 형식)은 모듈 이름과 정규화된 이름 사이에 점 구분자(.) 대신 콜론 (:) 구분자를 사용하여 타입의 완전히 정규화된 이름을 형식화합니다.

f-문자열을 사용하는 예:

>>> import datetime
>>> f"{datetime.timedelta:N}"  # fully qualified name
'datetime.timedelta'
>>> f"{datetime.timedelta:#N}" # fully qualified name, colon separator
'datetime:timedelta'

#N 형식이 사용하는 콜론(:) 구분자 덕분에 이름을 임포트할 때 추측할 필요가 없습니다. pkgutil.resolve_name(), python -m inspect 명령줄 인터페이스, setuptools 진입점을 참고하십시오.

이 변경은 운영 위원회에서 거부되었습니다.

str(type) 변경

type.__str__() 메서드를 수정하여 타입 이름을 다르게 포맷할 수 있습니다. 예를 들어, 타입의 완전한 정규화된 이름을 반환하도록 할 수 있습니다.

문제는 이것이 하위 호환성을 깨는 변경이라는 점입니다. 예를 들어, 표준 라이브러리의 enum, functools, optparse, pdb, xmlrpc.server 모듈을 수정해야 합니다. test_dataclasses, test_descrtut, test_cmd_line_script 테스트도 수정해야 합니다.

풀 리퀘스트: type(str)이 정규화된 전체 이름을 반환을 참고하십시오.

객체의 타입을 얻는 !t 포매터 추가

f"{type(obj):T}"와 비슷하게, f"{obj!t:T}"를 사용하여 type(obj).__fully_qualified_name__을 포맷합니다.

2018년에 !t 포매터가 제안되었을 때, Eric Smith는 이에 강하게 반대했습니다. Eric은 f-문자열 PEP 498 “리터럴 문자열 보간(Literal String Interpolation)”의 작성자입니다.

str % args에 형식 추가

str % arg에서 타입 이름을 포맷하는 형식을 추가하자는 제안이 있었습니다. 예를 들어, 타입의 완전한 정규화된 이름을 포맷하는 %T 형식을 추가합니다.

오늘날 새 코드에서는 f-문자열이 선호됩니다.

C에서 타입 이름을 포맷하는 다른 방법

printf() 함수는 여러 크기 수정자를 지원합니다: hh (char), h (short), l (long), ll (long long), z (size_t), t (ptrdiff_t), j (intmax_t). PyUnicode_FromFormat() 함수는 이들 대부분을 지원합니다.

h와 hh 길이 수정자를 사용하는 제안된 형식은 다음과 같습니다:

  • %hhT는 type.__name__을 포맷합니다.
  • %hT는 type.__qualname__을 포맷합니다.
  • %T는 type.__fully_qualified_name__을 포맷합니다.

길이 수정자는 인자를 포맷하는 방식을 바꾸는 것이 아니라 인자의 C 타입을 지정하는 데 사용됩니다. 대체 형식(#)은 인자를 포맷하는 방식을 바꿉니다. 여기서 인자의 C 타입은 항상 PyObject*입니다.

그 밖에 제안된 형식은 다음과 같습니다:

  • %Q
  • %t.
  • %lT는 type.__fully_qualified_name__을 포맷합니다.
  • %Tn은 type.__name__을 포맷합니다.
  • %Tq는 type.__qualname__을 포맷합니다.
  • %Tf는 type.__fully_qualified_name__을 포맷합니다.

타입 이름을 포매팅하는 방법이 많아지면 모듈마다 일관성이 떨어지고 API에서 오류가 발생하기 쉬워질 수 있습니다.

%t 형식과 관련하여, 현재 printf()는 t를 ptrdiff_t 인자의 길이 수정자로 사용합니다.

타입을 포매팅할 때는 다음 API를 사용합니다:

C API Python API 형식
PyType_GetName() type.__name__ 타입의 짧은 이름입니다.
PyType_GetQualName() type.__qualname__ 타입의 정규화된 이름입니다.
PyType_GetModuleName() type.__module__ 타입의 모듈 이름입니다.

Py_TYPE()과 함께 %T 형식 사용: 타입 전달

다음과 같이 %T 형식에 타입을 전달하자는 제안이 있었습니다:

PyErr_Format(PyExc_TypeError, "object type name: %T", Py_TYPE(obj));

Py_TYPE() 함수는 빌린 참조를 반환합니다. 오류 메시지를 포매팅하는 용도라면 타입에 대한 빌린 참조를 사용해도 안전해 보입니다. 하지만 실제로는 크래시가 발생할 수 있습니다. 예시:

import gc
import my_cext

class ClassA:
    pass

def create_object():
     class ClassB:
          def __repr__(self):
                self.__class__ = ClassA
                gc.collect()
                return "ClassB repr"
     return ClassB()

obj = create_object()
my_cext.func(obj)

여기서 my_cext.func()는 다음을 호출하는 C 함수입니다:

PyErr_Format(PyExc_ValueError,
             "Unexpected value %R of type %T",
             obj, Py_TYPE(obj));

PyErr_Format()은 ClassB에 대한 빌린 참조를 가지고 호출됩니다. %R 형식에 의해 repr(obj)가 호출되면 ClassB에 대한 마지막 참조가 제거되고 클래스의 메모리가 해제됩니다. %T 형식을 처리할 때 Py_TYPE(obj)는 이미 댕글링 포인터이며 Python이 크래시합니다.

타입의 완전히 정규화된 이름을 얻기 위해 제안된 다른 API

  • type.__fullyqualname__ 속성을 추가합니다: 단어 사이에 밑줄이 없는 이름입니다. 최근에 추가된 것을 포함하여 여러 던더 이름이 단어 사이에 밑줄을 사용합니다: __class_getitem__, __release_buffer__, __type_params__, __init_subclass__, __text_signature__가 그 예입니다.
  • type.__fqn__ 속성을 추가합니다: FQN은 Fully Qualified Name의 약자입니다.
  • type.fully_qualified_name() 메서드를 추가합니다. type에 추가된 메서드는 모든 타입이 상속하므로 기존 코드에 영향을 줄 수 있습니다.
  • inspect 모듈에 함수를 추가합니다. 이 함수를 사용하려면 inspect 모듈을 임포트해야 합니다.

타입의 완전한 정규화된 이름에 __main__ 모듈을 포함합니다

type.__fully_qualified_name__을 f"{type.__module__}.{type.__qualname__}"로 서식화하며, type.__module__이 문자열이 아니거나 "builtins"와 같으면 type.__qualname__으로 서식화합니다. __main__ 모듈을 다르게 취급하지 않고 이름에 포함합니다.

type.__repr__(), collections.abc, unittest 모듈 같은 기존 코드는 f'{obj.__module__}.{obj.__qualname__}'로 타입 이름을 서식화하며, 모듈이 builtins와 같을 때만 모듈 부분을 생략합니다.

traceback과 pdb 모듈만이 모듈이 "builtins" 또는 "__main__"과 같을 때 모듈을 생략합니다.

type.__fully_qualified_name__ 속성은 흔한 경우, 즉 python script.py로 실행한 스크립트에서 정의된 타입에 대해 더 짧은 이름을 만들기 위해 __main__ 모듈을 생략합니다. 디버깅할 때는 타입에 repr() 함수를 사용할 수 있으며, 이 함수는 타입 이름에 __main__ 모듈을 포함합니다. 또는 "builtins" 모듈인 경우에도 항상 모듈 이름을 포함하려면 f"{type.__module__}.{type.__qualname__}" 형식을 사용하십시오.

스크립트 예시:

class MyType:
    pass

print(f"name: {MyType.__fully_qualified_name__}")
print(f"repr: {repr(MyType)}")

출력:

name: MyType
repr: <class '__main__.MyType'>

논의