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

Python 개선 제안 한국어 번역

PEP 674 – 매크로를 l-value로 사용하는 것을 금지합니다

Author:
Victor Stinner <vstinner at python.org>
Status:
Deferred
Type:
Standards Track
Created:
30-Nov-2021
Python-Version:
3.12

Table of Contents

번역·라이선스 안내

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

초록

매크로를 l-value로 사용하는 것을 금지합니다. 예를 들어, Py_TYPE(obj) = new_type은 이제 컴파일러 오류가 발생합니다.

실제로 영향을 받는 프로젝트 대부분은 두 가지만 변경하면 됩니다.

  • Py_TYPE(obj) = new_typePy_SET_TYPE(obj, new_type)로 바꾸십시오.
  • Py_SIZE(obj) = new_sizePy_SET_SIZE(obj, new_size)로 바꾸십시오.

PEP 연기

SC reply to PEP 674 – Disallow using macros as l-values (2022년 2월)을 참조하십시오.

근거

매크로를 l-value로 사용하기

Python C API에서는 일반 함수를 작성하는 것보다 매크로를 작성하는 것이 더 간단하므로 일부 함수가 매크로로 구현되어 있습니다. 매크로가 구조체 멤버를 직접 노출하는 경우, 이 매크로를 사용하여 구조체 멤버를 가져올 뿐 아니라 설정하는 것도 기술적으로 가능합니다.

Python 3.10의 Py_TYPE() 매크로를 사용한 예:

#define Py_TYPE(ob) (((PyObject *)(ob))->ob_type)

이 매크로는 r-value로 사용하여 객체 타입을 get할 수 있습니다.:

type = Py_TYPE(object);

또한 l-value로 사용하여 객체 타입을 set할 수도 있습니다.:

Py_TYPE(object) = new_type;

Py_REFCNT()Py_SIZE() 매크로를 사용하여 객체의 참조 횟수와 객체 크기를 설정하는 것도 가능합니다.

객체 속성을 직접 설정하는 것은 현재의 정확한 CPython 구현에 의존합니다. 다른 Python 구현에서 이 기능을 구현하면 해당 구현의 C API가 덜 효율적일 수 있습니다.

CPython nogil 포크

Sam Gross는 GIL을 제거하기 위해 Python 3.9를 포크했습니다. 바로 nogil 브랜치입니다. 이 포크에는 PyObject.ob_refcnt 멤버가 없고 참조 횟수를 계산하기 위한 더 정교한 구현이 있으므로, Py_REFCNT(obj) = new_refcnt; 코드는 컴파일러 오류가 발생합니다.

nogil 포크를 업스트림 CPython main 브랜치에 병합하려면 먼저 이 C API 하위 호환성 문제를 해결해야 합니다. 이는 C API로 인해 Python 최적화가 간접적으로 차단된 구체적인 사례입니다.

이 문제는 Python 3.10에서 이미 해결되었습니다. Py_REFCNT() 매크로가 l-value로 사용되지 않도록 이미 수정되었습니다.

이러한 설명은 Sam Gross(nogil 개발자)의 지지를 받았습니다.

HPy 프로젝트

HPy project는 핸들과 함수 호출만 사용하는 완전히 새로운 Python용 C API입니다. 핸들은 불투명하고, 구조체 멤버에 직접 액세스할 수 없으며, 포인터를 역참조할 수 없습니다.

Py_SET_SIZE()를 검색하여 치환하는 것이 Py_SIZE()의 일부 특이한 매크로 사용 사례를 검색하여 치환하는 것보다 쉽고 안전합니다. Py_SIZE()HPy_Length()로 반기계적으로 치환할 수 있지만, Py_SET_SIZE()가 보이면 HPy로 포팅하려면 코드에 더 큰 변경이 필요하다는 점이 즉시 분명해집니다(예를 들어 HPyTupleBuilder 또는 HPyListBuilder를 사용하는 방식입니다).

매크로를 통해 노출되는 내부 세부 사항이 적을수록 HPy가 직접 대응하는 기능을 제공하기 쉬워집니다. “non-public” 인터페이스를 참조하는 모든 매크로는 사실상 해당 인터페이스를 공개적으로 노출합니다.

이러한 설명은 Antonio Cuni(HPy 개발자)의 지지를 받았습니다.

