新聞中心
Argument Clinic 的用法
作者

Larry Hastings
摘要
Argument Clinic 是 Cpython 的一個(gè) C 文件預(yù)處理器。旨在自動(dòng)處理所有與“內(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è)計(jì)目標(biāo)
Argument Clinic 的主要目標(biāo),是接管 CPython 中的所有參數(shù)解析代碼。這意味著,如果要把某個(gè)函數(shù)轉(zhuǎn)換為配合 Argument Clinic一起工作,則該函數(shù)不應(yīng)再作任何參數(shù)解析工作——Argument Clinic 生成的代碼應(yīng)該是個(gè)“黑盒”,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)該 可以實(shí)現(xiàn)較大的速度提升——代碼生成器可以重寫一下,以產(chǎn)生量身定做的參數(shù)解析代碼,而不是調(diào)用通用的 CPython 參數(shù)解析庫。 這會讓參數(shù)解析達(dá)到最佳速度!)
此外,Argument Clinic 必須足夠靈活,能夠與任何參數(shù)解析的方法一起工作。Python 有一些函數(shù)具備一些非常奇怪的解析行為;Argument Clinic 的目標(biāo)是支持所有這些函數(shù)。
最后,Argument Clinic 的初衷是為 CPython 內(nèi)置程序提供內(nèi)省“簽名”。以前如果傳入一個(gè)內(nèi)置函數(shù),內(nèi)省查詢函數(shù)會拋出異常。有了 Argument Clinic,再不會發(fā)生這種問題了!
在與 Argument Clinic 合作時(shí),應(yīng)該牢記一個(gè)理念:給它的信息越多,它做得就會越好。誠然,Argument Clinic 現(xiàn)在還比較簡單。但會演變得越來越復(fù)雜,應(yīng)該能夠利用給出的全部信息干很多聰明而有趣的事情。
基本概念和用法
Argument Clinic 與 CPython 一起提供,位于 Tools/clinic/clinic.py 。若要運(yùn)行它,請指定一個(gè) 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 在解析某一塊時(shí),會生成輸出信息。輸出信息會緊跟著該塊寫入 C 文件中,后面還會跟著包含校驗(yàn)和的注釋?,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àn)行的輸出信息。不過如果輸入沒有變化,則輸出也不會有變化。
不應(yīng)去改動(dòng) Argument Clinic 塊的輸出部分。而應(yīng)去修改輸入,直到生成所需的輸出信息。(這就是校驗(yàn)和的用途——檢測是否有人改動(dòng)了輸出信息,因?yàn)樵?Argument Clinic 下次寫入新的輸出時(shí),這些改動(dòng)都會丟失)。
為了清晰起見,下面列出了 Argument Clinic 用到的術(shù)語:
-
注釋的第一行
/*[clinic input]是 起始行 。 -
注釋(
[clinic start generated code]*/)的最后一行是 結(jié)束行。 -
最后一行(
/*[clinic end generated code: checksum=...]*/)是 校驗(yàn)和行 。 -
在起始行和結(jié)束行之間是 輸入數(shù)據(jù)。
-
在結(jié)束行和校驗(yàn)和行之間是 輸出數(shù)據(jù) 。
-
從開始行到校驗(yàn)和行的所有文本,都是 塊。(Argument Clinic 尚未處理成功的塊,沒有輸出或校驗(yàn)和行,但仍視作一個(gè)塊)。
函數(shù)的轉(zhuǎn)換
要想了解 Argument Clinic 是如何工作的,最好的方式就是轉(zhuǎn)換一個(gè)函數(shù)與之合作。下面介紹需遵循的最基本步驟。請注意,若真的準(zhǔn)備在 CPython 中進(jìn)行檢查,則應(yīng)進(jìn)行更深入的轉(zhuǎn)換,使用一些本文后續(xù)會介紹到的高級概念(比如 “返回轉(zhuǎn)換” 和 “自轉(zhuǎn)換”)。但以下例子將維持簡單,以供學(xué)習(xí)。
就此開始
-
請確保 CPython 是最新的已簽出版本。
-
找到一個(gè)調(diào)用 PyArg_ParseTuple() 或 PyArg_ParseTupleAndKeywords() ,且未被轉(zhuǎn)換為采用 Argument Clinic 的 Python 內(nèi)置程序。這里用了
_pickle.Pickler.dump()。 -
如果對
PyArg_Parse函數(shù)的調(diào)用采用了以下格式化單元:O&O!eses#etet#
或者多次調(diào)用 PyArg_ParseTuple(),則應(yīng)再選一個(gè)函數(shù)。Argument Clinic 確實(shí) 支持上述這些狀況。 但這些都是高階內(nèi)容——第一次就簡單一些吧。
此外,如果多次調(diào)用 PyArg_ParseTuple() 或 PyArg_ParseTupleAndKeywords() 且同一參數(shù)需支持不同的類型,或者用到 PyArg_Parse 以外的函數(shù)來解析參數(shù),則可能不適合轉(zhuǎn)換為 Argument Clinic 形式。 Argument Clinic 不支持通用函數(shù)或多態(tài)參數(shù)。
-
在函數(shù)上方添加以下模板,創(chuàng)建塊:
/*[clinic input][clinic start generated code]*/
-
剪下文檔字符串并粘貼到
[clinic]行之間,去除所有的無用字符,使其成為一個(gè)正確引用的 C 字符串。最有應(yīng)該只留下帶有左側(cè)縮進(jìn)的文本,且行寬不大于 80 個(gè)字符。(參數(shù) Clinic 將保留文檔字符串中的縮進(jìn)。)如果文檔字符串的第一行看起來像是函數(shù)的簽名,就把這一行去掉吧。((文檔串不再需要用到它——將來對內(nèi)置函數(shù)調(diào)用
help()時(shí),第一行將根據(jù)函數(shù)的簽名自動(dòng)建立。)示例:
/*[clinic input]Write a pickled representation of obj to the open file.[clinic start generated code]*/
-
如果文檔字符串中沒有“摘要”行,Argument Clinic 會報(bào)錯(cuò)。所以應(yīng)確保帶有摘要行。 “摘要”行應(yīng)為在文檔字符串開頭的一個(gè)段落,由一個(gè)80列的單行構(gòu)成。
(示例的文檔字符串只包括一個(gè)摘要行,所以示例代碼這一步不需改動(dòng))。
-
在文檔字符串上方,輸入函數(shù)的名稱,后面是空行。這應(yīng)是函數(shù)的 Python 名稱,而且應(yīng)是句點(diǎn)分隔的完整路徑——以模塊的名稱開始,包含所有子模塊名,若函數(shù)為類方法則還應(yīng)包含類名。
示例:
/*[clinic input]_pickle.Pickler.dumpWrite a pickled representation of obj to the open file.[clinic start generated code]*/
-
如果是第一次在此 C 文件中用到 Argument Clinic 的模塊或類,必須對其進(jìn)行聲明。清晰的 Argument Clinic 寫法應(yīng)于 C 文件頂部附近的某個(gè)單獨(dú)塊中聲明這些,就像 include 文件和 statics 放在頂部一樣。(在這里的示例代碼中,將這兩個(gè)塊相鄰給出。)
類和模塊的名稱應(yīng)與暴露給 Python 的相同。請適時(shí)檢查 PyModuleDef 或 PyTypeObject 中定義的名稱。
在聲明某個(gè)類時(shí),還必須指定其 C 語言類型的兩個(gè)部分:用于指向該類實(shí)例的指針的類型聲明,和指向該類 PyTypeObject 的指針。
示例:
/*[clinic input]module _pickleclass _pickle.Pickler "PicklerObject *" "&Pickler_Type"[clinic start generated code]*//*[clinic input]_pickle.Pickler.dumpWrite a pickled representation of obj to the open file.[clinic start generated code]*/
-
聲明函數(shù)的所有參數(shù)。每個(gè)參數(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ù)行下面添加一個(gè)空行。
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 “l(fā)egacy converter”—a convenience syntax intended to make porting old code into Argument Clinic easier.
每個(gè)參數(shù)都要從``PyArg_Parse()`` 格式參數(shù)中復(fù)制其 “格式單元”,并以帶引號字符串的形式指定其轉(zhuǎn)換器。(“格式單元”是
format參數(shù)的1-3個(gè)字符的正式名稱,用于讓參數(shù)解析函數(shù)知曉該變量的類型及轉(zhuǎn)換方法。關(guān)于格式單位的更多信息,請參閱 解析參數(shù)并構(gòu)建值變量 )。對于像
z#這樣的多字符格式單元,要使用2-3個(gè)字符組成的整個(gè)字符串。示例:
/*[clinic input]module _pickleclass _pickle.Pickler "PicklerObject *" "&Pickler_Type"[clinic start generated code]*//*[clinic input]_pickle.Pickler.dumpobj: 'O'Write a pickled representation of obj to the open file.[clinic start generated code]*/
-
如果函數(shù)的格式字符串包含
|,意味著有些參數(shù)帶有缺省值,這可以忽略。Argument Clinic 根據(jù)參數(shù)是否有缺省值來推斷哪些參數(shù)是可選的。如果函數(shù)的格式字符串中包含 $,意味著只接受關(guān)鍵字參數(shù),請?jiān)诘谝粋€(gè)關(guān)鍵字參數(shù)之前單獨(dú)給出一行
*,縮進(jìn)與參數(shù)行對齊。(
_pickle.Pickler.dump兩種格式字符串都沒有,所以這里的示例不用改動(dòng)。) -
如果 C 函數(shù)調(diào)用的是 PyArg_ParseTuple() (而不是 PyArg_ParseTupleAndKeywords()),那么其所有參數(shù)均是僅限位置參數(shù)。
若要在 Argument Clinic 中把所有參數(shù)都標(biāo)記為只認(rèn)位置,請?jiān)谧詈笠粋€(gè)參數(shù)后面一行加入一個(gè)
/,縮進(jìn)程度與參數(shù)行對齊。目前這個(gè)標(biāo)記是全體生效;要么所有參數(shù)都是只認(rèn)位置,要么都不是。(以后 Argument Clinic 可能會放寬這一限制。)
示例:
/*[clinic input]module _pickleclass _pickle.Pickler "PicklerObject *" "&Pickler_Type"[clinic start generated code]*//*[clinic input]_pickle.Pickler.dumpobj: 'O'/Write a pickled representation of obj to the open file.[clinic start generated code]*/
-
為每個(gè)參數(shù)都編寫一個(gè)文檔字符串,這很有意義。但這是可選項(xiàng);可以跳過這一步。
下面介紹如何添加逐參數(shù)的文檔字符串。逐參數(shù)文檔字符串的第一行必須比參數(shù)定義多縮進(jìn)一層。第一行的左邊距即確定了所有逐參數(shù)文檔字符串的左邊距;所有文檔字符串文本都要同等縮進(jìn)。文本可以用多行編寫。
示例:
/*[clinic input]module _pickleclass _pickle.Pickler "PicklerObject *" "&Pickler_Type"[clinic start generated code]*//*[clinic input]_pickle.Pickler.dumpobj: 'O'The object to be pickled./Write a pickled representation of obj to the open file.[clinic start generated code]*/
-
保存并關(guān)閉該文件,然后運(yùn)行
Tools/clinic/clinic.py。 運(yùn)氣好的話就萬事大吉——程序塊現(xiàn)在有了輸出信息,并且生成了一個(gè).c.h文件!在文本編輯器中重新打開該文件,可以看到:/*[clinic input]_pickle.Pickler.dumpobj: '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)生任何輸出,那是因?yàn)樵谳斎胄畔⒅邪l(fā)現(xiàn)了錯(cuò)誤。繼續(xù)修正錯(cuò)誤并重試,直至 Argument Clinic 正確地處理好文件。
為了便于閱讀,大部分“膠水”代碼已寫入
.c.h文件中。需在原.c文件中包含這個(gè)文件,通常是在 clinic 模塊之后:#include "clinic/_pickle.c.h"
-
請仔細(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é)尾,這種改動(dòng)不會影響使用,因此不必?fù)?dān)心。)第三,如果格式單元需要指定兩個(gè)參數(shù)(比如長度、編碼字符串或指向轉(zhuǎn)換函數(shù)的指針),請確保第二個(gè)參數(shù)在兩次調(diào)用時(shí) 完全 相同。
第四,在輸出部分會有一個(gè)預(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ǎn)存在不一致,請調(diào)整 Argument Clinic 函數(shù)定義,并重新運(yùn)行
Tools/clinic/clinic.py,直至 完全 相同。 -
注意,輸出部分的最后一行是“實(shí)現(xiàn)”函數(shù)的聲明。也就是該內(nèi)置函數(shù)的實(shí)現(xiàn)代碼所在。刪除需要修改的函數(shù)的現(xiàn)有原型,但保留開頭的大括號。再刪除其參數(shù)解析代碼和輸入變量的所有聲明。注意現(xiàn)在 Python 所見的參數(shù)即為此實(shí)現(xiàn)函數(shù)的參數(shù);如果實(shí)現(xiàn)代碼給這些變量采用了不同的命名,請進(jìn)行修正。
因?yàn)樯燥@怪異,所以還是重申一下?,F(xiàn)在的代碼應(yīng)該如下所示:
static return_typeyour_function_impl(...)/*[clinic end generated code: checksum=...]*/{...
上面是 Argument Clinic 生成的校驗(yàn)值和函數(shù)原型。函數(shù)應(yīng)該帶有閉合的大括號,實(shí)現(xiàn)代碼位于其中。
示例:
/*[clinic input]module _pickleclass _pickle.Pickler "PicklerObject *" "&Pickler_Type"[clinic start generated code]*//*[clinic end generated code: checksum=da39a3ee5e6b4b0d3255bfef95601890afd80709]*//*[clinic input]_pickle.Pickler.dumpobj: '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, whichwould 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;...
-
還記得用到 PyMethodDef 結(jié)構(gòu)的宏吧?找到函數(shù)中已有的 PyMethodDef 結(jié)構(gòu),并替換為宏的引用。(如果函數(shù)是模塊級的,可能會在文件的末尾附近;如果函數(shù)是個(gè)類方法,則可能會在靠近實(shí)現(xiàn)代碼的下方。)
注意,宏尾部帶有一個(gè)逗號。所以若用宏替換已有的靜態(tài)結(jié)構(gòu) PyMethodDef 時(shí),請勿 在結(jié)尾添加逗號了。
示例:
static struct PyMethodDef Pickler_methods[] = {__PICKLE_PICKLER_DUMP_METHODDEF__PICKLE_PICKLER_CLEAR_MEMO_METHODDEF{NULL, NULL} /* sentinel */};
-
Compile, then run the relevant portions of the regression-test suite. This change should not introduce any new compile-time warnings or errors, and there should be no externally visible change to Python’s behavior.
差別只有一個(gè),即
inspect.signature()運(yùn)行于新的函數(shù)上,現(xiàn)在應(yīng)該新提供一個(gè)有效的簽名!祝賀你,現(xiàn)在已經(jīng)用 Argument Clinic 移植了第一個(gè)函數(shù)。
進(jìn)階
現(xiàn)在 Argument Clinic 的使用經(jīng)驗(yàn)已具備了一些,該介紹一些高級內(nèi)容了。
符號化默認(rèn)值
提供給參數(shù)的默認(rèn)值不能是表達(dá)式。目前明確支持以下形式:
-
數(shù)值型常數(shù)(整數(shù)和浮點(diǎn)數(shù))。
-
字符串常量
-
True、False和None。 -
以模塊名開頭的簡單符號常量,如
sys.maxsize。
(未來可能需要加以細(xì)化,以便可以采用 CONSTANT - 1 之類的完整表達(dá)式。)
對 Argument Clinic 生成的 C 函數(shù)和變量進(jìn)行重命名
Argument Clinic 會自動(dòng)為其生成的函數(shù)命名。如果生成的名稱與現(xiàn)有的 C 函數(shù)沖突,這偶爾可能會造成問題,有一個(gè)簡單的解決方案:覆蓋 C 函數(shù)的名稱。只要在函數(shù)聲明中加入關(guān)鍵字 "as" ,然后再加上要使用的函數(shù)名。Argument Clinic 將以該函數(shù)名為基礎(chǔ)作為(生成的)函數(shù)名,然后在后面加上 "_impl",并用作實(shí)現(xiàn)函數(shù)的名稱。
例如,若對 pickle.Pickler.dump 生成的 C 函數(shù)進(jìn)行重命名,應(yīng)如下所示:
/*[clinic input]pickle.Pickler.dump as pickler_dumper...
原函數(shù)會被命名為 pickler_dumper(),而實(shí)現(xiàn)函數(shù)現(xiàn)在被命名為``pickler_dumper_impl()``。
同樣的問題依然會出現(xiàn):想給某個(gè)參數(shù)取個(gè) Python 用名,但在 C 語言中可能用不了。Argument Clinic 允許在 Python 和 C 中為同一個(gè)參數(shù)取不同的名字,依然是利用 "as" 語法:
/*[clinic input]pickle.Pickler.dumpobj: objectfile as file_obj: objectprotocol: 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ù),并將每個(gè)參數(shù)定義為 object??梢灾付?type 參數(shù),以便能轉(zhuǎn)換為合適的類型。所有參數(shù)都應(yīng)標(biāo)記為只認(rèn)位置(在最后一個(gè)參數(shù)后面加上 /)。
目前,所生成的代碼將會用到 PyArg_ParseTuple() ,但很快會做出改動(dòng)。
可選參數(shù)組
有些過時(shí)的函數(shù)用到了一種讓人頭疼的函數(shù)解析方式:計(jì)算位置參數(shù)的數(shù)量,據(jù)此用 switch 語句進(jìn)行各個(gè)不同的 PyArg_ParseTuple() 調(diào)用。(這些函數(shù)不能接受只認(rèn)關(guān)鍵字的參數(shù)。)在沒有 PyArg_ParseTupleAndKeywords() 之前,這種方式曾被用于模擬可選參數(shù)。
雖然這種函數(shù)通??梢赞D(zhuǎn)換為采用 PyArg_ParseTupleAndKeywords() 、可選參數(shù)和默認(rèn)值的方式,但并不是全都可以做到。這些過時(shí)函數(shù)中, PyArg_ParseTupleAndKeywords() 并不能直接支持某些功能。最明顯的例子是內(nèi)置函數(shù) range(),它的必需參數(shù)的 左 邊存在一個(gè)可選參數(shù)!另一個(gè)例子是 curses.window.addch(),它的兩個(gè)參數(shù)是一組,必須同時(shí)指定。(參數(shù)名為 x 和 y;如果調(diào)用函數(shù)時(shí)傳入了 x,則必須同時(shí)傳入``y``;如果未傳入 x ,則也不能傳入 y)。
不管怎么說,Argument Clinic 的目標(biāo)就是在不改變語義的情況下支持所有現(xiàn)有 CPython 內(nèi)置參數(shù)的解析。因此,Argument Clinic 采用所謂的 可選組 方案來支持這種解析方式??蛇x組是必須一起傳入的參數(shù)組。他們可以在必需參數(shù)的左邊或右邊,只能 用于只認(rèn)位置的參數(shù)。
備注
可選組 僅 適用于多次調(diào)用 PyArg_ParseTuple() 的函數(shù)!采用 任何 其他方式解析參數(shù)的函數(shù),應(yīng)該 幾乎不 采用可選組轉(zhuǎn)換為 Argument Clinic 解析。目前,采用可選組的函數(shù)在 Python 中無法獲得準(zhǔn)確的簽名,因?yàn)?Python 不能理解這個(gè)概念。請盡可能避免使用可選組。
若要定義可選組,可在要分組的參數(shù)前面加上 [,在這些參數(shù)后加上``]`` ,要在同一行上。舉個(gè)例子,下面是 curses.window.addch 采用可選組的用法,前兩個(gè)參數(shù)和最后一個(gè)參數(shù)可選:
/*[clinic input]curses.window.addch[x: intX-coordinate.y: intY-coordinate.]ch: objectCharacter to add.[attr: longAttributes for the character.]/...
注:
-
每一個(gè)可選組,都會額外傳入一個(gè)代表分組的參數(shù)。 參數(shù)為 int 型,名為
group_{direction}_{number},其中{direction}取決于此參數(shù)組位于必需參數(shù)right還是left,而{number}是一個(gè)遞增數(shù)字(從 1 開始),表示此參數(shù)組與必需參數(shù)之間的距離。 在調(diào)用函數(shù)時(shí),若未用到此參數(shù)組則此參數(shù)將設(shè)為零,若用到了參數(shù)組則該參數(shù)為非零。 所謂的用到或未用到,是指在本次調(diào)用中形參是否收到了實(shí)參。 -
如果不存在必需參數(shù),可選組的行為等同于出現(xiàn)在必需參數(shù)的右側(cè)。
-
在模棱兩可的情況下,參數(shù)解析代碼更傾向于參數(shù)左側(cè)(在必需參數(shù)之前)。
-
可選組只能包含只認(rèn)位置的參數(shù)。
-
可選組 僅限 用于過時(shí)代碼。請勿在新的代碼中使用可選組。
采用真正的 Argument Clinic 轉(zhuǎn)換器,而不是 “傳統(tǒng)轉(zhuǎn)換器”
為了節(jié)省時(shí)間,盡量減少要學(xué)習(xí)的內(nèi)容,實(shí)現(xiàn)第一次適用 Argument Clinic 的移植,上述練習(xí)簡述的是“傳統(tǒng)轉(zhuǎn)換器”的用法?!皞鹘y(tǒng)轉(zhuǎn)換器”只是一種簡便用法,目的就是更容易地讓現(xiàn)有代碼移植為適用于 Argument Clinic 。說白了,在移植 Python 3.4 的代碼時(shí),可以考慮采用。
不過從長遠(yuǎn)來看,可能希望所有代碼塊都采用真正的 Argument Clinic 轉(zhuǎn)換器語法。原因如下:
-
合適的轉(zhuǎn)換器可讀性更好,意圖也更清晰。
-
有些格式單元是“傳統(tǒng)轉(zhuǎn)換器”無法支持的,因?yàn)檫@些格式需要帶上參數(shù),而傳統(tǒng)轉(zhuǎn)換器的語法不支持指定參數(shù)。
-
后續(xù)可能會有新版的參數(shù)解析庫,提供超過 PyArg_ParseTuple() 支持的功能;而這種靈活性將無法適用于傳統(tǒng)轉(zhuǎn)換器轉(zhuǎn)換的參數(shù)。
因此,若是不介意多花一點(diǎn)精力,請使用正常的轉(zhuǎn)換器,而不是傳統(tǒng)轉(zhuǎn)換器。
簡而言之,Argument Clinic(非傳統(tǒng))轉(zhuǎn)換器的語法看起來像是 Python 函數(shù)調(diào)用。但如果函數(shù)沒有明確的參數(shù)(所有函數(shù)都取默認(rèn)值),則可以省略括號。因此 bool 和 bool() 是完全相同的轉(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ù)的注解值。目前尚不支持,因?yàn)?PEP 8 規(guī)定 Python 庫不得使用注解。
此外,某些轉(zhuǎn)換器還可接受額外的參數(shù)。下面列出了這些額外參數(shù)及其含義:
accept一些 Python 類型的集合(可能還有偽類型);用于限制只接受這些類型的 Python 參數(shù)。(并非通用特性;只支持傳統(tǒng)轉(zhuǎn)換器列表中給出的類型)。
若要能接受
None,請?jiān)诩现刑砑?NoneType。
bitwise僅用于無符號整數(shù)。寫入形參的將是 Python 實(shí)參的原生整數(shù)值,不做任何越界檢查,即便是負(fù)值也一樣。
converter僅用于
object轉(zhuǎn)換器。為某個(gè) C 轉(zhuǎn)換函數(shù) 指定名稱,用于將對象轉(zhuǎn)換為原生類型。
encoding僅用于字符串。指定將 Python str(Unicode) 轉(zhuǎn)換為 C 語言的
char *時(shí)應(yīng)該采用的編碼。
subclass_of僅用于
object轉(zhuǎn)換器。要求 Python 值是 Python 類型的子類,用 C 語言表示。
type僅用于
object和self轉(zhuǎn)換器。指定用于聲明變量的 C 類型。 默認(rèn)值是"PyObject *"。
zeroes僅用于字符串。如果為 True,則允許在值中嵌入 NUL 字節(jié)(
'\\0')。字符串的長度將通過名為的參數(shù)傳入,跟在字符串參數(shù)的后面。_length
請注意,并不是所有參數(shù)的組合都能正常生效。通常這些參數(shù)是由相應(yīng)的 PyArg_ParseTuple 格式單元 實(shí)現(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)該換成的文本。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
舉個(gè)例子,下面是采用合適的轉(zhuǎn)換器的例子 pickle.Pickler.dump:
/*[clinic input]pickle.Pickler.dumpobj: objectThe object to be pickled./Write a pickled representation of obj to the open file.[clinic start generated code]*/
真正的轉(zhuǎn)換器有一個(gè)優(yōu)點(diǎn),就是比傳統(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)換器就做不到這一點(diǎn)。
Argument Clinic 會列明其全部轉(zhuǎn)換器。每個(gè)轉(zhuǎn)換器都會給出可接受的全部參數(shù),以及每個(gè)參數(shù)的默認(rèn)值。只要運(yùn)行 Tools/clinic/clinic.py --converters 就能得到完整的列表。
Py_buffer
在使用 Py_buffer 轉(zhuǎn)換器(或者 's*'、'w*'、'*y' 或 'z*' 傳統(tǒng)轉(zhuǎn)換器)時(shí),不可 在所提供的緩沖區(qū)上調(diào)用 PyBuffer_Release()。 Argument Clinic 生成的代碼會自動(dòng)完成此操作(在解析函數(shù)中)。
高級轉(zhuǎn)換器
還記得編寫第一個(gè)函數(shù)時(shí)跳過的那些格式單元嗎,因?yàn)樗麄兪歉呒墐?nèi)容?下面就來介紹這些內(nèi)容。
其實(shí)訣竅在于,這些格式單元都需要給出參數(shù)——要么是轉(zhuǎn)換函數(shù),要么是類型,要么是指定編碼的字符串。(但 “傳統(tǒng)轉(zhuǎn)換器”不支持參數(shù)。這就是為什么第一個(gè)函數(shù)要跳過這些內(nèi)容)。為格式單元指定的參數(shù)于是就成了轉(zhuǎn)換器的參數(shù);參數(shù)可以是 converter``(對于 ``O&)、subclass_of``(對于 ``O!),或者是 encoding (對于 e 開頭的格式單元)。
在使用 subclass_of 時(shí),可能還需要用到 object() 的另一個(gè)自定義參數(shù):type,用于設(shè)置參數(shù)的實(shí)際類型。例如,為了確保對象是 PyUnicode_Type 的子類,可能想采用轉(zhuǎn)換器 object(type='PyUnicodeObject *', subclass_of='&PyUnicode_Type')。
Argument Clinic 用起來可能存在一個(gè)問題:喪失了 e 開頭的格式單位的一些靈活性。在手工編寫 PyArg_Parse 調(diào)用時(shí),理論上可以在運(yùn)行時(shí)決定傳給 PyArg_ParseTuple() 的編碼字符串。但現(xiàn)在這個(gè)字符串必須在 Argument-Clinic 預(yù)處理時(shí)進(jìn)行硬編碼。這個(gè)限制是故意設(shè)置的;以便簡化對這種格式單元的支持,并允許以后進(jìn)行優(yōu)化。這個(gè)限制似乎并不合理;CPython 本身總是為 e 開頭的格式單位參數(shù)傳入靜態(tài)的硬編碼字符串。
參數(shù)的默認(rèn)值
參數(shù)的默認(rèn)值可以是多個(gè)值中的一個(gè)。最簡單的可以是字符串、int 或 float 字面量。
foo: str = "abc"bar: int = 123bat: float = 45.6
還可以使用 Python 的任何內(nèi)置常量。
yep: bool = Truenope: bool = Falsenada: object = None
對默認(rèn)值 NULL 和簡單表達(dá)式還提供特別的支持,下面將一一介紹。
默認(rèn)值 NULL
對于字符串和對象參數(shù)而言,可以設(shè)為 None,表示沒有默認(rèn)值。但這意味著會將 C 變量初始化為 Py_None。為了方便起見,提供了一個(gè)特殊值``NULL``,目的就是為了讓 Python 認(rèn)為默認(rèn)值就是 None,而 C 變量則會初始化為 NULL。
設(shè)為默認(rèn)值的表達(dá)式
參數(shù)的默認(rèn)值不僅可以是字面量。還可以是一個(gè)完整的表達(dá)式,可采用數(shù)學(xué)運(yùn)算符及對象的屬性。但這種支持并沒有那么簡單,因?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í)候在請求函數(shù)簽名時(shí)會被求值。
在對表達(dá)式進(jìn)行求值時(shí),可以使用什么命名空間呢?求值過程運(yùn)行于內(nèi)置模塊的上下文中。 因此,如果模塊帶有名為 max_widgets 的屬性,直接引用即可。
foo: Py_ssize_t = max_widgets
如果表達(dá)式不在當(dāng)前模塊中,就會去 sys.modules 查找。比如 sys.maxsize 就是如此找到的。(因?yàn)槭孪炔恢烙脩魰虞d哪些模塊到解釋器中,所以最好只用到 Python 會預(yù)加載的模塊。)
僅當(dāng)運(yùn)行時(shí)才對缺省值求值,意味著 Argument Clinic 無法計(jì)算出正確的 C 缺省值。所以需顯式給出。在使用表達(dá)式時(shí),必須同時(shí)用轉(zhuǎn)換器的``c_default`` 參數(shù)指定 C 語言中的等價(jià)表達(dá)式。
foo: Py_ssize_t(c_default="PY_SSIZE_T_MAX - 1") = sys.maxsize - 1
還有一個(gè)問題也比較復(fù)雜。Argument Clinic 無法事先知道表達(dá)式是否有效。 解析只能保證看起來是有效值,但無法 實(shí)際 知曉。在用表達(dá)式時(shí)須十分小心,確保在運(yùn)行時(shí)能得到有效值。
最后一點(diǎn),由于表達(dá)式必須能表示為靜態(tài)的 C 語言值,所以存在許多限制。 以下列出了不得使用的 Python 特性:
-
功能
-
行內(nèi) if 語句(
3 if foo else 5) -
序列類自動(dòng)解包(
*[1, 2, 3]) -
列表、集合、字典的解析和生成器表達(dá)式。
-
元組、列表、集合、字典的字面量
返回值轉(zhuǎn)換器
Argument Clinic 生成的植入函數(shù)默認(rèn)會返回 PyObject *。但是通常 C 函數(shù)的任務(wù)是要對某些 C 類型進(jìn)行計(jì)算,然后將其轉(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ù)同時(shí)用到了 "as" 和返回值轉(zhuǎn)換器, "as" 應(yīng)位于返回值轉(zhuǎn)換器之前。)
返回值轉(zhuǎn)換器還存在一個(gè)復(fù)雜的問題:出錯(cuò)信息如何表示?通常函數(shù)在執(zhí)行成功時(shí)會返回一個(gè)有效(非 NULL)指針,失敗則返回 NULL。但如果使用了整數(shù)的返回值轉(zhuǎn)換器,所有整數(shù)都是有效值。Argument Clinic 怎么檢測錯(cuò)誤呢?解決方案是:返回值轉(zhuǎn)換器會隱含尋找一個(gè)代表錯(cuò)誤的特殊值。如果返回該特殊值,且設(shè)置了出錯(cuò)標(biāo)記( PyErr_Occurred() 返回 True),那么生成的代碼會傳遞該錯(cuò)誤。否則,會對返回值進(jìn)行正常編碼。
目前 Argument Clinic 只支持少數(shù)幾種返回值轉(zhuǎn)換器。
boolintunsigned intlongunsigned intsize_tPy_ssize_tfloatdoubleDecodeFSDefault
這些轉(zhuǎn)換器都不需要參數(shù)。前3個(gè)轉(zhuǎn)換器如果返回 -1 則表示出錯(cuò)。DecodeFSDefault 的返回值類型是 const char *;若返回 NULL 指針則表示出錯(cuò)。
(還有一個(gè) NoneType 轉(zhuǎn)換器是實(shí)驗(yàn)性質(zhì)的,成功時(shí)返回 Py_None ,失敗則返回 NULL,且不會增加 Py_None 的引用計(jì)數(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ù)制的是文檔字符串;這樣就能指定一個(gè)新的文檔串。
下面是函數(shù)的克隆方法:
/*[clinic input]module.class.new_function [as c_basename] = module.class.existing_functionDocstring for new_function goes here.[clinic start generated code]*/
(原函數(shù)可以位于不同的模塊或類中。示例中的 module.class 只是為了說明,兩個(gè) 函數(shù)都必須使用全路徑)。
Sorry, there’s no syntax for partially cloning a function, or cloning a function then modifying it. Cloning is an all-or nothing proposition.
另外,要克隆的函數(shù)必須在當(dāng)前文件中已有定義。
調(diào)用 Python 代碼
下面的高級內(nèi)容需要編寫 Python 代碼,存于 C 文件中,并修改 Argument Clinic 的運(yùn)行狀態(tài)。其實(shí)很簡單:只需定義一個(gè) Python 塊。
Python 塊的分隔線與 Argument Clinic 函數(shù)塊不同。如下所示:
/*[python input]# python code goes here[python start generated code]*/
Python 塊內(nèi)的所有代碼都會在解析時(shí)執(zhí)行。塊內(nèi)寫入 stdout 的所有文本都被重定向到塊后的“輸出”部分。
以下例子包含了 Python 塊,用于在 C 代碼中添加一個(gè)靜態(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 用一個(gè)默認(rèn)的轉(zhuǎn)換器自動(dòng)添加一個(gè)“self”參數(shù)。自動(dòng)將 self 參數(shù)的 type 設(shè)為聲明類型時(shí)指定的“指向?qū)嵗闹羔槨?。不過 Argument Clinic 的轉(zhuǎn)換器可被覆蓋,也即自己指定一個(gè)轉(zhuǎn)換器。只要將自己的 self 參數(shù)作為塊的第一個(gè)參數(shù)即可,并確保其轉(zhuǎn)換器是 self_converter 的實(shí)例或其子類。
這有什么用呢?可用于覆蓋 self 的類型,或?yàn)槠浣o個(gè)不同的默認(rèn)名稱。
如何指定 self 對應(yīng)的自定義類型呢?如果只有 self 類型相同的一兩個(gè)函數(shù),可以直接使用 Argument Clinic 現(xiàn)有的 self 轉(zhuǎn)換器,把要用的類型作為 type 參數(shù)傳入:
/*[clinic input]_pickle.Pickler.dumpself: 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.dumpself: PicklerObjectobj: object/Write a pickled representation of the given object to the open file.[clinic start generated code]*/
“定義類”轉(zhuǎn)換器
Argument Clinic 為訪問方法定義所在的類提供了便利。因?yàn)?heap type 方法需要獲取模塊級的運(yùn)行狀態(tài),所以就十分有用。PyType_FromModuleAndSpec() 會將堆類型與模塊關(guān)聯(lián)起來。然后類就可用 PyType_GetModuleState() 獲取模塊狀態(tài)了,比如利用模塊的方法進(jìn)行獲取。
示例來自 Modules/zlibmodule.c。首先,在 clinic 的輸入塊添加 defining_class :
/*[clinic input]zlib.Compress.compresscls: defining_classdata: Py_bufferBinary 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);
每個(gè)方法只能有一個(gè)參數(shù)用到轉(zhuǎn)換器,且須位于 self 之后 ,若未用到 self 則為第一個(gè)參數(shù)。該參數(shù)的類型為 PyTypeObject *。__text_signature__ 中不會包含該參數(shù)。
defining_class 轉(zhuǎn)換器與 __init__ 及 __new__ 方法不兼容,他們不能使用 METH_METHOD 。
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 intlocal_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。
編寫自定義轉(zhuǎn)換器
上一節(jié)中已有提及……可以編寫自己的轉(zhuǎn)換器!轉(zhuǎn)換器就是一個(gè)繼承自``CConverter`` 的 Python 類。假如有個(gè)參數(shù)采用了 O& 格式,對此參數(shù)進(jìn)行解析就會去調(diào)用某個(gè)“轉(zhuǎn)換器函數(shù)” PyArg_ParseTuple() ,也就會用到自定義轉(zhuǎn)換器。
自定義轉(zhuǎn)換器類應(yīng)命名為 *something*_converter。只要按此規(guī)則命名,自定義轉(zhuǎn)換器類就會在 Argument Clinic 中自動(dòng)注冊;轉(zhuǎn)換器的名稱就是去除了 _converter 后綴的類名。(通過元類完成)。
不得由 CConverter.__init__ 派生子類。而應(yīng)編寫一個(gè) converter_init() 函數(shù)。converter_init() 必須能接受一個(gè) self 參數(shù);所有后續(xù)的其他參數(shù) 必須 是只認(rèn)關(guān)鍵字的參數(shù)。傳給 Argument Clinic 轉(zhuǎn)換器的所有參數(shù)都會傳入自定義 converter_init() 函數(shù)。
CConverter 的其他一些成員,可能需要在自定義子類中定義。下面列出了目前的成員:
type
變量要采用的 C 語言數(shù)據(jù)類型。type 應(yīng)為 int 之類的 Python 字符串,用于指定變量的類型。若為指針類型,則字符串應(yīng)以 ' *' 結(jié)尾。
default
該參數(shù)的缺省值,為 Python 數(shù)據(jù)類型。若無缺省值,則為 unspecified。
網(wǎng)站題目:創(chuàng)新互聯(lián)Python教程:ArgumentClinic的用法
標(biāo)題來源:http://www.dlmjj.cn/article/djoeshp.html


咨詢
建站咨詢
