这是一篇译文,原文地址:https://tenthousandmeters.com/blog/python-behind-the-scenes-3-stepping-through-the-cPython-source-code/欢迎关注我的公众号:ReadingPython在系列第一篇和第二篇中,我们讨论了Python程序编译与执行的基本原理,之后我们还是会聚焦一些原理性的东西。
不过,在本篇中,让我们来看看这些原理在代码中的具体实现。
0.本篇计划CPython代码库有大约35万行C代码(头文件除外),60万行PYTHON代码,一次性看完不太现实,今天我们主要看那些每次运行python程序都会执行的部分。
从python可执行文件对应的main函数开始,一步步往下,直到求值循环(evaluationloop),也就是运行Python字节码的地方。
我们并不需要理解每一行代码,而是重点关注一些有意思的地方,争取对Python程序的启动过程有一个基本概念。
另外有两点说明。
一是,我们只深入讨论部分函数而概览其它,不过,我会按执行顺序来讲解。
二是,除了极少数结构体定义,我会按代码库中的原貌呈现代码,唯一的改动就是增加一些说明性的注释。
在后文代码中,多行注释/**/都是原来就有的,单行注释//是我新增的。
现在,让我们开启CPython源码之旅吧。
1.获取CPython源码首先,把CPython代码库下载下来:$gitclonehttps://github.com/python/cpython/&&cdcpython目前,master分支上的是CPython3.10。
我们要看的是最新稳定版本,也就是CPython3.9。
先切换分支:$gitcheckout3.9根目录下,可以看到下面这些内容:$ls-p
CODE_OF_CONDUCT.mdObjects/config.sub
Doc/PC/configure
Grammar/PCbuild/configure.ac
Include/Parser/install-sh
LICENSEPrograms/m4/
Lib/Python/netlify.toml
Mac/README.rstpyconfig.h.in
Makefile.pre.inTools/setup.py
Misc/aclocal.m4
Modules/config.guess其中一些子件夹是本系列文章会重点关注的:Grammar/中是我们上篇讨论的语法文件。
Include/中是一些头文件,供CPython或调用Python/C接口的用户使用。
Lib/中是Python写的标准库,其中一些库,如argparse和wave,是纯Python实现的,另一些则包含了C代码,比如io库就封装了C语言实现的_io模块。
Modules/中的则是C语言写的标准库,其中一些,如itertools等,可以直接引入使用,另一些则要由对应的Python模块封装后使用。
Objects/中是内置类型的具体实现,如果想知道int或float是如何实现的,你会来到这个文件夹。
Parser/中是旧版解析器、旧版解析器生成器、新版解析器以及分词器。
Programs/中是各种可执行文件的源码。
Python/中的是解释器的源文件,包括编译器、求值循环、内置模块等。
Tools/中包含了一些构建和管理CPython的工具,新版解析器生成器也放在这里。
如果你是那种没看到tests文件夹就要心跳加速的人,请放心,它在Lib文件夹中。
这些测试不仅在开发代码的时候很有用,而且能帮我们更好地理解CPython。
比如,要理解窥孔优化器(peepholeoptimizer)到底优化了哪些东西,可以查看Lib/test/test_peepholer.py文件,要理解某部分代码的功能,可以注释掉那部分代码,重新编译CPython,再运行测试:$./python.exe-mtesttest_peepholer看看哪些用例失败了。
理想情况下,编译CPython只需要执行两条命令:$./configure
$make-j-smake命令将生成python可执行文件。
如果你在Mac系统下看到python.exe,不要觉得惊讶,这里的.exe后缀只是用于在大小写不敏感的文件系统中区分可执行文件与Python/文件夹而已。
更多编译相关信息,可以查看开发者手册。
现在,我们可以自豪地说,我们已经构建了自有版本的CPython了:$./python.exe
Python3.9.0+(heads/3.9-dirty:20bdeedfb4,Oct102020,16:55:24)
[Clang10.0.0(clang-1000.10.44.4)]ondarwin
Type"help","copyright","credits"or"license"formoreinformation.
>>>2**16
655362.源码正如所有C程序一样,CPython的执行入口是Python/python.c中的一个main()函数:/*Minimalmainprogram–everythingisloadedfromthelibrary*/
#include"Python.h"
#ifdefMS_WINDOWS
int
wmain(intargc,wchar_t**argv)
{
returnPy_Main(argc,argv);
}
#else
int
main(intargc,char**argv)
{
returnPy_BytesMain(argc,argv);
}
#endif这里没什么内容。
唯一值得一提的是,在Windows系统中,为接收UTF-16编码的字符串参数,CPython使用wmain()函数作为入口。
而在其它平台上,CPython需要额外执行一个步骤,将char字符串转为wchar_t字符串,char字符串的编码方式取决于locale设置,而wchar_t的编码方式则取决于wchar_t的长度。
例如,如果sizeof(wchar_t)为4,则采用UCS-4编码。
Py_Main()和Py_BytesMain()在Modules/main.c中定义,其实只是以不同参数调用pymain_main()函数而已:int
Py_Main(intargc,wchar_t**argv)
{
_PyArgvargs={
.argc=argc,
.use_bytes_argv=0,
.bytes_argv=NULL,
.wchar_argv=argv};
returnpymain_main(&args);
}
int
Py_BytesMain(intargc,char**argv)
{
_PyArgvargs={
.argc=argc,
.use_bytes_argv=1,
.bytes_argv=argv,
.wchar_argv=NULL};
returnpymain_main(&args);
}我们可以看下pymain_main()函数。
初看上去,它好像也没做什么事情:staticint
pymain_main(_PyArgv*args)
{
PyStatusstatus=pymain_init(args);
if(_PyStatus_IS_EXIT(status)){
pymain_free();
returnstatus.exitcode;
}
if(_PyStatus_EXCEPTION(status)){
pymain_exit_error(status);
}
returnPy_RunMain();
}上一篇中,我们看到,在一个Python程序执行前,CPython需要做很多编译工作。
其实,在编译之前,CPython就已经做了很多事情了,这些事情组成了CPython初始化过程。
在第一篇中,我们曾说,CPython的工作包括三个阶段:初始化编译,以及解释因此,pymain_main()首先调用pymain_init()执行初始化,然后调用Py_RunMain()进行下一步工作。
问题来了:CPython在初始化阶段做了哪些事情呢?我们可以推测一下,至少,它要做以下几件事:根据操作系统的不同,为参数、环境变量、标准输入输出流以及文件系统选择一种合适的编码方式解析命令行参数,读取环境变量,确定运行方式初始化运行时状态、主解释器状态以及主线程状态初始化内置类型与内置模块初始化sys模块准备好模块导入系统创建__main__模块在进入pymain_init()函数前,我们先具体讨论下初始化过程。
2.1初始化CPython3.8之后,初始化被分为三个阶段:预初始化(preinitialization)核心初始化(coreinitialization)主初始化(maininitialization)预初始化阶段负责初始化运行时状态,准备默认的内存分配器,完成基本配置。
这里还看不到Python的影子。
核心初始化阶段负责初始化解释器状态、主线程状态、内置类型与异常、内置模块,准备sys模块与模块导入系统。
此时,我们已经可以使用Python的“核心”部分了。
不过,还是有些功能没准备好,例如,sys模块只有部分功能、只支持导入内置模块与冻结模块(frozenmodules,译者注:由Python实现,但字节码封装在可执行文件中,不需要解释器即可执行的模块)等。
主初始化阶段后,CPython才完成所有初始化过程,可以进行编译或解释工作了。
把初始化过程分为三个阶段有什么好处呢?简单地说,这样可以更方便地配置CPython。
比如,用户可以在核心初始化阶段覆盖相关路径,从而使用自定义的内存分配器。
当然,CPython自己不需要再“自定义”什么东西,但对使用Python/C接口的人来说,这种能力是很重要的。
PEP432和PEP587具体地说明了多阶段初始化的优势。
pymain_init()函数负责预初始化,然后调用Py_InitializeFromConfig()进入核心初始化和主初始化阶段。
staticPyStatus
pymain_init(const_PyArgv*args)
{
PyStatusstatus;
//初始化运行时状态
status=_PyRuntime_Initialize();
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
//初始化默认配置
PyPreConfigpreconfig;
PyPreConfig_InitPythonConfig(&preconfig);
//预初始化
status=_Py_PreInitializeFromPyArgv(&preconfig,args);
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
//预初始化完成,为下一个初始化阶段准备参数
//初始化默认配置
PyConfigconfig;
PyConfig_InitPythonConfig(&config);
//将命令行参数存储至`config->argv`
if(args->use_bytes_argv){
status=PyConfig_SetBytesArgv(&config,args->argc,args->bytes_argv);
}
else{
status=PyConfig_SetArgv(&config,args->argc,args->wchar_argv);
}
if(_PyStatus_EXCEPTION(status)){
gotodone;
}
//执行核心初始化和主初始化
status=Py_InitializeFromConfig(&config);
if(_PyStatus_EXCEPTION(status)){
gotodone;
}
status=_PyStatus_OK();
done:
PyConfig_Clear(&config);
returnstatus;
}
_PyRuntime_Initialize()负责初始化运行时状态,运行时状态存储在_PyRuntime全局变量中,它的结构体定义如下:/*FullPythonruntimestate*/
typedefstructpyruntimestate{
/*IsrunningPy_PreInitialize()?*/
intpreinitializing;
/*IsPythonpreinitialized?Setto1byPy_PreInitialize()*/
intpreinitialized;
/*IsPythoncoreinitialized?Setto1by_Py_InitializeCore()*/
intcore_initialized;
/*IsPythonfullyinitialized?Setto1byPy_Initialize()*/
intinitialized;
/*SetbyPy_FinalizeEx().OnlyresettoNULLifPy_Initialize()iscalledagain.*/
_Py_atomic_address_finalizing;
structpyinterpreters{
PyThread_type_lockmutex;
PyInterpreterState*head;
PyInterpreterState*main;
int64_tnext_id;
}interpreters;
unsignedlongmain_thread;
struct_ceval_runtime_stateceval;
struct_gilstate_runtime_stategilstate;
PyPreConfigpreconfig;
//…后面是一些暂时可以忽略的东西
}_PyRuntimeState;结构体最后一个字段是preconfig,负责保存CPython预初始化相关配置,同时也用于之后两个阶段。
下面是它的类型定义:typedefstruct{
int_config_init;/*_PyConfigInitEnumvalue*/
/*ParsePy_PreInitializeFromBytesArgs()arguments?
SeePyConfig.parse_argv*/
intparse_argv;
/*Ifgreaterthan0,enableisolatedmode:sys.pathcontains
neitherthescript'sdirectorynortheuser'ssite-packagesdirectory.
Setto1bythe-Icommandlineoption.Ifsetto-1(default),inherit
Py_IsolatedFlagvalue.*/
intisolated;
/*Ifgreaterthan0:useenvironmentvariables.
Setto0by-Ecommandlineoption.Ifsetto-1(default),itis
setto!Py_IgnoreEnvironmentFlag.*/
intuse_environment;
/*SettheLC_CTYPElocaletotheuserpreferredlocale?Ifequalsto0,
setcoerce_c_localeandcoerce_c_locale_warnto0.*/
intconfigure_locale;
/*CoercetheLC_CTYPElocaleifit'sequalto"C"?(PEP538)
Setto0byPYTHONCOERCECLOCALE=0.Setto1byPYTHONCOERCECLOCALE=1.
Setto2iftheuserpreferredLC_CTYPElocaleis"C".
Ifitisequalto1,LC_CTYPElocaleisreadtodecideifitshouldbe
coercedornot(ex:PYTHONCOERCECLOCALE=1).Internally,itissetto2
iftheLC_CTYPElocalemustbecoerced.
Disablebydefault(setto0).Setitto-1toletPythondecideifit
shouldbeenabledornot.*/
intcoerce_c_locale;
/*EmitawarningiftheLC_CTYPElocaleiscoerced?
Setto1byPYTHONCOERCECLOCALE=warn.
Disablebydefault(setto0).Setitto-1toletPythondecideifit
shouldbeenabledornot.*/
intcoerce_c_locale_warn;
#ifdefMS_WINDOWS
/*Ifgreaterthan1,usethe"mbcs"encodinginsteadoftheUTF-8
encodingforthefilesystemencoding.
Setto1ifthePYTHONLEGACYWINDOWSFSENCODINGenvironmentvariableis
settoanon-emptystring.Ifsetto-1(default),inherit
Py_LegacyWindowsFSEncodingFlagvalue.
SeePEP529formoredetails.*/
intlegacy_windows_fs_encoding;
#endif
/*EnableUTF-8mode?(PEP540)
Disabledbydefault(equalsto0).
Setto1by"-Xutf8"and"-Xutf8=1"commandlineoptions.
Setto1byPYTHONUTF8=1environmentvariable.
Setto0by"-Xutf8=0"andPYTHONUTF8=0.
Ifequalsto-1,itissetto1iftheLC_CTYPElocaleis"C"or
"POSIX",otherwiseitissetto0.InheritPy_UTF8Modevaluevalue.*/
intutf8_mode;
/*Ifnon-zero,enablethePythonDevelopmentMode.
Setto1bythe-Xdevcommandlineoption.SetbythePYTHONDEVMODE
environmentvariable.*/
intdev_mode;
/*Memoryallocator:PYTHONMALLOCenvvar.
SeePyMemAllocatorNameforvalidvalues.*/
intallocator;
}PyPreConfig;调用_PyRuntime_Initialize()后,_PyRuntime会按默认值完成初始化,随后,PyPreConfig_InitPythonConfig()按预定义值再次将它初始化,再由_Py_PreInitializeFromPyArgv()执行真正的预初始化过程。
为什么_PyRuntime要执行两次初始化呢?因为CPython调用的很多函数同时也供Python/C接口调用,因此,CPython也会统一按Python/C接口的调用模式调用这些函数。
也因为同一个原因,CPython源码中经常会看到一些不好理解的函数调用。
比如,在整个初始化过程中,_PyRuntime_Initialize()函数就被调用了很多次,实际上后面几次调用并没有什么作用。
_Py_PreInitializeFromPyArgv()负责读取命令行参数、环境变量以及全局配置,并完成_PyRuntime.preconfig、本地化以及内存分配器设置。
它只读取和预初始化相关的参数,例如,命令行参数中的-E-I-X等。
此时,运行时已经预初始化了。
接下来,pymain_init()会准备好下一步初始化需要的配置。
注意,这个配置和前面的preconfig不是一个东西,这里的配置保存着绝大多数Python相关配置,在整个初始化、以及Python程序执行过程中使用广泛。
你可以看一下它的结构体的超长定义:/*—PyConfig———————————————-*/
typedefstruct{
int_config_init;/*_PyConfigInitEnumvalue*/
intisolated;/*Isolatedmode?seePyPreConfig.isolated*/
intuse_environment;/*Useenvironmentvariables?seePyPreConfig.use_environment*/
intdev_mode;/*PythonDevelopmentMode?SeePyPreConfig.dev_mode*/
/*Installsignalhandlers?Yesbydefault.*/
intinstall_signal_handlers;
intuse_hash_seed;/*PYTHONHASHSEED=x*/
unsignedlonghash_seed;
/*Enablefaulthandler?
Setto1by-XfaulthandlerandPYTHONFAULTHANDLER.-1meansunset.*/
intfaulthandler;
/*EnablePEGparser?
1bydefault,setto0by-XoldparserandPYTHONOLDPARSER*/
int_use_peg_parser;
/*Enabletracemalloc?
Setby-Xtracemalloc=NandPYTHONTRACEMALLOC.-1meansunset*/
inttracemalloc;
intimport_time;/*PYTHONPROFILEIMPORTTIME,-Ximporttime*/
intshow_ref_count;/*-Xshowrefcount*/
intdump_refs;/*PYTHONDUMPREFS*/
intmalloc_stats;/*PYTHONMALLOCSTATS*/
/*Pythonfilesystemencodinganderrorhandler:
sys.getfilesystemencoding()andsys.getfilesystemencodeerrors().
Defaultencodinganderrorhandler:
*ifPy_SetStandardStreamEncoding()hasbeencalled:theyhavethe
highestpriority;
*PYTHONIOENCODINGenvironmentvariable;
*TheUTF-8ModeusesUTF-8/surrogateescape;
*IfPythonforcestheusageoftheASCIIencoding(ex:Clocale
orPOSIXlocaleonFreeBSDorHP-UX),useASCII/surrogateescape;
*localeencoding:ANSIcodepageonWindows,UTF-8onAndroidand
VxWorks,LC_CTYPElocaleencodingonotherplatforms;
*OnWindows,"surrogateescape"errorhandler;
*"surrogateescape"errorhandleriftheLC_CTYPElocaleis"C"or"POSIX";
*"surrogateescape"errorhandleriftheLC_CTYPElocalehasbeencoerced
(PEP538);
*"strict"errorhandler.
Supportederrorhandlers:"strict","surrogateescape"and
"surrogatepass".Thesurrogatepasserrorhandlerisonlysupported
ifPy_DecodeLocale()andPy_EncodeLocale()usedirectlytheUTF-8codec;
it'sonlyusedonWindows.
initfsencoding()updatestheencodingtothePythoncodecname.
Forexample,"ANSI_X3.4-1968"isreplacedwith"ascii".
OnWindows,sys._enablelegacywindowsfsencoding()setsthe
encoding/errorstombcs/replaceatruntime.
SeePy_FileSystemDefaultEncodingandPy_FileSystemDefaultEncodeErrors.
*/
wchar_t*filesystem_encoding;
wchar_t*filesystem_errors;
wchar_t*pycache_prefix;/*PYTHONPYCACHEPREFIX,-Xpycache_prefix=PATH*/
intparse_argv;/*Parseargvcommandlinearguments?*/
/*Commandlinearguments(sys.argv).
Setparse_argvto1toparseargvasPythoncommandlinearguments
andthenstripPythonargumentsfromargv.
Ifargvisempty,anemptystringisaddedtoensurethatsys.argv
alwaysexistsandisneverempty.*/
PyWideStringListargv;
/*Programname:
-IfPy_SetProgramName()wascalled,useitsvalue.
-OnmacOS,usePYTHONEXECUTABLEenvironmentvariableifset.
-IfWITH_NEXT_FRAMEWORKmacroisdefined,use__PYVENV_LAUNCHER__
environmentvariableisset.
-Useargv[0]ifavailableandnon-empty.
-Use"python"onWindows,or"python3onotherplatforms.*/
wchar_t*program_name;
PyWideStringListxoptions;/*Commandline-Xoptions*/
/*Warningsoptions:lowesttohighestpriority.warnings.filters
isbuiltinthereverseorder(highesttolowestpriority).*/
PyWideStringListwarnoptions;
/*Ifequaltozero,disabletheimportofthemodulesiteandthe
site-dependentmanipulationsofsys.paththatitentails.Alsodisable
thesemanipulationsifsiteisexplicitlyimportedlater(call
site.main()ifyouwantthemtobetriggered).
Setto0bythe-Scommandlineoption.Ifsetto-1(default),itis
setto!Py_NoSiteFlag.*/
intsite_import;
/*Byteswarnings:
*Ifequalto1,issueawarningwhencomparingbytesorbytearraywith
strorbyteswithint.
*Ifequalorgreaterto2,issueanerror.
Incrementedbythe-bcommandlineoption.Ifsetto-1(default),inherit
Py_BytesWarningFlagvalue.*/
intbytes_warning;
/*Ifgreaterthan0,enableinspect:whenascriptispassedasfirst
argumentorthe-coptionisused,enterinteractivemodeafter
executingthescriptorthecommand,evenwhensys.stdindoesnotappear
tobeaterminal.
Incrementedbythe-icommandlineoption.Setto1ifthePYTHONINSPECT
environmentvariableisnon-empty.Ifsetto-1(default),inherit
Py_InspectFlagvalue.*/
intinspect;
/*Ifgreaterthan0:enabletheinteractivemode(REPL).
Incrementedbythe-icommandlineoption.Ifsetto-1(default),
inheritPy_InteractiveFlagvalue.*/
intinteractive;
/*Optimizationlevel.
Incrementedbythe-Ocommandlineoption.SetbythePYTHONOPTIMIZE
environmentvariable.Ifsetto-1(default),inheritPy_OptimizeFlag
value.*/
intoptimization_level;
/*Ifgreaterthan0,enablethedebugmode:turnonparserdebugging
output(forexpertonly,dependingoncompilationoptions).
Incrementedbythe-dcommandlineoption.SetbythePYTHONDEBUG
environmentvariable.Ifsetto-1(default),inheritPy_DebugFlag
value.*/
intparser_debug;
/*Ifequalto0,Pythonwon'ttrytowrite“.pyc“filesonthe
importofsourcemodules.
Setto0bythe-BcommandlineoptionandthePYTHONDONTWRITEBYTECODE
environmentvariable.Ifsetto-1(default),itissetto
!Py_DontWriteBytecodeFlag.*/
intwrite_bytecode;
/*Ifgreaterthan0,enabletheverbosemode:printamessageeachtimea
moduleisinitialized,showingtheplace(filenameorbuilt-inmodule)
fromwhichitisloaded.
Ifgreaterorequalto2,printamessageforeachfilethatischecked
forwhensearchingforamodule.Alsoprovidesinformationonmodule
cleanupatexit.
Incrementedbythe-voption.SetbythePYTHONVERBOSEenvironment
variable.Ifsetto-1(default),inheritPy_VerboseFlagvalue.*/
intverbose;
/*Ifgreaterthan0,enablethequietmode:Don'tdisplaythecopyright
andversionmessagesevenininteractivemode.
Incrementedbythe-qoption.Ifsetto-1(default),inherit
Py_QuietFlagvalue.*/
intquiet;
/*Ifgreaterthan0,don'taddtheusersite-packagesdirectoryto
sys.path.
Setto0bythe-sand-Icommandlineoptions,andthePYTHONNOUSERSITE
environmentvariable.Ifsetto-1(default),itissetto
!Py_NoUserSiteDirectory.*/
intuser_site_directory;
/*Ifnon-zero,configureCstandardsteams(stdio,stdout,
stderr):
-SetO_BINARYmodeonWindows.
-Ifbuffered_stdioisequaltozero,makestreamsunbuffered.
Otherwise,enablestreamsbufferingifinteractiveisnon-zero.*/
intconfigure_c_stdio;
/*Ifequalto0,enableunbufferedmode:forcethestdoutandstderr
streamstobeunbuffered.
Setto0bythe-uoption.SetbythePYTHONUNBUFFEREDenvironment
variable.
Ifsetto-1(default),itissetto!Py_UnbufferedStdioFlag.*/
intbuffered_stdio;
/*Encodingofsys.stdin,sys.stdoutandsys.stderr.
ValuesetfromPYTHONIOENCODINGenvironmentvariableand
Py_SetStandardStreamEncoding()function.
Seealso'stdio_errors'attribute.*/
wchar_t*stdio_encoding;
/*Errorhandlerofsys.stdinandsys.stdout.
ValuesetfromPYTHONIOENCODINGenvironmentvariableand
Py_SetStandardStreamEncoding()function.
Seealso'stdio_encoding'attribute.*/
wchar_t*stdio_errors;
#ifdefMS_WINDOWS
/*Ifgreaterthanzero,useio.FileIOinsteadofWindowsConsoleIOforsys
standardstreams.
Setto1ifthePYTHONLEGACYWINDOWSSTDIOenvironmentvariableissetto
anon-emptystring.Ifsetto-1(default),inherit
Py_LegacyWindowsStdioFlagvalue.
SeePEP528formoredetails.*/
intlegacy_windows_stdio;
#endif
/*Valueofthe–check-hash-based-pycscommandlineoption:
-"default"meansthe'check_source'flaginhash-basedpycs
determinesinvalidation
-"always"causestheinterpretertohashthesourcefilefor
invalidationregardlessofvalueof'check_source'bit
-"never"causestheinterpretertoalwaysassumehash-basedpycsare
valid
Thedefaultvalueis"default".
SeePEP552"Deterministicpycs"formoredetails.*/
wchar_t*check_hash_pycs_mode;
/*—Pathconfigurationinputs————*/
/*Ifgreaterthan0,suppress_PyPathConfig_Calculate()warningsonUnix.
TheparameterhasnoeffectonWindows.
Ifsetto-1(default),inherit!Py_FrozenFlagvalue.*/
intpathconfig_warnings;
wchar_t*pythonpath_env;/*PYTHONPATHenvironmentvariable*/
wchar_t*home;/*PYTHONHOMEenvironmentvariable,
seealsoPy_SetPythonHome().*/
/*—Pathconfigurationoutputs———–*/
intmodule_search_paths_set;/*Ifnon-zero,usemodule_search_paths*/
PyWideStringListmodule_search_paths;/*sys.pathpaths.Computedif
module_search_paths_setisequal
tozero.*/
wchar_t*executable;/*sys.executable*/
wchar_t*base_executable;/*sys._base_executable*/
wchar_t*prefix;/*sys.prefix*/
wchar_t*base_prefix;/*sys.base_prefix*/
wchar_t*exec_prefix;/*sys.exec_prefix*/
wchar_t*base_exec_prefix;/*sys.base_exec_prefix*/
wchar_t*platlibdir;/*sys.platlibdir*/
/*—ParameteronlyusedbyPy_Main()———-*/
/*Skipthefirstlineofthesource('run_filename'parameter),allowinguseofnon-Unixformsof
"#!cmd".ThisisintendedforaDOSspecifichackonly.
Setbythe-xcommandlineoption.*/
intskip_source_first_line;
wchar_t*run_command;/*-ccommandlineargument*/
wchar_t*run_module;/*-mcommandlineargument*/
wchar_t*run_filename;/*Trailingcommandlineargumentwithout-cor-m*/
/*—Privatefields—————————-*/
/*Installimportlib?Ifsetto0,importlibisnotinitializedatall.
Neededbyfreeze_importlib.*/
int_install_importlib;
/*Ifequalto0,stopPythoninitializationbeforethe"main"phase*/
int_init_main;
/*Ifnon-zero,disallowthreads,subprocesses,andfork.
Default:0.*/
int_isolated_interpreter;
/*Originalcommandlinearguments.If_orig_argvisemptyand_argvis
notequalto[''],PyConfig_Read()copiestheconfiguration'argv'list
into'_orig_argv'listbeforemodifying'argv'list(ifparse_argv
isnon-zero).
_PyConfig_Write()initializesPy_GetArgcArgv()tothislist.*/
PyWideStringList_orig_argv;
}PyConfig;pymain_init()先调用PyConfig_InitPythonConfig()创建默认配置,然后调用PyConfig_SetBytesArgv()将命令行参数存储至config.argv中,最后调用Py_InitializeFromConfig()执行核心初始化和主初始化。
下面,我们来看看Py_InitializeFromConfig():PyStatus
Py_InitializeFromConfig(constPyConfig*config)
{
if(config==NULL){
return_PyStatus_ERR("initializationconfigisNULL");
}
PyStatusstatus;
//看到没,这里又调用了一次!
status=_PyRuntime_Initialize();
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
_PyRuntimeState*runtime=&_PyRuntime;
PyThreadState*tstate=NULL;
//核心初始化阶段
status=pyinit_core(runtime,config,&tstate);
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
config=_PyInterpreterState_GetConfig(tstate->interp);
if(config->_init_main){
//主初始化阶段
status=pyinit_main(tstate);
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
}
return_PyStatus_OK();
}我们可以清楚地看到初始化的不同阶段。
核心初始化由pyinit_core()完成,主初始化由pyinit_main()完成。
pyinit_core()函数初始化了Python“核心”部分,具体可以分为两步:准备相关配置:解析命令行参数,读取环境变量,确定文件路径,选择标准流与文件系统的编码方式,并将这些数据写入配置变量的对应位置;应用这些配置:设置标准流,生成哈希函数密钥,创建主解释器状态与主线程状态,初始化GIL并占用,使能垃圾收集器,初始化内置类型与异常,初始化sys模块及内置模块,为内置模块与冻结模块准备好模块导入系统;在第一步中,CPython会计算config.module_search_paths,之后,这个路径会被复制到sys.path。
其它内容比较无聊,我们先略过。
我们来看看pyinit_config(),它被pyinit_core函数调用,负责执行第二步:staticPyStatus
pyinit_config(_PyRuntimeState*runtime,
PyThreadState**tstate_p,
constPyConfig*config)
{
//根据配置设置Py_*全局变量
//初始化标准流(stdin,stdout,stderr)
//为哈希函数设置密钥
PyStatusstatus=pycore_init_runtime(runtime,config);
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
PyThreadState*tstate;
//创建主解释器状态和主线程状态
//占用GIL
status=pycore_create_interpreter(runtime,config,&tstate);
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
*tstate_p=tstate;
//初始化数据类型、异常、sys、内置函数和模块、导入系统等
status=pycore_interp_init(tstate);
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
/*Onlywhenwegethereistheruntimecorefullyinitialized*/
runtime->core_initialized=1;
return_PyStatus_OK();
}首先,pycore_init_runtime()会把一些配置数据复制到对应的全局变量中,这些全局变量将在PyConfig准备好之前用于配置CPython,同时也作为Python/C接口的一部分。
然后,pycore_init_runtime()将设置标准输入输出流对应的文件句柄与缓存模式,在类Unix系统中,也就是调用库函数setvbug()。
最后,pycore_init_runtime()会为哈希函数生成密钥,存储在全局变量_Py_HashSecret中。
这个密钥将作为CPython所用的哈希函数SipHash24的参数。
每次CPython启动,都会随机生成一个新的密钥,以防止哈希冲突攻击。
Python与其它很多编程语言,如PHP、Ruby、JavaScript以及C#等,都曾存在哈希冲突攻击漏洞。
攻击者可以用一组生成相同哈希值的字符串攻击相关应用,由于这些字符串得到的哈希值相同,把它们放在同一个集合或字典(哈希表)中,会导致每次数据存取都花费大量计算,占用CPU性能。
解决方案就是为哈希函数提供一个随机密钥。
另外,Python也支持设置PYTHONHASHSEED环境变量,控制密钥的生成。
关于哈希冲突攻击,可以参考这个演讲。
关于CPython的哈希算法,可以参考PEP456。
在系列第一篇中,我们知道,CPython使用线程状态保存线程相关数据,如调用栈、异常状态等,使用解释器状态保存解释器相关数据,如加载的模块、导入设置等。
pycore_create_interpreter()函数负责为主线程创建解释器状态与线程状态。
下面是解释器状态的结构体定义://解释器状态的定义在Include/pystate.h
struct_is{
//_PyRuntime.interpreters.head保存了最近创建的解释器
//`next`指针让我们可以访问所有解释器
struct_is*next;
//`tstate_head`指向最近创建的线程状态
//同一个解释器下的线程状态在一个链表中
struct_ts*tstate_head;
/*Referencetothe_PyRuntimeglobalvariable.Thisfieldexists
tonothavetopassruntimeinadditiontotstatetoafunction.
Getruntimefromtstate:tstate->interp->runtime.*/
structpyruntimestate*runtime;
int64_tid;
//解释器的引用记录
int64_tid_refcount;
intrequires_idref;
PyThread_type_lockid_mutex;
intfinalizing;
struct_ceval_stateceval;
struct_gc_runtime_stategc;
PyObject*modules;//sys.modules对应的指针
PyObject*modules_by_index;
PyObject*sysdict;//sys.__dict__对应的指针
PyObject*builtins;//builtins.__dict__对应的指针
PyObject*importlib;
//编解码器搜索
PyObject*codec_search_path;
PyObject*codec_search_cache;
PyObject*codec_error_registry;
intcodecs_initialized;
struct_Py_unicode_stateunicode;
PyConfigconfig;
PyObject*dict;/*Storesper-interpreterstate*/
PyObject*builtins_copy;
PyObject*import_func;
/*InitializedtoPyEval_EvalFrameDefault().*/
_PyFrameEvalFunctioneval_frame;
//可以看`atexit`模块
void(*pyexitfunc)(PyObject*);
PyObject*pyexitmodule;
uint64_ttstate_next_unique_id;
//可以看`warnings`模块
struct_warnings_runtime_statewarnings;
//审计钩子,可以看sys.addaudithook
PyObject*audit_hooks;
#if_PY_NSMALLNEGINTS+_PY_NSMALLPOSINTS>0
//小整数保存在这里,便于复用
//默认范围为[-5,256].
PyLongObject*small_ints[_PY_NSMALLNEGINTS+_PY_NSMALLPOSINTS];
#endif
//…暂时不关心的内容
};值得特别关注的是,我们之前读取的各类参数保存在新创建的解释器状态的config字段中。
配置归属于解释器状态。
线程状态的结构体定义如下://ThePyThreadStatetypedefisinInclude/pystate.h.
struct_ts{
//同解释器下的线程状态保存在一个双链表中
struct_ts*prev;
struct_ts*next;
PyInterpreterState*interp;
//当前帧的引用(可以是NULL)
//通过frame->f_back可以访问调用栈
PyFrameObject*frame;
//…检查递归层次是否太深
//…追踪/记录状态
/*Theexceptioncurrentlybeingraised*/
PyObject*curexc_type;
PyObject*curexc_value;
PyObject*curexc_traceback;
/*Theexceptioncurrentlybeinghandled,ifnocoroutines/generators
*arepresent.Alwayslastelementonthestackreferredtobeexc_info.
*/
_PyErr_StackItemexc_state;
/*Pointertothetopofthestackoftheexceptionscurrently
*beinghandled*/
_PyErr_StackItem*exc_info;
PyObject*dict;/*Storesper-threadstate*/
intgilstate_counter;
PyObject*async_exc;/*Asynchronousexceptiontoraise*/
unsignedlongthread_id;/*Threadidwherethiststatewascreated*/
/*Uniquethreadstateid.*/
uint64_tid;
//…其它暂时可忽略的东西
};创建主线程状态后,pycore_create_interpreter()函数将初始化GIL,避免多个线程同时操作Python对象。
如果你通过threading模块创建新线程,它将在每次进入求值循环前等待,直到占用GIL锁,同时,线程状态将作为求值函数的一个参数,供线程随时访问。
如果你要通过Python/C接口手动占用GIL锁,也必须同时提供对应的线程状态。
此时,需要将线程状态存储至特定的线程存储空间(在类Unix系统中,即调用pthread_setspecific()库函数)。
GIL需要单独一篇文章来讨论,Python对象系统和import机制也一样。
不过,本篇还是会简单提及一些内容。
创建第一个解释器状态和线程状态后,pyinit_config()调用pycore_interp_init()函数完成核心初始化。
pycore_interp_init()函数的代码逻辑很清晰:staticPyStatus
pycore_interp_init(PyThreadState*tstate)
{
PyStatusstatus;
PyObject*sysmod=NULL;
status=pycore_init_types(tstate);
if(_PyStatus_EXCEPTION(status)){
gotodone;
}
status=_PySys_Create(tstate,&sysmod);
if(_PyStatus_EXCEPTION(status)){
gotodone;
}
status=pycore_init_builtins(tstate);
if(_PyStatus_EXCEPTION(status)){
gotodone;
}
status=pycore_init_import_warnings(tstate,sysmod);
done:
//Py_XDECREF()减少对象引用计数
//如果引用计数变为0,将销毁对象,回收内存
Py_XDECREF(sysmod);
returnstatus;
}pycore_init_types()函数负责初始化内置类型。
具体做了哪些事情呢?内置类型又是什么?我们知道,Python中一切皆对象。
数字、字符串、列表、函数、模块、帧、自定义类乃至内置类型都是Python对象。
所有Python对象都是PyObject结构或以PyObject作为第一个字段的其它C结构的一个实例。
PyObject有两个字段,第一个是Py_ssize_t类型的引用计数,第二个是PyTypeObject指针,指向对象类型。
下面是PyObject结构体的定义:typedefstruct_object{
_PyObject_HEAD_EXTRA//fordebuggingonly
Py_ssize_tob_refcnt;
PyTypeObject*ob_type;
}PyObject;而下面的是大家熟悉的float类型的结构体定义:typedefstruct{
PyObject_HEAD//一个宏,扩展为PyObjectob_base;
doubleob_fval;
}PyFloatObject;在C语言中,指向任意结构体的指针可以转换为指向该结构体第一个成员的指针,反过来也一样。
因此,由于Python对象的第一个成员都是PyObject,CPython可以将所有Python对象都当作PyObject处理。
你可以把它当作一种C语言中实现子类的方式。
这种做法的好处是实现了多态性,比方说,通过传递PyObject,可以将任意Python对象作为参数传给函数。
CPython之所以能借由PyObject完成许多操作,是因为Python对象由其类型所决定,而PyObject指定了Python对象的类型。
通过类型,CPython可以知道对象如何创建,如何计算哈希值,如何互相加减,如何调用,如何访问其属性,以及如何销毁等等。
类型本身也是Python对象,由PyTypeObject结构体表示。
所有的类型都属于PyType_Type类型,PyType_Type类型的类型指向它自身。
听起来比较复杂,看一个例子就清楚了:$./python.exe-q
>>>type([])
<class'list'>
>>>type(type([]))
<class'type'>
>>>type(type(type([])))
<class'type'>PyTypeObject的详细说明可以参考Python/C接口参考手册。
这里只给出相关结构体定义,读者对类型对象存储的信息有个大概概念就行。
//PyTypeObject类型定义
struct_typeobject{
PyObject_VAR_HEAD//扩展为
//PyObjectob_base;
//Py_ssize_tob_size;
constchar*tp_name;/*Forprinting,informat"<module>.<name>"*/
Py_ssize_ttp_basicsize,tp_itemsize;/*Forallocation*/
/*Methodstoimplementstandardoperations*/
destructortp_dealloc;
Py_ssize_ttp_vectorcall_offset;
getattrfunctp_getattr;
setattrfunctp_setattr;
PyAsyncMethods*tp_as_async;/*formerlyknownastp_compare(Python2)
ortp_reserved(Python3)*/
reprfunctp_repr;
/*Methodsuitesforstandardclasses*/
PyNumberMethods*tp_as_number;
PySequenceMethods*tp_as_sequence;
PyMappingMethods*tp_as_mapping;
/*Morestandardoperations(hereforbinarycompatibility)*/
hashfunctp_hash;
ternaryfunctp_call;
reprfunctp_str;
getattrofunctp_getattro;
setattrofunctp_setattro;
/*Functionstoaccessobjectasinput/outputbuffer*/
PyBufferProcs*tp_as_buffer;
/*Flagstodefinepresenceofoptional/expandedfeatures*/
unsignedlongtp_flags;
constchar*tp_doc;/*Documentationstring*/
/*Assignedmeaninginrelease2.0*/
/*callfunctionforallaccessibleobjects*/
traverseproctp_traverse;
/*deletereferencestocontainedobjects*/
inquirytp_clear;
/*Assignedmeaninginrelease2.1*/
/*richcomparisons*/
richcmpfunctp_richcompare;
/*weakreferenceenabler*/
Py_ssize_ttp_weaklistoffset;
/*Iterators*/
getiterfunctp_iter;
iternextfunctp_iternext;
/*Attributedescriptorandsubclassingstuff*/
structPyMethodDef*tp_methods;
structPyMemberDef*tp_members;
structPyGetSetDef*tp_getset;
struct_typeobject*tp_base;
PyObject*tp_dict;
descrgetfunctp_descr_get;
descrsetfunctp_descr_set;
Py_ssize_ttp_dictoffset;
initproctp_init;
allocfunctp_alloc;
newfunctp_new;
freefunctp_free;/*Low-levelfree-memoryroutine*/
inquirytp_is_gc;/*ForPyObject_IS_GC*/
PyObject*tp_bases;
PyObject*tp_mro;/*methodresolutionorder*/
PyObject*tp_cache;
PyObject*tp_subclasses;
PyObject*tp_weaklist;
destructortp_del;
/*Typeattributecacheversiontag.Addedinversion2.6*/
unsignedinttp_version_tag;
destructortp_finalize;
vectorcallfunctp_vectorcall;
};内置类型,如int、list等,是通过静态声明PyTypeObject实例实现的:PyTypeObjectPyList_Type={
PyVarObject_HEAD_INIT(&PyType_Type,0)
"list",
sizeof(PyListObject),
0,
(destructor)list_dealloc,/*tp_dealloc*/
0,/*tp_vectorcall_offset*/
0,/*tp_getattr*/
0,/*tp_setattr*/
0,/*tp_as_async*/
(reprfunc)list_repr,/*tp_repr*/
0,/*tp_as_number*/
&list_as_sequence,/*tp_as_sequence*/
&list_as_mapping,/*tp_as_mapping*/
PyObject_HashNotImplemented,/*tp_hash*/
0,/*tp_call*/
0,/*tp_str*/
PyObject_GenericGetAttr,/*tp_getattro*/
0,/*tp_setattro*/
0,/*tp_as_buffer*/
Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_GC|
Py_TPFLAGS_BASETYPE|Py_TPFLAGS_LIST_SUBCLASS,/*tp_flags*/
list___init____doc__,/*tp_doc*/
(traverseproc)list_traverse,/*tp_traverse*/
(inquiry)_list_clear,/*tp_clear*/
list_richcompare,/*tp_richcompare*/
0,/*tp_weaklistoffset*/
list_iter,/*tp_iter*/
0,/*tp_iternext*/
list_methods,/*tp_methods*/
0,/*tp_members*/
0,/*tp_getset*/
0,/*tp_base*/
0,/*tp_dict*/
0,/*tp_descr_get*/
0,/*tp_descr_set*/
0,/*tp_dictoffset*/
(initproc)list___init__,/*tp_init*/
PyType_GenericAlloc,/*tp_alloc*/
PyType_GenericNew,/*tp_new*/
PyObject_GC_Del,/*tp_free*/
.tp_vectorcall=list_vectorcall,
};类型声明之后,需要进行初始化。
比如,将__call__、__eq__等方法添加到该类型对应的字典中,并指向相应的tp_*函数。
这个初始化过程是通过调用PyType_Ready()函数完成的:PyStatus
_PyTypes_Init(void)
{
//添加"__hash__","__call_"等魔法函数
PyStatusstatus=_PyTypes_InitSlotDefs();
if(_PyStatus_EXCEPTION(status)){
returnstatus;
}
#defineINIT_TYPE(TYPE,NAME)
do{
if(PyType_Ready(TYPE)<0){
return_PyStatus_ERR("Can'tinitialize"NAME"type");
}
}while(0)
INIT_TYPE(&PyBaseObject_Type,"object");
INIT_TYPE(&PyType_Type,"type");
INIT_TYPE(&_PyWeakref_RefType,"weakref");
INIT_TYPE(&_PyWeakref_CallableProxyType,"callableweakrefproxy");
INIT_TYPE(&_PyWeakref_ProxyType,"weakrefproxy");
INIT_TYPE(&PyLong_Type,"int");
INIT_TYPE(&PyBool_Type,"bool");
INIT_TYPE(&PyByteArray_Type,"bytearray");
INIT_TYPE(&PyBytes_Type,"str");
INIT_TYPE(&PyList_Type,"list");
INIT_TYPE(&_PyNone_Type,"None");
INIT_TYPE(&_PyNotImplemented_Type,"NotImplemented");
INIT_TYPE(&PyTraceBack_Type,"traceback");
INIT_TYPE(&PySuper_Type,"super");
INIT_TYPE(&PyRange_Type,"range");
INIT_TYPE(&PyDict_Type,"dict");
INIT_TYPE(&PyDictKeys_Type,"dictkeys");
//…另外50种类型的初始化
return_PyStatus_OK();
#undefINIT_TYPE
}有些内置类型还会执行一些特殊的初始化操作。
例如,初始化int时,需要生成一些小整数,存放在interp->small_ints列表中,便于之后复用;初始化float时,需要判断浮点数在当前系统中的存储格式。
内置类型初始化完成后,pycore_interp_init()调用_PySys_Create()创建sys模块。
为什么sys模块需要第一个创建呢?这个模块当然是很重要的,它包含了命令行参数(sys.argv),模块搜索路径(sys.path),各种系统、实现相关参数(sys.version/sys.implementation/sys.thread_info等),以及可与解释器交互的各种函数(sys.addaudithook()/sys.settrace()等)。
但之所以要最先初始化这个模块,主要目的还是为了初始化sys.modules。
sys.modules指向interp->modules字典。
这个字典也是由_PySys_Create()创建的。
所有已导入的模块都会缓存在这里,搜索模块时,也会首先查找这个字典。
模块导入系统强依赖于sys.modules。
实际上,_PySys_Create()函数只完成了sys模块的部分初始化。
调用相关的数据,如sys.argv、sys._xoptions等,与路径相关的数据,如sys.path、sys.exec_prefix等,将在主初始化流程完成设置。
接下来,pycore_interp_init()调用pycore_init_builtins()执行内置模块的初始化。
内置模块的内容包括内置函数,如abs()、dir()、print()等,内置类型,如dict、int、str等,内置异常,如Exception、ValueError等,以及内置常数,如False、Ellipsis、None等。
内置函数本身是内置模块定义的一部分,而内置类型、异常、常数等则必须显式移入模块字典中。
运行代码时,frame->f_builtins将指向模块字典,从而可以搜索到这些内置名称。
这也是内置模块不需要手动引入的原因。
核心初始化的最后一步是调用pycore_init_import_warnings()函数。
你可能已经见识过Python的警告机制,比如:$./python.exe-q
>>>importimp
<stdin>:1:DeprecationWarning:theimpmoduleisdeprecatedinfavourofimportlib;…CPython中包含一些过滤器,可以忽略Warning,或将其升级为异常,或以各种形式将其展示给用户。
pycore_init_import_warnings()负责打开这些过滤器。
另外,这个函数还为内置模块与冻结模块准备好导入系统。
内置模块与冻结模块比较特殊。
它们都直接编译进Python可执行文件。
不过,内置模块是C语言实现的,而冻结模块则是用Python写的。
怎么把Python写的模块直接编译进可执行文件呢?办法是把模块的代码对象的二进制表示合并到C语言源码中。
而代码对象的二进制表示是通过Freeze工具生成的。
_frozen_importlib就是一个冻结模块,也是整个导入系统的核心部分。
Python代码中的import语句最终都会走到_frozen_importlib._find_and_load()函数。
为支持内置模块与冻结模块的导入,pycore_init_import_warnings()将调用init_importlib()函数,而该函数做的第一件事就是导入_frozen_importlib。
看上去,导入这个模块的动作本身就依赖于这个模块,而CPython避开了这个问题。
_frozen_importlib依赖于另外两个模块,一个是sys,以便访问sys.modules,另一个是_imp,负责底层导入函数的实现,包括用于创建内置模块与冻结模块的函数。
为避开依赖自身以导入自身的问题,这里通过init_importlib()函数直接创建_imp模块,然后调用_frozen_importlib._install(sys,_imp)函数将它与sys模块注入到_frozen_importlib中。
完成这个自启动过程后,核心初始化阶段也就宣告完成。
下一步是主初始化阶段,即pyinit_main()。
执行一些校验之后,该函数将调用init_interp_main()完成主要工作,这些工作可以总结如下:获取系统真实时间和单调时间(译者注:系统启动后经历的ticks),确保time.time(),time.monotonic(),time.perf_counter()等函数正常工作。
完成sys模块初始化,包括设置路径,如sys.path,sys.executable,sys.exec_prefix等,以及调用参数相关变量,如sys.argv,sys._xoptions等。
支持基于路径的(外部)模块导入。
初始化过程会导入一个冻结模块,importlib._bootstrap_external。
它支持基于sys.path的模块导入。
同时,另一个冻结模块,zipimport,也会被导入,以支持导入ZIP压缩格式的模块,也就是说,sys.path下的文件夹可以是以被压缩格式存在的。
规范文件系统与标准流的编码格式,设置编解码错误处理器。
设置默认的信号处理器,以处理进程接收到的SIGINT等系统信号。
用户可以通过signal模块自定义信号处理器。
导入io模块,初始化sys.stdin、sys.stdout、sys.stderr,本质上就是通过io.open()打开标准流对应的文件描述符。
将builtins.open设置为io.OpenWrapper,使用户可以直接使用这个内置函数。
创建__main__模块,将__main__.__builtins__设置为builtins,__main__.__loader__设置为_frozen_importlib.BuiltinImporter。
此时,__main__模块中还没有内容。
导入warnings、site模块,site模块会在sys.path中添加/usr/local/lib/python3.9/site-packages/相关路径。
将interp->runtime->initialized设置为1。
至此,CPython初始化完成。
下面,我们来看看Py_RunMain()。
2.2运行Python程序看上去,Py_RunMain()函数本身做的事情不多:int
Py_RunMain(void)
{
intexitcode=0;
pymain_run_python(&exitcode);
if(Py_FinalizeEx()<0){
/*Valueunlikelytobeconfusedwithanon-errorexitstatusor
otherspecialmeaning*/
exitcode=120;
}
//释放Py_FinalizeEx()没有释放的内存
pymain_free();
if(_Py_UnhandledKeyboardInterrupt){
exitcode=exit_sigint();
}
returnexitcode;
}Py_RunMain()首先调用pymain_run_python()运行Python,然后调用Py_FinalizeEx()执行去初始化。
这个函数释放了大多数CPython能释放的内存,剩余部分由pymain_free()释放。
另外,Py_FinalizeEx()还会调用各种退出函数,包括用户通过atexit模块注册的退出函数。
我们知道,运行Python代码有几种方式,即:交互式:$./cpython/python.exe
>>>importsys
>>>sys.path[:1]
['']作为标准输入流:$echo"importsys;print(sys.path[:1])"|./cpython/python.exe
['']命令形式:$./cpython/python.exe-c"importsys;print(sys.path[:1])"
['']脚本形式:$./cpython/python.exe03/print_path0.py
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes/03']以模块运行:$./cpython/python.exe-m03.print_path0
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes']以及,可能比较少见的,把包作为脚本运行(print_path0_package是一个文件夹,其中包含__main__.py文件):$./cpython/python.exe03/print_path0_package
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes/03/print_path0_package']注意,我们是在cpython/文件夹之外执行指令的,可以看到,不同调用模式下,sys.path[0]有不同的值。
我们下一个要看的函数,pymain_run_python(),会计算sys.path[0]的值,并以不同的模式运行Python:staticvoid
pymain_run_python(int*exitcode)
{
PyInterpreterState*interp=_PyInterpreterState_GET();
PyConfig*config=(PyConfig*)_PyInterpreterState_GetConfig(interp);
//预设`sys.path`
PyObject*main_importer_path=NULL;
if(config->run_filename!=NULL){
//Calculatethesearchpathforthecasewhenthefilenameisapackage
//(ex:directoryorZIPfile)whichcontains__main__.py,storeitin`main_importer_path`.
//Otherwise,left`main_importer_path`unchanged.
//Handleothercaseslater.
if(pymain_get_importer(config->run_filename,&main_importer_path,
exitcode)){
return;
}
}
if(main_importer_path!=NULL){
if(pymain_sys_path_add_path0(interp,main_importer_path)<0){
gotoerror;
}
}
elseif(!config->isolated){
PyObject*path0=NULL;
//计算要添加到`sys.path`的模块搜索路径
//如果以脚本运行,即脚本所在文件夹
//如果以模块运行(-m),即当前所在文件夹
//否则为空字符串
intres=_PyPathConfig_ComputeSysPath0(&config->argv,&path0);
if(res<0){
gotoerror;
}
if(res>0){
if(pymain_sys_path_add_path0(interp,path0)<0){
Py_DECREF(path0);
gotoerror;
}
Py_DECREF(path0);
}
}
PyCompilerFlagscf=_PyCompilerFlags_INIT;
//在交互模式,打印版本与平台信息
pymain_header(config);
//在交互模式,导入`readline`模块,
//支持自动补完、行内编辑、历史命令等功能
pymain_import_readline(config);
//按调用模式运行Python(如脚本,-m,-c等)
if(config->run_command){
*exitcode=pymain_run_command(config->run_command,&cf);
}
elseif(config->run_module){
*exitcode=pymain_run_module(config->run_module,1);
}
elseif(main_importer_path!=NULL){
*exitcode=pymain_run_module(L"__main__",0);
}
elseif(config->run_filename!=NULL){
*exitcode=pymain_run_file(config,&cf);
}
else{
*exitcode=pymain_run_stdin(config,&cf);
}
//程序执行后进入交互模式
//即支持`-i`、`PYTHONINSPECT`选项
pymain_repl(config,&cf,exitcode);
gotodone;
error:
*exitcode=pymain_exit_err_print();
done:
Py_XDECREF(main_importer_path);
}这里,我们以脚本模式为例。
下一步将执行pymain_run_file()函数,检查文件是否能被打开,是不是一个文件夹等,然后调用PyRun_AnyFileExFlags(),如果文件是一个终端(isatty(fd)返回1),程序进入交互模式:$./python.exe/dev/ttys000
>>>1+1
2否则,调用PyRun_SimpleFileExFlags()。
你可能对模块中__pycache__文件夹下的.pyc文件已经很熟悉了。
.pyc文件中的是编译好的源码,即该模块所包含的代码对象。
由于.pyc文件的存在,模块不必在每次导入时都重新编译——我想,这个你已经知道了。
不过,你知道我们可以直接运行.pyc文件吗:$./cpython/python.exe03/__pycache__/print_path0.cpython-39.pyc
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes/03/__pycache__']PyRun_SimpleFileExFlags()函数会检查用户执行的是不是.pyc文件,这个.pyc文件是不是匹配当前CPython版本,如果匹配,则执行run_pyc_file()函数。
如果不是.pyc文件,它将调用PyRun_FileExFlags()函数。
最重要的是,PyRun_SimpleFileExFlags()还将导入__main__模块,并将它的字典传入PyRun_FileExFlags(),作为文件执行时的全局与本地命名空间:int
PyRun_SimpleFileExFlags(FILE*fp,constchar*filename,intcloseit,
PyCompilerFlags*flags)
{
PyObject*m,*d,*v;
constchar*ext;
intset_file_name=0,ret=-1;
size_tlen;
m=PyImport_AddModule("__main__");
if(m==NULL)
return-1;
Py_INCREF(m);
d=PyModule_GetDict(m);
if(PyDict_GetItemString(d,"__file__")==NULL){
PyObject*f;
f=PyUnicode_DecodeFSDefault(filename);
if(f==NULL)
gotodone;
if(PyDict_SetItemString(d,"__file__",f)<0){
Py_DECREF(f);
gotodone;
}
if(PyDict_SetItemString(d,"__cached__",Py_None)<0){
Py_DECREF(f);
gotodone;
}
set_file_name=1;
Py_DECREF(f);
}
//检查是不是.pyc文件
len=strlen(filename);
ext=filename+len-(len>4?4:0);
if(maybe_pyc_file(fp,filename,ext,closeit)){
FILE*pyc_fp;
/*Trytorunapycfile.First,re-openinbinary*/
if(closeit)
fclose(fp);
if((pyc_fp=_Py_fopen(filename,"rb"))==NULL){
fprintf(stderr,"python:Can'treopen.pycfilen");
gotodone;
}
if(set_main_loader(d,filename,"SourcelessFileLoader")<0){
fprintf(stderr,"python:failedtoset__main__.__loader__n");
ret=-1;
fclose(pyc_fp);
gotodone;
}
v=run_pyc_file(pyc_fp,filename,d,d,flags);
}else{
/*Whenrunningfromstdin,leave__main__.__loader__alone*/
if(strcmp(filename,"<stdin>")!=0&&
set_main_loader(d,filename,"SourceFileLoader")<0){
fprintf(stderr,"python:failedtoset__main__.__loader__n");
ret=-1;
gotodone;
}
v=PyRun_FileExFlags(fp,filename,Py_file_input,d,d,
closeit,flags);
}
flush_io();
if(v==NULL){
Py_CLEAR(m);
PyErr_Print();
gotodone;
}
Py_DECREF(v);
ret=0;
done:
if(set_file_name){
if(PyDict_DelItemString(d,"__file__")){
PyErr_Clear();
}
if(PyDict_DelItemString(d,"__cached__")){
PyErr_Clear();
}
}
Py_XDECREF(m);
returnret;
}PyRun_FileExFlags()会执行编译过程,运行解析器,获取模块的AST,并调用run_mod()运行AST。
同时,它还负责创建PyArena,供CPython保存小对象(小于等于512字节的对象)。
PyObject*
PyRun_FileExFlags(FILE*fp,constchar*filename_str,intstart,PyObject*globals,
PyObject*locals,intcloseit,PyCompilerFlags*flags)
{
PyObject*ret=NULL;
mod_tymod;
PyArena*arena=NULL;
PyObject*filename;
intuse_peg=_PyInterpreterState_GET()->config._use_peg_parser;
filename=PyUnicode_DecodeFSDefault(filename_str);
if(filename==NULL)
gotoexit;
arena=PyArena_New();
if(arena==NULL)
gotoexit;
//运行解析器
//默认使用新版PEG解析器
//传入`-Xoldparser`可以使用旧版解析器
//`mod`表示模块,也是AST的根节点
if(use_peg){
mod=PyPegen_ASTFromFileObject(fp,filename,start,NULL,NULL,NULL,
flags,NULL,arena);
}
else{
mod=PyParser_ASTFromFileObject(fp,filename,NULL,start,0,0,
flags,NULL,arena);
}
if(closeit)
fclose(fp);
if(mod==NULL){
gotoexit;
}
//编译AST并运行
ret=run_mod(mod,filename,globals,locals,flags,arena);
exit:
Py_XDECREF(filename);
if(arena!=NULL)
PyArena_Free(arena);
returnret;
}run_mod()调用PyAST_CompileObject()以运行编译器,获取模块的代码对象,然后调用run_eval_code_obj()执行代码对象。
期间还抛出exec事件——这是CPython将运行时事件通知审计工具的方式。
相关机制可以参考PEP578。
staticPyObject*
run_mod(mod_tymod,PyObject*filename,PyObject*globals,PyObject*locals,
PyCompilerFlags*flags,PyArena*arena)
{
PyThreadState*tstate=_PyThreadState_GET();
PyCodeObject*co=PyAST_CompileObject(mod,filename,flags,-1,arena);
if(co==NULL)
returnNULL;
if(_PySys_Audit(tstate,"exec","O",co)<0){
Py_DECREF(co);
returnNULL;
}
PyObject*v=run_eval_code_obj(tstate,co,globals,locals);
Py_DECREF(co);
returnv;
}在第二篇文章中,我们已经看到编译器的工作方式:构建符号表创建基本块的CFG将CFG集成为代码对象这正是PyAST_CompileObject()所做的工作,因此,这个函数我们不再过多讨论。
通过一系列的调用,run_eval_code_obj()最终会到达_PyEval_EvalCode()。
我把整个调用链粘贴过来,方便大家看到参数是怎么一路传过去的:staticPyObject*
run_eval_code_obj(PyThreadState*tstate,PyCodeObject*co,PyObject*globals,PyObject*locals)
{
PyObject*v;
//处理CPython被嵌入使用的情况,我们可以忽略
/*
*Weexplicitlyre-initialize_Py_UnhandledKeyboardInterrupteveryeval
*_justincase_someoneiscallingintoanembeddedPythonwherethey
*don'tcareaboutanuncaughtKeyboardInterruptexception(whydidn'tthey
*leaveconfig.install_signal_handlerssetto0?!?)butthenlatercall
*Py_Main()itself(which_checks_thisflaganddieswithasignalafter
*itsinterpreterexits).Wedon'twantapreviousembeddedinterpreter's
*uncaughtexceptiontotriggeranunexplainedsignalexitfromafuture
*Py_Main()basedone.
*/
_Py_UnhandledKeyboardInterrupt=0;
/*Setglobals['__builtins__']ifitdoesn'texist*/
//在我们的场景中,已经在主初始化时设置为`builtins`模块
if(globals!=NULL&&PyDict_GetItemString(globals,"__builtins__")==NULL){
if(PyDict_SetItemString(globals,"__builtins__",
tstate->interp->builtins)<0){
returnNULL;
}
}
v=PyEval_EvalCode((PyObject*)co,globals,locals);
if(!v&&_PyErr_Occurred(tstate)==PyExc_KeyboardInterrupt){
_Py_UnhandledKeyboardInterrupt=1;
}
returnv;
}PyObject*
PyEval_EvalCode(PyObject*co,PyObject*globals,PyObject*locals)
{
returnPyEval_EvalCodeEx(co,
globals,locals,
(PyObject**)NULL,0,
(PyObject**)NULL,0,
(PyObject**)NULL,0,
NULL,NULL);
}PyObject*
PyEval_EvalCodeEx(PyObject*_co,PyObject*globals,PyObject*locals,
PyObject*const*args,intargcount,
PyObject*const*kws,intkwcount,
PyObject*const*defs,intdefcount,
PyObject*kwdefs,PyObject*closure)
{
return_PyEval_EvalCodeWithName(_co,globals,locals,
args,argcount,
kws,kws!=NULL?kws+1:NULL,
kwcount,2,
defs,defcount,
kwdefs,closure,
NULL,NULL);
}PyObject*
_PyEval_EvalCodeWithName(PyObject*_co,PyObject*globals,PyObject*locals,
PyObject*const*args,Py_ssize_targcount,
PyObject*const*kwnames,PyObject*const*kwargs,
Py_ssize_tkwcount,intkwstep,
PyObject*const*defs,Py_ssize_tdefcount,
PyObject*kwdefs,PyObject*closure,
PyObject*name,PyObject*qualname)
{
PyThreadState*tstate=_PyThreadState_GET();
return_PyEval_EvalCode(tstate,_co,globals,locals,
args,argcount,
kwnames,kwargs,
kwcount,kwstep,
defs,defcount,
kwdefs,closure,
name,qualname);
}我们说过,代码对象表示代码要执行的动作,而要执行一个代码对象,还必须依赖CPython创建的相应状态,即帧对象。
_PyEval_EvalCode()会根据参数为指定代码对象创建帧对象。
在我们的场景中,大多参数都是NULL,因此,创建帧对象的工作量不大。
而在CPython根据不同传入参数执行代码对象的时候,则有大量工作要做。
也因为这个原因,_PyEval_EvalCode()函数长达300行。
我们会在之后的文章中讨论这些代码所做的事情,暂时可以跳过它们,只要注意到它最终调用_PyEval_EvalFrame()对帧对象求值即可:PyObject*
_PyEval_EvalCode(PyThreadState*tstate,
PyObject*_co,PyObject*globals,PyObject*locals,
PyObject*const*args,Py_ssize_targcount,
PyObject*const*kwnames,PyObject*const*kwargs,
Py_ssize_tkwcount,intkwstep,
PyObject*const*defs,Py_ssize_tdefcount,
PyObject*kwdefs,PyObject*closure,
PyObject*name,PyObject*qualname)
{
assert(is_tstate_valid(tstate));
PyCodeObject*co=(PyCodeObject*)_co;
PyFrameObject*f;
PyObject*retval=NULL;
PyObject**fastlocals,**freevars;
PyObject*x,*u;
constPy_ssize_ttotal_args=co->co_argcount+co->co_kwonlyargcount;
Py_ssize_ti,j,n;
PyObject*kwdict;
if(globals==NULL){
_PyErr_SetString(tstate,PyExc_SystemError,
"PyEval_EvalCodeEx:NULLglobals");
returnNULL;
}
/*Createtheframe*/
f=_PyFrame_New_NoTrack(tstate,co,globals,locals);
if(f==NULL){
returnNULL;
}
fastlocals=f->f_localsplus;
freevars=f->f_localsplus+co->co_nlocals;
/*Createadictionaryforkeywordparameters(**kwags)*/
if(co->co_flags&CO_VARKEYWORDS){
kwdict=PyDict_New();
if(kwdict==NULL)
gotofail;
i=total_args;
if(co->co_flags&CO_VARARGS){
i++;
}
SETLOCAL(i,kwdict);
}
else{
kwdict=NULL;
}
/*Copyallpositionalargumentsintolocalvariables*/
if(argcount>co->co_argcount){
n=co->co_argcount;
}
else{
n=argcount;
}
for(j=0;j<n;j++){
x=args[j];
Py_INCREF(x);
SETLOCAL(j,x);
}
/*Packotherpositionalargumentsintothe*argsargument*/
if(co->co_flags&CO_VARARGS){
u=_PyTuple_FromArray(args+n,argcount-n);
if(u==NULL){
gotofail;
}
SETLOCAL(total_args,u);
}
/*Handlekeywordargumentspassedastwostridedarrays*/
kwcount*=kwstep;
for(i=0;i<kwcount;i+=kwstep){
PyObject**co_varnames;
PyObject*keyword=kwnames[i];
PyObject*value=kwargs[i];
Py_ssize_tj;
if(keyword==NULL||!PyUnicode_Check(keyword)){
_PyErr_Format(tstate,PyExc_TypeError,
"%U()keywordsmustbestrings",
co->co_name);
gotofail;
}
/*Speedhack:dorawpointercompares.Asnamesare
normallyinternedthisshouldalmostalwayshit.*/
co_varnames=((PyTupleObject*)(co->co_varnames))->ob_item;
for(j=co->co_posonlyargcount;j<total_args;j++){
PyObject*name=co_varnames[j];
if(name==keyword){
gotokw_found;
}
}
/*Slowfallback,justincase*/
for(j=co->co_posonlyargcount;j<total_args;j++){
PyObject*name=co_varnames[j];
intcmp=PyObject_RichCompareBool(keyword,name,Py_EQ);
if(cmp>0){
gotokw_found;
}
elseif(cmp<0){
gotofail;
}
}
assert(j>=total_args);
if(kwdict==NULL){
if(co->co_posonlyargcount
&&positional_only_passed_as_keyword(tstate,co,
kwcount,kwnames))
{
gotofail;
}
_PyErr_Format(tstate,PyExc_TypeError,
"%U()gotanunexpectedkeywordargument'%S'",
co->co_name,keyword);
gotofail;
}
if(PyDict_SetItem(kwdict,keyword,value)==-1){
gotofail;
}
continue;
kw_found:
if(GETLOCAL(j)!=NULL){
_PyErr_Format(tstate,PyExc_TypeError,
"%U()gotmultiplevaluesforargument'%S'",
co->co_name,keyword);
gotofail;
}
Py_INCREF(value);
SETLOCAL(j,value);
}
/*Checkthenumberofpositionalarguments*/
if((argcount>co->co_argcount)&&!(co->co_flags&CO_VARARGS)){
too_many_positional(tstate,co,argcount,defcount,fastlocals);
gotofail;
}
/*Addmissingpositionalarguments(copydefaultvaluesfromdefs)*/
if(argcount<co->co_argcount){
Py_ssize_tm=co->co_argcount-defcount;
Py_ssize_tmissing=0;
for(i=argcount;i<m;i++){
if(GETLOCAL(i)==NULL){
missing++;
}
}
if(missing){
missing_arguments(tstate,co,missing,defcount,fastlocals);
gotofail;
}
if(n>m)
i=n-m;
else
i=0;
for(;i<defcount;i++){
if(GETLOCAL(m+i)==NULL){
PyObject*def=defs[i];
Py_INCREF(def);
SETLOCAL(m+i,def);
}
}
}
/*Addmissingkeywordarguments(copydefaultvaluesfromkwdefs)*/
if(co->co_kwonlyargcount>0){
Py_ssize_tmissing=0;
for(i=co->co_argcount;i<total_args;i++){
PyObject*name;
if(GETLOCAL(i)!=NULL)
continue;
name=PyTuple_GET_ITEM(co->co_varnames,i);
if(kwdefs!=NULL){
PyObject*def=PyDict_GetItemWithError(kwdefs,name);
if(def){
Py_INCREF(def);
SETLOCAL(i,def);
continue;
}
elseif(_PyErr_Occurred(tstate)){
gotofail;
}
}
missing++;
}
if(missing){
missing_arguments(tstate,co,missing,-1,fastlocals);
gotofail;
}
}
/*Allocateandinitializestorageforcellvars,andcopyfree
varsintoframe.*/
for(i=0;i<PyTuple_GET_SIZE(co->co_cellvars);++i){
PyObject*c;
Py_ssize_targ;
/*Possiblyaccountforthecellvariablebeinganargument.*/
if(co->co_cell2arg!=NULL&&
(arg=co->co_cell2arg[i])!=CO_CELL_NOT_AN_ARG){
c=PyCell_New(GETLOCAL(arg));
/*Clearthelocalcopy.*/
SETLOCAL(arg,NULL);
}
else{
c=PyCell_New(NULL);
}
if(c==NULL)
gotofail;
SETLOCAL(co->co_nlocals+i,c);
}
/*Copyclosurevariablestofreevariables*/
for(i=0;i<PyTuple_GET_SIZE(co->co_freevars);++i){
PyObject*o=PyTuple_GET_ITEM(closure,i);
Py_INCREF(o);
freevars[PyTuple_GET_SIZE(co->co_cellvars)+i]=o;
}
/*Handlegenerator/coroutine/asynchronousgenerator*/
if(co->co_flags&(CO_GENERATOR|CO_COROUTINE|CO_ASYNC_GENERATOR)){
PyObject*gen;
intis_coro=co->co_flags&CO_COROUTINE;
/*Don'tneedtokeepthereferencetof_back,itwillbeset
*whenthegeneratorisresumed.*/
Py_CLEAR(f->f_back);
/*Createanewgeneratorthatownsthereadytorunframe
*andreturnthatasthevalue.*/
if(is_coro){
gen=PyCoro_New(f,name,qualname);
}elseif(co->co_flags&CO_ASYNC_GENERATOR){
gen=PyAsyncGen_New(f,name,qualname);
}else{
gen=PyGen_NewWithQualName(f,name,qualname);
}
if(gen==NULL){
returnNULL;
}
_PyObject_GC_TRACK(f);
returngen;
}
retval=_PyEval_EvalFrame(tstate,f,0);
fail:/*Jumpherefrompreludeonfailure*/
/*decref'ingtheframecancause__del__methodstogetinvoked,
whichcancallbackintoPython.Whilewe'redonewiththe
currentPythonframe(f),theassociatedCstackisstillinuse,
sorecursion_depthmustbeboostedfortheduration.
*/
if(Py_REFCNT(f)>1){
Py_DECREF(f);
_PyObject_GC_TRACK(f);
}
else{
++tstate->recursion_depth;
Py_DECREF(f);
–tstate->recursion_depth;
}
returnretval;
}_PyEval_EvalFrame()封装了帧求值函数interp->eval_frame()。
其实,用户可以自定义帧求值函数。
什么情况下需要自定义呢?比如说,为了添加一个JIT编译器,要将编译的机器码保存在代码对象中并执行的时候。
这个功能是由PEP523在CPython3.6引入的。
interp->eval_frame()默认为_PyEval_EvalFrameDefault()。
这个函数在Python/ceval.c文件中定义,代码将近3000行。
不过,今天,我们只对其中的第1336行感兴趣,这行代码我们已经找了很久了:求值循环。
3.总结本篇讨论了很多内容。
首先是CPython项目的概览,然后编译CPython,跟着源码一步步学习各个初始化阶段。
希望这篇文章让你对CPython开始解析字节码前的工作有了大概的了解。
至于后面的流程,则是下一篇文章的内容。
同时,为巩固今天的内容,了解更多有意思的东西,强烈推荐大家自己花点时间看看CPython源码。
我相信,读完这篇文章后,你会有许多疑问,带着这些疑问去读源码是个不错的选择。
祝大家探索愉快!