译|Python幕后(3):漫步CPython源码
liebian365 2024-11-10 11:59 11 浏览 0 评论
这是一篇译文,原文地址: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 函数开始,一步步往下,直到求值循环(evaluation loop),也就是运行 Python 字节码的地方。
我们并不需要理解每一行代码,而是重点关注一些有意思的地方,争取对 Python 程序的启动过程有一个基本概念。
另外有两点说明。一是,我们只深入讨论部分函数而概览其它,不过,我会按执行顺序来讲解。二是,除了极少数结构体定义,我会按代码库中的原貌呈现代码,唯一的改动就是增加一些说明性的注释。在后文代码中,多行注释 /**/ 都是原来就有的,单行注释 // 是我新增的。
现在,让我们开启 CPython 源码之旅吧。
1. 获取 CPython 源码
首先,把 CPython 代码库下载下来:
$ git clone https://github.com/python/cpython/ && cd cpython
目前, master 分支上的是 CPython 3.10。我们要看的是最新稳定版本,也就是 CPython 3.9。先切换分支:
$ git checkout 3.9
根目录下,可以看到下面这些内容:
$ ls -p
CODE_OF_CONDUCT.md Objects/ config.sub
Doc/ PC/ configure
Grammar/ PCbuild/ configure.ac
Include/ Parser/ install-sh
LICENSE Programs/ m4/
Lib/ Python/ netlify.toml
Mac/ README.rst pyconfig.h.in
Makefile.pre.in Tools/ 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。
比如,要理解窥孔优化器(peephole optimizer)到底优化了哪些东西,可以查看 Lib/test/test_peepholer.py 文件,要理解某部分代码的功能,可以注释掉那部分代码,重新编译 CPython,再运行测试:
$ ./python.exe -m test test_peepholer
看看哪些用例失败了。
理想情况下,编译 CPython 只需要执行两条命令:
$ ./configure
$ make -j -s
make 命令将生成 python 可执行文件。如果你在 Mac 系统下看到 python.exe,不要觉得惊讶,这里的 .exe 后缀只是用于在大小写不敏感的文件系统中区分可执行文件与 Python/ 文件夹而已。更多编译相关信息,可以查看开发者手册。
现在,我们可以自豪地说,我们已经构建了自有版本的 CPython 了:
$ ./python.exe
Python 3.9.0+ (heads/3.9-dirty:20bdeedfb4, Oct 10 2020, 16:55:24)
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 2 ** 16
65536
2. 源码
正如所有 C 程序一样,CPython 的执行入口是 Python/python.c 中的一个 main() 函数:
/* Minimal main program -- everything is loaded from the library */
#include "Python.h"
#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
return Py_Main(argc, argv);
}
#else
int
main(int argc, char **argv)
{
return Py_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(int argc, wchar_t **argv)
{
_PyArgv args = {
.argc = argc,
.use_bytes_argv = 0,
.bytes_argv = NULL,
.wchar_argv = argv};
return pymain_main(&args);
}
int
Py_BytesMain(int argc, char **argv)
{
_PyArgv args = {
.argc = argc,
.use_bytes_argv = 1,
.bytes_argv = argv,
.wchar_argv = NULL};
return pymain_main(&args);
}
我们可以看下 pymain_main() 函数。初看上去,它好像也没做什么事情:
static int
pymain_main(_PyArgv *args)
{
PyStatus status = pymain_init(args);
if (_PyStatus_IS_EXIT(status)) {
pymain_free();
return status.exitcode;
}
if (_PyStatus_EXCEPTION(status)) {
pymain_exit_error(status);
}
return Py_RunMain();
}
上一篇中,我们看到,在一个 Python 程序执行前,CPython 需要做很多编译工作。其实,在编译之前,CPython 就已经做了很多事情了,这些事情组成了 CPython 初始化过程。在第一篇中,我们曾说,CPython 的工作包括三个阶段:
- 初始化
- 编译,以及
- 解释
因此,pymain_main() 首先调用 pymain_init() 执行初始化,然后调用 Py_RunMain() 进行下一步工作。问题来了:CPython 在初始化阶段做了哪些事情呢?我们可以推测一下,至少,它要做以下几件事:
- 根据操作系统的不同,为参数、环境变量、标准输入输出流以及文件系统选择一种合适的编码方式
- 解析命令行参数,读取环境变量,确定运行方式
- 初始化运行时状态、主解释器状态以及主线程状态
- 初始化内置类型与内置模块
- 初始化 sys 模块
- 准备好模块导入系统
- 创建 __main__ 模块
在进入 pymain_init() 函数前,我们先具体讨论下初始化过程。
2.1 初始化
CPython 3.8 之后,初始化被分为三个阶段:
- 预初始化(preinitialization)
- 核心初始化(core initialization)
- 主初始化(main initialization)
预初始化阶段负责初始化运行时状态,准备默认的内存分配器,完成基本配置。这里还看不到 Python 的影子。
核心初始化阶段负责初始化解释器状态、主线程状态、内置类型与异常、内置模块,准备 sys 模块与模块导入系统。此时,我们已经可以使用 Python 的“核心”部分了。不过,还是有些功能没准备好,例如,sys 模块只有部分功能、只支持导入内置模块与冻结模块(frozen modules,译者注:由 Python 实现,但字节码封装在可执行文件中,不需要解释器即可执行的模块)等。
主初始化阶段后,CPython 才完成所有初始化过程,可以进行编译或解释工作了。
把初始化过程分为三个阶段有什么好处呢?简单地说,这样可以更方便地配置 CPython。比如,用户可以在核心初始化阶段覆盖相关路径,从而使用自定义的内存分配器。
当然,CPython 自己不需要再“自定义”什么东西,但对使用 Python/C 接口的人来说,这种能力是很重要的。PEP432 和 PEP587 具体地说明了多阶段初始化的优势。
pymain_init() 函数负责预初始化,然后调用 Py_InitializeFromConfig() 进入核心初始化和主初始化阶段。
static PyStatus
pymain_init(const _PyArgv *args)
{
PyStatus status;
// 初始化运行时状态
status = _PyRuntime_Initialize();
if (_PyStatus_EXCEPTION(status)) {
return status;
}
// 初始化默认配置
PyPreConfig preconfig;
PyPreConfig_InitPythonConfig(&preconfig);
// 预初始化
status = _Py_PreInitializeFromPyArgv(&preconfig, args);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
// 预初始化完成,为下一个初始化阶段准备参数
// 初始化默认配置
PyConfig config;
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)) {
goto done;
}
// 执行核心初始化和主初始化
status = Py_InitializeFromConfig(&config);
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = _PyStatus_OK();
done:
PyConfig_Clear(&config);
return status;
}
_PyRuntime_Initialize() 负责初始化运行时状态,运行时状态存储在 _PyRuntime 全局变量中,它的结构体定义如下:
/* Full Python runtime state */
typedef struct pyruntimestate {
/* Is running Py_PreInitialize()? */
int preinitializing;
/* Is Python preinitialized? Set to 1 by Py_PreInitialize() */
int preinitialized;
/* Is Python core initialized? Set to 1 by _Py_InitializeCore() */
int core_initialized;
/* Is Python fully initialized? Set to 1 by Py_Initialize() */
int initialized;
/* Set by Py_FinalizeEx(). Only reset to NULL if Py_Initialize() is called again. */
_Py_atomic_address _finalizing;
struct pyinterpreters {
PyThread_type_lock mutex;
PyInterpreterState *head;
PyInterpreterState *main;
int64_t next_id;
} interpreters;
unsigned long main_thread;
struct _ceval_runtime_state ceval;
struct _gilstate_runtime_state gilstate;
PyPreConfig preconfig;
// ... 后面是一些暂时可以忽略的东西
} _PyRuntimeState;
结构体最后一个字段是 preconfig,负责保存 CPython 预初始化相关配置,同时也用于之后两个阶段。下面是它的类型定义:
typedef struct {
int _config_init; /* _PyConfigInitEnum value */
/* Parse Py_PreInitializeFromBytesArgs() arguments?
See PyConfig.parse_argv */
int parse_argv;
/* If greater than 0, enable isolated mode: sys.path contains
neither the script's directory nor the user's site-packages directory.
Set to 1 by the -I command line option. If set to -1 (default), inherit
Py_IsolatedFlag value. */
int isolated;
/* If greater than 0: use environment variables.
Set to 0 by -E command line option. If set to -1 (default), it is
set to !Py_IgnoreEnvironmentFlag. */
int use_environment;
/* Set the LC_CTYPE locale to the user preferred locale? If equals to 0,
set coerce_c_locale and coerce_c_locale_warn to 0. */
int configure_locale;
/* Coerce the LC_CTYPE locale if it's equal to "C"? (PEP 538)
Set to 0 by PYTHONCOERCECLOCALE=0. Set to 1 by PYTHONCOERCECLOCALE=1.
Set to 2 if the user preferred LC_CTYPE locale is "C".
If it is equal to 1, LC_CTYPE locale is read to decide if it should be
coerced or not (ex: PYTHONCOERCECLOCALE=1). Internally, it is set to 2
if the LC_CTYPE locale must be coerced.
Disable by default (set to 0). Set it to -1 to let Python decide if it
should be enabled or not. */
int coerce_c_locale;
/* Emit a warning if the LC_CTYPE locale is coerced?
Set to 1 by PYTHONCOERCECLOCALE=warn.
Disable by default (set to 0). Set it to -1 to let Python decide if it
should be enabled or not. */
int coerce_c_locale_warn;
#ifdef MS_WINDOWS
/* If greater than 1, use the "mbcs" encoding instead of the UTF-8
encoding for the filesystem encoding.
Set to 1 if the PYTHONLEGACYWINDOWSFSENCODING environment variable is
set to a non-empty string. If set to -1 (default), inherit
Py_LegacyWindowsFSEncodingFlag value.
See PEP 529 for more details. */
int legacy_windows_fs_encoding;
#endif
/* Enable UTF-8 mode? (PEP 540)
Disabled by default (equals to 0).
Set to 1 by "-X utf8" and "-X utf8=1" command line options.
Set to 1 by PYTHONUTF8=1 environment variable.
Set to 0 by "-X utf8=0" and PYTHONUTF8=0.
If equals to -1, it is set to 1 if the LC_CTYPE locale is "C" or
"POSIX", otherwise it is set to 0. Inherit Py_UTF8Mode value value. */
int utf8_mode;
/* If non-zero, enable the Python Development Mode.
Set to 1 by the -X dev command line option. Set by the PYTHONDEVMODE
environment variable. */
int dev_mode;
/* Memory allocator: PYTHONMALLOC env var.
See PyMemAllocatorName for valid values. */
int allocator;
} 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 ---------------------------------------------- */
typedef struct {
int _config_init; /* _PyConfigInitEnum value */
int isolated; /* Isolated mode? see PyPreConfig.isolated */
int use_environment; /* Use environment variables? see PyPreConfig.use_environment */
int dev_mode; /* Python Development Mode? See PyPreConfig.dev_mode */
/* Install signal handlers? Yes by default. */
int install_signal_handlers;
int use_hash_seed; /* PYTHONHASHSEED=x */
unsigned long hash_seed;
/* Enable faulthandler?
Set to 1 by -X faulthandler and PYTHONFAULTHANDLER. -1 means unset. */
int faulthandler;
/* Enable PEG parser?
1 by default, set to 0 by -X oldparser and PYTHONOLDPARSER */
int _use_peg_parser;
/* Enable tracemalloc?
Set by -X tracemalloc=N and PYTHONTRACEMALLOC. -1 means unset */
int tracemalloc;
int import_time; /* PYTHONPROFILEIMPORTTIME, -X importtime */
int show_ref_count; /* -X showrefcount */
int dump_refs; /* PYTHONDUMPREFS */
int malloc_stats; /* PYTHONMALLOCSTATS */
/* Python filesystem encoding and error handler:
sys.getfilesystemencoding() and sys.getfilesystemencodeerrors().
Default encoding and error handler:
* if Py_SetStandardStreamEncoding() has been called: they have the
highest priority;
* PYTHONIOENCODING environment variable;
* The UTF-8 Mode uses UTF-8/surrogateescape;
* If Python forces the usage of the ASCII encoding (ex: C locale
or POSIX locale on FreeBSD or HP-UX), use ASCII/surrogateescape;
* locale encoding: ANSI code page on Windows, UTF-8 on Android and
VxWorks, LC_CTYPE locale encoding on other platforms;
* On Windows, "surrogateescape" error handler;
* "surrogateescape" error handler if the LC_CTYPE locale is "C" or "POSIX";
* "surrogateescape" error handler if the LC_CTYPE locale has been coerced
(PEP 538);
* "strict" error handler.
Supported error handlers: "strict", "surrogateescape" and
"surrogatepass". The surrogatepass error handler is only supported
if Py_DecodeLocale() and Py_EncodeLocale() use directly the UTF-8 codec;
it's only used on Windows.
initfsencoding() updates the encoding to the Python codec name.
For example, "ANSI_X3.4-1968" is replaced with "ascii".
On Windows, sys._enablelegacywindowsfsencoding() sets the
encoding/errors to mbcs/replace at runtime.
See Py_FileSystemDefaultEncoding and Py_FileSystemDefaultEncodeErrors.
*/
wchar_t *filesystem_encoding;
wchar_t *filesystem_errors;
wchar_t *pycache_prefix; /* PYTHONPYCACHEPREFIX, -X pycache_prefix=PATH */
int parse_argv; /* Parse argv command line arguments? */
/* Command line arguments (sys.argv).
Set parse_argv to 1 to parse argv as Python command line arguments
and then strip Python arguments from argv.
If argv is empty, an empty string is added to ensure that sys.argv
always exists and is never empty. */
PyWideStringList argv;
/* Program name:
- If Py_SetProgramName() was called, use its value.
- On macOS, use PYTHONEXECUTABLE environment variable if set.
- If WITH_NEXT_FRAMEWORK macro is defined, use __PYVENV_LAUNCHER__
environment variable is set.
- Use argv[0] if available and non-empty.
- Use "python" on Windows, or "python3 on other platforms. */
wchar_t *program_name;
PyWideStringList xoptions; /* Command line -X options */
/* Warnings options: lowest to highest priority. warnings.filters
is built in the reverse order (highest to lowest priority). */
PyWideStringList warnoptions;
/* If equal to zero, disable the import of the module site and the
site-dependent manipulations of sys.path that it entails. Also disable
these manipulations if site is explicitly imported later (call
site.main() if you want them to be triggered).
Set to 0 by the -S command line option. If set to -1 (default), it is
set to !Py_NoSiteFlag. */
int site_import;
/* Bytes warnings:
* If equal to 1, issue a warning when comparing bytes or bytearray with
str or bytes with int.
* If equal or greater to 2, issue an error.
Incremented by the -b command line option. If set to -1 (default), inherit
Py_BytesWarningFlag value. */
int bytes_warning;
/* If greater than 0, enable inspect: when a script is passed as first
argument or the -c option is used, enter interactive mode after
executing the script or the command, even when sys.stdin does not appear
to be a terminal.
Incremented by the -i command line option. Set to 1 if the PYTHONINSPECT
environment variable is non-empty. If set to -1 (default), inherit
Py_InspectFlag value. */
int inspect;
/* If greater than 0: enable the interactive mode (REPL).
Incremented by the -i command line option. If set to -1 (default),
inherit Py_InteractiveFlag value. */
int interactive;
/* Optimization level.
Incremented by the -O command line option. Set by the PYTHONOPTIMIZE
environment variable. If set to -1 (default), inherit Py_OptimizeFlag
value. */
int optimization_level;
/* If greater than 0, enable the debug mode: turn on parser debugging
output (for expert only, depending on compilation options).
Incremented by the -d command line option. Set by the PYTHONDEBUG
environment variable. If set to -1 (default), inherit Py_DebugFlag
value. */
int parser_debug;
/* If equal to 0, Python won't try to write ``.pyc`` files on the
import of source modules.
Set to 0 by the -B command line option and the PYTHONDONTWRITEBYTECODE
environment variable. If set to -1 (default), it is set to
!Py_DontWriteBytecodeFlag. */
int write_bytecode;
/* If greater than 0, enable the verbose mode: print a message each time a
module is initialized, showing the place (filename or built-in module)
from which it is loaded.
If greater or equal to 2, print a message for each file that is checked
for when searching for a module. Also provides information on module
cleanup at exit.
Incremented by the -v option. Set by the PYTHONVERBOSE environment
variable. If set to -1 (default), inherit Py_VerboseFlag value. */
int verbose;
/* If greater than 0, enable the quiet mode: Don't display the copyright
and version messages even in interactive mode.
Incremented by the -q option. If set to -1 (default), inherit
Py_QuietFlag value. */
int quiet;
/* If greater than 0, don't add the user site-packages directory to
sys.path.
Set to 0 by the -s and -I command line options , and the PYTHONNOUSERSITE
environment variable. If set to -1 (default), it is set to
!Py_NoUserSiteDirectory. */
int user_site_directory;
/* If non-zero, configure C standard steams (stdio, stdout,
stderr):
- Set O_BINARY mode on Windows.
- If buffered_stdio is equal to zero, make streams unbuffered.
Otherwise, enable streams buffering if interactive is non-zero. */
int configure_c_stdio;
/* If equal to 0, enable unbuffered mode: force the stdout and stderr
streams to be unbuffered.
Set to 0 by the -u option. Set by the PYTHONUNBUFFERED environment
variable.
If set to -1 (default), it is set to !Py_UnbufferedStdioFlag. */
int buffered_stdio;
/* Encoding of sys.stdin, sys.stdout and sys.stderr.
Value set from PYTHONIOENCODING environment variable and
Py_SetStandardStreamEncoding() function.
See also 'stdio_errors' attribute. */
wchar_t *stdio_encoding;
/* Error handler of sys.stdin and sys.stdout.
Value set from PYTHONIOENCODING environment variable and
Py_SetStandardStreamEncoding() function.
See also 'stdio_encoding' attribute. */
wchar_t *stdio_errors;
#ifdef MS_WINDOWS
/* If greater than zero, use io.FileIO instead of WindowsConsoleIO for sys
standard streams.
Set to 1 if the PYTHONLEGACYWINDOWSSTDIO environment variable is set to
a non-empty string. If set to -1 (default), inherit
Py_LegacyWindowsStdioFlag value.
See PEP 528 for more details. */
int legacy_windows_stdio;
#endif
/* Value of the --check-hash-based-pycs command line option:
- "default" means the 'check_source' flag in hash-based pycs
determines invalidation
- "always" causes the interpreter to hash the source file for
invalidation regardless of value of 'check_source' bit
- "never" causes the interpreter to always assume hash-based pycs are
valid
The default value is "default".
See PEP 552 "Deterministic pycs" for more details. */
wchar_t *check_hash_pycs_mode;
/* --- Path configuration inputs ------------ */
/* If greater than 0, suppress _PyPathConfig_Calculate() warnings on Unix.
The parameter has no effect on Windows.
If set to -1 (default), inherit !Py_FrozenFlag value. */
int pathconfig_warnings;
wchar_t *pythonpath_env; /* PYTHONPATH environment variable */
wchar_t *home; /* PYTHONHOME environment variable,
see also Py_SetPythonHome(). */
/* --- Path configuration outputs ----------- */
int module_search_paths_set; /* If non-zero, use module_search_paths */
PyWideStringList module_search_paths; /* sys.path paths. Computed if
module_search_paths_set is equal
to zero. */
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 */
/* --- Parameter only used by Py_Main() ---------- */
/* Skip the first line of the source ('run_filename' parameter), allowing use of non-Unix forms of
"#!cmd". This is intended for a DOS specific hack only.
Set by the -x command line option. */
int skip_source_first_line;
wchar_t *run_command; /* -c command line argument */
wchar_t *run_module; /* -m command line argument */
wchar_t *run_filename; /* Trailing command line argument without -c or -m */
/* --- Private fields ---------------------------- */
/* Install importlib? If set to 0, importlib is not initialized at all.
Needed by freeze_importlib. */
int _install_importlib;
/* If equal to 0, stop Python initialization before the "main" phase */
int _init_main;
/* If non-zero, disallow threads, subprocesses, and fork.
Default: 0. */
int _isolated_interpreter;
/* Original command line arguments. If _orig_argv is empty and _argv is
not equal to [''], PyConfig_Read() copies the configuration 'argv' list
into '_orig_argv' list before modifying 'argv' list (if parse_argv
is non-zero).
_PyConfig_Write() initializes Py_GetArgcArgv() to this list. */
PyWideStringList _orig_argv;
} PyConfig;
pymain_init() 先调用 PyConfig_InitPythonConfig() 创建默认配置,然后调用 PyConfig_SetBytesArgv() 将命令行参数存储至 config.argv 中,最后调用 Py_InitializeFromConfig() 执行核心初始化和主初始化。
下面,我们来看看 Py_InitializeFromConfig():
PyStatus
Py_InitializeFromConfig(const PyConfig *config)
{
if (config == NULL) {
return _PyStatus_ERR("initialization config is NULL");
}
PyStatus status;
// 看到没,这里又调用了一次!
status = _PyRuntime_Initialize();
if (_PyStatus_EXCEPTION(status)) {
return status;
}
_PyRuntimeState *runtime = &_PyRuntime;
PyThreadState *tstate = NULL;
// 核心初始化阶段
status = pyinit_core(runtime, config, &tstate);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
config = _PyInterpreterState_GetConfig(tstate->interp);
if (config->_init_main) {
// 主初始化阶段
status = pyinit_main(tstate);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
}
return _PyStatus_OK();
}
我们可以清楚地看到初始化的不同阶段。核心初始化由 pyinit_core() 完成,主初始化由 pyinit_main() 完成。pyinit_core() 函数初始化了 Python “核心”部分,具体可以分为两步:
- 准备相关配置:解析命令行参数,读取环境变量,确定文件路径,选择标准流与文件系统的编码方式,并将这些数据写入配置变量的对应位置;
- 应用这些配置:设置标准流,生成哈希函数密钥,创建主解释器状态与主线程状态,初始化 GIL 并占用,使能垃圾收集器,初始化内置类型与异常,初始化 sys 模块及内置模块,为内置模块与冻结模块准备好模块导入系统;
在第一步中,CPython 会计算 config.module_search_paths,之后,这个路径会被复制到 sys.path。其它内容比较无聊,我们先略过。
我们来看看 pyinit_config(),它被 pyinit_core 函数调用,负责执行第二步:
static PyStatus
pyinit_config(_PyRuntimeState *runtime,
PyThreadState **tstate_p,
const PyConfig *config)
{
// 根据配置设置 Py_* 全局变量
// 初始化标准流(stdin, stdout, stderr)
// 为哈希函数设置密钥
PyStatus status = pycore_init_runtime(runtime, config);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
PyThreadState *tstate;
// 创建主解释器状态和主线程状态
// 占用 GIL
status = pycore_create_interpreter(runtime, config, &tstate);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
*tstate_p = tstate;
// 初始化数据类型、异常、sys、内置函数和模块、导入系统等
status = pycore_interp_init(tstate);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
/* Only when we get here is the runtime core fully initialized */
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;
/* Reference to the _PyRuntime global variable. This field exists
to not have to pass runtime in addition to tstate to a function.
Get runtime from tstate: tstate->interp->runtime. */
struct pyruntimestate *runtime;
int64_t id;
// 解释器的引用记录
int64_t id_refcount;
int requires_idref;
PyThread_type_lock id_mutex;
int finalizing;
struct _ceval_state ceval;
struct _gc_runtime_state gc;
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;
int codecs_initialized;
struct _Py_unicode_state unicode;
PyConfig config;
PyObject *dict; /* Stores per-interpreter state */
PyObject *builtins_copy;
PyObject *import_func;
/* Initialized to PyEval_EvalFrameDefault(). */
_PyFrameEvalFunction eval_frame;
// 可以看 `atexit` 模块
void (*pyexitfunc)(PyObject *);
PyObject *pyexitmodule;
uint64_t tstate_next_unique_id;
// 可以看 `warnings` 模块
struct _warnings_runtime_state warnings;
// 审计钩子,可以看 sys.addaudithook
PyObject *audit_hooks;
#if _PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS > 0
// 小整数保存在这里,便于复用
// 默认范围为 [-5, 256].
PyLongObject* small_ints[_PY_NSMALLNEGINTS + _PY_NSMALLPOSINTS];
#endif
// ... 暂时不关心的内容
};
值得特别关注的是,我们之前读取的各类参数保存在新创建的解释器状态的 config 字段中。配置归属于解释器状态。
线程状态的结构体定义如下:
// The PyThreadState typedef is in Include/pystate.h.
struct _ts {
// 同解释器下的线程状态保存在一个双链表中
struct _ts *prev;
struct _ts *next;
PyInterpreterState *interp;
// 当前帧的引用(可以是 NULL)
// 通过 frame->f_back 可以访问调用栈
PyFrameObject *frame;
// ... 检查递归层次是否太深
// ... 追踪/记录状态
/* The exception currently being raised */
PyObject *curexc_type;
PyObject *curexc_value;
PyObject *curexc_traceback;
/* The exception currently being handled, if no coroutines/generators
* are present. Always last element on the stack referred to be exc_info.
*/
_PyErr_StackItem exc_state;
/* Pointer to the top of the stack of the exceptions currently
* being handled */
_PyErr_StackItem *exc_info;
PyObject *dict; /* Stores per-thread state */
int gilstate_counter;
PyObject *async_exc; /* Asynchronous exception to raise */
unsigned long thread_id; /* Thread id where this tstate was created */
/* Unique thread state id. */
uint64_t id;
// ... 其它暂时可忽略的东西
};
创建主线程状态后,pycore_create_interpreter() 函数将初始化 GIL,避免多个线程同时操作 Python 对象。如果你通过 threading 模块创建新线程,它将在每次进入求值循环前等待,直到占用 GIL 锁,同时,线程状态将作为求值函数的一个参数,供线程随时访问。
如果你要通过 Python/C 接口手动占用 GIL 锁,也必须同时提供对应的线程状态。此时,需要将线程状态存储至特定的线程存储空间(在类 Unix 系统中,即调用 pthread_setspecific() 库函数)。
GIL 需要单独一篇文章来讨论,Python 对象系统和 import 机制也一样。不过,本篇还是会简单提及一些内容。
创建第一个解释器状态和线程状态后,pyinit_config() 调用 pycore_interp_init() 函数完成核心初始化。pycore_interp_init() 函数的代码逻辑很清晰:
static PyStatus
pycore_interp_init(PyThreadState *tstate)
{
PyStatus status;
PyObject *sysmod = NULL;
status = pycore_init_types(tstate);
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = _PySys_Create(tstate, &sysmod);
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = pycore_init_builtins(tstate);
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = pycore_init_import_warnings(tstate, sysmod);
done:
// Py_XDECREF() 减少对象引用计数
// 如果引用计数变为 0,将销毁对象,回收内存
Py_XDECREF(sysmod);
return status;
}
pycore_init_types() 函数负责初始化内置类型。具体做了哪些事情呢?内置类型又是什么?我们知道,Python 中一切皆对象。数字、字符串、列表、函数、模块、帧、自定义类乃至内置类型都是 Python 对象。
所有 Python 对象都是 PyObject 结构或以 PyObject 作为第一个字段的其它 C 结构的一个实例。PyObject 有两个字段,第一个是 Py_ssize_t 类型的引用计数,第二个是 PyTypeObject 指针,指向对象类型。下面是 PyObject 结构体的定义:
typedef struct _object {
_PyObject_HEAD_EXTRA // for debugging only
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;
而下面的是大家熟悉的 float 类型的结构体定义:
typedef struct {
PyObject_HEAD // 一个宏,扩展为 PyObject ob_base;
double ob_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 // 扩展为
// PyObject ob_base;
// Py_ssize_t ob_size;
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
/* Methods to implement standard operations */
destructor tp_dealloc;
Py_ssize_t tp_vectorcall_offset;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
or tp_reserved (Python 3) */
reprfunc tp_repr;
/* Method suites for standard classes */
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
/* More standard operations (here for binary compatibility) */
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
/* Functions to access object as input/output buffer */
PyBufferProcs *tp_as_buffer;
/* Flags to define presence of optional/expanded features */
unsigned long tp_flags;
const char *tp_doc; /* Documentation string */
/* Assigned meaning in release 2.0 */
/* call function for all accessible objects */
traverseproc tp_traverse;
/* delete references to contained objects */
inquiry tp_clear;
/* Assigned meaning in release 2.1 */
/* rich comparisons */
richcmpfunc tp_richcompare;
/* weak reference enabler */
Py_ssize_t tp_weaklistoffset;
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;
/* Attribute descriptor and subclassing stuff */
struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;
struct _typeobject *tp_base;
PyObject *tp_dict;
descrgetfunc tp_descr_get;
descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free; /* Low-level free-memory routine */
inquiry tp_is_gc; /* For PyObject_IS_GC */
PyObject *tp_bases;
PyObject *tp_mro; /* method resolution order */
PyObject *tp_cache;
PyObject *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;
/* Type attribute cache version tag. Added in version 2.6 */
unsigned int tp_version_tag;
destructor tp_finalize;
vectorcallfunc tp_vectorcall;
};
内置类型,如 int、list 等,是通过静态声明 PyTypeObject 实例实现的:
PyTypeObject PyList_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_" 等魔法函数
PyStatus status = _PyTypes_InitSlotDefs();
if (_PyStatus_EXCEPTION(status)) {
return status;
}
#define INIT_TYPE(TYPE, NAME) \
do { \
if (PyType_Ready(TYPE) < 0) { \
return _PyStatus_ERR("Can't initialize " NAME " type"); \
} \
} while (0)
INIT_TYPE(&PyBaseObject_Type, "object");
INIT_TYPE(&PyType_Type, "type");
INIT_TYPE(&_PyWeakref_RefType, "weakref");
INIT_TYPE(&_PyWeakref_CallableProxyType, "callable weakref proxy");
INIT_TYPE(&_PyWeakref_ProxyType, "weakref proxy");
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, "dict keys");
// ... 另外 50 种类型的初始化
return _PyStatus_OK();
#undef INIT_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
>>> import imp
<stdin>:1: DeprecationWarning: the imp module is deprecated in favour of importlib; ...
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)
{
int exitcode = 0;
pymain_run_python(&exitcode);
if (Py_FinalizeEx() < 0) {
/* Value unlikely to be confused with a non-error exit status or
other special meaning */
exitcode = 120;
}
// 释放 Py_FinalizeEx() 没有释放的内存
pymain_free();
if (_Py_UnhandledKeyboardInterrupt) {
exitcode = exit_sigint();
}
return exitcode;
}
Py_RunMain() 首先调用 pymain_run_python() 运行 Python,然后调用 Py_FinalizeEx() 执行去初始化。这个函数释放了大多数 CPython 能释放的内存,剩余部分由 pymain_free() 释放。另外,Py_FinalizeEx() 还会调用各种退出函数,包括用户通过 atexit 模块注册的退出函数。
我们知道,运行 Python 代码有几种方式,即:
- 交互式:
$ ./cpython/python.exe
>>> import sys
>>> sys.path[:1]
['']
- 作为标准输入流:
$ echo "import sys; print(sys.path[:1])" | ./cpython/python.exe
['']
- 命令形式:
$ ./cpython/python.exe -c "import sys; print(sys.path[:1])"
['']
- 脚本形式:
$ ./cpython/python.exe 03/print_path0.py
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes/03']
- 以模块运行:
$ ./cpython/python.exe -m 03.print_path0
['/Users/Victor/Projects/tenthousandmeters/python_behind_the_scenes']
- 以及,可能比较少见的,把包作为脚本运行(print_path0_package 是一个文件夹,其中包含 __main__.py 文件):
$ ./cpython/python.exe 03/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:
static void
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) {
// Calculate the search path for the case when the filename is a package
// (ex: directory or ZIP file) which contains __main__.py, store it in `main_importer_path`.
// Otherwise, left `main_importer_path` unchanged.
// Handle other cases later.
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) {
goto error;
}
}
else if (!config->isolated) {
PyObject *path0 = NULL;
// 计算要添加到 `sys.path` 的模块搜索路径
// 如果以脚本运行,即脚本所在文件夹
// 如果以模块运行(-m),即当前所在文件夹
// 否则为空字符串
int res = _PyPathConfig_ComputeSysPath0(&config->argv, &path0);
if (res < 0) {
goto error;
}
if (res > 0) {
if (pymain_sys_path_add_path0(interp, path0) < 0) {
Py_DECREF(path0);
goto error;
}
Py_DECREF(path0);
}
}
PyCompilerFlags cf = _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);
}
else if (config->run_module) {
*exitcode = pymain_run_module(config->run_module, 1);
}
else if (main_importer_path != NULL) {
*exitcode = pymain_run_module(L"__main__", 0);
}
else if (config->run_filename != NULL) {
*exitcode = pymain_run_file(config, &cf);
}
else {
*exitcode = pymain_run_stdin(config, &cf);
}
// 程序执行后进入交互模式
// 即支持 `-i`、`PYTHONINSPECT`选项
pymain_repl(config, &cf, exitcode);
goto done;
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.exe 03/__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, const char *filename, int closeit,
PyCompilerFlags *flags)
{
PyObject *m, *d, *v;
const char *ext;
int set_file_name = 0, ret = -1;
size_t len;
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)
goto done;
if (PyDict_SetItemString(d, "__file__", f) < 0) {
Py_DECREF(f);
goto done;
}
if (PyDict_SetItemString(d, "__cached__", Py_None) < 0) {
Py_DECREF(f);
goto done;
}
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;
/* Try to run a pyc file. First, re-open in binary */
if (closeit)
fclose(fp);
if ((pyc_fp = _Py_fopen(filename, "rb")) == NULL) {
fprintf(stderr, "python: Can't reopen .pyc file\n");
goto done;
}
if (set_main_loader(d, filename, "SourcelessFileLoader") < 0) {
fprintf(stderr, "python: failed to set __main__.__loader__\n");
ret = -1;
fclose(pyc_fp);
goto done;
}
v = run_pyc_file(pyc_fp, filename, d, d, flags);
} else {
/* When running from stdin, leave __main__.__loader__ alone */
if (strcmp(filename, "<stdin>") != 0 &&
set_main_loader(d, filename, "SourceFileLoader") < 0) {
fprintf(stderr, "python: failed to set __main__.__loader__\n");
ret = -1;
goto done;
}
v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
closeit, flags);
}
flush_io();
if (v == NULL) {
Py_CLEAR(m);
PyErr_Print();
goto done;
}
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);
return ret;
}
PyRun_FileExFlags() 会执行编译过程,运行解析器,获取模块的 AST,并调用 run_mod() 运行 AST。同时,它还负责创建 PyArena,供 CPython 保存小对象(小于等于 512 字节的对象)。
PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
PyObject *locals, int closeit, PyCompilerFlags *flags)
{
PyObject *ret = NULL;
mod_ty mod;
PyArena *arena = NULL;
PyObject *filename;
int use_peg = _PyInterpreterState_GET()->config._use_peg_parser;
filename = PyUnicode_DecodeFSDefault(filename_str);
if (filename == NULL)
goto exit;
arena = PyArena_New();
if (arena == NULL)
goto exit;
// 运行解析器
// 默认使用新版 PEG 解析器
// 传入 `-X oldparser` 可以使用旧版解析器
// `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) {
goto exit;
}
// 编译 AST 并运行
ret = run_mod(mod, filename, globals, locals, flags, arena);
exit:
Py_XDECREF(filename);
if (arena != NULL)
PyArena_Free(arena);
return ret;
}
run_mod() 调用 PyAST_CompileObject() 以运行编译器,获取模块的代码对象,然后调用 run_eval_code_obj() 执行代码对象。期间还抛出 exec 事件——这是 CPython 将运行时事件通知审计工具的方式。相关机制可以参考 PEP 578。
static PyObject *
run_mod(mod_ty mod, 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)
return NULL;
if (_PySys_Audit(tstate, "exec", "O", co) < 0) {
Py_DECREF(co);
return NULL;
}
PyObject *v = run_eval_code_obj(tstate, co, globals, locals);
Py_DECREF(co);
return v;
}
在第二篇文章中,我们已经看到编译器的工作方式:
- 构建符号表
- 创建基本块的 CFG
- 将 CFG 集成为代码对象
这正是 PyAST_CompileObject() 所做的工作,因此,这个函数我们不再过多讨论。
通过一系列的调用,run_eval_code_obj() 最终会到达 _PyEval_EvalCode()。我把整个调用链粘贴过来,方便大家看到参数是怎么一路传过去的:
static PyObject *
run_eval_code_obj(PyThreadState *tstate, PyCodeObject *co, PyObject *globals, PyObject *locals)
{
PyObject *v;
// 处理 CPython 被嵌入使用的情况,我们可以忽略
/*
* We explicitly re-initialize _Py_UnhandledKeyboardInterrupt every eval
* _just in case_ someone is calling into an embedded Python where they
* don't care about an uncaught KeyboardInterrupt exception (why didn't they
* leave config.install_signal_handlers set to 0?!?) but then later call
* Py_Main() itself (which _checks_ this flag and dies with a signal after
* its interpreter exits). We don't want a previous embedded interpreter's
* uncaught exception to trigger an unexplained signal exit from a future
* Py_Main() based one.
*/
_Py_UnhandledKeyboardInterrupt = 0;
/* Set globals['__builtins__'] if it doesn't exist */
// 在我们的场景中,已经在主初始化时设置为 `builtins` 模块
if (globals != NULL && PyDict_GetItemString(globals, "__builtins__") == NULL) {
if (PyDict_SetItemString(globals, "__builtins__",
tstate->interp->builtins) < 0) {
return NULL;
}
}
v = PyEval_EvalCode((PyObject*)co, globals, locals);
if (!v && _PyErr_Occurred(tstate) == PyExc_KeyboardInterrupt) {
_Py_UnhandledKeyboardInterrupt = 1;
}
return v;
}
PyObject *
PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals)
{
return PyEval_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, int argcount,
PyObject *const *kws, int kwcount,
PyObject *const *defs, int defcount,
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_t argcount,
PyObject *const *kwnames, PyObject *const *kwargs,
Py_ssize_t kwcount, int kwstep,
PyObject *const *defs, Py_ssize_t defcount,
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_t argcount,
PyObject *const *kwnames, PyObject *const *kwargs,
Py_ssize_t kwcount, int kwstep,
PyObject *const *defs, Py_ssize_t defcount,
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;
const Py_ssize_t total_args = co->co_argcount + co->co_kwonlyargcount;
Py_ssize_t i, j, n;
PyObject *kwdict;
if (globals == NULL) {
_PyErr_SetString(tstate, PyExc_SystemError,
"PyEval_EvalCodeEx: NULL globals");
return NULL;
}
/* Create the frame */
f = _PyFrame_New_NoTrack(tstate, co, globals, locals);
if (f == NULL) {
return NULL;
}
fastlocals = f->f_localsplus;
freevars = f->f_localsplus + co->co_nlocals;
/* Create a dictionary for keyword parameters (**kwags) */
if (co->co_flags & CO_VARKEYWORDS) {
kwdict = PyDict_New();
if (kwdict == NULL)
goto fail;
i = total_args;
if (co->co_flags & CO_VARARGS) {
i++;
}
SETLOCAL(i, kwdict);
}
else {
kwdict = NULL;
}
/* Copy all positional arguments into local variables */
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);
}
/* Pack other positional arguments into the *args argument */
if (co->co_flags & CO_VARARGS) {
u = _PyTuple_FromArray(args + n, argcount - n);
if (u == NULL) {
goto fail;
}
SETLOCAL(total_args, u);
}
/* Handle keyword arguments passed as two strided arrays */
kwcount *= kwstep;
for (i = 0; i < kwcount; i += kwstep) {
PyObject **co_varnames;
PyObject *keyword = kwnames[i];
PyObject *value = kwargs[i];
Py_ssize_t j;
if (keyword == NULL || !PyUnicode_Check(keyword)) {
_PyErr_Format(tstate, PyExc_TypeError,
"%U() keywords must be strings",
co->co_name);
goto fail;
}
/* Speed hack: do raw pointer compares. As names are
normally interned this should almost always hit. */
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) {
goto kw_found;
}
}
/* Slow fallback, just in case */
for (j = co->co_posonlyargcount; j < total_args; j++) {
PyObject *name = co_varnames[j];
int cmp = PyObject_RichCompareBool( keyword, name, Py_EQ);
if (cmp > 0) {
goto kw_found;
}
else if (cmp < 0) {
goto fail;
}
}
assert(j >= total_args);
if (kwdict == NULL) {
if (co->co_posonlyargcount
&& positional_only_passed_as_keyword(tstate, co,
kwcount, kwnames))
{
goto fail;
}
_PyErr_Format(tstate, PyExc_TypeError,
"%U() got an unexpected keyword argument '%S'",
co->co_name, keyword);
goto fail;
}
if (PyDict_SetItem(kwdict, keyword, value) == -1) {
goto fail;
}
continue;
kw_found:
if (GETLOCAL(j) != NULL) {
_PyErr_Format(tstate, PyExc_TypeError,
"%U() got multiple values for argument '%S'",
co->co_name, keyword);
goto fail;
}
Py_INCREF(value);
SETLOCAL(j, value);
}
/* Check the number of positional arguments */
if ((argcount > co->co_argcount) && !(co->co_flags & CO_VARARGS)) {
too_many_positional(tstate, co, argcount, defcount, fastlocals);
goto fail;
}
/* Add missing positional arguments (copy default values from defs) */
if (argcount < co->co_argcount) {
Py_ssize_t m = co->co_argcount - defcount;
Py_ssize_t missing = 0;
for (i = argcount; i < m; i++) {
if (GETLOCAL(i) == NULL) {
missing++;
}
}
if (missing) {
missing_arguments(tstate, co, missing, defcount, fastlocals);
goto fail;
}
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);
}
}
}
/* Add missing keyword arguments (copy default values from kwdefs) */
if (co->co_kwonlyargcount > 0) {
Py_ssize_t missing = 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;
}
else if (_PyErr_Occurred(tstate)) {
goto fail;
}
}
missing++;
}
if (missing) {
missing_arguments(tstate, co, missing, -1, fastlocals);
goto fail;
}
}
/* Allocate and initialize storage for cell vars, and copy free
vars into frame. */
for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) {
PyObject *c;
Py_ssize_t arg;
/* Possibly account for the cell variable being an argument. */
if (co->co_cell2arg != NULL &&
(arg = co->co_cell2arg[i]) != CO_CELL_NOT_AN_ARG) {
c = PyCell_New(GETLOCAL(arg));
/* Clear the local copy. */
SETLOCAL(arg, NULL);
}
else {
c = PyCell_New(NULL);
}
if (c == NULL)
goto fail;
SETLOCAL(co->co_nlocals + i, c);
}
/* Copy closure variables to free variables */
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;
}
/* Handle generator/coroutine/asynchronous generator */
if (co->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) {
PyObject *gen;
int is_coro = co->co_flags & CO_COROUTINE;
/* Don't need to keep the reference to f_back, it will be set
* when the generator is resumed. */
Py_CLEAR(f->f_back);
/* Create a new generator that owns the ready to run frame
* and return that as the value. */
if (is_coro) {
gen = PyCoro_New(f, name, qualname);
} else if (co->co_flags & CO_ASYNC_GENERATOR) {
gen = PyAsyncGen_New(f, name, qualname);
} else {
gen = PyGen_NewWithQualName(f, name, qualname);
}
if (gen == NULL) {
return NULL;
}
_PyObject_GC_TRACK(f);
return gen;
}
retval = _PyEval_EvalFrame(tstate, f, 0);
fail: /* Jump here from prelude on failure */
/* decref'ing the frame can cause __del__ methods to get invoked,
which can call back into Python. While we're done with the
current Python frame (f), the associated C stack is still in use,
so recursion_depth must be boosted for the duration.
*/
if (Py_REFCNT(f) > 1) {
Py_DECREF(f);
_PyObject_GC_TRACK(f);
}
else {
++tstate->recursion_depth;
Py_DECREF(f);
--tstate->recursion_depth;
}
return retval;
}
_PyEval_EvalFrame() 封装了帧求值函数 interp->eval_frame()。其实,用户可以自定义帧求值函数。什么情况下需要自定义呢?比如说,为了添加一个 JIT 编译器,要将编译的机器码保存在代码对象中并执行的时候。这个功能是由 PEP 523 在 CPython3.6 引入的。
interp->eval_frame() 默认为 _PyEval_EvalFrameDefault()。这个函数在 Python/ceval.c 文件中定义,代码将近 3000 行。不过,今天,我们只对其中的第 1336 行感兴趣,这行代码我们已经找了很久了:求值循环。
3. 总结
本篇讨论了很多内容。首先是 CPython 项目的概览,然后编译 CPython,跟着源码一步步学习各个初始化阶段。希望这篇文章让你对 CPython 开始解析字节码前的工作有了大概的了解。至于后面的流程,则是下一篇文章的内容。
同时,为巩固今天的内容,了解更多有意思的东西,强烈推荐大家自己花点时间看看 CPython 源码。我相信,读完这篇文章后,你会有许多疑问,带着这些疑问去读源码是个不错的选择。祝大家探索愉快!
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)