長年打log突然提出一個問題,logging模塊是如何定位到打印log的具體位置(文件、函數、行)的呢?以下從logging模塊的源碼解決這個問題。
調用棧分析
當代碼運行到
logging.debug('debug')
的時候,我們來看看發(fā)生了什么。
函數調用路徑如下:
函數 | 函數內調用 |
---|---|
logging.debug | Logger()._log |
Logger()._log | Logger().findCaller、 Logger().makeRecord |
Logger().findCaller | currentframe |
currentframe | sys.exc_info |
makeRecord | 調用的函數與本文關系不大 |
以下詳細分析這些函數的細節(jié)
currentframe函數
currentframe
函數的作用是返回當前的棧幀。實現是制造并捕獲一個異常,并通過sys.exc_info函數得到這個異常的信息,實現簡單而巧妙。
currentframe函數的實現
def currentframe():
"""Return the frame object for the caller's stack frame."""
try:
raise Exception
except:
return sys.exc_info()[2].tb_frame.f_back
sys.exc_info
函數獲取當前正在處理的異常類,exc_type、exc_value、exc_traceback是當前處理的異常詳細信息三元組(type, value, traceback)。
sys.exc_info()[2]
返回當前異常的棧,調用tb_frame
返回當前異常的幀,也就是該函數raise Exception
的異常幀,打印log的目的是查看log具體的位置,所以當前的信息肯定是不需要的,所以調用f_back
得到他的outer層,并返回。
此時我們返回到父函數findCaller
findCaller
findCaller
繼續(xù)子函數currentframe
的f_back
功能,在while循環(huán)里面不斷追溯上游捕抓Exception的位置,直到當前棧所在文件不是當前的文件_srcfile
為止,實現如下:
def findCaller(self):
f = currentframe() # 調用上面提到的currentframe函數
if f is not None:
f = f.f_back
rv = "(unknown file)", 0, "(unknown function)"
while hasattr(f, "f_code"): # 利用f_code屬性
co = f.f_code
filename = os.path.normcase(co.co_filename)
if filename == _srcfile: # 如果上游捕抓的位置是當前文件,那么繼續(xù)上溯,最終得到打印log的位置
f = f.f_back # 繼續(xù)上溯
continue
rv = (co.co_filename, f.f_lineno, co.co_name) # 文件名、行數和函數名
break
return rv
# _srcfile定義
_srcfile = os.path.normcase(currentframe.__code__.co_filename) # currentframe函數和findCaller函數在同一個文件里面
返回的rv也是一個三元組(filename、lineno、co_name),包含打印log的文件名、代碼行數、函數名字。到這為止,打印log需要的信息就收集到了。
總結
log日志信息的實現是利用異常的捕獲機制去做的,我們知道當拋出一個異常的時候,異常會一步一步往上拋,直到得到處理或者程序異常結束為止,這樣就一定可以追溯到源頭。
另外可以看到,在尋找_srcfile
的時候,調用了__code__
來返回當前代碼所屬的文件名;在findCaller
里用frame.f_lineno
來獲取代碼行,用co.co_name
來獲取當前函數的名字;等等。可見,得益于強大自省的特性,整體實現簡單明了。