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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 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'>
논의
- Discourse: PEP 737 – Unify type name formatting (2023).
- Discourse: Enhance type name formatting when raising an exception: add %T format in C, and add type.__fullyqualname__ (2023).
- 이슈: PyUnicode_FromFormat(): Add %T format to format the type name of an object (2023).
- 이슈: C API: Investigate how the PyTypeObject members can be removed from the public C API (2023).
- python-dev 스레드: bpo-34595: How to format a type name? (2018).
- 이슈: PyUnicode_FromFormat(): add %T format for an object type name (2018).
- 이슈: Replace %.100s by %s in PyErr_Format(): the arbitrary limit of 500 bytes is outdated (2011).
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.