사용법 안내서

Argument Clinic이 생성하는 C 함수와 변수의 이름을 바꾸는 방법

Argument Clinic은 생성하는 함수의 이름을 자동으로 지어 줍니다. 생성된 이름이 기존 C 함수의 이름과 충돌하면 가끔 문제가 생길 수 있습니다. 쉬운 해결책이 있습니다. C 함수에 사용되는 이름을 재정의하면 됩니다. 함수 선언 줄에 "as" 키워드를 추가하고, 그 뒤에 사용하려는 함수 이름을 적기만 하면 됩니다. Argument Clinic은 그 함수 이름을 기본(생성된) 함수에 사용하고, 끝에 "_impl"을 붙인 이름을 impl 함수의 이름으로 사용합니다.

예를 들어, pickle.Pickler.dump()에 대해 생성되는 C 함수 이름을 바꾸고 싶다면 다음과 같이 합니다:

/*[clinic input]
pickle.Pickler.dump as pickler_dumper

...

이제 기본 함수의 이름은 pickler_dumper()가 되고, impl 함수의 이름은 pickler_dumper_impl()이 됩니다.

마찬가지로, 매개변수에 특정 Python 이름을 부여하고 싶지만 그 이름이 C에서는 불편할 수 있는 문제가 생길 수 있습니다. Argument Clinic에서는 같은 "as" 구문을 사용하여 매개변수에 Python과 C에서 서로 다른 이름을 부여할 수 있습니다:

/*[clinic input]
pickle.Pickler.dump

    obj: object
    file as file_obj: object
    protocol: object = NULL
    *
    fix_imports: bool = True

여기서 Python에서 사용되는 이름(시그니처와 keywords 배열에서)은 file이지만, C 변수의 이름은 file_obj가 됩니다.

이 방법으로 self 매개변수의 이름도 바꿀 수 있습니다!

PyArg_UnpackTuple을 사용하는 함수를 변환하는 방법

PyArg_UnpackTuple()로 인자를 구문 분석하는 함수를 변환하려면, 모든 인자를 적고 각각을 object로 지정하기만 하면 됩니다. 형을 적절하게 변환하도록 type 인자를 지정할 수 있습니다. 모든 인자는 위치 전용으로 표시해야 합니다(마지막 인자 뒤에 /만 있는 줄을 따로 추가하십시오).

현재 생성되는 코드는 PyArg_ParseTuple()을 사용하지만, 이는 곧 바뀔 예정입니다.

선택적 그룹을 사용하는 방법

일부 레거시 함수는 인자를 구문 분석하는 방식이 까다롭습니다. 위치 인자의 개수를 센 다음, switch 문을 사용하여 위치 인자가 몇 개인지에 따라 여러 가지 PyArg_ParseTuple() 호출 중 하나를 호출합니다. (이러한 함수는 키워드 전용 인자를 받을 수 없습니다.) 이 방식은 PyArg_ParseTupleAndKeywords()가 만들어지기 전에 선택적 인자를 흉내 내는 데 사용되었습니다.

이 방식을 사용하는 함수는 대개 PyArg_ParseTupleAndKeywords(), 선택적 인자, 기본값을 사용하도록 변환할 수 있지만, 항상 가능한 것은 아닙니다. 이러한 레거시 함수 중 일부에는 PyArg_ParseTupleAndKeywords()가 직접 지원하지 않는 동작이 있습니다. 가장 분명한 예는 내장 함수 range()로, 필수 인자의 왼쪽에 선택적 인자가 있습니다! 또 다른 예는 curses.window.addch()로, 항상 함께 지정해야 하는 두 인자로 이루어진 그룹이 있습니다. (인자의 이름은 x와 y입니다. 함수를 호출할 때 x를 전달하면 y도 반드시 전달해야 하고, x를 전달하지 않으면 y도 전달할 수 없습니다.)

어쨌든 Argument Clinic의 목표는 기존의 모든 CPython 내장 함수의 의미를 바꾸지 않고 이들의 인자 파싱을 지원하는 것입니다. 따라서 Argument Clinic은 선택적 그룹이라는 것을 사용하여 이 대안적인 구문 분석 방식을 지원합니다. 선택적 그룹은 모두 함께 전달되어야 하는 인자들의 그룹입니다. 선택적 그룹은 필수 인자의 왼쪽이나 오른쪽에 올 수 있습니다. 선택적 그룹은 오직 위치 전용 매개변수에만 사용할 수 있습니다.

참고

선택적 그룹은 오직 다음을 변환할 때만 사용하도록 의도되었습니다 PyArg_ParseTuple()를 여러 번 호출하는 함수에만 해당합니다! 인자 파싱에 다른 어떤 방식을 사용하는 함수도 선택적 그룹을 사용하여 Argument Clinic으로 변환해서는 거의 절대 안 됩니다. 선택적 그룹을 사용하는 함수는 현재 Python에서 정확한 시그니처를 가질 수 없습니다. Python이 그 개념을 이해하지 못하기 때문입니다. 가능한 한 선택적 그룹을 사용하지 마십시오.

선택적 그룹을 지정하려면 함께 묶으려는 매개변수 앞에 [만 있는 줄을 추가하고, 해당 매개변수 뒤에 ]만 있는 줄을 추가하십시오. 예를 들어, 다음은 curses.window.addch()가 선택적 그룹을 사용하여 처음 두 매개변수와 마지막 매개변수를 선택적으로 만드는 방법입니다:

/*[clinic input]

curses.window.addch

    [
    x: int
      X-coordinate.
    y: int
      Y-coordinate.
    ]

    ch: object
      Character to add.

    [
    attr: long
      Attributes for the character.
    ]
    /

...

참고 사항:

  • 각 선택적 그룹마다 해당 그룹을 나타내는 추가 매개변수 하나가 impl 함수에 전달됩니다. 이 매개변수는 group_{direction}_{number}라는 이름의 int입니다. 여기서 {direction}은 그룹이 필수 매개변수의 앞에 있는지 뒤에 있는지에 따라 right 또는 left 중 하나이고, {number}는 그룹이 필수 매개변수로부터 얼마나 떨어져 있는지를 나타내는, 단조 증가하는 숫자(1부터 시작)입니다. impl이 호출될 때, 이 매개변수는 해당 그룹이 사용되지 않았으면 0으로, 사용되었으면 0이 아닌 값으로 설정됩니다. (여기서 사용되었는지 여부는 이번 호출에서 해당 매개변수가 인자를 받았는지 여부를 뜻합니다.)

  • 필수 인자가 없으면 선택적 그룹은 필수 인자의 오른쪽에 있는 것처럼 동작합니다.

  • 모호한 경우, 인자 파싱 코드는 왼쪽(필수 매개변수 앞)에 있는 매개변수를 우선합니다.

  • 선택적 그룹에는 위치 전용 매개변수만 포함할 수 있습니다.

  • 선택적 그룹은 오직 레거시 코드를 위한 것입니다. 새 코드에는 선택적 그룹을 사용하지 마십시오.

“레거시 변환기” 대신 실제 Argument Clinic 변환기를 사용하는 방법

시간을 절약하고 Argument Clinic으로 처음 이식하는 데 배워야 할 내용을 최소화할 수 있도록, 위의 안내에서는 “레거시 변환기”를 사용하라고 설명합니다. “레거시 변환기”는 기존 코드를 Argument Clinic으로 더 쉽게 이식할 수 있도록 명시적으로 설계된 편의 기능입니다.

하지만 장기적으로는 모든 블록이 Argument Clinic의 실제 변환기 문법을 사용하는 것이 바람직할 것입니다. 왜 그렇습니까? 몇 가지 이유가 있습니다:

  • 정식 변환기는 훨씬 읽기 쉽고 의도가 더 명확합니다.

  • “레거시 변환기”로 지원되지 않는 형식 단위가 몇 가지 있습니다. 이들은 인자를 필요로 하지만 레거시 변환기 구문은 인자 지정을 지원하지 않기 때문입니다.

  • 향후에는 PyArg_ParseTuple()이 지원하는 범위에 제한되지 않는 새로운 인자 파싱 라이브러리가 생길 수도 있습니다. 이러한 유연성은 레거시 변환기를 사용하는 매개변수에는 제공되지 않습니다.

따라서 약간의 추가 노력이 괜찮다면 레거시 변환기 대신 일반 변환기를 사용하십시오.

간단히 말해, Argument Clinic (레거시가 아닌) 변환기의 구문은 Python 함수 호출처럼 보입니다. 하지만 함수에 명시적인 인자가 없으면(모든 인자가 기본값을 사용하면) 괄호를 생략할 수 있습니다. 따라서 bool과 bool()은 완전히 같은 변환기입니다.

Argument Clinic 변환기의 모든 인자는 키워드 전용입니다. 모든 Argument Clinic 변환기는 다음 인자를 받습니다:

c_default

C로 정의할 때 이 매개변수의 기본값입니다. 구체적으로는 “구문 분석 함수(parse function)”에서 선언되는 변수의 초기화 값이 됩니다. 사용 방법은 기본값에 관한 절을 참조하십시오. 문자열로 지정합니다.

annotation

이 매개변수의 어노테이션 값입니다. 현재는 지원되지 않습니다. PEP 8에서 Python 라이브러리가 어노테이션을 사용하지 않도록 규정하고 있기 때문입니다.

unused

impl 함수 시그니처에서 인자를 Py_UNUSED 매크로로 감싸십시오.

또한 일부 변환기는 추가 인자를 받습니다. 다음은 이러한 인자와 그 의미의 목록입니다:

accept

Python 타입(그리고 경우에 따라 의사 타입)의 집합입니다. 허용되는 Python 인자를 이 타입들의 값으로 제한합니다. (이는 범용 기능이 아닙니다. 원칙적으로 레거시 변환기 표에 나온 특정 타입 목록만 지원합니다.)

None을 허용하려면 이 집합에 NoneType을 추가하십시오.

bitwise

부호 없는 정수에 대해서만 지원됩니다. 이 Python 인자의 네이티브 정수 값은 음수인 경우에도 범위 검사 없이 매개변수에 기록됩니다.

converter

object 변환기에서만 지원됩니다. 이 객체를 네이티브 형으로 변환하는 데 사용할 C “변환기 함수”의 이름을 지정합니다.

encoding

문자열에 대해서만 지원됩니다. 이 문자열을 Python str(유니코드) 값에서 C char * 값으로 변환할 때 사용할 인코딩을 지정합니다.

subclass_of

object 변환기에서만 지원됩니다. Python 값이 C로 표현된 Python 타입의 서브클래스여야 함을 요구합니다.

type

object 및 self 변환기에서만 지원됩니다. 변수를 선언하는 데 사용할 C 형을 지정합니다. 기본값은 "PyObject *"입니다.

zeroes

문자열에만 지원됩니다. 참이면 값 안에 포함된 NUL 바이트('\\0')가 허용됩니다. 문자열의 길이는 문자열 매개변수 바로 뒤에 <parameter_name>_length라는 이름의 매개변수로 impl 함수에 전달됩니다.

가능한 모든 인자 조합이 동작하는 것은 아니라는 점에 유의하십시오. 보통 이러한 인자는 특정 동작을 하는 특정 PyArg_ParseTuple() 형식 단위로 구현됩니다. 예를 들어 현재는 bitwise=True를 함께 지정하지 않고는 unsigned_short를 호출할 수 없습니다. 이것이 동작하리라고 생각하는 것은 지극히 타당하지만, 이러한 의미는 기존의 어떤 형식 단위에도 대응하지 않습니다. 그래서 Argument Clinic은 이를 지원하지 않습니다. (적어도 아직은 지원하지 않습니다.)

아래는 레거시 변환기와 실제 Argument Clinic 변환기의 대응 관계를 보여 주는 표입니다. 왼쪽은 레거시 변환기이고, 오른쪽은 이를 대체할 텍스트입니다.

'B'

unsigned_char(bitwise=True)

'b'

unsigned_char

'c'

char

'C'

int(accept={str})

'd'

double

'D'

Py_complex

'es'

str(encoding='name_of_encoding')

'es#'

str(encoding='name_of_encoding', zeroes=True)

'et'

str(encoding='name_of_encoding', accept={bytes, bytearray, str})

'et#'

str(encoding='name_of_encoding', accept={bytes, bytearray, str}, zeroes=True)

'f'

float

'h'

short

'H'

unsigned_short(bitwise=True)

'i'

int

'I'

unsigned_int(bitwise=True)

'k'

unsigned_long(bitwise=True)

'K'

unsigned_long_long(bitwise=True)

'l'

long

'L'

long long

'n'

Py_ssize_t

'O'

object

'O!'

object(subclass_of='&PySomething_Type')

'O&'

object(converter='name_of_c_function')

'p'

bool

'S'

PyBytesObject

's'

str

's#'

str(zeroes=True)

's*'

Py_buffer(accept={buffer, str})

'U'

unicode

'u'

wchar_t

'u#'

wchar_t(zeroes=True)

'w*'

Py_buffer(accept={rwbuffer})

'Y'

PyByteArrayObject

'y'

str(accept={bytes})

'y#'

str(accept={robuffer}, zeroes=True)

'y*'

Py_buffer

'Z'

wchar_t(accept={str, NoneType})

'Z#'

wchar_t(accept={str, NoneType}, zeroes=True)

'z'

str(accept={str, NoneType})

'z#'

str(accept={str, NoneType}, zeroes=True)

'z*'

Py_buffer(accept={buffer, str, NoneType})

예를 들어 다음은 적절한 변환기를 사용하는 pickle.Pickler.dump 예제입니다.:

/*[clinic input]
pickle.Pickler.dump

    obj: object
        The object to be pickled.
    /

Write a pickled representation of obj to the open file.
[clinic start generated code]*/

