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__)

규정된 이름은 PEP 3155 “클래스와 함수의 규정된 이름”에 의해 Python 3.3에서 타입에 추가되었습니다(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 “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

Py_TYPE(obj)->tp_name코드는 제한된 C API와 함께 사용할 수 없습니다. PyTypeObject멤버가 제한된 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 “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)과 유사합니다. Name은 타입 Name을 의미합니다.
  • %T는 객체의 타입완전한 수식 이름을 형식화하며, PyType_GetFullyQualifiedName(Py_TYPE(obj))와 유사합니다. Type은 객체 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__)을 형식화합니다. NName에서 따온 것입니다.
  • #N(대체 형식)은 모듈 이름과 수식 이름 사이에서 점 구분자(.) 대신 콜론(:) 구분자를 사용하여 타입의 완전 수식 이름을 형식화합니다.

f-string을 사용하는 예:

>>> 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 진입점을 참조하십시오.

이 변경은 Steering Council에 의해 거부되었습니다.

str(type) 변경

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

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

풀 리퀘스트: type(str)이 완전 수식 이름을 반환함을 참조하십시오.

객체 타입을 가져오는 !t 포매터 추가

f"{obj!t:T}"을 사용하여 type(obj).__fully_qualified_name__을 형식화합니다. 이는 f"{type(obj):T}"와 유사합니다.

!t 포매터가 2018년에 제안되었을 때, Eric Smith was strongly opposed to this; Eric은 f-string PEP 498의 “Literal String Interpolation” 저자입니다.

str % args에 형식 추가

str % arg에서 타입 이름을 형식화할 수 있도록 형식을 추가하자는 제안이 있었습니다. 예를 들어, 타입의 완전 수식 이름을 형식화할 수 있도록 %T 형식을 추가하자는 것입니다.

오늘날에는 새 코드에 f-string을 사용하는 것이 선호됩니다.

C에서 타입 이름을 형식화하는 다른 방법

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

hhh 길이 수정자를 사용하는 제안된 형식:

  • %hhTtype.__name__을 형식화합니다.
  • %hTtype.__qualname__을 형식화합니다.
  • %Ttype.__fully_qualified_name__을 형식화합니다.

길이 수정자는 인자의 형식을 변경하기 위한 것이 아니라 인자의 C 타입을 지정하는 데 사용됩니다. 대체 형식(#)은 인자의 형식을 변경합니다. 여기서 인자의 C 타입은 항상 PyObject*입니다.

제안된 다른 형식:

  • %Q
  • %t.
  • %lTtype.__fully_qualified_name__을 형식화합니다.
  • %Tntype.__name__을 형식화합니다.
  • %Tqtype.__qualname__을 형식화합니다.
  • %Tftype.__fully_qualified_name__을 형식화합니다.

타입 이름을 형식화하는 옵션이 많아지면 모듈마다 일관성이 없어지고 API에서 오류가 발생하기 쉬워질 수 있습니다.

%t 형식과 관련하여, printf()는 이제 ptrdiff_t 인자에 대한 길이 수정자로 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.abcunittest 모듈과 같은 기존 코드는 f'{obj.__module__}.{obj.__qualname__}'을 사용하여 타입 이름을 형식화하며, 모듈이 builtins와 같을 때만 모듈 부분을 생략합니다.

tracebackpdb 모듈만 모듈이 "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'>

논의