GraalVM Python

GraalVM에서 Python C API가 Python 객체에 액세스할 때, C API 에뮬레이션 계층은 CPython 구조체(PyObject, PyLongObject, PyTypeObject 등)의 내부 구조를 노출하는 래퍼로 GraalVM 객체를 감싸야 합니다. C 코드가 객체에 직접 또는 매크로를 통해 액세스할 때 GraalVM이 가로챌 수 있는 것은 구조체 오프셋에서의 읽기뿐이며, 이를 GraalVM의 표현으로 다시 매핑해야 하기 때문입니다. 노출되는 구조체 멤버의 “유효” 개수가 (매크로를 함수로 대체하여) 적을수록 GraalVM 래퍼를 더 단순하게 만들 수 있습니다.

이 PEP만으로는 GraalVM에서 래퍼를 제거하기에 충분하지 않지만, 이 장기적인 목표를 향한 단계입니다. GraalVM은 이미 장기적으로 더 나은 해결책인 HPy를 지원합니다.

이러한 내용은 Tim Felgentreff(GraalVM Python 개발자)의 지지를 받았습니다.

사양

매크로를 l-값으로 사용하는 것을 허용하지 않음

다음 65개 매크로는 l-값으로 사용할 수 없도록 수정되었습니다.

PyObject 및 PyVarObject 매크로

  • Py_TYPE(): 대신 Py_SET_TYPE()을 사용해야 합니다.
  • Py_SIZE(): 대신 Py_SET_SIZE()를 사용해야 합니다.

GET 매크로

  • PyByteArray_GET_SIZE()
  • PyBytes_GET_SIZE()
  • PyCFunction_GET_CLASS()
  • PyCFunction_GET_FLAGS()
  • PyCFunction_GET_FUNCTION()
  • PyCFunction_GET_SELF()
  • PyCell_GET()
  • PyCode_GetNumFree()
  • PyDict_GET_SIZE()
  • PyFunction_GET_ANNOTATIONS()
  • PyFunction_GET_CLOSURE()
  • PyFunction_GET_CODE()
  • PyFunction_GET_DEFAULTS()
  • PyFunction_GET_GLOBALS()
  • PyFunction_GET_KW_DEFAULTS()
  • PyFunction_GET_MODULE()
  • PyHeapType_GET_MEMBERS()
  • PyInstanceMethod_GET_FUNCTION()
  • PyList_GET_SIZE()
  • PyMemoryView_GET_BASE()
  • PyMemoryView_GET_BUFFER()
  • PyMethod_GET_FUNCTION()
  • PyMethod_GET_SELF()
  • PySet_GET_SIZE()
  • PyTuple_GET_SIZE()
  • PyUnicode_GET_DATA_SIZE()
  • PyUnicode_GET_LENGTH()
  • PyUnicode_GET_LENGTH()
  • PyUnicode_GET_SIZE()
  • PyWeakref_GET_OBJECT()

AS 매크로

  • PyByteArray_AS_STRING()
  • PyBytes_AS_STRING()
  • PyFloat_AS_DOUBLE()
  • PyUnicode_AS_DATA()
  • PyUnicode_AS_UNICODE()

PyUnicode 매크로

  • PyUnicode_1BYTE_DATA()
  • PyUnicode_2BYTE_DATA()
  • PyUnicode_4BYTE_DATA()
  • PyUnicode_DATA()
  • PyUnicode_IS_ASCII()
  • PyUnicode_IS_COMPACT()
  • PyUnicode_IS_READY()
  • PyUnicode_KIND()
  • PyUnicode_READ()
  • PyUnicode_READ_CHAR()

PyDateTime GET 매크로

  • PyDateTime_DATE_GET_FOLD()
  • PyDateTime_DATE_GET_HOUR()
  • PyDateTime_DATE_GET_MICROSECOND()
  • PyDateTime_DATE_GET_MINUTE()
  • PyDateTime_DATE_GET_SECOND()
  • PyDateTime_DATE_GET_TZINFO()
  • PyDateTime_DELTA_GET_DAYS()
  • PyDateTime_DELTA_GET_MICROSECONDS()
  • PyDateTime_DELTA_GET_SECONDS()
  • PyDateTime_GET_DAY()
  • PyDateTime_GET_MONTH()
  • PyDateTime_GET_YEAR()
  • PyDateTime_TIME_GET_FOLD()
  • PyDateTime_TIME_GET_HOUR()
  • PyDateTime_TIME_GET_MICROSECOND()
  • PyDateTime_TIME_GET_MINUTE()
  • PyDateTime_TIME_GET_SECOND()
  • PyDateTime_TIME_GET_TZINFO()