실제 변환기의 한 가지 장점은 레거시 변환기보다 더 유연하다는 것입니다. 예를 들어 unsigned_int 변환기(및 모든 unsigned_ 변환기)는 bitwise=True 없이 지정할 수 있습니다. 이 변환기들의 기본 동작은 값의 범위를 검사하며, 음수를 받아들이지 않습니다. 레거시 변환기로는 이렇게 할 수 없습니다!

Argument Clinic은 사용할 수 있는 모든 변환기를 보여 줍니다. 각 변환기에 대해 받아들이는 모든 매개변수와 각 매개변수의 기본값을 함께 보여 줍니다. 전체 목록을 보려면 Tools/clinic/clinic.py --converters를 실행하기만 하면 됩니다.

Py_buffer 변환기 사용 방법

Py_buffer 변환기(또는 's*', 'w*', '*y', 'z*' 레거시 변환기)를 사용할 때는 제공된 버퍼에 대해 PyBuffer_Release()를 절대로 호출해서는 안 됩니다. Argument Clinic이 (파싱 함수 안에서) 이 작업을 대신 수행하는 코드를 생성합니다.

고급 변환기 사용 방법

처음에는 고급 기능이라서 건너뛰었던 형식 단위들을 기억하십니까? 이제 그것들도 처리하는 방법을 알아봅시다.

요령은 이러한 형식 단위가 모두 인자를 받는다는 것입니다—변환 함수, 타입, 또는 인코딩을 지정하는 문자열 중 하나를 받습니다. (하지만 “레거시 변환기”는 인자를 지원하지 않습니다. 그래서 첫 번째 함수에서는 그것들을 건너뛰었던 것입니다.) 형식 단위에 지정했던 인자는 이제 변환기의 인자가 됩니다. 이 인자는 converter (O&의 경우), subclass_of (O!의 경우), 또는 encoding (e로 시작하는 모든 형식 단위의 경우) 중 하나입니다.

subclass_of를 사용할 때는 object()의 또 다른 사용자 정의 인자인 type도 함께 사용하고 싶을 수 있습니다. 이 인자로 매개변수에 실제로 사용되는 타입을 설정할 수 있습니다. 예를 들어 객체가 PyUnicode_Type의 서브클래스임을 보장하고 싶다면 object(type='PyUnicodeObject *', subclass_of='&PyUnicode_Type') 변환기를 사용하는 것이 좋습니다.

