PEP 437 – 시그니처, 어노테이션 및 인자 변환기를 지정하기 위한 DSL
- Author:
- Stefan Krah <skrah at bytereef.org>
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 11-Mar-2013
- Python-Version:
- 3.4
- Post-History:
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 문서는 Open Publication License v1.0 이상에 따라 만든 수정된 한국어 번역본입니다. 수정자: yeokja/yeokja 프로젝트. 수정일: 2026-08-29. 변경 내용: 영어 원문을 한국어로 번역했습니다. 원저자와 저작권 표시는 위 Author 필드와 아래 Copyright 절에 유지했으며, 이 번역은 원저자의 승인이나 보증을 뜻하지 않습니다. 수정되지 않은 기준 원문 · 공식 최신판 · Open Publication License v1.0
초록
Python C-API에는 현재 함수 시그니처, 어노테이션 또는 사용자 지정 인자 변환기를 지정하고 자동으로 생성하는 메커니즘이 없습니다.
이 문제에 접근하는 방법은 여러 가지가 있습니다. Cython은 필요한 정보를 생성하기 위해 .pyx 파일에서 cdef 정의를 사용합니다. 그러나 CPython의 C-API 함수에는 추가 초기화 및 정리 코드 조각이 필요한 경우가 많으므로, 이를 cdef 에 지정하기는 어렵습니다.
PEP 436에서는 C 주석으로 둘러싸인 도메인 특정 언어(DSL)를 제안하며, 이는 대체로 매개변수별 구성 파일과 유사합니다. 전처리기는 주석을 읽고 인자 구문 분석 함수, 독스트링 및 구문 분석 단계의 결과를 활용하는 함수의 헤더를 출력합니다.
후자의 함수는 이후 구현 함수라고 합니다.
거부 공지
이 PEP는 PyCon US 2013에서 Guido van Rossum에 의해 거부되었습니다. 그러나 이 PEP에서 제기된 몇 가지 구체적인 문제는 second iteration of the PEP 436 DSL을 설계할 때 고려되었습니다.
근거
C 파일의 맥락에서 PEP 436 DSL이 적합한지에 대해서는 의견이 엇갈립니다. 이 PEP에서는 대안적인 DSL을 제안합니다. 반대 제안을 촉발한 PEP 436의 구체적인 문제는 이 PEP의 마지막 절에서 설명합니다.
범위
이 PEP는 DSL에만 전적으로 초점을 맞춥니다. 독스트링의 출력 위치나 생성된 코드와 같은 주제는 이 PEP의 범위를 벗어납니다.
그러나 이미 Cython에 구현되어 있는 기능인 사용자 지정 인자 파서를 생성하는 데 DSL이 적합해야 한다는 점은 매우 중요합니다. 따라서 이 PEP의 목표 중 하나는 DSL을 기존 솔루션에 가깝게 유지하여 Cython의 관련 부분을 CPython 소스 트리에 포함할 가능성을 높이는 것입니다.
DSL 개요
타입 안전성과 어노테이션
Python 값을 C 값으로 변환하는 방식은 변환기 함수의 타입에 의해 완전히 정의됩니다. PyArg_Parse* 계열 함수는 잘 알려진 기본 변환기인 “i”, “f” 등을 비롯하여 사용자 지정 변환기도 허용합니다.
이 PEP에서는 기본 변환기를 실제 구현 방식과 관계없이 추상 함수로 간주합니다.
Include/converters.h
변환기 함수는 전방 선언되어야 합니다. 모든 변환기 함수는 Include/converters.h 파일에 입력해야 합니다. 전처리기는 .c 파일을 변환하기 전에 이 파일을 읽습니다. 다음은 발췌한 내용입니다.:
/*[converter]
##### Default converters #####
"s": str -> const char *res;
"s*": [str, bytes, bytearray, rw_buffer] -> Py_buffer &res;
[...]
"es#": str -> (const char *res_encoding, char **res, Py_ssize_t *res_length);
[...]
##### Custom converters #####
path_converter: [str, bytes, int] -> path_t &res;
OS_STAT_DIR_FD_CONVERTER: [int, None] -> int res;
[converter_end]*/
변환기는 이름, Python 입력 타입(들) 및 C 출력 타입(들)으로 지정됩니다. 기본 변환기는 인용된 이름을 가져야 하며, 사용자 지정 변환기는 일반 이름을 가져야 합니다. Python 타입은 해당 이름으로 지정합니다. 함수가 여러 Python 타입을 허용하는 경우, 해당 집합은 리스트 형식으로 작성합니다.
기본 변환기는 여러 개의 암시적 반환 값을 가질 수 있으므로, C 출력 타입(들)은 다음 규칙에 따라 작성합니다.
주 반환 값의 이름은 res로 지정해야 합니다. 이는 DSL에서 나중에 지정되는 실제 변수 이름을 위한 자리 표시자입니다. 추가 암시적 반환 값에는 res_를 접두사로 붙여야 합니다.
기본적으로 변수는 값으로 구현 함수에 전달됩니다. 주소를 대신 전달해야 하는 경우에는 res 앞에 앰퍼샌드를 붙여야 합니다.
추가 선언은 .c 파일에 배치할 수 있습니다. 함수 타입이 동일한 경우 중복 선언이 허용됩니다.
사용자 지정 변환기 타입은 변환기 함수 정의 바로 위에서 두 번째로 선언하는 것이 좋습니다. 그러면 전처리기가 선언 간의 불일치를 포착합니다.
변환기의 복잡성을 관리 가능한 수준으로 유지하기 위해 PY_SSIZE_T_CLEAN은 더 이상 사용되지 않게 되며, 모든 길이 인자에는 Py_ssize_t가 사용된다고 가정합니다.
미정: rw_buffer와 같은 가상 타입 목록을 작성합니다.
함수 사양
키워드 인자
이 예제에는 os.stat의 정의가 포함되어 있습니다. 각 섹션은 자세히 설명합니다. 문법적으로 전체 define 블록은 함수 사양과 출력 섹션으로 구성됩니다. 함수 사양은 다시 선언 섹션, 선택적인 C 선언 섹션 및 선택적인 정리 코드 섹션으로 구성됩니다. 함수 사양 내의 섹션은 yacc 스타일로 ‘%%’로 구분됩니다.:
/*[define posix_stat]
def os.stat(path: path_converter, *, dir_fd: OS_STAT_DIR_FD_CONVERTER = None,
follow_symlinks: "p" = True) -> os.stat_result: pass
%%
path_t path = PATH_T_INITIALIZE("stat", 0, 1);
int dir_fd = DEFAULT_DIR_FD;
int follow_symlinks = 1;
%%
path_cleanup(&path);
[define_end]*/
<literal C output>
/*[define_output_end]*/
Define 블록
함수 사양 블록은 /*[define 토큰으로 시작하며, 선택적인 C 함수 이름이 이어지고, 닫는 대괄호가 이어집니다. C 함수 이름이 지정되지 않으면 선언 이름에서 생성됩니다. 이 예제에서 posix_stat을 생략하면 C 함수 이름 os_stat이 됩니다.
선언
필수 선언은 (거의) 유효한 Python 함수 정의입니다. ‘def’ 키워드와 함수 본문은 중복되지만, 이 PEP의 작성자는 해당 요소가 있으면 정의를 더 읽기 쉽다고 생각합니다.
함수 이름은 일반 식별자 대신 경로일 수 있습니다. 각 인자에는 해당 인자에 적용될 변환기 함수의 이름으로 주석이 지정됩니다.
기본값은 일반적인 Python 방식으로 지정되며, 유효한 Python 표현식이면 무엇이든 될 수 있습니다.
반환값은 어떤 Python 표현식이든 될 수 있습니다. 일반적으로 객체의 이름이지만, 대체 반환값은 리스트 형식으로 지정할 수도 있습니다.
C 선언
이 선택적 섹션에는 C 변수 선언이 포함됩니다. 변환기 함수가 미리 선언되어 있으므로 전처리기는 선언의 타입을 검사할 수 있습니다.
정리
선택적 정리 섹션에는 구현 함수 뒤에 수정 없이 삽입될 리터럴 C 코드가 포함됩니다.
출력
출력 섹션에는 전처리기가 생성하는 코드가 포함됩니다.
위치 전용 인자
키워드 인자를 받지 않는 함수는 slash 특수 매개변수의 존재로 표시됩니다.:
/*[define stat_float_times]
def os.stat_float_times(/, newval: "i") -> os.stat_result: pass
%%
int newval = -1;
[define_end]*/
전처리기는 이 정의를 PyArg_ParseTuple() 호출로 변환합니다. 슬래시 오른쪽의 모든 인자는 선택적 인자입니다.
왼쪽 및 오른쪽 선택적 인자
일부 레거시 함수에는 중앙 매개변수의 왼쪽과 오른쪽 모두에 선택적 인자 그룹이 포함되어 있습니다. 새 도구가 이러한 함수를 지원해야 하는지는 논쟁의 여지가 있습니다. 완전성을 위해 제안하는 구문은 다음과 같습니다.:
/*[define]
def curses.window.addch(y: "i", x: "i", ch: "O", attr: "l") -> None: pass
where groups = [[ch], [ch, attr], [y, x, ch], [y, x, ch, attr]]
[define_end]*/
여기서 ch는 중앙 매개변수이고, 오른쪽에 attr을 선택적으로 추가할 수 있으며, 그룹 [y, x]는 왼쪽에 선택적으로 추가할 수 있습니다.
본질적으로 규칙은 중앙 매개변수와 선택적 그룹의 모든 순서 있는 조합이 가능해야 하며, 어떤 두 조합도 동일한 길이를 갖지 않아야 한다는 것입니다.
이는 중앙 매개변수를 목록의 맨 앞에 배치한 다음 선택적 인자 그룹을 왼쪽과 오른쪽에 차례로 추가하는 것으로 간결하게 표현됩니다.
서식 지정의 유연성
위의 os.stat 예제가 너무 압축되어 있다고 생각되면 다음과 같이 쉽게 서식을 지정할 수 있습니다.:
/*[define posix_stat]
def os.stat(path: path_converter,
*,
dir_fd: OS_STAT_DIR_FD_CONVERTER = None,
follow_symlinks: "p" = True)
-> os.stat_result: pass
%%
path_t path = PATH_T_INITIALIZE("stat", 0, 1);
int dir_fd = DEFAULT_DIR_FD;
int follow_symlinks = 1;
%%
path_cleanup(&path);
[define_end]*/
<literal C output>
/*[define_output_end]*/
간결한 표기법의 이점
많은 수의 매개변수가 포함될 때 간결한 표기법의 장점은 특히 분명합니다. _posixsubprocess.fork_exec의 인자 구문 분석 부분은 이 정의로 완전히 지정됩니다.:
/*[define subprocess_fork_exec]
def _posixsubprocess.fork_exec(
process_args: "O", executable_list: "O",
close_fds: "p", py_fds_to_keep: "O",
cwd_obj: "O", env_list: "O",
p2cread: "i", p2cwrite: "i", c2pread: "i", c2pwrite: "i",
errread: "i", errwrite: "i", errpipe_read: "i", errpipe_write: "i",
restore_signals: "i", call_setsid: "i", preexec_fn: "i", /) -> int: pass
[define_end]*/
preprocess 도구는 현재 이 예제에 대해 중복된 C 선언 섹션을 생성하므로 출력이 필요 이상으로 길다는 점에 유의하십시오.
정의의 간편한 검증
경험이 부족한 사용자가 os.stat와 같은 정의를 어떻게 검증할 수 있습니까? os.stat을 os_stat으로 변경하고, 누락된 변환기를 정의한 다음 정의를 Python 대화형 인터프리터에 붙여 넣기만 하면 됩니다!
사실 converters.py 모듈은 converters.h에서 자동으로 생성할 수 있습니다.
참조 구현
참조 구현은 issue 16612에서 확인할 수 있습니다. 이 PEP는 시간 제약 속에서 작성되었고 저자가 PLY 툴체인에 익숙하지 않았기 때문에, 이 소프트웨어는 Standard ML로 작성되었으며 ml-yacc/ml-lex 툴체인을 사용합니다.
문법은 충돌이 없으며 ml-yacc가 읽을 수 있는 BNF 형식으로 제공됩니다.
두 가지 도구를 사용할 수 있습니다:
- printsemant는 컨버터 헤더와 .c 파일을 읽어 의미론적으로 검사된 파스 트리를 표준 출력으로 덤프합니다.
- preprocess는 컨버터 헤더와 .c 파일을 읽어 전처리된 .c 파일을 표준 출력으로 덤프합니다.
알려진 결함:
- Python의 ‘test’ 표현식은 의미론적으로 검사되지 않습니다. 다만 문법의 일부이므로 구문은 검사됩니다.
- 렉서는 삼중 인용 문자열을 처리하지 않습니다.
- C 선언은 원시적인 방식으로 파싱됩니다. 최종 구현에서는 C 문법의 ‘declarator’와 ‘init-declarator’를 활용해야 합니다.
- preprocess 도구는 좌우 선택적 인자 케이스에 대한 코드를 생성하지 않습니다. printsemant 도구는 이 케이스를 처리할 수 있습니다.
- preprocess 도구가 파스 트리로부터 출력을 생성하기 때문에, define 블록의 원래 들여쓰기는 소실됩니다.
문법
TBD: 문법은 ml-yacc 가독 형식으로 존재하지만, 아마 여기에 EBNF 표기법으로 포함되어야 합니다.
PEP 436과의 비교
이 PEP의 저자는 PEP 436에서 제안된 DSL에 대해 다음과 같은 우려를 갖고 있습니다:
- 공백에 민감한 설정 파일과 같은 구문은 C 파일에서 어색해 보입니다.
- 함수 정의의 구조는 매개변수별 명세 속에서 사라집니다. positional-only, required, keyword-only와 같은 키워드는 너무 많은 서로 다른 곳에 흩어져 있습니다.
반면, 대안 DSL에서는 함수 정의의 구조를 한눈에 파악할 수 있습니다.
- 이 PEP 436 DSL에는 문서화된 플래그 14개와 문서화되지 않은 플래그(allow_fd) 최소 한 개가 있습니다. 2**15가지의 가능한 조합 중 어느 것이 유효한지 알아내는 것은 사용자에게 불필요한 부담을 지웁니다.
경험에 따르면, PEP 3118 버퍼 플래그에서 유효한 조합을 가려내는 일(그리고 철저히 테스트하는 일!)은 대단히 지루한 작업임이 드러났습니다. 많은 사람은 여전히 PEP 3118 플래그를 잘 이해하지 못합니다.
반면, 대안 DSL에는 원하는 변환기를 빠르게 검색할 수 있는 중앙 파일 Include/converters.h가 있습니다. 변환기 중 다수는 이미 알려져 있으며, (자주 사용되기 때문에) 사람들이 어쩌면 암기하고 있을 수도 있습니다.
- 이 PEP 436 DSL은 지나치게 많은 자유를 허용합니다. 타입은 명백히 생략될 수 있고, 전처리기는 알 수 없는 키워드를 받아들이며(그리고 무시하며), 때로는 독스트링 뒤에 공백을 추가하면 assertion 오류가 발생합니다.
반면 대안 DSL은 그러한 자유를 허용하지 않습니다. 변환기나 반환값 어노테이션을 생략하는 것은 명백히 구문 오류입니다. LALR(1) 문법은 모호하지 않으며 전체 번역 단위에 대해 명세되어 있습니다.
Copyright
This document is licensed under the Open Publication License.