日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > python >内容正文

python

Python 内部:可调用对象是如何工作的

發(fā)布時(shí)間:2025/3/21 python 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Python 内部:可调用对象是如何工作的 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

【這篇文章所描述的 Python 版本是 3.x,更確切地說,是 CPython 3.3 alpha。】

在 Python 中,可調(diào)用對(duì)象 (callable) 的概念是十分基本的。當(dāng)我們說什么東西是“可調(diào)用的”,馬上可以聯(lián)想到的顯而易見的答案便是函數(shù)。無論是用戶定義的函數(shù) (你所編寫的) 還是內(nèi)置的函數(shù) (經(jīng)常是在 CPython 解析器內(nèi)由 C 實(shí)現(xiàn)的),他們總是用來被調(diào)用的,不是么?

當(dāng)然,還有方法也可以調(diào)用,但他們僅僅是被限制在對(duì)象中的特殊函數(shù)而已,沒什么有趣的地方。還有什么可以被調(diào)用呢?你可能知道,也可能不知道,只要一個(gè)對(duì)象所屬的類定義了?__call__?魔術(shù)方法,它也是可以被調(diào)用的。所以對(duì)象可以像函數(shù)那樣使用。再深入思考一點(diǎn),類也是可以被調(diào)用的。終究,我們是這樣創(chuàng)建新的對(duì)象的:

class Joe:... [contents of class]joe = Joe()

在這里,我們“調(diào)用”了?Joe?來創(chuàng)建新的實(shí)例。所以說類也可以像函數(shù)那樣使用!

可以證明,所有這些概念都很漂亮地在 CPython 被實(shí)現(xiàn)。在 Python 中,一切皆對(duì)象,包括我們?cè)谇懊娴亩温渲刑岬降拿恳粋€(gè)東西 (用戶定義和內(nèi)置函數(shù)、方法、對(duì)象、類)。所有這些調(diào)用都是由一個(gè)單一的機(jī)制來完成的。這一機(jī)制十分優(yōu)雅,并且一點(diǎn)都不難理解,所以這很值得我們?nèi)チ私狻2贿^首先我們從頭開始。

編譯調(diào)用