Argument Clinic 사용 시 생길 수 있는 문제가 하나 있습니다. e로 시작하는 형식 단위에서 일부 유연성이 사라진다는 점입니다. PyArg_Parse*() 호출을 직접 작성할 때는 이론적으로 해당 호출에 어떤 인코딩 문자열을 전달할지 런타임에 결정할 수 있었습니다. 하지만 이제는 이 문자열을 Argument Clinic 전처리 시점에 하드코딩해야 합니다. 이 제한은 의도된 것입니다. 이 덕분에 이 형식 단위를 지원하기가 훨씬 쉬워졌고, 향후 최적화가 가능해질 수도 있습니다. 이 제한은 불합리해 보이지 않습니다. CPython 자체도 형식 단위가 e로 시작하는 매개변수에는 항상 정적으로 하드코딩된 인코딩 문자열을 전달합니다.

매개변수에 기본값을 지정하는 방법

매개변수의 기본값으로는 여러 가지 값을 사용할 수 있습니다. 가장 간단하게는 문자열, 정수, 부동 소수점 리터럴을 사용할 수 있습니다:

foo: str = "abc"
bar: int = 123
bat: float = 45.6

Python의 내장 상수도 사용할 수 있습니다:

yep:  bool = True
nope: bool = False
nada: object = None

또한 기본값 NULL과 간단한 표현식도 특별히 지원하며, 이에 대해서는 다음 절에서 설명합니다.

NULL 기본값

문자열 매개변수와 객체 매개변수는 기본값이 없음을 나타내기 위해 None으로 설정할 수 있습니다. 하지만 그렇게 하면 C 변수가 Py_None으로 초기화됩니다. 바로 이런 경우를 위해 편의상 NULL이라는 특별한 값이 있습니다. Python 관점에서는 기본값 None처럼 동작하지만, C 변수는 NULL로 초기화됩니다.

기호 기본값

매개변수에 지정하는 기본값으로 아무 표현식이나 사용할 수는 없습니다. 현재 명시적으로 지원하는 것은 다음과 같습니다:

  • 숫자 상수(정수와 부동 소수점)

  • 문자열 상수

  • True, False, None

  • sys.maxsize와 같이 모듈 이름으로 시작해야 하는 간단한 기호 상수

(앞으로는 CONSTANT - 1과 같은 완전한 표현식을 허용하기 위해 이 기능을 더 정교하게 만들어야 할 수도 있습니다.)

표현식을 기본값으로 사용하기

매개변수의 기본값은 단순한 리터럴 값에 그치지 않습니다. 수학 연산자를 사용하고 객체의 어트리뷰트를 조회하는 완전한 표현식도 사용할 수 있습니다. 하지만 쉽게 드러나지 않는 몇 가지 의미 규칙 때문에 이 기능이 그리 단순하지는 않습니다.

다음 예를 살펴보십시오:

foo: Py_ssize_t = sys.maxsize - 1

sys.maxsize는 플랫폼마다 다른 값을 가질 수 있습니다. 따라서 Argument Clinic은 이 표현식을 로컬에서 그냥 평가해서 C 코드에 하드코딩할 수 없습니다. 그래서 사용자가 함수의 시그니처를 요청할 때 런타임에 평가되도록 기본값을 저장합니다.

표현식이 평가될 때 어떤 네임스페이스를 사용할 수 있습니까? 표현식은 해당 내장 함수가 속한 모듈의 컨텍스트에서 평가됩니다. 따라서 모듈에 max_widgets라는 어트리뷰트가 있다면 그대로 사용할 수 있습니다:

foo: Py_ssize_t = max_widgets

현재 모듈에서 심볼을 찾지 못하면 sys.modules에서 찾는 것으로 넘어갑니다. 예를 들어 sys.maxsize를 찾을 수 있는 것도 이 때문입니다. (사용자가 인터프리터에 어떤 모듈을 로드할지 미리 알 수 없으므로, Python 자체가 미리 로드하는 모듈로 제한하는 것이 가장 좋습니다.)

기본값을 런타임에만 평가한다는 것은 Argument Clinic이 이에 상응하는 올바른 C 기본값을 계산할 수 없다는 뜻입니다. 따라서 이를 명시적으로 알려 주어야 합니다. 표현식을 사용할 때는 변환기의 c_default 매개변수를 사용하여 C로 된 동등한 표현식도 함께 지정해야 합니다:

foo: Py_ssize_t(c_default="PY_SSIZE_T_MAX - 1") = sys.maxsize - 1

또 다른 복잡한 문제가 있습니다. Argument Clinic은 여러분이 제공한 표현식이 유효한지 미리 알 수 없습니다. 표현식을 구문 분석하여 올바른 형태인지 확인하지만, 실제로 유효한지는 알 수 없습니다. 런타임에 유효함이 보장되는 값을 표현식으로 지정할 때는 매우 주의해야 합니다!

마지막으로, 표현식은 정적 C 값으로 나타낼 수 있어야 하므로 허용되는 표현식에는 많은 제약이 있습니다. 사용이 허용되지 않는 Python 기능의 목록은 다음과 같습니다:

  • 함수 호출.

  • 인라인 if 문(3 if foo else 5).

  • 자동 시퀀스 언패킹(*[1, 2, 3]).

  • 리스트/집합/딕셔너리 컴프리헨션과 제너레이터 표현식.

  • 튜플/리스트/집합/딕셔너리 리터럴.

반환 변환기 사용법

기본적으로 Argument Clinic이 생성하는 impl 함수는 PyObject *를 반환합니다. 하지만 C 함수는 흔히 어떤 C 타입을 계산한 다음 마지막 순간에 이를 PyObject *로 변환합니다. Argument Clinic은 입력을 Python 타입에서 네이티브 C 타입으로 변환하는 일을 처리합니다. 그렇다면 반환 값도 네이티브 C 타입에서 Python 타입으로 변환하게 하지 않을 이유가 있겠습니까?

그것이 바로 “반환 변환기”가 하는 일입니다. 반환 변환기는 impl 함수가 어떤 C 타입을 반환하도록 바꾼 다음, 생성된 (impl이 아닌) 함수에 그 값을 적절한 PyObject *로 변환하는 코드를 추가합니다.

반환 변환기의 문법은 매개변수 변환기의 문법과 비슷합니다. 반환 변환기는 -> 표기법을 사용하여 함수 자체의 반환 어노테이션처럼 지정합니다.

예를 들면 다음과 같습니다:

/*[clinic input]
add -> int

    a: int
    b: int
    /

[clinic start generated code]*/

반환 변환기는 매개변수 변환기와 거의 똑같이 동작합니다. 인자를 받으며, 인자는 모두 키워드 전용이고, 기본 인자를 하나도 변경하지 않는다면 괄호를 생략할 수 있습니다.

(함수에 "as"와 반환 변환기를 모두 사용한다면 "as"가 반환 변환기보다 앞에 와야 합니다.)

반환 변환기를 사용할 때는 한 가지 더 복잡한 점이 있습니다. 오류가 발생했다는 것을 어떻게 알릴 수 있습니까? 일반적으로 함수는 성공하면 유효한 (NULL이 아닌) 포인터를 반환하고, 실패하면 NULL을 반환합니다. 하지만 정수 반환 변환기를 사용하면 모든 정수가 유효한 값입니다. 그렇다면 Argument Clinic은 오류를 어떻게 감지할 수 있습니까? 해결책은 이렇습니다. 각 반환 변환기는 오류를 나타내는 특별한 값을 암묵적으로 확인합니다. 그 값을 반환했고 오류가 설정되어 있다면(c:func:PyErr_Occurred가 참 값을 반환한다면), 생성된 코드는 오류를 전파합니다. 그렇지 않으면 반환한 값을 평소처럼 인코딩합니다.

현재 Argument Clinic은 몇 가지 반환 변환기만 지원합니다:

bool
double
float
int
long
Py_ssize_t
size_t
unsigned int
unsigned long

이들 중 매개변수를 받는 것은 없습니다. 모든 경우에 오류를 나타내려면 -1을 반환하십시오.

Argument Clinic이 지원하는 모든 반환 변환기와 그 매개변수(있는 경우)의 전체 목록을 보려면 Tools/clinic/clinic.py --converters를 실행하십시오.

기존 함수를 복제하는 방법

비슷하게 생긴 함수가 여러 개 있다면 Clinic의 “복제(clone)” 기능을 사용할 수 있습니다. 기존 함수를 복제하면 다음을 재사용합니다:

  • 함수의 매개변수, 여기에는 다음이 포함됩니다.

    • 매개변수의 이름,

    • 매개변수의 변환기와 그 모든 매개변수,

    • 매개변수의 기본값,

    • 매개변수별 독스트링,

    • 매개변수의 종류(위치 전용인지, 위치 또는 키워드인지, 키워드 전용인지), 그리고

  • 함수의 반환 변환기.

원래 함수에서 복사되지 않는 유일한 것은 독스트링이며, 문법상 새 독스트링을 지정할 수 있습니다.

함수를 복제하는 문법은 다음과 같습니다:

/*[clinic input]
module.class.new_function [as c_basename] = module.class.existing_function

Docstring for new_function goes here.
[clinic start generated code]*/

(함수들은 서로 다른 모듈이나 클래스에 있어도 됩니다. 예제에 module.class라고 쓴 것은 두 함수 모두 전체 경로를 사용해야 한다는 것을 보여 주기 위해서일 뿐입니다.)

안타깝게도 함수를 부분적으로 복제하거나, 함수를 복제한 뒤 수정하는 문법은 없습니다. 복제는 전부 아니면 전무입니다.

또한 복제할 원본 함수는 현재 파일 안에서 먼저 정의되어 있어야 합니다.

Python 코드를 호출하는 방법

나머지 고급 주제에서는 C 파일 안에 위치하면서 Argument Clinic의 런타임 상태를 수정하는 Python 코드를 작성해야 합니다. 이는 간단합니다. Python 블록을 정의하기만 하면 됩니다.

Python 블록은 Argument Clinic 함수 블록과 다른 구분자 줄을 사용합니다. 다음과 같은 형태입니다:

/*[python input]
# python code goes here
[python start generated code]*/

Python 블록 안의 모든 코드는 구문 분석되는 시점에 실행됩니다. 블록 안에서 stdout에 쓴 모든 텍스트는 블록 뒤의 “출력”으로 리디렉션됩니다.

예를 들어, 다음은 C 코드에 정적 정수 변수를 추가하는 Python 블록입니다:

/*[python input]
print('static int __ignored_unused_variable__ = 0;')
[python start generated code]*/
static int __ignored_unused_variable__ = 0;
/*[python checksum:...]*/

“self 변환기”를 사용하는 방법

Argument Clinic은 기본 변환기를 사용하여 “self” 매개변수를 자동으로 추가합니다. 이 매개변수의 type은 타입을 선언할 때 지정한 “인스턴스를 가리키는 포인터”로 자동 설정됩니다. 하지만 Argument Clinic의 변환기를 재정의하여 변환기를 직접 지정할 수 있습니다. 블록의 첫 번째 매개변수로 직접 만든 self 매개변수를 추가하고, 그 변환기가 self_converter 또는 그 서브클래스의 인스턴스인지 확인하기만 하면 됩니다.

이것이 왜 필요합니까? 이렇게 하면 self의 타입을 재정의하거나 다른 기본 이름을 지정할 수 있습니다.

self를 캐스팅할 사용자 정의 타입은 어떻게 지정합니까? self에 같은 타입을 사용하는 함수가 한두 개뿐이라면, 사용할 타입을 type 매개변수로 전달하여 Argument Clinic의 기존 self 변환기를 직접 사용할 수 있습니다:

/*[clinic input]

_pickle.Pickler.dump

  self: self(type="PicklerObject *")
  obj: object
  /

Write a pickled representation of the given object to the open file.
[clinic start generated code]*/

반면에 self에 같은 타입을 사용하는 함수가 많다면, self_converter 클래스를 상속하되 type 구성원을 재정의하는 자체 변환기를 만드는 것이 가장 좋습니다:

/*[python input]
class PicklerObject_converter(self_converter):
    type = "PicklerObject *"
[python start generated code]*/

/*[clinic input]

_pickle.Pickler.dump

  self: PicklerObject
  obj: object
  /

Write a pickled representation of the given object to the open file.
[clinic start generated code]*/

“정의 클래스” 변환기를 사용하는 방법

Argument Clinic을 사용하면 메서드의 정의 클래스에 쉽게 접근할 수 있습니다. 이는 모듈 수준 상태를 가져와야 하는 힙 타입 메서드에 유용합니다. PyType_FromModuleAndSpec()을 사용하여 새 힙 타입을 모듈과 연결하십시오. 이제 정의 클래스에 PyType_GetModuleState()를 사용하여, 예를 들어 모듈 메서드에서 모듈 상태를 가져올 수 있습니다.

Modules/zlibmodule.c의 예제입니다. 먼저 clinic 입력에 defining_class를 추가합니다:

/*[clinic input]
zlib.Compress.compress

  cls: defining_class
  data: Py_buffer
    Binary data to be compressed.
  /

Argument Clinic 도구를 실행하면 다음과 같은 함수 시그니처가 생성됩니다:

/*[clinic start generated code]*/
static PyObject *
zlib_Compress_compress_impl(compobject *self, PyTypeObject *cls,
                            Py_buffer *data)
/*[clinic end generated code: output=6731b3f0ff357ca6 input=04d00f65ab01d260]*/

이제 다음 코드에서 PyType_GetModuleState(cls)를 사용하여 모듈 상태를 가져올 수 있습니다:

zlibstate *state = PyType_GetModuleState(cls);

각 메서드에는 이 변환기를 사용하는 인자가 하나만 있을 수 있으며, 그 인자는 self 뒤에 와야 하고, self를 사용하지 않는 경우에는 첫 번째 인자로 와야 합니다. 이 인자의 타입은 PyTypeObject *입니다. 이 인자는 __text_signature__에 나타나지 않습니다.

defining_class 변환기는 __init__() 및 __new__() 메서드와 호환되지 않습니다. 이 메서드들은 METH_METHOD 매크로 규약을 사용할 수 없기 때문입니다.

슬롯 메서드에서는 defining_class를 사용할 수 없습니다. 이러한 메서드에서 모듈 상태를 가져오려면, PyType_GetModuleByDef()로 모듈을 조회한 다음 PyModule_GetState()로 모듈 상태를 가져오십시오. Modules/_threadmodule.c의 setattro 슬롯 메서드 예제입니다:

static int
local_setattro(localobject *self, PyObject *name, PyObject *v)
{
    PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &thread_module);
    thread_module_state *state = get_thread_state(module);
    ...
}

PEP 573도 참조하십시오.

사용자 정의 변환기를 작성하는 방법

변환기는 CConverter를 상속하는 Python 클래스입니다. 사용자 정의 변환기의 주된 목적은 O& 형식 단위로 파싱되는 매개변수를 위한 것입니다 — 이러한 매개변수를 파싱한다는 것은 PyArg_ParseTuple() “변환기 함수”를 호출한다는 뜻입니다.

변환기 클래스의 이름은 ConverterName_converter 형식이어야 합니다. 이 규칙을 따르면 변환기 클래스가 Argument Clinic에 자동으로 등록되며, 이때 변환기 이름은 변환기 클래스 이름에서 _converter 접미사를 제거한 것이 됩니다.

CConverter.__init__()을 서브클래싱하는 대신 converter_init() 메서드를 작성하십시오. converter_init()은 항상 self 매개변수를 받습니다. self 다음에 오는 모든 추가 매개변수는 반드시 키워드 전용이어야 합니다. Argument Clinic에서 변환기에 전달된 모든 인자는 여러분의 converter_init() 메서드로 그대로 전달됩니다. 서브클래스에서 지정할 수 있는 구성원 목록은 CConverter 클래스를 참조하십시오.

