Argument Clinic 的用法?

作者

Larry Hastings

摘要

Argument Clinic 是 CPython 的一個 C 文件預(yù)處理器。旨在自動處理所有與“內(nèi)置”參數(shù)解析有關(guān)的代碼。本文展示了將 C 函數(shù)轉(zhuǎn)換為配合 Argument Clinic 工作的做法,還介紹了一些關(guān)于 Argument Clinic 用法的進(jìn)階內(nèi)容。

目前 Argument Clinic 視作僅供 CPython 內(nèi)部使用。不支持在 CPython 之外的文件中使用,也不保證未來版本會向下兼容。換句話說:如果維護(hù)的是 CPython 的外部 C 語言擴(kuò)展,歡迎在自己的代碼中試用 Argument Clinic。但 Argument Clinic 與新版 CPython 中的版本 可能 完全不兼容,且會打亂全部代碼。

Argument Clinic 的設(shè)計目標(biāo)?

Argument Clinic 的主要目標(biāo),是接管 CPython 中的所有參數(shù)解析代碼。這意味著,如果要把某個函數(shù)轉(zhuǎn)換為配合 Argument Clinic一起工作,則該函數(shù)不應(yīng)再作任何參數(shù)解析工作——Argument Clinic 生成的代碼應(yīng)該是個“黑盒”,CPython 會在頂部發(fā)起調(diào)用,底部則調(diào)用自己的代碼, PyObject *args (也許還有 PyObject *kwargs )會神奇地轉(zhuǎn)換成所需的 C 變量和類型。

Argument Clinic 為了能完成主要目標(biāo),用起來必須方便。目前,使用 CPython 的參數(shù)解析庫是一件苦差事,需要在很多地方維護(hù)冗余信息。如果使用 Argument Clinic,則不必再重復(fù)代碼了。

顯然,除非 Argument Clinic 解決了自身的問題,且沒有產(chǎn)生新的問題,否則沒有人會愿意用它。所以,Argument Clinic 最重要的事情就是生成正確的代碼。如果能加速代碼的運(yùn)行當(dāng)然更好,但至少不應(yīng)引入明顯的減速。(最終 Argument Clinic 應(yīng)該 可以實現(xiàn)較大的速度提升——代碼生成器可以重寫一下,以產(chǎn)生量身定做的參數(shù)解析代碼,而不是調(diào)用通用的 CPython 參數(shù)解析庫。 這會讓參數(shù)解析達(dá)到最佳速度?。?/p>

此外,Argument Clinic 必須足夠靈活,能夠與任何參數(shù)解析的方法一起工作。Python 有一些函數(shù)具備一些非常奇怪的解析行為;Argument Clinic 的目標(biāo)是支持所有這些函數(shù)。

最后,Argument Clinic 的初衷是為 CPython 內(nèi)置程序提供內(nèi)省“簽名”。以前如果傳入一個內(nèi)置函數(shù),內(nèi)省查詢函數(shù)會拋出異常。有了 Argument Clinic,再不會發(fā)生這種問題了!

在與 Argument Clinic 合作時,應(yīng)該牢記一個理念:給它的信息越多,它做得就會越好。誠然,Argument Clinic 現(xiàn)在還比較簡單。但會演變得越來越復(fù)雜,應(yīng)該能夠利用給出的全部信息干很多聰明而有趣的事情。

基本概念和用法?

Argument Clinic 與 CPython 一起提供,位于 Tools/clinic/clinic.py 。若要運(yùn)行它,請指定一個 C 文件作為參數(shù)。

$ python3 Tools/clinic/clinic.py foo.c

Argument Clinic 會掃描 C 文件,精確查找以下代碼:

/*[clinic input]

一旦找到一條后,就會讀取所有內(nèi)容,直至遇到以下代碼:

[clinic start generated code]*/

這兩行之間的所有內(nèi)容都是 Argument Clinic 的輸入。所有行,包括開始和結(jié)束的注釋行,統(tǒng)稱為 Argument Clinic “塊”。

Argument Clinic 在解析某一塊時,會生成輸出信息。輸出信息會緊跟著該塊寫入 C 文件中,后面還會跟著包含校驗和的注釋?,F(xiàn)在 Argument Clinic 塊看起來應(yīng)如下所示:

/*[clinic input]
... clinic input goes here ...
[clinic start generated code]*/
... clinic output goes here ...
/*[clinic end generated code: checksum=...]*/

如果對同一文件第二次運(yùn)行 Argument Clinic,則它會丟棄之前的輸出信息,并寫入帶有新校驗行的輸出信息。不過如果輸入沒有變化,則輸出也不會有變化。

不應(yīng)去改動 Argument Clinic 塊的輸出部分。而應(yīng)去修改輸入,直到生成所需的輸出信息。(這就是校驗和的用途——檢測是否有人改動了輸出信息,因為在 Argument Clinic 下次寫入新的輸出時,這些改動都會丟失)。

為了清晰起見,下面列出了 Argument Clinic 用到的術(shù)語:

  • 注釋的第一行 /*[clinic input]起始行 。

  • 注釋([clinic start generated code]*/)的最后一行是 結(jié)束行。

  • 最后一行(/*[clinic end generated code: checksum=...]*/)是 校驗和行 。

  • 在起始行和結(jié)束行之間是 輸入數(shù)據(jù)

  • 在結(jié)束行和校驗和行之間是 輸出數(shù)據(jù) 。

  • 從開始行到校驗和行的所有文本,都是 。(Argument Clinic 尚未處理成功的塊,沒有輸出或校驗和行,但仍視作一個塊)。

函數(shù)的轉(zhuǎn)換?

要想了解 Argument Clinic 是如何工作的,最好的方式就是轉(zhuǎn)換一個函數(shù)與之合作。下面介紹需遵循的最基本步驟。請注意,若真的準(zhǔn)備在 CPython 中進(jìn)行檢查,則應(yīng)進(jìn)行更深入的轉(zhuǎn)換,使用一些本文后續(xù)會介紹到的高級概念(比如 “返回轉(zhuǎn)換” 和 “自轉(zhuǎn)換”)。但以下例子將維持簡單,以供學(xué)習(xí)。