C 확장을 Python 3.11로 포팅합니다.

실제로 이러한 PEP의 영향을 받는 프로젝트 대부분은 두 가지 변경만 하면 됩니다:

  • Py_TYPE(obj) = new_typePy_SET_TYPE(obj, new_type)으로 바꿉니다.
  • Py_SIZE(obj) = new_sizePy_SET_SIZE(obj, new_size)로 바꿉니다.

pythoncapi_compat project는 C 확장을 자동으로 업데이트하는 데 사용할 수 있습니다. 이전 Python 버전에 대한 지원을 유지하면서 Python 3.11 지원을 추가합니다. 이 프로젝트는 Python 3.8 이하에 Py_SET_REFCNT(), Py_SET_TYPE()Py_SET_SIZE() 함수를 제공하는 헤더 파일을 제공합니다.

PyTuple_GET_ITEM() 및 PyList_GET_ITEM()은 변경되지 않습니다.

PyTuple_GET_ITEM()PyList_GET_ITEM() 매크로는 변경되지 않습니다.

&PyTuple_GET_ITEM(tuple, 0)&PyList_GET_ITEM(list, 0) 코드 패턴은 내부 PyObject** 배열에 접근하기 위해 여전히 흔히 사용됩니다.

이러한 매크로를 변경하는 것은 이 PEP의 범위에 포함되지 않습니다.

PyDescr_NAME() 및 PyDescr_TYPE()은 변경되지 않습니다.

PyDescr_NAME()PyDescr_TYPE() 매크로는 변경되지 않습니다.

이러한 매크로를 사용하면 PyDescrObject.d_namePyDescrObject.d_type 멤버에 접근할 수 있습니다. 이러한 매크로를 l-value로 사용하여 해당 멤버를 설정할 수 있습니다.

SWIG 프로젝트는 이러한 매크로를 l-value로 사용하여 해당 멤버를 설정합니다. PyDescrObject 구조체 멤버를 직접 설정하지 못하도록 SWIG를 수정할 수는 있지만, PyDescrObject 구조체는 성능이 중요한 구조체가 아니며 조만간 변경될 가능성도 낮으므로 그렇게 할 만한 가치는 없습니다.

자세한 내용은 bpo-46538 “[C API] Make the PyDescrObject structure opaque: PyDescr_NAME() and PyDescr_TYPE()” 이슈를 참조하십시오.

구현

구현은 bpo-45476: [C API] PEP 674: Disallow using macros as l-values 항목에서 추적됩니다.

Py_TYPE() 및 Py_SIZE() 매크로

2020년 5월에 Py_TYPE()Py_SIZE() 매크로가 l-value로 사용되지 못하도록 수정되었습니다(Py_TYPE, Py_SIZE).

2020년 11월에 이 변경 사항은 너무 많은 서드 파티 프로젝트를 중단시켰기 때문에 reverted되었습니다.

2021년 6월에 대부분의 서드 파티 프로젝트가 업데이트된 후 second attempt가 이루어졌지만, Windows에서 test_exceptions가 중단되었기 때문에 reverted again되어야 했습니다.

2021년 9월에 test_exceptions has been fixed된 후, Py_TYPE() 및 Py_SIZE()가 마침내 changed되었습니다.

2021년 11월에 이 하위 호환성이 없는 변경 사항은 Steering Council exception을 받았습니다.

2022년 10월에 Python 3.11이 Py_TYPE() 및 Py_SIZE()의 하위 호환성이 없는 변경 사항과 함께 출시되었습니다.

하위 호환성

제안된 C API 변경 사항은 의도적으로 하위 호환성이 없습니다.

실제로 l-value로 사용되는 매크로는 Py_TYPE()Py_SIZE()뿐입니다.

이 변경 사항은 PEP 387 폐기 프로세스를 따르지 않습니다. 매크로가 l-value로 사용될 때만 폐기 경고를 내보내고, 다른 방식(예: r-value로 사용될 때)으로 사용될 때는 내보내지 않는 알려진 방법이 없습니다.

