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로 사용하여 객체 타입을 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_type을Py_SET_TYPE(obj, new_type)으로 바꿉니다.Py_SIZE(obj) = new_size를Py_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_name 및 PyDescrObject.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개):
또한 15개 프로젝트는 Cython 코드를 재생성해야 합니다.
수정 사항과 함께 출시된 프로젝트(12개):
- bitarray (1.6.2): 커밋
- Cython (0.29.20): commit
- immutables (0.15): 커밋
- mercurial (5.7): 커밋, 버그 보고서
- mypy (v0.930): 커밋
- numpy (1.22.1): 커밋, 커밋 2
- pycurl (7.44.1): 커밋
- PyGObject (3.42.0)
- pyside2 (5.15.1): 버그 보고서
- python-snappy (0.6.1): 수정됨
- recordclass (0.17.2): 수정됨
- zstd (1.5.0.3): 커밋
이 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은 이제 예상대로 컴파일러 오류가 발생합니다.
이전 이력
- PEP 674 “매크로를 l-value로 사용하는 것을 금지” 및 Python 3.11 (2022년 8월 18일)
- PEP 674에 대한 SC 답변 – 매크로를 l-value로 사용하는 것을 금지 (2022년 2월 22일)
- PEP 674: 매크로를 l-value로 사용하는 것을 금지 (버전 2) (2022년 1월 18일)
- PEP 674: 매크로를 l-value로 사용하는 것을 금지 (2021년 11월 30일)
참고 문헌
- Python C API: PyObject에 접근하는 함수 추가 (10월 2021) Victor Stinner가 작성한 문서
- [capi-sig] Py_TYPE() 및 Py_SIZE()가 정적 인라인 함수가 됨 (2021년 9월)
- [C API] PyObject 및 PyVarObject 멤버에 직접 접근하지 않기: Py_SET_TYPE() 및 Py_IS_TYPE() 추가, Py_TYPE(obj)=type 금지 (2020년 2월)
- bpo-30459: PyList_SET_ITEM을 더 안전하게 만들 수 있음 (2017년 5월)
버전 이력
- 버전 3: PyDescr_TYPE() 및 PyDescr_NAME() 매크로를 더 이상 변경하지 않습니다.
- 버전 2: “HPy 프로젝트와의 관계” 섹션을 추가하고 PyPy 섹션을 제거합니다.
- 버전 1: 최초 공개 버전입니다.
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.