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_type을 Py_SET_TYPE(obj, new_type)으로 바꾸십시오.
  • Py_SIZE(obj) = new_size를 Py_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로 사용할 수 있습니다:

type = Py_TYPE(object);

객체 타입을 설정하기 위한 l-value로도 사용할 수 있습니다:

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 프로젝트는 핸들과 함수 호출만 사용하는 완전히 새로운 Python용 C API입니다. 핸들은 불투명하고, 구조체 멤버에 직접 접근할 수 없으며, 포인터를 역참조할 수 없습니다.

Py_SET_SIZE()를 검색하여 바꾸는 것은 Py_SIZE()의 이상한 매크로 사용을 검색하여 바꾸는 것보다 더 쉽고 안전합니다. Py_SIZE()는 HPy_Length()로 반기계적으로 대체할 수 있는 반면, Py_SET_SIZE()가 보이면 해당 코드를 HPy로 이식하기 위해 더 큰 변경(예를 들어 HPyTupleBuilder 또는 HPyListBuilder 사용)이 필요하다는 것을 즉시 알 수 있습니다.

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

이 내용은 Antonio Cuni(HPy 개발자)가 지지합니다.

GraalVM Python

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

이 PEP만으로는 GraalVM의 래퍼를 없애기에 충분하지 않지만, 이 장기 목표를 향한 한 걸음입니다. GraalVM은 장기적으로 더 나은 해결책인 HPy를 이미 지원합니다.

이 내용은 Tim Felgentreff(GraalVM Python 개발자)가 지지하는 내용입니다.

명세

매크로를 l-value로 사용하는 것을 금지

다음 65개 매크로는 l-value로 사용할 수 없도록 수정됩니다.

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_type을 Py_SET_TYPE(obj, new_type)으로 바꾸십시오.
  • Py_SIZE(obj) = new_size를 Py_SET_SIZE(obj, new_size)로 바꾸십시오.

pythoncapi_compat 프로젝트를 사용하면 C 확장을 자동으로 업데이트할 수 있습니다. 이전 파이썬 버전에 대한 지원을 잃지 않으면서 파이썬 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_name 및 PyDescrObject.d_type 멤버에 접근할 수 있습니다. 이 매크로들을 l-value로 사용하여 해당 멤버를 설정할 수 있습니다.

SWIG 프로젝트는 이 매크로들을 l-value로 사용하여 해당 멤버를 설정합니다. SWIG를 수정하여 PyDescrObject 구조체 멤버를 직접 설정하지 않도록 할 수도 있겠지만, 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월에 이 변경은 너무 많은 서드파티 프로젝트를 망가뜨렸기 때문에 되돌려졌습니다.

2021년 6월에 대부분의 서드파티 프로젝트가 업데이트된 후 두 번째 시도가 이루어졌지만, Windows에서 test_exceptions를 망가뜨렸기 때문에 다시 되돌려야 했습니다.

2021년 9월에 test_exceptions가 수정된 후 Py_TYPE()과 Py_SIZE()가 마침내 변경되었습니다.

2021년 11월에 하위 호환성을 깨는 이 변경은 운영 위원회 예외를 승인받았습니다.

2022년 10월에 Py_TYPE()과 Py_SIZE()의 호환되지 않는 변경을 포함한 Python 3.11이 출시되었습니다.

하위 호환성

제안된 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().

통계

전체적으로(PyPI에 있는 프로젝트와 없는 프로젝트를 합쳐) 34개 프로젝트가 이 PEP의 영향을 받는 것으로 알려져 있습니다:

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

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

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

PyPI 상위 5000개

수정 대기 중인 프로젝트(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개):

  • boost (1.78.0): 커밋
  • breezy (3.2.1): 버그 보고
  • duplicity (0.8.18): 커밋
  • gobject-introspection(GObject 인트로스펙션) (1.70.0): MR

HPy 프로젝트와의 관계

HPy 프로젝트

HPy 프로젝트가 바라는 것은 이식이 쉽도록 원래 API에 가까운 C API를 제공하고, 그 성능도 기존 API에 최대한 가깝게 만드는 것입니다. 동시에 HPy는 구현 세부 사항을 드러내지 않는 좋은 “C 확장 API”(CPython 구현 API의 안정된 부분 집합과는 다른)가 될 수 있을 만큼 충분히 분리되어 있습니다. 이 후자의 특성을 보장하기 위해 HPy 프로젝트는 모든 것을 CPython, PyPy, GraalVM Python용으로 병행하여 개발하려고 합니다.

HPy는 아직도 매우 빠르게 발전하고 있습니다. NumPy를 이전하면서 여전히 문제를 해결하고 있으며, Cython에 HPy 지원을 추가하는 작업도 시작되었습니다. pybind11에 대한 작업도 곧 시작됩니다. Tim Felgentreff는 기존 C API의 이러한 사용자들이 HPy에서 동작하게 될 즈음이면 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를 제거하는 것을 합리적으로 고려할 수 있게 될 때에야 이러한 문제를 해결할 것입니다.

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

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

예를 들어, NumPy 1.22는 307,300줄의 C 코드로 이루어져 있으며, 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 함수를 사용하도록 요구할 수 있습니다. 예를 들어, Python 3.9에 Py_SET_TYPE() 함수가 추가되었으며, 이제 Py_TYPE() 문서는 객체 타입을 설정할 때 Py_SET_TYPE() 함수를 사용하도록 요구합니다.

개발자가 매크로를 l-value로 사용한다면, 코드가 망가졌을 때 그 책임은 Python이 아니라 개발자에게 있습니다. 우리는 합의한 성인 원칙(consenting adults principle)에 따라 운영합니다. 즉, Python C API 사용자가 문서에 기술된 대로 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: 최초 공개 버전입니다.