다음은 Modules/zlibmodule.c에서 가져온 가장 간단한 사용자 정의 변환기 예입니다:

/*[python input]

class ssize_t_converter(CConverter):
    type = 'Py_ssize_t'
    converter = 'ssize_t_converter'

[python start generated code]*/
/*[python end generated code: output=da39a3ee5e6b4b0d input=35521e4e733823c7]*/

이 블록은 Argument Clinic에 ssize_t라는 이름의 변환기를 추가합니다. ssize_t로 선언된 매개변수는 Py_ssize_t 타입으로 선언되며, 'O&' 형식 단위로 파싱됩니다. 이 형식 단위는 ssize_t_converter() 변환기 C 함수를 호출합니다. ssize_t 변수는 기본값을 자동으로 지원합니다.

더 정교한 사용자 정의 변환기는 초기화와 정리를 처리하는 사용자 정의 C 코드를 삽입할 수 있습니다. CPython 소스 트리에서 더 많은 사용자 정의 변환기 예를 볼 수 있습니다. C 파일에서 문자열 CConverter를 grep으로 검색하십시오.

사용자 정의 반환 변환기를 작성하는 방법

사용자 정의 반환 변환기를 작성하는 것은 사용자 정의 변환기를 작성하는 것과 매우 비슷합니다. 다만 반환 변환기 자체가 훨씬 단순하기 때문에 작성하기가 좀 더 간단합니다.

반환 변환기는 CReturnConverter 클래스의 서브클래스여야 합니다. 사용자 정의 반환 변환기는 아직 널리 사용되지 않기 때문에 아직 예가 없습니다. 자신만의 반환 변환기를 작성하려면 Tools/clinic/clinic.py를, 특히 CReturnConverter와 그 모든 서브클래스의 구현을 읽어 보십시오.

METH_O와 METH_NOARGS 함수를 변환하는 방법

METH_O 매크로를 사용하는 함수를 변환하려면, 함수의 단일 인자가 object 변환기를 사용하는지 확인하고 인자를 위치 전용으로 표시하십시오:

/*[clinic input]
meth_o_sample

     argument: object
     /
[clinic start generated code]*/

METH_NOARGS 매크로를 사용하는 함수를 변환하려면 인자를 아무것도 지정하지 않으면 됩니다.

METH_O 매크로에 대해서도 여전히 self 변환기와 반환 변환기를 사용할 수 있으며, 객체 변환기에 type 인자를 지정할 수도 있습니다.

*args 매개변수(starargs / var-positional)를 변환하는 방법

*args에 적합한 변환기는 array와 tuple 두 가지입니다.

array 변환기를 사용하면 구현 함수에 PyObject * 타입의 C 배열 args와 배열의 항목 수를 나타내는 Py_ssize_t 타입의 args_length가 제공됩니다. 예를 들면 다음과 같습니다:

/*[clinic input]
var_positional_sample

    spam: int
    *args: array
[clinic start generated code]*/

tuple 변환기를 사용하면 구현 함수에 표준 PyTupleObject가 제공됩니다. 예를 들면 다음과 같습니다:

/*[clinic input]
var_positional_sample

    spam: int
    *args: tuple
[clinic start generated code]*/

Added in version 3.11.

tp_new와 tp_init 함수를 변환하는 방법

tp_new 및 tp_init 구성원 함수를 변환할 수 있습니다. 각각 알맞게 __new__ 또는 __init__으로 이름을 지정하기만 하면 됩니다. 참고 사항:

  • __new__에 대해 생성되는 함수 이름은 기본 동작과 달리 __new__로 끝나지 않습니다. 이 이름은 단순히 클래스 이름을 유효한 C 식별자로 변환한 것입니다.

  • 이 함수들에 대해서는 PyMethodDef #define이 생성되지 않습니다.

  • __init__ 함수는 PyObject *가 아니라 int를 반환합니다.

  • 독스트링을 클래스 독스트링으로 사용하십시오.

  • __new__ 함수와 __init__ 함수는 항상 args 객체와 kwargs 객체를 모두 받아야 하지만, 변환할 때는 이 함수들에 원하는 어떤 시그니처든 지정할 수 있습니다. (함수가 키워드를 지원하지 않는 경우, 생성된 파싱 함수는 키워드를 전달받으면 예외를 발생시킵니다.)

Clinic의 출력을 변경하고 리디렉션하는 방법

Clinic의 출력이 손으로 직접 편집한 일반 C 코드 사이사이에 섞여 있으면 불편할 수 있습니다. 다행히 Clinic은 설정을 변경할 수 있습니다. 출력을 버퍼에 모아 두었다가 나중에(또는 더 일찍!) 출력하거나, 출력을 별도의 파일에 쓸 수 있습니다. Clinic이 생성한 출력의 모든 줄에 접두사나 접미사를 추가할 수도 있습니다.

이런 방식으로 Clinic의 출력을 변경하면 가독성에 큰 도움이 될 수 있지만, Clinic 코드가 타입이 정의되기 전에 그 타입을 사용하게 되거나, 여러분의 코드가 Clinic이 생성한 코드를 그것이 정의되기 전에 사용하려 하는 문제가 생길 수 있습니다. 이러한 문제는 파일 안의 선언을 재배치하거나, Clinic이 생성한 코드가 들어가는 위치를 옮기면 쉽게 해결할 수 있습니다. (Clinic의 기본 동작이 모든 것을 현재 블록에 출력하는 것도 이 때문입니다. 많은 사람이 이 방식이 가독성을 해친다고 생각하지만, 정의 전 사용 문제를 해결하려고 코드를 재배치해야 하는 일은 절대 생기지 않습니다.)

먼저 몇 가지 용어를 정의하는 것부터 시작합시다:

필드

이 문맥에서 필드란 Clinic 출력의 하위 부분을 말합니다. 예를 들어, PyMethodDef 구조체에 대한 #define은 methoddef_define이라는 필드입니다. Clinic은 함수 정의 하나당 일곱 가지 서로 다른 필드를 출력할 수 있습니다:

docstring_prototype
docstring_definition
methoddef_define
impl_prototype
parser_prototype
parser_definition
impl_definition