就此開始

  1. 請確保 CPython 是最新的已簽出版本。

  2. 找到一個調(diào)用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() ,且未被轉(zhuǎn)換為采用 Argument Clinic 的 Python 內(nèi)置程序。這里用了 _pickle.Pickler.dump()。

  3. 如果對 PyArg_Parse 函數(shù)的調(diào)用采用了以下格式化單元:

    O&
    O!
    es
    es#
    et
    et#
    

    或者多次調(diào)用 PyArg_ParseTuple(),則應(yīng)再選一個函數(shù)。Argument Clinic 確實 支持上述這些狀況。 但這些都是高階內(nèi)容——第一次就簡單一些吧。

    此外,如果多次調(diào)用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() 且同一參數(shù)需支持不同的類型,或者用到 PyArg_Parse 以外的函數(shù)來解析參數(shù),則可能不適合轉(zhuǎn)換為 Argument Clinic 形式。 Argument Clinic 不支持通用函數(shù)或多態(tài)參數(shù)。

  4. 在函數(shù)上方添加以下模板,創(chuàng)建塊:

    /*[clinic input]
    [clinic start generated code]*/
    
  5. 剪下文檔字符串并粘貼到 [clinic] 行之間,去除所有的無用字符,使其成為一個正確引用的 C 字符串。最有應(yīng)該只留下帶有左側(cè)縮進(jìn)的文本,且行寬不大于 80 個字符。(參數(shù) Clinic 將保留文檔字符串中的縮進(jìn)。)

    如果文檔字符串的第一行看起來像是函數(shù)的簽名,就把這一行去掉吧。((文檔串不再需要用到它——將來對內(nèi)置函數(shù)調(diào)用 help() 時,第一行將根據(jù)函數(shù)的簽名自動建立。)

    示例:

    /*[clinic input]
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
  6. 如果文檔字符串中沒有“摘要”行,Argument Clinic 會報錯。所以應(yīng)確保帶有摘要行。 “摘要”行應(yīng)為在文檔字符串開頭的一個段落,由一個80列的單行構(gòu)成。

    (示例的文檔字符串只包括一個摘要行,所以示例代碼這一步不需改動)。

  7. 在文檔字符串上方,輸入函數(shù)的名稱,后面是空行。這應(yīng)是函數(shù)的 Python 名稱,而且應(yīng)是句點分隔的完整路徑——以模塊的名稱開始,包含所有子模塊名,若函數(shù)為類方法則還應(yīng)包含類名。

    示例:

    /*[clinic input]
    _pickle.Pickler.dump
    
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
  8. 如果是第一次在此 C 文件中用到 Argument Clinic 的模塊或類,必須對其進(jìn)行聲明。清晰的 Argument Clinic 寫法應(yīng)于 C 文件頂部附近的某個單獨(dú)塊中聲明這些,就像 include 文件和 statics 放在頂部一樣。(在這里的示例代碼中,將這兩個塊相鄰給出。)

    類和模塊的名稱應(yīng)與暴露給 Python 的相同。請適時檢查 PyModuleDefPyTypeObject 中定義的名稱。

    在聲明某個類時,還必須指定其 C 語言類型的兩個部分:用于指向該類實例的指針的類型聲明,和指向該類 PyTypeObject 的指針。

    示例:

    /*[clinic input]
    module _pickle
    class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
    [clinic start generated code]*/
    
    /*[clinic input]
    _pickle.Pickler.dump
    
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
  9. 聲明函數(shù)的所有參數(shù)。每個參數(shù)都應(yīng)另起一行。所有的參數(shù)行都應(yīng)對齊函數(shù)名和文檔字符串進(jìn)行縮進(jìn)。

    這些參數(shù)行的常規(guī)形式如下:

    name_of_parameter: converter
    

    如果參數(shù)帶有缺省值,請加在轉(zhuǎn)換器之后:

    name_of_parameter: converter = default_value
    

    Argument Clinic 對 “缺省值” 的支持方式相當(dāng)復(fù)雜;更多信息請參見 關(guān)于缺省值的部分

    在參數(shù)行下面添加一個空行。

    What's a "converter"? It establishes both the type of the variable used in C, and the method to convert the Python value into a C value at runtime. For now you're going to use what's called a "legacy converter"—a convenience syntax intended to make porting old code into Argument Clinic easier.

    每個參數(shù)都要從``PyArg_Parse()`` 格式參數(shù)中復(fù)制其 “格式單元”,并以帶引號字符串的形式指定其轉(zhuǎn)換器。(“格式單元”是 format 參數(shù)的1-3個字符的正式名稱,用于讓參數(shù)解析函數(shù)知曉該變量的類型及轉(zhuǎn)換方法。關(guān)于格式單位的更多信息,請參閱 解析參數(shù)并構(gòu)建值變量 )。

    對于像 z# 這樣的多字符格式單元,要使用2-3個字符組成的整個字符串。

    示例:

     /*[clinic input]
     module _pickle
     class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
     [clinic start generated code]*/
    
     /*[clinic input]
     _pickle.Pickler.dump
    
        obj: 'O'
    
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
  10. 如果函數(shù)的格式字符串包含 |,意味著有些參數(shù)帶有缺省值,這可以忽略。Argument Clinic 根據(jù)參數(shù)是否有缺省值來推斷哪些參數(shù)是可選的。

    如果函數(shù)的格式字符串中包含 $,意味著只接受關(guān)鍵字參數(shù),請在第一個關(guān)鍵字參數(shù)之前單獨(dú)給出一行 *,縮進(jìn)與參數(shù)行對齊。

    _pickle.Pickler.dump 兩種格式字符串都沒有,所以這里的示例不用改動。)

  11. 如果 C 函數(shù)調(diào)用的是 PyArg_ParseTuple() (而不是 PyArg_ParseTupleAndKeywords()),那么其所有參數(shù)均是僅限位置參數(shù)。

    若要在 Argument Clinic 中把所有參數(shù)都標(biāo)記為只認(rèn)位置,請在最后一個參數(shù)后面一行加入一個 /,縮進(jìn)程度與參數(shù)行對齊。

    目前這個標(biāo)記是全體生效;要么所有參數(shù)都是只認(rèn)位置,要么都不是。(以后 Argument Clinic 可能會放寬這一限制。)

    示例:

    /*[clinic input]
    module _pickle
    class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
    [clinic start generated code]*/
    
    /*[clinic input]
    _pickle.Pickler.dump
    
        obj: 'O'
        /
    
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
  12. 為每個參數(shù)都編寫一個文檔字符串,這很有意義。但這是可選項;可以跳過這一步。

    下面介紹如何添加逐參數(shù)的文檔字符串。逐參數(shù)文檔字符串的第一行必須比參數(shù)定義多縮進(jìn)一層。第一行的左邊距即確定了所有逐參數(shù)文檔字符串的左邊距;所有文檔字符串文本都要同等縮進(jìn)。文本可以用多行編寫。

    示例:

    /*[clinic input]
    module _pickle
    class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
    [clinic start generated code]*/
    
    /*[clinic input]
    _pickle.Pickler.dump
    
        obj: 'O'
            The object to be pickled.
        /
    
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
  13. 保存并關(guān)閉該文件,然后運(yùn)行 Tools/clinic/clinic.py 。 運(yùn)氣好的話就萬事大吉——程序塊現(xiàn)在有了輸出信息,并且生成了一個 .c.h 文件!在文本編輯器中重新打開該文件,可以看到:

    /*[clinic input]
    _pickle.Pickler.dump
    
        obj: 'O'
            The object to be pickled.
        /
    
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
    static PyObject *
    _pickle_Pickler_dump(PicklerObject *self, PyObject *obj)
    /*[clinic end generated code: output=87ecad1261e02ac7 input=552eb1c0f52260d9]*/
    

    顯然,如果 Argument Clinic 未產(chǎn)生任何輸出,那是因為在輸入信息中發(fā)現(xiàn)了錯誤。繼續(xù)修正錯誤并重試,直至 Argument Clinic 正確地處理好文件。

    為了便于閱讀,大部分“膠水”代碼已寫入 .c.h 文件中。需在原 .c 文件中包含這個文件,通常是在 clinic 模塊之后:

    #include "clinic/_pickle.c.h"
    
  14. 請仔細(xì)檢查 Argument Clinic 生成的參數(shù)解析代碼,是否與原有代碼基本相同。

    首先,確保兩種代碼使用相同的參數(shù)解析函數(shù)。原有代碼必須調(diào)用 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() ;確保 Argument Clinic 生成的代碼調(diào)用 完全 相同的函數(shù)。

    其次,傳給 PyArg_ParseTuple()PyArg_ParseTupleAndKeywords() 的格式字符串應(yīng)該 完全 與原有函數(shù)中的相同,直到冒號或分號為止。

    (Argument Clinic 生成的格式串一定是函數(shù)名后跟著 :。如果現(xiàn)有代碼的格式串以 ; 結(jié)尾,這種改動不會影響使用,因此不必?fù)?dān)心。)

    第三,如果格式單元需要指定兩個參數(shù)(比如長度、編碼字符串或指向轉(zhuǎn)換函數(shù)的指針),請確保第二個參數(shù)在兩次調(diào)用時 完全 相同。

    第四,在輸出部分會有一個預(yù)處理器宏,為該內(nèi)置函數(shù)定義合適的靜態(tài) PyMethodDef 結(jié)構(gòu):

    #define __PICKLE_PICKLER_DUMP_METHODDEF    \
    {"dump", (PyCFunction)__pickle_Pickler_dump, METH_O, __pickle_Pickler_dump__doc__},
    

    此靜態(tài)結(jié)構(gòu)應(yīng)與本內(nèi)置函數(shù)現(xiàn)有的靜態(tài)結(jié)構(gòu) PyMethodDef 完全 相同。

    只要上述這幾點存在不一致,請調(diào)整 Argument Clinic 函數(shù)定義,并重新運(yùn)行 Tools/clinic/clinic.py ,直至 完全 相同。

  15. 注意,輸出部分的最后一行是“實現(xiàn)”函數(shù)的聲明。也就是該內(nèi)置函數(shù)的實現(xiàn)代碼所在。刪除需要修改的函數(shù)的現(xiàn)有原型,但保留開頭的大括號。再刪除其參數(shù)解析代碼和輸入變量的所有聲明。注意現(xiàn)在 Python 所見的參數(shù)即為此實現(xiàn)函數(shù)的參數(shù);如果實現(xiàn)代碼給這些變量采用了不同的命名,請進(jìn)行修正。

    因為稍顯怪異,所以還是重申一下?,F(xiàn)在的代碼應(yīng)該如下所示:

    static return_type
    your_function_impl(...)
    /*[clinic end generated code: checksum=...]*/
    {
    ...
    

    上面是 Argument Clinic 生成的校驗值和函數(shù)原型。函數(shù)應(yīng)該帶有閉合的大括號,實現(xiàn)代碼位于其中。

    示例:

    /*[clinic input]
    module _pickle
    class _pickle.Pickler "PicklerObject *" "&Pickler_Type"
    [clinic start generated code]*/
    /*[clinic end generated code: checksum=da39a3ee5e6b4b0d3255bfef95601890afd80709]*/
    
    /*[clinic input]
    _pickle.Pickler.dump
    
        obj: 'O'
            The object to be pickled.
        /
    
    Write a pickled representation of obj to the open file.
    [clinic start generated code]*/
    
    PyDoc_STRVAR(__pickle_Pickler_dump__doc__,
    "Write a pickled representation of obj to the open file.\n"
    "\n"
    ...
    static PyObject *
    _pickle_Pickler_dump_impl(PicklerObject *self, PyObject *obj)
    /*[clinic end generated code: checksum=3bd30745bf206a48f8b576a1da3d90f55a0a4187]*/
    {
        /* Check whether the Pickler was initialized correctly (issue3664).
           Developers often forget to call __init__() in their subclasses, which
           would trigger a segfault without this check. */
        if (self->write == NULL) {
            PyErr_Format(PicklingError,
                         "Pickler.__init__() was not called by %s.__init__()",
                         Py_TYPE(self)->tp_name);
            return NULL;
        }
    
        if (_Pickler_ClearBuffer(self) < 0)
            return NULL;
    
        ...
    
  16. 還記得用到 PyMethodDef 結(jié)構(gòu)的宏吧?找到函數(shù)中已有的 PyMethodDef 結(jié)構(gòu),并替換為宏的引用。(如果函數(shù)是模塊級的,可能會在文件的末尾附近;如果函數(shù)是個類方法,則可能會在靠近實現(xiàn)代碼的下方。)

    注意,宏尾部帶有一個逗號。所以若用宏替換已有的靜態(tài)結(jié)構(gòu) PyMethodDef 時,請勿 在結(jié)尾添加逗號了。

    示例:

    static struct PyMethodDef Pickler_methods[] = {
        __PICKLE_PICKLER_DUMP_METHODDEF
        __PICKLE_PICKLER_CLEAR_MEMO_METHODDEF
        {NULL, NULL}                /* sentinel */
    };
    
  17. 編譯,然后運(yùn)行回歸測試套件中的有關(guān)測試程序。不應(yīng)引入新的編譯警告或錯誤,且對 Python 也不應(yīng)有外部可見的變化。

    差別只有一個,即 inspect.signature() 運(yùn)行于新的函數(shù)上,現(xiàn)在應(yīng)該新提供一個有效的簽名!

    祝賀你,現(xiàn)在已經(jīng)用 Argument Clinic 移植了第一個函數(shù)。

進(jìn)階?

現(xiàn)在 Argument Clinic 的使用經(jīng)驗已具備了一些,該介紹一些高級內(nèi)容了。

Symbolic default values?

提供給參數(shù)的默認(rèn)值不能是表達(dá)式。目前明確支持以下形式:

  • 數(shù)值型常數(shù)(整數(shù)和浮點數(shù))。

  • 字符串常量

  • TrueFalseNone 。

  • 以模塊名開頭的簡單符號常量,如 sys.maxsize。

(未來可能需要加以細(xì)化,以便可以采用 CONSTANT - 1 之類的完整表達(dá)式。)

對 Argument Clinic 生成的 C 函數(shù)和變量進(jìn)行重命名?

Argument Clinic 會自動為其生成的函數(shù)命名。如果生成的名稱與現(xiàn)有的 C 函數(shù)沖突,這偶爾可能會造成問題,有一個簡單的解決方案:覆蓋 C 函數(shù)的名稱。只要在函數(shù)聲明中加入關(guān)鍵字 "as" ,然后再加上要使用的函數(shù)名。Argument Clinic 將以該函數(shù)名為基礎(chǔ)作為(生成的)函數(shù)名,然后在后面加上 "_impl",并用作實現(xiàn)函數(shù)的名稱。

例如,若對 pickle.Pickler.dump 生成的 C 函數(shù)進(jìn)行重命名,應(yīng)如下所示:

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

...

原函數(shù)會被命名為 pickler_dumper(),而實現(xiàn)函數(shù)現(xiàn)在被命名為``pickler_dumper_impl()``。

同樣的問題依然會出現(xiàn):想給某個參數(shù)取個 Python 用名,但在 C 語言中可能用不了。Argument Clinic 允許在 Python 和 C 中為同一個參數(shù)取不同的名字,依然是利用 "as" 語法:

/*[clinic input]
pickle.Pickler.dump

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

這里 Python(簽名和 keywords 數(shù)組中)中用的名稱是 file,而 C 語言中的變量命名為 file_obj

self 參數(shù)也可以進(jìn)行重命名。

函數(shù)轉(zhuǎn)換會用到 PyArg_UnpackTuple?

若要將函數(shù)轉(zhuǎn)換為采用 PyArg_UnpackTuple() 解析其參數(shù),只需寫出所有參數(shù),并將每個參數(shù)定義為 object??梢灾付?type 參數(shù),以便能轉(zhuǎn)換為合適的類型。所有參數(shù)都應(yīng)標(biāo)記為只認(rèn)位置(在最后一個參數(shù)后面加上 /)。

目前,所生成的代碼將會用到 PyArg_ParseTuple() ,但很快會做出改動。

可選參數(shù)組?

有些過時的函數(shù)用到了一種讓人頭疼的函數(shù)解析方式:計算位置參數(shù)的數(shù)量,據(jù)此用 switch 語句進(jìn)行各個不同的 PyArg_ParseTuple() 調(diào)用。(這些函數(shù)不能接受只認(rèn)關(guān)鍵字的參數(shù)。)在沒有 PyArg_ParseTupleAndKeywords() 之前,這種方式曾被用于模擬可選參數(shù)。

雖然這種函數(shù)通??梢赞D(zhuǎn)換為采用 PyArg_ParseTupleAndKeywords() 、可選參數(shù)和默認(rèn)值的方式,但并不是全都可以做到。這些過時函數(shù)中, PyArg_ParseTupleAndKeywords() 并不能直接支持某些功能。最明顯的例子是內(nèi)置函數(shù) range(),它的必需參數(shù)的 邊存在一個可選參數(shù)!另一個例子是 curses.window.addch(),它的兩個參數(shù)是一組,必須同時指定。(參數(shù)名為 xy;如果調(diào)用函數(shù)時傳入了 x,則必須同時傳入``y``;如果未傳入 x ,則也不能傳入 y)。

不管怎么說,Argument Clinic 的目標(biāo)就是在不改變語義的情況下支持所有現(xiàn)有 CPython 內(nèi)置參數(shù)的解析。因此,Argument Clinic 采用所謂的 可選組 方案來支持這種解析方式。可選組是必須一起傳入的參數(shù)組。他們可以在必需參數(shù)的左邊或右邊,只能 用于只認(rèn)位置的參數(shù)。

備注

可選組 適用于多次調(diào)用 PyArg_ParseTuple() 的函數(shù)!采用 任何 其他方式解析參數(shù)的函數(shù),應(yīng)該 幾乎不 采用可選組轉(zhuǎn)換為 Argument Clinic 解析。目前,采用可選組的函數(shù)在 Python 中無法獲得準(zhǔn)確的簽名,因為 Python 不能理解這個概念。請盡可能避免使用可選組。

若要定義可選組,可在要分組的參數(shù)前面加上 [,在這些參數(shù)后加上``]`` ,要在同一行上。舉個例子,下面是 curses.window.addch 采用可選組的用法,前兩個參數(shù)和最后一個參數(shù)可選:

/*[clinic input]

curses.window.addch

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

    ch: object
      Character to add.

    [
    attr: long
      Attributes for the character.
    ]
    /

...

注:

  • 每一個可選組,都會額外傳入一個代表分組的參數(shù)。 參數(shù)為 int 型,名為 group_{direction}_{number},其中 {direction} 取決于此參數(shù)組位于必需參數(shù) right 還是 left,而 {number} 是一個遞增數(shù)字(從 1 開始),表示此參數(shù)組與必需參數(shù)之間的距離。 在調(diào)用函數(shù)時,若未用到此參數(shù)組則此參數(shù)將設(shè)為零,若用到了參數(shù)組則該參數(shù)為非零。 所謂的用到或未用到,是指在本次調(diào)用中形參是否收到了實參。

  • 如果不存在必需參數(shù),可選組的行為等同于出現(xiàn)在必需參數(shù)的右側(cè)。

  • 在模棱兩可的情況下,參數(shù)解析代碼更傾向于參數(shù)左側(cè)(在必需參數(shù)之前)。

  • 可選組只能包含只認(rèn)位置的參數(shù)。

  • 可選組 僅限 用于過時代碼。請勿在新的代碼中使用可選組。

采用真正的 Argument Clinic 轉(zhuǎn)換器,而不是 “傳統(tǒng)轉(zhuǎn)換器”?

為了節(jié)省時間,盡量減少要學(xué)習(xí)的內(nèi)容,實現(xiàn)第一次適用 Argument Clinic 的移植,上述練習(xí)簡述的是“傳統(tǒng)轉(zhuǎn)換器”的用法?!皞鹘y(tǒng)轉(zhuǎn)換器”只是一種簡便用法,目的就是更容易地讓現(xiàn)有代碼移植為適用于 Argument Clinic 。說白了,在移植 Python 3.4 的代碼時,可以考慮采用。

不過從長遠(yuǎn)來看,可能希望所有代碼塊都采用真正的 Argument Clinic 轉(zhuǎn)換器語法。原因如下:

  • 合適的轉(zhuǎn)換器可讀性更好,意圖也更清晰。

  • 有些格式單元是“傳統(tǒng)轉(zhuǎn)換器”無法支持的,因為這些格式需要帶上參數(shù),而傳統(tǒng)轉(zhuǎn)換器的語法不支持指定參數(shù)。

  • 后續(xù)可能會有新版的參數(shù)解析庫,提供超過 PyArg_ParseTuple() 支持的功能;而這種靈活性將無法適用于傳統(tǒng)轉(zhuǎn)換器轉(zhuǎn)換的參數(shù)。

因此,若是不介意多花一點精力,請使用正常的轉(zhuǎn)換器,而不是傳統(tǒng)轉(zhuǎn)換器。

簡而言之,Argument Clinic(非傳統(tǒng))轉(zhuǎn)換器的語法看起來像是 Python 函數(shù)調(diào)用。但如果函數(shù)沒有明確的參數(shù)(所有函數(shù)都取默認(rèn)值),則可以省略括號。因此 boolbool() 是完全相同的轉(zhuǎn)換器。

Argument Clinic 轉(zhuǎn)換器的所有參數(shù)都只認(rèn)關(guān)鍵字。所有 Argument Clinic 轉(zhuǎn)換器均可接受以下參數(shù):

c_default

該參數(shù)在 C 語言中的默認(rèn)值。具體來說,將是在“解析函數(shù)”中聲明的變量的初始化器。用法參見 the section on default values 。定義為字符串。

annotation

參數(shù)的注解值。目前尚不支持,因為 PEP 8 規(guī)定 Python 庫不得使用注解。

此外,某些轉(zhuǎn)換器還可接受額外的參數(shù)。下面列出了這些額外參數(shù)及其含義:

accept

一些 Python 類型的集合(可能還有偽類型);用于限制只接受這些類型的 Python 參數(shù)。(并非通用特性;只支持傳統(tǒng)轉(zhuǎn)換器列表中給出的類型)。

若要能接受 None,請在集合中添加 NoneType。

bitwise

僅用于無符號整數(shù)。寫入形參的將是 Python 實參的原生整數(shù)值,不做任何越界檢查,即便是負(fù)值也一樣。

converter

僅用于 object 轉(zhuǎn)換器。為某個 C 轉(zhuǎn)換函數(shù) 指定名稱,用于將對象轉(zhuǎn)換為原生類型。

encoding

僅用于字符串。指定將 Python str(Unicode) 轉(zhuǎn)換為 C 語言的 char * 時應(yīng)該采用的編碼。

subclass_of

僅用于 object 轉(zhuǎn)換器。要求 Python 值是 Python 類型的子類,用 C 語言表示。

type

僅用于 objectself 轉(zhuǎn)換器。指定用于聲明變量的 C 類型。 默認(rèn)值是 "PyObject *"。

zeroes

僅用于字符串。如果為 True,則允許在值中嵌入 NUL 字節(jié)('\\0')。字符串的長度將通過名為 <parameter_name>_length 的參數(shù)傳入,跟在字符串參數(shù)的后面。

請注意,并不是所有參數(shù)的組合都能正常生效。通常這些參數(shù)是由相應(yīng)的 PyArg_ParseTuple 格式單元 實現(xiàn)的,行為是固定的。比如目前不能不指定 bitwise=True 就去調(diào)用 unsigned_short。雖然完全有理由認(rèn)為這樣可行,但這些語義并沒有映射到任何現(xiàn)有的格式單元。所以 Argument Clinic 并不支持。(或者說,至少目前還不支持。)

下表列出了傳統(tǒng)轉(zhuǎn)換器與真正的 Argument Clinic 轉(zhuǎn)換器之間的映射關(guān)系。左邊是傳統(tǒng)的轉(zhuǎn)換器,右邊是應(yīng)該換成的文本。

'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})

舉個例子,下面是采用合適的轉(zhuǎn)換器的例子 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]*/

真正的轉(zhuǎn)換器有一個優(yōu)點,就是比傳統(tǒng)的轉(zhuǎn)換器更加靈活。例如,unsigned_int 轉(zhuǎn)換器(以及所有 unsigned_ 轉(zhuǎn)換器)可以不設(shè)置 bitwise=True 。 他們默認(rèn)會對數(shù)值進(jìn)行范圍檢查,而且不會接受負(fù)數(shù)。 用傳統(tǒng)轉(zhuǎn)換器就做不到這一點。

Argument Clinic 會列明其全部轉(zhuǎn)換器。每個轉(zhuǎn)換器都會給出可接受的全部參數(shù),以及每個參數(shù)的默認(rèn)值。只要運(yùn)行 Tools/clinic/clinic.py --converters 就能得到完整的列表。

Py_buffer?

在使用 Py_buffer 轉(zhuǎn)換器(或者 's*'、'w*'、'*y''z*' 傳統(tǒng)轉(zhuǎn)換器)時,不可 在所提供的緩沖區(qū)上調(diào)用 PyBuffer_Release()。 Argument Clinic 生成的代碼會自動完成此操作(在解析函數(shù)中)。

高級轉(zhuǎn)換器?

還記得編寫第一個函數(shù)時跳過的那些格式單元嗎,因為他們是高級內(nèi)容?下面就來介紹這些內(nèi)容。

其實訣竅在于,這些格式單元都需要給出參數(shù)——要么是轉(zhuǎn)換函數(shù),要么是類型,要么是指定編碼的字符串。(但 “傳統(tǒng)轉(zhuǎn)換器”不支持參數(shù)。這就是為什么第一個函數(shù)要跳過這些內(nèi)容)。為格式單元指定的參數(shù)于是就成了轉(zhuǎn)換器的參數(shù);參數(shù)可以是 converter``(對于 ``O&)、subclass_of``(對于 ``O!),或者是 encoding (對于 e 開頭的格式單元)。

在使用 subclass_of 時,可能還需要用到 object() 的另一個自定義參數(shù):type,用于設(shè)置參數(shù)的實際類型。例如,為了確保對象是 PyUnicode_Type 的子類,可能想采用轉(zhuǎn)換器 object(type='PyUnicodeObject *', subclass_of='&PyUnicode_Type')。

Argument Clinic 用起來可能存在一個問題:喪失了 e 開頭的格式單位的一些靈活性。在手工編寫 PyArg_Parse 調(diào)用時,理論上可以在運(yùn)行時決定傳給 PyArg_ParseTuple() 的編碼字符串。但現(xiàn)在這個字符串必須在 Argument-Clinic 預(yù)處理時進(jìn)行硬編碼。這個限制是故意設(shè)置的;以便簡化對這種格式單元的支持,并允許以后進(jìn)行優(yōu)化。這個限制似乎并不合理;CPython 本身總是為 e 開頭的格式單位參數(shù)傳入靜態(tài)的硬編碼字符串。

參數(shù)的默認(rèn)值?

參數(shù)的默認(rèn)值可以是多個值中的一個。最簡單的可以是字符串、int 或 float 字面量。

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

還可以使用 Python 的任何內(nèi)置常量。

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

對默認(rèn)值 NULL 和簡單表達(dá)式還提供特別的支持,下面將一一介紹。

默認(rèn)值 NULL?

對于字符串和對象參數(shù)而言,可以設(shè)為 None,表示沒有默認(rèn)值。但這意味著會將 C 變量初始化為 Py_None。為了方便起見,提供了一個特殊值``NULL``,目的就是為了讓 Python 認(rèn)為默認(rèn)值就是 None,而 C 變量則會初始化為 NULL。

設(shè)為默認(rèn)值的表達(dá)式?

參數(shù)的默認(rèn)值不僅可以是字面量。還可以是一個完整的表達(dá)式,可采用數(shù)學(xué)運(yùn)算符及對象的屬性。但這種支持并沒有那么簡單,因為存在一些不明顯的語義。

請考慮以下例子:

foo: Py_ssize_t = sys.maxsize - 1

sys.maxsize 在不同的系統(tǒng)平臺可能有不同的值。因此,Argument Clinic 不能簡單地在本底環(huán)境對表達(dá)式求值并用 C 語言硬編碼。所以默認(rèn)值將用表達(dá)式的方式存儲下來,運(yùn)行的時候在請求函數(shù)簽名時會被求值。

在對表達(dá)式進(jìn)行求值時,可以使用什么命名空間呢?求值過程運(yùn)行于內(nèi)置模塊的上下文中。 因此,如果模塊帶有名為 max_widgets 的屬性,直接引用即可。

foo: Py_ssize_t = max_widgets

如果表達(dá)式不在當(dāng)前模塊中,就會去 sys.modules 查找。比如 sys.maxsize 就是如此找到的。(因為事先不知道用戶會加載哪些模塊到解釋器中,所以最好只用到 Python 會預(yù)加載的模塊。)

僅當(dāng)運(yùn)行時才對缺省值求值,意味著 Argument Clinic 無法計算出正確的 C 缺省值。所以需顯式給出。在使用表達(dá)式時,必須同時用轉(zhuǎn)換器的``c_default`` 參數(shù)指定 C 語言中的等價表達(dá)式。

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

還有一個問題也比較復(fù)雜。Argument Clinic 無法事先知道表達(dá)式是否有效。 解析只能保證看起來是有效值,但無法 實際 知曉。在用表達(dá)式時須十分小心,確保在運(yùn)行時能得到有效值。

最后一點,由于表達(dá)式必須能表示為靜態(tài)的 C 語言值,所以存在許多限制。 以下列出了不得使用的 Python 特性:

  • 功能

  • 行內(nèi) if 語句(3 if foo else 5

  • 序列類自動解包(*[1, 2, 3]

  • 列表、集合、字典的解析和生成器表達(dá)式。

  • 元組、列表、集合、字典的字面量

返回值轉(zhuǎn)換器?

Argument Clinic 生成的植入函數(shù)默認(rèn)會返回 PyObject *。但是通常 C 函數(shù)的任務(wù)是要對某些 C 類型進(jìn)行計算,然后將其轉(zhuǎn)換為 PyObject * 作為結(jié)果。Argument Clinic 可以將輸入?yún)?shù)由 Python 類型轉(zhuǎn)換為本地 C 類型——為什么不讓它將返回值由本地 C 類型轉(zhuǎn)換為 Python 類型呢?

這就是“返回值轉(zhuǎn)換器”的用途。它將植入函數(shù)修改成返回某種 C 語言類型,然后在生成的(非植入)函數(shù)中添加代碼,以便將 C 語言值轉(zhuǎn)換為合適的 PyObject *

返回值轉(zhuǎn)換器的語法與參數(shù)轉(zhuǎn)換器的類似。返回值轉(zhuǎn)換器的定義方式,類似于函數(shù)返回值的注解。返回值轉(zhuǎn)換器的行為與參數(shù)轉(zhuǎn)換器基本相同,接受參數(shù),參數(shù)只認(rèn)關(guān)鍵字,如果不修改默認(rèn)參數(shù)則可省略括號。

(如果函數(shù)同時用到了 "as" 和返回值轉(zhuǎn)換器, "as" 應(yīng)位于返回值轉(zhuǎn)換器之前。)

返回值轉(zhuǎn)換器還存在一個復(fù)雜的問題:出錯信息如何表示?通常函數(shù)在執(zhí)行成功時會返回一個有效(非 NULL)指針,失敗則返回 NULL。但如果使用了整數(shù)的返回值轉(zhuǎn)換器,所有整數(shù)都是有效值。Argument Clinic 怎么檢測錯誤呢?解決方案是:返回值轉(zhuǎn)換器會隱含尋找一個代表錯誤的特殊值。如果返回該特殊值,且設(shè)置了出錯標(biāo)記( PyErr_Occurred() 返回 True),那么生成的代碼會傳遞該錯誤。否則,會對返回值進(jìn)行正常編碼。

目前 Argument Clinic 只支持少數(shù)幾種返回值轉(zhuǎn)換器。

bool
int
unsigned int
long
unsigned int
size_t
Py_ssize_t
float
double
DecodeFSDefault

這些轉(zhuǎn)換器都不需要參數(shù)。前3個轉(zhuǎn)換器如果返回 -1 則表示出錯。DecodeFSDefault 的返回值類型是 const char *;若返回 NULL 指針則表示出錯。

(還有一個 NoneType 轉(zhuǎn)換器是實驗性質(zhì)的,成功時返回 Py_None ,失敗則返回 NULL,且不會增加 Py_None 的引用計數(shù)。此轉(zhuǎn)換器是否值得適用,尚不明確)。

只要運(yùn)行 Tools/clinic/clinic.py --converters ,即可查看 Argument Clinic 支持的所有返回值轉(zhuǎn)換器,包括其參數(shù)。

克隆已有的函數(shù)?

如果已有一些函數(shù)比較相似,或許可以采用 Clinic 的“克隆”功能。 克隆之后能夠復(fù)用以下內(nèi)容:

  • 參數(shù),包括:

    • 名稱

    • 轉(zhuǎn)換器(帶有全部參數(shù))

    • 默認(rèn)值

    • 參數(shù)前的文檔字符串

    • 類別 (只認(rèn)位置、位置或關(guān)鍵字、只認(rèn)關(guān)鍵字)

  • 返回值轉(zhuǎn)換器

唯一不從原函數(shù)中復(fù)制的是文檔字符串;這樣就能指定一個新的文檔串。

下面是函數(shù)的克隆方法:

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

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

(原函數(shù)可以位于不同的模塊或類中。示例中的 module.class 只是為了說明,兩個 函數(shù)都必須使用全路徑)。

對不起,沒有什么語法可對函數(shù)進(jìn)行部分克隆或克隆后進(jìn)行修改??寺∫慈幸慈珶o。

另外,要克隆的函數(shù)必須在當(dāng)前文件中已有定義。

調(diào)用 Python 代碼?

下面的高級內(nèi)容需要編寫 Python 代碼,存于 C 文件中,并修改 Argument Clinic 的運(yùn)行狀態(tài)。其實很簡單:只需定義一個 Python 塊。

Python 塊的分隔線與 Argument Clinic 函數(shù)塊不同。如下所示:

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

Python 塊內(nèi)的所有代碼都會在解析時執(zhí)行。塊內(nèi)寫入 stdout 的所有文本都被重定向到塊后的“輸出”部分。

以下例子包含了 Python 塊,用于在 C 代碼中添加一個靜態(tài)整數(shù)變量:

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

self 轉(zhuǎn)換器的用法?

Argument Clinic 用一個默認(rèn)的轉(zhuǎn)換器自動添加一個“self”參數(shù)。自動將 self 參數(shù)的 type 設(shè)為聲明類型時指定的“指向?qū)嵗闹羔槨薄2贿^ Argument Clinic 的轉(zhuǎn)換器可被覆蓋,也即自己指定一個轉(zhuǎn)換器。只要將自己的 self 參數(shù)作為塊的第一個參數(shù)即可,并確保其轉(zhuǎn)換器是 self_converter 的實例或其子類。

這有什么用呢?可用于覆蓋 self 的類型,或為其給個不同的默認(rèn)名稱。

如何指定 self 對應(yīng)的自定義類型呢?如果只有 self 類型相同的一兩個函數(shù),可以直接使用 Argument Clinic 現(xiàn)有的 self 轉(zhuǎn)換器,把要用的類型作為 type 參數(shù)傳入:

/*[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]*/

如果有很多函數(shù)將使用同一類型的 self,則最好創(chuàng)建自己的轉(zhuǎn)換器,繼承自 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]*/

“定義類”轉(zhuǎn)換器?

Argument Clinic 為訪問方法的定義類提供了便利。這對 heap type 方法十分有用,因為它需要獲取模塊級的運(yùn)行狀態(tài)。用 PyType_FromModuleAndSpec() 將新的堆類型與模塊聯(lián)系起來。現(xiàn)在可以在定義類上用 PyType_GetModuleState() 獲取模塊狀態(tài)了,例如從模塊方法中獲取。

示例來自 Modules/zlibmodule.c。首先,在 clinic 的輸入塊添加 defining_class

/*[clinic input]
zlib.Compress.compress

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

運(yùn)行 Argument Clinic 工具后,會生成以下函數(shù)簽名:

/*[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]*/

現(xiàn)在,以下代碼可以用 PyType_GetModuleState(cls) 獲取模塊狀態(tài)了:

zlibstate *state = PyType_GetModuleState(cls);

Each method may only have one argument using this converter, and it must appear after self, or, if self is not used, as the first argument. The argument will be of type PyTypeObject *. The argument will not appear in the __text_signature__.

The defining_class converter is not compatible with __init__ and __new__ methods, which cannot use the METH_METHOD convention.

It is not possible to use defining_class with slot methods. In order to fetch the module state from such methods, use PyType_GetModuleByDef() to look up the module and then PyModule_GetState() to fetch the module state. Example from the setattro slot method in Modules/_threadmodule.c:

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);
    ...
}