영향을 받는 프로젝트 수를 줄이기 위해 다음 4개 매크로는 변경되지 않은 상태로 두었습니다: PyDescr_NAME(), PyDescr_TYPE(), PyList_GET_ITEM()PyTuple_GET_ITEM().

통계

총 34개 프로젝트(PyPI에 있는 프로젝트와 없는 프로젝트 포함)가 이 PEP의 영향을 받는 것으로 알려져 있습니다.

  • 16개 프로젝트(47%)는 이미 수정되었습니다.
  • 18개 프로젝트(53%)는 아직 수정되지 않았습니다(수정 대기 중이거나 Cython 코드를 재생성해야 합니다).

2022년 9월 1일 기준으로 이 PEP는 상위 5000개 PyPI 프로젝트 중 18개 프로젝트(0.4%)에 영향을 줍니다.

  • 15개 프로젝트(0.3%)는 Cython 코드를 재생성해야 합니다.
  • 3개 프로젝트(0.1%)는 수정이 대기 중입니다.

상위 5000개 PyPI

수정이 대기 중인 프로젝트(3개):

  • datatable (1.0.0): 수정됨
  • guppy3 (3.1.2): 수정됨
  • scipy (1.9.3): boost python을 업데이트해야 합니다.

또한 15개 프로젝트는 Cython 코드를 재생성해야 합니다.

수정 사항과 함께 출시된 프로젝트(12개):

이 PEP의 영향을 받는 백포트 프로젝트도 두 개 있습니다:

  • pickle5 (0.0.12): Python <= 3.7용 백포트
  • pysha3 (1.0.2): Python <= 3.5용 백포트

이러한 프로젝트는 사용해서는 안 되며 Python 3.11에서는 사용할 수도 없습니다.

영향을 받는 기타 프로젝트

수정 사항과 함께 릴리스된 기타 프로젝트 (4개):

HPy 프로젝트와의 관계

HPy 프로젝트

HPy 프로젝트는 포팅을 쉽게 하기 위해 원래 API에 가까운 C API를 제공하고, 가능한 한 기존 API에 가깝게 동작하도록 하는 것을 목표로 합니다. 동시에 HPy는 구현 세부 정보를 노출하지 않으면서도 훌륭한 “C 확장 API”가 될 수 있을 만큼 충분히 분리되어 있습니다(CPython 구현 API의 안정적인 부분 집합과는 다릅니다). 이 후자의 특성을 보장하기 위해 HPy 프로젝트는 CPython, PyPy 및 GraalVM Python용으로 모든 것을 병렬로 개발하려고 합니다.

HPy는 여전히 매우 빠르게 발전하고 있습니다. NumPy를 마이그레이션하는 동안에도 문제를 계속 해결하고 있으며, Cython에 HPy 지원을 추가하는 작업도 시작되었습니다. pybind11에 대한 작업도 곧 시작될 예정입니다. Tim Felgentreff는 HPy가 기존 C API의 이러한 사용자들을 지원하게 될 때쯤에는 HPy가 일반적으로 유용하고 추가 개발이 더 안정적인 프로세스를 따를 수 있을 만큼 안정적인 상태로 간주될 수 있어야 한다고 생각합니다.

장기적으로 HPy 프로젝트는 Python C 확장을 작성하기 위한 권장 API가 되기를 바랍니다.

HPy 프로젝트는 장기적으로 좋은 해결책입니다. Python 외부에서 개발된다는 장점이 있으며 C API를 변경할 필요가 없습니다.

C API는 앞으로 몇 년 더 유지될 것입니다.

HPy에 대한 첫 번째 우려는 현재 HPy가 성숙하지도 널리 사용되지도 않는다는 점이며, CPython은 가까운 시일 내에 HPy로 이식될 가능성이 낮은 많은 C 확장을 계속 지원해야 한다는 점입니다.

두 번째 우려는 새로운 최적화를 구현할 수 있도록 CPython 내부를 발전시키기 어렵다는 점과, PyPy, GraalPython 등에서 현재 C API가 비효율적으로 구현되어 있다는 점입니다. 안타깝게도 HPy는 대부분의 C 확장이 HPy로 완전히 이식될 때, 즉 “레거시” Python C API를 제거하는 것을 고려하는 것이 합리적이 될 때에만 이러한 문제를 해결할 것입니다.