CPython 經(jīng)過兩個(gè)主要的步驟來執(zhí)行我們的程序:

  • Python 源代碼被編譯為字節(jié)碼。
  • 一個(gè)虛擬機(jī)使用一系列的內(nèi)置對(duì)象和模塊來執(zhí)行這些字節(jié)碼。
  • 在這一節(jié)中,我會(huì)粗略地概括一下第一步中如何處理一個(gè)調(diào)用。我不會(huì)深入這些細(xì)節(jié),而且他們也不是我想在這篇文章中關(guān)注的真正有趣的部分。如果你想了解更多 Python 代碼在編譯器中經(jīng)歷的流程,可以閱讀?這篇文章?。

    簡(jiǎn)單地來說,Python 編譯器將表達(dá)式中的所有類似?(參數(shù)?…)?的結(jié)構(gòu)都識(shí)別為一個(gè)調(diào)用?[1]?。這個(gè)操作的 AST 節(jié)點(diǎn)叫?Call?,編譯器通過Python/compile.c?文件中的?compiler_call?函數(shù)來生成?Call?對(duì)應(yīng)的代碼。在大多數(shù)情況下會(huì)生成?CALL_FUNCTION?字節(jié)碼指令。它也有一些變種,例如含有“星號(hào)參數(shù)”——形如?func(a,?b,?*args)?,有一個(gè)專門的指令?CALL_FUNCTION_VAR?,但這些都不是我們文章所關(guān)注的,所以就忽略掉好了,它們僅僅是這個(gè)主題的一些小變種而已。

    CALL_FUNCTION

    于是?CALL_FUNCTION?就是我們這兒所關(guān)注的指令。這是?它做了什么?:

    CALL_FUNCTION(argc)

    調(diào)用一個(gè)函數(shù)。?argc?的低字節(jié)描述了定位參數(shù) (positional parameters) 的數(shù)量,高字節(jié)則是關(guān)鍵字參數(shù) (keyword parameters) 的數(shù)量。在棧中,操作碼首先找到關(guān)鍵字參數(shù)。對(duì)于每個(gè)關(guān)鍵字參數(shù),值在鍵的上面。而定位參數(shù)則在關(guān)鍵詞參數(shù)的下面,其中最右邊的參數(shù)在最上面。在所有參數(shù)下面,是要被調(diào)用的函數(shù)對(duì)象。將所有的函數(shù)參數(shù)和函數(shù)本身出棧,并將返回值壓入棧。

    CPython 的字節(jié)碼由?Python/ceval.c?文件的一個(gè)巨大的函數(shù)?PyEval_EvalFrameEx?來執(zhí)行。這個(gè)函數(shù)十分恐怖,不過也僅僅是一個(gè)特別的操作碼分發(fā)器而已。他從指定幀的代碼對(duì)象中讀取指令并執(zhí)行它們。例如說這里是?CALL_FUNCTION?的處理器 (進(jìn)行了一些清理,移除了跟蹤和計(jì)時(shí)的宏):

    TARGET(CALL_FUNCTION) {PyObject **sp;sp = stack_pointer;x = call_function(&sp, oparg);stack_pointer = sp;PUSH(x);if (x != NULL)DISPATCH();break; }

    并不是很難——事實(shí)上它十分容易看懂。?call_function?根本沒有真正進(jìn)行調(diào)用 (我們將在之后細(xì)究這件事),?oparg?是指令的數(shù)字參數(shù),stack_pointer?則指向棧頂?[2]?。?call_function?返回的值被壓入棧中,?DISPATCH?僅僅是調(diào)用下一條指令的宏。

    call_function?也在?Python/ceval.c?文件。它真正實(shí)現(xiàn)了這條指令的功能。它雖然不算很長(zhǎng),但80行也已經(jīng)長(zhǎng)到我不可能把它完全貼在這兒了。我將會(huì)從總體上解釋這個(gè)流程,并貼一些相關(guān)的小代碼片段取而代之。你完全可以在你最喜歡的編輯器中打開這些代碼。

    所有的調(diào)用僅僅是對(duì)象調(diào)用

    要理解調(diào)用過程在 Python 中是如何進(jìn)行的,最重要的第一步是忽略?call_function?所做的大多數(shù)事情。是的,我就是這個(gè)意思。這個(gè)函數(shù)最最主要的代碼都是為了對(duì)各種情況進(jìn)行優(yōu)化。完全移除這些對(duì)解析器的正確性毫無影響,影響的僅僅是它的性能。如果我們忽略所有的時(shí)間優(yōu)化,?call_function?所做的僅僅是從單參數(shù)的?CALL_FUNCTION?指令中解碼參數(shù)和關(guān)鍵詞參數(shù)的數(shù)量,并且將它們轉(zhuǎn)給?do_call?。我們將在后面重新回到這些優(yōu)化因?yàn)樗麄兒苡幸馑?#xff0c;不過現(xiàn)在先讓我們看看核心的流程。

    do_call?從棧中將參數(shù)加載到?PyObject?對(duì)象中 (定位參數(shù)存入一個(gè)元組,關(guān)鍵詞對(duì)象存入一個(gè)字典),做一些跟綜和優(yōu)化,最后調(diào)用?PyObject_Call?。

    PyObject_Call?是一個(gè)極其重要的函數(shù)。它可以在 Python 的 C API 中被擴(kuò)展。這就是它完整的代碼:

    PyObject * PyObject_Call(PyObject *func, PyObject *arg, PyObject *kw) {ternaryfunc call;if ((call = func->ob_type->tp_call) != NULL) {PyObject *result;if (Py_EnterRecursiveCall(" while calling a Python object"))return NULL;result = (*call)(func, arg, kw);Py_LeaveRecursiveCall();if (result == NULL && !PyErr_Occurred())PyErr_SetString(PyExc_SystemError,"NULL result without error in PyObject_Call");return result;}PyErr_Format(PyExc_TypeError, "'%.200s' object is not callable",func->ob_type->tp_name);return NULL; }

    拋開深遞歸保護(hù)和錯(cuò)誤處理?[3]?,?PyObject_Call?提取出對(duì)象的?tp_call?屬性并且調(diào)用它?[4]?,?tp_call?是一個(gè)函數(shù)指針,因此我們可以這樣做。

    先讓它這樣一會(huì)兒。忽略所有那些精彩的優(yōu)化,?Python 中的所有調(diào)用?都可以濃縮為下面這些內(nèi)容:

    • Python 中一切皆對(duì)象?[5]?。
    • 所有對(duì)象都有類型,對(duì)象的類型規(guī)定了對(duì)象可以做和被做的事情。
    • 當(dāng)一個(gè)對(duì)象是可被調(diào)用的,它的類型的?tp_call?將被調(diào)用。

    作為一個(gè) Python 用戶,你唯一需要直接與?tp_call?進(jìn)行的交互是在你希望你的對(duì)象可以被調(diào)用的時(shí)候。當(dāng)你在 Python 中定義你的類時(shí),你需要實(shí)現(xiàn)__call__?方法來達(dá)到這一目的。這個(gè)方法被 CPython 直接映射到了?tp_call?上。如果你在 C 擴(kuò)展中定義你的類,你需要自己手動(dòng)給類對(duì)象的?tp_call屬性賦值。

    我們回想起類本身也可以被“調(diào)用”以創(chuàng)建新的對(duì)象,所以?tp_call?也在這里起到了作用。甚至更加基本地,當(dāng)你定義一個(gè)類時(shí)也會(huì)產(chǎn)生一次調(diào)用——在類的元類中。這是一個(gè)有意思的話題,我將會(huì)在未來的文章中討論它。

    附加:CALL_FUNCTION 里的優(yōu)化

    文章的主要部分在前面那個(gè)小節(jié)已經(jīng)講完了,所以這一部分是選讀的。之前說過,我覺得這些內(nèi)容很有意思,它展示了一些你可能并不認(rèn)為是對(duì)象但事實(shí)上卻是對(duì)象的東西。

    我之前提到過,我們對(duì)于所有的?CALL_FUNCTION?僅僅需要使用?PyObject_Call?就可以處理。事實(shí)上,對(duì)一些常見的情況做一些優(yōu)化是很有意義的,對(duì)這些情況來說,前面的方法可能過于麻煩了。?PyObject_Call?是一個(gè)非常通用的函數(shù),它需要將所有的參數(shù)放入專門的元組和字典對(duì)象中 (按順序?qū)?yīng)于定位參數(shù)和關(guān)鍵詞參數(shù))。?PyObject_Call?需要它的調(diào)用者為它從棧中取出所有這些參數(shù),并且存放好。然而在一些常見的情況中,我們可以避免很多這樣的開銷,這正是?call_function?中優(yōu)化的所在。

    在?call_function?中的第一個(gè)特殊情況是:

    /* Always dispatch PyCFunction first, because these are presumed to be the most frequent callable object. */ if (PyCFunction_Check(func) && nk == 0) {

    這處理了?builtin_function_or_method?類型的對(duì)象 (在 C 實(shí)現(xiàn)中表現(xiàn)為 PyCFunction 類型)。正如上面的注釋所說的,Python 里有很多這樣的函數(shù)。所有使用 C 實(shí)現(xiàn)的函數(shù),無論是 CPython 解析器自帶的還是 C 擴(kuò)展里的,都會(huì)進(jìn)入這一類。例如說:

    >>> type(chr) <class 'builtin_function_or_method'> >>> type("".split) <class 'builtin_function_or_method'> >>> from pickle import dump >>> type(dump) <class 'builtin_function_or_method'>

    這里的?if?還有一個(gè)附加條件——傳入函數(shù)的關(guān)鍵詞參數(shù)數(shù)量為0。如果這個(gè)函數(shù)不接受任何參數(shù) (在函數(shù)創(chuàng)建時(shí)以?METH_NOARGS?標(biāo)志標(biāo)明) 或僅僅一個(gè)對(duì)象參數(shù) (METH_0?標(biāo)志),?call_function?就不需要通過正常的參數(shù)打包流程而可以直接調(diào)用函數(shù)指針。為了搞清楚這是如何實(shí)現(xiàn)的,我高度推薦你讀一讀?文檔這個(gè)部分?關(guān)于?PyCFunction?和?METH_?標(biāo)志的介紹。

    下面,還有一個(gè)對(duì) Python 寫的類方法的特殊處理:

    else {if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {

    PyMethod?是一個(gè)用于表示?有界方法?(bound methods) 的內(nèi)部對(duì)象。方法的特殊之處在于它還帶有一個(gè)所在對(duì)象的引用。?call_function?提取這個(gè)對(duì)象并且將他放入棧中作為下一步的準(zhǔn)備工作。

    這是調(diào)用部分的代碼剩下的部分 (在這之后在?call_object?中只有一些清理?xiàng)5拇a):

    if (PyFunction_Check(func))x = fast_function(func, pp_stack, n, na, nk); elsex = do_call(func, pp_stack, na, nk);

    我們已經(jīng)見過?do_call?了——它實(shí)現(xiàn)了調(diào)用的最通用形式。然而,這里還有一個(gè)優(yōu)化——如果?func?是一個(gè)?PyFunction?對(duì)象 (一個(gè)在?內(nèi)部?用于表示使用 Python 代碼定義的函數(shù)的對(duì)象),程序選擇了另一條路徑——?fast_function?。

    為了理解?fast_function?做了什么,最重要的是首先要考慮在執(zhí)行一個(gè) Python 函數(shù)時(shí)發(fā)生了什么。簡(jiǎn)單地說,它的代碼對(duì)象被執(zhí)行 (也就是PyEval_EvalCodeEx?本身)。這些代碼期望它的參數(shù)已經(jīng)在棧中,因此在大多數(shù)情況下,沒必要將參數(shù)打包到容器中再重新釋放出來。稍稍注意一下,就可以將參數(shù)留在棧中,這樣許多寶貴的 CPU 周期就可以被節(jié)省出來。

    剩下的一切最終落回到?do_call?上,順便,包括含有關(guān)鍵詞參數(shù)的 PyCFunction 對(duì)象。一個(gè)不尋常的事實(shí)是,對(duì)于那些既接受關(guān)鍵詞參數(shù)又接受定位參數(shù)的 C 函數(shù),不給它們傳遞關(guān)鍵詞參數(shù)要稍稍更高效一些。例如說?[6]?:

    $ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"' 's.split(";")' 1000000 loops, best of 3: 0.3 usec per loop $ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"' 's.split(sep=";")' 1000000 loops, best of 3: 0.469 usec per loop

    這是一個(gè)巨大的差異,但輸入數(shù)據(jù)很小。對(duì)于更大的字符串,這個(gè)差異就幾乎沒有了:

    $ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"*1000' 's.split(";")' 10000 loops, best of 3: 98.4 usec per loop $ ~/test/python_src/33/python -m timeit -s's="a;b;c;d;e"*1000' 's.split(sep=";")' 10000 loops, best of 3: 98.7 usec per loop

    總結(jié)?

    這篇文章的目的是討論在 Python 中,可調(diào)用對(duì)象意味著什么,并且從盡可能最底層的概念——CPython 虛擬機(jī)中的實(shí)現(xiàn)細(xì)節(jié)——來接近它。就我個(gè)人來說,我覺得這個(gè)實(shí)現(xiàn)非常優(yōu)雅,因?yàn)樗鼘⒉煌母拍罱y(tǒng)一到了同一個(gè)東西上。在附加部分里我們看到,在 Python 中有些我們常常認(rèn)為不是對(duì)象的東西如函數(shù)和方法,實(shí)際上也是對(duì)象,并且也可以以相同的統(tǒng)一的方法來處理。我保證了,在以后的文章中我將會(huì)深入?tp_call?創(chuàng)建新的 Python 對(duì)象和類的內(nèi)容。


    [1]這是故意的簡(jiǎn)化——?()?同樣可以用作其他用途如類定義 (用以列舉基類)、函數(shù)定義 (列舉參數(shù))、修飾器等等,但它們并不在表達(dá)式中。我同樣也故意忽略了生成器表達(dá)式。
    [2]CPython 虛擬機(jī)是一個(gè)?棧機(jī)器?。
    [3]在 C 代碼可能結(jié)束調(diào)用 Python 代碼的地方需要使用?Py_EnterRecursiveCall?來讓 CPython 保持對(duì)遞歸層級(jí)的跟蹤,并在遞歸過深時(shí)跳出。注意,用 C 寫的函數(shù)并不需要遵守這個(gè)遞歸限制。這也是為什么?do_call?的特殊情況?PyCFunction?先于調(diào)用?PyObject_Call?。
    [4]這里的“屬性”我表示的是一個(gè)結(jié)構(gòu)體的字段。如果你對(duì)于 Python C 擴(kuò)展的定義方式完全不熟悉,可以看看?這個(gè)頁面?。
    [5]當(dāng)我說?一切?皆對(duì)象時(shí),我的意思就是它。你也許會(huì)覺得對(duì)象是你定義的類的實(shí)例。然而,深入到 C 一級(jí),CPython 如你一樣創(chuàng)建和耍弄許許多多的對(duì)象。類型 (類)、內(nèi)置對(duì)象、函數(shù)、模塊,所有這些都表現(xiàn)為對(duì)象。
    [6]這個(gè)例子只能在 Python 3.3 中運(yùn)行,因?yàn)?split?的?sep?這個(gè)關(guān)鍵詞參數(shù)是在這個(gè)版本中新加的。在之前版本的 Python 中?split?僅僅接受定位參數(shù)。

    from:?http://pycoders-weekly-chinese.readthedocs.io/en/latest/issue6/python-internals-how-callables-work.html

    總結(jié)

    以上是生活随笔為你收集整理的Python 内部:可调用对象是如何工作的的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。