See also PEP 573.

Writing a custom converter?

As we hinted at in the previous section... you can write your own converters! A converter is simply a Python class that inherits from CConverter. The main purpose of a custom converter is if you have a parameter using the O& format unit—parsing this parameter means calling a PyArg_ParseTuple() "converter function".

Your converter class should be named *something*_converter. If the name follows this convention, then your converter class will be automatically registered with Argument Clinic; its name will be the name of your class with the _converter suffix stripped off. (This is accomplished with a metaclass.)

You shouldn't subclass CConverter.__init__. Instead, you should write a converter_init() function. converter_init() always accepts a self parameter; after that, all additional parameters must be keyword-only. Any arguments passed in to the converter in Argument Clinic will be passed along to your converter_init().

There are some additional members of CConverter you may wish to specify in your subclass. Here's the current list:

type

The C type to use for this variable. type should be a Python string specifying the type, e.g. int. If this is a pointer type, the type string should end with ' *'.

default

The Python default value for this parameter, as a Python value. Or the magic value unspecified if there is no default.

py_default

default as it should appear in Python code, as a string. Or None if there is no default.

c_default

default as it should appear in C code, as a string. Or None if there is no default.

c_ignored_default

The default value used to initialize the C variable when there is no default, but not specifying a default may result in an "uninitialized variable" warning. This can easily happen when using option groups—although properly-written code will never actually use this value, the variable does get passed in to the impl, and the C compiler will complain about the "use" of the uninitialized value. This value should always be a non-empty string.

