利用一個簡單的例子窺探CPython內(nèi)核的運行機制
我最近花了一些時間在探索CPython,并且我想要在這里分享我的一些冒險經(jīng)歷。Allison Kaptur的excellent guide to getting started with Python internals 有一點啰嗦,我想逐步介紹我自己的探索過程會更加有條理性,這樣也許其他好奇的Python使用者可以跟著一起做。
1.注意到了一些奇怪的事情
一開始,我只是設置好Nose對一些我寫的Python 3代碼進行測試。當我運行這些測試的時候,我得到了一個不可思議的錯誤信息:”TypeError: bad argument type for built-in operation”,這是我之前在這個程序里沒有見到過的。
最終造成這個錯誤的原因有一點顯而易見——我不小心在程序里留了一個PDB斷點(`import pdb; pdb.set_trace()`)。當我把它去掉后,測試正常運行了。
但是,我曾經(jīng)使用Nose在Python 2 repos上進行測試,并且在那種情況下,錯誤留下的斷點并不會導致Nose崩潰,而是看上去像是“掛起”了。程序并不是真的掛起了——它僅僅是不顯示東西到stdout(標準輸出)了。Nose是故意這樣做的,而當我正在運行一套測試的時候這樣做是有意義的。我可能僅僅是想看測試的結果,而不是一大堆程序自己打印出來的狀態(tài)。如果你在這個腳本里面敲擊“c”,Nose僅僅像通常那樣經(jīng)過這個斷點。
通常情況下,我可能只是聳聳肩,移除掉這個斷點,然后繼續(xù)我的工作。但是!我在一個黑客學校并且有時間深入研究任何抓住我興趣的東西,所以我決定利用這次機會去窺探一下Python的內(nèi)核。
2.制造一個最簡單的測試樣例。
結果這次的問題研究起來有一點復雜——我并不能確定問題是在Nose,還是PDB或者CPython自己的代碼里面。并且,我當然不能使用任何斷點,因為這些斷點會導致我的程序崩潰。
最終,在驗證了一些假設后,看上去PDB對`input()`的調(diào)用導致了崩潰。所以:在Python2和Python3里面,input的實現(xiàn)有什么不同嗎?或者是其他的某些東西不同?
我和Jesse一起進行調(diào)試,最后我們意識到Nose以一種有趣的方式處理標準輸出:
self._buf = StringIO() sys.stdout = self._buf
這里用sys.stout表示Python中的所有標準輸出,即表示所有向終端輸出的內(nèi)容都會發(fā)送到這里。但由于我們可以像訪問其他Python變量那樣訪問sys.stout,所以我們可以改變這個sys.stout。而Nose將sys.stoud設置為StringIO(),而這只是任意一個字符串。
如果你這么做,print函數(shù)就不會工作了!
import sys, io sys.stdout = io.StringIO() print(“Hello”) # Oh no, nothing printed!
我們懷疑是否那一行就是問題所在,所以我們構造了一個簡單的測試樣例:
import sys, io sys.stdout = io.StringIO() print("Hello!") # Nothing will appear input("Input: ") # Raises a TypeError
在Python 3 里面運行這個會出現(xiàn)我們之前看到過的”bad argument for built-in operation”。所以現(xiàn)在我們知道該調(diào)查哪里了!當你試圖改變sys.stdout的時候,內(nèi)建函數(shù)`input()`以一種奇怪的方式中斷下來。
3.了解一點CPython!
所以我們想要看下‘input'是怎樣實現(xiàn)的。Python有一個非??岬哪K叫做'inspect',能讓你檢查源代碼,像這樣:
>>> from collections import namedtuple >>> import inspect; print(inspect.getsource(namedtuple)) def namedtuple(typename, field_names, verbose=False, rename=False): """Returns a new subclass of tuple with named fields. .....
然而當你想要對'input'調(diào)用'inspect.getsource'的時候,結果會是:“TypeError: is not a module, class, method, function, traceback, frame, or code object.”這意味著我們的函數(shù)不是用Python實現(xiàn)的——它是用C語言實現(xiàn)的,因此'inspect;模塊不能為我們顯示它的代碼。
……但是,利用cinspect模塊的魔力,我們能查看C源代碼!
>>> import cinspect; print(cinspect.getsource(input)) static PyObject * builtin_input(PyObject *self, PyObject *args) { PyObject *line; char *str; .....
很好,現(xiàn)在我們知道我們想要找的函數(shù)叫做'builtin_input'。這時,我們將要開始瀏覽C代碼了,而不僅僅是Python代碼,我們將要在中端調(diào)試而不是在Python的解釋器里。你不需要一定是一個C語言專家才能看明白接下來的東西——我大多數(shù)時候會以根據(jù)函數(shù)名稱進行推測的方式進行。
flowerhack$ lldb -- /Users/flowerhack/cpython/python.exe flowerhack$ breakpoint set --file bltinmodule.c --line 2337
當單步步過源代碼的時候(這個過程和你在PDB里面做的事情很像——不停敲擊”n”來運行下一行代碼),我們發(fā)現(xiàn)問題第一次出現(xiàn)的那點代碼:
stdout_encoding_str = _PyUnicode_AsString(stdout_encoding); stdout_errors_str = _PyUnicode_AsString(stdout_errors); if (!stdout_encoding_str || !stdout_errors_str) goto _readline_errors; // "throws" an exception
第三行誤導了我:“如果編碼字符串是空或者錯誤字符串是空,那么我們會得到一個錯誤”。但是,請等一下,難道一個空的錯誤字符串不是意味著沒有錯誤被發(fā)現(xiàn)嗎?
因為這個,我進一步查看了_PyUnicode_AsString的定義(另一個C函數(shù)):
#define _PyUnicode_AsString PyUnicode_AsUTF8
那僅僅是一個宏:“嘿,當我們調(diào)用_PyUnicode_AsString的時候,去調(diào)用PyUnicode_AsUTF8。”所以我們真正想要找的是PyUnicode_AsUTF8的定義:
char* PyUnicode_AsUTF8(PyObject *unicode) { return PyUnicode_AsUTF8AndSize(unicode, NULL); }
……看上去這個函數(shù)所做的所有的事情是調(diào)用PyUnicode_AsUTF8AndSize,而這正是我們真正想要去閱讀的。
在PyUnicode_AsUTF8AndSize函數(shù)里面有若干個錯誤情況,每一個都返回NULL。在錯誤情況里面返回NULL而不是返回像-1這樣的錯誤代碼對我來說很奇怪。也許這里有其他我不熟悉的約定的考慮?
不管怎么樣,為了顯示出我究竟陷入了哪一個錯誤情況,我進行了“打印調(diào)試”——我在每一個可能的錯誤情況后面加入了一個打印語句,然后運行程序——這樣我們就能發(fā)現(xiàn)當我們調(diào)用PyUnicode_Check到底錯在了哪里。
那么,是否有在Python3里面進行了而沒有在Python2里面進行過的的檢查呢?嗯,我們能比較兩個版本的源代碼來找出這個答案。最后顯示出,Python 2 的源代碼沒有進行類似的編碼檢查,然而Python 3做了。所以,如果sys.stdout被錯誤編碼的東西代替了,它會在3里面運行失敗,在2里面就不會。
4.收獲!
看上去僅僅是找出一個非常普通的固定的BUG后面的原因,就做了非常多的工作。并且也許確實是這樣。但是!我們在這個過程中學到了一些很酷的東西。當我在驗證一些假設的時候我發(fā)現(xiàn)了很多Python處理標準輸入輸出的方式。我學到了更多如何閱讀大型的、很多宏的C工程的經(jīng)驗。我學到了GOTO語句仍然在使用,這讓我感到很吃驚。但是在連貫性上這樣做是有意義的——看上去如果不用GOTO在C里面做一些像是異常處理的事情的時候?qū)⒆兊暮芊爆?。并且瀏覽bltinmodule.c的input 函數(shù)在Python2 和Python3中的不同真的是一件很酷的事情——嚴格上來說,是檢查。他們重構和清理東西看上去很簡潔。
聲明:設置cinspect有一點復雜。在這個項目的README里面的介紹會有一些幫助,但是注意“indexing your sources”這一步將會花很多時間。
如果你之前習慣使用gdb,那么你僅僅需要知道的是lldb和它非常相似。如果你兩個都沒有用過,他們在調(diào)試上有一點像PDB。
相關文章
Django全局啟用登陸驗證login_required的方法
這篇文章主要介紹了Django全局啟用登陸驗證login_required的方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06基于CentOS搭建Python Django環(huán)境過程解析
這篇文章主要介紹了基于CentOS搭建Python Django環(huán)境過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-08-08yolov5訓練時參數(shù)workers與batch-size的深入理解
最近再學習YOLOv3與YOLOv5訓練數(shù)據(jù)集的具體步驟,幾經(jīng)波折終于實現(xiàn)了很好的效果,這篇文章主要給大家介紹了關于yolov5訓練時參數(shù)workers與batch-size的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-03-03Python虛擬機字節(jié)碼教程之裝飾器實現(xiàn)詳解
在本篇文章當中主要給大家介紹在?cpython?當中一些比較常見的字節(jié)碼,從根本上理解?python?程序的執(zhí)行。在本文當中主要介紹一些?python?基本操作的字節(jié)碼,并且將從字節(jié)碼的角度分析函數(shù)裝飾器的原理2023-04-04python隨機模塊random的22種函數(shù)(小結)
這篇文章主要介紹了python隨機模塊random的22種函數(shù),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-05-05解決nohup執(zhí)行python程序log文件寫入不及時的問題
今天小編就為大家分享一篇解決nohup執(zhí)行python程序log文件寫入不及時的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-01-01