C 확장을 CPython에서 HPy로 점진적으로 이식할 수는 있지만, 많은 코드를 수정해야 하며 시간이 걸립니다. 대부분의 C 확장을 HPy로 이식하는 데는 몇 년이 걸릴 것으로 예상됩니다.

이 PEP는 실질적인 문제를 일으키는 것으로 명확히 식별된 한 가지 문제, 즉 l-value로 사용되는 매크로를 수정하여 C API를 “덜 나쁘게” 만들 것을 제안합니다. 이 PEP는 소수의 C 확장만 업데이트하면 되며, 영향을 받는 확장에서는 일반적으로 몇 줄만 변경하면 됩니다.

예를 들어 NumPy 1.22는 C 코드 307,300줄로 이루어져 있으며, NumPy를 이 PEP에 맞게 조정하는 과정에서는 11줄만 수정하고(Py_SET_TYPE 및 Py_SET_SIZE 사용) 4줄을 추가했습니다(Python 3.8 이하에서 Py_SET_TYPE 및 Py_SET_SIZE를 정의하기 위해). NumPy를 HPy로 이식하는 초기 작업에서는 이미 그보다 더 많은 줄을 수정해야 했습니다.

현재로서는 어떤 접근 방식이 가장 좋은지 판단하기 어렵습니다. 현재 C API를 수정할지, 아니면 HPy에 집중할지의 문제입니다. HPy에만 집중하는 것은 위험할 수 있습니다.

거부된 아이디어: 매크로를 현재 상태로 유지하기

각 함수의 문서에서 개발자가 Python 객체를 수정하는 데 매크로를 사용하지 않도록 권고할 수 있습니다.

대입이 필요한 경우에는 setter 함수를 추가하고, 매크로 문서에서 setter 함수를 사용하도록 요구할 수 있습니다. 예를 들어 Py_SET_TYPE() 함수가 Python 3.9에 추가되었으며, 이제 Py_TYPE()의 문서에서는 객체 타입을 설정할 때 Py_SET_TYPE() 함수를 사용하도록 요구합니다.

개발자가 매크로를 l-value로 사용한다면 코드가 망가졌을 때 그 책임은 Python이 아니라 개발자에게 있습니다. 우리는 성인 상호 동의 원칙에 따라 운영합니다. Python C API 사용자가 문서에 명시된 대로 이를 사용하고, 그렇게 하지 않아 문제가 발생할 경우 그 결과를 감당할 것으로 기대합니다.

이 아이디어가 거부된 이유는 문서를 읽는 개발자가 소수에 불과하고, Python C API 문서의 변경 사항을 추적하는 사람도 소수에 불과하기 때문입니다. 대부분의 개발자는 CPython만 사용하므로 다른 Python 구현과의 호환성 문제를 알지 못합니다.

더욱이 매크로를 l-value로 계속 사용할 수 있도록 허용해도 HPy 프로젝트에는 도움이 되지 않으며, GraalVM의 Python 구현에 해당 매크로를 에뮬레이션해야 하는 부담만 남깁니다.

이미 수정된 매크로

다음 C API 매크로는 l-value로 사용하지 못하도록 이미 수정되었습니다.

  • PyCell_SET()
  • PyList_SET_ITEM()
  • PyTuple_SET_ITEM()
  • Py_REFCNT() (Python 3.10): Py_SET_REFCNT()를 사용해야 합니다.
  • _PyGCHead_SET_FINALIZED()
  • _PyGCHead_SET_NEXT()
  • asdl_seq_GET()
  • asdl_seq_GET_UNTYPED()
  • asdl_seq_LEN()
  • asdl_seq_SET()
  • asdl_seq_SET_UNTYPED()

예를 들어, PyList_SET_ITEM(list, 0, item) < 0은 이제 예상대로 컴파일러 오류가 발생합니다.

이전 이력

참고 문헌

버전 이력

  • 버전 3: PyDescr_TYPE() 및 PyDescr_NAME() 매크로를 더 이상 변경하지 않습니다.
  • 버전 2: “HPy 프로젝트와의 관계” 섹션을 추가하고 PyPy 섹션을 제거합니다.
  • 버전 1: 최초 공개 버전입니다.