converter

The name of the C converter function, as a string.

impl_by_reference

A boolean value. If true, Argument Clinic will add a & in front of the name of the variable when passing it into the impl function.

parse_by_reference

A boolean value. If true, Argument Clinic will add a & in front of the name of the variable when passing it into PyArg_ParseTuple().

Here's the simplest example of a custom converter, from 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]*/

This block adds a converter to Argument Clinic named ssize_t. Parameters declared as ssize_t will be declared as type Py_ssize_t, and will be parsed by the 'O&' format unit, which will call the ssize_t_converter converter function. ssize_t variables automatically support default values.

More sophisticated custom converters can insert custom C code to handle initialization and cleanup. You can see more examples of custom converters in the CPython source tree; grep the C files for the string CConverter.

Writing a custom return converter?

Writing a custom return converter is much like writing a custom converter. Except it's somewhat simpler, because return converters are themselves much simpler.

Return converters must subclass CReturnConverter. There are no examples yet of custom return converters, because they are not widely used yet. If you wish to write your own return converter, please read Tools/clinic/clinic.py, specifically the implementation of CReturnConverter and all its subclasses.

METH_O and METH_NOARGS?

To convert a function using METH_O, make sure the function's single argument is using the object converter, and mark the arguments as positional-only:

/*[clinic input]
meth_o_sample

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

To convert a function using METH_NOARGS, just don't specify any arguments.

You can still use a self converter, a return converter, and specify a type argument to the object converter for METH_O.

tp_new and tp_init functions?

You can convert tp_new and tp_init functions. Just name them __new__ or __init__ as appropriate. Notes:

  • The function name generated for __new__ doesn't end in __new__ like it would by default. It's just the name of the class, converted into a valid C identifier.

  • No PyMethodDef #define is generated for these functions.

  • __init__ functions return int, not PyObject *.

  • Use the docstring as the class docstring.

  • Although __new__ and __init__ functions must always accept both the args and kwargs objects, when converting you may specify any signature for these functions that you like. (If your function doesn't support keywords, the parsing function generated will throw an exception if it receives any.)

Changing and redirecting Clinic's output?

It can be inconvenient to have Clinic's output interspersed with your conventional hand-edited C code. Luckily, Clinic is configurable: you can buffer up its output for printing later (or earlier!), or write its output to a separate file. You can also add a prefix or suffix to every line of Clinic's generated output.

While changing Clinic's output in this manner can be a boon to readability, it may result in Clinic code using types before they are defined, or your code attempting to use Clinic-generated code before it is defined. These problems can be easily solved by rearranging the declarations in your file, or moving where Clinic's generated code goes. (This is why the default behavior of Clinic is to output everything into the current block; while many people consider this hampers readability, it will never require rearranging your code to fix definition-before-use problems.)

Let's start with defining some terminology:

field

A field, in this context, is a subsection of Clinic's output. For example, the #define for the PyMethodDef structure is a field, called methoddef_define. Clinic has seven different fields it can output per function definition:

docstring_prototype
docstring_definition
methoddef_define
impl_prototype
parser_prototype
parser_definition
impl_definition

All the names are of the form "<a>_<b>", where "<a>" is the semantic object represented (the parsing function, the impl function, the docstring, or the methoddef structure) and "<b>" represents what kind of statement the field is. Field names that end in "_prototype" represent forward declarations of that thing, without the actual body/data of the thing; field names that end in "_definition" represent the actual definition of the thing, with the body/data of the thing. ("methoddef" is special, it's the only one that ends with "_define", representing that it's a preprocessor #define.)

destination

A destination is a place Clinic can write output to. There are five built-in destinations:

block

The default destination: printed in the output section of the current Clinic block.

buffer

A text buffer where you can save text for later. Text sent here is appended to the end of any existing text. It's an error to have any text left in the buffer when Clinic finishes processing a file.

file

A separate "clinic file" that will be created automatically by Clinic. The filename chosen for the file is {basename}.clinic{extension}, where basename and extension were assigned the output from os.path.splitext() run on the current file. (Example: the file destination for _pickle.c would be written to _pickle.clinic.c.)

Important: When using a file destination, you must check in the generated file!

two-pass

A buffer like buffer. However, a two-pass buffer can only be dumped once, and it prints out all text sent to it during all processing, even from Clinic blocks after the dumping point.

suppress

The text is suppressed—thrown away.

Clinic defines five new directives that let you reconfigure its output.

The first new directive is dump:

dump <destination>

This dumps the current contents of the named destination into the output of the current block, and empties it. This only works with buffer and two-pass destinations.

The second new directive is output. The most basic form of output is like this:

output <field> <destination>

This tells Clinic to output field to destination. output also supports a special meta-destination, called everything, which tells Clinic to output all fields to that destination.

output has a number of other functions:

output push
output pop
output preset <preset>

output push and output pop allow you to push and pop configurations on an internal configuration stack, so that you can temporarily modify the output configuration, then easily restore the previous configuration. Simply push before your change to save the current configuration, then pop when you wish to restore the previous configuration.

output preset sets Clinic's output to one of several built-in preset configurations, as follows:

block

Clinic's original starting configuration. Writes everything immediately after the input block.

Suppress the parser_prototype and docstring_prototype, write everything else to block.

file

Designed to write everything to the "clinic file" that it can. You then #include this file near the top of your file. You may need to rearrange your file to make this work, though usually this just means creating forward declarations for various typedef and PyTypeObject definitions.

Suppress the parser_prototype and docstring_prototype, write the impl_definition to block, and write everything else to file.

The default filename is "{dirname}/clinic/{basename}.h".

buffer

Save up most of the output from Clinic, to be written into your file near the end. For Python files implementing modules or builtin types, it's recommended that you dump the buffer just above the static structures for your module or builtin type; these are normally very near the end. Using buffer may require even more editing than file, if your file has static PyMethodDef arrays defined in the middle of the file.

Suppress the parser_prototype, impl_prototype, and docstring_prototype, write the impl_definition to block, and write everything else to file.

two-pass

Similar to the buffer preset, but writes forward declarations to the two-pass buffer, and definitions to the buffer. This is similar to the buffer preset, but may require less editing than buffer. Dump the two-pass buffer near the top of your file, and dump the buffer near the end just like you would when using the buffer preset.

Suppresses the impl_prototype, write the impl_definition to block, write docstring_prototype, methoddef_define, and parser_prototype to two-pass, write everything else to buffer.

partial-buffer

Similar to the buffer preset, but writes more things to block, only writing the really big chunks of generated code to buffer. This avoids the definition-before-use problem of buffer completely, at the small cost of having slightly more stuff in the block's output. Dump the buffer near the end, just like you would when using the buffer preset.

Suppresses the impl_prototype, write the docstring_definition and parser_definition to buffer, write everything else to block.

The third new directive is destination:

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

This performs an operation on the destination named name.

There are two defined subcommands: new and clear.

The new subcommand works like this:

destination <name> new <type>

This creates a new destination with name <name> and type <type>.

There are five destination types:

suppress

Throws the text away.

block

Writes the text to the current block. This is what Clinic originally did.

buffer

A simple text buffer, like the "buffer" builtin destination above.

file

A text file. The file destination takes an extra argument, a template to use for building the filename, like so:

destination <name> new <type> <file_template>

The template can use three strings internally that will be replaced by bits of the filename:

{path}

The full path to the file, including directory and full filename.

{dirname}

The name of the directory the file is in.

{basename}

Just the name of the file, not including the directory.

{basename_root}

Basename with the extension clipped off (everything up to but not including the last '.').

{basename_extension}

The last '.' and everything after it. If the basename does not contain a period, this will be the empty string.

If there are no periods in the filename, {basename} and {filename} are the same, and {extension} is empty. "{basename}{extension}" is always exactly the same as "{filename}"."

two-pass

A two-pass buffer, like the "two-pass" builtin destination above.

The clear subcommand works like this:

destination <name> clear

It removes all the accumulated text up to this point in the destination. (I don't know what you'd need this for, but I thought maybe it'd be useful while someone's experimenting.)

The fourth new directive is set:

set line_prefix "string"
set line_suffix "string"

set lets you set two internal variables in Clinic. line_prefix is a string that will be prepended to every line of Clinic's output; line_suffix is a string that will be appended to every line of Clinic's output.

Both of these support two format strings:

{block comment start}

Turns into the string /*, the start-comment text sequence for C files.

{block comment end}

Turns into the string */, the end-comment text sequence for C files.