모든 이름은 "<a>_<b>" 형식이며, 여기서 "<a>"는 나타내는 의미상의 객체(파싱 함수, impl 함수, 독스트링 또는 methoddef 구조체)이고 "<b>"는 그 필드가 어떤 종류의 문인지를 나타냅니다. "_prototype"으로 끝나는 필드 이름은 해당 대상의 실제 본문/데이터 없이 전방 선언만을 나타내고, "_definition"으로 끝나는 필드 이름은 해당 대상의 본문/데이터를 포함한 실제 정의를 나타냅니다. ("methoddef"는 특별한 경우로, "_define"으로 끝나는 유일한 필드이며 전처리기 #define임을 나타냅니다.)

출력 대상

출력 대상이란 Clinic이 출력을 쓸 수 있는 곳입니다. 내장 출력 대상은 다섯 가지가 있습니다:

block

기본 출력 대상입니다. 현재 Clinic 블록의 출력 섹션에 출력됩니다.

buffer

나중에 사용할 텍스트를 저장해 둘 수 있는 텍스트 버퍼입니다. 여기로 보낸 텍스트는 기존 텍스트의 끝에 덧붙여집니다. Clinic이 파일 처리를 마쳤을 때 버퍼에 텍스트가 남아 있으면 오류입니다.

file

Clinic이 자동으로 생성하는 별도의 “clinic 파일”입니다. 이 파일의 이름은 {basename}.clinic{extension}이며, 여기서 basename과 extension에는 현재 파일에 대해 os.path.splitext()를 실행한 출력이 할당됩니다. (예: _pickle.c의 file 대상은 _pickle.clinic.c에 기록됩니다.)

중요: file 대상을 사용할 때는 생성된 파일을 반드시 체크인해야 합니다!

two-pass

buffer와 같은 버퍼입니다. 하지만 two-pass 버퍼는 한 번만 덤프할 수 있으며, 전체 처리 과정에서 전달받은 모든 텍스트를 출력합니다. 덤프 지점 이후에 있는 Clinic 블록에서 보낸 텍스트까지 포함합니다.

suppress

텍스트가 억제됩니다. 즉, 버려집니다.

Clinic은 출력을 다시 구성할 수 있는 새 지시문 다섯 개를 정의합니다.

첫 번째 새 지시문은 dump입니다:

dump <destination>

이 지시문은 이름으로 지정한 대상의 현재 내용을 현재 블록의 출력으로 덤프하고, 그 대상을 비웁니다. 이 지시문은 buffer와 two-pass 대상에서만 동작합니다.

두 번째 새 지시문은 output입니다. output의 가장 기본적인 형태는 다음과 같습니다:

output <field> <destination>

이 지시문은 Clinic에 field를 destination으로 출력하라고 지시합니다. output은 everything이라는 특별한 메타 대상도 지원합니다. 이 메타 대상은 Clinic에 모든 필드를 해당 destination으로 출력하라고 지시합니다.

output에는 그 밖에도 여러 함수가 있습니다:

output push
output pop
output preset <preset>

output push와 output pop을 사용하면 내부 구성 스택에 구성을 푸시하거나 팝할 수 있으므로, 출력 구성을 일시적으로 수정한 다음 이전 구성을 쉽게 복원할 수 있습니다. 변경하기 전에 푸시하여 현재 구성을 저장하고, 이전 구성을 복원하고 싶을 때 팝하기만 하면 됩니다.

output preset은 Clinic의 출력을 다음과 같은 여러 내장 프리셋 구성 중 하나로 설정합니다:

block

Clinic의 원래 초기 구성입니다. 모든 내용을 입력 블록 바로 뒤에 씁니다.

parser_prototype과 docstring_prototype을 억제하고, 나머지는 모두 block에 씁니다.

file

쓸 수 있는 모든 내용을 “clinic 파일”에 쓰도록 설계된 구성입니다. 그런 다음 여러분의 파일 상단 근처에서 이 파일을 #include합니다. 이 구성이 동작하려면 파일을 재배치해야 할 수도 있지만, 보통은 여러 typedef와 PyTypeObject 정의에 대한 전방 선언을 만들기만 하면 됩니다.

parser_prototype과 docstring_prototype을 억제하고, impl_definition은 block에, 나머지는 모두 file에 씁니다.

기본 파일 이름은 "{dirname}/clinic/{basename}.h"입니다.

buffer

Clinic 출력의 대부분을 모아 두었다가 파일 끝부분 근처에 기록합니다. 모듈이나 내장 타입을 구현하는 Python 파일에서는 해당 모듈이나 내장 타입의 정적 구조체 바로 위에 버퍼를 덤프하는 것을 권장합니다. 이 구조체들은 보통 파일 끝에 아주 가까이 있습니다. 파일 중간에 정적 PyMethodDef 배열이 정의되어 있다면 buffer를 사용할 때 file보다 훨씬 더 많은 편집이 필요할 수 있습니다.

parser_prototype, impl_prototype, docstring_prototype을 억제하고, impl_definition은 block에, 나머지는 모두 file에 기록합니다.

two-pass

buffer 프리셋과 비슷하지만, 전방 선언은 two-pass 버퍼에, 정의는 buffer에 기록합니다. buffer 프리셋과 비슷하지만, buffer보다 편집이 덜 필요할 수 있습니다. two-pass 버퍼는 파일 윗부분 근처에 덤프하고, buffer는 buffer 프리셋을 사용할 때와 마찬가지로 끝부분 근처에 덤프하십시오.

impl_prototype을 억제하고, impl_definition은 block에, docstring_prototype, methoddef_define, parser_prototype은 two-pass에, 나머지는 모두 buffer에 기록합니다.

partial-buffer

buffer 프리셋과 비슷하지만, 더 많은 것을 block에 기록하고 생성된 코드 중 정말 큰 덩어리만 buffer에 기록합니다. 이렇게 하면 블록 출력에 들어가는 내용이 약간 늘어나는 작은 대가만으로 buffer의 정의 전 사용 문제를 완전히 피할 수 있습니다. buffer 프리셋을 사용할 때와 마찬가지로 buffer를 끝부분 근처에 덤프하십시오.

impl_prototype을 억제하고, docstring_definition과 parser_definition은 buffer에, 나머지는 모두 block에 기록합니다.

세 번째 새 지시문은 destination입니다:

destination <name> <command> [...]

이 지시자는 name이라는 이름의 대상에 대해 연산을 수행합니다.

정의된 하위 명령은 new와 clear 두 가지입니다.

new 하위 명령은 다음과 같이 동작합니다:

destination <name> new <type>

이 명령은 이름이 <name>이고 타입이 <type>인 새 대상을 만듭니다.

대상 타입은 다섯 가지입니다:

suppress

텍스트를 버립니다.

block

텍스트를 현재 블록에 기록합니다. 이것이 Clinic이 원래 하던 방식입니다.

buffer

위에서 설명한 “buffer” 내장 대상과 같은 단순한 텍스트 버퍼입니다.

file

텍스트 파일입니다. 파일 대상은 다음과 같이 파일 이름을 만드는 데 사용할 템플릿을 추가 인자로 받습니다:

destination <name> new <type> <file_template>

템플릿 안에서는 파일 이름의 일부로 치환되는 세 가지 문자열을 사용할 수 있습니다:

{path}

디렉터리와 전체 파일 이름을 포함한 파일의 전체 경로입니다.

{dirname}

파일이 들어 있는 디렉터리의 이름입니다.

{basename}

디렉터리를 제외한 파일 이름입니다.

{basename_root}

확장자를 잘라낸 basename입니다(마지막 ‘.’ 앞까지의 모든 부분이며, 마지막 ‘.’은 포함하지 않습니다).

{basename_extension}

마지막 ‘.’과 그 뒤의 모든 부분입니다. basename에 마침표가 없으면 빈 문자열이 됩니다.

파일 이름에 마침표가 없으면 {basename}과 {filename}은 같고, {extension}은 비어 있습니다. {basename}{extension}은 언제나 {filename}과 정확히 같습니다.

two-pass

위에서 설명한 “two-pass” 내장 대상과 같은 2패스 버퍼입니다.

clear 하위 명령은 다음과 같이 동작합니다:

destination <name> clear

대상에 지금까지 누적된 텍스트를 모두 제거합니다. (어디에 필요할지는 모르겠지만, 누군가 실험할 때 유용할 수도 있겠다고 생각했습니다.)

네 번째 새 지시문은 set입니다:

set line_prefix "string"
set line_suffix "string"

set을 사용하면 Clinic의 내부 변수 두 개를 설정할 수 있습니다. line_prefix는 Clinic 출력의 모든 줄 앞에 붙는 문자열이고, line_suffix는 Clinic 출력의 모든 줄 뒤에 붙는 문자열입니다.

두 변수 모두 다음 두 가지 형식 문자열을 지원합니다:

{block comment start}

C 파일의 주석 시작 텍스트 시퀀스인 문자열 /*로 바뀝니다.

{block comment end}

C 파일의 주석 끝 텍스트 시퀀스인 문자열 */로 바뀝니다.

마지막 새 지시문은 직접 사용할 일이 없을 preserve입니다:

preserve

이 지시문은 출력의 현재 내용을 수정하지 않고 그대로 유지해야 한다고 Clinic에 알립니다. 이 지시문은 Clinic이 file 파일에 출력을 덤프할 때 내부적으로 사용합니다. 출력을 Clinic 블록으로 감싸면 Clinic은 기존 체크섬 기능을 사용하여, 파일을 덮어쓰기 전에 누군가 파일을 손으로 수정하지 않았는지 확인할 수 있습니다.

#ifdef 트릭 사용법

모든 플랫폼에서 사용할 수 있지는 않은 함수를 변환하는 경우, 작업을 조금 더 쉽게 해 주는 트릭을 사용할 수 있습니다. 기존 코드는 아마 다음과 같을 것입니다:

#ifdef HAVE_FUNCTIONNAME
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */

그리고 아래쪽의 PyMethodDef 구조체에는 다음과 같은 기존 코드가 있을 것입니다:

#ifdef HAVE_FUNCTIONNAME
{'functionname', ... },
#endif /* HAVE_FUNCTIONNAME */

이 경우에는 다음과 같이 impl 함수의 본문을 #ifdef 안에 넣어야 합니다:

#ifdef HAVE_FUNCTIONNAME
/*[clinic input]
module.functionname
...
[clinic start generated code]*/
static module_functionname(...)
{
...
}
#endif /* HAVE_FUNCTIONNAME */

그런 다음 PyMethodDef 구조체에서 이 세 줄을 제거하고, Argument Clinic이 생성한 매크로로 대체하십시오:

MODULE_FUNCTIONNAME_METHODDEF

(이 매크로의 실제 이름은 생성된 코드 안에서 찾을 수 있습니다. 또는 직접 계산할 수도 있습니다. 블록의 첫 줄에 정의된 함수 이름에서 마침표를 밑줄로 바꾸고, 대문자로 변환한 다음, 끝에 "_METHODDEF"를 덧붙인 이름입니다.)

HAVE_FUNCTIONNAME이 정의되어 있지 않으면 어떻게 되는지 궁금하실 수도 있습니다. 그러면 MODULE_FUNCTIONNAME_METHODDEF 매크로도 정의되지 않습니다!

바로 이 부분에서 Argument Clinic이 아주 영리하게 동작합니다. Argument Clinic은 Argument Clinic 블록이 #ifdef에 의해 비활성화될 수 있다는 것을 실제로 감지합니다. 그런 경우에는 다음과 같은 약간의 추가 코드를 생성합니다:

#ifndef MODULE_FUNCTIONNAME_METHODDEF
    #define MODULE_FUNCTIONNAME_METHODDEF
#endif /* !defined(MODULE_FUNCTIONNAME_METHODDEF) */

따라서 이 매크로는 항상 동작합니다. 함수가 정의되어 있으면 이 매크로는 끝의 쉼표를 포함한 올바른 구조체로 바뀝니다. 함수가 정의되어 있지 않으면 이 매크로는 빈 내용으로 바뀝니다.

그러나 이로 인해 까다로운 문제가 하나 생깁니다: “block”(블록) 출력 프리셋을 사용할 때 Argument Clinic은 이 추가 코드를 어디에 두어야 합니까? 출력 블록은 #ifdef에 의해 비활성화될 수 있으므로 그곳에는 둘 수 없습니다. (바로 그것이 핵심입니다!)

이러한 상황에서 Argument Clinic은 추가 코드를 “buffer”(버퍼) 대상에 씁니다. 그래서 Argument Clinic이 다음과 같은 불만을 표시할 수도 있습니다:

Warning in file "Modules/posixmodule.c" on line 12357:
Destination buffer 'buffer' not empty at end of file, emptying.

이런 경우에는 파일을 열어 Argument Clinic이 파일에 추가한 dump buffer 블록(파일 맨 아래에 있습니다)을 찾은 다음, 해당 매크로가 사용되는 PyMethodDef 구조체 위로 옮기기만 하면 됩니다.

Python 파일에서 Argument Clinic을 사용하는 방법

실제로 Argument Clinic을 사용하여 Python 파일을 전처리할 수도 있습니다. 물론 출력이 Python 인터프리터에게 아무런 의미가 없으므로 Argument Clinic 블록을 사용하는 것은 의미가 없습니다. 하지만 Argument Clinic으로 Python 블록을 실행하면 Python을 Python 전처리기로 사용할 수 있습니다!

Python 주석은 C 주석과 다르기 때문에 Python 파일에 포함된 Argument Clinic 블록은 모양이 약간 다릅니다. 다음과 같은 모양입니다:

#/*[python input]
#print("def foo(): pass")
#[python start generated code]*/
def foo(): pass
#/*[python checksum:...]*/

제한된 C API를 사용하는 방법

Argument Clinic 입력이 #define Py_LIMITED_API가 포함된 C 소스 파일 안에 있으면, Argument Clinic은 인자를 구문 분석하는 데 제한된 API를 사용하는 C 코드를 생성합니다. 이 방식의 장점은 생성된 코드가 비공개 함수를 사용하지 않는다는 것입니다. 하지만 이 때문에 경우에 따라 Argument Clinic이 덜 효율적인 코드를 생성하게 될 수도 있습니다. 성능 저하의 정도는 매개변수(타입, 개수 등)에 따라 달라집니다.

Added in version 3.13.

생성된 시그니처를 재정의하는 방법

@text_signature 지시문을 사용하면 독스트링에 기본으로 생성되는 시그니처를 재정의할 수 있습니다. 이 지시어는 Argument Clinic이 처리할 수 없는 복잡한 시그니처에 유용할 수 있습니다. @text_signature 지시문은 인자 하나를 받습니다. 바로 문자열로 된 사용자 정의 시그니처입니다. 제공된 시그니처는 생성된 독스트링에 그대로 복사됩니다.

Objects/codeobject.c의 예제:

/*[clinic input]
@text_signature "($self, /, **changes)"
code.replace
    *
    co_argcount: int(c_default="self->co_argcount") = unchanged
    co_posonlyargcount: int(c_default="self->co_posonlyargcount") = unchanged
    # etc ...

    Return a copy of the code object with new values for the specified fields.
[clinic start generated output]*/

생성된 독스트링은 최종적으로 다음과 같습니다:

replace($self, /, **changes)
--

Return a copy of the code object with new values for the specified fields.

Argument Clinic에서 임계 영역을 사용하는 방법

@critical_section 지시문을 사용하면 Argument Clinic이 “impl” 함수 호출을 “Python 임계 영역”으로 감싸도록 지시할 수 있습니다. 전역 인터프리터 잠금(“GIL”)이 없는 CPython 빌드에서는 스레드 간 교착 상태를 일으키지 않으면서 스레드 안전성을 확보하기 위해 임계 영역이 필요합니다. 임계 영역에 진입하면 데코레이트된 함수의 첫 번째 인자에 연결된 객체별 잠금을 획득합니다. 잠금은 임계 영역을 벗어날 때 해제됩니다.

GIL이 있는 CPython 빌드에서 파이썬 임계 영역은 아무 동작도 하지 않습니다. 임계 영역에 관한 자세한 내용은 Include/internal/pycore_critical_section.h 및 PEP 703 문서를 참조하십시오.

Modules/_io/bufferedio.c에서 가져온 예제:

/*[clinic input]
@critical_section
_io._Buffered.close
[clinic start generated code]*/

생성된 글루 코드는 다음과 같습니다:

static PyObject *
_io__Buffered_close(buffered *self, PyObject *Py_UNUSED(ignored))
{
   PyObject *return_value = NULL;

   Py_BEGIN_CRITICAL_SECTION(self);
   return_value = _io__Buffered_close_impl(self);
   Py_END_CRITICAL_SECTION();

   return return_value;
}

@critical_section 지시문에 C 변수 이름을 인자로 전달하면 객체를 한두 개 더 잠글 수 있습니다. Modules/_weakref.c에서 가져온 이 예제는 추가 인자 하나(object라는 이름의 C 변수)를 받습니다:

/*[clinic input]
@critical_section object
_weakref.getweakrefcount -> Py_ssize_t

object: object
/
Return the number of weak references to 'object'.
[clinic start generated code]*/

생성된 글루 코드는 다음과 같습니다:

static PyObject *
_weakref_getweakrefs(PyObject *module, PyObject *object)
{
  PyObject *return_value = NULL;

  Py_BEGIN_CRITICAL_SECTION(object);
  return_value = _weakref_getweakrefs_impl(module, object);
  Py_END_CRITICAL_SECTION();

  return return_value;
}

Added in version 3.13.

PyGetSetDef (“게터/세터”) 함수를 선언하는 방법

“게터(getter)”와 “세터(setter)”는 PyGetSetDef 구조체에 정의되는 C 함수로, 클래스에 property와 비슷한 방식의 접근을 제공합니다. @getter 지시문과 @setter 지시문을 사용하면 Argument Clinic으로 “impl” 함수를 생성할 수 있습니다.

Modules/_io/textio.c에서 가져온 이 예제는 @getter와 @setter를 @critical_section 지시문(스레드 간 교착 상태를 일으키지 않으면서 스레드 안전성을 확보합니다)과 함께 사용하는 방법을 보여 줍니다:

/*[clinic input]
@critical_section
@getter
_io.TextIOWrapper._CHUNK_SIZE
[clinic start generated code]*/

/*[clinic input]
@critical_section
@setter
_io.TextIOWrapper._CHUNK_SIZE
[clinic start generated code]*/

생성된 글루 코드는 다음과 같습니다:

static PyObject *
_io_TextIOWrapper__CHUNK_SIZE_get(PyObject *self, void *Py_UNUSED(context))
{
    PyObject *return_value = NULL;

    Py_BEGIN_CRITICAL_SECTION(self);
    return_value = _io_TextIOWrapper__CHUNK_SIZE_get_impl((textio *)self);
    Py_END_CRITICAL_SECTION();

    return return_value;
}

static int
_io_TextIOWrapper__CHUNK_SIZE_set(PyObject *self, PyObject *value, void *Py_UNUSED(context))
{
    int return_value;

    if (value == NULL) {
        PyErr_Format(PyExc_AttributeError,
                     "attribute '_CHUNK_SIZE' of '%.100s' objects cannot be deleted",
                     Py_TYPE(self)->tp_name);
        return -1;
    }
    Py_BEGIN_CRITICAL_SECTION(self);
    return_value = _io_TextIOWrapper__CHUNK_SIZE_set_impl((textio *)self, value);
    Py_END_CRITICAL_SECTION();

    return return_value;
}

참고

게터와 세터는 별도의 함수로 선언해야 합니다. “세터”의 value 매개변수는 Argument Clinic이 암묵적으로 추가합니다. @getter에 독스트링을 추가하면 프로퍼티의 독스트링을 만들 수 있습니다. 같은 어트리뷰트의 접근자들은 C 기본 이름(basename)을 공유해야 합니다. 같은 접근자를 두 번 선언하면 오류가 발생합니다.

PyGetSetDef의 세터 슬롯은 어트리뷰트를 설정할 때와 삭제할 때 모두 사용됩니다. 어트리뷰트를 삭제할 때는 값으로 NULL을 전달하여 세터를 호출합니다. 위에서 보듯이 생성된 세터는 “impl” 함수를 호출하기 전에 AttributeError를 발생시켜 삭제를 거부합니다.

속성을 삭제할 수 있다면 @setter 뒤에 @deleter 지시문을 추가하십시오. 그러면 “impl” 함수가 NULL로 호출되며, Objects/funcobject.c에서 가져온 이 예제에서처럼 이 경우의 처리는 “impl” 함수가 맡습니다:

/*[clinic input]
@critical_section
@setter
@deleter
function.__annotations__
[clinic start generated code]*/
static int
function___annotations___set_impl(PyFunctionObject *self, PyObject *value)
{
    if (value == Py_None)
        value = NULL;
    /* Legal to del f.func_annotations.
     * Can only set func_annotations to NULL (through C api)
     * or a dict. */
    if (value != NULL && !PyDict_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
            "__annotations__ must be set to a dict object");
        return -1;
    }
    ...
}

