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
Table of Contents
번역·라이선스 안내
이 문서는 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의 범위를 벗어납니다.
그러나 DSL이 사용자 정의 인자 파서를 생성하는 데 적합해야 한다는 점은 매우 중요하며, 이 기능은 Cython에 이미 구현되어 있습니다. 따라서 이 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가 사용된다고 가정합니다.
TBD: 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 변수 선언이 들어갑니다. 변환기 함수가 미리 선언되어 있으므로, 전처리기는 선언의 타입을 검사할 수 있습니다.
정리
선택 사항인 정리(cleanup) 섹션에는 구현 함수 뒤에 수정 없이 그대로 삽입될 리터럴 C 코드가 들어 있습니다.
출력
출력 섹션에는 전처리기가 생성한 코드가 들어갑니다.
위치 전용 인자
키워드 인자를 받지 않는 함수는 슬래시 특수 매개변수가 있는 것으로 표시합니다:
/*[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 파일을 읽어 의미 검사를 거친 구문 분석 트리를 stdout으로 출력합니다.
- preprocess는 변환기 헤더와 .c 파일을 읽어 전처리된 .c 파일을 stdout으로 출력합니다.
알려진 결함:
- Python ‘test’ 표현식은 의미 검사를 거치지 않습니다. 하지만 구문은 문법의 일부이므로 검사됩니다.
- 렉서는 삼중 따옴표 문자열을 처리하지 않습니다.
- C 선언은 원시적인 방식으로 구문 분석됩니다. 최종 구현에서는 C 문법의 ‘declarator’와 ‘init-declarator’를 활용해야 합니다.
- preprocess 도구는 좌우 양쪽에 선택 인자가 있는 경우에 대한 코드를 생성하지 않습니다. printsemant 도구는 이 경우를 처리할 수 있습니다.
- preprocess 도구는 구문 분석 트리로부터 출력을 생성하므로, define 블록의 원래 들여쓰기가 사라집니다.
문법
미정: 문법은 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은 지나치게 많은 자유를 허용합니다. 보아하니 타입을 생략할 수 있고, 전처리기는 알 수 없는 키워드를 받아들이며(그리고 무시하며), 때로는 독스트링 뒤에 공백을 추가하면 단언 오류가 발생합니다.
반면에 대안 DSL은 그러한 자유를 전혀 허용하지 않습니다. 변환기나 반환값 어노테이션을 생략하면 명백한 문법 오류입니다. LALR(1) 문법은 모호하지 않으며 번역 단위 전체에 대해 명세되어 있습니다.
Copyright
This document is licensed under the Open Publication License.