The final new directive is one you shouldn't need to use directly, called preserve:

preserve

This tells Clinic that the current contents of the output should be kept, unmodified. This is used internally by Clinic when dumping output into file files; wrapping it in a Clinic block lets Clinic use its existing checksum functionality to ensure the file was not modified by hand before it gets overwritten.

The #ifdef trick?

If you're converting a function that isn't available on all platforms, there's a trick you can use to make life a little easier. The existing code probably looks like this:

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

And then in the PyMethodDef structure at the bottom the existing code will have:

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

In this scenario, you should enclose the body of your impl function inside the #ifdef, like so:

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

Then, remove those three lines from the PyMethodDef structure, replacing them with the macro Argument Clinic generated:

MODULE_FUNCTIONNAME_METHODDEF

(You can find the real name for this macro inside the generated code. Or you can calculate it yourself: it's the name of your function as defined on the first line of your block, but with periods changed to underscores, uppercased, and "_METHODDEF" added to the end.)

Perhaps you're wondering: what if HAVE_FUNCTIONNAME isn't defined? The MODULE_FUNCTIONNAME_METHODDEF macro won't be defined either!

Here's where Argument Clinic gets very clever. It actually detects that the Argument Clinic block might be deactivated by the #ifdef. When that happens, it generates a little extra code that looks like this:

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

That means the macro always works. If the function is defined, this turns into the correct structure, including the trailing comma. If the function is undefined, this turns into nothing.

However, this causes one ticklish problem: where should Argument Clinic put this extra code when using the "block" output preset? It can't go in the output block, because that could be deactivated by the #ifdef. (That's the whole point!)

In this situation, Argument Clinic writes the extra code to the "buffer" destination. This may mean that you get a complaint from Argument Clinic:

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

When this happens, just open your file, find the dump buffer block that Argument Clinic added to your file (it'll be at the very bottom), then move it above the PyMethodDef structure where that macro is used.

Using Argument Clinic in Python files?

It's actually possible to use Argument Clinic to preprocess Python files. There's no point to using Argument Clinic blocks, of course, as the output wouldn't make any sense to the Python interpreter. But using Argument Clinic to run Python blocks lets you use Python as a Python preprocessor!

Since Python comments are different from C comments, Argument Clinic blocks embedded in Python files look slightly different. They look like this:

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