그러면 구현은 property 클래스로 장식된 Python 메서드와 똑같이 동작합니다:

>>> import sys, _io
>>> a = _io.TextIOWrapper(sys.stdout)
>>> a._CHUNK_SIZE
8192
>>> a._CHUNK_SIZE = 30
>>> a._CHUNK_SIZE
30

Added in version 3.13.

매개변수를 위치로 전달하거나 키워드로 전달하는 방식을 폐지 예정으로 만드는 방법

Argument Clinic은 위치-또는-키워드 매개변수에 인자를 위치로 전달하거나 키워드로 전달하는 방식을 폐지 예정으로 만드는 코드를 생성할 수 있는 문법을 제공합니다. 예를 들어, 위치 전용 매개변수 a, 위치-또는-키워드 매개변수 b, c, d 세 개, 그리고 키워드 전용 매개변수 e까지 총 다섯 개의 매개변수를 갖는 모듈 수준 함수 foo.myfunc()가 있다고 가정하겠습니다:

/*[clinic input]
module foo
myfunc
    a: int
    /
    b: int
    c: int
    d: int
    *
    e: int
[clinic start generated output]*/

이제 b 매개변수를 위치 전용으로, d 매개변수를 키워드 전용으로 만들고자 합니다. 하지만 Python의 하위 호환성 정책(PEP 387 참고)에 따라 이 변경을 적용하기까지 릴리스 두 번을 기다려야 합니다. 이 예제에서는 지금이 Python 3.12 개발 단계라고 가정합시다. 그러면 Python 3.12에서는 b 매개변수의 인자가 키워드로 전달되거나 d 매개변수의 인자가 위치로 전달될 때마다 폐지 예정 경고를 내보낼 수 있고, 아무리 빨라도 Python 3.14가 되어야 각각을 위치 전용과 키워드 전용으로 만들 수 있습니다.

[from ...] 구문을 사용하면 Argument Clinic이 원하는 폐지 예정 경고를 발생시키도록 할 수 있습니다. b 매개변수 바로 아래에 / [from 3.14] 줄을 추가하고 d 매개변수 바로 위에 * [from 3.14] 줄을 추가하면 됩니다:

/*[clinic input]
module foo
myfunc
    a: int
    /
    b: int
    / [from 3.14]
    c: int
    * [from 3.14]
    d: int
    *
    e: int
[clinic start generated output]*/

다음으로 Argument Clinic 코드를 다시 생성하고(make clinic), 새로운 동작에 대한 단위 테스트를 추가하십시오.

이제 생성된 코드는 parameter d에 대한 argument가 위치 인자로 전달되거나(예: myfunc(1, 2, 3, 4, e=5)) 매개변수 b에 대한 인자가 키워드로 전달되면(예: myfunc(1, b=2, c=3, d=4, e=5)) DeprecationWarning을 발생시킵니다. 폐지 예정 기간이 끝났는데도, 즉 지정한 Python 버전의 알파 단계가 시작되었는데도 Argument Clinic 입력에서 [from ...] 줄을 제거하지 않았다면 컴파일러 경고를 내보내도록 C 전처리기 지시문도 함께 생성됩니다.

예제로 돌아가 2년 뒤로 건너뛰어 봅시다. 이제 Python 3.14 개발이 알파 단계에 들어섰는데, myfunc()의 Argument Clinic 코드를 갱신하는 일을 까맣게 잊고 있었습니다! 다행히도 이제 컴파일러 경고가 생성됩니다:

In file included from Modules/foomodule.c:139:
Modules/clinic/foomodule.c.h:139:8: warning: In 'foomodule.c', update the clinic input of 'mymod.myfunc'. [-W#warnings]
 #    warning "In 'foomodule.c', update the clinic input of 'mymod.myfunc'. [-W#warnings]"
      ^

이제 a를 위치 전용으로, c를 키워드 전용으로 만들어 폐지 예정 단계를 마무리합니다. b 아래의 / [from ...] 줄을 a 아래 줄의 /로 교체하고, d 위의 * [from ...] 줄을 e 위 줄의 *로 교체하십시오:

/*[clinic input]
module foo
myfunc
    a: int
    b: int
    /
    c: int
    *
    d: int
    e: int
[clinic start generated output]*/

마지막으로 make clinic을 실행하여 Argument Clinic 코드를 다시 생성하고, 새로운 동작을 반영하도록 단위 테스트를 갱신하십시오.

참고

알파 및 베타 단계 동안 입력 블록을 갱신하는 것을 잊으면, 릴리스 후보 단계가 시작될 때 컴파일러 경고가 컴파일러 오류로 바뀝니다.