diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..7d0517a --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,60 @@ +name: Deploy VitePress to GitHub Pages + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build with VitePress + run: npm run build + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: .vitepress/dist + + deploy: + runs-on: ubuntu-latest + needs: build + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 36276d8..80d69cc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,10 +10,21 @@ node_modules # Book build output _book +# VitePress build output / cache +.vitepress/dist +.vitepress/cache + # eBook build output *.epub *.mobi *.pdf # mac -.DS_Store \ No newline at end of file +.DS_Store + +# python +__pycache__/ +*.pyc + +# playwright mcp test artifacts +.playwright-mcp/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b10a1cf..0000000 --- a/.travis.yml +++ /dev/null @@ -1,45 +0,0 @@ -language: node_js - -node_js: - - "10.6" - -# 缓存依赖 -cache: - directories: - - $HOME/.npm - -before_install: - - export TZ='Asia/Shanghai' # 更改时区 - -# 依赖安装 -install: - - npm install gitbook-cli -g - # 安装 gitbook 插件 - - gitbook install - -# 构建脚本 -script: - - gitbook build - -# 分支白名单 -branches: - only: - - master # 只对 master 分支进行构建 - -# GitHub Pages 部署 -deploy: - provider: pages - skip_cleanup: true - # 在项目仪表盘的 Settings -> Environment Variables 中配置 - github_token: $GITHUB_TOKEN - # 将 build 目录下的内容推送到默认的 gh-pages 分支上,并不会连带 build 目录一起 - local_dir: _book - on: - branch: master - -# 通知 -notifications: - email: - recipients: - - wangbinxin001@126.com - on_failure: always \ No newline at end of file diff --git a/.vitepress/config.mts b/.vitepress/config.mts new file mode 100644 index 0000000..0d19867 --- /dev/null +++ b/.vitepress/config.mts @@ -0,0 +1,136 @@ +import { defineConfig } from 'vitepress' +import taskLists from 'markdown-it-task-lists' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + lang: 'zh-Hans', + title: 'Python 3 源码分析', + description: '致力于分析 Python 3.7.0 的源码实现', + + // 部署在 https://flaggo.github.io/python3-source-code-analysis/ + base: '/python3-source-code-analysis/', + + // 正文全部收纳在 docs/ 下;首页为 docs/index.md(背景 + Roadmap) + srcDir: 'docs', + + lastUpdated: true, + cleanUrls: true, + ignoreDeadLinks: true, + + markdown: { + lineNumbers: true, + config: (md) => { + md.use(taskLists) + } + }, + + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Playground', link: '/playground/' } + ], + + sidebar: [ + { + text: '第 1 部分:准备', + items: [ + { text: '前言', link: '/' }, + { text: 'Python 源代码的组织', link: '/preface/code-organization/' }, + { text: 'Windows 环境下编译 Python', link: '/preface/windows-build/' }, + { text: 'UNIX/Linux 环境下编译 Python', link: '/preface/unix-linux-build/' }, + { text: '修改 Python 源码', link: '/preface/modify-code/' } + ] + }, + { + text: '第 2 部分:对象与类型系统', + items: [ + { text: 'Python 对象初探', link: '/objects/object/' }, + { text: 'Python 整数对象', link: '/objects/long-object/' }, + { text: 'Python 浮点数对象', link: '/objects/float-object/' }, + { text: 'Python 字符串对象', link: '/objects/str-object/' }, + { text: 'Python bytes 与 bytearray 对象', link: '/objects/bytes-object/' }, + { text: 'Python 列表对象', link: '/objects/list-object/' }, + { text: 'Python 元组对象', link: '/objects/tuple-object/' }, + { text: 'Python 字典对象', link: '/objects/dict-object/' }, + { text: 'Python 集合对象', link: '/objects/set-object/' }, + { text: 'Python 布尔与 None 对象', link: '/objects/bool-none-object/' }, + { text: 'Python 类型对象与自定义类', link: '/objects/type-object/' } + ] + }, + { + text: '第 3 部分:编译', + items: [ + { text: '从源码到字节码(编译过程)', link: '/compile/source-to-bytecode/' }, + { text: '编译的产物:code object 与 pyc', link: '/compile/code-object/' } + ] + }, + { + text: '第 4 部分:虚拟机', + items: [ + { text: 'Python 虚拟机框架(帧对象与求值循环)', link: '/vm/frame-and-eval-loop/' }, + { text: '一般表达式与名字空间', link: '/vm/expressions-and-names/' }, + { text: '控制流:跳转、循环与迭代器', link: '/vm/control-flow/' }, + { text: '异常机制:block 栈与栈展开', link: '/vm/exceptions/' }, + { text: '函数机制:调用、参数与闭包', link: '/vm/functions/' }, + { text: '生成器与协程', link: '/vm/generators/' } + ] + }, + { + text: '第 5 部分:运行时', + items: [ + { text: 'Python 运行环境初始化', link: '/runtime/initialization/' }, + { text: '模块与 import 机制', link: '/runtime/import-system/' }, + { text: '多线程与 GIL', link: '/runtime/gil/' } + ] + }, + { + text: '第 6 部分:内存管理', + items: [ + { text: '内存分配与引用计数(pymalloc)', link: '/memory/allocation-refcount/' }, + { text: '循环垃圾回收(分代 GC)', link: '/memory/garbage-collection/' } + ] + }, + { + text: '第 7 部分:实战', + items: [ + { text: '动手:用 Python 写一个迷你 Python 虚拟机', link: '/practice/mini-vm/' } + ] + } + ], + + socialLinks: [ + { icon: 'github', link: 'https://github.com/flaggo/python3-source-code-analysis' } + ], + + search: { + provider: 'local' + }, + + editLink: { + pattern: 'https://github.com/flaggo/python3-source-code-analysis/edit/master/docs/:path', + text: '编辑此页面' + }, + + outline: { + label: '本页目录', + level: [2, 3] + }, + + docFooter: { + prev: '上一篇', + next: '下一篇' + }, + + lastUpdated: { + text: '最后更新于' + }, + + footer: { + copyright: 'Copyright © FlagGo 2019-2026' + }, + + darkModeSwitchLabel: '主题', + returnToTopLabel: '回到顶部', + sidebarMenuLabel: '菜单' + } +}) diff --git a/.vitepress/theme/MiniVM.vue b/.vitepress/theme/MiniVM.vue new file mode 100644 index 0000000..cf1a9c9 --- /dev/null +++ b/.vitepress/theme/MiniVM.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/.vitepress/theme/MiniVMPlayground.vue b/.vitepress/theme/MiniVMPlayground.vue new file mode 100644 index 0000000..d1a47b3 --- /dev/null +++ b/.vitepress/theme/MiniVMPlayground.vue @@ -0,0 +1,381 @@ + + + + + diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts new file mode 100644 index 0000000..5e83fb5 --- /dev/null +++ b/.vitepress/theme/index.ts @@ -0,0 +1,12 @@ +import DefaultTheme from 'vitepress/theme' +import type { Theme } from 'vitepress' +import MiniVM from './MiniVM.vue' +import MiniVMPlayground from './MiniVMPlayground.vue' + +export default { + extends: DefaultTheme, + enhanceApp({ app }) { + app.component('MiniVM', MiniVM) + app.component('MiniVMPlayground', MiniVMPlayground) + } +} satisfies Theme diff --git a/.vitepress/theme/pyodide.ts b/.vitepress/theme/pyodide.ts new file mode 100644 index 0000000..33aae57 --- /dev/null +++ b/.vitepress/theme/pyodide.ts @@ -0,0 +1,22 @@ +// 共享的 Pyodide 加载器:整个页面只下载并初始化一次,多个组件复用同一个实例。 +const PYODIDE_VERSION = 'v0.26.4' +const CDN = `https://cdn.jsdelivr.net/pyodide/${PYODIDE_VERSION}/full/` + +let _promise: Promise | null = null + +export function getPyodide(): Promise { + if (_promise) return _promise + _promise = (async () => { + if (!(window as any).loadPyodide) { + await new Promise((resolve, reject) => { + const s = document.createElement('script') + s.src = CDN + 'pyodide.js' + s.onload = () => resolve() + s.onerror = () => reject(new Error('加载 Pyodide 脚本失败,请检查网络')) + document.head.appendChild(s) + }) + } + return await (window as any).loadPyodide({ indexURL: CDN }) + })() + return _promise +} diff --git a/Makefile b/Makefile index 803e1e1..b9c8fb6 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,23 @@ +NPM ?= npm + +.DEFAULT_GOAL := help + +.PHONY: help install serve build clean + help: - @echo "\033[32minit\033[0m" - @echo " 初始化GitBook" - @echo "\033[32mrun\033[0m" - @echo " 运行GitBook服务器" - @echo "\033[32mbuild\033[0m" - @echo " 构建GitBook静态页面" + @printf "\033[32m%-10s\033[0m %s\n" "install" "安装项目依赖" + @printf "\033[32m%-10s\033[0m %s\n" "serve" "启动本地预览服务器" + @printf "\033[32m%-10s\033[0m %s\n" "build" "构建静态页面" + @printf "\033[32m%-10s\033[0m %s\n" "clean" "删除构建产物" -init: - sudo npm i -g gitbook-cli - gitbook install +install: + $(NPM) install -run: - gitbook serve +serve: + $(NPM) run serve build: - gitbook build + $(NPM) run build + +clean: + rm -rf _book .vitepress/dist .vitepress/cache .playwright-mcp diff --git a/README.md b/README.md index 10139bb..950c5bd 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,20 @@ -# 介绍 +# Python 3 源码分析 本项目致力于对 Python 3.7 的源码分析,深度参考陈儒大大的《Python 源码剖析》,编写 Python 3 的版本。 希望各位 Python 爱好者能参与其中,一起探索 Python 魔法背后的奥秘! -# 使用 +## 阅读 -您可以直接访问 [在线版](https://flaggo.github.io/python3-source-code-analysis/),或者根据以下步骤访问本地版。 +直接访问 [在线版](https://flaggo.github.io/python3-source-code-analysis/) 即可阅读全书。 -## 前置条件 +## 本地运行 -您的系统上需要安装好 node。 - -## 使用 make 命令 - -若您可使用 make 命令,简单执行如下命令进行初始化: - -```console -make init -``` - -执行如下命令运行服务端: +本项目使用 [VitePress](https://vitepress.dev/) 构建。直接运行 `make` 可查看所有命令: ```console -make run +make install # 安装依赖 +make serve # 启动本地预览,访问 http://localhost:5173/python3-source-code-analysis/ +make build # 构建静态页面到 .vitepress/dist +make clean # 删除构建产物 ``` - -## 使用 gitbook 命令 - -若您不能使用 make 命令,或想直接使用 gitbook 命令,执行如下命令进行初始化: - -```console -npm i -g gitbook-cli #可能需要sudo -gitbook install -``` - -执行如下命令运行服务端: - -```console -gitbook serve -``` - -## 访问 - -直接访问 http://localhost:4000 即可查看本书内容。 - -# Roadmap - -大体按照《Python 源码剖析》中的目录结构进行编写。依次介绍 Python 源码基本信息、内建对象和虚拟机。 - -- [x] 章节 - - [x] 序章 - - [x] 前言 - - [x] Python 源代码的组织 - - [x] Windows 环境下编译 Python - - [x] UNIX/Linux 环境下编译 Python - - [x] 修改 Python 源码 -- [ ] Python 内建对象 - - [x] Python 对象初探 - - [x] Python 整数对象 - - [ ] Python 字符串 对象 - - [x] Python List 对象 - - [x] Python Dict 对象 - - [x] Python Set 对象 - - [ ] 实现简版 Python -- [ ] Python 虚拟机 - - [ ] Python 编译结果 - - [ ] Python 虚拟机框架 - - [ ] 虚拟机一般表达式 - - [ ] Python 虚拟机控制流 - - [ ] Python 虚拟机函数机制 - - [ ] Python 运行环境初始化 - - [ ] Python 模块加载机制 - - [ ] Python 多线程机制 - - [ ] Python 内存管理机制 - - - diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index 4f20991..0000000 --- a/SUMMARY.md +++ /dev/null @@ -1,21 +0,0 @@ -# 大纲 - -## 第 1 部分:序章 - -- [前言](README.md) -- [Python 源代码的组织](preface/code-organization.md) -- [Windows 环境下编译 Python](preface/windows-build.md) -- [UNIX/Linux 环境下编译 Python](preface/unix-linux-build.md) -- [修改 Python 源码](preface/modify-code.md) - -## 第 2 部分:Python 内建对象 - -- [Python 对象初探](objects/object.md) -- [Python 整数对象](objects/long-object.md) -- [Python 字符串 对象](objects/string-object.md) -- [Python List 对象](objects/list-object.md) -- [Python Dict 对象](objects/dict-object.md) -- [Python Set 对象](objects/set-object.md) -- [实现简版 Python](objects/simple-implementation.md) - -## 第 3 部分:Python 虚拟机 diff --git a/book.json b/book.json deleted file mode 100644 index 4ad9ba9..0000000 --- a/book.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "title": "Python 3 源码分析", - "description": "致力于分析Python 3.7.0 的源码实现", - "author": "Prodesire", - "output.name": "site", - "language": "zh-hans", - "gitbook": "3.2.3", - "root": ".", - "structure": { - "readme": "README.md" - }, - "links": { - "sidebar": { - "主页": "https://github.com/flaggo/python3-source-code-analysis" - } - }, - "plugins": [ - "-search", - "search-plus@^0.0.11", - "-sharing", - "sharing-plus", - "github@^2.0.0", - "github-buttons@2.1.0", - "edit-link@^2.0.2", - "splitter", - "tbfed-pagefooter", - "prism", - "page-toc-button", - "back-to-top-button" - ], - - "pluginsConfig": { - "theme-default": { - "showLevel": true - }, - "github": { - "url": "https://github.com/flaggo/python3-source-code-analysis" - }, - "github-buttons": { - "repo": "flaggo/python3-source-code-analysis", - "types": ["star"], - "size": "small" - }, - "sharing": { - "weibo": true, - "douban": true, - "linkedin": true, - "facebook": true, - "google": true, - "twitter": true, - "all": ["weibo", "douban", "linkedin", "facebook", "google", "twitter"] - }, - "tbfed-pagefooter": { - "copyright": "Copyright © Prodesire 2018", - "modify_label": "该文件修订时间:", - "modify_format": "YYYY-MM-DD HH:mm:ss" - }, - "edit-link": { - "base": - "https://github.com/flaggo/python3-source-code-analysis/edit/master", - "label": "编辑此页面" - }, - "anchor-navigation-ex": { - "isRewritePageTitle": false, - "tocLevel1Icon": "fa fa-hand-o-right", - "tocLevel2Icon": "fa fa-hand-o-right", - "tocLevel3Icon": "fa fa-hand-o-right" - }, - "prism": { - "lang": { - "console": "bash", - "shell": "bash" - }, - "css": [ - "prismjs/themes/prism-okaidia.css" - ] - } - } -} diff --git a/docs/compile/code-object/code-object-fields.svg b/docs/compile/code-object/code-object-fields.svg new file mode 100644 index 0000000..681b5f4 --- /dev/null +++ b/docs/compile/code-object/code-object-fields.svg @@ -0,0 +1,60 @@ + +PyCodeObject 的字段,按用途分三组 +一个 code object 把编译产物打包在一起。它的字段大致分三组:基本信息(参数个数、局部变量数、求值栈深度、标志位、起始行号),执行素材(字节码 co_code,以及它按下标引用的常量表 co_consts、全局名表 co_names、局部名表 co_varnames、闭包相关的 co_freevars/co_cellvars),调试与标识(源文件名、对象名、行号映射表 co_lnotab)。 + + + + + + + + + +PyCodeObject:编译产物打成一个包 + + + +基本信息 + + co_argcount参数个数 + co_kwonlyargcount + co_nlocals局部变量数 + co_stacksize栈深度 + co_flags标志位 + co_firstlineno起始行 + +虚拟机建栈帧、 +绑定参数时要用 + + + +执行素材 + + co_code字节码 + co_consts常量表 + co_names全局名表 + co_varnames局部名表 + co_freevars自由变量 + co_cellvarscell 变量 + +字节码按「下标」 +引用后面这些表 + + + +调试与标识 + + co_filename源文件 + co_name对象名 + co_lnotab行号映射 + +出错回溯(traceback) +和调试器靠它定位 +「这条字节码对应 +哪个文件、哪一行」 + diff --git a/docs/compile/code-object/index.md b/docs/compile/code-object/index.md new file mode 100644 index 0000000..b2ac781 --- /dev/null +++ b/docs/compile/code-object/index.md @@ -0,0 +1,173 @@ +# 编译的产物:code object 与 pyc + +上一章我们走完了编译管线,得到了最终产物——一个 **code object**。这一章就把它拆开看:里面的字节码、常量表、名字表分别长什么样,以及它是怎么被存成 `.pyc` 文件、下次导入时直接复用的。 + +`compile()`、函数的 `__code__`、模块的顶层代码,拿到的都是 code object(C 层的 `PyCodeObject`)。它本质上是**一个把「这段代码运行所需的一切」打包在一起的对象**。 + +## code object 的字段 + +先看它的结构。字段不少,但按用途分成三组就清晰了: + +`源文件:`[Include/code.h](https://github.com/python/cpython/blob/v3.7.0/Include/code.h#L21) + +```c +// Include/code.h —— PyCodeObject(节选) +typedef struct { + PyObject_HEAD + int co_argcount; // 位置参数个数 + int co_nlocals; // 局部变量个数 + int co_stacksize; // 求值栈最大深度 + int co_flags; // 标志位(CO_OPTIMIZED 等) + int co_firstlineno; // 起始行号 + PyObject *co_code; // 字节码(bytes) + PyObject *co_consts; // 常量表(tuple) + PyObject *co_names; // 全局名/属性名表(tuple) + PyObject *co_varnames; // 局部变量名表(tuple) + PyObject *co_freevars; // 自由变量名表 + PyObject *co_cellvars; // cell 变量名表 + PyObject *co_filename; // 源文件名 + PyObject *co_name; // 对象名(函数名/模块名) + PyObject *co_lnotab; // 字节码偏移 ↔ 源码行号 的映射 + ...... +} PyCodeObject; +``` + +![PyCodeObject 的字段分组](code-object-fields.svg) + +- **基本信息**(`co_argcount`、`co_nlocals`、`co_stacksize`、`co_flags`、`co_firstlineno`):虚拟机执行这段代码、建立栈帧、绑定参数时要用的元数据。 +- **执行素材**(`co_code` 及它引用的 `co_consts`、`co_names`、`co_varnames`……):字节码本身,以及字节码会按下标引用的几张表。下一节细看。 +- **调试与标识**(`co_filename`、`co_name`、`co_lnotab`):出错时的 traceback、调试器定位「哪一行」靠它们。 + +这些字段都能在 Python 层直接读到。拿一个简单函数看看: + +```python +>>> def add(a, b): +... c = a + b +... return c +... +>>> co = add.__code__ +>>> co.co_argcount +2 +>>> co.co_varnames # 局部名表:两个参数 + 一个局部变量 +('a', 'b', 'c') +>>> co.co_nlocals +3 +>>> co.co_consts # 这个函数没有字面量,只有隐含的 None +(None,) +>>> co.co_name, co.co_firstlineno +('add', 2) +``` + +`co_varnames` 是 `('a', 'b', 'c')`——参数和局部变量都在里面,**顺序就是它们的编号**。这个编号,正是字节码用来指代它们的下标。 + +## 字节码:每条指令两字节 + +`co_code` 是一段 `bytes`,也就是真正的**字节码**。从 Python 3.6 起,字节码采用 **wordcode** 格式:**每条指令固定两字节——前一字节是操作码 `opcode`(干什么),后一字节是参数 `oparg`(对谁干)**。 + +把上面 `add` 函数体反汇编出来(用标准库 `dis`),就能看到这些指令。下面是它在 **3.7** 下的字节码: + +``` + 3 0 LOAD_FAST 0 (a) + 2 LOAD_FAST 1 (b) + 4 BINARY_ADD + 6 STORE_FAST 2 (c) + + 4 8 LOAD_FAST 2 (c) + 10 RETURN_VALUE +``` + +> 不同 Python 版本的字节码指令会有出入(3.8+ 还会多出一些指令),这里展示的是 3.7 的形式,用来说明结构即可。 + +每行从左到右是:源码行号、字节码偏移、操作码、参数、以及参数解析后的含义。偏移是 `0, 2, 4, 6, 8, 10`——每条指令占两字节,所以两两递增。读一遍就是这段代码的执行步骤: + +- `LOAD_FAST 0`:把局部变量 0 号(`a`)压上求值栈; +- `LOAD_FAST 1`:把 1 号(`b`)压栈; +- `BINARY_ADD`:弹出栈顶两个值相加,结果压栈(它不需要参数); +- `STORE_FAST 2`:把栈顶存进 2 号局部变量(`c`); +- 最后把 `c` 压栈、`RETURN_VALUE` 返回。 + +这里的关键是:**`oparg` 通常是某张表的下标**。`LOAD_FAST 0` 的 `0` 不是数字 0,而是 `co_varnames[0]`——也就是名字 `a`。 + +![wordcode 与表的下标关系](wordcode.svg) + +不同指令查不同的表:取局部变量的 `LOAD_FAST` 查 `co_varnames`,取全局名的 `LOAD_GLOBAL` 查 `co_names`,取常量的 `LOAD_CONST` 查 `co_consts`。所以**字节码本身很紧凑(全是小整数下标),真正的名字和常量都集中放在那几张表里**。这些指令具体怎么在虚拟机里执行,是下一部分的主题;这里只要建立「指令 + 下标 → 表里的值」这个印象。 + +## lnotab:字节码与源码行号的对应 + +注意上面反汇编里左侧的行号(`3`、`4`)。字节码本身是线性的指令流,并不带行号;「第 6 号字节码属于源码第 3 行」这种对应关系,单独存在 `co_lnotab` 里——一张紧凑编码的「字节码偏移 ↔ 行号」映射表。 + +它平时不影响执行,只在**需要把字节码位置翻译回源码位置时**才用到:抛异常打印 traceback、调试器单步、`trace`/`profile` 统计行号,背后都是查这张表。所以一个 code object 不光能跑,还随身带着「我从哪行源码来」的信息。 + +## marshal:把 code object 序列化 + +code object 是个内存对象。要想把编译结果存到磁盘、下次直接用,就得把它**序列化**成字节序列——这件事由 `marshal` 模块负责。`marshal` 是 CPython 内部专用的序列化格式,能处理 code object 这种内置类型,且与具体 Python 版本绑定: + +```python +>>> import marshal +>>> data = marshal.dumps(co) # code object → bytes +>>> type(data).__name__ +'bytes' +>>> co2 = marshal.loads(data) # bytes → code object +>>> co2.co_varnames # 还原如初 +('a', 'b', 'c') +``` + +> `marshal` 不同于 `pickle`:它格式更底层、更快,但**不保证跨版本兼容**,也不为通用对象设计——它就是给「保存字节码」这类内部用途准备的。 + +`marshal.dumps(code)` 正是 `.pyc` 文件主体的来源。 + +## pyc 文件:编译结果的缓存 + +现在拼出完整的 `.pyc`。每次 `import` 一个模块,Python 都要编译它;为避免重复编译,编译结果会被缓存成 `__pycache__/xxx.cpython-37.pyc`。一个 `.pyc` 文件 = **16 字节头 + `marshal` 序列化的 code object**。 + +这 16 字节头的布局(3.7 起遵循 [PEP 552](https://peps.python.org/pep-0552/)),可以在 [Lib/importlib/_bootstrap_external.py](https://github.com/python/cpython/blob/v3.7.0/Lib/importlib/_bootstrap_external.py#L536) 里看到生成代码: + +```python +# Lib/importlib/_bootstrap_external.py —— 按时间戳的 pyc +data = bytearray(MAGIC_NUMBER) # 前 4 字节:magic number +data.extend(_w_long(0)) # 接 4 字节:flags(0 = 按时间戳) +data.extend(_w_long(mtime)) # 接 4 字节:源文件修改时间 +data.extend(_w_long(source_size)) # 接 4 字节:源文件大小 +data.extend(marshal.dumps(code)) # 其余:序列化的 code object +``` + +![pyc 文件结构](pyc-layout.svg) + +四个部分各司其职: + +- **magic number**:标识编译用的 Python 版本,**每个版本都不一样**——导入时一比对就能拒收别的版本生成的 `.pyc`,避免字节码不兼容。 +- **flags**:PEP 552 引入的标志位。为 `0` 是默认的「按时间戳」校验;最低位为 `1` 则是「按哈希」校验(后 8 字节改存源文件内容的哈希,适合可重现构建)。 +- **mtime + size**(或 hash):用来判断**源文件有没有改过**——若源文件的修改时间或大小和 `.pyc` 里记的对不上,就说明缓存过期,需要重新编译。 + +我们可以亲手把一个 `.pyc` 的头读出来看看: + +```python +>>> import py_compile, struct, tempfile, os +>>> d = tempfile.mkdtemp() +>>> src = os.path.join(d, "m.py") +>>> open(src, "w").write("x = 1 + 2\n") +10 +>>> pyc = py_compile.compile(src) +>>> raw = open(pyc, "rb").read(16) +>>> int.from_bytes(raw[0:2], "little") # magic 数(本机为 3.12) +3531 +>>> struct.unpack(">> struct.unpack(" +.pyc 文件结构:16 字节头 + 序列化的 code object +3.7 的 pyc 文件由 16 字节头加上 marshal 序列化的 code object 组成。头的前 4 字节是 magic number(每个 Python 版本不同,用来拒收跨版本的 pyc),接着 4 字节是 PEP 552 的标志位。标志位为 0 时是「按时间戳」校验:后 8 字节存源文件的修改时间和大小;标志位最低位为 1 时是「按哈希」校验:后 8 字节存源文件内容的哈希。头之后就是被 marshal 序列化的 code object。 + + + + + + + + + + +.pyc = 16 字节头 + marshal 序列化的 code object + + + + magic4 字节 + flags4 字节·PEP552 + 后 8 字节(含义随 flags 而定)见下方两种情形 + code objectmarshal 序列化 + +┄┄┄ 16 字节头 ┄┄┄ +变长主体 + + +flags = 0 —— 按时间戳校验(默认) + + +mtime源文件修改时间 +size源文件大小 + +flags 最低位=1 —— 按哈希校验(PEP 552) + +source_hash源文件内容哈希(8 字节) + +导入时先比对 magic(版本是否匹配),再按 flags 校验源文件是否变过; +都通过就直接 marshal.loads 还原 code object,省去重新编译 + diff --git a/docs/compile/code-object/wordcode.svg b/docs/compile/code-object/wordcode.svg new file mode 100644 index 0000000..2a4b26a --- /dev/null +++ b/docs/compile/code-object/wordcode.svg @@ -0,0 +1,56 @@ + +wordcode:每条指令两字节,oparg 是各表的下标 +3.6 起 co_code 采用 wordcode:每条指令固定两字节,前一字节是操作码 opcode,后一字节是参数 oparg。oparg 往往是某张表的下标——例如 LOAD_FAST 0 表示取局部名表 co_varnames 的第 0 项 a。图中展示 c = a + b 的字节码,以及 LOAD_FAST 的 oparg 如何索引到 co_varnames。 + + + + + + + +co_code:每条指令两字节(opcode + oparg) +源码:c = a + b + + + + + + LOAD_FAST + 0 + + LOAD_FAST + 1 + + BINARY_ADD + 0 + + STORE_FAST + 2 + + + + opcodeoparg + 第 0 条(偏移 0) + 第 1 条(偏移 2) + 第 2 条(偏移 4) + 第 3 条(偏移 6) + + + +co_varnames + + a[0] + b[1] + c[2] + + + + +oparg 0 → co_varnames[0] 即名字 a;STORE_FAST 2 → 写回 c + diff --git a/docs/compile/source-to-bytecode/ast-tree.svg b/docs/compile/source-to-bytecode/ast-tree.svg new file mode 100644 index 0000000..76b9a01 --- /dev/null +++ b/docs/compile/source-to-bytecode/ast-tree.svg @@ -0,0 +1,55 @@ + +x = 1 + 2 的抽象语法树 AST +语法分析把 token 流组织成一棵抽象语法树。根是 Module,下面是一条 Assign 赋值语句:它的 targets 是名字 x(Store 上下文),value 是一个 BinOp 二元运算,BinOp 的 left 是常量 1、op 是加法 Add、right 是常量 2。树形结构清楚地表达了「把 1+2 的结果赋给 x」。 + + + + + + + + + + +x = 1 + 2 的抽象语法树(AST) + + + + + + + + + + + + targets + value + left + op + right + + + + + + Module + + Assign + + Name 'x'ctx = Store + + BinOp二元运算 + + Constant 13.7 为 Num(n=1) + + Add + + Constant 23.7 为 Num(n=2) + + diff --git a/docs/compile/source-to-bytecode/compile-pipeline.svg b/docs/compile/source-to-bytecode/compile-pipeline.svg new file mode 100644 index 0000000..aa7f44a --- /dev/null +++ b/docs/compile/source-to-bytecode/compile-pipeline.svg @@ -0,0 +1,54 @@ + +从源码到字节码的编译管线 +Python 源码先经词法分析(tokenizer.c)切成 token 流,再经语法分析(parser 与 ast.c)建成抽象语法树 AST,AST 经优化与符号表分析(ast_opt.c、symtable.c)后,由代码生成(compile.c)汇编成 code object,其中 co_code 就是字节码。这一整条管线把文本变成可被虚拟机执行的字节码。 + + + + + + + + + + + + + +编译:源码文本 → 字节码 + + + +源码文本 +x = 1 + 2 + + +token 流 +NAME = NUM + NUM + + +语法树 AST +Assign·BinOp + + +code object +co_code 字节码 + + + + + +词法分析语法分析代码生成 +tokenizer.cparser·ast.ccompile.c + + + + +先优化 AST,再分析符号表 +ast_opt.c(常量折叠)· symtable.c(名字作用域) + diff --git a/docs/compile/source-to-bytecode/index.md b/docs/compile/source-to-bytecode/index.md new file mode 100644 index 0000000..ac7784a --- /dev/null +++ b/docs/compile/source-to-bytecode/index.md @@ -0,0 +1,147 @@ +# 从源码到字节码:编译过程 + +第二部分我们认识了 Python 里各种「对象」。从这一部分开始,话题转向**这些对象是怎么跑起来的**——也就是 Python 虚拟机。 + +第一个要打破的直觉是:**Python 并不是「一行行直接解释源码」的**。它其实分成清清楚楚的两步——先把源码**编译**成一种叫「字节码」的中间指令,再由虚拟机逐条**执行**这些字节码。这一章讲前半步「编译」,后面几章讲后半步「执行」。 + +这件事其实你早就见过痕迹:导入一个模块后,旁边会冒出 `__pycache__/xxx.cpython-37.pyc`——那就是编译产物被缓存了下来。本章就来看:一段源码,是怎么一步步变成字节码的。 + +## 编译管线总览 + +CPython 把「源码 → 字节码」拆成几个前后衔接的阶段,每个阶段产出一种更结构化的中间形式: + +1. **词法分析**:把源码字符串切成一个个 **token**(最小的词法单元)。 +2. **语法分析**:把 token 流按语法规则组织成一棵**抽象语法树(AST)**。 +3. **符号表分析**:扫一遍 AST,搞清楚每个名字属于哪种作用域(局部、全局、自由变量……)。 +4. **代码生成**:遍历 AST、参考符号表,生成字节码,汇编成一个 **code object**。 + +![编译管线](compile-pipeline.svg) + +这条管线的总入口在 `PyAST_CompileObject`,它把 AST 一路加工成 `PyCodeObject`: + +`源文件:`[Python/compile.c](https://github.com/python/cpython/blob/v3.7.0/Python/compile.c#L301) + +```c +// Python/compile.c —— PyAST_CompileObject(精简) +if (!_PyAST_Optimize(mod, arena, c.c_optimize)) { // 先在 AST 层做优化(如常量折叠) + goto finally; +} +c.c_st = PySymtable_BuildObject(mod, filename, c.c_future); // 构建符号表 +...... +co = compiler_mod(&c, mod); // 遍历 AST 生成字节码,汇编成 code object +``` + +下面逐个阶段拆开看。一路上我们都拿同一行最简单的代码做例子:`x = 1 + 2`。 + +## 词法分析:源码切成 token 流 + +源码在内存里只是一串字符。**词法分析(lexing)**做的第一件事,就是把这串字符按词法规则切成一个个有类型的最小单元——**token**。比如把 `x = 1 + 2` 切成「名字 `x`」「运算符 `=`」「数字 `1`」「运算符 `+`」「数字 `2`」「换行」。 + +这一步由 [Parser/tokenizer.c](https://github.com/python/cpython/blob/v3.7.0/Parser/tokenizer.c#L1347) 里的 `tok_get` 完成,它每被调用一次就吐出一个 token。我们可以用标准库的 `tokenize` 模块亲眼看到这个切分结果: + +```python +>>> import tokenize, io +>>> src = "x = 1 + 2\n" +>>> for tok in tokenize.generate_tokens(io.StringIO(src).readline): +... print(f"{tokenize.tok_name[tok.type]:10} {tok.string!r}") +... +NAME 'x' +OP '=' +NUMBER '1' +OP '+' +NUMBER '2' +NEWLINE '\n' +ENDMARKER '' +``` + +![词法分析切出 token 流](token-stream.svg) + +可以看到,token 就是「**类型 + 文本**」的二元组:`x` 的类型是 `NAME`(名字),`=` 和 `+` 是 `OP`(运算符),`1`、`2` 是 `NUMBER`(数字字面量)。末尾的 `NEWLINE`、`ENDMARKER` 是表示「行结束」「文件结束」的特殊 token。词法分析此时还完全不关心这些 token 怎么组合、是否合法,它只负责「切词」。 + +## 语法分析:token 流组织成 AST + +光有一串 token 还不够——`1 + 2` 和 `+ 1 2` 的 token 几乎一样,但只有前者合法。**语法分析(parsing)**就是按 Python 的语法规则,判断 token 流是否合法,并把它组织成一棵能表达「谁包含谁、谁先算」的树。 + +CPython 在 3.7 里分两小步:先由解析器([Parser/parsetok.c](https://github.com/python/cpython/blob/v3.7.0/Parser/parsetok.c#L44))按语法生成一棵**具体语法树(CST)**,再由 [Python/ast.c](https://github.com/python/cpython/blob/v3.7.0/Python/ast.c#L768) 的 `PyAST_FromNodeObject` 把它转成更简洁的**抽象语法树(AST)**。CST 贴着语法规则、节点很啰嗦;AST 则只保留语义上要紧的结构,是后续阶段真正使用的形式。 + +标准库的 `ast` 模块能把 AST 直接打印出来: + +```python +>>> import ast +>>> print(ast.dump(ast.parse("x = 1 + 2"))) +Module(body=[Assign(targets=[Name(id='x', ctx=Store())], value=BinOp(left=Constant(value=1), op=Add(), right=Constant(value=2)))], type_ignores=[]) +``` + +> 上面是 Python 3.8+ 的输出形式;在 **3.7** 里,数字字面量显示为 `Num(n=1)` 而非 `Constant(value=1)`(`Constant` 是 3.8 起对 `Num`/`Str` 等的统一),也没有末尾的 `type_ignores`。树的结构是一样的。 + +把这串文字画成树就一目了然了: + +![x = 1 + 2 的抽象语法树](ast-tree.svg) + +根节点 `Module` 代表整个模块;它的 `body` 里是一条 `Assign`(赋值语句);`Assign` 的 `targets`(赋值目标)是名字 `x`,`value`(赋的值)是一个 `BinOp`(二元运算);`BinOp` 又拆成 `left`(左操作数 `1`)、`op`(运算符 `Add`)、`right`(右操作数 `2`)。 + +注意 `Name` 节点带了个 `ctx=Store()`——它标记这个 `x` 是被**写入**(赋值左边)而不是被读取。同一个名字读还是写,生成的字节码不同,这个信息从 AST 阶段就记下了。**树形结构天然表达了运算的优先级与嵌套**,这正是后续生成字节码所需要的。 + +## 符号表:分析名字的作用域 + +有了 AST,编译器还要回答一个关键问题:代码里每个名字,到底是**局部变量、全局变量,还是来自外层函数的自由变量**?这直接决定该用哪条取值指令(局部用 `LOAD_FAST`、全局用 `LOAD_GLOBAL`……,下一章会细讲)。回答这个问题的,就是**符号表(symbol table)**。 + +它由 [Python/symtable.c](https://github.com/python/cpython/blob/v3.7.0/Python/symtable.c#L249) 的 `PySymtable_BuildObject` 构建:再扫一遍 AST,为每个作用域记录其中出现的名字、以及每个名字的「身份」。判定规则很直白——**在本作用域里被赋值的名字就是局部的,只读不写、本地又没有的名字则到外层去找**。 + +标准库的 `symtable` 模块能把这套分析结果取出来: + +```python +>>> import symtable +>>> code = """ +... g = 0 +... def f(a): +... b = a + g +... return b +... """ +>>> top = symtable.symtable(code, "", "exec") +>>> f = top.lookup("f").get_namespace() # 取函数 f 的作用域 +>>> f.get_parameters() +('a',) +>>> for s in sorted(f.get_symbols(), key=lambda s: s.get_name()): +... kind = "局部" if s.is_local() else ("全局" if s.is_global() else "其他") +... print(s.get_name(), "->", kind) +... +a -> 局部 +b -> 局部 +g -> 全局 +``` + +![符号表分析名字的作用域](symtable-scope.svg) + +结果正合直觉:参数 `a` 和在函数里被赋值的 `b` 都是**局部**;而 `g` 在函数里只被读取、没有被赋值,于是判定为**全局**,运行时要到外层模块作用域去找。编译器拿到这张表,才能为 `a`、`b`、`g` 分别生成正确的取值指令。 + +## 代码生成:AST 变成 code object + +最后一步,编译器遍历 AST、参考符号表,把每个节点翻译成对应的字节码指令,再**汇编**成一个 **code object**(`PyCodeObject`)。它由 [Python/compile.c](https://github.com/python/cpython/blob/v3.7.0/Python/compile.c#L1512) 的 `compiler_mod` 驱动,内部对 AST 做深度遍历:遇到 `BinOp` 就先生成「把两个操作数压栈」、再生成「相加」指令,遇到 `Assign` 就生成「把栈顶存进 `x`」…… + +`compile()` 这个内建函数能让我们直接拿到编译产物: + +```python +>>> co = compile("x = 1 + 2", "", "exec") +>>> type(co).__name__ +'code' +>>> co.co_consts # 用到的常量 +(3, None) +>>> co.co_names # 用到的全局名字 +('x',) +``` + +`compile()` 返回的就是一个 code object,里面 `co_code` 是字节码、`co_consts` 是常量表、`co_names` 是名字表……这些字段下一章会逐个拆解。 + +这里有个有意思的细节:`co_consts` 是 `(3, None)`——**源码里写的是 `1 + 2`,常量表里却直接是 `3`**。这正是开头管线图里「AST 优化」那一步干的:[Python/ast_opt.c](https://github.com/python/cpython/blob/v3.7.0/Python/ast_opt.c#L802) 的 `_PyAST_Optimize`(3.7 新增)会在编译期就把 `1 + 2` 这种**常量表达式直接折叠成结果**,省得运行时再算一遍。此外汇编完还有一道字节码层的窥孔优化([Python/peephole.c](https://github.com/python/cpython/blob/v3.7.0/Python/peephole.c#L222))做些指令级的清理。所以「编译」不只是翻译,还顺带做了优化。 + +--- + +小结一下这条编译管线: + +- Python 是**先编译成字节码、再执行**的;本章讲的是「编译」这半步,产物是 code object; +- 编译分四个阶段:**词法分析**(源码 → token 流,`tokenizer.c`)→ **语法分析**(token → AST,`parser` + `ast.c`)→ **符号表分析**(定每个名字的作用域,`symtable.c`)→ **代码生成**(AST → 字节码,`compile.c`); +- 每一步都能用标准库亲手观察:`tokenize` 看 token、`ast` 看语法树、`symtable` 看作用域、`compile` 看产物; +- 编译期还会做优化:AST 层的常量折叠(`ast_opt.c`)把 `1 + 2` 直接折成 `3`,字节码层再做窥孔优化(`peephole.c`)。 + +下一章,我们就钻进编译的产物——**code object** 的内部结构,看看字节码、常量表、名字表到底长什么样,以及它是怎么被缓存成 `.pyc` 文件的。 diff --git a/docs/compile/source-to-bytecode/symtable-scope.svg b/docs/compile/source-to-bytecode/symtable-scope.svg new file mode 100644 index 0000000..4ee5954 --- /dev/null +++ b/docs/compile/source-to-bytecode/symtable-scope.svg @@ -0,0 +1,51 @@ + +符号表:分析每个名字的作用域 +符号表为每段代码分析其中每个名字属于哪种作用域。在函数 f(a) 里,参数 a 和赋值得到的 b 都是局部变量;而 g 在函数里只被读取、没有被赋值,于是被判定为全局变量,需要到外层模块作用域去找。编译器据此为不同名字生成不同的取值字节码。 + + + + + + + + + +符号表:判定每个名字的作用域 + + + +模块(全局作用域) +g = 0 + + + +g +全局变量 + + + +函数 f 的局部作用域 + + def f(a): + b = a + g + return b + + + + +a +参数·局部 + +b +赋值·局部 + + + +本地没赋值 → 判为全局,去外层找 + diff --git a/docs/compile/source-to-bytecode/token-stream.svg b/docs/compile/source-to-bytecode/token-stream.svg new file mode 100644 index 0000000..e678631 --- /dev/null +++ b/docs/compile/source-to-bytecode/token-stream.svg @@ -0,0 +1,53 @@ + +词法分析:源码切成 token 流 +词法分析把源码字符串 x = 1 + 2 逐字符扫描,切成一个个有类型的最小单元 token:名字 x 是 NAME,等号和加号是 OP,1 和 2 是 NUMBER,行尾是 NEWLINE。token 流是后续语法分析的输入。 + + + + + + + + + +词法分析:x = 1 + 2 切成 token 流 + + + +x = 1 + 2 + + + + + + NAME + 'x' + + + OP + '=' + + + NUMBER + '1' + + + OP + '+' + + + NUMBER + '2' + + + NEW + LINE + \n + +每个 token 是「类型 + 文本」的最小单元,是语法分析的输入 + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..4f360c3 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,47 @@ +# 前言 + +本项目致力于对 Python 3.7 的源码分析,深度参考陈儒大大的《Python 源码剖析》,编写 Python 3 的版本。 + +希望各位 Python 爱好者能参与其中,一起探索 Python 魔法背后的奥秘! + +## Roadmap + +在《Python 源码剖析》的基础上,按更贴合 CPython 3.7 的结构重新编排,分为六个部分:准备、对象与类型系统、编译、虚拟机、运行时、内存管理。 + +- [x] 第 1 部分:准备 + - [x] 前言 + - [x] Python 源代码的组织 + - [x] Windows 环境下编译 Python + - [x] UNIX/Linux 环境下编译 Python + - [x] 修改 Python 源码 +- [x] 第 2 部分:对象与类型系统 + - [x] Python 对象初探 + - [x] Python 整数对象 + - [x] Python 浮点数对象 + - [x] Python 字符串对象 + - [x] Python bytes 与 bytearray 对象 + - [x] Python 列表对象 + - [x] Python 元组对象 + - [x] Python 字典对象 + - [x] Python 集合对象 + - [x] Python 布尔与 None 对象 + - [x] Python 类型对象与自定义类 +- [x] 第 3 部分:编译 + - [x] 从源码到字节码(编译过程) + - [x] 编译的产物:code object 与 pyc +- [x] 第 4 部分:虚拟机 + - [x] Python 虚拟机框架(帧对象与求值循环) + - [x] 一般表达式与名字空间 + - [x] 控制流:跳转、循环与迭代器 + - [x] 异常机制:block 栈与栈展开 + - [x] 函数机制:调用、参数与闭包 + - [x] 生成器与协程 +- [x] 第 5 部分:运行时 + - [x] Python 运行环境初始化 + - [x] 模块与 import 机制 + - [x] 多线程与 GIL +- [x] 第 6 部分:内存管理 + - [x] 内存分配与引用计数(pymalloc) + - [x] 循环垃圾回收(分代 GC) +- [x] 第 7 部分:实战 + - [x] 动手:用 Python 写一个迷你 Python 虚拟机(WASM 交互) diff --git a/docs/memory/allocation-refcount/arena-pool-block.svg b/docs/memory/allocation-refcount/arena-pool-block.svg new file mode 100644 index 0000000..2280f2d --- /dev/null +++ b/docs/memory/allocation-refcount/arena-pool-block.svg @@ -0,0 +1,53 @@ + +arena / pool / block 三层内存结构 +pymalloc 一次向系统要一大块再自己切,分三级。arena 竞技场 256KB,向操作系统 mmap 批发的大块。一个 arena 切成约 64 个 pool 池,每个 pool 占一个内存页 4KB,一个 pool 只服务一种大小规格。一个 pool 再切成许多等大的 block 块,这才是真正交到对象手里的分配单元。图示一个 arena 含多个 pool,其中一个 pool 被放大,切成同样大小的 block,部分已用部分空闲。 + + + + + + + + + + +arena → pool → block:批发再零售 + + + +arena 256 KB +向 OS(mmap)批发 + + + + + + + + + + +切成约 64 个 pool(每个 4 KB) + + + + + + +一个 pool 4 KB +只服务一种 size class + + block 已用 + block 已用 + 空闲 + 空闲 + 空闲 + 空闲 + +切成等大 block —— 交给对象的分配单元 + diff --git a/docs/memory/allocation-refcount/block-freelist.svg b/docs/memory/allocation-refcount/block-freelist.svg new file mode 100644 index 0000000..dbfeb7f --- /dev/null +++ b/docs/memory/allocation-refcount/block-freelist.svg @@ -0,0 +1,43 @@ + +分配/释放等于空闲链表的推拉 +每个 pool 内部维护一条空闲 block 链表。分配一个 block 就是从对应规格 pool 的空闲链表头取下一块,几个指针操作,不碰系统调用。释放一个 block 就是把它塞回空闲链表头,同样几个指针操作,内存不还给系统,留着下次同规格请求复用。于是造一个小对象又销毁这种最高频操作,退化成链表的一推一拉,快得很。 + + + + + + + + + + +分配/释放 = 空闲链表的一推一拉 + + +pool 的空闲 block 链表 + + block + block + block + + + +head → + + + + +分配 = 取下 + + + + +释放 = 塞回 + +几个指针操作,不碰系统调用;释放的内存留着同规格复用,碎片受控 + diff --git a/docs/memory/allocation-refcount/index.md b/docs/memory/allocation-refcount/index.md new file mode 100644 index 0000000..dd6262c --- /dev/null +++ b/docs/memory/allocation-refcount/index.md @@ -0,0 +1,168 @@ +# 内存分配与引用计数(pymalloc) + +前面五部分,对象一直是我们的主角——可我们从没认真问过两个最基础的问题:**这些对象的内存从哪来?又在什么时候被回收?** 这正是最后一部分「内存管理」要回答的。 + +这一章先讲两件事,恰好对应那两个问题:**引用计数**——对象生死的裁决者,决定一个对象**何时死**;**pymalloc**——CPython 为小对象量身定做的分配器,决定对象的内存**从哪来、有多快**。上一章我们说 GIL 之所以存在就是为了保护引用计数,现在就来看它本身。 + +## 引用计数:对象生死的裁决者 + +回到第二部分的起点:每个对象的头部都有个 `ob_refcnt`,记录「有多少处引用着我」。这个数字的增减,由两个无处不在的宏掌管: + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L793) + +```c +// Include/object.h —— 引用计数的增与减(精简) +#define Py_INCREF(op) (((PyObject *)(op))->ob_refcnt++) // 多一处引用:加一 + +#define Py_DECREF(op) \ + do { \ + PyObject *tmp = (PyObject *)(op); \ + if (--tmp->ob_refcnt != 0) \ + ; \ + else \ + _Py_Dealloc(tmp); /* 计数归零 → 立即销毁 */ \ + } while (0) +``` + +规则朴素得近乎天真:**多一处引用就 `+1`,少一处就 `-1`;一旦减到 `0`,对象当场被销毁**(`_Py_Dealloc` 调用类型的 `tp_dealloc` 释放它)。「当场」二字是引用计数最大的特点——它是**即时、确定性**的回收:对象死在它最后一个引用消失的那一刻,不拖延、不等某个回收器来扫。 + +![引用计数:从诞生到销毁](refcount-lifecycle.svg) + +我们可以亲眼看着这个数字变动(`sys.getrefcount` 读的就是 `ob_refcnt`): + +```python +>>> import sys +>>> a = [] # 新对象,一处引用 +>>> sys.getrefcount(a) # 显示 2 = 1(a)+ 1(getrefcount 的参数也算一处) +2 +>>> b = a # 又一处引用 +>>> sys.getrefcount(a) +3 +>>> del b # 少一处 +>>> sys.getrefcount(a) +2 +``` + +> `getrefcount` 的结果总比你以为的多 1——因为把对象作为参数传进去时,参数本身也构成一处临时引用。 + +引用计数的优点很迷人:**回收及时**(内存不囤积)、**行为确定**(`del` 或离开作用域,对象立即清理,析构时机可预测)、实现也直观。代价是:**每一次赋值、传参、返回都要改计数**,频繁而细碎;而且——它有一个根本性的盲区:**循环引用**。两个对象互相引用,计数永远不为 0,谁也回收不了。这个洞要靠下一章的「循环垃圾回收」来补,本章先按下不表。 + +## 内存的分层:为什么不直接用 malloc + +裁决了「何时死」,再看「内存从哪来」。最朴素的办法是:要对象就 `malloc`、销毁就 `free`。但 Python 程序的特点是**疯狂地创建、销毁大量小对象**——一个循环里造几百万个小整数、小元组是家常便饭。直接用系统 `malloc`/`free` 会有两个问题:**慢**(每次都可能陷入系统调用、走通用分配器的复杂逻辑)和**碎片**(大量小块把堆搞得千疮百孔)。 + +CPython 的对策是**分层**——在系统 `malloc` 之上叠几层缓存,让绝大多数小对象的分配走快速路径: + +![内存分配的分层](memory-layers.svg) + +从下往上: + +- **第 0 层:系统 `malloc`** —— 兜底。大块内存、以及 pymalloc 自己要的大块,都向它要。 +- **第 1 层:pymalloc** —— 小对象(≤512 字节)的专用分配器,本章主角。 +- **第 2 层:对象级 free list** —— 某些高频类型(`float`、`tuple`、`frame`……)自带的缓存,连 pymalloc 都不必惊动。 + +下面从中间这层 pymalloc 说起。 + +## pymalloc:小对象的专用分配器 + +pymalloc 的分工很清晰,由一个阈值划界: + +`源文件:`[Objects/obmalloc.c](https://github.com/python/cpython/blob/v3.7.0/Objects/obmalloc.c#L795) + +```c +// Objects/obmalloc.c +#define SMALL_REQUEST_THRESHOLD 512 // 小于等于 512 字节才归 pymalloc 管 +``` + +**请求 ≤ 512 字节,走 pymalloc;大于 512 字节,直接转交系统 `malloc`。** 因为 Python 里绝大多数对象都很小(一个 `int`、一个小 `tuple` 都在百字节内),所以这条快路径覆盖了实际中的大部分分配: + +![pymalloc 的分流](pymalloc-routing.svg) + +那 pymalloc 凭什么比直接 `malloc` 快?秘密在它的三层内存结构。 + +## arena / pool / block:三层内存结构 + +pymalloc 不会零敲碎打地向系统要内存,而是**一次要一大块,再自己切**。这一大块到一个对象的距离,分三级: + +`源文件:`[Objects/obmalloc.c](https://github.com/python/cpython/blob/v3.7.0/Objects/obmalloc.c#L833) + +```c +// Objects/obmalloc.c +#define ARENA_SIZE (256 << 10) // arena:256 KB,向操作系统申请的大块 +#define POOL_SIZE SYSTEM_PAGE_SIZE // pool:一个内存页,通常 4 KB +``` + +- **arena(竞技场,256 KB)**:pymalloc 向操作系统(`mmap`)批发的大块内存。要内存时一次拿 256KB,摊薄了系统调用的成本。 +- **pool(池,4 KB)**:一个 arena 切成约 64 个 pool,每个 pool 占一个内存页。**一个 pool 只服务一种「大小规格」**。 +- **block(块)**:一个 pool 再切成许多等大的 block——这才是**真正交到对象手里的分配单元**。 + +![arena / pool / block 三层结构](arena-pool-block.svg) + +「一个 pool 只服务一种大小」是关键设计。pymalloc 把 1~512 字节的请求按 **8 字节对齐**归入 **64 个大小规格(size class)**:1~8 字节的请求都给 8 字节的 block、9~16 字节的都给 16 字节的 block……以此类推: + +`源文件:`[Objects/obmalloc.c](https://github.com/python/cpython/blob/v3.7.0/Objects/obmalloc.c#L716) + +``` +// Objects/obmalloc.c —— size class 对照表(节选) +请求字节数 分配的 block 大小 size class 序号 + 1- 8 8 0 + 9-16 16 1 + 17-24 24 2 + ... ... ... + 505-512 512 63 +``` + +![size class:按 8 字节归类](size-classes.svg) + +申请 30 字节,pymalloc 把它归到「32 字节」这一规格(序号 3),从一个专门切成 32 字节 block 的 pool 里取一块给你。同一规格的对象,永远从同规格的 pool 里分配。 + +## 为什么快:分配/释放只是链表的一推一拉 + +三层结构带来的好处,在「分配」和「释放」时立刻兑现。每个 pool 内部维护一条**空闲 block 链表**: + +- **分配一个 block**:从对应规格 pool 的空闲链表头**取下**一块——几个指针操作,**不碰系统调用**; +- **释放一个 block**:把它**塞回**空闲链表头——同样几个指针操作,内存不还给系统,留着下次同规格的请求复用。 + +![分配/释放 = 空闲链表的推拉](block-freelist.svg) + +于是「造一个小对象、又销毁」这种最高频的操作,退化成了**链表的一推一拉**,快得很。而且同规格对象共用 pool,碎片被牢牢限制在「规格内」,不会蔓延。这正是 pymalloc 为「海量小对象」场景优化的精髓。 + +> 当一个 pool 里的 block 全部释放,pool 可被改作其他规格;当一个 arena 里所有 pool 都空了,这 256KB 才可能被还给操作系统。所以 Python 进程的内存有时「居高不下」,是因为内存被留在 pymalloc 的各级空闲链表里待复用,而非泄漏。 + +## 再上一层:对象级 free list + +还有比 pymalloc 更快的——**干脆连分配器都不惊动**。第二部分里我们多次见过:`float`、`tuple`、`list`、`frame` 等高频类型,各自维护一个**对象级 free list**。一个 `float` 被销毁时,它的内存块不还给 pymalloc,而是挂进 float 专属的 free list;下次要新 `float`,直接从这条链表上摘一个来用: + +![对象级 free list:同类型直接复用](object-freelist.svg) + +```python +>>> id(1.5 + 1.5) # 造一个临时 float,用完即弃 +4302590512 +>>> id(2.0 + 2.0) # 新 float 很可能复用了刚才那块内存 +4302590512 # 同一地址——它来自 float 的 free list +``` + +这层缓存绕过了「算 size class、找 pool、取 block」的全部步骤,连同类型对象的初始化都省了一半。代价是每个类型要自己实现和维护这份缓存——只有最高频的类型才值得。 + +## 串起来:一个小对象的内存之旅 + +把这一章串成一条线,新建一个小对象(比如 `x = 3.5`)的内存从哪来: + +1. **先问对象级 free list**:float 的 free list 里有现成的吗?有就直接拿(最快); +2. **再问 pymalloc**:没有就向 pymalloc 要内存。对象很小(≤512 字节),按 size class 找到对应 pool,从空闲链表摘一个 block; +3. **pool 也没空块**:从 arena 里切一个新 pool;arena 也不够,就向系统 `mmap` 一块新的 256KB arena; +4. 对象用完,`x` 的引用消失 → `Py_DECREF` 把 `ob_refcnt` 减到 0 → `_Py_Dealloc` 销毁它 → 内存块**回到 float 的 free list 或 pool 的空闲链表**,等待下一次复用。 + +整条链路,绝大多数时候都停在第 1、2 步,既不碰系统调用、也不产生碎片——这就是 Python「小对象满天飞却依然轻快」的底气。 + +--- + +小结一下内存分配与引用计数: + +- **引用计数**是对象生死的裁决者:`Py_INCREF`/`Py_DECREF` 增减 `ob_refcnt`,**减到 0 立即 `_Py_Dealloc` 销毁**——回收即时、行为确定;代价是每次引用变动都要改计数,且**管不了循环引用**(下一章补); +- 直接用系统 `malloc` 应付海量小对象既**慢**又**碎**,CPython 用**分层**化解:对象级 free list → pymalloc → 系统 `malloc`; +- **pymalloc** 接管 **≤512 字节**的小请求,用 **arena(256KB)→ pool(4KB,一种规格)→ block(分配单元)** 三层结构管理内存;按 **8 字节对齐**分成 **64 个 size class**; +- 分配/释放一个 block 只是 pool 空闲链表的**一推一拉**,不碰系统调用、碎片受控——这是 pymalloc 快的根本; +- 最上层的**对象级 free list**让 `float`、`tuple` 等高频类型直接复用销毁的对象,连 pymalloc 都省了。 + +引用计数干净利落,却栽在**循环引用**上:`a` 引用 `b`、`b` 又引用 `a`,哪怕外界再无人引用它们,两者的计数也永远停在 1,成了回收不掉的「孤岛」。CPython 如何揪出并清除这些孤岛?最后一章,我们拆 **循环垃圾回收(分代 GC)**。 diff --git a/docs/memory/allocation-refcount/memory-layers.svg b/docs/memory/allocation-refcount/memory-layers.svg new file mode 100644 index 0000000..b56a9e0 --- /dev/null +++ b/docs/memory/allocation-refcount/memory-layers.svg @@ -0,0 +1,39 @@ + +内存分配的分层 +CPython 在系统 malloc 之上叠几层缓存,让绝大多数小对象的分配走快速路径。从上往下:第 2 层对象级 free list,某些高频类型如 float、tuple、frame 自带的缓存,连 pymalloc 都不必惊动,最快;第 1 层 pymalloc,小对象小于等于 512 字节的专用分配器;第 0 层系统 malloc,兜底,大块内存以及 pymalloc 自己要的大块都向它要。请求优先走上层,命中即返回。 + + + + + + + + + + +小对象分配,优先走上层快路径 + + +第 2 层 对象级 free list +float / tuple / frame 等高频类型的缓存(最快) + + +第 1 层 pymalloc +小对象(≤512 字节)专用分配器 ← 本章主角 + + +第 0 层 系统 malloc +兜底:大块内存、arena 都向它要 + + + +未命中 +才下沉 + +越靠上越快;绝大多数小对象停在上两层,不碰系统调用 + diff --git a/docs/memory/allocation-refcount/object-freelist.svg b/docs/memory/allocation-refcount/object-freelist.svg new file mode 100644 index 0000000..c92e3b8 --- /dev/null +++ b/docs/memory/allocation-refcount/object-freelist.svg @@ -0,0 +1,46 @@ + +对象级 free list:同类型直接复用 +比 pymalloc 更快的是干脆连分配器都不惊动。float、tuple、list、frame 等高频类型各自维护一个对象级 free list。一个 float 被销毁时,它的内存块不还给 pymalloc,而是挂进 float 专属的 free list;下次要新 float,直接从这条链表上摘一个来用。这层缓存绕过了算 size class、找 pool、取 block 的全部步骤,连同类型对象的初始化都省了一半。代价是每个类型要自己实现维护这份缓存,只有最高频的类型才值得。 + + + + + + + + + + +float 的 free list:销毁即复用,绕过 pymalloc + + + +float free list + + 空 float + 空 float + 空 float + + + + +销毁一个 float +不还给 pymalloc + +挂回 + + + +要个新 float +直接摘一个 + +取下 + +绕过「算 size class、找 pool、取 block」的全部步骤 +id(1.5+1.5) == id(2.0+2.0) —— 复用了同一块内存 + diff --git a/docs/memory/allocation-refcount/pymalloc-routing.svg b/docs/memory/allocation-refcount/pymalloc-routing.svg new file mode 100644 index 0000000..7002dae --- /dev/null +++ b/docs/memory/allocation-refcount/pymalloc-routing.svg @@ -0,0 +1,46 @@ + +pymalloc 的分流:512 字节阈值 +pymalloc 由一个阈值划界,SMALL_REQUEST_THRESHOLD 等于 512。请求小于等于 512 字节走 pymalloc 快路径,大于 512 字节直接转交系统 malloc。因为 Python 里绝大多数对象都很小,一个 int、一个小 tuple 都在百字节内,所以这条快路径覆盖了实际中的大部分分配。 + + + + + + + + + + +512 字节阈值划界 + + +内存请求 +PyObject_Malloc(n) + + + +n ≤ 512 ? +阈值 + + + + +pymalloc +小对象快路径 + + + + + +系统 malloc +大对象 + + + +int、小 tuple 都在百字节内 → 大部分分配走 pymalloc + diff --git a/docs/memory/allocation-refcount/refcount-lifecycle.svg b/docs/memory/allocation-refcount/refcount-lifecycle.svg new file mode 100644 index 0000000..3807e4e --- /dev/null +++ b/docs/memory/allocation-refcount/refcount-lifecycle.svg @@ -0,0 +1,44 @@ + +引用计数:从诞生到销毁 +对象头部的 ob_refcnt 记录有多少处引用着它。多一处引用就 Py_INCREF 加一,少一处就 Py_DECREF 减一,一旦减到 0,对象当场被 _Py_Dealloc 销毁。图示一个列表对象:创建时计数 1,b=a 后计数 2,del b 后计数 1,del a 后计数 0 立即销毁。这是即时、确定性的回收,对象死在它最后一个引用消失的那一刻。 + + + + + + + + + +ob_refcnt 减到 0,对象当场销毁 + + + + refcnt = 1 + a = [] + + + refcnt = 2 + b = a INCREF + + + refcnt = 1 + del b DECREF + + + refcnt = 0 + del a → _Py_Dealloc + + + + + + +即时、确定性回收 —— 对象死在最后一个引用消失的那一刻 +代价:每次赋值/传参/返回都要改计数;且管不了循环引用 + diff --git a/docs/memory/allocation-refcount/size-classes.svg b/docs/memory/allocation-refcount/size-classes.svg new file mode 100644 index 0000000..0d86ce7 --- /dev/null +++ b/docs/memory/allocation-refcount/size-classes.svg @@ -0,0 +1,34 @@ + +size class:按 8 字节归类 +pymalloc 把 1 到 512 字节的请求按 8 字节对齐归入 64 个大小规格 size class。1 到 8 字节的请求都给 8 字节的 block,序号 0;9 到 16 字节的都给 16 字节,序号 1;17 到 24 给 24,序号 2;以此类推到 505 到 512 给 512,序号 63。申请 30 字节会被归到 32 字节这一规格序号 3,从专门切成 32 字节 block 的 pool 里取一块。同一规格的对象永远从同规格的 pool 分配。 + + + + + + + +1–512 字节 → 64 个规格(8 字节一档) + + + + + 请求字节数 + block 大小 + 规格序号 + + 1 – 880 + 9 – 16161 + 17 – 24242 + 25 – 32 ← 申请 30323 + + 505 – 51251263 + + +申请 30 字节 → 归 32 字节规格 → 从切成 32 字节 block 的 pool 取一块 + diff --git a/docs/memory/garbage-collection/cycle-island.svg b/docs/memory/garbage-collection/cycle-island.svg new file mode 100644 index 0000000..b387ad1 --- /dev/null +++ b/docs/memory/garbage-collection/cycle-island.svg @@ -0,0 +1,43 @@ + +循环引用的孤岛 +引用计数管不了循环引用。对象 a 引用 b、b 又引用 a 成环,del a、b 切断所有外部引用后,外界再也无法访问这两个对象,它们是垃圾。但引用计数无能为力:a 还被 b.ref 指着、b 还被 a.ref 指着,两者计数都是 1 永不归零,于是永远不会被回收,成了内存里访问不到又清不掉的孤岛。打开循环 GC 手动收一次,孤岛立刻被清除。 + + + + + + + + + + +成环对象计数永不归零 → 清不掉的孤岛 + + + +外部 +del a, b 后无引用 + +已切断 + + + +a +refcnt=1 + +b +refcnt=1 + + + +a.ref → b +b.ref → a + +外部已无引用,但互相指着对方 → 计数停在 1,引用计数回收不了 +这正是循环 GC 的用武之地(gc.collect() 可清除) + diff --git a/docs/memory/garbage-collection/find-unreachable.svg b/docs/memory/garbage-collection/find-unreachable.svg new file mode 100644 index 0000000..d6d0925 --- /dev/null +++ b/docs/memory/garbage-collection/find-unreachable.svg @@ -0,0 +1,47 @@ + +用引用计数差值找出不可达的环 +用 a 和 b 互引的例子走一遍。两者真实计数都是 1,且这唯一的引用都来自对方即集合内部,subtract_refs 减完后 gc_refs 双双归 0。又没有任何外部可达对象引用它们,可达性传播 move_unreachable 也救不回它们,于是被准确判定为不可达垃圾。作为对比,若 c 被外部一个根引用着,它的 gc_refs 减完仍大于 0,是确定可达,会留下。 + + + + + + + + + + + +减完后看 gc_refs:归 0 的就是垃圾 + + +无外部引用的环 → 垃圾 + +a +gc_refs=0 + +b +gc_refs=0 + + +引用全来自对方 → 减完归 0 → 不可达 + + +有外部根引用 → 可达 + +外部根 +栈/全局 + +c +gc_refs>0 + +外部引用未被减掉 → 留下 + +再从「确定可达」对象传播可达性(move_unreachable),救回被牵连者 +最终仍归 0 且没被救回的,才是真垃圾 + diff --git a/docs/memory/garbage-collection/gc-pipeline.svg b/docs/memory/garbage-collection/gc-pipeline.svg new file mode 100644 index 0000000..9856f0b --- /dev/null +++ b/docs/memory/garbage-collection/gc-pipeline.svg @@ -0,0 +1,56 @@ + +一次循环回收的全流程 +把整章串成一条线,一次循环回收的全流程:update_refs 把每个候选对象的真实计数抄进 gc_refs;subtract_refs 减掉所有内部引用,得到外部引用数;move_unreachable 从外部可达对象传播可达性,分出可达与不可达;对不可达对象调 tp_clear 打破环;环一断,引用计数立即把它们逐个 Py_DECREF 销毁。前三步找垃圾,后两步清垃圾,清的活儿仍交还引用计数。 + + + + + + + + + + + +一次循环回收:前三步找垃圾,后两步清垃圾 + + +① update_refs +gc_refs = 真实计数 +抄副本 + + +② subtract_refs +减内部引用 +得外部引用数 + + +③ move_unreach +传播可达性 +分出真垃圾 + + +④ tp_clear +置空引用 +打破环 + + +⑤ 引用计数 +Py_DECREF +逐个销毁 + + + + + + +找垃圾(引用计数差值) +清垃圾(交还引用计数) + + + diff --git a/docs/memory/garbage-collection/generations.svg b/docs/memory/garbage-collection/generations.svg new file mode 100644 index 0000000..fb7527c --- /dev/null +++ b/docs/memory/garbage-collection/generations.svg @@ -0,0 +1,49 @@ + +三代与对象的升代 +GC 把被追踪对象分三代,基于分代假说大多数对象朝生暮死。新对象在第 0 代,最年轻、回收最频繁,每当分配数减释放数累积超过 700 就触发一次第 0 代回收。熬过第 0 代回收的对象升入第 1 代,第 0 代回收满 10 次顺带回收一次第 1 代。同理第 1 代回收满 10 次触发一次涵盖全部三代的完整回收。第 2 代最老、最少回收。海量朝生暮死的新对象在廉价的第 0 代回收里就被清掉,少数长寿对象被请到高代越老越少被打扰。 + + + + + + + + + + +三代:年轻代扫得勤,老年代扫得疏 + + +第 0 代 +新对象 +阈值 700 +回收最频繁 +分配−释放 > 700 触发 + + +第 1 代 +熬过一次的 +阈值 10 +第 0 代收 10 次 +顺带收一次 + + +第 2 代 +长寿对象 +阈值 10 +最少回收 +完整回收才涉及 + + + +升代 +升代 + +分代假说:对象多朝生暮死 → 把开销集中在最可能产出垃圾的年轻代 +gc.get_threshold() == (700, 10, 10) + diff --git a/docs/memory/garbage-collection/index.md b/docs/memory/garbage-collection/index.md new file mode 100644 index 0000000..9a4db06 --- /dev/null +++ b/docs/memory/garbage-collection/index.md @@ -0,0 +1,185 @@ +# 循环垃圾回收(分代 GC) + +上一章结尾,引用计数留下了一个无解的尴尬:**循环引用**。`a` 引用 `b`、`b` 又引用 `a`,哪怕外界再无人引用它们,两者的计数也永远停在 1,成了回收不掉的「孤岛」。这一章——也是全书的最后一章——就来看 CPython 如何揪出并清除这些孤岛:**循环垃圾回收(分代 GC)**。 + +## 问题:引用计数管不了的孤岛 + +先把问题摆到眼前。造一个自我引用的循环,再切断所有外部引用,看看引用计数能不能回收它: + +```python +>>> import gc, sys +>>> gc.disable() # 先关掉循环 GC,只剩引用计数 +>>> class Node: pass +>>> a = Node(); b = Node() +>>> a.ref = b; b.ref = a # a ↔ b 互相引用,成环 +>>> del a, b # 切断所有外部引用 +``` + +`del a, b` 之后,外界再也无法访问这两个对象——它们是**垃圾**。但引用计数无能为力:`a` 还被 `b.ref` 指着、`b` 还被 `a.ref` 指着,**两者的计数都是 1,永不归零**,于是永远不会被 `Py_DECREF` 回收。它们就成了内存里一座访问不到、又清不掉的孤岛: + +![循环引用的孤岛](cycle-island.svg) + +打开 GC 手动收一次,孤岛立刻被清除——这正是循环 GC 的用武之地: + +```python +>>> gc.enable() +>>> gc.collect() # 手动触发循环回收 +2 # 回收了 2 个不可达对象(a、b) +``` + +**引用计数负责绝大多数即时回收,循环 GC 专门补上「成环垃圾」这个洞**——两者是互补的搭档,而非替代。 + +## 只有容器才需要被追踪 + +循环 GC 并不盯着所有对象。能形成循环的,只有**能引用别的对象的容器**——`list`、`dict`、`set`、实例对象、`tuple` 等。而 `int`、`float`、`str` 这类「原子」对象只装数据、不引用别的对象,**根本不可能成环**,自然无需 GC 操心,它们一生只靠引用计数。 + +判断标准很明确:一个类型若实现了 `tp_traverse`(能「遍历自己引用了谁」),它的实例就会被 GC 追踪。被追踪的对象,内存里会在对象结构**前面**多挂一个 GC 头 `PyGC_Head`: + +`源文件:`[Include/objimpl.h](https://github.com/python/cpython/blob/v3.7.0/Include/objimpl.h#L252) + +```c +// Include/objimpl.h —— GC 头,挂在对象结构「前面」 +typedef union _gc_head { + struct { + union _gc_head *gc_next; // 把同代对象串成双向链表 + union _gc_head *gc_prev; + Py_ssize_t gc_refs; // GC 工作时的私有计数(下面的主角) + } gc; + double dummy; +} PyGC_Head; +``` + +![被追踪的容器 vs 不被追踪的原子对象](tracked-untracked.svg) + +这个 `gc_next`/`gc_prev` 把所有被追踪的对象串成**双向链表**(按「代」分组,见后文),GC 扫描时就沿着链表走。而那个 `gc_refs`,是接下来整个算法的核心道具。 + +## 核心难题:怎么判断「从外部不可达」 + +回收成环垃圾的关键,是判断一个对象**是否还能从「垃圾集合之外」访问到**。直觉做法是从根(全局、栈上的变量……)出发遍历,标记所有可达对象,剩下的就是垃圾——但 CPython 用了一个更巧妙、不需要枚举根的办法:**用引用计数做减法**。 + +思路是这样的:一个对象的 `ob_refcnt`,等于「指向它的所有引用数」。这些引用,一部分来自**垃圾候选集合内部**(比如环里其他对象指向它),一部分来自**集合外部**(真正的根)。**如果我们把「来自集合内部的引用」从计数里减掉,剩下的就是「来自外部的引用数」——它若大于 0,说明外部还够得着它,不是垃圾。** + +![引用计数差值:减掉内部引用,剩下外部引用](refcount-diff.svg) + +这套减法分两步,对应两个函数。 + +**第一步 `update_refs`:把真实计数抄一份到 `gc_refs`。** 不能直接改 `ob_refcnt`(那会破坏正常的引用计数),于是 GC 头里的 `gc_refs` 就派上用场——把每个候选对象的 `ob_refcnt` 复制进去,作为可以随意涂改的工作副本: + +`源文件:`[Modules/gcmodule.c](https://github.com/python/cpython/blob/v3.7.0/Modules/gcmodule.c#L237) + +```c +// Modules/gcmodule.c —— update_refs(精简) +for (gc = containers->gc.gc_next; gc != containers; gc = gc->gc.gc_next) { + _PyGCHead_SET_REFS(gc, Py_REFCNT(FROM_GC(gc))); // gc_refs = 对象的真实 ob_refcnt +} +``` + +**第二步 `subtract_refs`:减掉所有「内部引用」。** 遍历每个候选对象,让它通过 `tp_traverse`「招出」自己引用了谁;每招出一个(也在候选集里的)对象,就把那个对象的 `gc_refs` 减一: + +`源文件:`[Modules/gcmodule.c](https://github.com/python/cpython/blob/v3.7.0/Modules/gcmodule.c#L290) + +```c +// Modules/gcmodule.c —— subtract_refs + visit_decref(精简) +static int visit_decref(PyObject *op, void *data) { + if (PyObject_IS_GC(op)) { + PyGC_Head *gc = AS_GC(op); + if (_PyGCHead_REFS(gc) > 0) + _PyGCHead_DECREF(gc); // 把「被集合内部引用」的那一次减掉 + } + return 0; +} +static void subtract_refs(PyGC_Head *containers) { + for (gc = containers->gc.gc_next; gc != containers; gc = gc->gc.gc_next) + Py_TYPE(FROM_GC(gc))->tp_traverse(FROM_GC(gc), visit_decref, NULL); // 招出我引用了谁 +} +``` + +减完之后,每个候选对象的 `gc_refs` 就等于**来自集合外部的引用数**: + +- **`gc_refs > 0`**:还有外部引用够得着它——它是「根」或被根牵连,**可达,不能回收**; +- **`gc_refs == 0`**:指向它的引用**全部来自候选集合内部**——很可能是垃圾。 + +## 找出真正的垃圾:传播可达性 + +`gc_refs == 0` 只是「疑似垃圾」,还不能直接回收。因为一个本身 `gc_refs == 0` 的对象,可能被另一个**外部可达**的对象引用着——那它其实也是可达的。所以还要做一轮**可达性传播**:从所有 `gc_refs > 0` 的「确定可达」对象出发,把它们能碰到的对象也标记为可达,一层层扩散。 + +`源文件:`[Modules/gcmodule.c](https://github.com/python/cpython/blob/v3.7.0/Modules/gcmodule.c#L354) + +```c +// Modules/gcmodule.c —— move_unreachable(精简思路) +while (gc != young) { + if (_PyGCHead_REFS(gc)) { // gc_refs > 0:确定可达 + _PyGCHead_SET_REFS(gc, GC_REACHABLE); + traverse(op, visit_reachable, young); // 把它引用到的对象也「救」回可达 + } else { // 暂定不可达,先挪到 unreachable + gc_list_move(gc, unreachable); + _PyGCHead_SET_REFS(gc, GC_TENTATIVELY_UNREACHABLE); + } + gc = next; +} +``` + +传播结束,还留在 `unreachable` 链表里的,就是**确确实实从外部无法到达的对象**——成环垃圾。用我们开头 `a ↔ b` 的例子走一遍:两者的真实计数都是 1,且这唯一的引用都来自对方(集合内部),减完后 `gc_refs` 双双归 0;又没有任何外部可达对象引用它们,可达性传播也救不回它们——于是被准确判定为垃圾: + +![用引用计数差值找出不可达的环](find-unreachable.svg) + +## 清除:tp_clear 打破环,引用计数收尾 + +找到了垃圾,怎么回收?直接 `free` 会有麻烦——环里对象互相引用,贸然释放一个,另一个手里的指针就悬空了。GC 的办法很巧:调用垃圾对象的 **`tp_clear`**,把它持有的引用**逐个置空**。这一置空,环就被**打破**了——内部互引一旦解除,对象们的真实 `ob_refcnt` 纷纷归零,于是**又回到引用计数的地盘,被正常 `Py_DECREF` 逐个销毁**: + +![tp_clear 打破环,引用计数收尾](tp-clear.svg) + +所以循环 GC 并不亲自 `free` 内存,它只负责**打破环**;真正的回收,仍交还给那个干净利落的引用计数。两者的互补在这里收束得很优雅。 + +## 分代:大多数对象朝生暮死 + +还有最后一个性能问题:每次都扫描**所有**被追踪的对象,太贵了。GC 的优化基于一个经验观察——**分代假说**:绝大多数对象都「朝生暮死」,刚创建不久就被丢弃;而活得够久的对象,往往会继续活很久。 + +于是 CPython 把被追踪的对象分成**三代**,新对象在第 0 代,每熬过一次回收就「升一代」。各代有各自的阈值: + +`源文件:`[Modules/gcmodule.c](https://github.com/python/cpython/blob/v3.7.0/Modules/gcmodule.c#L65) + +```c +// Modules/gcmodule.c —— 三代与阈值(精简) +struct gc_generation generations[NUM_GENERATIONS] = { + /* 链表头, 阈值, 计数 */ + { ..., 700, 0 }, // 第 0 代:新对象,最常回收 + { ..., 10, 0 }, // 第 1 代 + { ..., 10, 0 }, // 第 2 代:老对象,最少回收 +}; +``` + +![三代与对象的升代](generations.svg) + +- **第 0 代**最年轻、回收最频繁:每当「分配数 − 释放数」累积超过 **700**,就触发一次第 0 代回收; +- 熬过第 0 代回收的对象**升入第 1 代**;第 0 代回收满 **10** 次,顺带回收一次第 1 代; +- 同理,第 1 代回收满 **10** 次,触发一次涵盖全部三代的「完整回收」。 + +这样一来,海量「朝生暮死」的新对象在廉价的第 0 代回收里就被清掉,而少数长寿对象被「请」到高代,越老越少被打扰——把 GC 的开销集中花在最可能产出垃圾的年轻对象上。这套阈值都能查看和调整: + +```python +>>> import gc +>>> gc.get_threshold() # (第0代阈值, 第1代, 第2代) +(700, 10, 10) +>>> gc.get_count() # 当前各代的计数 +(123, 4, 1) +>>> gc.collect() # 也可手动触发完整回收 +0 +``` + +需要时还能 `gc.disable()` 关掉自动回收(比如某些短生命周期、确定无环的批处理场景,关掉可省去扫描开销),或用 `gc.freeze()` 把当前存活对象移入「永久代」不再扫描。但对绝大多数程序,让它默默工作就好。 + +--- + +小结一下循环垃圾回收: + +- 引用计数**回收不了循环引用**——成环对象计数永不归零,成为访问不到又清不掉的孤岛;循环 GC 专门补这个洞,与引用计数互补; +- 只有可能成环的**容器对象**(实现了 `tp_traverse` 的)才被 GC 追踪,每个挂一个 `PyGC_Head`;`int`/`str`/`float` 等原子对象永不被追踪; +- 判断「外部不可达」用**引用计数差值**的巧思:`update_refs` 把 `ob_refcnt` 抄进 `gc_refs`,`subtract_refs` 减掉所有内部引用,剩下的 `gc_refs` 即外部引用数——为 0 者疑似垃圾; +- 再从外部可达对象**传播可达性**(`move_unreachable`),救回被牵连者,剩下的才是真垃圾; +- 清除时调 **`tp_clear` 打破环**,对象真实计数随即归零,**交还引用计数完成回收**; +- **分代**(三代,阈值 700/10/10)基于「对象朝生暮死」的假说:年轻代回收勤、老年代回收疏,把开销花在刀刃上。 + +至此,最后一部分「内存管理」也讲完了——从引用计数的即时回收、pymalloc 的分层分配,到这一章的分代循环 GC,CPython 管理对象生死的全套机制就此拼齐。 + +回望整本书:我们从**一个对象的内存布局**起步,看它如何被**编译**成字节码,被**虚拟机**逐条执行,在**运行时**被初始化、被 import、被多线程争用,最终又如何在**内存管理**中诞生与消亡。`type`、`int`、帧、求值循环、GIL、引用计数……这些曾经神秘的名字,如今都还原成了 C 源码里一段段具体的逻辑。Python 那些「魔法」背后,从来没有魔法——只有一层层清晰、精巧、彼此咬合的工程设计。愿这趟源码之旅,让你此后写下的每一行 Python,都多一分「我知道它在底下做什么」的笃定。 diff --git a/docs/memory/garbage-collection/refcount-diff.svg b/docs/memory/garbage-collection/refcount-diff.svg new file mode 100644 index 0000000..e28f6ec --- /dev/null +++ b/docs/memory/garbage-collection/refcount-diff.svg @@ -0,0 +1,46 @@ + +引用计数差值:减掉内部引用,剩下外部引用 +判断对象是否从外部不可达,用引用计数做减法。一个对象的真实计数等于指向它的所有引用数,一部分来自垃圾候选集合内部,一部分来自集合外部即真正的根。第一步 update_refs 把 ob_refcnt 抄一份到 GC 头的 gc_refs,作为可随意涂改的工作副本,不破坏真实计数。第二步 subtract_refs 遍历每个候选对象,让它通过 tp_traverse 招出引用了谁,每招出一个集合内对象就把那对象的 gc_refs 减一。减完后 gc_refs 等于来自集合外部的引用数,大于 0 说明外部够得着不是垃圾,等于 0 说明引用全来自内部疑似垃圾。 + + + + + + + + + + +gc_refs = 真实计数 − 内部引用 = 外部引用 + + +① update_refs +gc_refs = ob_refcnt +抄一份工作副本 +不动真实计数 +(在 GC 头里) + + +② subtract_refs +遍历每个对象的引用 +指向的集合内对象 +gc_refs -- +减掉所有内部引用 + + +③ 判定 +gc_refs > 0 +外部可达,留 +gc_refs == 0 +全是内部引用,疑似垃圾 + + + + +不需要枚举根 —— 用引用计数做减法,就能算出「外部引用数」 + diff --git a/docs/memory/garbage-collection/tp-clear.svg b/docs/memory/garbage-collection/tp-clear.svg new file mode 100644 index 0000000..8d7eeba --- /dev/null +++ b/docs/memory/garbage-collection/tp-clear.svg @@ -0,0 +1,46 @@ + +tp_clear 打破环,引用计数收尾 +找到垃圾后,直接 free 会让环里互引的指针悬空。GC 的办法是调用垃圾对象的 tp_clear,把它持有的引用逐个置空。这一置空环就被打破,内部互引一旦解除,对象们的真实 ob_refcnt 纷纷归零,于是又回到引用计数的地盘,被正常 Py_DECREF 逐个销毁。所以循环 GC 并不亲自 free 内存,它只负责打破环,真正的回收仍交还给引用计数。 + + + + + + + + + + +tp_clear 打破环,引用计数完成回收 + + +环还在 + +a=1 + +b=1 + + + + + +tp_clear +把引用逐个置空 + + + +环被打破 → 计数归零 + +a=0 + +b=0 + +Py_DECREF 逐个销毁 + +循环 GC 不亲自 free,只负责打破环;真正的回收交还引用计数 + diff --git a/docs/memory/garbage-collection/tracked-untracked.svg b/docs/memory/garbage-collection/tracked-untracked.svg new file mode 100644 index 0000000..53c07c8 --- /dev/null +++ b/docs/memory/garbage-collection/tracked-untracked.svg @@ -0,0 +1,43 @@ + +被追踪的容器 vs 不被追踪的原子对象 +循环 GC 只盯着能成环的容器。list、dict、set、实例、tuple 这类能引用别的对象的容器,实现了 tp_traverse,会被 GC 追踪,内存里会在对象结构前面多挂一个 GC 头 PyGC_Head,含 gc_next、gc_prev、gc_refs。int、float、str 这类原子对象只装数据、不引用别的对象,不可能成环,不被追踪,一生只靠引用计数。gc_next 和 gc_prev 把被追踪对象串成双向链表,gc_refs 是 GC 工作时的私有计数。 + + + + + + + + + +只有能成环的容器才被 GC 追踪 + + + +被追踪:容器(有 tp_traverse) +list · dict · set · 实例 · tuple + + +PyGC_Head +挂在前面 + +对象本体 + + gc_next / gc_prev串成双向链表 + gc_refsGC 私有计数 + + + + +不被追踪:原子对象 +int · float · str · bytes + +对象本体(无 GC 头) +只装数据,不引用别人 +不可能成环 → 一生只靠引用计数 + diff --git a/docs/objects/bool-none-object/bool-int.svg b/docs/objects/bool-none-object/bool-int.svg new file mode 100644 index 0000000..f1c5043 --- /dev/null +++ b/docs/objects/bool-none-object/bool-int.svg @@ -0,0 +1,41 @@ + +bool 是 int 的子类,True/False 是整数 1/0 +PyBool_Type 的 tp_base 是 PyLong_Type,所以 bool 是 int 的子类(继承自 object → int → bool)。True 和 False 本身就是值为 1 和 0 的整数对象,只是类型为 bool。因此 True == 1、True + True == 2、isinstance(True, int) 为真。 + + + + + + + + + + +bool 是 int 的子类;True / False 是值为 1 / 0 的整数 + + +object +int PyLong_Type +bool PyBool_Type + + + +tp_base +tp_base + + +True整数 1 · type=bool +False整数 0 · type=bool + + + + +ob_type + +True == 1,True + True == 2,sum([True,True,True]) == 3,isinstance(True, int) 为真 + diff --git a/docs/objects/bool-none-object/caching-patterns.svg b/docs/objects/bool-none-object/caching-patterns.svg new file mode 100644 index 0000000..fba2811 --- /dev/null +++ b/docs/objects/bool-none-object/caching-patterns.svg @@ -0,0 +1,41 @@ + +贯穿全书的「一个对象代替无数等价对象」模式 +CPython 反复使用同一种省内存手法:用一个对象代替无数等价对象。包括小整数池 [-5,257)、字符串驻留(标识符)、空元组单例、浮点数 free list(最多 100)、以及 None/True/False 等内建单例。好处是省内存,并让 is 比较更快。 + + + + + + + + + + + + + + + + + + + + + + +一个对象 +代替无数等价对象 + + +小整数池 [-5, 257) +字符串驻留(标识符) +空元组 () 单例 +浮点 free list(≤100) +None / True / False 单例 + +共同的好处:省内存,并让 is 比较更快 + diff --git a/docs/objects/bool-none-object/index.md b/docs/objects/bool-none-object/index.md new file mode 100644 index 0000000..e06e02f --- /dev/null +++ b/docs/objects/bool-none-object/index.md @@ -0,0 +1,169 @@ +# Python 布尔与 None 对象 + +`True`、`False`、`None` 是我们每天都在用、却很少深究的几个值。它们有个共同点:都是**单例**——全局只有一个对象。这一章我们就来看它们的实现,并顺势把贯穿全书的「用一个对象代替无数等价对象」这个省内存手法串起来。 + +```python +>>> True is True, None is None +(True, True) +>>> True == 1, isinstance(True, int) +(True, True) +``` + +第二行也许让你意外:`True` 居然等于 `1`、还是 `int` 的实例?这正是布尔对象最有意思的地方。 + +## 布尔对象:是 int 的子类 + +在 CPython 里,`bool` 不是独立的类型,而是 `int` 的**子类**——它的类型对象 `PyBool_Type` 把 `tp_base` 指向了 `PyLong_Type`: + +`源文件:`[Objects/boolobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/boolobject.c#L134) + +```c +// Objects/boolobject.c +PyTypeObject PyBool_Type = { + ...... + "bool", /* tp_name */ + ...... + &PyLong_Type, /* tp_base */ // 父类是 int + ...... + bool_new, /* tp_new */ +}; +``` + +而 `True` 和 `False` 本身,就是两个值为 1 和 0 的**整数对象**(`struct _longobject`),只不过类型被设成了 `bool`: + +`源文件:`[Objects/boolobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/boolobject.c#L177) + +```c +// Objects/boolobject.c +struct _longobject _Py_FalseStruct = { + PyVarObject_HEAD_INIT(&PyBool_Type, 0) // 类型 bool,值 0 + { 0 } +}; +struct _longobject _Py_TrueStruct = { + PyVarObject_HEAD_INIT(&PyBool_Type, 1) // 类型 bool,值 1 + { 1 } +}; +``` + +![bool 是 int 的子类](bool-int.svg) + +所以布尔值在数值上下文里就是 1 和 0,能直接参与运算: + +```python +>>> type(True), bool.__bases__ +(, (,)) +>>> True + True, True * 3 +(2, 3) +>>> sum([True, True, False, True]) # 统计 True 的个数,常用技巧 +3 +``` + +`sum([...])` 这个「数 `True` 的个数」是很常见的写法,背后正是因为 `True` 就是整数 1。 + +## True 与 False 是单例 + +布尔类型永远只有两个对象。看 `bool(x)` 的实现——它不新建对象,只返回 `Py_True` 或 `Py_False` 这两个现成的单例: + +`源文件:`[Objects/boolobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/boolobject.c#L40) + +```c +// Objects/boolobject.c +/* We define bool_new to always return either Py_True or Py_False */ +static PyObject * +bool_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + ...... + return PyBool_FromLong(ok); // 只会返回 Py_True / Py_False +} +``` + +```python +>>> bool(5) is True, bool(0) is False +(True, True) +>>> bool(5) is bool(42) # 任何真值 bool() 出来都是同一个 True +True +``` + +正因为只有两个单例,判断真假时直接写 `if x:` 即可,不必(也不该)写 `if x == True:`。 + +## None 对象 + +`None` 是 `NoneType` 类型唯一的实例,同样是一个全局单例: + +`源文件:`[Objects/object.c](https://github.com/python/cpython/blob/v3.7.0/Objects/object.c#L1607) + +```c +// Objects/object.c +PyObject _Py_NoneStruct = { + _PyObject_EXTRA_INIT + 1, &_PyNone_Type // 引用计数 1,类型 NoneType +}; +``` + +`Py_None` 这个宏就指向它,全局只此一个。它甚至**永远不会被销毁**——它的析构函数被设计成直接报致命错误: + +`源文件:`[Objects/object.c](https://github.com/python/cpython/blob/v3.7.0/Objects/object.c#L1505) + +```c +// Objects/object.c +static void +none_dealloc(PyObject* ignore) +{ + /* This should never get called ... */ + Py_FatalError("deallocating None"); // 万一 None 被销毁,直接致命错误 +} +``` + +```python +>>> None is None +True +>>> type(None) + +>>> type(None)() is None # 连 NoneType() 也只返回同一个 None +True +``` + +正因为 `None` 是单例,判断一个值是不是 `None`,应该用 **`is None`** 而不是 `== None`——`is` 直接比指针,又快又不会被自定义的 `__eq__` 干扰。 + +## 其它单例:Ellipsis 与 NotImplemented + +除了 `None`,CPython 还有两个内建单例: + +- **`Ellipsis`**(写作 `...`):占位用的单例,常见于 numpy 的多维切片、类型注解等。 +- **`NotImplemented`**:运算符方法(如 `__eq__`、`__add__`)在「我处理不了这种类型」时返回它,提示解释器去尝试对方的反向方法。 + +![内建单例对象](singletons.svg) + +```python +>>> ... is Ellipsis, type(...) +(True, ) +>>> NotImplemented +NotImplemented +``` + +> 注意 `NotImplemented`(运算符回退用的单例)和 `NotImplementedError`(一个异常类)是两回事,别混用。 + +## 单例与缓存:一个贯穿全书的模式 + +把视野拉开,你会发现 `None`、`True`、`False` 只是一个更大模式的特例——CPython 反复在用同一招:**用一个对象代替无数个等价的对象**。前面各章其实都见过它: + +| 机制 | 谁来代替谁 | 出处 | +|---|---|---| +| 小整数对象池 | `[-5, 257)` 内的整数共享同一对象 | 整数对象 | +| 字符串驻留 | 等值的标识符字符串共享同一对象 | 字符串对象 | +| 空元组单例 | 所有 `()` 是同一个对象 | 元组对象 | +| 浮点 free list | 复用最多 100 个已释放的浮点对象 | 浮点数对象 | +| `None` / `True` / `False` | 各自全局唯一的单例 | 本章 | + +![一个对象代替无数等价对象](caching-patterns.svg) + +这套手法的共同好处有两个:**省内存**(不为等价的值重复分配),以及**比较更快**(同一对象可以直接用 `is` 比指针,而不必逐字段比较)。理解了这一点,再看 `a is b` 在不同对象上时而成立、时而不成立,就不会困惑了——成立与否,取决于该值有没有被「单例化/缓存」。 + +--- + +小结一下: + +- `bool` 是 `int` 的**子类**,`True`/`False` 就是值为 1/0 的整数对象(类型为 `bool`),所以能参与数值运算(`sum(bools)` 数个数); +- `True`/`False`/`None` 都是**单例**,`bool(x)`、`NoneType()` 都只返回现成的单例对象,判断时用 `if x:` 和 `is None`; +- `None` 永不销毁(`none_dealloc` 会触发致命错误);另有 `Ellipsis`、`NotImplemented` 两个内建单例; +- 这些单例是 CPython「**用一个对象代替无数等价对象**」这一省内存模式的体现,和小整数池、字符串驻留、空元组、浮点 free list 一脉相承——既省内存,又让 `is` 比较更快。 diff --git a/docs/objects/bool-none-object/singletons.svg b/docs/objects/bool-none-object/singletons.svg new file mode 100644 index 0000000..495dcca --- /dev/null +++ b/docs/objects/bool-none-object/singletons.svg @@ -0,0 +1,48 @@ + +内建单例对象 +None、True、False、Ellipsis、NotImplemented 都是全局唯一、永久存在的单例。无论多少处使用 None,拿到的都是同一个对象,所以用 is 判断(x is None);None 永不被销毁,若引用计数归零会触发 Py_FatalError。 + + + + + + + + + +内建单例:全局各一个、永久存在 + + + + a = None + b = None + c = None + + + + + + + + + +None +唯一对象 + + +其它内建单例 + + True + False + Ellipsis (...) + NotImplemented + + +无论多少处使用,都是同一对象 → 用 is 判断(x is None);永不被销毁 + diff --git a/docs/objects/bytes-object/bytearray-mutate.svg b/docs/objects/bytes-object/bytearray-mutate.svg new file mode 100644 index 0000000..19fc343 --- /dev/null +++ b/docs/objects/bytes-object/bytearray-mutate.svg @@ -0,0 +1,43 @@ + +bytearray 的原地修改与追加 +bytearray 可变:ba[0]=65 直接在 ob_bytes 缓冲里把第 0 个字节改成 A;ba.append(33) 把字节追加到预留的空位(!)。ob_alloc 预留的空位让 append 摊还 O(1),空间不足时按和列表相同的公式过分配。 + + + + + + +bytearray 可变:在 ob_bytes 缓冲里原地改 / 追加 + + +bytearray(b'abc') + + +abc + + + +ba[0] = 65 + + + +A +bc + +原地改第 0 个字节 + + +ba.append(33) + + + +Abc +! + +追加到预留空位 + diff --git a/docs/objects/bytes-object/bytearray-pointers.svg b/docs/objects/bytes-object/bytearray-pointers.svg new file mode 100644 index 0000000..40c1318 --- /dev/null +++ b/docs/objects/bytes-object/bytearray-pointers.svg @@ -0,0 +1,49 @@ + +bytearray 的两个指针:从头部删除时只移动 ob_start +bytearray 同时记录 ob_bytes(malloc 出的缓冲块起点,用于最终释放)和 ob_start(逻辑上的第一个字节)。执行 del ba[0] 时,只需把 ob_start 向右移一格,缓冲里的字节完全不用搬动,因此从头部删除是 O(1)。被跳过的字节成为头部空槽。 + + + + + + + +del ba[0]:只把 ob_start 右移一格,缓冲不搬动 + + +删除前 +ob_start == ob_bytes + + + + abcd + + + + +ob_bytes += ob_start + + +删除后 +ob_start 前移 1 + + + + + a + bcd + + + + +ob_bytes + +ob_start(右移) +头部空槽 + diff --git a/docs/objects/bytes-object/bytes-bytearray-struct.svg b/docs/objects/bytes-object/bytes-bytearray-struct.svg new file mode 100644 index 0000000..02b33c3 --- /dev/null +++ b/docs/objects/bytes-object/bytes-bytearray-struct.svg @@ -0,0 +1,38 @@ + +bytes 与 bytearray 的内存布局对比 +bytes 把字节数据内联在对象自身里(ob_sval),是一块连续内存、不可变,并把哈希缓存在 ob_shash,类似不可变的字符串。bytearray 则用 ob_bytes 指向另一块可增长的缓冲,配合 ob_alloc 过分配,所以可变、可 append,类似字节版的列表。 + + + + + + + + + +bytes b'abc' + + + +对象头ob_shashabc\0 + +一块连续内存:字节内联(ob_sval),不可变,哈希缓存在 ob_shash + + +bytearray + + +对象头ob_alloc=8*ob_bytes ● + + + +abc + +ob_bytes 缓冲(可增长) + +bytes 像不可变的字符串(一块·内联·哈希缓存);bytearray 像可变的列表(独立缓冲·过分配扩容) + diff --git a/docs/objects/bytes-object/bytes-cache.svg b/docs/objects/bytes-object/bytes-cache.svg new file mode 100644 index 0000000..069317c --- /dev/null +++ b/docs/objects/bytes-object/bytes-cache.svg @@ -0,0 +1,31 @@ + +bytes 的缓存:空串单例与单字节缓存 +和小整数池同理,CPython 缓存了等价的小 bytes 对象:空 bytes b'' 是单例(nullstring),长度为 1 的 bytes(共 256 种)各缓存一个(characters[256])。所以 bytes() is bytes()、bytes([97]) is bytes([97]) 都为 True。 + + + + + + + + +bytes 的缓存:空串单例 + 单字节缓存 + + +空 bytes 单例(nullstring) + +b'' (唯一) + + +单字节缓存 characters[256](长度 1 的 bytes 各一个) + + +b'\x00'b'\x01'b'\x02'b'a'=97b'\xff' + +bytes() is bytes() → True | bytes([97]) is bytes([97]) → True (与小整数池同理) + diff --git a/docs/objects/bytes-object/bytes-index.svg b/docs/objects/bytes-object/bytes-index.svg new file mode 100644 index 0000000..101aff6 --- /dev/null +++ b/docs/objects/bytes-object/bytes-index.svg @@ -0,0 +1,39 @@ + +bytes 索引返回整数,切片返回 bytes +对 b'abc':b[0] 返回的是该字节的整数值 97(0–255 的 int),而不是长度 1 的 bytes;b[1:3] 返回的才是 bytes,即 b'bc'。这与字符串不同——'abc'[0] 返回长度 1 的字符串 'a'。 + + + + + + + + + +bytes 索引出整数,切片出 bytes + + +b'abc' + + +abc +979899 + + + +b[0] +97int + + + +b[1:3] +b'bc'bytes + +对比字符串:'abc'[0] → 'a'(长度 1 的 str),而 bytes 的索引给的是字节的数值 + diff --git a/docs/objects/bytes-object/index.md b/docs/objects/bytes-object/index.md new file mode 100644 index 0000000..4d899e8 --- /dev/null +++ b/docs/objects/bytes-object/index.md @@ -0,0 +1,342 @@ +# Python bytes 与 bytearray 对象 + +字符串处理的是**文本**(Unicode 码点),而 `bytes` 和 `bytearray` 处理的是**原始字节**(0–255 的字节序列)——读写文件、网络收发、图片音视频、二进制协议,到了最底层都是一串字节。所以只要写真实程序,迟早会和这两个对象打交道。 + +它们俩的关系,正好和「字符串 / 列表」遥相呼应:一个**不可变**、一个**可变**。 + +```python +>>> b = b"abc" +>>> b[0] # 注意:索引返回的是整数,不是 b'a' +97 +>>> b[1:3] # 切片才返回 bytes +b'bc' +>>> "café".encode() # str 编码成 bytes +b'caf\xc3\xa9' +``` + +第二行的 `97` 是很多人第一次用 `bytes` 都会愣一下的地方——明明 `"abc"[0]` 是 `'a'`,怎么 `b"abc"[0]` 就成了数字?这一章我们就从 `PyBytesObject` 和 `PyByteArrayObject` 的结构出发,把这些行为一个个讲清楚。 + +## 数据结构:不可变的 bytes 与可变的 bytearray + +先看不可变的 `bytes`: + +`源文件:`[Include/bytesobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/bytesobject.h#L31) + +```c +// Include/bytesobject.h +typedef struct { + PyObject_VAR_HEAD + Py_hash_t ob_shash; // 缓存的哈希值,-1 表示尚未计算 + char ob_sval[1]; // 内联的字节数组(末尾留一个 \0) +} PyBytesObject; +``` + +逐个字段看: + +- `PyObject_VAR_HEAD` 是**变长对象**的头部,里面的 `ob_size` 记录这个 `bytes` 有多少个字节。变长,是因为不同 `bytes` 长度不同,但每个 `bytes` 一旦创建,长度就固定了。 +- `ob_shash` 缓存哈希值。`bytes` 不可变,哈希算一次就不会变,于是存下来下次直接用,初始值 `-1` 表示「还没算过」。 +- `ob_sval[1]` 才是真正的字节数据。声明成长度 1,是 C 里经典的「结构体尾部变长数组」技巧:实际分配内存时按真实长度多要一截,字节数据就**紧接在对象后面、和对象同属一块内存**。末尾还多留一个 `\0`,这样把它当 C 字符串用时也安全。 + +所以 `bytes` 的样子是:**对象头 + 哈希 + 一段内联的、连续的、不可变的字节**——和字符串 `str` 的设计如出一辙。 + +再看可变的 `bytearray`: + +`源文件:`[Include/bytearrayobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/bytearrayobject.h#L23) + +```c +// Include/bytearrayobject.h +typedef struct { + PyObject_VAR_HEAD + Py_ssize_t ob_alloc; // 缓冲区已分配的字节数 + char *ob_bytes; // 指向实际字节缓冲(另一块内存)的起点 + char *ob_start; // 逻辑上的第一个字节 + int ob_exports; // 当前被导出为 buffer 的次数 +} PyByteArrayObject; +``` + +和 `bytes` 最大的不同:字节数据不再内联,而是放在 `ob_bytes` 指向的**另一块**内存里。这样这块缓冲就能独立地重新分配、增大缩小,`bytearray` 因此可变、可 `append`——像极了列表。其余字段先记个印象,后面各有专门一节展开: + +- `ob_alloc`:缓冲实际分配了多少字节(通常 ≥ 当前长度,多出来的是为后续追加预留的空位)。 +- `ob_bytes` 和 `ob_start`:**两个**指针。`ob_bytes` 指向 malloc 出来的缓冲块起点(释放时要用它),`ob_start` 指向逻辑上的第 0 个字节。多数时候两者相等,但从头部删除字节时它们会分开——这正是高效头部删除的关键。 +- `ob_exports`:这块缓冲被 `memoryview` 等「借走」了几次,借出期间不许改大小。 + +![bytes 与 bytearray 的内存布局](bytes-bytearray-struct.svg) + +一句话对照:**`bytes` 像不可变的字符串(一块、内联、哈希缓存),`bytearray` 像可变的列表(独立缓冲、过分配扩容),只不过它们的元素都是字节。** + +## 索引出整数,切片出 bytes + +这是 `bytes`/`bytearray` 最容易让人意外的地方:**按下标取出的是整数,不是单字节的 bytes**。 + +```python +>>> b = b"abc" +>>> b[0] # 整数(该字节的值,0–255) +97 +>>> b[-1] # 同样是整数 +99 +>>> b[1:3] # 切片才返回 bytes +b'bc' +``` + +这和字符串截然不同——`"abc"[0]` 返回的是长度 1 的字符串 `'a'`。原因其实很自然:一个字节本质上就是 0–255 之间的一个数,用整数来表示最直接;而字符串的「一个字符」背后是 Unicode 码点,仍是文本,所以取出来还是 `str`。 + +连**遍历**也是一样,迭代 `bytes` 得到的是一串整数: + +```python +>>> for x in b"AB": +... print(x) +... +65 +66 +>>> list(b"AB") +[65, 66] +``` + +那想从一个字节拿回「单字节的 bytes」怎么办?用切片,或者用整数列表反向构造: + +```python +>>> b = b"abc" +>>> b[0:1] # 切片:长度 1 的 bytes +b'a' +>>> bytes([b[0]]) # 用整数列表构造 +b'a' +``` + +![bytes 索引出整数](bytes-index.svg) + +记住这条「**索引出整数、切片出 bytes**」,解析二进制协议时就不会被类型搞晕。 + +## 创建 bytes 的多种方式 + +`bytes` 的构造函数很「多面」,传不同类型的参数含义完全不同,值得一次理清: + +```python +>>> bytes(3) # 传整数 n:得到 n 个零字节 +b'\x00\x00\x00' +>>> bytes([97, 98, 99]) # 传可迭代的整数(每个须在 0–255):逐字节填入 +b'abc' +>>> bytes("café", "utf-8") # 传字符串 + 编码:等价于 "café".encode("utf-8") +b'caf\xc3\xa9' +>>> bytes(b"abc") # 传另一个 bytes:拷贝一份 +b'abc' +``` + +特别留意第一行:`bytes(3)` **不是** `b'3'`,而是三个 `\x00`。把整数当「长度」来理解——它常用来预先开一段全零的缓冲。 + +和十六进制的互转也很常用,二进制调试时几乎离不开: + +```python +>>> bytes.fromhex("48 65 6c") # 十六进制字符串 → bytes(空格会被忽略) +b'Hel' +>>> b"Hel".hex() # bytes → 十六进制字符串 +'48656c' +``` + +## 单字节与空 bytes 的缓存 + +和小整数池、单字符字符串一样,`bytes` 也用了「缓存小对象」的手法来省内存、省分配。看创建函数 `PyBytes_FromStringAndSize`:**长度 1 的 bytes 会被缓存复用**(256 种字节各留一个),**空 bytes 则是全局单例**: + +`源文件:`[Objects/bytesobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/bytesobject.c#L101) + +```c +// Objects/bytesobject.c +static PyBytesObject *characters[UCHAR_MAX + 1]; // 256 个单字节 bytes 缓存 +static PyBytesObject *nullstring; // 空 bytes 单例 + +PyObject * +PyBytes_FromStringAndSize(const char *str, Py_ssize_t size) +{ + ...... + if (size == 1 && str != NULL && + (op = characters[*str & UCHAR_MAX]) != NULL) // 命中单字节缓存 + { + Py_INCREF(op); + return (PyObject *)op; + } + op = (PyBytesObject *)_PyBytes_FromSize(size, 0); + ...... + if (size == 1) { + characters[*str & UCHAR_MAX] = op; // 长度 1 → 存入缓存 + Py_INCREF(op); + } + return (PyObject *) op; +} +``` + +这两个缓存都是**懒加载**:单字节缓存初始全为 `NULL`,某个字节第一次被创建时才填进 `characters` 数组,以后再要同样的单字节就直接复用同一个对象。 + +![bytes 的缓存](bytes-cache.svg) + +```python +>>> bytes() is bytes() # 空 bytes 是单例 +True +>>> bytes([97]) is bytes([97]) # 单字节 bytes 被缓存复用 +True +>>> bytes([97, 98]) is bytes([97, 98]) # 长度 ≥ 2 就不缓存了,是不同对象 +False +``` + +最后一行点出了缓存的边界:**只有长度 0 和 1 的 bytes 才共享**,更长的每次都是新对象。这和小整数池「只缓存 -5~256」是同一个权衡——小对象太常见,缓存收益大;大对象种类太多,缓存不划算。 + +## 不可变、可哈希 vs 可变、不可哈希 + +`bytes` 不可变,因此**可哈希**——哈希值算一次就缓存在 `ob_shash`(初始 `-1`),下次直接返回,和字符串完全一致: + +`源文件:`[Objects/bytesobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/bytesobject.c#L1644) + +```c +// Objects/bytesobject.c —— bytes_hash +if (a->ob_shash != -1) + return a->ob_shash; // 已算过,直接返回缓存 +...... +a->ob_shash = x; // 算一次,存进 ob_shash +``` + +所以 `bytes` 能当字典的键、集合的元素。而 `bytearray` 可变,一旦内容能变,哈希值就无从「钉死」,于是**不可哈希**: + +```python +>>> b"abc"[0] = 65 # bytes 不可变,不能改单个字节 +Traceback (most recent call last): + ... +TypeError: 'bytes' object does not support item assignment +>>> isinstance(hash(b"abc"), int) # bytes 可哈希,能算出哈希值 +True +>>> hash(bytearray(b"abc")) # bytearray 不可哈希 +Traceback (most recent call last): + ... +TypeError: unhashable type: 'bytearray' +``` + +这正是贯穿全书的那条老规矩——**可变 ⇒ 不可哈希**(和列表、集合一样)。可变对象如果能进字典/集合,改一下内容它的哈希就变了,之前存进去的位置就再也找不到,所以语言干脆禁止。 + +## bytearray 的原地修改与扩容 + +`bytearray` 支持原地改字节、追加、删除。先看最常见的改与加: + +```python +>>> ba = bytearray(b"abc") +>>> ba[0] = 65 # 原地把第 0 个字节改成 'A'(65) +>>> ba.append(33) # 追加一个字节 '!'(33) +>>> ba +bytearray(b'Abc!') +``` + +![bytearray 的原地修改与追加](bytearray-mutate.svg) + +追加为什么快?因为缓冲是**过分配**的——`ob_alloc` 通常比当前长度大,留了空位,多数 `append` 只是往空位里填一个字节、把长度加一,不必重新分配内存。只有空位用完了,才调用 `PyByteArray_Resize` 申请更大的缓冲。它的过分配公式和列表**完全一样**,源码注释甚至直接写明「overallocate similar to list_resize()」: + +`源文件:`[Objects/bytearrayobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/bytearrayobject.c#L228) + +```c +// Objects/bytearrayobject.c —— PyByteArray_Resize +if (size <= alloc * 1.125) { + /* Moderate upsize; overallocate similar to list_resize() */ + alloc = size + (size >> 3) + (size < 9 ? 3 : 6); +} +``` + +`size >> 3` 就是「多留约 1/8」。正因为每次扩容都多备一些,`append` 平均下来是**摊还 O(1)**:偶尔一次扩容较贵,但被后面许多次「填空位」的廉价追加均摊掉了。这套策略我们在[《Python 列表对象》](../list-object/)里已经见过,`bytearray` 原封不动地复用了它。 + +## 两个指针:bytearray 为何能 O(1) 从头部删除 + +回到结构里那个伏笔——`bytearray` 为什么要 `ob_bytes` 和 `ob_start` **两个**指针?答案藏在「从头部删除」这个操作里。 + +设想 `del ba[0]`:朴素做法是把后面所有字节整体往前挪一格(`memmove`),那是 O(n)。CPython 的巧法是:**字节一个都不动,只把 `ob_start` 这个「逻辑起点」向右挪一格**。于是被跳过的那个字节成了缓冲头部的空槽,长度减一,而 `ob_bytes`(真正的 malloc 块起点)保持不变,留着将来释放内存时用。 + +`源文件:`[Objects/bytearrayobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/bytearrayobject.c#L463) + +```c +// Objects/bytearrayobject.c —— bytearray_setslice_linear(精简) +if (lo == 0) { + /* Shrink the buffer by advancing its logical start */ + self->ob_start -= growth; // 从头部删除:只移动逻辑起点,数据不搬动 +} +else { + memmove(buf + lo + bytes_len, buf + hi, ...); // 从中间删:才需要搬 +} +``` + +(这里 `growth` 是负数,`ob_start -= growth` 即把 `ob_start` 往后移。) + +![bytearray 的两个指针与头部删除](bytearray-pointers.svg) + +```python +>>> ba = bytearray(b"abcdef") +>>> del ba[0] # 头部删除:内部只把 ob_start 右移一格 +>>> ba +bytearray(b'bcdef') +``` + +所以这两个指针是一种空间换时间:多记一个 `ob_start`,换来从头部反复删除(比如把 `bytearray` 当队列、不断从前面消费数据)也能高效。等到下次扩容时,`Resize` 会把残留的头部空槽一并整理掉(重新分配并从 `ob_start` 拷贝),缓冲又变回紧凑的样子。 + +## buffer 协议与 ob_exports:被引用时不能扩容 + +最后一个字段 `ob_exports` 服务于 **buffer 协议**——一种让 `memoryview`、`numpy` 等直接共享 `bytearray` 底层内存、零拷贝读写的机制。每被 `memoryview` 借走一次,`ob_exports` 加一;释放时减一。 + +问题来了:别人正拿着指向这块缓冲的指针,你这边却把缓冲 `realloc` 到别处去了,那别人手里就是一个悬空指针。为避免这种灾难,`bytearray` 在改大小前都会检查 `ob_exports`,一旦大于 0 就直接报错,不准 resize: + +`源文件:`[Objects/bytearrayobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/bytearrayobject.c#L86) + +```c +// Objects/bytearrayobject.c —— _canresize +if (self->ob_exports > 0) { + PyErr_SetString(PyExc_BufferError, + "Existing exports of data: object cannot be re-sized"); + return 0; +} +``` + +```python +>>> ba = bytearray(b"abc") +>>> mv = memoryview(ba) # 借出缓冲,ob_exports 变为 1 +>>> ba.append(100) # 此时扩容会改变缓冲地址 → 被拒绝 +Traceback (most recent call last): + ... +BufferError: Existing exports of data: object cannot be re-sized +>>> mv.release() # 归还,ob_exports 回到 0 +>>> ba.append(100) # 现在可以扩容了 +>>> ba +bytearray(b'abcd') +``` + +值得一提的是:**原地改字节(不改长度)始终允许**,因为缓冲地址不变;被禁止的只是「改大小」这类可能搬动缓冲的操作。 + +## bytes/bytearray 与 str:文本 vs 二进制 + +把三者放一起对照收尾。它们都是「序列」,但分处两个世界——文本与二进制: + +| 类型 | 内容 | 可变性 | `x[0]` 返回 | 可哈希 | +|---|---|---|---|---| +| `str` | 文本(Unicode 码点) | 不可变 | 长度 1 的 `str` | 是 | +| `bytes` | 二进制(字节 0–255) | 不可变 | `int` | 是 | +| `bytearray` | 二进制(字节 0–255) | 可变 | `int` | 否 | + +值得一提的是,`bytes`/`bytearray` 也有一整套和 `str` 同名的方法(`split`、`strip`、`startswith`、`replace`、`find` 等),用起来手感几乎一样,只是参数和返回值都换成了字节: + +```python +>>> b"a,b,c".split(b",") # 和 str.split 一个用法,只是分隔符是 bytes +[b'a', b'b', b'c'] +>>> b" hi ".strip() +b'hi' +``` + +`str` 与 `bytes` 之间靠 **`encode` / `decode`** 转换(详见[《Python 字符串对象》](../str-object/)的「编码与解码」): + +```python +>>> "café".encode("utf-8") # str → bytes +b'caf\xc3\xa9' +>>> b"caf\xc3\xa9".decode("utf-8") # bytes → str +'café' +``` + +一条实用经验:**程序内部一律用 `str` 处理文本,只在 I/O 边界(读写文件、收发网络)才转成 `bytes`**;如果需要可变的字节缓冲(比如逐步拼装一段二进制数据、或把它当字节队列从头部消费),就用 `bytearray`。 + +--- + +小结一下: + +- `bytes` 像**不可变的字符串**:字节数据**内联**在对象里(`ob_sval`),是一块连续内存、哈希缓存在 `ob_shash`、可作字典键;并缓存了空串单例与 256 个单字节对象; +- `bytearray` 像**可变的列表**:`ob_bytes` 指向独立的可增长缓冲,按和列表相同的公式过分配,`append` 摊还 O(1),但因可变而**不可哈希**; +- `bytearray` 用 `ob_bytes`/`ob_start` **两个指针**,让「从头部删除」只需移动逻辑起点、O(1) 完成;用 `ob_exports` 配合 buffer 协议,在缓冲被 `memoryview` 借出期间禁止扩容; +- 两者**索引都返回整数**(0–255),切片才返回 `bytes`/`bytearray`——这是相对字符串的关键差异;构造函数则按参数类型「多面」行事(整数当长度、可迭代逐字节填、字符串按编码转); +- `str` 是文本、`bytes`/`bytearray` 是二进制,靠 `encode`/`decode` 在边界处转换。 diff --git a/docs/objects/dict-object/dict-create.svg b/docs/objects/dict-object/dict-create.svg new file mode 100644 index 0000000..60b3745 --- /dev/null +++ b/docs/objects/dict-object/dict-create.svg @@ -0,0 +1,32 @@ + +新建空字典的初始状态 +执行 d = {} 后,dk_size = 8 的索引数组全部填成 -1(DKIX_EMPTY,表示空槽),dk_entries 暂无任何记录,最多可用条目数 dk_usable = USABLE_FRACTION(8) = 5。 + + + + + + +d = {} + + +dk_indices(dk_size = 8,全为 -1) +01234567 + + +-1-1-1-1-1-1-1-1 + + +dk_entries(dk_usable = 5,当前 0 条) + + + + +ma_used = 0 + diff --git a/docs/objects/dict-object/dict-insert.svg b/docs/objects/dict-object/dict-insert.svg new file mode 100644 index 0000000..84ef8b9 --- /dev/null +++ b/docs/objects/dict-object/dict-insert.svg @@ -0,0 +1,52 @@ + +字典插入 d["1"]="2" 示意 +插入一个新键值对:先算 key 的 hash 并 & mask 定位到一个索引槽,槽里记下新记录在 dk_entries 中的下标 0;记录本身(me_hash、me_key、me_value)追加到 dk_entries 的末尾,保持插入顺序。 + + + + + + + +d["1"] = "2" + + +① hash("1") & mask → 定位索引槽 + + +dk_indices + + + +-1-1-1-1-1-1-1 +0 +② 记下下标 0 + + + +③ 记录追加到 dk_entries 末尾 + + +dk_entries + + + + +me_hashme_keyme_value +h"1""2" +entry 0 + + + + + + +查找撞空槽 → +即新 key, +在此落库 + diff --git a/docs/objects/dict-object/dict-lookup.svg b/docs/objects/dict-object/dict-lookup.svg new file mode 100644 index 0000000..fa06618 --- /dev/null +++ b/docs/objects/dict-object/dict-lookup.svg @@ -0,0 +1,54 @@ + +字典查找与探测(开放寻址) +查找一个 key:从 hash & mask 定位的索引槽出发,若槽非空但 key 不同则发生冲突,按扰动公式 i = (i*5 + perturb + 1) & mask 跳到下一槽(perturb 每次右移 5 位);如此重复,直到 key 相同(命中,返回 entry 下标)或遇到空槽(key 不存在)。 + + + + + + + + + + + + + +起始槽 +i = hash & mask +槽非空且 key 不同 → 冲突 + + + +探测下一槽 +i = (i*5+perturb+1) & mask +perturb >>= 5 + + + +仍冲突则重复 + + + + + + +key 相同 → 命中 +返回 entry 下标 + + + +遇到空槽 +→ key 不存在 + + + + + +从 hash 定位的槽出发,冲突就按扰动公式跳到下一槽,直到命中或遇空槽(平均 O(1)) + diff --git a/docs/objects/dict-object/dict-mem.svg b/docs/objects/dict-object/dict-mem.svg new file mode 100644 index 0000000..31fb329 --- /dev/null +++ b/docs/objects/dict-object/dict-mem.svg @@ -0,0 +1,89 @@ + +Python 字典相关数据结构的内存布局 +PyDictObject 通过 *ma_keys 指向 PyDictKeysObject;PyDictKeysObject 含 dk_refcnt、dk_size、dk_lookup、dk_usable、dk_nentries 和 dk_indices[];dk_indices 是一组索引值(节省 PyDictKeyEntry 空间),这些索引指向真正存放数据的 PyDictKeyEntry 数组,每个 entry 含 me_hash、me_key、me_value。 + + + + + + + + + + + + + + + +PyDictObject + + + + Pyobject_HEADma_usedma_version_tag*ma_keys**ma_values + + + python 基本类型字典 items 数量版本标识 · 更改即变 + + + + + +PyDictKeysObject + + + dk_refcntdk_sizedk_lookupdk_usabledk_nentriesdk_indices[] + + + 大小为 2 的幂哈希查找函数已使用 entry 数 + + + + + + +dk_indices[]:存储索引值,减少 PyDictKeyEntry 的占用空间 + + + + 00000000 + + + + + +索引指向 entry + + + + + + + PyDictKeyEntry + + me_hashme_keyme_value + + + + PyDictKeyEntry + + me_hashme_keyme_value + + + + PyDictKeyEntry + + me_hashme_keyme_value + + + + + + 保存 hash 值保存 key保存 value + + diff --git a/docs/objects/dict-object/dict-tables.svg b/docs/objects/dict-object/dict-tables.svg new file mode 100644 index 0000000..77facb6 --- /dev/null +++ b/docs/objects/dict-object/dict-tables.svg @@ -0,0 +1,69 @@ + +字典的两种表:combined 与 split +combined 联合表(默认):key 与 value 一起存在 ma_keys 指向的 entries 里,ma_values 为 NULL。split 分离表(用于实例 __dict__):多个实例共享同一份只含 key 的 ma_keys,各自的 value 单独存放在自己的 ma_values 数组里,从而省内存。 + + + + + + + + + + + + +combined(联合表)· 默认 + + + +PyDictObject +ma_keys ●ma_values +NULL + + + +PyDictKeysObject +entries: +key + value +一起存放 + +key 与 value 同存于 entries + + +split(分离表)· 实例 __dict__ + + + +实例 a +ma_values ● + + + +实例 b +ma_values ● + + +values_a + +values_b + + + +共享 keys +只存 key +(不含 value) + + + + + + + +多实例共享 keys,value 各存一份 → 省内存 + diff --git a/docs/objects/dict-object/index.md b/docs/objects/dict-object/index.md new file mode 100644 index 0000000..74b0544 --- /dev/null +++ b/docs/objects/dict-object/index.md @@ -0,0 +1,322 @@ +# Python 字典对象 + +字典是 Python 里最重要的数据结构之一——变量名查找、对象属性、关键字参数、模块命名空间……背后几乎都是字典。我们天天用它,靠的是两个让人放心的特性: + +```python +>>> d = {"name": "py", "year": 1991} +>>> d["name"] # 按 key 取值,平均 O(1),再大也几乎不变慢 +'py' +>>> list(d) # 从 Python 3.7 起,遍历顺序 == 插入顺序 +['name', 'year'] +``` + +按 key 存取近乎常数时间,说明它底层是一张**哈希表**;而「遍历有序」这个 3.7 才正式写进语言规范的特性,则要归功于 3.6 引入的**紧凑字典(compact dict)**设计。这一章我们就来看 CPython 是怎么把这两件事一起做到的。 + +## 核心设计:稀疏索引 + 紧凑条目 + +传统哈希表会开一个很大的数组,直接把「键值对」按哈希散落在里面。空间利用率低(数组大半是空槽),而且遍历顺序由哈希决定、杂乱无章。 + +CPython 3.6 改用了一个巧妙的两段式结构,把「哈希定位」和「数据存储」拆开: + +- **`dk_indices`**:一个稀疏的索引数组,大小为 2 的幂。它**不直接存键值对**,只存一个个小整数——指向下面 entries 数组的下标(空槽用 -1 表示)。哈希就散布在这个数组里。 +- **`dk_entries`**:一个紧凑的条目数组,**按插入顺序**依次存放真正的记录(哈希、key、value)。 + +查找一个 key 时:先用哈希在稀疏的 `dk_indices` 里定位,拿到一个下标,再去紧凑的 `dk_entries` 里取出那条记录。 + +这样设计的两个好处立竿见影: + +1. **省内存**:稀疏的大数组只存小整数(按规模可能只占 1 字节),而又大又重的键值记录紧凑排列、不留空洞。 +2. **天然有序**:`dk_entries` 按插入顺序追加,遍历它自然就是插入顺序——这正是 dict 有序的由来。 + +每条记录的类型是 `PyDictKeyEntry`: + +`源文件:`[Objects/dict-common.h](https://github.com/python/cpython/blob/v3.7.0/Objects/dict-common.h#L4) + +```c +// Objects/dict-common.h +typedef struct { + Py_hash_t me_hash; // 缓存的哈希值(避免重复计算) + PyObject *me_key; // 键 + PyObject *me_value; // 值(仅 combined 表用到,见下) +} PyDictKeyEntry; +``` + +把这套结构画出来,就是下面这张图:稀疏的 `dk_indices` 存的是「位置」,真正的键值记录在右侧紧凑的 `PyDictKeyEntry` 数组里: + +![python_dict_mem](dict-mem.svg) + +## 数据结构 + +字典对象本体是 `PyDictObject`: + +`源文件:`[Include/dictobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/dictobject.h#L17) + +```c +// Include/dictobject.h +typedef struct { + PyObject_HEAD + Py_ssize_t ma_used; // 当前键值对个数,即 len(d) + uint64_t ma_version_tag; // 版本号:每次修改都变,用于优化与一致性检查 + PyDictKeysObject *ma_keys; // 指向 keys 对象(哈希表主体) + PyObject **ma_values; // combined 表为 NULL;split 表时单独存放 values +} PyDictObject; +``` + +而哈希表的主体在 `PyDictKeysObject`: + +`源文件:`[Objects/dict-common.h](https://github.com/python/cpython/blob/v3.7.0/Objects/dict-common.h#L22) + +```c +// Objects/dict-common.h +struct _dictkeysobject { + Py_ssize_t dk_refcnt; // 引用计数(keys 可被多个实例共享) + Py_ssize_t dk_size; // dk_indices 的大小,必须是 2 的幂 + dict_lookup_func dk_lookup; // 查找函数(按 key 类型特化,见下) + Py_ssize_t dk_usable; // 还能再放多少个 entry + Py_ssize_t dk_nentries; // dk_entries 中已用的条目数 + char dk_indices[]; // 稀疏索引数组;其后紧跟 dk_entries 数组 +}; +``` + +`dk_indices` 之后紧跟着 `dk_entries` 数组(用 `DK_ENTRIES()` 宏访问),两者连续分配在同一块内存里。索引槽里存的值有两个特殊含义:`DKIX_EMPTY`(-1,空槽)和 `DKIX_DUMMY`(-2,曾用过、现已删除)。 + +> **entry 的几种状态**:一个槽位会在这几种状态间流转——*Unused*(从未使用,索引为 EMPTY)、*Active*(存着有效键值对)、*Dummy*(键值对被删除后留下的「墓碑」,探测时不能当空槽跳过,否则会漏掉它后面发生过哈希冲突的键)、*Pending*(split 表特有,key 已就位但 value 尚未填入)。 + +## 两种表:combined 与 split + +字典分两种实现(细节见 [PEP 412](https://www.python.org/dev/peps/pep-0412/)): + +- **combined 表(联合表)**:默认形态。`{}`、`dict()`、模块命名空间等绝大多数字典都是它。key 和 value 都存在 `ma_keys` 指向的 entries 里,`ma_values` 为 `NULL`。 +- **split 表(分离表)**:专门用于实例的 `__dict__`。同一个类的所有实例属性名(keys)是一样的,于是大家**共享同一份 keys**,各自的 value 单独存到 `ma_values` 数组里,从而省下大量内存。一旦实例属性的结构发生变化(比如删 key、重新调整大小),它会退化成 combined 表以保证正确性。 + +![两种表对比](dict-tables.svg) + +下面以最常见的 combined 表为例,跟着源码走一遍「创建 → 插入 → 查找」。 + +## 字典的创建 + +从一段最简单的脚本入手,看它的字节码: + +```python +d = {} +``` + +```text +BUILD_MAP 0 # 创建一个空字典 +STORE_NAME d +``` + +`BUILD_MAP` 对应的虚拟机指令最终调用 `_PyDict_NewPresized` 来创建字典: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2358) · [Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L1242) + +```c +// Objects/dictobject.c +PyObject * +_PyDict_NewPresized(Py_ssize_t minused) +{ + ...... + Py_ssize_t newsize = PyDict_MINSIZE; // 起始大小为 8 + while (newsize < minsize) newsize <<= 1; // 不够则翻倍,始终保持 2 的幂 + ...... + new_keys = new_keys_object(newsize); // 1. 创建 keys 对象(哈希表主体) + return new_dict(new_keys, NULL); // 2. 包装成 PyDictObject 返回 +} +``` + +新字典的初始大小是 `PyDict_MINSIZE`,也就是 **8**。`new_keys_object` 负责申请并初始化 keys 对象——它会根据 `dk_size` 选择索引槽的字节宽度(表小就用 1 字节存索引,省内存),把索引数组全部填成 -1(空),并把查找函数 `dk_lookup` 设为针对字符串 key 优化的 `lookdict_unicode_nodummy`: + +`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L504) + +```c +// Objects/dictobject.c +static PyDictKeysObject *new_keys_object(Py_ssize_t size) +{ + ...... + usable = USABLE_FRACTION(size); // 可用条目数 = size 的 2/3 + if (size <= 0xff) es = 1; // 索引槽宽度随表大小而定 + else if (size <= 0xffff) es = 2; + ...... + dk->dk_size = size; + dk->dk_usable = usable; + dk->dk_lookup = lookdict_unicode_nodummy; // 默认查找函数(字符串优化版) + dk->dk_nentries = 0; + memset(&dk->dk_indices[0], 0xff, es * size); // 索引数组全置 -1(空) + ...... + return dk; +} +``` + +`new_dict` 则把 keys 包进一个 `PyDictObject`(同样有对象缓冲池可复用),把 `ma_used` 置 0,于是一个空字典就准备好了。这里出现的 `USABLE_FRACTION(size)` 是个关键数字——它等于 `size × 2 / 3`,意味着**哈希表最多用到 2/3 就会扩容**,留出足够空槽来降低冲突。 + +![新建空字典](dict-create.svg) + +## 字典的插入与查找 + +再看赋值语句的字节码: + +```python +d["1"] = "2" +``` + +```text +STORE_SUBSCR # d["1"] = "2" +``` + +`STORE_SUBSCR` 的调用链是:`PyObject_SetItem` → 取出字典类型的 `tp_as_mapping->mp_ass_subscript`(即 `dict_ass_sub`)→ 根据有无 value 分流到删除或设置: + +`源文件:`[Objects/abstract.c](https://github.com/python/cpython/blob/v3.7.0/Objects/abstract.c#L188) · [Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L2042) + +```c +// Objects/dictobject.c +static int +dict_ass_sub(PyDictObject *mp, PyObject *v, PyObject *w) +{ + if (w == NULL) + return PyDict_DelItem((PyObject *)mp, v); // d[k] 被 del → 删除 + else + return PyDict_SetItem((PyObject *)mp, v, w);// d[k] = w → 设置 +} +``` + +`PyDict_SetItem` 先算出 key 的哈希(字符串 key 直接读缓存好的 hash,否则调用 key 类型的 `tp_hash`),再交给 `insertdict` 真正落库: + +`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L1434) + +```c +// Objects/dictobject.c +int +PyDict_SetItem(PyObject *op, PyObject *key, PyObject *value) +{ + ...... + if (!PyUnicode_CheckExact(key) || + (hash = ((PyASCIIObject *) key)->hash) == -1) + { + hash = PyObject_Hash(key); // 计算哈希:调用 key 的 tp_hash + if (hash == -1) return -1; + } + return insertdict(mp, key, hash, value); +} +``` + +`insertdict` 是插入的核心。它先用 `dk_lookup` 查这个 key 在不在表里,再据此决定是「更新已有值」还是「占用一个新槽」: + +`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L994) + +```c +// Objects/dictobject.c +static int +insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value) +{ + ...... + Py_ssize_t ix = mp->ma_keys->dk_lookup(mp, key, hash, &old_value); // 先查找 + ...... + if (ix == DKIX_EMPTY) { // 没找到 → 这是一个新 key + if (mp->ma_keys->dk_usable <= 0) { // 可用槽不足,先扩容 + if (insertion_resize(mp) < 0) goto Fail; + } + Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash); // 找一个空索引槽 + ep = &DK_ENTRIES(mp->ma_keys)[mp->ma_keys->dk_nentries]; // 在 entries 末尾追加 + dk_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries); // 索引槽 → 记录下标 + ep->me_key = key; // 写入 key + ep->me_hash = hash; // 写入哈希 + ep->me_value = value; // 写入 value(combined 表) + mp->ma_used++; + mp->ma_keys->dk_usable--; + mp->ma_keys->dk_nentries++; // entries 又多了一条 + return 0; + } + ...... // 找到了 → 更新旧 value + DK_ENTRIES(mp->ma_keys)[ix].me_value = value; + ...... +} +``` + +注意新 key 的写入方式,正好印证了前面的设计:记录总是**追加到 `dk_entries` 末尾**(保持插入顺序),同时把它的下标登记进哈希定位到的那个**索引槽**。 + +![插入示意](dict-insert.svg) + +那「按哈希定位、处理冲突」具体怎么做?看默认的查找函数 `lookdict_unicode_nodummy`: + +`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L816) + +```c +// Objects/dictobject.c +static Py_ssize_t _Py_HOT_FUNCTION +lookdict_unicode_nodummy(PyDictObject *mp, PyObject *key, + Py_hash_t hash, PyObject **value_addr) +{ + ...... + size_t mask = DK_MASK(mp->ma_keys); + size_t perturb = (size_t)hash; + size_t i = (size_t)hash & mask; // 初始槽位 = hash & (size-1) + + for (;;) { + Py_ssize_t ix = dk_get_index(mp->ma_keys, i); // 取索引槽的值 + if (ix == DKIX_EMPTY) { // 空槽 → 表里没有这个 key + *value_addr = NULL; + return DKIX_EMPTY; + } + PyDictKeyEntry *ep = &ep0[ix]; + if (ep->me_key == key || // 命中:同一对象,或哈希相同且值相等 + (ep->me_hash == hash && unicode_eq(ep->me_key, key))) { + *value_addr = ep->me_value; + return ix; + } + perturb >>= PERTURB_SHIFT; // 没命中,按扰动探测下一个槽 + i = mask & (i*5 + perturb + 1); + } +} +``` + +逻辑是经典的**开放寻址**: + +1. 用 `hash & (size - 1)` 取初始槽位(因 `size` 是 2 的幂,等价于对 `size` 取模); +2. 槽位为空 → key 不存在;槽位非空但 key 不匹配 → 发生**哈希冲突**,按 `i = (i*5 + perturb + 1) & mask` 探测下一个槽,并把 `perturb`(初值为完整哈希)逐步右移 `PERTURB_SHIFT`(=5)位混入,让探测序列既能快速跳开、又能逐渐覆盖整张表; +3. 直到命中相同的 key,或撞上空槽为止。 + +![查找与探测](dict-lookup.svg) + +`PyDict_SetItem` 复用了同一套查找:找到就更新 value,没找到(撞空槽)就在那个位置安家。查找和插入共享这套寻址,这正是字典平均 O(1) 的来源。 + +## 动手观测紧凑字典 + +光看代码不够直观,我们可以改源码把哈希表的内部状态打印出来。在 `insertdict` 里加几行,打印 `dk_indices`(索引数组)和 `dk_entries`(键值记录),[重新编译](../../preface/unix-linux-build/)后操作一个字典(key 用整数,因为整数的哈希就是它自身,方便心算槽位): + +```python +>>> d = {20000: 2} + indices : 0 -1 -1 -1 -1 -1 -1 -1 # size 8 + entries : key 20000 value 2 +``` + +`20000 & 7 == 0`,所以 key 20000 落在 0 号索引槽,槽里存的 `0` 表示它是 entries 中的第 0 条。继续插入: + +```python +>>> d[2] = 3 # 2 & 7 = 2 + indices : 0 -1 1 -1 -1 -1 -1 -1 +>>> d[3] = 4 # 3 & 7 = 3 + indices : 0 -1 1 2 -1 -1 -1 -1 +>>> d[5] = 6 # 5 & 7 = 5 + indices : 0 -1 1 2 -1 3 -1 -1 +>>> d[7] = 8 # 7 & 7 = 7 + indices : 0 -1 1 2 -1 3 -1 4 +``` + +可以清楚看到:**索引数组是稀疏的**(`-1` 是空槽),里面存的只是「这是第几条记录」;**记录本身则按插入顺序紧凑排在 entries 里**。当装入第 6 个键值对时(此时 size=8,可用上限 `8 × 2/3 ≈ 5`),表超过 2/3 负载,于是自动扩容: + +```python +>>> d[9] = 10 # 触发扩容 + indices : 0 -1 1 2 -1 3 -1 4 -1 5 -1 -1 -1 -1 -1 -1 # size 8 → 16 + entries : 20000→2, 2→3, 3→4, 5→6, 7→8, 9→10 # 顺序保持不变 +``` + +`dk_size` 从 8 变成 16,索引数组随之扩大、所有 key 按新 mask 重新定位,但 **entries 里的插入顺序原封不动**。这就是字典「容量翻倍扩容」和「遍历有序」在内部的真实样子。 + +--- + +小结一下字典的实现要点: + +- 字典是一张**哈希表**,平均 O(1) 存取; +- 3.6 起采用**紧凑字典**:稀疏的 `dk_indices` 只存「位置」,紧凑的 `dk_entries` 按插入顺序存「哈希 + key + value」——前者带来内存节省,后者带来**遍历有序**; +- 默认是 **combined 表**,实例 `__dict__` 用共享 keys 的 **split 表**省内存; +- 冲突用**开放寻址 + 扰动探测**解决,负载达到 **2/3** 即扩容(容量保持 2 的幂、按需翻倍)。 diff --git a/docs/objects/float-object/float-freelist.svg b/docs/objects/float-object/float-freelist.svg new file mode 100644 index 0000000..1ab6250 --- /dev/null +++ b/docs/objects/float-object/float-freelist.svg @@ -0,0 +1,32 @@ + +浮点数的 free list 缓冲池 +CPython 用一条单链表 free_list 缓存被释放的浮点对象,最多保存 100 个。创建 PyFloat_FromDouble 时若链表非空就取链表头复用,否则才 malloc 新建;浮点数销毁时挂回链表头,超过 100 个才真正释放。 + + + + + + + + +浮点数缓冲池 free_list(单链表,最多 100 个) + +free_list + + + + + 空闲 float + 空闲 float + 空闲 float + +⋯ ≤ 100 + +创建 PyFloat_FromDouble:链表非空就取链表头复用,否则 malloc 新建 +销毁:挂回链表头;已满 100 个时才真正释放 + diff --git a/docs/objects/float-object/float-hash.svg b/docs/objects/float-object/float-hash.svg new file mode 100644 index 0000000..1bdfb00 --- /dev/null +++ b/docs/objects/float-object/float-hash.svg @@ -0,0 +1,37 @@ + +数值相等的 int 与 float 哈希一致 +整数 1 与浮点数 1.0 数值相等(1 == 1.0),CPython 让它们的哈希也相等(hash(1) == hash(1.0))。因此在字典里它们是同一个键:d = {1: 'a'} 之后,d[1.0] 也得到 'a'。 + + + + + + + + + +数值相等的 int 与 float 是同一个字典键 + +int 1 +float 1.0 + + + + + + + +1 == 1.0 +hash 相同 + + + +字典里同一项 + +d = {1: 'a'} → d[1.0] 也得到 'a' + diff --git a/docs/objects/float-object/float-ieee754.svg b/docs/objects/float-object/float-ieee754.svg new file mode 100644 index 0000000..12ec7ff --- /dev/null +++ b/docs/objects/float-object/float-ieee754.svg @@ -0,0 +1,36 @@ + +IEEE 754 双精度与 0.1 的不精确 +Python 的 float 是 IEEE 754 双精度浮点数,64 位由 1 位符号、11 位指数、52 位尾数组成。0.1 的二进制是无限循环小数,52 位尾数装不下,只能存最接近的值约 0.1000000000000000055,所以 0.1 + 0.2 得到 0.30000000000000004,不等于 0.3。 + + + + + + + + + +IEEE 754 双精度(double,共 64 位) + + + +符号 +1 位 + +指数 +11 位 + +尾数(小数部分) +52 位 + + +0.1 的二进制是无限循环小数 0.0001100110011…,52 位尾数装不下 +→ 只能存最接近的值 ≈ 0.1000000000000000055… + +所以 0.1 + 0.2 = 0.30000000000000004 ≠ 0.3(就像十进制写不尽 1/3) + diff --git a/docs/objects/float-object/float-struct.svg b/docs/objects/float-object/float-struct.svg new file mode 100644 index 0000000..45c589d --- /dev/null +++ b/docs/objects/float-object/float-struct.svg @@ -0,0 +1,33 @@ + +PyFloatObject:定长对象 +PyFloatObject 只在对象头之后放一个 C 的 double(ob_fval,8 字节),是定长对象——任何浮点数都占同样大小,getsizeof(0.0) 等于 getsizeof(1e308)。这与整数 PyLongObject 的变长(按位段增长)形成对照。 + + + + + + + + + + + +PyFloatObject + + +ob_refcnt*ob_typeob_fval +double · 8 字节 + + + +定长对象:大小恒定 +getsizeof(0.0) == getsizeof(1e308) + +对照整数 PyLongObject 是变长 +(按位段增长);浮点数始终一个 double + diff --git a/docs/objects/float-object/index.md b/docs/objects/float-object/index.md new file mode 100644 index 0000000..c3ea41e --- /dev/null +++ b/docs/objects/float-object/index.md @@ -0,0 +1,185 @@ +# Python 浮点数对象 + +浮点数大概是最容易让人「踩坑」的类型——下面这一幕几乎每个 Python 工程师都见过: + +```python +>>> 0.1 + 0.2 +0.30000000000000004 +>>> 0.1 + 0.2 == 0.3 +False +``` + +这不是 Python 的 bug,而是 IEEE 754 浮点数的固有特性。这一章我们就来看 `PyFloatObject` 是怎么实现的,并把「为什么不精确」讲清楚。 + +## 数据结构 + +浮点数的结构体简单到了极点: + +`源文件:`[Include/floatobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/floatobject.h#L15) + +```c +// Include/floatobject.h +typedef struct { + PyObject_HEAD + double ob_fval; // 一个 C 的 double +} PyFloatObject; +``` + +对象头之后只跟一个 C 的 `double`(`ob_fval`)。注意它用的是 `PyObject_HEAD` 而非 `PyObject_VAR_HEAD`——所以浮点数是**定长对象**:无论值是 `0.0` 还是 `1e308`,占用的内存都一样大。 + +![PyFloatObject 定长结构](float-struct.svg) + +这一点可以直接验证,也正好和整数(变长、按位段增长)形成对照: + +```python +>>> import sys +>>> sys.getsizeof(0.0) == sys.getsizeof(1e308) +True +``` + +## IEEE 754:浮点数为什么「不精确」 + +那个 `double` 遵循 IEEE 754 双精度标准:64 个二进制位,分成 **1 位符号 + 11 位指数 + 52 位尾数**。它能表示极大、极小的数,但只有 **52 位尾数**来记录有效数字。 + +问题就出在这里:很多十进制小数,换成二进制是**无限循环小数**,52 位根本装不下。`0.1` 就是典型——它的二进制是 `0.0001100110011…` 无限循环,就像十进制写不尽 `1/3` 一样。于是计算机只能存一个**最接近的可表示值**: + +![IEEE 754 与 0.1 的不精确](float-ieee754.svg) + +`0.1` 在内存里实际存的并不是 0.1,而是一个极接近它的值。把它的「真身」打印出来看看: + +```python +>>> from decimal import Decimal +>>> Decimal(0.1) +Decimal('0.1000000000000000055511151231257827021181583404541015625') +``` + +既然 `0.1`、`0.2` 存进去就已经有微小误差,它们相加自然不会正好等于同样有误差的 `0.3`,于是 `0.1 + 0.2` 得到 `0.30000000000000004`。 + +> 那为什么 `print(0.1)` 显示的是干净的 `0.1`?因为 Python 的 `repr` 会输出「能唯一还原出这个 double 的最短十进制字符串」——`0.1` 足以还原,就不必把后面那串 `…0055` 都显示出来。显示是「最短表示」,存储仍是那个带误差的二进制值。 + +所以涉及金额等需要精确小数的场景,应改用 `decimal.Decimal` 或整数(以「分」为单位)。 + +## 浮点数的创建与缓冲池 + +和整数、元组类似,浮点数也有**缓冲池**复用对象,避免频繁申请释放。它用一条单链表 `free_list`,最多缓存 100 个: + +`源文件:`[Objects/floatobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/floatobject.c#L24) + +```c +// Objects/floatobject.c +#define PyFloat_MAXFREELIST 100 // 最多缓存 100 个空闲浮点对象 +static PyFloatObject *free_list = NULL; // 空闲对象单链表 +``` + +创建浮点数走 `PyFloat_FromDouble`:链表非空就取链表头复用,否则才向系统申请: + +`源文件:`[Objects/floatobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/floatobject.c#L115) + +```c +// Objects/floatobject.c +PyObject * +PyFloat_FromDouble(double fval) +{ + PyFloatObject *op = free_list; + if (op != NULL) { + free_list = (PyFloatObject *) Py_TYPE(op); // 取链表头复用(链表穿过 ob_type 字段) + numfree--; + } else { + op = (PyFloatObject*) PyObject_MALLOC(sizeof(PyFloatObject)); // 池空才新建 + ...... + } + (void)PyObject_INIT(op, &PyFloat_Type); + op->ob_fval = fval; // 填入数值 + return (PyObject *) op; +} +``` + +![浮点数缓冲池 free_list](float-freelist.svg) + +浮点数销毁时则挂回链表头;只有当池里已满 100 个,才真正 `free` 掉。 + +## 浮点数的比较 + +浮点数之间的比较是直接比 `double`。有意思的是**浮点数和整数比较**——CPython 在这里特别小心,避免精度损失。看 `float_richcompare`: + +`源文件:`[Objects/floatobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/floatobject.c#L348) + +```c +// Objects/floatobject.c —— float_richcompare(与 int 比较的分支) +else if (PyLong_Check(w)) { + int vsign = i == 0.0 ? 0 : i < 0.0 ? -1 : 1; + int wsign = _PyLong_Sign(w); + if (vsign != wsign) { + /* 符号不同,光看符号就能定胜负,无需比较大小 */ + ...... + } + /* 符号相同:按位数等信息精确比较,而不是粗暴地把 int 转成 double */ + nbits = _PyLong_NumBits(w); + ...... +} +``` + +它没有简单地「把 int 转成 double 再比」——因为一个很大的整数转成 `double` 会丢失精度,比较结果就可能出错。CPython 先比符号,再按整数的位数等信息**精确**比较。这带来一个微妙但正确的结果: + +```python +>>> 2.0 ** 53 == 2 ** 53 +True +>>> 2.0 ** 53 == 2 ** 53 + 1 # float 表示不了 2^53+1,但比较仍然精确 +False +>>> float(2 ** 53 + 1) == 2 ** 53 + 1 # 一旦把 int 转成 float,就丢了精度 +False +``` + +`2.0 ** 53` 这个 `double` 和整数 `2**53+1` 比较,得到正确的 `False`;而如果先 `float(2**53+1)`,它会被舍入成 `2^53`,精度就丢了。 + +## 浮点数的哈希 + +为了让「数值相等的对象哈希也相等」,CPython 精心设计了数值的哈希:**值相等的 `int` 和 `float` 哈希一致**。 + +```python +>>> 2.0 == 2 +True +>>> hash(2.0) == hash(2) +True +``` + +这条规则很重要:它保证了数值相等的 `int` 与 `float` 在字典、集合里是**同一个键**。 + +![int 与 float 哈希一致](float-hash.svg) + +```python +>>> d = {1: 'a'} +>>> d[1.0] # 1 == 1.0 且哈希相同 → 命中同一项 +'a' +``` + +(浮点数的哈希由 `_Py_HashDouble` 计算,整数与浮点数共用一套规则,使等值者哈希一致。) + +## 特殊值:inf 与 nan + +IEEE 754 还定义了几个特殊值:正负**无穷大** `inf` 和**非数** `nan`(Not a Number,如 `0.0/0.0` 的结果)。 + +```python +>>> float('inf'), float('-inf'), float('nan') +(inf, -inf, nan) +``` + +`nan` 有一个反直觉但符合标准的性质:**它不等于任何值,包括它自己**。 + +```python +>>> n = float('nan') +>>> n == n +False +``` + +所以判断一个浮点数是不是 `nan`,不能用 `x == x`(对 `nan` 恒为 `False`),要用 `math.isnan(x)`。这也意味着含 `nan` 的容器在做成员判断时要小心。 + +--- + +小结一下浮点数对象的要点: + +- `PyFloatObject` 只在对象头后放一个 C 的 `double`,是**定长对象**(对照整数的变长),任何浮点数大小恒定; +- 它遵循 **IEEE 754 双精度**:52 位尾数装不下像 `0.1` 这样的二进制无限循环小数,只能存最接近的值,这就是 `0.1 + 0.2 != 0.3` 的根源;需要精确小数请用 `decimal`; +- 创建时有 **free list 缓冲池**(单链表,≤ 100)复用对象; +- 与整数比较时**精确处理**避免精度损失;数值相等的 `int` 与 `float` **哈希一致**,是同一个字典键; +- 特殊值 `inf`、`nan` 中,`nan` 不等于包括自身在内的任何值,判断用 `math.isnan`。 diff --git a/docs/objects/list-object/PyListStructure.svg b/docs/objects/list-object/PyListStructure.svg new file mode 100644 index 0000000..40e67b4 --- /dev/null +++ b/docs/objects/list-object/PyListStructure.svg @@ -0,0 +1,53 @@ + +PyListObject 存储结构 +PyListObject 基于 PyVarObject(含 PyObject ob_base 与 ob_size),通过 *ob_item 指向元素指针数组,allocated 记录数组容量。执行 lst=[]; lst.append(1) 后 ob_size 为 1、allocated 为 4,ob_item 第 0 项为 1,其余为 null。 + + + + + + + + + + + + +PyListObject + + + + +PyVarObject + +PyObject ob_base +ob_size = 1 + + + + +*ob_item + + +allocated +4 + + + + + +ob_item(容量 allocated = 4) + + + +1 +nullnullnull + +lst = []; lst.append(1) → ob_size = 1,allocated = 4 + diff --git a/docs/objects/list-object/index.md b/docs/objects/list-object/index.md new file mode 100644 index 0000000..0feee66 --- /dev/null +++ b/docs/objects/list-object/index.md @@ -0,0 +1,266 @@ +# Python 列表对象 + +`list` 大概是 Python 里用得最顺手的容器:能装任意类型、随便混搭,下标访问飞快,`append` 起来也不心疼。 + +```python +>>> lst = [1, "hello", 3.14, [2, 3]] # 什么类型都能往里放 +>>> lst[0] # 下标访问,瞬间返回 +1 +>>> lst.append("new") # 追加,几乎不花时间 +``` + +它凭什么能「什么都装」又「下标飞快」?答案藏在一个关键设计里:**`list` 存的不是元素本身,而是一排指向元素的指针**。这一章我们就来看 `PyListObject` 是怎么实现这个「动态指针数组」的。 + +## PyListObject:一排指针 + 容量账本 + +`源文件:`[Include/listobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/listobject.h#L23) + +```c +// Include/listobject.h +typedef struct { + PyObject_VAR_HEAD // 变长对象头,含 ob_size = 当前元素个数 + PyObject **ob_item; // 指向「元素指针数组」的指针 + Py_ssize_t allocated; // 已申请的容量(可容纳多少个元素) +} PyListObject; +``` + +短短三部分,却把 `list` 的两个特性都解释清楚了: + +- **`ob_item`** 是一个 `PyObject **`——指向一段连续内存,里面存的是一个个 `PyObject *` 指针,`list[i]` 就是 `ob_item[i]`。因为存的是「指针」而非对象本体,所以列表能混装任意类型;因为指针数组连续排列,所以按下标取值是一次地址计算,**O(1)**。 +- **`ob_size` 与 `allocated`** 是一对「容量账本」:`ob_size`(来自对象头)是**当前实际元素个数**(就是 `len(lst)`),`allocated` 是**已经申请好的容量**。两者满足 `0 <= ob_size <= allocated`——也就是说,列表往往会**预留一些空位**,不会每加一个元素就重新申请内存。这个细节正是 `append` 高效的关键,后面会细讲。 + +来看个最简单的例子: + +```python +lst = [] +lst.append(1) +``` + +执行后,列表里有 1 个元素(`ob_size == 1`),但底层一次就申请了 4 个空位(`allocated == 4`),`ob_item[0]` 指向整数对象 `1`,其余空位待填: + +![PyList structure](PyListStructure.svg) + +## 列表的创建 + +频繁创建、销毁列表(比如循环里不断生成临时列表)开销不小。CPython 为此准备了一个**空闲列表对象缓冲池** `free_list`,销毁的列表对象先回收到池里,下次创建时优先复用: + +`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L104) + +```c +// Objects/listobject.c +/* Empty list reuse scheme to save calls to malloc and free */ +#ifndef PyList_MAXFREELIST +#define PyList_MAXFREELIST 80 // 缓冲池最多缓存 80 个列表对象 +#endif +static PyListObject *free_list[PyList_MAXFREELIST]; +static int numfree = 0; +``` + +创建函数 `PyList_New` 的逻辑就是「**池里有就复用,没有才向系统申请**」: + +`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L138) + +```c +// Objects/listobject.c +PyObject * +PyList_New(Py_ssize_t size) +{ + PyListObject *op; + ...... + if (numfree) { // 缓冲池有可用对象 + numfree--; + op = free_list[numfree]; // 直接复用 + _Py_NewReference((PyObject *)op); + } else { // 池空,才真正申请内存 + op = PyObject_GC_New(PyListObject, &PyList_Type); + ...... + } + if (size <= 0) + op->ob_item = NULL; + else { // 为元素指针数组申请空间 + op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *)); + ...... + } + Py_SIZE(op) = size; + op->allocated = size; + _PyObject_GC_TRACK(op); + return (PyObject *) op; +} +``` + +注意这里复用的只是 `PyListObject` 这个「壳」(对象头那部分),存放元素的 `ob_item` 数组仍按需另行申请。 + +![列表的创建与缓冲池](list-create.svg) + +## 列表的下标存取 + +下标访问之所以快,是因为它直接落到 `ob_item[i]`,没有任何查找: + +`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L197) + +```c +// Objects/listobject.c +PyObject * +PyList_GetItem(PyObject *op, Py_ssize_t i) +{ + ...... + if (i < 0 || i >= Py_SIZE(op)) { // 越界检查 + ...... + PyErr_SetObject(PyExc_IndexError, indexerr); + return NULL; + } + return ((PyListObject *)op) -> ob_item[i]; // 直接返回第 i 个指针 +} +``` + +赋值 `list[i] = x` 走 `PyList_SetItem`,同样是定位到 `ob_item + i` 后替换指针(并用 `Py_XSETREF` 妥善处理新旧元素的引用计数): + +`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L217) + +```c +// Objects/listobject.c +int +PyList_SetItem(PyObject *op, Py_ssize_t i, PyObject *newitem) +{ + PyObject **p; + ...... + p = ((PyListObject *)op) -> ob_item + i; + Py_XSETREF(*p, newitem); // 用 newitem 替换旧指针,处理好引用计数 + return 0; +} +``` + +## 列表的追加与插入 + +`append` 对应 `PyList_Append`,它转交给内部的 `app1`: + +`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L281) + +```c +// Objects/listobject.c +static int +app1(PyListObject *self, PyObject *v) +{ + Py_ssize_t n = PyList_GET_SIZE(self); + ...... + if (list_resize(self, n+1) < 0) // 1. 把容量调整到能放下 n+1 个 + return -1; + Py_INCREF(v); + PyList_SET_ITEM(self, n, v); // 2. 放到末尾 + return 0; +} +``` + +流程很直白:先把大小调到 `n+1`,再把新元素放到末尾。由于通常有预留空位,多数 `append` 连内存都不用重新申请(详见下一节),所以很快。 + +![append 示意](list-append.svg) + +`insert` 对应 `ins1`,比 `append` 多一步——**要把插入点之后的元素整体后移**: + +`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L238) + +```c +// Objects/listobject.c +static int +ins1(PyListObject *self, Py_ssize_t where, PyObject *v) +{ + Py_ssize_t i, n = Py_SIZE(self); + PyObject **items; + ...... + if (list_resize(self, n+1) < 0) // 1. 扩到 n+1 + return -1; + ...... + items = self->ob_item; + for (i = n; --i >= where; ) // 2. where 之后的元素逐个后移一格 + items[i+1] = items[i]; + Py_INCREF(v); + items[where] = v; // 3. 空出来的位置放新元素 + return 0; +} +``` + +![insert 示意](list-insert.svg) + +这解释了一个常见的性能直觉:`lst.append(x)` 摊还下来是 O(1),而 `lst.insert(0, x)`(在头部插入)要搬移全部元素,是 O(n)。需要频繁头部插入时,应改用 `collections.deque`。 + +## 列表的扩容 + +追加和插入都要先调用 `list_resize`。它是列表性能的核心,做了一个关键优化:**容量不是一次加一,而是「过分配」一批,留作后续增长**。 + +`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L34) + +```c +// Objects/listobject.c +static int +list_resize(PyListObject *self, Py_ssize_t newsize) +{ + ...... + Py_ssize_t allocated = self->allocated; + + /* 容量够用、且没缩水到一半以下:只改 ob_size,不动内存 */ + if (allocated >= newsize && newsize >= (allocated >> 1)) { + Py_SIZE(self) = newsize; + return 0; + } + + /* 否则按比例过分配,预留增长空间。 + 增长序列为:0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ... */ + new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6); + ...... + items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes); + ...... + self->ob_item = items; + Py_SIZE(self) = newsize; + self->allocated = new_allocated; + return 0; +} +``` + +理解这段的关键是分清两种情况: + +- **容量够用**(`allocated/2 <= newsize <= allocated`):直接改 `ob_size` 就返回,**不申请内存**。这就是为什么连续 `append` 大多数时候几乎零成本。 +- **容量不足或严重冗余**:才调用 `PyMem_Realloc` 重新分配,并按 `new_allocated = newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6)` **多要一些**(约 1/8),于是容量按 `0, 4, 8, 16, 25, 35, 46, 58, 72, 88, …` 的节奏增长。 + +![扩容示意](list-resize.svg) + +正因为每次扩容都「多要一点」,n 次 `append` 触发的 `realloc` 次数是 O(log n) 级别、搬移的总元素数是 O(n) 级别,平摊到每次 `append` 就是 **O(1)**。反过来,当列表大幅缩短(`newsize` 小于 `allocated` 的一半)时,也会触发一次 `realloc` 把多余空间还回去。 + +## 列表的删除 + +`lst.remove(x)` 对应 `list_remove`:从头**线性查找**第一个等于 `x` 的元素,找到就删除: + +`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L2548) + +```c +// Objects/listobject.c +static PyObject * +list_remove(PyListObject *self, PyObject *value) +{ + Py_ssize_t i; + for (i = 0; i < Py_SIZE(self); i++) { + int cmp = PyObject_RichCompareBool(self->ob_item[i], value, Py_EQ); + if (cmp > 0) { // 找到相等的元素 + if (list_ass_slice(self, i, i+1, (PyObject *)NULL) == 0) + Py_RETURN_NONE; // 用切片赋值删除该位置 + return NULL; + } + else if (cmp < 0) + return NULL; // 比较过程中出错 + } + PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); + return NULL; +} +``` + +它用 `PyObject_RichCompareBool(..., Py_EQ)` 逐个做「值相等」比较(注意是 `==` 而非 `is`),命中后交给 `list_ass_slice` 完成实际删除;遍历到底都没找到就抛 `ValueError`。 + +![remove 示意](list-remove.svg) + +--- + +小结一下 `list` 的实现要点: + +- `PyListObject` 本质是一个**指针动态数组**:`ob_item` 存一排 `PyObject *`,因此能混装任意类型,且下标存取是 O(1); +- `ob_size`(实际元素数)与 `allocated`(已分配容量)分离,配合 `list_resize` 的**过分配**策略,使 `append` 摊还为 O(1); +- 头部插入/删除要搬移元素(O(n)),有此需求应选 `deque`; +- 创建时还有 `free_list` 缓冲池复用对象,减少内存申请。 diff --git a/docs/objects/list-object/list-append.svg b/docs/objects/list-object/list-append.svg new file mode 100644 index 0000000..ff74fef --- /dev/null +++ b/docs/objects/list-object/list-append.svg @@ -0,0 +1,42 @@ + +列表追加 append 示意 +对 [1,2,3](ob_size=3,allocated=4)执行 append(4):末尾本就预留了一个空位,直接把 4 放进去,ob_size 变为 4,allocated 不变,无需重新分配内存。 + + + + + + + + + +调用前 +0123 + + +123 + +ob_size = 3allocated = 4 + + +append(4) + + + +调用后 +0123 + + + +123 +4 +ob_size = 4allocated = 4 + +末尾有预留空位 → 直接放入,无需重新分配内存(append 摊还 O(1)) + diff --git a/docs/objects/list-object/list-create.svg b/docs/objects/list-object/list-create.svg new file mode 100644 index 0000000..309912a --- /dev/null +++ b/docs/objects/list-object/list-create.svg @@ -0,0 +1,42 @@ + +列表的创建与 free_list 缓冲池 +创建列表时,PyList_New 先看缓冲池 free_list(最多缓存 80 个已释放的列表对象壳):池里有就复用一个,没有才用 PyObject_GC_New 新建。复用或新建的只是 PyListObject 对象壳(对象头部分),存放元素指针的 ob_item 数组总是另行申请。 + + + + + + + + + + + + +free_list(最多缓存 80 个空闲对象壳) + + +⋯ 壳 + + +PyList_New(size) + +numfree > 0 ? + + + +是:复用一个壳 + + + + +PyObject_GC_New 新建对象壳 + +复用/新建的只是对象壳;存放元素指针的 ob_item 数组总是另行申请 + diff --git a/docs/objects/list-object/list-insert.svg b/docs/objects/list-object/list-insert.svg new file mode 100644 index 0000000..4017dc0 --- /dev/null +++ b/docs/objects/list-object/list-insert.svg @@ -0,0 +1,44 @@ + +列表插入 insert 示意 +对 [1,2,3] 执行 insert(1, 9):插入点 index 1 之后的元素 2、3 整体右移一位,腾出的 index 1 放入新元素 9,结果为 [1,9,2,3],是 O(n) 操作。 + + + + + + +insert(1, 9) + + +调用前 +0123 + + +123 + + + + + + + + +2、3 右移一位 + + +调用后 +0123 + + + +123 +9 + +插入点之后的元素整体右移一位,腾出位置放入新元素(O(n)) + diff --git a/docs/objects/list-object/list-remove.svg b/docs/objects/list-object/list-remove.svg new file mode 100644 index 0000000..c0a4fbb --- /dev/null +++ b/docs/objects/list-object/list-remove.svg @@ -0,0 +1,45 @@ + +列表删除 remove 示意 +对 [1,2,3,4] 执行 remove(2):从头线性查找(按 == 比较)到 index 1 的元素 2,删除后其后的 3、4 整体左移一位填补,ob_size 减一,结果为 [1,3,4],是 O(n) 操作。 + + + + + + +remove(2) + + +调用前 +0123 + + + +134 +2 +查找到 2,删除 + + + + + + + +3、4 左移一位 + + +调用后 +0123 + + +134 + + +线性查找目标值,删除后其后元素整体左移一位,ob_size 减一(O(n)) + diff --git a/docs/objects/list-object/list-resize.svg b/docs/objects/list-object/list-resize.svg new file mode 100644 index 0000000..89d0244 --- /dev/null +++ b/docs/objects/list-object/list-resize.svg @@ -0,0 +1,44 @@ + +列表扩容(over-allocation)示意 +当容量用尽(ob_size == allocated)时再 append,list_resize 会 realloc 一块更大的内存(多分配约 1/8 作预留),把原元素拷贝过去并放入新元素。容量增长序列为 0,4,8,16,25,35,46,58,72,88,…。 + + + + + + + + + +容量已满 +0123 + + +1234 +ob_size = allocated = 4 + + +append(5) + + + +realloc 更大内存 +01234567 + + + + +1234 +5 +预留空位 +ob_size = 5 allocated = 8 + +容量不足时 realloc 一块更大内存,多分配约 1/8 作预留,避免每次追加都重新分配 +容量增长序列:0, 4, 8, 16, 25, 35, 46, 58, 72, 88, … + diff --git a/docs/objects/long-object/index.md b/docs/objects/long-object/index.md new file mode 100644 index 0000000..a950d23 --- /dev/null +++ b/docs/objects/long-object/index.md @@ -0,0 +1,433 @@ +# Python 整数对象 + +写其它语言时,整数有 `int`、`long`、`int64` 之分,还要时刻提防溢出。Python 却不一样——它的整数想多大就多大: + +```python +>>> 2 ** 1000 # 一个 302 位的大整数,精确无误 +10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376 +>>> import math +>>> math.factorial(100) # 100 的阶乘,158 位 +93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000 +``` + +这种「任意精度、永不溢出」的整数,到底是怎么实现的?这一章我们就钻进 CPython 的整数对象一探究竟。 + +> 一点历史:CPython 2 里整数有 `PyIntObject`(机器字长的小整数)和 `PyLongObject`(任意精度的大整数)两套实现;到了 CPython 3,两者合并,只保留了 `PyLongObject`。这个统一过程并不轻松——[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3) 第 3 行至今还留着一句自嘲:`/* XXX The functional organization of this file is terrible */`。 + +## PyLongObject:把大整数拆成「位段」 + +要支持任意大的整数,固定字长(如 64 位)肯定不够。CPython 的思路是:**把一个大整数拆成若干段,存进一个变长数组里**。先看类型定义: + +`源文件:`[Include/longobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/longobject.h#L10) + +```c +// Include/longobject.h +typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */ +``` + +真正的结构体藏在另一个头文件里: + +`源文件:`[Include/longintrepr.h](https://github.com/python/cpython/blob/v3.7.0/Include/longintrepr.h#L85) + +```c +// Include/longintrepr.h +struct _longobject { + PyObject_VAR_HEAD // 变长对象头:含 ob_refcnt、ob_type、ob_size + digit ob_digit[1]; // 存放各「位段」的数组(实际长度按需分配) +}; +``` + +回顾上一章:`PyObject_VAR_HEAD` 说明 `PyLongObject` 是个**变长对象**,带有记录长度的 `ob_size`。而 `ob_digit` 是一个「柔性数组」——声明时写 `[1]` 只是占位,创建对象时会按实际需要分配足够的空间,让 `ob_digit[0] ... ob_digit[abs(ob_size)-1]` 都可用。 + +那这些「位段」是怎么拼成一个整数的?源码注释把规则讲得很清楚: + +`源文件:`[Include/longintrepr.h](https://github.com/python/cpython/blob/v3.7.0/Include/longintrepr.h#L72) + +``` +一个数的绝对值 = SUM(for i = 0 .. abs(ob_size)-1) ob_digit[i] * 2**(SHIFT * i) + +- 负数:用 ob_size < 0 表示(即 ob_size 的符号位兼任整数的正负号) +- 零 :用 ob_size == 0 表示 +- 规范化:最高位 ob_digit[abs(ob_size)-1] 不为 0 +``` + +换句话说,整数被当成一个 **2^SHIFT 进制的大数**:每个 `ob_digit` 是这个进制下的一「位」,`ob_size` 记录用了多少位、同时用正负号表示整数的正负。 + +这里的 `SHIFT` 就是 `PyLong_SHIFT`:64 位平台上是 **30**,32 位平台上是 15。也就是说,64 位下每个 `ob_digit` 装 30 个二进制位。 + +### 亲眼看看整数怎么存 + +光看注释不过瘾,我们可以改源码来「实地观测」。整数转成十进制字符串时会经过 `long_to_decimal_string_internal`,在它开头插几行打印: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L1582) + +```c +// Objects/longobject.c +static int +long_to_decimal_string_internal(PyObject *aa, ...) +{ + PyLongObject *a; + ... + a = (PyLongObject *)aa; + + // 临时加入的打印代码 + printf("ob_size = %d\n", Py_SIZE(a)); + for (int i = 0; i < Py_SIZE(a); ++i) { + printf("ob_digit[%d] = %d\n", i, a->ob_digit[i]); + } + ... +} +``` + +[重新编译安装](../../preface/unix-linux-build/)后,打印一个大整数: + +```python +>>> num = 9223372043297226753 +>>> print(num) +ob_size = 3 +ob_digit[0] = 1 +ob_digit[1] = 6 +ob_digit[2] = 8 +9223372043297226753 +``` + +`ob_size == 3` 说明用了 3 个位段,`ob_digit` 依次是 1、6、8。代入上面的公式(`SHIFT = 30`)验证一下: + +![longobject storage](long-storage.svg) + +`1·(2³⁰)⁰ + 6·(2³⁰)¹ + 8·(2³⁰)²` 正好等于 `9223372043297226753`。整数的存储结构,就是这么回事。 + +## 类型对象 PyLong_Type + +整数实例的 `ob_type` 指向类型对象 `PyLong_Type`,它就是 Python 里的 `int`。上一章讲过类型对象是对象的「说明书」,这里挑几个和整数密切相关的槽位看看: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5379) + +```c +// Objects/longobject.c +PyTypeObject PyLong_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "int", /* tp_name */ + offsetof(PyLongObject, ob_digit), /* tp_basicsize */ + sizeof(digit), /* tp_itemsize */ + long_dealloc, /* tp_dealloc */ + ...... + long_to_decimal_string, /* tp_repr */ + &long_as_number, /* tp_as_number */ // 数值操作族 + ...... + (hashfunc)long_hash, /* tp_hash */ + ...... + long_new, /* tp_new */ // 创建整数的入口 + PyObject_Del, /* tp_free */ +}; +``` + +注意 `tp_basicsize` 和 `tp_itemsize` 这一对:变长对象的内存大小 = `tp_basicsize + 元素个数 × tp_itemsize`。这里基础大小是「对象头到 `ob_digit` 之前」的偏移,每多一个位段就多 `sizeof(digit)` 字节——这正是「数值越大越占内存」的根源。`tp_as_number` 指向整数的数值操作族(稍后细看),`tp_new` 指向创建整数的入口 `long_new`。 + +## 小整数对象池 + +先看一个让很多人困惑的现象: + +```python +>>> a = 256 +>>> a is int("256") # 256 与「新建的 256」竟是同一个对象 +True +>>> n = 257 +>>> n is int("257") # 257 与「新建的 257」却不是 +False +``` + +`int("256")` 会在运行时新建一个值为 256 的整数。照理说它和字面量 `256` 应是两个不同对象,可 `is` 却判定它们是同一个;换成 257 就不是了。为什么 256 能共享同一对象、257 却不能?答案是**小整数对象池**。 + +像 0、1、-1 这些小整数在程序里出现得极其频繁,如果每次都新建、销毁,开销很大。于是 CPython 在解释器启动时就**预先创建好一批小整数并一直留着**,用到时直接取来共享,免去反复申请内存。 + +预分配的范围由两个宏决定: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L18) + +```c +// Objects/longobject.c +#ifndef NSMALLPOSINTS +#define NSMALLPOSINTS 257 // 正向:0 .. 256 +#endif +#ifndef NSMALLNEGINTS +#define NSMALLNEGINTS 5 // 负向:-1 .. -5 +#endif + +/* 预分配的小整数都存在这个数组里,供共享 */ +static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS]; +``` + +所以默认的小整数范围是 **[-5, 257)**,也就是 -5 到 256。256 落在池内,于是 `a` 和 `b` 拿到的是同一个对象;257 在池外,每次都是新建,自然就不是同一个了。 + +![小整数对象池](int-smallpool.svg) + +需要时如何从池中取?看 `get_small_int` 和配套的宏 `CHECK_SMALL_INT`: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L49) + +```c +// Objects/longobject.c +static PyObject * +get_small_int(sdigit ival) +{ + PyObject *v; + assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS); + v = (PyObject *)&small_ints[ival + NSMALLNEGINTS]; // 按值定位到池中元素 + Py_INCREF(v); // 复用,引用计数+1 + return v; +} + +#define CHECK_SMALL_INT(ival) \ + do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \ + return get_small_int((sdigit)ival); \ + } while(0) +``` + +`CHECK_SMALL_INT` 会先判断目标值是否落在小整数范围,是的话直接返回池中对象。各种创建整数的函数开头都嵌了它,以最常用的 `PyLong_FromLong` 为例: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L243) + +```c +// Objects/longobject.c +PyObject * +PyLong_FromLong(long ival) +{ + ...... + CHECK_SMALL_INT(ival); // 命中小整数池就直接返回,不再走下面的新建逻辑 + ...... +} +``` + +这批小整数则是在解释器初始化时由 `_PyLong_Init` 一次性填好的: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5462) + +```c +// Objects/longobject.c +int +_PyLong_Init(void) +{ + int ival, size; + PyLongObject *v = small_ints; + + for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++, v++) { + size = (ival < 0) ? -1 : ((ival == 0) ? 0 : 1); // 负/零/正 + ...... + (void)PyObject_INIT(v, &PyLong_Type); + Py_SIZE(v) = size; + v->ob_digit[0] = (digit)abs(ival); // 填入数值 + } + ...... +} +``` + +> 这里特意用 `int("257")` 在运行时构造整数,是为了绕开**编译期常量折叠**。如果直接写 `a = 257; b = 257`,编译器可能把同一个字面量 `257` 折叠成同一个对象,让 `a is b` 也变成 `True`——但那是常量缓存,和小整数池是两码事。用 `int(...)` 从字符串构造,才能稳定地观察到小整数池本身的效果。 + +## 整数的创建 + +从 `PyLong_Type` 的 `tp_new` 可知,创建整数的入口是 `long_new`。它(由 Argument Clinic 生成的样板代码)只是解析参数,真正的逻辑在 `long_new_impl`: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L4795) + +```c +// Objects/longobject.c +static PyObject * +long_new_impl(PyTypeObject *type, PyObject *x, PyObject *obase) +{ + ...... + if (x == NULL) { // int() —— 无参数 + ...... + return PyLong_FromLong(0L); // 返回 0 + } + if (obase == NULL) // int(x) —— 未指定进制 + return PyNumber_Long(x); // 按 x 的类型转换 + + // int(x, base) —— 指定了进制,x 必须是字符串/字节串 + base = PyNumber_AsSsize_t(obase, NULL); + ...... + if (PyUnicode_Check(x)) // int("ff", 16) 这类 + return PyLong_FromUnicodeObject(x, (int)base); + else if (PyByteArray_Check(x) || PyBytes_Check(x)) + return _PyLong_FromBytes(......); + ...... +} +``` + +对应到 Python 里就是 `int()`、`int(x)`、`int("10", 8)` 这几种用法:无参返回 0;只给一个对象就按其类型转换;再给一个进制,就把字符串/字节串按该进制解析。 + +![整数创建的分派](int-create.svg) + +## 整数的数值操作 + +整数「能做哪些运算」,记录在它类型对象的 `tp_as_number` 所指向的 `long_as_number` 里——这张表的每个槽位对应一种运算,填的是具体的实现函数: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5342) + +```c +// Objects/longobject.c +static PyNumberMethods long_as_number = { + (binaryfunc)long_add, /* nb_add 加法 + */ + (binaryfunc)long_sub, /* nb_subtract 减法 - */ + (binaryfunc)long_mul, /* nb_multiply 乘法 * */ + long_mod, /* nb_remainder 取余 % */ + long_divmod, /* nb_divmod divmod() */ + long_pow, /* nb_power 乘方 ** */ + (unaryfunc)long_neg, /* nb_negative 取负 */ + ...... +}; +``` + +于是 `a + b`(`a` 为整数)最终会走到 `a->ob_type->tp_as_number->nb_add`,也就是 `long_add`。下面挑加法和乘法看看大整数运算是怎么做的。 + +### 整数相加 + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3082) + +```c +// Objects/longobject.c +static PyObject * +long_add(PyLongObject *a, PyLongObject *b) +{ + PyLongObject *z; + CHECK_BINOP(a, b); + + // 快路径:a、b 都只有一位(小整数),直接用机器运算 + if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) { + return PyLong_FromLong(MEDIUM_VALUE(a) + MEDIUM_VALUE(b)); + } + // 慢路径:按符号转化为绝对值的加/减 + if (Py_SIZE(a) < 0) { + if (Py_SIZE(b) < 0) { + z = x_add(a, b); // (-a) + (-b) = -(a+b) + if (z != NULL) Py_SIZE(z) = -(Py_SIZE(z)); + } + else + z = x_sub(b, a); // (-a) + b = b - a + } + else { + if (Py_SIZE(b) < 0) + z = x_sub(a, b); // a + (-b) = a - b + else + z = x_add(a, b); // a + b + } + return (PyObject *)z; +} +``` + +`long_add` 先用一个**快路径**处理「两个都是单位段小整数」的常见情况,直接交给机器做加法。否则就根据 `a`、`b` 的正负号,把运算归约为对**绝对值**的相加(`x_add`)或相减(`x_sub`)——本质上就是小学竖式运算,只不过「逢十进一」换成了「逢 2³⁰ 进一」。 + +先看绝对值相加 `x_add`: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L2992) + +```c +// Objects/longobject.c +/* Add the absolute values of two integers. */ +static PyLongObject * +x_add(PyLongObject *a, PyLongObject *b) +{ + Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b)); + PyLongObject *z; + Py_ssize_t i; + digit carry = 0; // 进位 + + if (size_a < size_b) { /* 确保 a 是较长的那个,交换 a、b */ ... } + + z = _PyLong_New(size_a + 1); // 结果最多比 a 多一位 + for (i = 0; i < size_b; ++i) { // 低位到高位,逐位相加 + carry += a->ob_digit[i] + b->ob_digit[i]; + z->ob_digit[i] = carry & PyLong_MASK; // 保留低 SHIFT 位 + carry >>= PyLong_SHIFT; // 高位作为进位带入下一轮 + } + for (; i < size_a; ++i) { // 处理 a 剩下的高位 + carry += a->ob_digit[i]; + z->ob_digit[i] = carry & PyLong_MASK; + carry >>= PyLong_SHIFT; + } + z->ob_digit[i] = carry; + return long_normalize(z); // 去掉高位多余的 0 +} +``` + +从最低位的 `ob_digit[0]` 开始逐位相加,超出 `PyLong_SHIFT` 位的部分作为 `carry`(进位)带到下一位;算完再用 `long_normalize` 修剪掉高位多余的 0(保证最高位非零)。和竖式加法一模一样: + +![longobject x_add](long-x-add.svg) + +绝对值相减 `x_sub` 同理,只是把进位换成借位: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3026) + +```c +// Objects/longobject.c +/* Subtract the absolute values of two integers. */ +static PyLongObject * +x_sub(PyLongObject *a, PyLongObject *b) +{ + ...... + digit borrow = 0; // 借位 + + if (size_a < size_b) { sign = -1; /* 交换,保证 a >= b */ ... } + else if (size_a == size_b) { + /* 位数相同,从高位找到第一个不同的位,决定大小与符号 */ + ...... + } + + z = _PyLong_New(size_a); + for (i = 0; i < size_b; ++i) { // 逐位相减 + borrow = a->ob_digit[i] - b->ob_digit[i] - borrow; + z->ob_digit[i] = borrow & PyLong_MASK; + borrow >>= PyLong_SHIFT; + borrow &= 1; // 只保留一个符号位作借位 + } + for (; i < size_a; ++i) { ... } // 处理 a 剩下的高位 + if (sign < 0) Py_SIZE(z) = -Py_SIZE(z); // 按之前判定的符号定正负 + return long_normalize(z); +} +``` + +不够减时就向高一位借位(借的是一个 `2³⁰`): + +![longobject x_sub](long-x-sub.svg) + +### 整数相乘 + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3548) + +```c +// Objects/longobject.c +static PyObject * +long_mul(PyLongObject *a, PyLongObject *b) +{ + PyLongObject *z; + CHECK_BINOP(a, b); + + // 快路径:都是单位段,直接用机器乘法 + if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) { + stwodigits v = (stwodigits)(MEDIUM_VALUE(a)) * MEDIUM_VALUE(b); + return PyLong_FromLongLong((long long)v); + } + + z = k_mul(a, b); // 大数乘法 + /* 两数符号不同则结果为负 */ + if (((Py_SIZE(a) ^ Py_SIZE(b)) < 0) && z) { + _PyLong_Negate(&z); + ...... + } + return (PyObject *)z; +} +``` + +同样是「小数走快路径、大数走专门算法」。大数乘法 `k_mul`([L3274](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3274))用的是 **Karatsuba 算法**: + +> Karatsuba 算法专门用于两个大数相乘。它把位数很多的两个大数各拆成高、低两半,原本需要 4 次子乘法的运算,通过巧妙的代数变形只需 **3 次**乘法(外加少量加法和移位),并递归地施加这一手法,从而把复杂度从 O(n²) 降到约 O(n^1.585),在大数下显著提速。 + +算法细节可参考维基百科的 [Karatsuba 算法](https://zh.wikipedia.org/wiki/Karatsuba算法)。取余、乘方等其余运算的实现都能在 `long_as_number` 里按图索骥找到对应函数,这里不再一一展开。 + +--- + +小结一下整数对象的要点: + +- `PyLongObject` 是**变长对象**,把大整数按 `2^SHIFT` 进制(64 位下 SHIFT=30)拆成若干位段存进 `ob_digit` 数组,用 `ob_size` 记录位段数并以其符号表示正负,这正是「任意精度」的来源; +- 常用的小整数 **[-5, 257)** 在启动时预分配成对象池并共享,这解释了 `a is b` 在小整数上为何成立; +- 加减乘等运算本质是「`2³⁰` 进制下的竖式计算」,并对单位段小整数和大数分别走快路径与专门算法(如乘法的 Karatsuba)。 diff --git a/docs/objects/long-object/int-create.svg b/docs/objects/long-object/int-create.svg new file mode 100644 index 0000000..c96c350 --- /dev/null +++ b/docs/objects/long-object/int-create.svg @@ -0,0 +1,41 @@ + +整数创建的三路分派 +long_new_impl 按参数分流:int() 无参时返回 PyLong_FromLong(0) 即整数 0;int(x) 未指定进制时调用 PyNumber_Long(x) 按 x 的类型转换;int(s, base) 指定进制时调用 PyLong_FromString 按该进制解析字符串或字节串。 + + + + + + + + + + +整数创建:long_new_impl 按参数分派 + + + + + + + + +int() 无参 +PyLong_FromLong(0) +整数 0 + + +int(x) 未给进制 +PyNumber_Long(x) +按 x 的类型转换 + + +int(s, base) 给进制 +PyLong_FromString +按进制解析字符串 + diff --git a/docs/objects/long-object/int-smallpool.svg b/docs/objects/long-object/int-smallpool.svg new file mode 100644 index 0000000..b49700b --- /dev/null +++ b/docs/objects/long-object/int-smallpool.svg @@ -0,0 +1,45 @@ + +小整数对象池 +CPython 在启动时预分配并共享 [-5, 257) 范围内的小整数对象。256 在池内,a=256 与 b=256 拿到同一个对象,a is b 为 True;257 在池外,a=257 与 b=257 各自新建对象,a is b 为 False。 + + + + + + + + + + + +小整数对象池 small_ints[] 预分配 [-5, 257),全局共享 + + + +-501256 + + + + +256 在池内 +a = 256 +b = 256 +池中的 256 + +a is b → True(同一对象) + + +257 在池外 +a = 257 +b = 257 +257 新对象① +257 新对象② + +a is b → False(各自新建) + diff --git a/docs/objects/long-object/long-storage.svg b/docs/objects/long-object/long-storage.svg new file mode 100644 index 0000000..5487d9f --- /dev/null +++ b/docs/objects/long-object/long-storage.svg @@ -0,0 +1,45 @@ + +整数对象的存储结构 +整数 9223372043297226753 以 30 位为一组拆分存入 ob_digit 数组:ob_size 为 3,ob_digit 依次为 1、6、8,其值等于 1·(2^30)^0 + 6·(2^30)^1 + 8·(2^30)^2。 + + + + + + + + + +ob_size + +3 + + + + 0 + 1 + 2 + +ob_digit + + + + + 1 + 6 + 8 + + + + + + +1·(230)0 + 6·(230)1 + 8·(230)2 + += 9223372043297226753 (230 由 PyLong_SHIFT 决定) + diff --git a/docs/objects/long-object/long-x-add.svg b/docs/objects/long-object/long-x-add.svg new file mode 100644 index 0000000..3d117ff --- /dev/null +++ b/docs/objects/long-object/long-x-add.svg @@ -0,0 +1,48 @@ + +整数加法 x_add 示意 +x_add 从 ob_digit 低位开始按位相加:a 为 [1,6,8],b 为 [1,6,8],逐位相加得 z 为 [2,12,16],进位单元为 2 的 30 次方。 + + + + + + + + + + 0 + 1 + 2 + + + +a + + +168 + + ++ + + +b + + +168 + + + + + +z + + +21216 + +从低位按位相加,进位单元为 230(carry) + diff --git a/docs/objects/long-object/long-x-sub.svg b/docs/objects/long-object/long-x-sub.svg new file mode 100644 index 0000000..6999702 --- /dev/null +++ b/docs/objects/long-object/long-x-sub.svg @@ -0,0 +1,45 @@ + +整数减法 x_sub 示意 +x_sub 从低位按位相减,不够减则向高位借位(借 2 的 30 次方):a 为 [1,0,3],b 为 [5,8],相减得 z 为 [1073741820, 1073741815, 2]。 + + + + + + + + +012 +a + + +103 + + + + + +01 +b + + +58 + + + + + +012 +z + + +107374182010737418152 + +不够减则向高位借位(借 230,borrow) + diff --git a/docs/objects/object/PyObject.svg b/docs/objects/object/PyObject.svg new file mode 100644 index 0000000..f0109de --- /dev/null +++ b/docs/objects/object/PyObject.svg @@ -0,0 +1,33 @@ + +PyObject 对象头结构 +PyObject 是所有 Python 对象共享的对象头,包含引用计数 ob_refcnt 和指向类型对象的指针 ob_type。 + + + + + + + + + + + + +PyObject + + + + + +ob_refcnt +引用计数 +*ob_type +类型指针 + +所有 Python 对象共享的对象头 + diff --git a/docs/objects/object/PyVarObject.svg b/docs/objects/object/PyVarObject.svg new file mode 100644 index 0000000..a9e6c74 --- /dev/null +++ b/docs/objects/object/PyVarObject.svg @@ -0,0 +1,37 @@ + +PyVarObject 变长对象结构 +PyVarObject 在 PyObject 对象头(ob_base:引用计数 ob_refcnt 与类型指针 ob_type)的基础上,扩展出一个记录元素个数的 ob_size 字段。 + + + + + + + + + + + + +PyVarObject + + + + +PyObject ob_base + +ob_refcnt +*ob_type + + + +ob_size + + +变长对象 = PyObject 对象头 + ob_size(元素个数) + diff --git a/docs/objects/object/index.md b/docs/objects/object/index.md new file mode 100644 index 0000000..e3046c5 --- /dev/null +++ b/docs/objects/object/index.md @@ -0,0 +1,474 @@ +# Python 对象初探 + +写 Python 时我们整天和「对象」打交道:`1` 是对象,`"hello"` 是对象,列表、字典是对象。可你有没有想过——`int`、`list` 这些**类型**本身是不是对象?函数呢?模块呢? + +在 Python 里答案出奇地一致:**一切皆对象**。不论是整数、字符串,还是类型、函数、模块,统统都是对象。这一章我们就从这句口号出发,钻进 CPython 的 C 源码,看看「对象」到底是怎么被实现出来的。 + +> 本书分析的是 [CPython 3.7.0](https://github.com/python/cpython/tree/v3.7.0) 的源码。CPython 是用 C 写成的,所以一个 Python 对象,落到底层其实就是**一块按特定结构体布局的堆内存**。 + +## 先从 Python 的视角看「对象」 + +在深入 C 代码之前,先用纯 Python 建立一点直觉。在解释器里随手敲几行: + +```python +>>> type(1) # 整数的类型是 int + +>>> type(int) # int 这个类型的类型是 type + +>>> type(type) # type 的类型……还是 type + +>>> isinstance(int, object) # int 也是一个对象 +True +``` + +可以看到,**每个对象都「属于某个类型」**,连类型自己也不例外。Python 之父把这套机制设计得非常统一,而这份统一性,正源于所有对象在 C 层面共享同一个「开头」。 + +进一步说,Python 里的每个对象都同时具备三个要素: + +- **身份(identity)**:对象在内存中的唯一标识,用 `id(obj)` 查看(CPython 里就是它的内存地址); +- **类型(type)**:决定这个对象「是什么」、能干什么,用 `type(obj)` 查看; +- **值(value)**:对象具体存了什么。 + +记住这三个要素,因为接下来你会发现,前两个——身份和类型——被「焊死」在了每一个对象的内存开头。 + +## 对象的分类 + +CPython 内建了形形色色的对象。借用《Python 源码剖析》的归纳,可以大致分成五类: + +- **基础对象(Fundamental)**:类型对象(比如 `int`、`str` 背后的 `type`) +- **数值对象(Numeric)**:整数、浮点数、布尔值等 +- **序列对象(Sequence)**:容纳其他对象的序列集合(字符串、列表、元组等) +- **映射对象(Mapping)**:类似 C++ 中 `map` 的关联对象(字典) +- **内部对象(Internal)**:Python 虚拟机运行时内部使用的对象(函数、frame、code 等) + +![object category](object-category.svg) + +这只是帮助建立全局观的「软」分类。下面我们要看的是所有这些对象在 C 层面共享的「硬」基础。 + +## 对象机制的基石:PyObject + +CPython 用 C 结构体来表示对象。所有对象,无论多复杂,开头都嵌着同一个结构体 **`PyObject`**——可以说整个对象机制都是从它扩展出来的。先看它长什么样: + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L106) + +```c +// Include/object.h +typedef struct _object { + _PyObject_HEAD_EXTRA // 仅调试构建启用,正式构建为空(见下) + Py_ssize_t ob_refcnt; // 引用计数 + struct _typeobject *ob_type; // 指向类型对象的指针,决定该对象「是什么类型」 +} PyObject; +``` + +去掉那个一般为空的 `_PyObject_HEAD_EXTRA` 后,`PyObject` 其实只有两个字段,却撑起了整个对象模型: + +- **`ob_refcnt`**——引用计数。记录「有多少处引用着这个对象」,是 CPython 内存管理的核心,决定对象何时被销毁(本章末会展开)。 +- **`ob_type`**——类型指针。指向一个**类型对象**,回答了「我是谁」。`type(obj)` 读的就是这个字段。 + +回想上一节的三要素:对象的**身份**就是这个结构体的地址(`id(obj)`),**类型**就是 `ob_type`。它们对每个对象都成立,因为每个对象的内存都以 `PyObject` 开头。 + +```python +>>> import sys +>>> a = object() +>>> id(a) # 身份:CPython 中即内存地址,对应 PyObject 的地址 +4388610168 +>>> sys.getrefcount(a) # 读取 ob_refcnt(返回值比真实值多 1,见“引用计数”一节) +2 +``` + +![PyObject](PyObject.svg) + +为了方便、安全地访问这两个字段(以及变长对象的长度),CPython 提供了几个常用宏,源码里随处可见: + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L117) + +```c +// Include/object.h +#define Py_REFCNT(ob) (((PyObject*)(ob))->ob_refcnt) // 取引用计数 +#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) // 取类型指针 +#define Py_SIZE(ob) (((PyVarObject*)(ob))->ob_size) // 取元素个数(变长对象) +``` + +### `_PyObject_HEAD_EXTRA` 是做什么的 + +`PyObject` 开头那行 `_PyObject_HEAD_EXTRA` 我们一笔带过,这里补充一下它的来历。它其实是个宏,而且只在**调试构建**下才会展开出内容: + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L69) + +```c +// Include/object.h +#ifdef Py_TRACE_REFS +/* Define pointers to support a doubly-linked list of all live heap objects. */ +#define _PyObject_HEAD_EXTRA \ + struct _object *_ob_next; \ + struct _object *_ob_prev; +#else +#define _PyObject_HEAD_EXTRA // 正式构建:什么都不展开,为空 +#endif +``` + +只有在开启 `Py_TRACE_REFS`(调试构建 `Py_DEBUG` 会自动带上)时,每个对象才会多出 `_ob_next`/`_ob_prev` 两个指针,把**所有存活对象**串成一条双向链表,方便调试时遍历、统计全部对象、排查内存泄漏。而在你日常使用的正式发行版里,这个宏为空——对象头就是干干净净的 `ob_refcnt` + `ob_type` 两个字段。 + +所以平时完全可以把对象头理解为「引用计数 + 类型指针」。(注意:这条调试用的链表和 Python 处理循环引用的垃圾回收不是一回事,后者另有机制,我们留到「内存管理」章节再讲。) + +## 定长对象与变长对象 + +除了按用途分类,对象还能按「大小是否固定」分为两类: + +- **定长对象**:内存大小固定。比如浮点数 `PyFloatObject`,无论值是多少都占同样大小。 +- **变长对象**:元素个数不定,内存大小随之变化。比如列表、字符串,以及——可能出乎意料——**整数**。 + +变长对象的开头不再是 `PyObject`,而是 **`PyVarObject`**。它在 `PyObject` 的基础上,只多加了一个字段 `ob_size`: + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L112) + +```c +// Include/object.h +typedef struct { + PyObject ob_base; // 内嵌一个 PyObject,复用 ob_refcnt 与 ob_type + Py_ssize_t ob_size; /* Number of items in variable part */ // 元素个数 +} PyVarObject; +``` + +注意 `ob_size` 是**元素个数**,不是字节数。比如一个有 3 个元素的列表,`ob_size` 就是 3。 + +![PyVarObject](PyVarObject.svg) + +整数是变长对象这一点,可以直接观测到:数值越大、需要的「位」越多,对象占用的内存就越大。 + +```python +>>> import sys +>>> sys.getsizeof(0) # 较小的整数 +>>> sys.getsizeof(2**30) # 大一些,占用更多 +>>> sys.getsizeof(2**1000) # 更大,占用进一步增加 +``` + +(具体字节数因平台和 Python 版本而异,但「越大越占内存」的趋势是一致的。)这正是因为整数把数值按若干「位段」存进了一段变长数组里,`ob_size` 记录用了多少段——具体实现我们会在[《Python 整数对象》](../long-object/)里展开。 + +## 类型对象 PyTypeObject + +前面反复提到 `ob_type` 指向一个「类型对象」。这个类型对象到底是什么?它就是 **`PyTypeObject`**——你在 Python 里写的 `int`、`str`、`list`,在 C 层面都对应着一个 `PyTypeObject` 实例。 + +类型对象远不止「一个名字」那么简单。它是该类型所有对象的「说明书」,保存着大量**元信息**:创建这种对象要分配多少内存、它支持哪些操作、怎么比较、怎么算哈希……可以粗略分成几类: + +- **类型名** `tp_name`,如 `"int"`,主要用于打印和调试; +- **创建对象时的内存大小** `tp_basicsize` 和 `tp_itemsize`; +- **大量操作的函数指针**,如析构 `tp_dealloc`、哈希 `tp_hash`、字符串表示 `tp_repr` 等; +- **几组标准操作族**,如数值运算 `tp_as_number`、序列操作 `tp_as_sequence`、映射操作 `tp_as_mapping`。 + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L346) + +```c +// Include/object.h +typedef struct _typeobject { + PyObject_VAR_HEAD // 注意:类型对象本身是个「变长对象」 + const char *tp_name; /* For printing, in format "." */ + Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ // 创建对象时分配的内存大小 + + /* Methods to implement standard operations */ // 一组标准操作的函数指针 + destructor tp_dealloc; + printfunc tp_print; + getattrfunc tp_getattr; + setattrfunc tp_setattr; + PyAsyncMethods *tp_as_async; + reprfunc tp_repr; + + /* Method suites for standard classes */ // 三大操作族 + PyNumberMethods *tp_as_number; // 数值型操作 + PySequenceMethods *tp_as_sequence; // 序列型操作 + PyMappingMethods *tp_as_mapping; // 映射型操作 + + /* More standard operations */ + hashfunc tp_hash; // 如何计算 hash + ternaryfunc tp_call; // 像函数一样被调用时怎么做 + reprfunc tp_str; + getattrofunc tp_getattro; + setattrofunc tp_setattro; + + ...... + +} PyTypeObject; +``` + +换句话说,「一个对象能做什么」并不写在对象自己身上,而是写在它的**类型对象**里。运行时只要顺着 `ob_type` 找到类型对象,再查对应的函数指针,就知道该怎么操作了。这个思路会贯穿全书。 + +## 类型的类型:元类 type + +注意上面 `PyTypeObject` 的第一行是宏 `PyObject_VAR_HEAD`,展开看看: + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L98) + +```c +// Include/object.h +#define PyObject_VAR_HEAD PyVarObject ob_base; +``` + +这说明**类型对象本身也是一个变长对象**——它开头嵌着 `PyVarObject`,因而也嵌着 `PyObject`,因而它也有 `ob_refcnt` 和 `ob_type`。结论很有意思:**类型对象自己也是对象**,它也有自己的类型。 + +那「类型的类型」是谁?我们在开头其实已经见过: + +```python +>>> type(int) # 普通类型的类型 + +>>> type(str) + +>>> type(type) # type 的类型,还是 type 自己 + +``` + +答案就是 `type`,它在 C 层对应 **`PyType_Type`**。所有类型对象的 `ob_type` 最终都指向它,因此 `type` 被称为「元类」(metaclass)。 + +`源文件:`[Objects/typeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/typeobject.c#L3540) + +```c +// Objects/typeobject.c +PyTypeObject PyType_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "type", /* tp_name */ + sizeof(PyHeapTypeObject), /* tp_basicsize */ + sizeof(PyMemberDef), /* tp_itemsize */ + + ...... +}; +``` + +注意第一行 `PyVarObject_HEAD_INIT(&PyType_Type, 0)`——`PyType_Type` 把自己的 `ob_type` 指向了**它自己**,这正对应 `type(type) is type`。所有用户自定义的 `class`,其对应的 `PyTypeObject` 也都是由 `PyType_Type` 创建出来的。 + +那么这个初始化宏做了什么?为了方便填充每个对象开头的引用计数和类型指针,CPython 提供了一对宏: + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L85) + +```c +// Include/object.h +#define PyObject_HEAD_INIT(type) \ + { _PyObject_EXTRA_INIT \ + 1, type }, // 初始引用计数为 1,类型指针为 type + +#define PyVarObject_HEAD_INIT(type, size) \ + { PyObject_HEAD_INIT(type) size }, // 在上面的基础上再补一个 ob_size +``` + +可以看到,`PyVarObject_HEAD_INIT(&PyType_Type, 0)` 的效果就是:把 `ob_refcnt` 设为 1、`ob_type` 设为 `&PyType_Type`、`ob_size` 设为 0。 + +有了这个宏,我们就能看清一个普通类型对象(如整数的 `PyLong_Type`)是怎样与 `PyType_Type` 建立联系的: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5379) + +```c +// Objects/longobject.c +PyTypeObject PyLong_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) // ob_type 指向 PyType_Type + "int", /* tp_name */ + offsetof(PyLongObject, ob_digit), /* tp_basicsize */ + sizeof(digit), /* tp_itemsize */ + + ...... +}; +``` + +把这层层关系串起来,就得到了对象在运行时的全貌:一个整数实例 `int(10)`,其 `ob_type` 指向类型对象 `PyLong_Type`;而 `PyLong_Type` 的 `ob_type` 指向元类 `PyType_Type`;`PyType_Type` 的 `ob_type` 则指向它自己。 + +![](object-runtime-relation.svg) + +## 对象的创建 + +CPython 在 C 层创建对象,大体有两类 API。 + +### 范型 API(AOL,Abstract Object Layer) + +这类 API 形如 `PyObject_XXX`,可作用于任意对象。例如用 `PyObject_New` 按某个类型分配一个对象: + +```c +PyObject *obj = PyObject_New(PyObject, &PyLong_Type); +``` + +### 类型相关 API(COL,Concrete Object Layer) + +这类 API 只针对某一种具体类型,每种内建对象都提供了一组。例如创建整数对象: + +```c +PyObject *longObj = PyLong_FromLong(10); +``` + +两者的区别,本质上就是「通用但笼统」与「专用但精确」的取舍,后续各对象章节会大量见到 COL 形式的 API。 + +## 对象的行为 + +回到 `PyTypeObject` 里那三个指针:`tp_as_number`、`tp_as_sequence`、`tp_as_mapping`。它们分别指向一组「操作族」,定义了对象作为「数值 / 序列 / 映射」时分别支持哪些操作。这就是 CPython 版本的「协议」。 + +以数值协议 **`PyNumberMethods`** 为例,它的字段是一串函数指针,每个对应一种数值运算: + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L240) + +```c +// Include/object.h +typedef struct { + binaryfunc nb_add; // 加法 + + binaryfunc nb_subtract; // 减法 - + binaryfunc nb_multiply; // 乘法 * + binaryfunc nb_remainder; // 取余 % + binaryfunc nb_divmod; // divmod() + ternaryfunc nb_power; // 乘方 ** + + ...... + + binaryfunc nb_matrix_multiply; // 矩阵乘 @(位于结构体末尾) + binaryfunc nb_inplace_matrix_multiply; // 原地矩阵乘 @= +} PyNumberMethods; +``` + +那么「整数能做加法」是怎么实现的?看整数类型对象的填表:它的 `tp_as_number` 指向 `long_as_number`,而 `long_as_number` 的 `nb_add` 槽位填的是 `long_add`。 + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5342) + +```c +// Objects/longobject.c +static PyNumberMethods long_as_number = { + (binaryfunc)long_add, /*nb_add*/ + (binaryfunc)long_sub, /*nb_subtract*/ + (binaryfunc)long_mul, /*nb_multiply*/ + + ...... +}; + +PyTypeObject PyLong_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "int", /* tp_name */ + offsetof(PyLongObject, ob_digit), /* tp_basicsize */ + sizeof(digit), /* tp_itemsize */ + long_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_reserved */ + long_to_decimal_string, /* tp_repr */ + &long_as_number, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + + ...... +}; +``` + +于是,当你写下 `a + b` 且 `a` 是整数时,解释器最终会顺着 `a->ob_type->tp_as_number->nb_add` 找到 `long_add` 并调用它。`tp_as_sequence`、`tp_as_mapping` 的套路完全一样,分别对应序列和映射的操作,这里不再展开。 + +![对象的行为:协议族](obj-protocols.svg) + +## 对象的多态 + +把上面的机制再抽象一层,就能理解 CPython 是怎样实现**多态**的。 + +CPython 创建一个对象(比如整数 `PyLongObject`)后,内部一律用 `PyObject*` 这种**范型指针**来传递它。函数拿到一个 `PyObject*`,并不知道它具体指向什么类型——只能通过 `ob_type` 在运行时动态判断、并取出对应的操作。换句话说,**多态就藏在 `ob_type` 里**。 + +看一个计算哈希的例子: + +```c +Py_hash_t +calc_hash(PyObject *object) +{ + Py_hash_t hash = object->ob_type->tp_hash(object); + return hash; +} +``` + +这个函数完全不关心 `object` 到底是什么类型。它只是顺着 `ob_type` 找到类型对象,调用其中的 `tp_hash`。 + +- 如果传进来的实际是整数对象,`ob_type` 指向 `PyLong_Type`,其 `tp_hash` 绑定的是 `long_hash`: + +`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5379) + +```c +// Objects/longobject.c +PyTypeObject PyLong_Type = { + ...... + (hashfunc)long_hash, /* tp_hash */ + ...... +}; +``` + +- 如果传进来的是字符串对象,`ob_type` 指向 `PyUnicode_Type`,其 `tp_hash` 绑定的则是 `unicode_hash`: + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L15066) + +```c +// Objects/unicodeobject.c +PyTypeObject PyUnicode_Type = { + PyVarObject_HEAD_INIT(&PyType_Type, 0) + "str", /* tp_name */ + ...... + (hashfunc) unicode_hash, /* tp_hash */ + ...... +}; +``` + +同一行 `object->ob_type->tp_hash(object)`,对整数和字符串却走进了不同的实现——这就是 CPython 用 C 语言「手工」实现的多态。 + +![对象的多态](obj-polymorphism.svg) + +## 引用计数 + +最后回到对象头里的另一个字段 `ob_refcnt`。CPython 用**引用计数**来决定一个对象在内存中的生死:每个对象都记录着「当前有多少处引用着我」,当这个数归零,对象就可以被回收。 + +操作引用计数主要靠两个宏:`Py_INCREF`(加一)和 `Py_DECREF`(减一)。 + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L777) + +```c +// Include/object.h +// 引用计数 +1 +#define Py_INCREF(op) ( \ + _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \ + ((PyObject *)(op))->ob_refcnt++) + +// 引用计数 -1;减到 0 则调用 _Py_Dealloc 触发回收 +#define Py_DECREF(op) \ + do { \ + PyObject *_py_decref_tmp = (PyObject *)(op); \ + if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \ + --(_py_decref_tmp)->ob_refcnt != 0) \ + _Py_CHECK_REFCNT(_py_decref_tmp) \ + else \ + _Py_Dealloc(_py_decref_tmp); \ + } while (0) +``` + +`Py_DECREF` 把 `ob_refcnt` 减 1 后,一旦发现归零,就调用 `_Py_Dealloc`。而 `_Py_Dealloc` 会顺着对象的类型去调用它的析构槽 `tp_dealloc`(又是「顺着 `ob_type` 找操作」的套路): + +`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L787) + +```c +// Include/object.h +#define _Py_Dealloc(op) ( \ + _Py_INC_TPFREES(op) _Py_COUNT_ALLOCS_COMMA \ + (*Py_TYPE(op)->tp_dealloc)((PyObject *)(op))) // 调用该类型的 tp_dealloc +``` + +你可以在 Python 层观测引用计数: + +```python +>>> import sys +>>> a = object() +>>> sys.getrefcount(a) # 返回值会比真实计数多 1 +2 +>>> b = a # 多了一处引用 +>>> sys.getrefcount(a) +3 +``` + +`sys.getrefcount` 的返回值总比「真实」引用数多 1,因为把对象作为参数传进函数这一动作本身,就临时多产生了一次引用。 + +![引用计数与销毁](obj-refcount.svg) + +需要强调的是:引用计数归零,**不一定**马上 `free` 掉内存。频繁向操作系统申请、释放内存会显著拖慢 Python,因此 CPython 大量使用**内存对象池**技术——对象「销毁」时,其占用的空间往往被归还给对象池而非真正释放,下次创建同类对象时可以直接复用。小整数、短字符串等都受益于这类优化,具体策略我们会在各对象章节里逐一剖析。 + +--- + +至此,我们已经摸清了 Python 对象机制的骨架: + +- 每个对象的内存都以 `PyObject` 开头,携带**引用计数** `ob_refcnt` 和**类型指针** `ob_type`; +- 变长对象在此基础上多一个 `ob_size`; +- 对象「能做什么」记录在它的**类型对象** `PyTypeObject` 里,运行时顺着 `ob_type` 查表调用,从而实现多态; +- 类型对象自己也是对象,其类型是元类 `type`(`PyType_Type`); +- 对象的生死由引用计数驱动,并辅以内存池优化。 + +这套「对象头 + 类型对象 + 函数指针表」的设计,是后续所有具体对象(整数、字符串、列表、字典、集合……)的共同底座。理解了它,再去看各类对象的实现,就会清晰许多。 diff --git a/docs/objects/object/obj-polymorphism.svg b/docs/objects/object/obj-polymorphism.svg new file mode 100644 index 0000000..abda7d0 --- /dev/null +++ b/docs/objects/object/obj-polymorphism.svg @@ -0,0 +1,45 @@ + +对象的多态:按 ob_type 分派 +同一行调用 object->ob_type->tp_hash(object):若传入整数对象,ob_type 指向 PyLong_Type,其 tp_hash 绑定 long_hash;若传入字符串对象,ob_type 指向 PyUnicode_Type,其 tp_hash 绑定 unicode_hash。同一调用因 ob_type 不同走到不同实现,这就是多态。 + + + + + + + + + + + + + + +object->ob_type->tp_hash(object) + + + +整数对象 int 10 +ob_type ↓ +PyLong_Type +tp_hash ↓ +long_hash() + + + + +字符串对象 str +ob_type ↓ +PyUnicode_Type +tp_hash ↓ +unicode_hash() + + + +同一行调用,按 ob_type 走到不同实现——这就是多态 + diff --git a/docs/objects/object/obj-protocols.svg b/docs/objects/object/obj-protocols.svg new file mode 100644 index 0000000..864a750 --- /dev/null +++ b/docs/objects/object/obj-protocols.svg @@ -0,0 +1,39 @@ + +对象的行为:类型对象里的协议族 +类型对象 PyTypeObject 有三组操作族指针:tp_as_number、tp_as_sequence、tp_as_mapping。整数的 tp_as_number 指向 long_as_number(PyNumberMethods),其中 nb_add 槽位填的是 long_add。所以 a + b 在 a 为整数时,会顺着 tp_as_number -> nb_add 找到 long_add 执行。序列、映射操作同理。 + + + + + + + + + +a + b(a 为整数)如何找到加法实现 + + + + +PyTypeObject(PyLong_Type) + + +tp_as_number ●tp_as_sequence ●tp_as_mapping ● + + + + + + + +PyNumberMethods(long_as_number) + + +nb_add  → long_addnb_subtract → long_subnb_multiply → long_mul ⋯ + +a + b 顺着 tp_as_number → nb_add 找到 long_add;序列、映射操作族同理 + diff --git a/docs/objects/object/obj-refcount.svg b/docs/objects/object/obj-refcount.svg new file mode 100644 index 0000000..f6afec4 --- /dev/null +++ b/docs/objects/object/obj-refcount.svg @@ -0,0 +1,47 @@ + +引用计数与对象的销毁 +Py_INCREF 让对象的 ob_refcnt 加一,Py_DECREF 让它减一。每次 Py_DECREF 后若 ob_refcnt 仍大于 0,对象继续存活;一旦归零,就调用 _Py_Dealloc,进而调用该类型的 tp_dealloc 释放内存(或把空间归还给对象池)。 + + + + + + + + + + + +Py_INCREF:新增一处引用 → ob_refcnt + 1 + + + +对象 +ob_refcnt + + + +Py_DECREF +−1 + + + +ob_refcnt == 0 ? + + + + + +_Py_Dealloc → tp_dealloc → 释放/回池 + + + + + +ob_refcnt > 0 → 对象继续存活 + diff --git a/docs/objects/object/object-category.svg b/docs/objects/object/object-category.svg new file mode 100644 index 0000000..b10a993 --- /dev/null +++ b/docs/objects/object/object-category.svg @@ -0,0 +1,87 @@ + +Python 对象的分类 +Python 对象大致分为五类:Fundamental(类型对象,如 type)、Numeric(数值对象,如 int、float、bool)、Sequence(序列对象,如 str、list、tuple、set)、Mapping(映射对象,如 dict)、Internal(虚拟机内部对象,如 function、code、frame、module、method)。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +对象 +object + + + +基础对象 +Fundamental + +数值对象 +Numeric + +序列对象 +Sequence + +映射对象 +Mapping + +内部对象 +Internal + + + + + type + + int + float + bool + + str + list + tuple + set + + dict + + function + code / frame + module / method + + diff --git a/docs/objects/object/object-runtime-relation.svg b/docs/objects/object/object-runtime-relation.svg new file mode 100644 index 0000000..793c14d --- /dev/null +++ b/docs/objects/object/object-runtime-relation.svg @@ -0,0 +1,68 @@ + +对象运行时的类型关系 +实例 int(10) 的 ob_type 指向类型对象 PyLong_Type;PyLong_Type 的 tp_base 指向基类型 PyBaseObject_Type,其 ob_type 指向元类型 PyType_Type;PyLong_Type 的 ob_type 也指向 PyType_Type;而 PyType_Type 的 ob_type 指向自身。 + + + + + + + + + + + + + + + + + + + + + + + + +int (10) + + +ob_refcnt*ob_typeob_sizeob_digit + + + + +PyLong_Type + + + +ob_refcnt*ob_typetp_base +… …… … + + + + +PyBaseObject_Type + + +ob_refcnt*ob_type +… … + + + + +PyType_Type + + +ob_refcnt*ob_type +… … + + +运行时类型关系:实例 int(10) → 类型对象 PyLong_Type → 元类型 PyType_Type(其类型指向自身) + diff --git a/docs/objects/set-object/index.md b/docs/objects/set-object/index.md new file mode 100644 index 0000000..a28f812 --- /dev/null +++ b/docs/objects/set-object/index.md @@ -0,0 +1,269 @@ +# Python 集合对象 + +`set` 是无序、不重复的集合。我们用它去重、做成员判断,以及交集、并集、差集这类数学运算: + +```python +>>> s = {1, 2, 2, 3} # 自动去重 +>>> s +{1, 2, 3} +>>> 2 in s # 成员判断,平均 O(1) +True +>>> {1, 2, 3} & {2, 3, 4} # 交集 +{2, 3} +``` + +「去重」和「O(1) 成员判断」这两件事,都指向同一个底层结构——**哈希表**。`set` 和 `dict` 是近亲:`dict` 存「键 → 值」,`set` 则只存「键」、没有值。理解了上一章的字典,这一章会轻松很多;我们重点看它和 `dict` 不一样的地方。 + +## 数据结构 + +`set` 里的每个槽位是一个 `setentry`,只存键和它的哈希: + +`源文件:`[Include/setobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/setobject.h#L26) + +```c +// Include/setobject.h +typedef struct { + PyObject *key; + Py_hash_t hash; /* Cached hash code of the key */ +} setentry; +``` + +集合对象本体是 `PySetObject`: + +`源文件:`[Include/setobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/setobject.h#L42) + +```c +// Include/setobject.h +typedef struct { + PyObject_HEAD + Py_ssize_t fill; // 活跃 + 已删除(dummy) 的槽位总数 + Py_ssize_t used; // 活跃槽位数,即 len(s) + Py_ssize_t mask; // 哈希表槽位数 - 1(槽位数是 2 的幂) + setentry *table; // 指向存放数据的数组 + Py_hash_t hash; // 仅 frozenset 使用 + Py_ssize_t finger; // pop() 的搜索游标 + setentry smalltable[PySet_MINSIZE]; // 内置的小数组,默认 8 个槽位 + PyObject *weakreflist; +} PySetObject; +``` + +有几个字段值得留意,它们正是 `set` 的设计特点: + +- **`smalltable` 与 `table`**:小集合直接用内置的 `smalltable`(8 个槽位),`table` 指针指向它;元素变多、需要更大的表时,才另行 `malloc` 一块内存并让 `table` 改指过去。这样小集合连一次额外内存申请都省了。`table` 永远非空,省去了大量判空。 +- **`mask`**:存的是「槽位数 - 1」而非槽位数。因为槽位数是 2 的幂,`hash & mask` 就等于「对槽位数取模」,用得最频繁,所以直接缓存掩码。 +- **`fill` 与 `used`**:`used` 是当前真正的元素个数(`len(s)`);`fill` 还额外算上**已删除但尚未清理的墓碑(dummy)**。为什么删除要留墓碑?下面讲删除时再说。 + +一个 set 的内存布局如下: + +![内存图片](set.svg) + +## 集合的创建 + +从字节码看,`{1, 2}` 这样的字面量由 `BUILD_SET` 指令创建,它调用 `PySet_New`,最终走到 `make_new_set` 完成初始化: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2318) · [Objects/setobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/setobject.c#L1052) + +```c +// Objects/setobject.c +static PyObject * +make_new_set(PyTypeObject *type, PyObject *iterable) +{ + PySetObject *so = (PySetObject *)type->tp_alloc(type, 0); + ...... + so->fill = 0; + so->used = 0; + so->mask = PySet_MINSIZE - 1; // PySet_MINSIZE = 8,故 mask = 7 + so->table = so->smalltable; // table 先指向内置的小数组 + so->hash = -1; + ...... + if (iterable != NULL) { // {1, 2} 的 1、2 由此逐个加入 + if (set_update_internal(so, iterable)) { ... } + } + return (PyObject *)so; +} +``` + +初始的 `mask = 7`(8 个槽位),`table` 指向内置的 `smalltable`。创建本身只是把这些字段摆好,真正的内容由后续的添加操作填入。 + +## 集合的插入 + +`s.add(x)` 走 `PySet_Add` → `set_add_key`(算出 key 的哈希)→ `set_add_entry`(真正插入)。核心在 `set_add_entry`: + +`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/setobject.c#L137) + +```c +// Objects/setobject.c +static int +set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash) +{ + ...... + restart: + mask = so->mask; + i = (size_t)hash & mask; // 初始槽位 + entry = &so->table[i]; + if (entry->key == NULL) // 槽位空 → 直接占用 + goto found_unused; + + perturb = hash; + while (1) { + if (entry->hash == hash) { // 哈希相同,进一步比较 key 是否真的相等 + ... // key 相等 → found_active(已存在,什么都不做) + } + else if (entry->hash == -1) + freeslot = entry; // 记下一个可复用的墓碑位置 + + // 先在邻近的 LINEAR_PROBES 个槽位里线性探测(对 CPU 缓存友好) + if (i + LINEAR_PROBES <= mask) { + for (j = 0 ; j < LINEAR_PROBES ; j++) { + entry++; + if (entry->hash == 0 && entry->key == NULL) + goto found_unused_or_dummy; + if (entry->hash == hash) { ... } // 同样比较 key + } + } + // 邻近都没空位,用扰动公式跳到下一处继续找 + perturb >>= PERTURB_SHIFT; + i = (i * 5 + 1 + perturb) & mask; + entry = &so->table[i]; + if (entry->key == NULL) + goto found_unused_or_dummy; + } + + found_unused: + so->fill++; + so->used++; + entry->key = key; + entry->hash = hash; + if ((size_t)so->fill*5 < mask*3) // 负载未达 3/5,结束 + return 0; + // 负载达到 3/5,扩容:元素数 > 50000 扩为 2 倍,否则 4 倍 + return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4); + ...... +} +``` + +插入逻辑同样是**开放寻址**,但探测策略和 `dict` 有个明显区别——它先做一段**线性探测**: + +1. 用 `hash & mask` 取初始槽位,空就直接放; +2. 若发生冲突,先在**紧邻其后的 `LINEAR_PROBES`(= 9)个槽位**里顺序找空位。连续内存上的线性扫描对 CPU 缓存非常友好,多数冲突在这一步就解决了; +3. 这一小段仍没找到,才用扰动公式 `i = (i*5 + 1 + perturb) & mask` 跳到下一处,重复上面的过程,直到找到空槽或墓碑。 + +来看一个具体过程。设 `s` 为空,依次加入 1、2、9(槽位数 8,`mask = 7`): + +`s.add(1)`:`1 & 7 = 1`,槽位 1 为空,直接放入。 + +![插入1](set-insert-one.svg) + +`s.add(2)`:`2 & 7 = 2`,槽位 2 为空,直接放入。 + +![插入2](set-insert-two.svg) + +`s.add(9)`:`9 & 7 = 1`,但槽位 1 已被 1 占用——发生**哈希冲突**。这里 `i + LINEAR_PROBES = 1 + 9 > mask`,跳过线性探测,直接走扰动公式:`perturb = 9 >> 5 = 0`,`i = (1×5 + 1 + 0) & 7 = 6`,于是 9 落到槽位 6。 + +![插入9](set-insert-nine.svg) + +每成功插入一个新元素,就检查负载:当 `fill × 5 >= mask × 3`(约占满 **3/5**)时触发扩容,按当前元素数 `used` 是否超过 50000 决定扩为 **2 倍还是 4 倍**——小集合 4 倍激进扩容以减少后续冲突,大集合 2 倍稳健扩容以控制内存。 + +## 集合的删除 + +`s.remove(x)` 走 `set_remove` → `set_discard_key` → `set_discard_entry`: + +`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/setobject.c#L401) + +```c +// Objects/setobject.c +static int +set_discard_entry(PySetObject *so, PyObject *key, Py_hash_t hash) +{ + setentry *entry = set_lookkey(so, key, hash); // 查找(逻辑与插入探测一致) + ...... + if (entry->key == NULL) + return DISCARD_NOTFOUND; // 没找到 + old_key = entry->key; + entry->key = dummy; // 关键:标记为墓碑,而非清空 + entry->hash = -1; + so->used--; // used 减 1,fill 不变 + Py_DECREF(old_key); + return DISCARD_FOUND; +} +``` + +注意删除并**不是把槽位清空**,而是把它标记成一个特殊的**墓碑 `dummy`**(同时 `used--`,但 `fill` 不变)。 + +为什么要留墓碑?这是开放寻址哈希表的关键细节:查找一个 key 时,要沿着探测序列一路找,**遇到真正的空槽才能断定「不存在」**。如果删除时直接把槽位清空,就会在探测链中间凿出一个「空洞」,导致原本排在它后面、因冲突才落到更远处的 key 再也找不到。用墓碑占位,探测时把它当作「此处曾有元素,继续往后找」,就保住了探测链的完整。这也是 `fill`(含墓碑)和 `used`(不含墓碑)要分开记的原因。 + +![删除示意](set-remove.svg) + +## 集合的扩容 + +墓碑会越积越多、拖慢查找,那它们什么时候清理?答案是**扩容时一并清掉**。扩容由 `set_table_resize` 完成: + +`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/setobject.c#L303) + +```c +// Objects/setobject.c +static int +set_table_resize(PySetObject *so, Py_ssize_t minused) +{ + ...... + size_t newsize = PySet_MINSIZE; + while (newsize <= (size_t)minused) { + newsize <<= 1; // 找到大于 minused 的最小 2 的幂 + } + ...... + newtable = PyMem_NEW(setentry, newsize); // 申请新表 + memset(newtable, 0, sizeof(setentry) * newsize); + so->mask = newsize - 1; + so->table = newtable; + + so->fill = so->used; // 墓碑被丢弃,fill 重新等于 used + for (entry = oldtable; entry <= oldtable + oldmask; entry++) { + if (entry->key != NULL && entry->key != dummy) { // 只搬运活跃元素 + set_insert_clean(newtable, newmask, entry->key, entry->hash); + } + } + ...... +} +``` + +扩容时申请一张更大的新表,然后**只把活跃的元素重新插入**,墓碑则直接丢弃——于是 `fill` 重新等于 `used`,表也「焕然一新」。重新插入用的是 `set_insert_clean`:因为新表里不可能有重复 key,它省去了所有比较,只需找空槽,所以很快: + +`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/setobject.c#L268) + +```c +// Objects/setobject.c +static void +set_insert_clean(setentry *table, size_t mask, PyObject *key, Py_hash_t hash) +{ + size_t perturb = hash; + size_t i = (size_t)hash & mask; + ...... + while (1) { + entry = &table[i]; + if (entry->key == NULL) goto found_null; // 只找空槽,无需比较 + if (i + LINEAR_PROBES <= mask) { // 同样先线性探测 + for (j = 0; j < LINEAR_PROBES; j++) { + entry++; + if (entry->key == NULL) goto found_null; + } + } + perturb >>= PERTURB_SHIFT; // 再用扰动公式 + i = (i * 5 + 1 + perturb) & mask; + } + found_null: + entry->key = key; + entry->hash = hash; +} +``` + +![扩容示意](set-resize.svg) + +--- + +小结一下 `set` 的实现要点,以及它与 `dict` 的异同: + +- `set` 同样是**哈希表**,但每个槽位 `setentry` 只有 key 和 hash、**没有 value**; +- 小集合用内置的 `smalltable`(8 槽)省内存,`mask = 槽位数 - 1` 让取模变成位运算; +- 冲突处理用**线性探测(LINEAR_PROBES = 9,缓存友好)+ 扰动探测**的组合,这是它与 `dict` 寻址方式的主要区别; +- 删除采用**墓碑 dummy** 占位以保持探测链完整,因此用 `fill`(含墓碑)和 `used`(不含)分别计数; +- 负载达 **3/5** 即扩容(元素数以 5 万为界,分别扩 2 倍 / 4 倍),扩容时只搬运活跃元素,**顺带清除所有墓碑**。 diff --git a/docs/objects/set-object/set-insert-nine.svg b/docs/objects/set-object/set-insert-nine.svg new file mode 100644 index 0000000..e7627be --- /dev/null +++ b/docs/objects/set-object/set-insert-nine.svg @@ -0,0 +1,65 @@ + +set 插入 9(哈希冲突与开放寻址探测) +执行 s.add(9),hash 为 9,9 & 7 = 1,但槽位 1 已被 1 占用产生冲突;按 i = (i*5 + 1 + perturb) & mask 探测,得索引 6,于是把 9 存入槽位 6。 + + + + + + + + + + + + + +PySetObject + + + + Pyobject_HEADfillusedmask*tablehashfingersmalltable[size]*weakreflist + + + python 基本类型活跃与空元素总数真正活跃的数量hash 数组掩码指向 key 数组默认 8 个元素 + + + + + + + + +插入 9 +hash = 9,9 & 7 = 1 → 槽位 1 冲突 +i = (i*5 + 1 + perturb) & mask = 6 + + +set 集合存入的元素值,默认初始化 8 个元素(setentry) + + 01234567 + + + + + + + + + + *key12*key*key*key9*key + hash12hashhashhash9hash + + + + +冲突后按 +5 探测,落到槽位 6 + diff --git a/docs/objects/set-object/set-insert-one.svg b/docs/objects/set-object/set-insert-one.svg new file mode 100644 index 0000000..dc99ef3 --- /dev/null +++ b/docs/objects/set-object/set-insert-one.svg @@ -0,0 +1,58 @@ + +set 插入 1 +执行 s.add(1),索引 = 1 & 7 = 1,槽位 1 为空,直接将 key 与 hash 都设为 1 存入 setentry 数组。 + + + + + + + + + + + + +PySetObject + + + + Pyobject_HEADfillusedmask*tablehashfingersmalltable[size]*weakreflist + + + python 基本类型活跃与空元素总数真正活跃的数量hash 数组掩码指向 key 数组默认 8 个元素 + + + + + + + + +插入 1 +index = 1 & 7 = 1 + + +set 集合存入的元素值,默认初始化 8 个元素(setentry) + + 01234567 + + + + + + + + + *key1*key*key*key*key*key*key + hash1hashhashhashhashhashhash + + diff --git a/docs/objects/set-object/set-insert-two.svg b/docs/objects/set-object/set-insert-two.svg new file mode 100644 index 0000000..1891391 --- /dev/null +++ b/docs/objects/set-object/set-insert-two.svg @@ -0,0 +1,58 @@ + +set 插入 2 +在已存入 1 的基础上执行 s.add(2),索引 = 2 & 7 = 2,槽位 2 为空,直接将 key 与 hash 都设为 2 存入 setentry 数组。 + + + + + + + + + + + + +PySetObject + + + + Pyobject_HEADfillusedmask*tablehashfingersmalltable[size]*weakreflist + + + python 基本类型活跃与空元素总数真正活跃的数量hash 数组掩码指向 key 数组默认 8 个元素 + + + + + + + + +插入 2 +index = 2 & 7 = 2 + + +set 集合存入的元素值,默认初始化 8 个元素(setentry) + + 01234567 + + + + + + + + + *key12*key*key*key*key*key + hash12hashhashhashhashhash + + diff --git a/docs/objects/set-object/set-remove.svg b/docs/objects/set-object/set-remove.svg new file mode 100644 index 0000000..115cf29 --- /dev/null +++ b/docs/objects/set-object/set-remove.svg @@ -0,0 +1,38 @@ + +集合删除 remove 示意(dummy 墓碑) +承接插入示例,集合为 {1,2,9}(分别在槽位 1、2、6)。执行 remove(2):并不清空槽位 2,而是把它标记为 dummy 墓碑,used 减一、fill 不变。保留墓碑是为了不在开放寻址的探测链中间留下空洞,否则会漏查后面因冲突落到更远处的元素(如槽位 6 的 9)。 + + + + + + +remove(2) + + +调用前 +01234567 + + + +129 +fill=3 used=3 + + +调用后 +01234567 + + + +19 +dummy +fill=3 used=2 + +删除不清空槽位,而是写入 dummy 墓碑:used 减一、fill 不变,以保持探测链不断 + diff --git a/docs/objects/set-object/set-resize.svg b/docs/objects/set-object/set-resize.svg new file mode 100644 index 0000000..042a480 --- /dev/null +++ b/docs/objects/set-object/set-resize.svg @@ -0,0 +1,41 @@ + +集合扩容 set_table_resize 示意 +旧表 size 8 中有活跃元素 1(槽1)、9(槽6)和一个 dummy 墓碑(槽2)。扩容时申请更大的新表 size 16,只把活跃元素按新掩码 mask=15 重新插入——1 落到槽1、9 落到槽9,dummy 墓碑被丢弃,于是 fill 重新等于 used。扩容大小由 used 决定:超过 5 万扩 2 倍,否则扩 4 倍。 + + + + + + + + + +旧表 size 8 mask=7 +01234567 + + + +19 +dummy +fill=3 used=2 + + + +重哈希活跃元素,丢弃墓碑(used>50000 ? ×2 : ×4) + + +新表 size 16 mask=15 +0123456789101112131415 + + + +19 + +只把活跃元素按新掩码重新插入,丢弃所有墓碑,于是 fill 重新等于 used + diff --git a/docs/objects/set-object/set.svg b/docs/objects/set-object/set.svg new file mode 100644 index 0000000..dee00b4 --- /dev/null +++ b/docs/objects/set-object/set.svg @@ -0,0 +1,67 @@ + +PySetObject 内存布局 +PySetObject 含 Pyobject_HEAD、fill、used、mask、*table、hash、finger、smalltable 与 *weakreflist;table 指向存放数据的 setentry 数组,默认初始化为 8 个元素,每个 setentry 含 *key 与 hash 两个字段。 + + + + + + + + + + + + +PySetObject + + + + Pyobject_HEAD + fill + used + mask + *table + hash + finger + smalltable[size] + *weakreflist + + + + + python 基本类型 + 活跃与空元素总数 + 真正活跃的数量 + hash 数组掩码 + 指向 key 数组 + 默认 8 个元素 + + + + + + + + + + + +set 集合存入的元素值,默认初始化 8 个元素(setentry) + + 01234567 + + + + + + *key*key*key*key*key*key*key*key + hashhashhashhashhashhashhashhash + + diff --git a/docs/objects/str-object/index.md b/docs/objects/str-object/index.md new file mode 100644 index 0000000..48646d3 --- /dev/null +++ b/docs/objects/str-object/index.md @@ -0,0 +1,353 @@ +# Python 字符串对象 + +字符串是日常用得最多的类型之一。Python 3 的 `str` 有两个鲜明特点:它是**不可变**的,而且是**完整的 Unicode 序列**——能装下从英文字母到中文、再到 emoji 的任何字符。 + +```python +>>> s = "abc中文😀" +>>> len(s) # 6 个「字符」(码点),而不是字节数 +6 +>>> s[3] # 按下标取第 3 个字符,瞬间返回 +'中' +``` + +这里藏着一个设计难题:Unicode 码点的范围是 U+0000 到 U+10FFFF。如果每个字符都用固定的 4 字节存,纯英文文本会浪费 3/4 的内存;如果用变长的 UTF-8 存,按下标取第 n 个字符又会退化成 O(n)(得从头数)。Python 3 的解法是 PEP 393 提出的**灵活字符串表示**。这一章我们就来看 `PyUnicodeObject` 是怎么实现的。 + +## 灵活的内部表示:PEP 393 + +PEP 393 的核心思想是:**看字符串里最宽的那个字符,整串统一用 1、2 或 4 字节来存每个字符**。 + +- 全是 ASCII / Latin-1 字符(U+0000–U+00FF)→ 每字符 **1 字节**; +- 含有 BMP 字符(最大到 U+FFFF,如多数汉字)→ 每字符 **2 字节**; +- 含有星位字符(U+10000 以上,如 emoji)→ 每字符 **4 字节**。 + +这套规则就写在创建字符串的 `PyUnicode_New` 里——它根据传入的 `maxchar`(串中最大码点)选择存储宽度: + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L1252) + +```c +// Objects/unicodeobject.c —— PyUnicode_New +if (maxchar < 128) { + kind = PyUnicode_1BYTE_KIND; char_size = 1; is_ascii = 1; // 纯 ASCII +} +else if (maxchar < 256) { + kind = PyUnicode_1BYTE_KIND; char_size = 1; // Latin-1 +} +else if (maxchar < 65536) { + kind = PyUnicode_2BYTE_KIND; char_size = 2; // BMP(UCS-2) +} +else { + // maxchar <= 0x10FFFF + kind = PyUnicode_4BYTE_KIND; char_size = 4; // 全 Unicode(UCS-4) +} +``` + +这个 `kind`(1 / 2 / 4)是字符串表示的灵魂。它带来一个直接可观测的现象——同一个字符,放在不同的串里可能占不同的字节数: + +![PEP 393 三种字符宽度](str-kinds.svg) + +我们可以用「每多一个字符增加多少字节」来反推 `char_size`: + +```python +>>> import sys +>>> def per_char(s): +... return sys.getsizeof(s * 2) - sys.getsizeof(s) +... +>>> per_char("a") # 纯 ASCII +1 +>>> per_char("中") # BMP 汉字 +2 +>>> per_char("😀") # 星位 emoji +4 +``` + +注意 `kind` 由**最宽的字符**决定:哪怕只夹了一个 emoji,整串都会升到 4 字节。所以 `"a😀b"` 里本可用 1 字节存的 `a`、`b`,也按 4 字节存。 + +这套「定宽」存储的好处是,**按下标取字符是 O(1)**:第 `i` 个字符就在 `数据起始 + i × char_size` 处,一步算出地址。而且这里的「下标」是按**码点**算的,不是 UTF-8 字节、也不是 UTF-16 码元: + +```python +>>> s = "a😀b" +>>> len(s) # 3 个码点(UTF-16 下 😀 会算成 2,这里不会) +3 +>>> s[1] # 直接定位到第 1 个码点 +'😀' +``` + +## 数据结构 + +字符串对象有三层结构,一层比一层多。最基础的是 `PyASCIIObject`: + +`源文件:`[Include/unicodeobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/unicodeobject.h#L271) + +```c +// Include/unicodeobject.h +typedef struct { + PyObject_HEAD + Py_ssize_t length; // 码点个数,即 len(s) + Py_hash_t hash; // 缓存的哈希值,-1 表示尚未计算 + struct { + unsigned int interned:2; // 驻留状态(见下) + unsigned int kind:3; // 字符宽度:1 / 2 / 4 字节 + unsigned int compact:1; // 结构体与数据是否同在一块内存 + unsigned int ascii:1; // 是否全为 ASCII + unsigned int ready:1; // 布局是否就绪 + } state; + wchar_t *wstr; // 兼容旧 wchar_t API 的表示 +} PyASCIIObject; +``` + +短短几个字段里,`state` 这个**位域**最关键——它用几个比特就记下了字符串的全部「身份信息」:是否驻留、每字符几字节(`kind`)、是否 compact、是否纯 ASCII、布局是否就绪。 + +另外两层是在它基础上的扩展(非 ASCII 串会多出 UTF-8 缓存等字段): + +`源文件:`[Include/unicodeobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/unicodeobject.h#L336) + +```c +// Include/unicodeobject.h +typedef struct { + PyASCIIObject _base; + Py_ssize_t utf8_length; // utf8 表示的字节数 + char *utf8; // 按需缓存的 UTF-8 表示 + Py_ssize_t wstr_length; +} PyCompactUnicodeObject; + +typedef struct { + PyCompactUnicodeObject _base; + union { void *any; Py_UCS1 *latin1; Py_UCS2 *ucs2; Py_UCS4 *ucs4; } data; +} PyUnicodeObject; +``` + +这里还有一个省内存的关键设计:**compact**。对于常见的字符串,CPython 把**对象头和字符数据放在同一块连续内存里**——`PyUnicode_New` 一次就申请 `结构体大小 + (字符数 + 1) × char_size` 的空间,字符数据紧跟在结构体后面(末尾留一个 `\0`): + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L1293) + +```c +// Objects/unicodeobject.c —— PyUnicode_New +obj = (PyObject *) PyObject_MALLOC(struct_size + (size + 1) * char_size); +``` + +这样只需一次内存分配(而不是「结构体一块、数据另一块」),而且数据紧挨着对象头,对 CPU 缓存更友好。以纯 ASCII 字符串 `"abc"` 为例,它的内存布局是这样的: + +![compact ASCII 字符串内存布局](str-struct.svg) + +## 字符串的创建 + +我们在源码里写下的字符串字面量、或在 C 层从一个 C 字符串构造 `str`,最终都殊途同归——**解码**。看从 C 字符串创建字符串的入口: + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L2084) + +```c +// Objects/unicodeobject.c +PyObject * +PyUnicode_FromStringAndSize(const char *u, Py_ssize_t size) +{ + ...... + if (u != NULL) + return PyUnicode_DecodeUTF8Stateful(u, size, NULL, NULL); // 把 UTF-8 字节解码成码点 + else + return (PyObject *)_PyUnicode_New(size); +} +``` + +也就是说,「创建一个字符串」本质上是**把 UTF-8 字节解码成码点序列**,再由 `PyUnicode_New` 按 PEP 393 选好 `kind`、分配 compact 内存存进去。Python 源文件默认就是 UTF-8 编码,里面的字符串字面量也是这样被解码、构造出来的。 + +## 字符串的拼接 + +用 `+` 拼接两个字符串(`PyUnicode_Concat`)总是**新建**一个字符串。但 `s += t` 这种写法,CPython 藏了一个优化。 + +回想字符串是不可变的——可一旦某个字符串**只被唯一引用**(没人会察觉它被改动),原地修改就是安全的。`s += t` 正是利用了这点。先看 `s += t` 在虚拟机里的处理: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L4977) + +```c +// Python/ceval.c —— unicode_concatenate +if (Py_REFCNT(v) == 2) { + /* 通常 += 时,这个字符串有 2 个引用:求值栈上一个、变量里一个。 + 这里先把变量删掉,让引用计数降到 1。 */ + ...... + SETLOCAL(oparg, NULL); // 删掉变量的引用 + ...... +} +``` + +引用计数降到 1 后,`PyUnicode_Append` 就能走**原地追加**: + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L11262) + +```c +// Objects/unicodeobject.c —— PyUnicode_Append +if (unicode_modifiable(left) // 可原地修改吗? + && PyUnicode_CheckExact(right) + && PyUnicode_KIND(right) <= PyUnicode_KIND(left) + && !(PyUnicode_IS_ASCII(left) && !PyUnicode_IS_ASCII(right))) +{ + /* append inplace —— 原地扩展,把 right 拷到 left 末尾 */ + if (unicode_resize(p_left, new_len) != 0) goto error; + _PyUnicode_FastCopyCharacters(*p_left, left_len, right, 0, right_len); +} +else { + ...... // 否则:新建一个字符串 +} +``` + +什么叫「可原地修改」?看 `unicode_modifiable` 的判断——必须**引用计数为 1、尚未算过哈希、未被驻留、是精确的 str 类型**: + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L1827) + +```c +// Objects/unicodeobject.c +static int +unicode_modifiable(PyObject *unicode) +{ + if (Py_REFCNT(unicode) != 1) return 0; // 还有别人引用 + if (_PyUnicode_HASH(unicode) != -1) return 0; // 哈希已被缓存,改了会失效 + if (PyUnicode_CHECK_INTERNED(unicode)) return 0; // 已驻留,是共享的 + if (!PyUnicode_CheckExact(unicode)) return 0; // 子类,行为未知 + return 1; +} +``` + +![字符串 += 的两种结果](str-concat.svg) + +**但请注意:这只是 CPython 的实现细节,并不保证触发**——只要字符串被多处引用、已经算过哈希、或被驻留,就会回退到新建。所以在循环里反复 `s += ...` 仍可能退化成 O(n²)。可靠的做法是把片段收集起来,最后用 `''.join()` 一次拼好:它会**先算出总长度、一次性分配**,再把各片段依次拷入,稳定 O(n)。 + +```python +>>> "-".join(["a", "b", "c"]) +'a-b-c' +``` + +## 字符串的查找 + +`s.find(sub)`、`sub in s`、`s.replace(...)` 这些都依赖同一套子串查找。它们最终调用到 stringlib 的 **`FASTSEARCH`**: + +`源文件:`[Objects/stringlib/fastsearch.h](https://github.com/python/cpython/blob/v3.7.0/Objects/stringlib/fastsearch.h#L5) + +```c +// Objects/stringlib/fastsearch.h +/* fast search/count implementation, based on a mix between boyer- + moore and horspool, with a few more bells and whistles on the top. */ +``` + +它是 **Boyer-Moore / Horspool 的混合算法**:预处理模式串,匹配失败时根据「坏字符」一次性**跳过**一大段、而不是逐位回退;并用一个 **Bloom 过滤器**(位掩码)快速判断某个字符是否出现在模式串里,进一步加速跳跃。单字符查找则直接走 `memchr` 快路径。平均下来,查找远快于「逐位比对」的朴素做法。 + +![跳跃式查找](str-find.svg) + +```python +>>> "hello world".find("world") +6 +>>> "world" in "hello world" +True +>>> "hello world".find("xyz") # 找不到返回 -1 +-1 +``` + +## 编码与解码 + +`str` 存的是**码点**(内部按 PEP 393 定宽存储),而 `bytes` 存的是**字节**。两者之间靠 `encode` / `decode` 转换: + +![str 与 bytes 的编码解码](str-encode.svg) + +```python +>>> "café".encode("utf-8") # str → bytes +b'caf\xc3\xa9' +>>> b"caf\xc3\xa9".decode("utf-8") # bytes → str +'café' +>>> len("café"), len("café".encode("utf-8")) # 码点数 vs 字节数 +(4, 5) +``` + +`"café"` 有 4 个码点,编码成 UTF-8 是 5 个字节(`é` 占 2 字节)。前面 `PyUnicode_FromString` 创建字符串走的就是 `decode` 的反向。 + +C 层经常需要一个字符串的 UTF-8 表示(比如把字符串当文件名、传给操作系统)。为避免重复编码,CPython 会把 UTF-8 结果**缓存**到对象的 `utf8` 字段里,下次直接复用: + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L3781) + +```c +// Objects/unicodeobject.c —— PyUnicode_AsUTF8AndSize +if (PyUnicode_UTF8(unicode) == NULL) { // 尚未缓存 + ...... // 编码成 UTF-8 + _PyUnicode_UTF8(unicode) = ...; // 存进 utf8 字段 +} +return PyUnicode_UTF8(unicode); // 返回缓存 +``` + +对于纯 ASCII 字符串,ASCII 本身就是合法的 UTF-8,所以它的 `utf8` 直接和字符数据共享、无需另存——这也是 ASCII 字符串用最省的 `PyASCIIObject` 结构的原因之一。 + +## 字符串的驻留 + +先看一个常见现象: + +```python +>>> a = "foo" +>>> b = "foo" +>>> a is b # 两个 "foo" 竟是同一个对象 +True +``` + +明明是两次写 `"foo"`,为什么 `a` 和 `b` 是同一个对象?这就是**字符串驻留(interning)**:CPython 维护一个全局的 `interned` 字典,把驻留过的字符串去重,等值的字符串只保留**一个**对象。 + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L15174) + +```c +// Objects/unicodeobject.c —— PyUnicode_InternInPlace +if (interned == NULL) { + interned = PyDict_New(); // 全局唯一的驻留字典 + ...... +} +t = PyDict_SetDefault(interned, s, s); // 字典里已有等值串就返回它,否则存入 s +...... +if (t != s) { + Py_INCREF(t); + Py_SETREF(*p, t); // 已存在 → 改用已有对象,丢弃 s + return; +} +_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL; // 标记为已驻留 +``` + +逻辑很直白:拿字符串去 `interned` 字典里查,已有等值的就复用那一个、丢弃新的;没有就存进去并打上驻留标记。 + +![字符串驻留](str-intern.svg) + +驻留的好处是**比较变快**:变量名、属性名、字典的字符串键……这些标识符在程序里反复出现,驻留后它们的相等比较可以直接比指针(同一对象),不必逐字符对比。所以 CPython 会**自动驻留**那些「长得像标识符」的字符串常量(只含字母、数字、下划线),上面的 `"foo"` 正是如此。 + +对于不会自动驻留的字符串(比如带空格、标点的),可以用 `sys.intern()` 手动驻留: + +```python +>>> import sys +>>> sys.intern("hi there!") is sys.intern("hi there!") +True +``` + +> 和小整数池类似,`a = "foo"; b = "foo"` 这种演示也要留意**编译期常量折叠**:同一个代码块里相同的字面量可能本就被折叠成一个对象。要稳定地验证「驻留」本身,用 `sys.intern()` 最可靠。 + +## 不可变与哈希缓存 + +`str` 是**不可变**的——一旦创建,内容就不会变。这不是个限制,而是很多便利的前提:正因为不可变,等值的字符串才能放心地共享同一对象(驻留),字符串才能作为字典的键。 + +不可变还带来一个优化:**哈希只需算一次**。回看结构体里的 `hash` 字段,它初始为 -1;第一次求哈希时算出来并缓存进去,以后直接返回缓存值: + +`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L11572) + +```c +// Objects/unicodeobject.c —— unicode_hash +if (_PyUnicode_HASH(self) != -1) + return _PyUnicode_HASH(self); // 已算过,直接返回缓存 +...... +x = _Py_HashBytes(PyUnicode_DATA(self), + PyUnicode_GET_LENGTH(self) * PyUnicode_KIND(self)); +_PyUnicode_HASH(self) = x; // 算一次,存进 hash 字段 +return x; +``` + +字符串频繁用作字典键、集合元素,哈希被反复用到,这个缓存省下了大量重复计算。 + +--- + +小结一下字符串对象的要点: + +- `str` 是不可变的 Unicode 序列,内部采用 PEP 393 的**灵活表示**:按串中最宽的字符,统一用 **1 / 2 / 4 字节**存每个字符,既省内存又让按下标取字符保持 **O(1)**; +- 结构上以 `PyASCIIObject` 为基础逐层扩展,`state` 位域记录 `kind`、`ascii`、`compact`、`interned` 等身份信息;常见字符串采用 **compact** 布局,对象头与字符数据共用一块内存; +- **创建**字符串本质是把 UTF-8 字节**解码**成码点;**编码/解码**在 `str`(码点)与 `bytes`(字节)间转换,UTF-8 结果会缓存到 `utf8` 字段; +- **拼接**:`+` 总是新建;`s += t` 在唯一引用时可原地扩展(CPython 实现细节,不保证),循环拼接应用 `''.join()` 获得稳定 O(n); +- **查找**用 stringlib 的 `FASTSEARCH`(Boyer-Moore / Horspool 混合 + Bloom 过滤器),靠跳跃式匹配远快于逐位比对; +- **驻留**机制让等值字符串(尤其是标识符)共享同一对象,使比较可走指针相等; +- 不可变性使字符串可被驻留、可作字典键,并让**哈希得以缓存**(`hash` 字段,初始 -1)。 diff --git a/docs/objects/str-object/str-concat.svg b/docs/objects/str-object/str-concat.svg new file mode 100644 index 0000000..f90385d --- /dev/null +++ b/docs/objects/str-object/str-concat.svg @@ -0,0 +1,48 @@ + +字符串 += 的两种结果:原地扩展 vs 新建 +执行 s += t 时,若 s 只被唯一引用(refcount==1,且未算过哈希、未驻留),CPython 会原地把 s 扩展并把 t 拷到末尾,仍是同一个对象;否则会新建一个字符串。这是 CPython 的实现细节,不保证触发;循环里拼接应用 ''.join() 以获得稳定的 O(n)。 + + + + + + + +s += t (s = "abc",t = "de") + + + +① s 仅被唯一引用(refcount==1) + + +abc + +原地扩展 + + + +abc +de +仍是同一对象,无需新分配 + + +② s 被共享 / 已哈希 / 已驻留 + + +abc +不变 + +新建 + + +abcde +新建对象,s 改指向它 + +这是 CPython 的实现细节,不保证触发;循环里拼接字符串请用 ''.join() 以获得稳定的 O(n) + diff --git a/docs/objects/str-object/str-encode.svg b/docs/objects/str-object/str-encode.svg new file mode 100644 index 0000000..45d16cf --- /dev/null +++ b/docs/objects/str-object/str-encode.svg @@ -0,0 +1,46 @@ + +字符串的编码与解码:str ⇄ bytes +str 在内部按码点定宽存储(PEP 393 的 1/2/4 字节);bytes 则是一串字节。encode 把 str 编码成 bytes(如 UTF-8 变长字节),decode 把 bytes 解码回 str。以 "café" 为例,4 个码点编码成 5 个 UTF-8 字节(é 占 2 字节)。内部还会把 UTF-8 结果缓存到 utf8 字段,供 C 层 API 复用。 + + + + + + + + + + + + + +str "café" + + +café +4 个码点 · 内部定宽存储 + + + + +bytes b'caf\xc3\xa9' + + +636166c3a9 +5 字节 UTF-8(é 占 2 字节) + + + +encode("utf-8") + +decode("utf-8") + +str 存「码点」,bytes 存「字节」;二者经编码/解码相互转换 +内部还会把 UTF-8 结果缓存到对象的 utf8 字段,供 C 层 API 复用 + diff --git a/docs/objects/str-object/str-find.svg b/docs/objects/str-object/str-find.svg new file mode 100644 index 0000000..6a49107 --- /dev/null +++ b/docs/objects/str-object/str-find.svg @@ -0,0 +1,38 @@ + +字符串查找:跳跃式匹配(FASTSEARCH) +str.find / in / replace 用 stringlib 的 FASTSEARCH,基于 Boyer-Moore 与 Horspool 的混合算法,并用 Bloom 过滤器快速判断某字符是否在模式串中。匹配失败时,按“坏字符”一次跳过一大段(最多约模式串长度),而不是像朴素做法那样逐位 +1,因此平均远快。 + + + + + + + + +在文本里查找模式串:失配时跳过一大段 + + +文本 + + +abxyzabc + + + + +abc +模式串:此处失配 + + + +失配 → 一次跳过一大段(坏字符 / Bloom) + + + +朴素做法只 +1,逐位回退 + diff --git a/docs/objects/str-object/str-intern.svg b/docs/objects/str-object/str-intern.svg new file mode 100644 index 0000000..9871b82 --- /dev/null +++ b/docs/objects/str-object/str-intern.svg @@ -0,0 +1,48 @@ + +字符串驻留(interning) +Python 维护一个全局 interned 字典,驻留过的等值字符串只保留一个对象。变量 a 和 b 都赋值 "foo",由于 "foo" 被驻留,两者指向同一个 str 对象,于是 a is b 成立。可用 sys.intern() 手动驻留。 + + + + + + + + + + +a = "foo" b = "foo" → a is b 成立 + + + +a + +b + + + + +同一个 str 对象 +"foo" +ob_refcnt 共享 + + + + + + + + +interned(全局字典) +{ "foo" : 唯一对象 } + +登记一次 + +驻留后等值字符串共享同一对象,比较可用指针相等(极快);也可用 sys.intern() 手动驻留 + diff --git a/docs/objects/str-object/str-kinds.svg b/docs/objects/str-object/str-kinds.svg new file mode 100644 index 0000000..439c15f --- /dev/null +++ b/docs/objects/str-object/str-kinds.svg @@ -0,0 +1,45 @@ + +PEP 393 灵活字符串表示:1 / 2 / 4 字节/字符 +Python 按字符串里最宽的字符,统一选择每个字符的存储宽度:全 ASCII/Latin-1 用 1 字节,含 BMP 字符用 2 字节,含星位(U+10000 以上)字符则整串用 4 字节。所以 "a😀b" 里本可用 1 字节的 a、b 也按 4 字节存。定宽存储让按下标取字符是 O(1)。 + + + + + + +PEP 393:按最宽字符,统一选择每字符的存储宽度 + + +"abc" + + +abc +1 字节/字符(Latin-1) +每格 = 1 字节 + + +"中文" + + + + +2 字节/字符(UCS-2) +细线 = 字节,粗线 = 字符 + + +"a😀b" + + + +a😀b +4 字节/字符(UCS-4) +a、b 本可 1 字节 + +一个星位字符 😀 就让整串都升到 4 字节;定宽存储让按下标取字符是 O(1) + diff --git a/docs/objects/str-object/str-struct.svg b/docs/objects/str-object/str-struct.svg new file mode 100644 index 0000000..bd4e3f4 --- /dev/null +++ b/docs/objects/str-object/str-struct.svg @@ -0,0 +1,50 @@ + +compact ASCII 字符串 "abc" 的内存布局 +compact 字符串把对象头和字符数据放在同一块连续内存里:PyASCIIObject 结构(含 ob_refcnt、ob_type、length、hash、state 位域、wstr)之后紧跟着字符数据 a b c 和结尾的 \0。state 位域里 kind=1、compact=1、ascii=1、ready=1,hash 初始为 -1。 + + + + + + + + + + + +一块连续内存(compact) + + + + +PyASCIIObject + + +ob_refcnt*ob_typelengthhashstate(位域)*wstr +3-1 + + +字符数据(紧随结构体之后) + + +abc\0 + + + + +state 位域: + + interned:2 = 0 + kind:3 = 1 (1 字节) + compact:1 = 1 + ascii:1 = 1 ready:1 = 1 + + +对象头与字符数据共用一块内存:少一次内存分配,对 CPU 缓存更友好 + diff --git a/docs/objects/tuple-object/index.md b/docs/objects/tuple-object/index.md new file mode 100644 index 0000000..ec181f4 --- /dev/null +++ b/docs/objects/tuple-object/index.md @@ -0,0 +1,348 @@ +# Python 元组对象 + +元组和列表很像——都是按顺序存放一串元素、都能下标访问。但元组是**不可变**的:一旦创建,就不能再增删或替换里面的元素。 + +```python +>>> point = (3, 4) +>>> point[0] # 下标访问,和列表一样 +3 +>>> def minmax(xs): # 函数返回多个值,本质就是返回一个元组 +... return min(xs), max(xs) +... +>>> minmax([3, 1, 2]) +(1, 3) +>>> {(0, 0): "原点"} # 元组能作字典的键,列表不能 +{(0, 0): '原点'} +``` + +「多返回值」「固定的记录」「作字典键」这些场景都在用元组。它和列表共享「序列」的外表,但「不可变」这一条,让它在内部实现上和列表走了两条不同的路。这一章我们就来看 `PyTupleObject`。 + +## 数据结构 + +`源文件:`[Include/tupleobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/tupleobject.h#L25) + +```c +// Include/tupleobject.h +typedef struct { + PyObject_VAR_HEAD + PyObject *ob_item[1]; // 存放元素指针的数组(实际长度按 ob_size 分配) +} PyTupleObject; +``` + +和列表一样,元组是**变长对象**(`PyObject_VAR_HEAD`,带 `ob_size`),存的也是一排指向元素的 `PyObject *` 指针——所以元组同样能装任意类型。 + +但请注意 `ob_item` 的写法:它不是「一个指向数组的指针」,而是**直接内联在结构体里的数组**(`ob_item[1]` 是柔性数组占位,创建时按元素个数分配足够空间)。对比一下列表——列表的 `ob_item` 是一个 `PyObject **` 指针,指向**另一块**可增长的数组,外加一个 `allocated` 记录容量。这个差别正是「可变 vs 不可变」在内存布局上的体现: + +![元组与列表的内存布局对比](tuple-struct.svg) + +- **元组**:对象头和元素指针数组**同在一块内存**里,长度在创建时定死,没有 `allocated`、不会扩容——因为它本就不需要增删。 +- **列表**:元素指针数组是独立的一块,配合 `allocated` 过分配,才能高效地 `append`。 + +少了一层间接、也没有为增长预留的空位,元组因此比同样内容的列表更**紧凑**: + +```python +>>> import sys +>>> sys.getsizeof((1, 2, 3)) < sys.getsizeof([1, 2, 3]) +True +``` + +## 元组的创建 + +创建元组的入口是 `PyTuple_New`。它和列表一样有**缓冲池**复用对象,而且做得更细——**按长度分桶**: + +`源文件:`[Objects/tupleobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/tupleobject.c#L16) + +```c +// Objects/tupleobject.c +#define PyTuple_MAXSAVESIZE 20 // 只缓存长度 < 20 的元组 +#define PyTuple_MAXFREELIST 2000 // 每种长度最多缓存 2000 个 + +static PyTupleObject *free_list[PyTuple_MAXSAVESIZE]; // 按长度分桶的空闲链表 +static int numfree[PyTuple_MAXSAVESIZE]; +``` + +`PyTuple_New` 在创建时,优先从对应长度的桶里取一个复用,没有才向系统申请: + +`源文件:`[Objects/tupleobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/tupleobject.c#L79) + +```c +// Objects/tupleobject.c +PyObject * +PyTuple_New(Py_ssize_t size) +{ + PyTupleObject *op; + ...... + if (size == 0 && free_list[0]) { + op = free_list[0]; + Py_INCREF(op); + return (PyObject *) op; // 空元组是单例,直接复用 + } + if (size < PyTuple_MAXSAVESIZE && (op = free_list[size]) != NULL) { + free_list[size] = (PyTupleObject *) op->ob_item[0]; // 从该长度的桶里取一个 + numfree[size]--; + _Py_NewReference((PyObject *)op); + } + else { + op = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size); // 桶空,才新申请 + ...... + } + ...... +} +``` + +![元组的 free list](tuple-freelist.svg) + +特别地,**空元组 `()` 是一个全局单例**——无论你写多少个 `()`,拿到的都是同一个对象: + +```python +>>> a = () +>>> b = () +>>> a is b +True +``` + +那我们在代码里写下的元组字面量,是怎么变成 `PyTuple_New` 调用的?看一段「打包」语句的字节码:把几个值聚成一个元组,靠的是 `BUILD_TUPLE` 指令。 + +```python +x = a, b # 括号可省;把 a、b 打包成元组 +``` + +```text +LOAD_NAME a +LOAD_NAME b +BUILD_TUPLE 2 # 弹出栈顶 2 个值,PyTuple_New 建元组并填入 +STORE_NAME x +``` + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2251) + +```c +// Python/ceval.c +TARGET(BUILD_TUPLE) { + PyObject *tup = PyTuple_New(oparg); // 按个数新建元组 + ...... + while (--oparg >= 0) { + PyObject *item = POP(); // 从栈上弹出各元素 + PyTuple_SET_ITEM(tup, oparg, item); + } + PUSH(tup); + ...... +} +``` + +> 一个小细节:如果元组的元素**全是常量**(如 `(1, 2, 3)`),编译器会在编译期就把整个元组折叠成一个常量,用一条 `LOAD_CONST` 直接加载,连 `BUILD_TUPLE` 都省了。只有像 `a, b` 这种含变量的元组,才会在运行时用 `BUILD_TUPLE` 现场打包。 + +## 元组的索引与切片 + +按下标取元素 `t[i]` 走 `tupleitem`,直接定位到内联数组的第 `i` 项,是 **O(1)**: + +`源文件:`[Objects/tupleobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/tupleobject.c#L390) + +```c +// Objects/tupleobject.c +static PyObject * +tupleitem(PyTupleObject *a, Py_ssize_t i) +{ + if (i < 0 || i >= Py_SIZE(a)) { + PyErr_SetString(PyExc_IndexError, "tuple index out of range"); + return NULL; + } + Py_INCREF(a->ob_item[i]); + return a->ob_item[i]; // 直接返回第 i 个指针 +} +``` + +而**切片** `t[i:j]` 走 `tupleslice`——因为元组不可变,切片无法像「视图」那样共享底层,它总是**新建一个元组**,把对应区间的元素指针拷过去: + +`源文件:`[Objects/tupleobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/tupleobject.c#L401) + +```c +// Objects/tupleobject.c +static PyObject * +tupleslice(PyTupleObject *a, Py_ssize_t ilow, Py_ssize_t ihigh) +{ + ...... + np = (PyTupleObject *)PyTuple_New(len); // 新建一个元组 + ...... + for (i = 0; i < len; i++) { + PyObject *v = src[i]; + Py_INCREF(v); + dest[i] = v; // 拷贝区间内的元素指针 + } + return (PyObject *)np; +} +``` + +```python +>>> t = (10, 20, 30, 40) +>>> t[1] # 索引:O(1) +20 +>>> t[1:3] # 切片:返回一个新元组 +(20, 30) +``` + +![索引与切片](tuple-index-slice.svg) + +## 元组的拼接、重复与比较 + +由于元组不可变,**拼接 `+` 和重复 `*` 都只能新建一个元组**——没法在原地改。拼接走 `tupleconcat`,新建一个容纳两段的元组,再把两边的元素指针依次拷入: + +`源文件:`[Objects/tupleobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/tupleobject.c#L443) + +```c +// Objects/tupleobject.c —— tupleconcat +np = (PyTupleObject *) PyTuple_New(size); // 新建,长度 = 两段之和 +...... +// 把 a 的元素、再把 b 的元素,依次拷进 np->ob_item +``` + +```python +>>> (1, 2) + (3,) # 拼接 → 新元组 +(1, 2, 3) +>>> (0,) * 3 # 重复 → 新元组 +(0, 0, 0) +``` + +**比较**走 `tuplerichcompare`,规则是**字典序**:从头逐个元素比,找到第一个不相等的位置就由它定胜负;若一路相等,则比长度。 + +`源文件:`[Objects/tupleobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/tupleobject.c#L631) + +```c +// Objects/tupleobject.c —— tuplerichcompare +for (i = 0; i < vlen && i < wlen; i++) { + int k = PyObject_RichCompareBool(vt->ob_item[i], wt->ob_item[i], Py_EQ); + if (k < 0) return NULL; + if (!k) break; // 找到第一个不相等的位置就停 +} +if (i >= vlen || i >= wlen) // 前缀都相等 → 比长度 + Py_RETURN_RICHCOMPARE(vlen, wlen, op); +return PyObject_RichCompare(vt->ob_item[i], wt->ob_item[i], op); // 由第一个差异决定 +``` + +```python +>>> (1, 2, 3) < (1, 3) # 第 1 位 2 < 3,立即判定 +True +>>> (1, 2) == (1, 2) +True +``` + +![字典序比较](tuple-compare.svg) + +## 元组的打包与解包 + +元组最 Pythonic 的用法是**打包(packing)与解包(unpacking)**。打包前面见过了(`BUILD_TUPLE`);解包则是反过来——把一个元组拆开,依次赋给多个变量: + +```python +a, b, c = t # 把元组 t 拆成 3 个值,分别赋给 a、b、c +``` + +它对应字节码 `UNPACK_SEQUENCE`: + +```text +LOAD_NAME t +UNPACK_SEQUENCE 3 # 把元组拆成 3 个值压栈 +STORE_NAME a +STORE_NAME b +STORE_NAME c +``` + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1957) + +```c +// Python/ceval.c +TARGET(UNPACK_SEQUENCE) { + PyObject *seq = POP(), *item, **items; + if (PyTuple_CheckExact(seq) && PyTuple_GET_SIZE(seq) == oparg) { + items = ((PyTupleObject *)seq)->ob_item; + while (oparg--) { + item = items[oparg]; + Py_INCREF(item); + PUSH(item); // 逆序压栈,从而左到右赋值 + } + } + ...... +} +``` + +![元组的打包与解包](tuple-unpack.svg) + +理解了这一点,几个常见写法就都顺理成章了: + +```python +>>> a, b = 1, 2 +>>> a, b = b, a # 交换:右边先打包成临时元组,再解包给左边 +>>> a, b +(2, 1) +>>> first, *rest = (1, 2, 3, 4) # 带星号的解包(UNPACK_EX) +>>> first, rest +(1, [2, 3, 4]) +``` + +`a, b = b, a` 这个「无需中间变量的交换」,本质就是「先把 `b, a` 打包成一个临时元组,再解包赋给 `a, b`」。 + +## 不可变与可哈希 + +元组的「不可变」体现在:不能替换、增删它的元素。 + +```python +>>> t = (1, 2, 3) +>>> t[0] = 9 +Traceback (most recent call last): + ... +TypeError: 'tuple' object does not support item assignment +``` + +不过要分清一层:元组里存的是**指针**,不可变指的是「这些指针不能改」(不能换成指向别的对象),但**指针指向的对象本身仍可能是可变的**。所以一个「装着列表的元组」,元组结构不能动,列表内容却能改。 + +这就引出元组最实用的特性之一——**可哈希**,从而能作字典键、集合元素。但能不能哈希,取决于它的内容。看元组的哈希函数: + +`源文件:`[Objects/tupleobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/tupleobject.c#L348) + +```c +// Objects/tupleobject.c +static Py_hash_t +tuplehash(PyTupleObject *v) +{ + ...... + while (--len >= 0) { + y = PyObject_Hash(*p++); // 逐个对元素求哈希 + if (y == -1) + return -1; // 任一元素不可哈希 → 整个元组不可哈希 + x = (x ^ y) * mult; // 把各元素的哈希混合起来 + ...... + } + ...... +} +``` + +元组的哈希值是由**所有元素的哈希值混合**而成的。这意味着:只要其中有一个元素不可哈希(比如里面装了个列表),`PyObject_Hash` 返回 -1,整个元组就不可哈希。 + +```python +>>> isinstance(hash((1, 2, 3)), int) # 元素都可哈希 → 算得出哈希值 +True +>>> hash((1, [2])) # 里面有个列表 → 不可哈希 +Traceback (most recent call last): + ... +TypeError: unhashable type: 'list' +``` + +![可哈希性取决于内容](tuple-hash.svg) + +也正因为元组不可变、(内容可哈希时)可哈希,它才能胜任「字典的键」「集合的元素」这些列表干不了的活。 + +## 元组与列表:该用哪个 + +两者都是序列,取舍可以归结为一句话:**元素个数固定、不打算改动,就用元组;需要动态增删改,就用列表。** + +- 倾向**元组**:固定的记录(如坐标 `(x, y)`、数据库行)、函数返回多个值、需要作字典键或集合元素、希望数据「只读」更安全。它更紧凑、有按长度复用的 free list,常量元组还会被缓存共享。 +- 倾向**列表**:内容会增删、需要 `append`/`sort`/原地修改的动态集合。 + +--- + +小结一下元组对象的要点,以及它和列表的分野: + +- `PyTupleObject` 是**变长对象**,但元素指针数组**内联**在对象自身里——整个元组**一块内存、长度固定**,没有 `allocated`、不会扩容;列表则是「结构体 + 独立可增长数组」两块内存,元组因此更**紧凑**; +- 创建时有**按长度分桶的 free list** 复用(长度 < 20、每桶 ≤ 2000),**空元组是全局单例**;含变量的元组靠 `BUILD_TUPLE` 打包,全常量元组则被折叠成常量; +- 不可变使**索引** O(1),而**切片、拼接 `+`、重复 `*`** 都只能新建元组;**比较**是逐元素的字典序; +- **打包/解包**(`BUILD_TUPLE` / `UNPACK_SEQUENCE`)支撑了多返回值、`a, b = b, a` 交换、星号解包等惯用法; +- 元组的哈希由**所有元素的哈希混合**而成,**内容都可哈希时元组才可哈希**,从而能作字典键、集合元素。 diff --git a/docs/objects/tuple-object/tuple-compare.svg b/docs/objects/tuple-object/tuple-compare.svg new file mode 100644 index 0000000..099fb9a --- /dev/null +++ b/docs/objects/tuple-object/tuple-compare.svg @@ -0,0 +1,39 @@ + +元组的字典序比较 +元组比较按字典序:从头逐个元素比,找到第一个不相等的位置就由它定胜负。比较 (1,2,3) 与 (1,3):第 0 位 1 等于 1 继续,第 1 位 2 小于 3,立即判定 (1,2,3) 小于 (1,3),后面的元素不再看。 + + + + + + +(1, 2, 3) < (1, 3) ? 按字典序逐位比较 + +012 + + +(1,2,3) + + +123 + + +(1,3) + + +13 + + += + +2<3 +不再看 + +第 1 位首次不等:2 < 3 → 立即判定 (1,2,3) < (1,3) 为 True + diff --git a/docs/objects/tuple-object/tuple-freelist.svg b/docs/objects/tuple-object/tuple-freelist.svg new file mode 100644 index 0000000..704c035 --- /dev/null +++ b/docs/objects/tuple-object/tuple-freelist.svg @@ -0,0 +1,56 @@ + +元组的 free list:按长度分桶缓存复用 +CPython 为元组维护一组按长度分桶的 free list:长度 0 的空元组是单例,长度 1 到 19 各有一个桶,每个桶最多缓存 2000 个被释放的元组对象。创建 PyTuple_New(n) 时优先从 free_list[n] 取一个复用,元组销毁时放回对应的桶;长度 ≥ 20 的元组不缓存。 + + + + + + +元组的 free list:按长度分桶,创建复用、销毁回收 + + + + free_list[0] + free_list[1] + free_list[2] + free_list[19] + + + + +( ) +空元组 · 单例 + + + + + + + 空闲空闲空闲 + … 每桶最多 2000 个 + + + + + + + 空闲空闲空闲 + + + + + + + 空闲空闲 + + +PyTuple_New(n):先从 free_list[n] 取一个复用;元组销毁:放回 free_list[n] +长度 ≥ 20 的元组不进 free list + diff --git a/docs/objects/tuple-object/tuple-hash.svg b/docs/objects/tuple-object/tuple-hash.svg new file mode 100644 index 0000000..a46afda --- /dev/null +++ b/docs/objects/tuple-object/tuple-hash.svg @@ -0,0 +1,43 @@ + +元组的可哈希性取决于内容 +元组的哈希由各元素的哈希混合而成。(1,2,3) 的元素都可哈希,混合后得到一个整数哈希值。(1,[2]) 里有一个列表,对它求哈希失败(list 不可哈希),于是整个元组都不可哈希。 + + + + + + + + + + + + + +(1, 2, 3) + + +逐个求哈希再混合 +h(1) ⊕ h(2) ⊕ h(3) + + +一个整数哈希 + + + +(1, [2]) + + +对 [2] 求哈希 → 失败 +list 不可哈希 + + +整个元组不可哈希 + +只要有一个元素不可哈希,整个元组就不可哈希(因而不能作字典键/集合元素) + diff --git a/docs/objects/tuple-object/tuple-index-slice.svg b/docs/objects/tuple-object/tuple-index-slice.svg new file mode 100644 index 0000000..cae3a38 --- /dev/null +++ b/docs/objects/tuple-object/tuple-index-slice.svg @@ -0,0 +1,51 @@ + +元组的索引与切片 +对元组 (10,20,30,40):索引 t[1] 直接定位到内联数组第 1 项、返回元素 20,是 O(1)。切片 t[1:3] 则新建一个元组,把区间内的元素指针复制过去——新元组与原元组共享同一批元素对象。 + + + + + + + + + + +t +0123 + + + + + + + 10 + 20 + 30 + 40 + + + + + + +t[1] → 20(O(1) 直达) + + +t[1:3] + + + + + + + +新建元组,复制指针 +指向同一批元素对象 + diff --git a/docs/objects/tuple-object/tuple-struct.svg b/docs/objects/tuple-object/tuple-struct.svg new file mode 100644 index 0000000..50cb48b --- /dev/null +++ b/docs/objects/tuple-object/tuple-struct.svg @@ -0,0 +1,41 @@ + +元组与列表的内存布局对比 +元组 PyTupleObject 把元素指针 ob_item 内联在对象自身里,整个元组就是一块连续内存、长度固定;列表 PyListObject 的 ob_item 是一个指针,指向另一块可增长的数组,再加 allocated 记录容量,所以是两块内存。两者数组里存的都是指向元素的 PyObject* 指针。 + + + + + + + + + +tuple (1,2,3) + + + +对象头ob_size=3→1→2→3 + +一块连续内存:对象头 + 内联指针数组(定长) + + +list [1,2,3] + + +对象头ob_size=3*ob_item ●allocated=4 + + + + + +→1→2→3 + +另一块:可增长的指针数组 + +元组把元素指针放在对象内部(一块、定长);列表指向另一块可增长的数组(两块) + diff --git a/docs/objects/tuple-object/tuple-unpack.svg b/docs/objects/tuple-object/tuple-unpack.svg new file mode 100644 index 0000000..1692054 --- /dev/null +++ b/docs/objects/tuple-object/tuple-unpack.svg @@ -0,0 +1,54 @@ + +元组的打包与解包 +打包:把多个值聚成一个元组,对应字节码 BUILD_TUPLE。解包:把一个元组拆成多个变量,对应字节码 UNPACK_SEQUENCE。交换 a, b = b, a 就是先把右边打包成临时元组、再解包赋给左边,无需中间变量。 + + + + + + + + + + + + 1 + 2 + 3 + + + + + + + + +打包 BUILD_TUPLE + + + +元组 +(1, 2, 3) + + + + + + + +解包 UNPACK_SEQUENCE + + + + a = 1 + b = 2 + c = 3 + + +a, b = b, a 交换变量:右边先打包成临时元组,再解包赋给左边,无需中间变量 + diff --git a/docs/objects/type-object/attr-lookup.svg b/docs/objects/type-object/attr-lookup.svg new file mode 100644 index 0000000..85b015e --- /dev/null +++ b/docs/objects/type-object/attr-lookup.svg @@ -0,0 +1,50 @@ + +obj.attr 的属性查找顺序 +访问 obj.attr 时,_PyObject_GenericGetAttrWithDict 按优先级查找:① 先在类型的 MRO 里找数据描述符(同时有 __get__ 和 __set__),命中则调用其 __get__ 返回;② 否则查实例自己的 __dict__;③ 否则用类 MRO 里找到的非数据描述符(如方法)或类属性;④ 都没有则抛 AttributeError。 + + + + + + + + + + + + +obj.attr 的查找顺序(优先级从上到下) + + + +① 数据描述符(类的 MRO 中) +同时有 __get__ 和 __set__,优先级最高 + + +② 实例自己的 __dict__ +对象上 self.x = ... 设的属性 + + +③ 非数据描述符 / 类属性(类 MRO) +方法(只有 __get__)、类变量等 + + +④ 都没有 → 抛 AttributeError + + + + + +未命中 ↓未命中 ↓未命中 ↓ + + + + + +命中→ 返回命中→返回命中→返回 + diff --git a/docs/objects/type-object/class-creation.svg b/docs/objects/type-object/class-creation.svg new file mode 100644 index 0000000..351e6b6 --- /dev/null +++ b/docs/objects/type-object/class-creation.svg @@ -0,0 +1,37 @@ + +类的创建:class 语句 → type(name, bases, namespace) +class 语句会被编译成对 __build_class__ 的调用:先执行类体得到一个命名空间字典,再调用元类(默认是 type)以 type(name, bases, namespace) 创建类型对象。所以「类」本身就是 type 的实例。 + + + + + + + + + +class 语句背后:调用元类 type(name, bases, namespace) 造类 + + + + + + +class Foo(Base): + x = 1 +(类体 → 命名空间字典) + + +type(name, bases, namespace) +__build_class__ 调用元类(默认 type) +→ type_new 创建类型对象 + + +类型对象 Foo +它本身是 type 的实例(type(Foo) is type) + diff --git a/docs/objects/type-object/descriptor.svg b/docs/objects/type-object/descriptor.svg new file mode 100644 index 0000000..71b79fa --- /dev/null +++ b/docs/objects/type-object/descriptor.svg @@ -0,0 +1,39 @@ + +方法是描述符:访问时绑定 self +类里定义的函数是非数据描述符(只有 __get__)。在实例上访问 p.m 时,触发 func_descr_get,返回一个把函数和实例绑定起来的「绑定方法」(自动带上 self=p);在类上访问 P.m 时则返回函数本身。这就是方法能拿到 self 的原因。 + + + + + + + + + + +方法是描述符:实例上访问时绑定 self + + + +类 P 里的函数 m +非数据描述符(只有 __get__) + + + +p.m(实例) + +func.__get__(p, P) → 绑定方法 +自动带上 self = p(即 PyMethod_New) + + + +P.m(类) + +函数 m 本身 +不绑定(obj 为 None) + diff --git a/docs/objects/type-object/getattr-hook.svg b/docs/objects/type-object/getattr-hook.svg new file mode 100644 index 0000000..0b0a0ad --- /dev/null +++ b/docs/objects/type-object/getattr-hook.svg @@ -0,0 +1,53 @@ + +__getattribute__ 总是先走,__getattr__ 只在找不到时兜底 +访问 obj.x 时,总是先调用 __getattribute__ 走正常的属性查找(数据描述符、实例字典、类属性)。若找到就返回;只有当它抛出 AttributeError 时,才回退去调用 __getattr__(如果类定义了的话)。所以 __getattribute__ 每次都执行,__getattr__ 只是兜底。 + + + + + + + + + + + + + +obj.x:先走 __getattribute__,找不到才兜底 __getattr__ + + + +obj.x + + + + +__getattribute__(总是执行) +数据描述符 → 实例字典 → 类属性 +即正常的属性查找顺序 + + + +找到 → 返回值 + +返回属性 + + + +抛 AttributeError → 回退 + +__getattr__(兜底,可选) +类若没定义它 → 真的 AttributeError + + + + +返回兜底值 + diff --git a/docs/objects/type-object/index.md b/docs/objects/type-object/index.md new file mode 100644 index 0000000..61c7204 --- /dev/null +++ b/docs/objects/type-object/index.md @@ -0,0 +1,308 @@ +# Python 类型对象与自定义类 + +[《Python 对象初探》](../object/)里我们认识了类型对象 `PyTypeObject`,也知道了「类型的类型」是元类 `type`。这一章我们深入到日常最常打交道、却也最多困惑的地方:**`class` 和实例到底是怎么工作的**——实例怎么创建、类怎么创建、`obj.attr` 是怎么找到的、`self` 从哪来、`@property` 凭什么生效、`super()` 怎么找到方法。这些问题的答案,都藏在类型对象的机制里。 + +## 实例的创建:`MyClass()` 背后 + +当你写下 `MyClass()`,看似在「调用类」,其实调用的是类型对象的 `tp_call`(即 `type_call`)——因为类本身也是对象,对它加括号就是调用它。`type_call` 把「造一个实例」拆成两步:先 `tp_new` 分配出一个新对象,再 `tp_init` 初始化它——正对应 Python 里的 `__new__` 和 `__init__`: + +`源文件:`[Objects/typeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/typeobject.c#L911) + +```c +// Objects/typeobject.c —— type_call(精简) +obj = type->tp_new(type, args, kwds); // ① __new__:分配并返回新对象 +...... +/* 若 __new__ 返回的不是本类(或子类)的实例,就不调用 __init__ */ +if (!PyType_IsSubtype(Py_TYPE(obj), type)) + return obj; +type = Py_TYPE(obj); +if (type->tp_init != NULL) { + int res = type->tp_init(obj, args, kwds); // ② __init__:初始化 + ...... +} +return obj; +``` + +![实例创建流程](instance-creation.svg) + +为什么要分成两步?因为这两步的职责完全不同:`__new__` 是**静态方法**,第一个参数是类 `cls`,负责「无中生有造出对象」并把它返回;`__init__` 是普通方法,第一个参数是已经造好的 `self`,负责「往对象里填属性」,不返回任何东西。日常我们几乎只重写 `__init__`,只有少数场景(不可变类型、单例、继承自 `int`/`str`/`tuple` 等)才需要插手 `__new__`。 + +```python +>>> class T: +... def __new__(cls): print("__new__ 分配"); return super().__new__(cls) +... def __init__(self): print("__init__ 初始化") +... +>>> T() +__new__ 分配 +__init__ 初始化 +``` + +源码里那行 `PyType_IsSubtype` 守卫值得留意:**如果 `__new__` 返回的不是本类的实例,`__init__` 就会被跳过**——给「别的类的对象」做本类的初始化没有意义。这能解释一个乍看奇怪的现象: + +```python +>>> class Weird: +... def __new__(cls): return 42 # 故意返回一个 int +... def __init__(self): print("我不会被调用") +... +>>> Weird() # __init__ 被跳过,直接得到 42 +42 +``` + +## 类的创建:`class` 语句背后 + +那「类」本身又是怎么来的?`class` 语句会被编译器翻译成对内建函数 `__build_class__` 的调用:先执行类体(那一段缩进的代码)得到一个命名空间字典,再调用**元类**(默认就是 `type`),以 `type(名字, 基类元组, 命名空间)` 创建出类型对象。 + +`源文件:`[Python/bltinmodule.c](https://github.com/python/cpython/blob/v3.7.0/Python/bltinmodule.c#L128) · [Objects/typeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/typeobject.c#L2346)(`type_new`) + +![类创建流程](class-creation.svg) + +这意味着:**类就是 `type` 的实例**。我们平时「定义类」其实是「用 `type` 造一个对象」,只不过这个对象比较特殊——它又能被用来造别的对象(实例)。理解了这层,你甚至可以绕过 `class` 语句,直接用 `type()` 三参数形式手动造一个类: + +```python +>>> C = type('C', (object,), {'x': 1}) # 等价于 class C: x = 1 +>>> C.x, C().x +(1, 1) +>>> type(C) is type, isinstance(C, type) +(True, True) +``` + +`class` 语句只是这件事的语法糖:它帮你执行类体、收集命名空间、挑选元类、再调用元类。把这条链记住,后面的元类、`__init_subclass__` 这些高级特性就都有了根基。 + +## 属性查找:实例、类与描述符 + +`obj.attr` 看似简单,背后却有一套**严格的优先级**。它由 `_PyObject_GenericGetAttrWithDict`(也就是默认的 `object.__getattribute__`)实现: + +`源文件:`[Objects/object.c](https://github.com/python/cpython/blob/v3.7.0/Objects/object.c#L1161) + +```c +// Objects/object.c —— _PyObject_GenericGetAttrWithDict(精简) +descr = _PyType_Lookup(tp, name); // 在类型的 MRO 里找 name +f = NULL; +if (descr != NULL) { + f = descr->ob_type->tp_descr_get; + if (f != NULL && PyDescr_IsData(descr)) { // ① 数据描述符(有 __get__ 且有 __set__) + return f(descr, obj, (PyObject *)obj->ob_type); // 优先级最高,直接调 __get__ + } +} +if (dict != NULL) { // ② 实例自己的 __dict__ + res = PyDict_GetItem(dict, name); + if (res != NULL) return res; +} +if (f != NULL) { // ③ 非数据描述符(只有 __get__) + return f(descr, obj, (PyObject *)Py_TYPE(obj)); +} +if (descr != NULL) return descr; // ③ 普通类属性 +... // ④ 都没有 → AttributeError +``` + +第一步的 `_PyType_Lookup` 负责「在类型的 MRO 里找这个名字」。它内部带一个**全局方法缓存**(`method_cache`,按类的版本号 `tp_version_tag` + 名字做键),所以同一个方法被反复访问时不必每次都重走一遍 MRO;一旦类被修改,版本号失效,缓存自动作废。这是 CPython 让属性访问保持飞快的关键优化之一。 + +把这个顺序记牢,几乎所有「属性从哪来」的疑惑都能解开: + +![属性查找顺序](attr-lookup.svg) + +**数据描述符(类 MRO) > 实例 `__dict__` > 非数据描述符 / 类属性(类 MRO) > AttributeError**。 + +「数据描述符优先于实例字典」这一条尤其关键——它正是 `@property` 能「拦截」属性访问的原因。哪怕实例字典里有同名的键,也会被 property 压过去: + +```python +>>> class Q: +... @property +... def v(self): return "来自 property" +... +>>> q = Q() +>>> q.__dict__['v'] = "来自实例字典" # 强行往实例字典塞一个 v +>>> q.v # 仍然走 property(数据描述符优先) +'来自 property' +``` + +## `__getattribute__` 与 `__getattr__`:常规与兜底 + +上面那套优先级是 `__getattribute__` 干的活——**每次** `obj.attr` 都会先走它。很多人会把它和 `__getattr__` 搞混,其实两者分工清楚: + +- `__getattribute__`:属性访问的**总入口**,每次都执行,里面就是上节那套「描述符 / 实例字典 / 类属性」的查找。 +- `__getattr__`:**兜底钩子**,只有当 `__getattribute__` 没找到、抛出 `AttributeError` 时才被调用。常规属性访问根本不会触发它。 + +源码里这个「先常规、失败才兜底」的逻辑写得很直白: + +`源文件:`[Objects/typeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/typeobject.c#L6410)(`slot_tp_getattr_hook`) + +```c +// Objects/typeobject.c —— slot_tp_getattr_hook(精简) +res = PyObject_GenericGetAttr(self, name); // 先走常规查找 +if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + res = call_attribute(self, getattr, name); // 失败且是 AttributeError → 才调 __getattr__ +} +return res; +``` + +![__getattribute__ 与 __getattr__ 的关系](getattr-hook.svg) + +```python +>>> class D: +... def __getattr__(self, name): +... return f"兜底:{name}" +... +>>> d = D() +>>> d.x = 1 +>>> d.x # 实例字典里有 → 常规查找就命中,不进 __getattr__ +1 +>>> d.y # 找不到 → 抛 AttributeError → 才触发 __getattr__ +'兜底:y' +``` + +正因为 `__getattr__` 只在「缺失时」触发,它很适合做惰性属性、属性代理、转发;而想拦截**所有**访问(包括已存在的属性)则要重写 `__getattribute__`——但那要小心,一不留神就无限递归。 + +## MRO 与多继承:C3 线性化 + +上面反复出现「在类型的 MRO 里找」。**MRO(Method Resolution Order,方法解析顺序)**就是把一个类的所有祖先排成一条线,属性查找沿这条线依次进行。单继承时这条线一目了然,就是「自己 → 父 → 祖父 → … → object」;多继承时则由 **C3 线性化**算法算出,保证顺序既尊重继承关系(子类排在父类前)、又无歧义: + +`源文件:`[Objects/typeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/typeobject.c#L1750)(`mro_implementation`) + +![MRO 与 C3 线性化](mro.svg) + +```python +>>> class A: pass +>>> class B(A): pass +>>> class Cc(A): pass +>>> class D(B, Cc): pass +>>> [c.__name__ for c in D.__mro__] +['D', 'B', 'Cc', 'A', 'object'] +``` + +注意 `A` 排在 `B`、`Cc` 之后而不是紧跟 `D`——C3 保证「所有子类都出现在它的父类之前」,所以共同祖先 `A` 必须等 `B` 和 `Cc` 都列完才轮到。`super()` 也正是沿着 MRO 找「下一个」类的方法,所以多继承下 `super()` 找到的未必是「直接父类」,而是 MRO 里排在当前类后面的那一个——这也是协作式多继承(cooperative multiple inheritance)能跑通的基础。 + +## 描述符:方法、property 背后的机制 + +前面多次提到「描述符」,现在正式讲清。一个对象只要实现了 `__get__`,就是**描述符**;按是否还实现 `__set__`/`__delete__`,分为两类,区别只在属性查找里的优先级: + +- **数据描述符**:实现了 `__set__`(或 `__delete__`)。如 `property`。**优先级高于实例字典**。 +- **非数据描述符**:只有 `__get__`。如**普通函数**。**优先级低于实例字典**。 + +最重要的一个事实:**类里 `def` 出来的函数,就是非数据描述符**。当你在实例上访问 `p.m`,触发函数的 `__get__`(`func_descr_get`),它返回一个把函数和实例绑在一起的**绑定方法**——`self` 就是这么凭空冒出来的: + +`源文件:`[Objects/funcobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/funcobject.c#L583) + +```c +// Objects/funcobject.c —— func_descr_get +static PyObject * +func_descr_get(PyObject *func, PyObject *obj, PyObject *type) +{ + if (obj == Py_None || obj == NULL) { + Py_INCREF(func); + return func; // 在类上访问 → 返回函数本身 + } + return PyMethod_New(func, obj); // 在实例上访问 → 绑定方法(带上 self=obj) +} +``` + +![方法是描述符](descriptor.svg) + +```python +>>> class P: +... def m(self): pass +... +>>> p = P() +>>> type(p.m), hasattr(p.m, '__self__') # 实例访问:绑定方法,带 __self__ +(, True) +>>> type(P.m) # 类访问:就是个函数 + +``` + +`@classmethod` 和 `@staticmethod` 也是描述符,只是各自的 `__get__` 绑定的对象不同——这正好把「方法的三种形态」串成一条线: + +`源文件:`[Objects/funcobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/funcobject.c#L687)(`cm_descr_get`) · [funcobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/funcobject.c#L869)(`sm_descr_get`) + +```c +// classmethod 的 __get__:绑定到「类」,于是第一个参数是 cls +return PyMethod_New(cm->cm_callable, type); +// staticmethod 的 __get__:原样返回函数,谁也不绑 +Py_INCREF(sm->sm_callable); +return sm->sm_callable; +``` + +![三种方法的绑定](method-kinds.svg) + +```python +>>> class C: +... def m(self): pass +... @classmethod +... def cm(cls): pass +... @staticmethod +... def sm(): pass +... +>>> c = C() +>>> c.m.__self__ is c # 普通函数 → 绑定到实例 +True +>>> c.cm.__self__ is C # classmethod → 绑定到类 +True +>>> type(c.sm).__name__ # staticmethod → 就是原函数,谁也不绑 +'function' +``` + +一句话收束:`property`、`classmethod`、`staticmethod` 都不是什么语言魔法,只是实现了不同 `__get__`/`__set__` 的描述符。看懂了描述符,这些装饰器就再无神秘可言。 + +## `__slots__`:去掉实例字典 + +默认情况下,每个实例都带一个 `__dict__`(实例字典)来存属性——非常灵活(随时能加新属性),但每个实例都多背一个字典,量大时很占内存。如果一个类的属性是固定的几个,可以用 `__slots__` 把它们声明出来,让实例**不再有 `__dict__`**,属性改为存在对象里预留好的固定槽位中: + +```python +>>> class S: +... __slots__ = ('a',) +... +>>> s = S(); s.a = 1 +>>> hasattr(s, '__dict__') # 没有实例字典了 +False +>>> s.b = 2 # 也不能再随意加未声明的属性 +Traceback (most recent call last): + ... +AttributeError: 'S' object has no attribute 'b' +``` + +它的底层机制依然是描述符:`__slots__` 里的每个名字,都会在类上生成一个**成员描述符**(`member_descriptor`,一个数据描述符),`__get__`/`__set__` 直接读写对象里那个固定偏移处的槽位;同时类的 `tp_dictoffset` 被设为 0,表示「实例没有字典」: + +```python +>>> type(S.a).__name__ # 每个 slot 是一个成员描述符 +'member_descriptor' +>>> S.__dictoffset__ # 0:实例不带 __dict__ +0 +``` + +大量小对象(如几百万条固定字段的记录)用 `__slots__` 能显著省内存,访问也略快。代价是失去了「随时加属性」的灵活性,且多继承时有一些限制。 + +## 元类:类的类 + +最后回到开头那句话:**类是 `type` 的实例**,而 `type` 就是默认的**元类**。元类之于类,正如类之于实例——类控制实例怎么创建,元类控制类怎么创建。 + +```python +>>> type(int), type(P), type(type) +(, , ) +``` + +自定义元类(继承 `type`、重写 `__new__`/`__init__`)可以在「类被创建的那一刻」插手——校验属性、自动注册子类、改写类体、注入方法等。下面这个最小例子,就在每个用它的类上自动加了一个属性: + +```python +>>> class Meta(type): +... def __new__(mcs, name, bases, ns): +... ns['greeting'] = 'hi' # 往类的命名空间里塞东西 +... return super().__new__(mcs, name, bases, ns) +... +>>> class App(metaclass=Meta): +... pass +... +>>> App.greeting, type(App) is Meta +('hi', True) +``` + +这正是许多框架(ORM 把类字段映射成表列、序列化库自动登记类型)背后的手法。日常开发未必用得上,但理解了「类也是对象、由元类创建」,这扇门就向你打开了。 + +--- + +小结一下类型机制: + +- `MyClass()` 由 `type_call` 驱动:先 `tp_new`(`__new__`,分配)再 `tp_init`(`__init__`,初始化);若 `__new__` 返回的不是本类实例,`__init__` 会被跳过; +- `class` 语句是语法糖,最终调用元类 `type(名字, 基类, 命名空间)` 造出类型对象——**类是 `type` 的实例**; +- `obj.attr` 的查找有严格优先级:**数据描述符 > 实例 `__dict__` > 非数据描述符/类属性 > AttributeError**,沿类型的 **MRO**(C3 线性化)进行,并有方法缓存加速;`__getattribute__` 每次都走,`__getattr__` 只在找不到时兜底; +- **描述符**是这套机制的核心:函数(非数据描述符)在实例上访问时绑定 `self`,`classmethod` 绑定类、`staticmethod` 谁也不绑;`property` 是数据描述符;`__slots__` 用成员描述符替掉实例字典以省内存; +- **元类**控制类的创建,是「类也是对象」的自然延伸。 diff --git a/docs/objects/type-object/instance-creation.svg b/docs/objects/type-object/instance-creation.svg new file mode 100644 index 0000000..bc58e85 --- /dev/null +++ b/docs/objects/type-object/instance-creation.svg @@ -0,0 +1,30 @@ + +实例创建:type_call → __new__ → __init__ +调用 MyClass() 时,type_call 先调用 tp_new(对应 __new__)分配并返回一个新对象,再调用 tp_init(对应 __init__)初始化它,最后得到实例。 + + + + + + + + + + + +MyClass() 背后:type_call 依次调用 __new__、__init__ + + + + + +MyClass() +tp_new (__new__)分配并返回新对象 +tp_init (__init__)初始化实例 +实例对象 + diff --git a/docs/objects/type-object/method-kinds.svg b/docs/objects/type-object/method-kinds.svg new file mode 100644 index 0000000..5efc1e1 --- /dev/null +++ b/docs/objects/type-object/method-kinds.svg @@ -0,0 +1,65 @@ + +三种方法都是描述符,区别在 __get__ 绑定谁 +普通函数、classmethod、staticmethod 都是类里定义的描述符,区别只在各自 __get__ 的行为:普通函数在实例上访问时绑定实例(self),classmethod 绑定类(cls),staticmethod 不绑定任何东西、原样返回函数。 + + + + + + + + + + + +三种方法都是描述符,区别在 __get__ 绑定谁 + + +类里定义 +__get__ 行为 +在实例上访问 c.f 得到 + + + +def m(self) +普通函数(非数据描述符) + + +绑定实例 +PyMethod_New(func, 实例) + + +绑定方法,self = 实例 +__self__ 是该实例 + + + +@classmethod +cm(cls) + + +绑定类 +PyMethod_New(func, 类) + + +绑定方法,cls = 类 +__self__ 是类本身 + + + +@staticmethod +sm() + + +不绑定 +原样返回函数 + + +就是原函数 +没有 self / cls + diff --git a/docs/objects/type-object/mro.svg b/docs/objects/type-object/mro.svg new file mode 100644 index 0000000..20834f8 --- /dev/null +++ b/docs/objects/type-object/mro.svg @@ -0,0 +1,53 @@ + +MRO 与多继承的 C3 线性化 +菱形继承 class D(B, Cc)、B(A)、Cc(A)、A(object),经 C3 线性化算出的方法解析顺序 MRO 为 D → B → Cc → A → object。属性查找就沿这个顺序在各个类里依次找。 + + + + + + + + + +菱形继承 + + + + + + + + + + + + + object + A + B + Cc + D + +D(B, Cc),B(A),Cc(A),A(object) + + + +C3 线性化得到 MRO + + D + B + Cc + A + object + + + + +属性查找沿此顺序 + diff --git a/docs/playground/index.md b/docs/playground/index.md new file mode 100644 index 0000000..a37b1d2 --- /dev/null +++ b/docs/playground/index.md @@ -0,0 +1,26 @@ +--- +layout: page +title: Playground +description: 在线改造并运行迷你 Python 虚拟机 +sidebar: false +aside: false +--- + +
+ +# 迷你 Python 虚拟机 · Playground + +左边是这台用 Python 写成的迷你虚拟机的**完整源码**,可以直接编辑;右边是一个**终端式 REPL**——直接在里面输入代码、回车执行,用你**改过的**虚拟机来跑,立刻看到效果。配套讲解见[实战章节:动手写一个迷你 Python 虚拟机](/practice/mini-vm/)。 + + + + + +
+ + diff --git a/docs/practice/mini-vm/call-frame-stack.svg b/docs/practice/mini-vm/call-frame-stack.svg new file mode 100644 index 0000000..5bb15f2 --- /dev/null +++ b/docs/practice/mini-vm/call-frame-stack.svg @@ -0,0 +1,47 @@ + +函数调用的帧栈压入与弹出 +每次 CALL_FUNCTION 都新建一个帧压入帧栈,RETURN_VALUE 则弹出帧、把返回值交还调用者。当前活动的永远是帧栈顶那个帧。以 fact 递归为例:模块帧调用 fact(3),压入 fact(n=3) 帧;它又调 fact(2)、fact(1),帧栈一层层叠起,每个帧有自己的局部变量 n 和求值栈。到达基准情形后逐层 RETURN,帧一层层弹出,返回值层层交还上一层,最终回到模块帧。这正是第四部分帧与求值循环讲的调用栈。 + + + + + + + + + + + +CALL 压帧,RETURN 弹帧,活动帧永在栈顶 + + + + + fact(n=1)活动帧(栈顶) + + fact(n=2)等待返回 + + fact(n=3)等待返回 + + <module>最初的帧 + + + + +CALL +压入新帧 +fact 调 fact + + + +RETURN +弹出帧 +返回值交还上一层 + +每个帧有自己的局部变量 n 与求值栈;递归 = 同一函数的多次调用各占一帧 + diff --git a/docs/practice/mini-vm/index.md b/docs/practice/mini-vm/index.md new file mode 100644 index 0000000..baa8357 --- /dev/null +++ b/docs/practice/mini-vm/index.md @@ -0,0 +1,242 @@ +# 动手:用 Python 写一个迷你 Python 虚拟机 + +读完前面六部分,我们把 CPython 的对象、编译、虚拟机、运行时、内存管理都拆开看了一遍。但「看懂」和「写得出」之间还隔着一层。这一章是全书的**实战 capstone**:我们用大约 300 行 Python,亲手实现一个**迷你 Python 虚拟机**——它能把一小段 Python **编译成字节码**,再用一个**求值循环**逐条执行,连**函数调用与递归**(帧栈)都支持。 + +更妙的是,**它就在你的浏览器里跑**:页面底部的交互组件通过 WebAssembly(Pyodide)把真正的 Python 运行环境搬进了网页,你写的代码会被我们的迷你虚拟机**单步执行**,求值栈、局部变量、调用栈的每一步变化都看得见。整章没有服务端,纯静态。 + +## 总览:一条和 CPython 同构的流水线 + +我们的迷你虚拟机走的是和 CPython **一模一样**的路子,只是每一环都简化到「够教学」的程度: + +![迷你虚拟机的流水线](pipeline.svg) + +1. **源码 → AST**:借用 Python 自带的 `ast.parse`,把源码解析成抽象语法树(第三部分讲过 CPython 也是这么干的); +2. **AST → 玩具字节码**:我们写一个**编译器**遍历 AST,吐出自定义的指令序列; +3. **字节码 → 求值循环**:我们写一个**虚拟机**,用一个大循环 + 帧栈逐条执行指令、操作求值栈; +4. **输出**:`print` 的结果。 + +唯一「偷懒」的是第 1 步借用了 `ast`——因为词法/语法分析不是本书重点。从 AST 往后的**编译**与**执行**,全是我们自己写的,也正是前几部分的核心。 + +## 设计指令集:玩具版对照 CPython + +先定指令集。我们刻意模仿 CPython 的栈式指令,但只保留最核心的十几条。为了**直观**,玩具字节码把名字和常量**直接内联**在参数里(真实 CPython 用下标去 `co_consts`/`co_varnames` 查表,第三部分见过): + +![玩具指令集对照表](instruction-set.svg) + +每条指令就是一个 `(op, arg)` 二元组,承载它的容器是 `Code`——对应 CPython 的 code object: + +```python +# minivm.py —— 一段可执行的字节码(模块体,或一个函数体) +class Code: + def __init__(self, name, params=()): + self.id = _new_id() + self.name = name + self.params = list(params) # 形参名 + self.instrs = [] # [[op, arg], ...] + + def emit(self, op, arg=None): + self.instrs.append([op, arg]) + return len(self.instrs) - 1 # 返回这条指令的位置(跳转回填要用) +``` + +## 编译器:把 AST 翻译成字节码 + +编译器是一个遍历 AST 的访问器。**表达式**编译成「把值压上求值栈」的指令,**语句**编译成「产生副作用」的指令。先看表达式——这正是第四部分求值栈那一套的逆向(生成端): + +```python +# minivm.py —— 表达式编译(节选) +def compile_expr(self, e): + if isinstance(e, ast.Constant): + self.code.emit("LOAD_CONST", e.value) # 常量 → 压栈 + elif isinstance(e, ast.Name): + self.load_name(e.id) # 变量 → 压栈 + elif isinstance(e, ast.BinOp): + self.compile_expr(e.left) # 先算左 + self.compile_expr(e.right) # 再算右 + self.code.emit("BINARY_OP", BINOPS[type(e.op)]) # 弹二压一 + elif isinstance(e, ast.Compare): + self.compile_expr(e.left) + self.compile_expr(e.comparators[0]) + self.code.emit("COMPARE_OP", CMPOPS[type(e.ops[0])]) + elif isinstance(e, ast.Call): + self.compile_call(e) + ... +``` + +`a + b * 2` 会被编译成 `LOAD a / LOAD b / LOAD_CONST 2 / BINARY_OP * / BINARY_OP +`——后缀顺序,正好喂给栈式机求值。赋值语句则是「算出值,再存进名字」: + +```python +# minivm.py —— 赋值语句 +if isinstance(s, ast.Assign): + self.compile_expr(s.value) # 算出右边的值(压栈) + self.store_name(s.targets[0].id) # 弹栈,存进左边的名字 +``` + +### 控制流:跳转回填 + +`if` 和 `while` 编译成**条件跳转 + 无条件跳转**(第四部分控制流章的核心)。难点在于:编译 `if` 的条件时,我们还**不知道** else 分支在哪——它的地址要等后面的指令都生成完才确定。办法是先 `emit` 一条**占位**的跳转,记下它的位置,等目标地址确定后再**回填**: + +![if / while 的跳转回填](jump-backpatch.svg) + +```python +# minivm.py —— 编译 while(跳转回填) +def compile_while(self, s): + start = len(self.code.instrs) # 循环顶部 + self.compile_expr(s.test) # 算条件 + jmp_end = self.code.emit("POP_JUMP_IF_FALSE", None) # 占位:条件假→跳出 + self.compile_stmts(s.body) # 循环体 + self.code.emit("JUMP_ABSOLUTE", start) # 往回跳,重来一轮 + self.code.instrs[jmp_end][1] = len(self.code.instrs) # 回填:循环出口地址 +``` + +注意末尾那行——循环体编译完了,循环出口的地址(`len(self.code.instrs)`)才确定,这时回头把占位的 `None` 改成真实地址。`if/else` 同理,只是回填两处(else 落点 + 汇合点)。 + +## 函数与帧栈:CALL / RETURN + +最有意思的部分来了——**函数**。`def` 编译成「造一个函数对象、存进名字」;调用编译成「压函数、压实参、`CALL_FUNCTION`」: + +```python +# minivm.py —— def 与调用 +def compile_funcdef(self, s): + params = [arg.arg for arg in s.args.args] + fcode = Code(s.name, params) # 函数体单独编译成一个 Code + local = set(params) | assigned_names(s.body) # 作用域分析:哪些名字是局部 + Compiler(fcode, "function", local).compile_stmts(s.body) + fcode.emit("LOAD_CONST", None); fcode.emit("RETURN_VALUE") # 隐式 return None + self.code.emit("MAKE_FUNCTION", fcode) # 造函数对象,压栈 + self.store_name(s.name) +``` + +这里藏着第四部分讲过的**作用域**:函数体里赋值过的名字(含形参)是**局部**,用 `LOAD_FAST`/`STORE_FAST`;其余名字(比如调用别的函数、递归调用自己)是**全局**,用 `LOAD_GLOBAL`——一个迷你版的 LEGB。 + +执行时,每次 `CALL_FUNCTION` 都**新建一个帧**压入帧栈,`RETURN_VALUE` 则弹出帧、把返回值交还调用者。这正是第四部分「帧与求值循环」的核心——**当前活动的永远是帧栈顶那个帧**: + +![函数调用的帧栈压入与弹出](call-frame-stack.svg) + +```python +# minivm.py —— 虚拟机里处理调用与返回(节选) +elif op == "CALL_FUNCTION": + args = [f.stack.pop() for _ in range(argc)][::-1] # 弹出实参 + func = f.stack.pop() # 弹出函数对象 + new_locals = dict(zip(func.params, args)) # 形参 ← 实参 + frames.append(Frame(func, new_locals, glob, "function")) # 压入新帧 + +elif op == "RETURN_VALUE": + retval = f.stack.pop() + frames.pop() # 弹出当前帧 + if frames: + frames[-1].stack.append(retval) # 返回值交还调用者 +``` + +递归(`fact`、`fib`)就这样自然成立——同一个函数的多次调用各有各的帧、各有各的局部变量,在帧栈上层层叠起、又层层退回。 + +## 虚拟机:一个大循环 + +把这一切驱动起来的,是那个我们已经无比熟悉的结构——**一个大循环 + 一个大 dispatch**,逐条「取指令 → 前进指针 → 执行」,直到帧栈空: + +```python +# minivm.py —— 求值循环骨架(节选) +while frames: + f = frames[-1] # 当前帧 = 帧栈顶 + if f.pc >= len(f.code.instrs): # 当前帧跑完了 + ... # 函数帧→返回 None;模块帧→整个程序结束 + record(frames, output) # 记录这一步的快照(给可视化用) + op, arg = f.code.instrs[f.pc] + f.pc += 1 # 先前进,跳转指令会再改写 + if op == "LOAD_CONST": f.stack.append(arg) + elif op == "LOAD_FAST": f.stack.append(f.locals[arg]) + elif op == "BINARY_OP": b = f.stack.pop(); a = f.stack.pop(); f.stack.append(_binop(arg, a, b)) + elif op == "POP_JUMP_IF_FALSE": + if not _truthy(f.stack.pop()): f.pc = arg + ... # 其余指令 +``` + +是不是和第四部分 `_PyEval_EvalFrameDefault` 的骨架如出一辙?这就是本书反复强调的:虚拟机的心脏,从来就是「取指令 → 派发 → 操作求值栈 → 再取下一条」。那行 `record(...)` 是我们额外加的——它把每一步的帧栈状态**快照**下来,串成一条轨迹,正是下面交互组件能「单步回放」的原因。 + +## 跑起来:单步看它执行 + +下面就是这台迷你虚拟机的**活体**。选一个示例(或自己改代码),点「编译并运行」,然后用「下一步」**单步**走——盯着右边:求值栈怎么压怎么弹、局部变量何时写入、调用栈在递归时怎么一层层叠起又退回。 + +> 首次点击会从 CDN 加载 Python 运行环境(Pyodide,约数 MB),请稍候片刻;之后即可流畅交互。 + + + + + +建议试试这几件事,把前几部分的知识对应起来: + +- 跑「**while 累加**」,单步看 `JUMP_ABSOLUTE` 如何**往回跳**形成循环(对应控制流章); +- 跑「**递归阶乘**」,看**调用栈**随 `fact(5)→fact(4)→…` 一层层**压起来**,到达基准情形后又一层层**退回**、把返回值交还上一层(对应帧与函数机制章); +- 跑「**递归 Fibonacci**」,感受同一个函数的不同调用各有独立的帧与局部变量。 + +## Playground:改造虚拟机本身 + +上面的组件让你跑「**用**虚拟机执行的代码」;而真正的乐趣,是**改虚拟机本身**。下面这个 Playground,左边是这台迷你虚拟机的**完整源码**(可直接编辑),右边是一个 **REPL**——用你**改过的**虚拟机来执行,立刻验证效果。 + +👉 打开 [**Playground**](/playground/)(顶部导航栏也有入口):左边直接编辑这台虚拟机的源码,右边是一个**终端式 REPL**,用你改过的虚拟机即时运行验证。几个上手实验: + +- **加一个运算符**:在 `BINARY_OP` 的 `_binop` 里加一行,让某个符号有新含义; +- **新增一条指令**:定义一个新 op,在编译器某处 `emit` 它、在虚拟机 `run` 里加一个分支处理它; +- **改改报错信息**,或在 `PRINT` 分支里给输出加个前缀——再在 REPL 里看变化。 + +> Playground 的 REPL 和本章 `minivm.py` 的命令行 REPL 是同一套 `execute()` 在背后驱动——变量与函数定义在多次输入间保留,「应用并重载 VM」或「清空会话」会重置。 + +## 旁注:看看真实的 CPython 字节码 + +我们的玩具指令集是简化版。真实 CPython 的字节码可以用标准库 `dis` 直接看——你会发现两者**形神俱似**: + +```python +>>> import dis +>>> def f(n): +... return n * 2 +>>> dis.dis(f) + 2 0 LOAD_FAST 0 (n) + 2 LOAD_CONST 1 (2) + 4 BINARY_MULTIPLY + 6 RETURN_VALUE +``` + +`LOAD_FAST`、`LOAD_CONST`、`RETURN_VALUE`——这些名字我们刚刚都亲手实现过。区别只在于:真实版用**下标**(`0`、`1`)去 `co_varnames`/`co_consts` 查表,而我们为了直观把名字和常量内联了;真实版还有上百条指令、`EXTENDED_ARG`、以及 3.11+ 的内联缓存等优化。但「栈式 + 取指派发」的内核,与你刚跑通的这台迷你虚拟机**完全一致**。 + +## 小结与扩展 + +这一章我们用 ~300 行 Python 把全书的主线**亲手走了一遍**: + +- **编译**:`ast.parse` 得到 AST,编译器把表达式翻成「压栈」指令、语句翻成「副作用」指令,`if/while` 用**跳转回填**生成条件/无条件跳转; +- **执行**:一个**大循环 + dispatch**逐条执行,操作**求值栈**; +- **函数**:`MAKE_FUNCTION`/`CALL_FUNCTION`/`RETURN_VALUE` + **帧栈**支撑了调用与递归,作用域分析区分 `LOAD_FAST`/`LOAD_GLOBAL`; +- 这套结构与 CPython 的 `compile.c` + `ceval.c` **同构**,只是处处从简。 + +想继续深入,这些都是很好的练习(难度递增): + +1. **字符串与列表**:让 `LOAD_CONST` 支持字符串,新增 `BUILD_LIST`/`BINARY_SUBSCR`; +2. **`break`/`continue`**:在 `while` 里用跳转实现(回顾控制流章的 block 栈思路); +3. **`and`/`or` 短路**:编译成条件跳转; +4. **闭包**:让函数能捕获外层局部变量——这要引入 cell 与 `LOAD_DEREF`,正是第四部分「函数机制:闭包」讲的那套; +5. **异常**:`try/except` 与栈展开,对应「异常机制」章。 + +每一项,回到对应章节都能找到 CPython 的「标准答案」。至此,从读源码到写实现,这趟旅程画上句号——愿你眼中的 Python,已经从一门「会用的语言」,变成了一台「看得见内部齿轮转动的机器」。 + +--- + +## 完整源码 + +上面的片段都摘自同一个文件 `minivm.py`,也正是交互组件里实际运行的那份代码(单一事实来源)。它还带一个命令行入口,把文件下载下来即可**在本地直接运行**: + +```console +$ python minivm.py # 进入交互式 REPL,边敲边执行 +迷你 Python 虚拟机 · 交互式 REPL +>>> 1 + 2 * 3 +7 +>>> def sq(n): +... return n * n +... +>>> print(sq(9)) +81 + +$ python minivm.py demo.py # 直接运行一个脚本文件 +``` + +完整源码列在这里: + +<<< @/practice/mini-vm/minivm.py{python} diff --git a/docs/practice/mini-vm/instruction-set.svg b/docs/practice/mini-vm/instruction-set.svg new file mode 100644 index 0000000..8c9a27e --- /dev/null +++ b/docs/practice/mini-vm/instruction-set.svg @@ -0,0 +1,40 @@ + +玩具指令集对照 CPython +迷你虚拟机的玩具指令集,刻意模仿 CPython 栈式指令,只保留最核心的十几条。LOAD_CONST 压入常量,LOAD_FAST 压入局部变量,STORE_FAST 弹栈存入局部变量,LOAD_NAME 与 STORE_NAME 用于模块级,LOAD_GLOBAL 取模块级名字,BINARY_OP 弹两个算一个压回,COMPARE_OP 比较,POP_JUMP_IF_FALSE 条件跳转,JUMP_ABSOLUTE 无条件跳转,PRINT 输出,MAKE_FUNCTION 造函数对象,CALL_FUNCTION 调用,RETURN_VALUE 返回。为直观,名字和常量直接内联在参数里,真实 CPython 用下标去 co_consts 和 co_varnames 查表。 + + + + + + + +玩具指令集(对照 CPython,只留核心十几条) + + + + 玩具指令 + 作用 + 对照 CPython + + + + LOAD_CONST v压入常量 v同名 + LOAD_FAST / STORE_FAST读 / 写局部变量同名 + LOAD_NAME / STORE_NAME读 / 写模块级名字同名 + LOAD_GLOBAL n取模块级名字(调函数)同名 + BINARY_OP / COMPARE_OP弹两个、算一个、压回BINARY_ADD… + POP_JUMP_IF_FALSE t栈顶为假则跳到 t同名 + JUMP_ABSOLUTE t无条件跳到 t同名 + MAKE_FUNCTION / CALL_FUNCTION造函数对象 / 调用同名 + RETURN_VALUE / PRINT返回 / 输出RETURN_VALUE + + + + +为直观,玩具版把名字 / 常量内联在参数里;真实 CPython 用下标查 co_consts / co_varnames 表 + diff --git a/docs/practice/mini-vm/jump-backpatch.svg b/docs/practice/mini-vm/jump-backpatch.svg new file mode 100644 index 0000000..c6bf119 --- /dev/null +++ b/docs/practice/mini-vm/jump-backpatch.svg @@ -0,0 +1,42 @@ + +if / while 的跳转回填 +编译 while 的条件时还不知道循环出口在哪,它的地址要等循环体的指令都生成完才确定。办法是先 emit 一条占位的 POP_JUMP_IF_FALSE,参数暂填 None,记下它的位置;编译完循环体、emit 末尾的 JUMP_ABSOLUTE 往回跳之后,循环出口地址才确定,这时回头把占位的 None 改成真实地址。这就是跳转回填。 + + + + + + + + + + +先占位、后回填:跳转目标编译完才知道 + + + + 2 ▸ 循环顶部:算条件 + 4 POP_JUMP_IF_FALSE ? + 6 循环体 … + 8 循环体 … + 10 JUMP_ABSOLUTE 2 + 12 循环出口(汇合点) + + + + +往回跳 +形成循环 + + + +回填 +? → 12 + +编译条件时先 emit 占位的 None,循环体编译完、出口地址确定后,再回头改写 + diff --git a/docs/practice/mini-vm/minivm.py b/docs/practice/mini-vm/minivm.py new file mode 100644 index 0000000..49612c7 --- /dev/null +++ b/docs/practice/mini-vm/minivm.py @@ -0,0 +1,505 @@ +"""minivm.py —— 一个用 Python 写成的迷你 Python 虚拟机。 + +流水线:源码 → AST(ast.parse)→ 玩具字节码 → 求值循环(带帧栈)。 + +对外只暴露 run_trace(src) -> JSON 字符串:把一段源码编译并「带轨迹地」执行, +返回每一步的帧栈快照,供浏览器里的可视化组件单步播放。 + +为了直观,玩具字节码把名字与常量直接内联在指令参数里 +(真实 CPython 用下标去 co_consts / co_varnames 查表)。 +""" +import ast +import json + +_counter = 0 +def _new_id(): + global _counter + _counter += 1 + return "c%d" % _counter + + +# ============ 字节码:一条指令就是 (op, arg) ============ + +class Code: + """一段可执行的字节码:模块体,或一个函数体。""" + def __init__(self, name, params=()): + self.id = _new_id() + self.name = name + self.params = list(params) # 形参名 + self.instrs = [] # [[op, arg], ...] + + def emit(self, op, arg=None): + self.instrs.append([op, arg]) + return len(self.instrs) - 1 + + def listing(self): + """生成给人看的反汇编文本。""" + out = [] + for op, arg in self.instrs: + if op == "MAKE_FUNCTION": + a = arg.name + elif arg is None: + a = "" + elif op == "LOAD_CONST": + a = repr(arg) + else: + a = str(arg) + out.append((op + " " + a).strip()) + return out + + +# ============ 编译器:AST → 玩具字节码 ============ + +BINOPS = {ast.Add: "+", ast.Sub: "-", ast.Mult: "*", ast.Div: "/", + ast.FloorDiv: "//", ast.Mod: "%", ast.Pow: "**"} +CMPOPS = {ast.Lt: "<", ast.LtE: "<=", ast.Gt: ">", ast.GtE: ">=", + ast.Eq: "==", ast.NotEq: "!="} + + +class CompileError(Exception): + pass + + +def assigned_names(body): + """收集函数体里被赋值过的名字(连同形参,就是这个函数的局部变量)。""" + names = set() + for stmt in body: + for node in ast.walk(stmt): + if isinstance(node, ast.Assign): + for t in node.targets: + if isinstance(t, ast.Name): + names.add(t.id) + elif isinstance(node, ast.AugAssign) and isinstance(node.target, ast.Name): + names.add(node.target.id) + return names + + +class Compiler: + def __init__(self, code, scope, localnames): + self.code = code + self.scope = scope # 'module' | 'function' + self.localnames = localnames # 函数作用域内的局部名字集合 + + def compile_stmts(self, stmts): + for s in stmts: + self.compile_stmt(s) + + # ---- 语句 ---- + def compile_stmt(self, s): + if isinstance(s, ast.Assign): + if len(s.targets) != 1 or not isinstance(s.targets[0], ast.Name): + raise CompileError("只支持 `名字 = 表达式` 形式的赋值") + self.compile_expr(s.value) + self.store_name(s.targets[0].id) + elif isinstance(s, ast.AugAssign): + if not isinstance(s.target, ast.Name): + raise CompileError("只支持对名字做 += 这类增量赋值") + if type(s.op) not in BINOPS: + raise CompileError("暂不支持的运算符") + self.load_name(s.target.id) + self.compile_expr(s.value) + self.code.emit("BINARY_OP", BINOPS[type(s.op)]) + self.store_name(s.target.id) + elif isinstance(s, ast.If): + self.compile_if(s) + elif isinstance(s, ast.While): + self.compile_while(s) + elif isinstance(s, ast.Return): + if self.scope != "function": + raise CompileError("return 只能用在函数里") + if s.value is None: + self.code.emit("LOAD_CONST", None) + else: + self.compile_expr(s.value) + self.code.emit("RETURN_VALUE") + elif isinstance(s, ast.FunctionDef): + self.compile_funcdef(s) + elif isinstance(s, ast.Expr): + self.compile_expr(s.value) + self.code.emit("POP_TOP") # 表达式语句:算完把结果丢弃 + elif isinstance(s, ast.Pass): + pass + else: + raise CompileError("暂不支持的语句:%s" % type(s).__name__) + + def compile_if(self, s): + self.compile_expr(s.test) + jmp_else = self.code.emit("POP_JUMP_IF_FALSE", None) # 占位,待回填 + self.compile_stmts(s.body) + if s.orelse: + jmp_end = self.code.emit("JUMP_ABSOLUTE", None) + self.code.instrs[jmp_else][1] = len(self.code.instrs) # 回填 else 落点 + self.compile_stmts(s.orelse) + self.code.instrs[jmp_end][1] = len(self.code.instrs) # 回填汇合点 + else: + self.code.instrs[jmp_else][1] = len(self.code.instrs) + + def compile_while(self, s): + start = len(self.code.instrs) + self.compile_expr(s.test) + jmp_end = self.code.emit("POP_JUMP_IF_FALSE", None) + self.compile_stmts(s.body) + self.code.emit("JUMP_ABSOLUTE", start) # 往回跳,形成循环 + self.code.instrs[jmp_end][1] = len(self.code.instrs) # 回填循环出口 + + def compile_funcdef(self, s): + if self.scope != "module": + raise CompileError("迷你版只支持在模块顶层定义函数(不支持嵌套 / 闭包)") + a = s.args + if (a.vararg or a.kwarg or a.kwonlyargs or a.defaults or a.kw_defaults): + raise CompileError("函数暂只支持简单的位置参数") + params = [arg.arg for arg in a.args] + fcode = Code(s.name, params) + local = set(params) | assigned_names(s.body) + Compiler(fcode, "function", local).compile_stmts(s.body) + fcode.emit("LOAD_CONST", None) # 函数体跑到尽头:隐式 return None + fcode.emit("RETURN_VALUE") + self.code.emit("MAKE_FUNCTION", fcode) + self.store_name(s.name) + + # ---- 表达式 ---- + def compile_expr(self, e): + if isinstance(e, ast.Constant): + if not isinstance(e.value, (int, float)) and e.value is not None: + raise CompileError("迷你版只支持数字 / 布尔 / None 常量") + self.code.emit("LOAD_CONST", e.value) + elif isinstance(e, ast.Name): + self.load_name(e.id) + elif isinstance(e, ast.BinOp): + if type(e.op) not in BINOPS: + raise CompileError("暂不支持的运算符") + self.compile_expr(e.left) + self.compile_expr(e.right) + self.code.emit("BINARY_OP", BINOPS[type(e.op)]) + elif isinstance(e, ast.UnaryOp) and isinstance(e.op, ast.USub): + self.compile_expr(e.operand) + self.code.emit("UNARY_NEG") + elif isinstance(e, ast.Compare): + if len(e.ops) != 1: + raise CompileError("暂不支持连续比较(如 a < b < c)") + if type(e.ops[0]) not in CMPOPS: + raise CompileError("暂不支持的比较运算符") + self.compile_expr(e.left) + self.compile_expr(e.comparators[0]) + self.code.emit("COMPARE_OP", CMPOPS[type(e.ops[0])]) + elif isinstance(e, ast.Call): + self.compile_call(e) + else: + raise CompileError("暂不支持的表达式:%s" % type(e).__name__) + + def compile_call(self, e): + if e.keywords: + raise CompileError("函数调用暂不支持关键字参数") + if isinstance(e.func, ast.Name) and e.func.id == "print": + if len(e.args) != 1: + raise CompileError("迷你版的 print 只接受一个参数") + self.compile_expr(e.args[0]) + self.code.emit("PRINT") # 输出,并压入 None(print 返回 None) + return + self.compile_expr(e.func) # 先把函数对象压栈 + for a in e.args: # 再依次压入实参 + self.compile_expr(a) + self.code.emit("CALL_FUNCTION", len(e.args)) + + # ---- 名字的载入 / 存储:迷你 LEGB ---- + def load_name(self, name): + if self.scope == "function": + if name in self.localnames: + self.code.emit("LOAD_FAST", name) # 局部变量 + else: + self.code.emit("LOAD_GLOBAL", name) # 模块级(如调用别的函数) + else: + self.code.emit("LOAD_NAME", name) + + def store_name(self, name): + if self.scope == "function": + self.code.emit("STORE_FAST", name) + else: + self.code.emit("STORE_NAME", name) + + +def compile_module(src): + tree = ast.parse(src) + code = Code("") + Compiler(code, "module", set()).compile_stmts(tree.body) + return code + + +# ============ 虚拟机:求值循环 + 帧栈 ============ + +class Frame: + """执行一段字节码的现场:求值栈、局部变量、指令指针。""" + def __init__(self, code, local, glob, kind): + self.code = code + self.locals = local # 名字 -> 值 + self.globals = glob + self.kind = kind # 'module' | 'function' + self.stack = [] # 求值栈 + self.pc = 0 # 指令指针 + + +class VMError(Exception): + pass + + +MAX_STEPS = 6000 +MAX_DEPTH = 60 + + +def _disp(v): + if isinstance(v, Code): + return "" % v.name + if v is None: + return "None" + if v is True: + return "True" + if v is False: + return "False" + return repr(v) + + +def _truthy(v): + return not (v is None or v is False or v == 0) + + +def _binop(op, a, b): + if op == "+": + return a + b + if op == "-": + return a - b + if op == "*": + return a * b + if op == "/": + if b == 0: + raise VMError("除以零") + return a / b + if op == "//": + if b == 0: + raise VMError("除以零") + return a // b + if op == "%": + return a % b + if op == "**": + return a ** b + raise VMError("未知运算符 %s" % op) + + +def _compare(op, a, b): + return {"<": a < b, "<=": a <= b, ">": a > b, ">=": a >= b, + "==": a == b, "!=": a != b}[op] + + +def run(module_code, record, glob=None): + if glob is None: + glob = {} + frames = [Frame(module_code, glob, glob, "module")] + output = [] + steps = 0 + + while frames: + f = frames[-1] + + # 当前帧跑到尽头 + if f.pc >= len(f.code.instrs): + if f.kind == "function": # 函数没显式 return:返回 None + frames.pop() + if frames: + frames[-1].stack.append(None) + continue + break # 模块体结束:整个程序结束 + + record(frames, output) # 记录「即将执行 f.pc 这条指令」的快照 + steps += 1 + if steps > MAX_STEPS: + raise VMError("执行步数过多(可能是死循环)") + + op, arg = f.code.instrs[f.pc] + f.pc += 1 # 先前进,跳转指令会再改写 + + if op == "LOAD_CONST": + f.stack.append(arg) + elif op == "LOAD_FAST": + if arg not in f.locals: + raise VMError("局部变量 %s 在赋值前被使用" % arg) + f.stack.append(f.locals[arg]) + elif op == "STORE_FAST": + f.locals[arg] = f.stack.pop() + elif op == "LOAD_NAME": + if arg not in f.locals: + raise VMError("名字 %s 未定义" % arg) + f.stack.append(f.locals[arg]) + elif op == "STORE_NAME": + f.locals[arg] = f.stack.pop() + elif op == "LOAD_GLOBAL": + if arg not in glob: + raise VMError("名字 %s 未定义" % arg) + f.stack.append(glob[arg]) + elif op == "BINARY_OP": + b = f.stack.pop() + a = f.stack.pop() + f.stack.append(_binop(arg, a, b)) + elif op == "UNARY_NEG": + f.stack.append(-f.stack.pop()) + elif op == "COMPARE_OP": + b = f.stack.pop() + a = f.stack.pop() + f.stack.append(_compare(arg, a, b)) + elif op == "POP_JUMP_IF_FALSE": + if not _truthy(f.stack.pop()): + f.pc = arg + elif op == "JUMP_ABSOLUTE": + f.pc = arg + elif op == "PRINT": + output.append(_disp(f.stack.pop())) + f.stack.append(None) + elif op == "POP_TOP": + f.stack.pop() + elif op == "MAKE_FUNCTION": + f.stack.append(arg) # arg 是函数体 Code,直接当函数对象 + elif op == "CALL_FUNCTION": + argc = arg + args = [f.stack.pop() for _ in range(argc)][::-1] + func = f.stack.pop() + if not isinstance(func, Code): + raise VMError("不是可调用对象") + if len(args) != len(func.params): + raise VMError("%s() 需要 %d 个参数,给了 %d 个" + % (func.name, len(func.params), len(args))) + if len(frames) >= MAX_DEPTH: + raise VMError("递归过深(可能无限递归)") + new_locals = dict(zip(func.params, args)) + frames.append(Frame(func, new_locals, glob, "function")) + elif op == "RETURN_VALUE": + retval = f.stack.pop() + frames.pop() + if frames: + frames[-1].stack.append(retval) + else: + raise VMError("未知指令 %s" % op) + + record(frames, output, done=True) + return output + + +def execute(src, glob=None): + """编译并执行一段源码,返回(输出文本, 全局名字空间)。 + + 传入并复用同一个 glob,即可在多次调用间保留变量与函数定义——REPL 靠它实现。 + """ + module_code = compile_module(src) + out = run(module_code, lambda *a, **k: None, glob) + return "\n".join(out), glob + + +# ============ 对外接口:带轨迹地跑一遍,返回 JSON ============ + +def _collect_codes(code, out): + out[code.id] = {"name": code.name, "listing": code.listing()} + for op, arg in code.instrs: + if op == "MAKE_FUNCTION": + _collect_codes(arg, out) + + +def run_trace(src): + global _counter + _counter = 0 + try: + module_code = compile_module(src) + except (SyntaxError, CompileError) as e: + return json.dumps({"ok": False, "error": "编译错误:%s" % e}) + + codes = {} + _collect_codes(module_code, codes) + trace = [] + + def record(frames, output, done=False): + trace.append({ + "frames": [ + { + "code_id": fr.code.id, + "name": fr.code.name, + "pc": fr.pc, + "stack": [_disp(x) for x in fr.stack], + "locals": [[k, _disp(v)] for k, v in fr.locals.items()], + } + for fr in frames + ], + "output": "\n".join(output), + "done": done, + }) + + try: + run(module_code, record) + except Exception as e: # noqa: BLE001 —— 任何运行期错误都友好返回 + return json.dumps({"ok": False, "error": "运行错误:%s" % e, + "codes": codes, "trace": trace}) + + return json.dumps({"ok": True, "codes": codes, "trace": trace}) + + +# ============ 命令行入口:直接运行文件,或进入交互式 REPL ============ + +def _maybe_echo(src): + """REPL 小贴心:若输入是一句纯表达式(且不是 print 调用),自动回显它的值。""" + try: + tree = ast.parse(src) + except SyntaxError: + return src + if len(tree.body) == 1 and isinstance(tree.body[0], ast.Expr): + e = tree.body[0].value + is_print = (isinstance(e, ast.Call) and isinstance(e.func, ast.Name) + and e.func.id == "print") + if not is_print: + return "print(" + src.strip() + ")" + return src + + +def _run_text(src, glob): + try: + text, glob = execute(src, glob) + except (CompileError, SyntaxError) as e: + print("编译错误:%s" % e) + except VMError as e: + print("运行错误:%s" % e) + else: + if text: + print(text) + return glob + + +def _run_file(path): + with open(path, encoding="utf-8") as fp: + _run_text(fp.read(), {}) + + +def _repl(): + print("迷你 Python 虚拟机 · 交互式 REPL") + print("支持:赋值、算术 / 比较、if / while、def / return(含递归)、print") + print("块语句(def/if/while)输入完后敲一个空行执行;Ctrl-D / Ctrl-C 退出。\n") + glob = {} + while True: + try: + first = input(">>> ") + if first.strip() == "": + continue + lines = [first] + # 以冒号结尾说明是块(def/if/while…),继续读到空行为止 + if first.rstrip().endswith(":"): + while True: + cont = input("... ") + if cont.strip() == "": + break + lines.append(cont) + src = "\n".join(lines) + glob = _run_text(_maybe_echo(src), glob) + except (EOFError, KeyboardInterrupt): + print("\n再见!") + break + + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + _run_file(sys.argv[1]) + else: + _repl() diff --git a/docs/practice/mini-vm/pipeline.svg b/docs/practice/mini-vm/pipeline.svg new file mode 100644 index 0000000..83f727c --- /dev/null +++ b/docs/practice/mini-vm/pipeline.svg @@ -0,0 +1,50 @@ + +迷你虚拟机的流水线 +迷你虚拟机走和 CPython 同构的路子。源码经 ast.parse 解析成 AST 抽象语法树,借用 Python 自带的 ast 模块;编译器遍历 AST 吐出自定义的玩具字节码;虚拟机用一个大循环加帧栈逐条执行指令、操作求值栈;最后产生 print 的输出。只有第一步借用了 ast,从 AST 往后的编译与执行全是自己写的。 + + + + + + + + + + + +源码 → AST → 玩具字节码 → 求值循环 + + +源码 +a = 5 … + + +AST +ast.parse(借用) + + +编译器 +自己写 + + +玩具字节码 +(op, arg) 序列 + + +虚拟机 +求值栈+帧栈 +→ 输出 + + + + + + + +这两环(编译 + 执行)是我们自己写的,也是前几部分的核心 + diff --git a/preface/code-organization.md b/docs/preface/code-organization/index.md similarity index 100% rename from preface/code-organization.md rename to docs/preface/code-organization/index.md diff --git a/preface/modify-code.md b/docs/preface/modify-code/index.md similarity index 100% rename from preface/modify-code.md rename to docs/preface/modify-code/index.md diff --git a/preface/unix-linux-build.md b/docs/preface/unix-linux-build/index.md similarity index 100% rename from preface/unix-linux-build.md rename to docs/preface/unix-linux-build/index.md diff --git a/preface/build-files.png b/docs/preface/windows-build/build-files.png similarity index 100% rename from preface/build-files.png rename to docs/preface/windows-build/build-files.png diff --git a/preface/windows-build.md b/docs/preface/windows-build/index.md similarity index 85% rename from preface/windows-build.md rename to docs/preface/windows-build/index.md index 29b7785..1a0de7a 100644 --- a/preface/windows-build.md +++ b/docs/preface/windows-build/index.md @@ -20,22 +20,22 @@ 在左侧的解决方案目录的顶端,右键选择“属性”,以打开属性界面(如下图所示)。 - + 由于我们只是研究 Python 的核心部分,可以选择不编译标准库和外部依赖,在“配置属性”->“配置”中仅勾选 python 和 pythoncore,然后点击“确定”(如下图所示)。 此外,默认情况下的编译设置是 Debug、32 位,您也可以根据自己的需求调整成 Release 或 64 位。 - + 在左侧的解决方案目录中选择 python,右键选择“生成”,以进行编译: - + 编译结束后,生成的文件存放在`PCbuild\win32`目录下(如下图所示),打开`python_d`即可打开新生成的 Python 3.7 解释器。 - + ## 更多内容 diff --git a/preface/vs2017-build.png b/docs/preface/windows-build/vs2017-build.png similarity index 100% rename from preface/vs2017-build.png rename to docs/preface/windows-build/vs2017-build.png diff --git a/preface/vs2017-configure.png b/docs/preface/windows-build/vs2017-configure.png similarity index 100% rename from preface/vs2017-configure.png rename to docs/preface/windows-build/vs2017-configure.png diff --git a/preface/vs2017-installation.png b/docs/preface/windows-build/vs2017-installation.png similarity index 100% rename from preface/vs2017-installation.png rename to docs/preface/windows-build/vs2017-installation.png diff --git a/preface/vs2017-properties.png b/docs/preface/windows-build/vs2017-properties.png similarity index 100% rename from preface/vs2017-properties.png rename to docs/preface/windows-build/vs2017-properties.png diff --git a/docs/runtime/gil/allow-threads.svg b/docs/runtime/gil/allow-threads.svg new file mode 100644 index 0000000..3be3c12 --- /dev/null +++ b/docs/runtime/gil/allow-threads.svg @@ -0,0 +1,36 @@ + +I/O 与 C 扩展主动让出 GIL +线程在做阻塞操作前会主动放下 GIL。CPython 的惯用法是一对宏 Py_BEGIN_ALLOW_THREADS 和 Py_END_ALLOW_THREADS,把可能阻塞的系统调用如读文件、等网络、sleep 夹在中间,进去前 drop_gil 放下棒,出来后 take_gil 拿回。于是一个线程等 I/O 时棒被别的线程拿去跑,I/O 等待被重叠掉。NumPy 这类 C 扩展做大块纯计算时也会释放 GIL,让多线程真正并行算数。 + + + + + + + + + +阻塞前主动放下棒,别的线程趁机跑 + + + + Py_BEGIN_ALLOW_THREADS + ↓ drop_gil 放下棒 + read() / sleep(阻塞) + 没占棒 → 别人能跑 + + + + + +另一线程拿到棒 +在这段时间真正并行 + +I/O 密集线程、释放 GIL 的 C 扩展(NumPy 等)→ 真正利用多核 +「GIL 让多线程没用」只对纯 Python 的 CPU 密集成立 + diff --git a/docs/runtime/gil/eval-breaker.svg b/docs/runtime/gil/eval-breaker.svg new file mode 100644 index 0000000..7552439 --- /dev/null +++ b/docs/runtime/gil/eval-breaker.svg @@ -0,0 +1,48 @@ + +eval_breaker:聚合所有待办的一个标志 +每条字节码都去检查多个标志太亏。CPython 用一个聚合标志 eval_breaker:只要任何一件待办发生,让棒、信号、待处理回调、异步异常,就把这一个标志置位。求值循环每轮只需做一次廉价的原子读,eval_breaker 没置位就径直执行下一条指令,只有它置位了才进去逐项核对到底是哪件事。常态下多线程的检查成本几乎可以忽略。 + + + + + + + + + + +eval_breaker:一次廉价检查,管住所有待办 + + + + gil_drop_request + 收到信号 + 待处理回调 + 异步异常 + + + + +eval_breaker +任一待办 → 置位 + + + + + + + + +求值循环每轮 +一次原子读 +没置位 → 直接跑下一条 +置位了 → 进去逐项核对 + + +常态零负担 —— 只有真有事时才付出逐项检查的代价 + diff --git a/docs/runtime/gil/gil-baton.svg b/docs/runtime/gil/gil-baton.svg new file mode 100644 index 0000000..302e337 --- /dev/null +++ b/docs/runtime/gil/gil-baton.svg @@ -0,0 +1,44 @@ + +GIL 是一根执行权接力棒 +GIL 本质是一把互斥锁,规则只有一条:一个线程必须先持有 GIL 才能执行字节码。于是 GIL 像一根接力棒,谁攥着它谁才能跑求值循环,其他线程只能排队等棒。多核机器上有多个线程,但因为只有一根棒,同一时刻仍只有一个线程在真正执行 Python 代码。图中线程 1 持棒运行,线程 2 和线程 3 在等待。 + + + + + + + + + +只有一根棒,谁攥着谁才能跑 + + + +GIL(棒) + + + +线程 1 +持棒 · 执行字节码 +求值循环运行中 + + + + +线程 2 +等棒 +阻塞,不执行 + + + +线程 3 +等棒 +阻塞,不执行 + +8 个线程也只有 1 根棒 —— 同一时刻只有 1 个线程在跑 Python 字节码 + diff --git a/docs/runtime/gil/gil-handoff.svg b/docs/runtime/gil/gil-handoff.svg new file mode 100644 index 0000000..b958dce --- /dev/null +++ b/docs/runtime/gil/gil-handoff.svg @@ -0,0 +1,47 @@ + +GIL 的协作式切换 +GIL 在线程间传递是协作式的,由两端配合。等待端线程在 take_gil 里定时等待,默认 5 毫秒,若超时 GIL 仍被同一线程攥着,就设置 gil_drop_request 标志,相当于催对方让棒。持有端线程不会被强行打断,它在求值循环的检查点主动查看有没有谁在催,一旦发现 gil_drop_request 就 drop_gil 让棒,给别人机会,再 take_gil 抢回。合起来:等待线程催,持有线程在检查点主动让,别人抢到棒。 + + + + + + + + + + + +协作式:等待方催,持有方主动让 + + + +持有线程(正在跑) + +求值循环检查点 +看到 gil_drop_request? + +drop_gil → take_gil +主动让棒,再抢回 + + + +等待线程(排队) + +take_gil:定时等 +最多 5ms(switch interval) + +超时 → SET_GIL_DROP_REQUEST +催对方让棒 + + + + + +让棒后,等待线程抢到棒开始跑 + diff --git a/docs/runtime/gil/gil-tradeoff.svg b/docs/runtime/gil/gil-tradeoff.svg new file mode 100644 index 0000000..dda7d36 --- /dev/null +++ b/docs/runtime/gil/gil-tradeoff.svg @@ -0,0 +1,46 @@ + +GIL 的取舍与出路 +GIL 的代价很明确:纯 Python 的 CPU 密集任务无法靠多线程吃满多核。绕开它的常见办法:I/O 密集放心用 threading,GIL 在 I/O 时会让出;CPU 密集用 multiprocessing 开多进程,每个进程有自己的解释器和 GIL,真正并行;重计算下沉到会释放 GIL 的 C 扩展如 NumPy;更远的方向是 3.12 起的子解释器与 per-interpreter GIL。GIL 不是设计缺陷,而是用牺牲一种并行换来实现简单、单线程高效、与海量 C 扩展轻松互操作。 + + + + + + + + + +受限场景:纯 Python 的 CPU 密集 —— 怎么绕 + + +I/O 密集 +threading +I/O 时让棒 +天然能重叠 + + +CPU 密集 +multiprocessing +多进程,各有 GIL +真正并行 + + +重计算 +C 扩展 +NumPy 等释放 GIL +多核算数 + + +未来 +子解释器 +per-interp +3.12+ + +GIL 不是缺陷,是权衡:牺牲一种并行,换来实现简单、单线程高效、C 扩展易写 +懂不懂 GIL,常是「为什么多线程没变快」与「该用线程还是进程」的分水岭 + diff --git a/docs/runtime/gil/index.md b/docs/runtime/gil/index.md new file mode 100644 index 0000000..48cd11f --- /dev/null +++ b/docs/runtime/gil/index.md @@ -0,0 +1,171 @@ +# 多线程与 GIL + +`import` 让多个模块协作;而**多线程**让多段代码看似「同时」运行。但 Python 的线程有个声名远扬的脾气——那把 **GIL(Global Interpreter Lock,全局解释器锁)**。它常被骂「让多线程形同虚设」,又被赞「让 CPython 简单又快」。这一章,也是第五部分的收尾,我们就把它看个透彻:GIL **是什么、为什么存在、怎么工作、代价何在**。 + +## 先看现象:多线程为何不加速 CPU 密集任务 + +不谈原理,先看一个让很多人困惑的实验。开两个线程各跑一段纯计算,按直觉应该快一倍,结果却和单线程**几乎一样慢**: + +```python +>>> import time, threading +>>> def cpu_bound(): +... x = 0 +... for _ in range(30_000_000): x += 1 +... +>>> t0 = time.time(); cpu_bound(); cpu_bound(); print("顺序:", time.time()-t0) +顺序: 2.01 +>>> t0 = time.time() +>>> ts = [threading.Thread(target=cpu_bound) for _ in range(2)] +>>> [t.start() for t in ts]; [t.join() for t in ts]; print("两线程:", time.time()-t0) +两线程: 2.03 # 没快!两个线程在抢同一把锁,本质还是轮流跑 +``` + +可换成 **I/O 密集**任务(比如 `time.sleep`、网络请求),多线程又确实能重叠、能提速。这个反差正是理解 GIL 的入口:**GIL 让任意时刻只有一个线程在执行 Python 字节码,但线程在等 I/O 时会让出它**。 + +## GIL 是什么:一根「执行权」的接力棒 + +GIL 本质上就是一把**互斥锁**——准确说,是一个「锁标志 + 条件变量」的组合。它的状态记在运行时里: + +`源文件:`[Include/internal/gil.h](https://github.com/python/cpython/blob/v3.7.0/Include/internal/gil.h#L19) + +```c +// Include/internal/gil.h —— GIL 的运行时状态(节选) +struct _gil_runtime_state { + unsigned long interval; // 切换间隔,默认 5000 微秒(5ms) + _Py_atomic_address last_holder; // 上一个持有者(用于判断有没有发生过切换) + _Py_atomic_int locked; // GIL 是否已被持有 + unsigned long switch_number; // 累计切换次数 + PyCOND_T cond; // 等待 GIL 释放的条件变量 + PyMUTEX_T mutex; +}; +``` + +规则只有一条,却管住了一切:**一个线程,必须先持有 GIL,才能执行字节码**。于是 GIL 就像一根**接力棒**——谁攥着它,谁才能跑求值循环;其他线程只能在一旁排队等棒。多核机器上你有 8 个线程,但因为只有一根棒,同一时刻**仍只有一个线程在真正执行 Python 代码**: + +![GIL 是一根执行权接力棒](gil-baton.svg) + +这就解释了上面的实验:两个 CPU 密集线程,本质是在抢这根棒、轮流跑,加起来的工作量没变,自然不会更快。 + +![CPU 密集不加速,I/O 密集能重叠](threads-no-speedup.svg) + +## 为什么需要 GIL:保护「不设防」的引用计数 + +一个自然的问题是:为什么要给自己套上这么一把锁?答案藏在 CPython 的内存管理里——**引用计数**(下一部分的主题)。 + +回想第二部分:每个对象都有个 `ob_refcnt`,增减引用时用 `Py_INCREF`/`Py_DECREF` 改它,归零就回收。问题是,**这俩操作不是原子的**:`Py_INCREF` 实质是「读出 refcnt → 加一 → 写回」三步。如果两个线程同时对一个对象 `INCREF`,可能双双读到旧值、各自加一、写回——**两次加一只生效了一次**。计数错误的后果是灾难性的:少计数会导致对象被**提前释放**(之后访问就是悬空指针、崩溃),多计数则导致**内存泄漏**。 + +![为何需要 GIL:引用计数的竞态](refcount-race.svg) + +GIL 用最简单粗暴的方式根除了这个问题:**既然任意时刻只有一个线程在跑字节码,引用计数和所有解释器内部状态就天然安全**,无需给每个对象都加锁。另一条路——给每个对象配一把锁——不仅会让单线程程序也付出沉重的加锁开销,还极易死锁。GIL 是 CPython 在「简单 + 单线程快」和「多线程并行」之间做出的取舍:**牺牲多线程并行,换来实现简单、单线程高效、C 扩展易写**。 + +## GIL 怎么切换:协作式的让棒 + +既然只有一根棒,那它是怎么在线程间传递的?这是一套**协作式**机制,由两端配合完成。 + +**等待端:超时就「催」。** 一个想要 GIL 的线程,在 `take_gil` 里等待持有者释放。它不会无限干等——而是定时等待 `interval`(默认 **5 毫秒**)。若超时了 GIL 仍被同一个线程攥着(期间没发生过切换),它就设置一个标志 `gil_drop_request`,相当于「**催**对方让棒」: + +`源文件:`[Python/ceval_gil.h](https://github.com/python/cpython/blob/v3.7.0/Python/ceval_gil.h#L191) + +```c +// Python/ceval_gil.h —— take_gil(精简) +while (gil.locked) { + saved_switchnum = gil.switch_number; + COND_TIMED_WAIT(gil.cond, gil.mutex, INTERVAL, timed_out); // 最多等 5ms + if (timed_out && gil.locked && gil.switch_number == saved_switchnum) { + SET_GIL_DROP_REQUEST(); // 等太久了:催当前持有者让棒 + } +} +// ……抢到棒:gil.locked = 1,更新 last_holder、switch_number +``` + +**持有端:到检查点就让。** 正在跑的线程不会被强行打断——它在求值循环里**主动**在检查点查看「有没有谁在催我」。一旦发现 `gil_drop_request`,就老老实实让棒(`drop_gil`)、给别人机会,再重新抢回来(`take_gil`): + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L967) + +```c +// Python/ceval.c —— 求值循环里的让棒点(精简) +if (_Py_atomic_load_relaxed(&_PyRuntime.ceval.gil_drop_request)) { + /* Give another thread a chance */ + PyThreadState_Swap(NULL); + drop_gil(tstate); // 放下棒 + /* Other threads may run now */ + take_gil(tstate); // 重新抢棒 + PyThreadState_Swap(tstate); +} +``` + +![GIL 的协作式切换](gil-handoff.svg) + +合起来就是:**等待线程催(5ms 超时设标志)→ 持有线程在检查点主动让 → 别人抢到棒**。这个 5ms 就是 `sys.getswitchinterval()` 返回的「切换间隔」,可以调: + +```python +>>> import sys +>>> sys.getswitchinterval() +0.005 +>>> sys.setswitchinterval(0.001) # 调成 1ms:切换更勤,但切换开销也更多 +``` + +> 这是 3.2 起的「新 GIL」——基于**时间**(5ms)。更早的旧 GIL 是基于**字节码条数**(每跑 100 条检查一次),在某些负载下切换很不公平,遂被时间式取代。 + +## eval_breaker:一次廉价的检查,管住所有「待办」 + +这里有个值得玩味的工程细节。「让棒」要在求值循环里频繁检查,可如果每执行一条字节码都去读 `gil_drop_request`、再读有没有信号、再读有没有待处理调用……每条指令都背上一串检查,太亏了。 + +CPython 的办法是一个聚合标志 **`eval_breaker`**:只要**任何一件待办**发生(有人催让棒、收到信号、有待处理的回调、有异步异常),就把这一个标志置位。求值循环每轮只需做**一次廉价的原子读**——`eval_breaker` 没置位(绝大多数情况),就径直执行下一条指令;只有它置位了,才进去逐项核对到底是哪件事: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L943) + +```c +// Python/ceval.c —— 求值循环顶部的统一检查点(精简) +if (_Py_atomic_load_relaxed(&_PyRuntime.ceval.eval_breaker)) { // 一次廉价原子读 + if (...pending.calls_to_do...) Py_MakePendingCalls(); // 待处理回调 + if (...gil_drop_request...) { drop_gil(); take_gil(); } // 让棒(上一节) + if (tstate->async_exc != NULL) { ...; goto error; } // 异步异常 +} +fast_next_opcode: + ... // 取下一条指令 +``` + +![eval_breaker:聚合所有待办的一个标志](eval-breaker.svg) + +这样一来,常态下多线程的「检查成本」几乎可以忽略,只有真有事时才付出代价。这也是为什么让棒是**协作式**的:线程只在这个检查点让棒,所以一条「不可中断」的长字节码(比如某些 C 实现的内建操作)跑起来时,GIL 是不会中途易手的。 + +## I/O 与 C 扩展:主动放下棒 + +回到开头那个反差:为什么 I/O 密集任务多线程能提速?因为**线程在做阻塞操作前会主动放下 GIL**。CPython 的惯用法是一对宏 `Py_BEGIN_ALLOW_THREADS` / `Py_END_ALLOW_THREADS`——把可能阻塞的系统调用(读文件、等网络、`time.sleep`)夹在中间,进去前 `drop_gil`、出来后 `take_gil`: + +![I/O 与 C 扩展主动让出 GIL](allow-threads.svg) + +```c +// 典型的阻塞 I/O 写法(示意) +Py_BEGIN_ALLOW_THREADS // 放下 GIL —— 别的线程现在能跑了 +ret = read(fd, buf, len); // 阻塞在这儿,但没占着棒 +Py_END_ALLOW_THREADS // 重新拿回 GIL +``` + +于是一个线程等 I/O 时,棒被别的线程拿去跑——I/O 的等待时间被**重叠**掉了。同理,NumPy 这类 C 扩展在做大块纯计算时也会释放 GIL,让多个线程真正并行算数。**所以「GIL 让多线程没用」并不准确**:它只挡住了**纯 Python 的 CPU 密集**并行,对 I/O 密集和「会释放 GIL 的 C 扩展」毫无妨碍。 + +## 代价与出路 + +GIL 的代价很明确:**纯 Python 的 CPU 密集任务,无法靠多线程吃满多核**。绕开它的常见办法有几条: + +![GIL 的取舍与出路](gil-tradeoff.svg) + +- **I/O 密集**:放心用 `threading`,GIL 在 I/O 时会让出; +- **CPU 密集**:用 `multiprocessing` 开**多进程**——每个进程有自己的解释器和 GIL,真正并行(代价是进程间通信开销); +- **重计算下沉到 C**:用会释放 GIL 的扩展(NumPy、加密库等); +- **更远的方向**:3.12 起的**子解释器**与 per-interpreter GIL,正朝着「一个进程内多个互不共享 GIL 的解释器」演进——但那是后话了。 + +GIL 不是设计缺陷,而是一个**权衡**:它用「牺牲一种并行」换来了实现的简单、单线程的高效,以及与海量 C 扩展的轻松互操作。理不理解 GIL,往往就是「为什么我的多线程没变快」与「我该用线程还是进程」之间的分水岭。 + +--- + +小结一下多线程与 GIL: + +- **GIL 是一把全局互斥锁**(`locked` 标志 + 条件变量),线程**必须持有它才能执行字节码**——像一根执行权接力棒,任意时刻只有一个线程在跑 Python 代码; +- 它存在的根本原因是**保护非原子的引用计数**(及所有解释器内部状态):没有 GIL,并发的 `Py_INCREF`/`Py_DECREF` 会算错计数,导致提前释放或泄漏; +- 切换是**协作式**的:等待线程在 `take_gil` 里超时(默认 **5ms**)后设 `gil_drop_request`,持有线程在求值循环的检查点主动 `drop_gil` 让棒、再 `take_gil` 抢回; +- **`eval_breaker`** 把「让棒、信号、待处理调用、异步异常」聚合成一个标志,让求值循环每轮只做一次廉价原子读——常态零负担; +- 线程在**阻塞 I/O**和**会释放 GIL 的 C 扩展**里主动让棒,所以 I/O 密集与重 C 计算能真正并行;纯 Python 的 **CPU 密集**才是 GIL 的受限场景,出路是多进程或下沉到 C。 + +至此,第五部分「运行时」就完整了:解释器如何初始化、如何 import 模块、多线程下 GIL 如何运转。这一路上,**引用计数**这个词反复出现——它是 GIL 之所以存在的根由,也是对象生死的裁决者。它究竟如何工作、又如何与「循环垃圾回收」配合?最后一部分「内存管理」,就从 **内存分配与引用计数** 讲起。 diff --git a/docs/runtime/gil/refcount-race.svg b/docs/runtime/gil/refcount-race.svg new file mode 100644 index 0000000..ee879b0 --- /dev/null +++ b/docs/runtime/gil/refcount-race.svg @@ -0,0 +1,42 @@ + +为何需要 GIL:引用计数的竞态 +GIL 存在的根本原因是保护非原子的引用计数。Py_INCREF 实质是读出 refcnt、加一、写回三步。如果两个线程同时对一个对象 INCREF,可能双双读到旧值 2、各自加一、都写回 3,两次加一只生效了一次,正确结果应是 4。计数错误后果灾难:少计数导致对象提前释放、之后访问悬空指针崩溃,多计数导致内存泄漏。GIL 让任意时刻只有一个线程跑字节码,引用计数天然安全,无需给每个对象加锁。 + + + + + + + + +没有 GIL:两个线程一起 INCREF 会算错 +Py_INCREF = 读 refcnt → 加一 → 写回(非原子) + + + +线程 A + + 读 refcnt = 2 + 加一 → 3 + 写回 refcnt = 3 + + + + +线程 B(同时) + + 读 refcnt = 2 ← 也读到旧值 + 加一 → 3 + 写回 refcnt = 3 + + + + +结果 refcnt = 3,应为 4 +少计数 → 提前释放、崩溃;多计数 → 泄漏 + diff --git a/docs/runtime/gil/threads-no-speedup.svg b/docs/runtime/gil/threads-no-speedup.svg new file mode 100644 index 0000000..f3260f0 --- /dev/null +++ b/docs/runtime/gil/threads-no-speedup.svg @@ -0,0 +1,49 @@ + +CPU 密集不加速,I/O 密集能重叠 +两种任务的时间线对比。上半部分是 CPU 密集:两个线程抢同一根棒,只能轮流跑,总时间和顺序执行一样,没有加速。下半部分是 I/O 密集:线程在等 I/O 时放下棒,等待时间被另一个线程的执行重叠掉,于是真正提速。这解释了为什么 GIL 只挡住纯 Python 的 CPU 密集并行,对 I/O 密集毫无妨碍。 + + + + + + +同样两个线程,结果天差地别 + + +CPU 密集:轮流跑,不加速 +线程1 +线程2 + + + + + + + + + + + + +总时长 ≈ 顺序执行(棒只有一根,两线程拼接着跑) + + +I/O 密集:等待重叠,提速 +线程1 +线程2 + + + + + + + +等 I/O(已放下棒) + +等 I/O 时让出棒 → 另一线程趁机跑 → 重叠提速 + diff --git a/docs/runtime/import-system/find-and-load-handoff.svg b/docs/runtime/import-system/find-and-load-handoff.svg new file mode 100644 index 0000000..90ecec3 --- /dev/null +++ b/docs/runtime/import-system/find-and-load-handoff.svg @@ -0,0 +1,38 @@ + +C 把查找加载交给 Python 写的 importlib +缓存未命中后,C 层的 import_find_and_load 实现短得惊人:把活儿原样转交给用 Python 写的 importlib,调它的 _find_and_load。这是一处精彩的分工:C 层只管查缓存这种性能敏感的快路径,真正复杂多变的查找加载逻辑全用 Python 写在 importlib._bootstrap 里,让它易读易改易扩展,代价由前面的 sys.modules 缓存兜住。 + + + + + + + + + +C 管缓存快路径,Python 管复杂逻辑 + + + +C 层(import.c) +查 sys.modules 缓存 +import_find_and_load +短小,只管快路径 + + + +Python 层(importlib) +_find_and_load +finder / loader 全套逻辑 +易读 · 易改 · 易扩展 + + +转交 + +用 Python 实现 import,加载开销由 sys.modules 缓存兜住 + diff --git a/docs/runtime/import-system/finder-loader-protocol.svg b/docs/runtime/import-system/finder-loader-protocol.svg new file mode 100644 index 0000000..252134b --- /dev/null +++ b/docs/runtime/import-system/finder-loader-protocol.svg @@ -0,0 +1,54 @@ + +find_spec 与 exec_module 两段式加载协议 +importlib 把导入一个模块拆成两段,由两类角色分工。finder 查找器回答这个模块在哪、该用什么方式加载,产出一个 ModuleSpec 导入规格,内含一个 loader。loader 加载器回答把模块真正建出来,执行模块代码、填充模块对象。流程是:遍历 finder 找到 spec,按 spec 造出空模块对象,先把它登记进 sys.modules,再调 loader.exec_module 在模块的 __dict__ 里执行其代码。先登记后执行支撑了循环导入。 + + + + + + + + + + +两段式:finder 找规格,loader 建模块 + + + +finder +find_spec(name) +在哪、怎么加载 + + + +ModuleSpec +.loader +导入规格 + + + +loader +exec_module(mod) +真正建出模块 + + + + + + +模块 +__dict__ +里跑代码 + + + + +关键顺序 +造空模块 → 先登记进 sys.modules → 再 exec_module +「先登记后执行」正是循环导入能半工作的原因 + diff --git a/docs/runtime/import-system/import-bytecode.svg b/docs/runtime/import-system/import-bytecode.svg new file mode 100644 index 0000000..383f052 --- /dev/null +++ b/docs/runtime/import-system/import-bytecode.svg @@ -0,0 +1,43 @@ + +import 语句的几种字节码 +三种 import 写法的字节码。import os 编译成 LOAD_CONST level、LOAD_CONST fromlist None、IMPORT_NAME os、STORE_NAME os,核心是 IMPORT_NAME。from os import getcwd 让 IMPORT_NAME 带上 fromlist 拿到模块,再用 IMPORT_FROM 从模块取出 getcwd 属性。from os import 星号走 IMPORT_STAR。三者起点都是同一个 IMPORT_NAME。 + + + + + + + + +三种 import,同一个起点 IMPORT_NAME + + +import os + + LOAD_CONST level / fromlist + IMPORT_NAME os + STORE_NAME os + + + +from os import getcwd + + IMPORT_NAME os(fromlist=getcwd) + IMPORT_FROM getcwd + STORE_NAME getcwd + + + +from os import * + + IMPORT_NAME os + IMPORT_STAR + + +核心都是 IMPORT_NAME + diff --git a/docs/runtime/import-system/import-is-call.svg b/docs/runtime/import-system/import-is-call.svg new file mode 100644 index 0000000..7cec752 --- /dev/null +++ b/docs/runtime/import-system/import-is-call.svg @@ -0,0 +1,46 @@ + +import 语句本质是调用 __import__ +IMPORT_NAME 做的第一件事是去 builtins 里取出 __import__ 函数来调用。所以 import X 本质等价于 X 等于 __import__ 调用 X。__import__ 是个普通的内建函数,因此可以被替换,这是导入钩子、沙箱、惰性导入库的工作原理。如果 __import__ 没被覆盖,走快路径直接调底层 C 实现 PyImport_ImportModuleLevelObject;否则当成普通函数调用。 + + + + + + + + + + +import X 等价于 X = __import__('X', …) + + +IMPORT_NAME +字节码指令 + + +builtins.__import__ +普通内建函数,可替换 + + +取出 + + + + + + +未覆盖(快路径) +C 实现 + + +已覆盖 +当普通函数调 + +替换 builtins.__import__ → 导入钩子 / 沙箱 / 惰性导入的工作原理 +import json ≡ json = __import__('json', globals(), locals(), None, 0) + diff --git a/docs/runtime/import-system/index.md b/docs/runtime/import-system/index.md new file mode 100644 index 0000000..0558e96 --- /dev/null +++ b/docs/runtime/import-system/index.md @@ -0,0 +1,210 @@ +# 模块与 import 机制 + +上一章我们看到,`importlib` 在初始化时被「自举」了起来——可它**怎么工作**,我们还没碰。这一章就来回答那个每天写无数遍、却很少深究的问题:当你敲下 `import numpy`,从这一刻到 `numpy` 这个名字出现在你的命名空间里,CPython 究竟做了什么? + +答案分成清晰的几层:`import` 语句其实是**一次函数调用**;调用先查一道**缓存**(`sys.modules`);缓存未命中,才真正去**查找并加载**——而查找/加载这套活,交给了用 Python 写的 `importlib`,由一组 **finder** 和 **loader** 按协议完成。我们逐层拆开。 + +## import 语句的真身:调用 __import__ + +先看 `import` 编译成了什么。`import os` 的字节码骨架是: + +``` + LOAD_CONST 0 # level(相对导入层级,绝对导入为 0) + LOAD_CONST None # fromlist(import os 时为 None) + IMPORT_NAME os # ★ 真正干活的指令 + STORE_NAME os # 把得到的模块对象绑定到名字 os +``` + +核心是 `IMPORT_NAME`。而它做的第一件事出人意料——**去 `builtins` 里取出 `__import__` 函数来调用**: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L4722) + +```c +// Python/ceval.c —— import_name(IMPORT_NAME 的实现,精简) +import_func = _PyDict_GetItemId(f->f_builtins, &PyId___import__); // 取 builtins.__import__ +if (import_func == NULL) { PyErr_SetString(...); return NULL; } +// 快路径:未被覆盖的 __import__ 直接调底层 C 实现 +if (import_func == interp->import_func) { + return PyImport_ImportModuleLevelObject(name, f->f_globals, f->f_locals, fromlist, ilevel); +} +// 否则当成普通函数调用(支持自定义 __import__) +res = _PyObject_FastCall(import_func, stack, 5); +``` + +这揭示了一个常被忽视的事实:**`import X` 本质等价于 `X = __import__('X', ...)`**。`__import__` 是个普通的内建函数,因此**可以被替换**——这正是一些导入钩子、沙箱、惰性导入库的工作原理: + +![import 语句 = 调用 __import__](import-is-call.svg) + +```python +>>> import builtins +>>> original = builtins.__import__ +>>> def traced(name, *args, **kw): +... print("正在导入:", name) +... return original(name, *args, **kw) +... +>>> builtins.__import__ = traced +>>> import json # 触发我们的钩子 +正在导入: json +>>> builtins.__import__ = original # 还原 +``` + +`from os import getcwd` 则多一步:`IMPORT_NAME` 带上 `fromlist=('getcwd',)` 拿到模块,再用 `IMPORT_FROM` 从模块里取出 `getcwd` 这个属性;`from os import *` 走的是 `IMPORT_STAR`。但它们的起点都是同一个 `IMPORT_NAME`: + +![import 的几种字节码](import-bytecode.svg) + +## 第一道关卡:sys.modules 缓存 + +未被覆盖时,`IMPORT_NAME` 落到 C 实现 `PyImport_ImportModuleLevelObject`。它在真正去加载之前,先查一道至关重要的缓存——**`sys.modules`**,一个「模块名 → 模块对象」的字典: + +`源文件:`[Python/import.c](https://github.com/python/cpython/blob/v3.7.0/Python/import.c#L1671) + +```c +// Python/import.c —— PyImport_ImportModuleLevelObject(精简主干) +abs_name = resolve_name(name, globals, level); // 解析成绝对模块名 +mod = _PyImport_GetModule(abs_name); // ★ 先查 sys.modules +if (mod != NULL && mod != Py_None) { + // 命中缓存:直接用,绝不重复加载 +} +else { + mod = import_find_and_load(abs_name); // 未命中:才去查找并加载 +} +``` + +![sys.modules:导入的第一道缓存](sys-modules-cache.svg) + +这道缓存解释了几个日常现象: + +- **模块顶层代码只执行一次**。第二次 `import os`,命中缓存直接返回,`os.py` 的顶层代码不会再跑: + +```python +>>> import sys +>>> 'json' in sys.modules +False +>>> import json +>>> 'json' in sys.modules # 加载后进了缓存 +True +>>> import json as j +>>> j is sys.modules['json'] # 再次 import 拿到的是缓存里同一个对象 +True +``` + +- **循环导入能「半工作」**。加载一个模块时,CPython 会**先把半成品模块放进 `sys.modules`、再执行它的代码**。于是当 A 导入 B、B 又回头导入 A 时,B 拿到的是那个还没执行完的 A 半成品——不会无限递归,但可能读到尚未定义的名字。这个「先登记后执行」的顺序是循环导入行为的根源,下面加载流程里还会再点到。 + +## 缓存未命中:C 把活儿交给 importlib + +缓存没命中,`import_find_and_load` 出场。它的实现短得惊人——**把活儿原样转交给用 Python 写的 `importlib`**: + +`源文件:`[Python/import.c](https://github.com/python/cpython/blob/v3.7.0/Python/import.c#L1615) + +```c +// Python/import.c —— import_find_and_load(精简) +PyInterpreterState *interp = PyThreadState_GET()->interp; +mod = _PyObject_CallMethodIdObjArgs(interp->importlib, // 上一章自举出的 importlib + &PyId__find_and_load, // 调它的 _find_and_load + abs_name, interp->import_func, NULL); +return mod; +``` + +这是一处精彩的分工:**C 层只管「查缓存」这种性能敏感的快路径,真正复杂多变的查找/加载逻辑,全用 Python 写在 `importlib._bootstrap` 里**。用 Python 实现 import,让它易读、易改、易扩展——代价(每次加载的开销)由前面的 `sys.modules` 缓存兜住了。 + +![C 把查找加载交给 importlib](find-and-load-handoff.svg) + +## finder 与 loader:两段式加载协议 + +进了 `importlib`,`_find_and_load` 把「导入一个模块」拆成**两段**,由两类角色分工: + +- **finder(查找器)**:回答「这个模块在哪、该用什么方式加载」,产出一个 **`ModuleSpec`**(导入规格,内含一个 loader); +- **loader(加载器)**:回答「把模块**真正建出来**」,执行模块代码、填充模块对象。 + +![find_spec 与 exec_module 两段式](finder-loader-protocol.svg) + +整个流程是这样(`importlib._bootstrap` 的逻辑,简化): + +```python +# importlib._bootstrap._find_and_load 的骨架(示意) +def _find_and_load(name, import_): + spec = _find_spec(name, ...) # ① 遍历 finder 找到 spec + module = module_from_spec(spec) # ② 按 spec 造出空模块对象 + sys.modules[name] = module # ③ ★ 先登记,再执行(循环导入靠这步) + spec.loader.exec_module(module) # ④ 在模块的 __dict__ 里执行其代码 + return module +``` + +第 ④ 步 `exec_module` 把模块源码编译成 code object、在模块自己的名字空间(`module.__dict__`)里跑一遍——这就回到了前面几部分讲的编译与求值:**模块的顶层代码,也是在一个帧里执行的**,只不过那个帧的全局名字空间就是这个模块的 `__dict__`。注意第 ③ 步在 ④ **之前**,这正是上一节「先登记后执行」的落点。 + +## meta_path:到哪去找 finder + +那么 `_find_spec` 是怎么找到合适 finder 的?它依次问 **`sys.meta_path`** 上的每一个 finder:「你认识这个模块吗?」谁先返回非空的 spec,就用谁。默认的 `sys.meta_path` 上有三位: + +![meta_path 上的三个查找器](meta-path-finders.svg) + +```python +>>> import sys +>>> sys.meta_path +[, + , + ] +``` + +- **`BuiltinImporter`**——找**内建模块**(编译进解释器的 C 模块,如 `sys`、`builtins`); +- **`FrozenImporter`**——找**冻结模块**(字节码固化进二进制的,如上一章的 `importlib._bootstrap`); +- **`PathFinder`**——找**文件系统上的模块**,也就是我们绝大多数 `import` 走的路。 + +`PathFinder` 是重头。它遍历 **`sys.path`**(那串目录列表),对每个目录借助 `sys.path_hooks` 取得一个 **`FileFinder`**(并缓存在 `sys.path_importer_cache` 里避免重复扫描);`FileFinder` 在目录里按后缀匹配,再挑选对应的 loader: + +- `.py` 源码文件 → `SourceFileLoader`(编译后执行,并写出 `.pyc` 缓存); +- `.pyc` 字节码文件 → `SourcelessFileLoader`; +- `.so` / `.pyd` 扩展模块 → `ExtensionFileLoader`。 + +```python +>>> import json +>>> json.__spec__.loader # json 由源码加载器加载 +<_frozen_importlib_external.SourceFileLoader object at 0x...> +>>> json.__file__ # 它来自这个文件 +'/usr/lib/python3.7/json/__init__.py' +``` + +这套「meta_path → PathFinder → sys.path → FileFinder → loader」的链条,就是 `import` 能在你机器上**找到**模块的全部秘密。想自定义导入行为(从 zip、从网络、从加密文件加载),只需往 `sys.meta_path` 或 `sys.path_hooks` 里插入自己的 finder——这就是 import 系统可扩展性的由来。 + +## 包:带 __init__.py 的目录 + +模块是单个文件,**包(package)则是一个目录**——准确说,是一个带 `__init__.py` 的目录。导入包时,`__init__.py` 作为包的「模块代码」被执行;它有一个特殊属性 **`__path__`**,是一个目录列表,引导**子模块**的查找: + +```python +>>> import json +>>> json.__path__ # 包才有 __path__,子模块到这些目录里找 +['/usr/lib/python3.7/json'] +>>> import json.decoder # 到 json.__path__ 指的目录里找 decoder +``` + +`import json.decoder` 会逐级导入:先导入 `json`(执行其 `__init__.py`),再在 `json.__path__` 指引下找到并导入 `decoder` 子模块。每一级都走前面那套完整流程、也各自进 `sys.modules` 缓存。 + +## 串起来:加载一个模块的全过程 + +把整章串成一条线,`import os`(假设首次导入)的完整旅程是: + +![加载一个模块的全过程](module-exec.svg) + +1. `IMPORT_NAME` 取出 `builtins.__import__` 并调用; +2. 落到 C 的 `PyImport_ImportModuleLevelObject`,解析出绝对名 `os`,查 **`sys.modules`**——未命中; +3. 转交 `importlib._find_and_load`; +4. `_find_spec` 遍历 **`sys.meta_path`**,`PathFinder` 在 `sys.path` 里找到 `os.py`,产出带 `SourceFileLoader` 的 **spec**; +5. 按 spec 造出空的 `os` 模块对象,**先放进 `sys.modules`**; +6. `loader.exec_module` 把 `os.py` 编译成字节码、在 `os.__dict__` 里执行(一个以模块字典为全局名字空间的帧); +7. 模块对象返回,`STORE_NAME` 把它绑定到当前名字空间的 `os`。 + +下次再 `import os`,流程在第 2 步就因缓存命中而返回——这就是为什么导入「第一次慢、后来快」。 + +--- + +小结一下模块与 import 机制: + +- **`import X` 本质是 `X = __import__('X', ...)`**,由 `IMPORT_NAME` 取 `builtins.__import__` 来调用——因此 `__import__` 可被替换以定制导入;`from ... import` 再加 `IMPORT_FROM`; +- 加载前先查 **`sys.modules`** 缓存:命中即返回,这是「顶层代码只跑一次」「循环导入半工作」的根源; +- 未命中时 C 层 `import_find_and_load` 把活儿**交给用 Python 写的 `importlib`**——C 管缓存快路径,复杂逻辑交给易改的 Python; +- `importlib` 用**两段式协议**加载:**finder** 产出 `ModuleSpec`(`find_spec`),**loader** 真正建模块(`exec_module`);中间「先登记进 `sys.modules`,再执行代码」支撑了循环导入; +- `_find_spec` 遍历 **`sys.meta_path`** 上的 `BuiltinImporter` / `FrozenImporter` / `PathFinder`;`PathFinder` 走 **`sys.path`** → `FileFinder` → 按后缀选 loader(源码/字节码/扩展); +- **包**是带 `__init__.py` 的目录,靠 `__path__` 引导子模块查找; +- 向 `sys.meta_path` / `sys.path_hooks` 注入自定义 finder,即可扩展导入来源——这是 import 系统灵活性的根基。 + +import 让多个模块协作,但当多个**线程**同时跑起来,又会怎样?那把传说中既保平安、又遭诟病的 **GIL**,到底是什么、为什么存在?下一章就来拆 **多线程与 GIL**。 diff --git a/docs/runtime/import-system/meta-path-finders.svg b/docs/runtime/import-system/meta-path-finders.svg new file mode 100644 index 0000000..4e2bea6 --- /dev/null +++ b/docs/runtime/import-system/meta-path-finders.svg @@ -0,0 +1,58 @@ + +meta_path 上的三个查找器与 sys.path 搜索 +_find_spec 依次问 sys.meta_path 上的每个 finder 你认识这个模块吗,谁先返回非空 spec 就用谁。默认有三位:BuiltinImporter 找编译进解释器的内建模块如 sys;FrozenImporter 找字节码固化进二进制的冻结模块;PathFinder 找文件系统上的模块,是绝大多数 import 走的路。PathFinder 遍历 sys.path 每个目录,借 sys.path_hooks 取得 FileFinder 并缓存,FileFinder 按后缀匹配选 loader:.py 用 SourceFileLoader,.pyc 用 SourcelessFileLoader,.so 用 ExtensionFileLoader。 + + + + + + + + + + +sys.meta_path:依次问每个查找器 + + + + BuiltinImporter + 内建模块(sys…) + + + FrozenImporter + 冻结模块 + + + PathFinder + 文件系统(最常走) + + + + + +PathFinder 的内部 + + +遍历 sys.path 目录 + +path_hooks → +FileFinder(缓存) + + +FileFinder 按后缀挑 loader: + + + .pySourceFileLoader + + .pycSourceless… + + .so / .pydExtensionFile… + + +向 sys.meta_path / sys.path_hooks 注入自定义 finder → 扩展导入来源 + diff --git a/docs/runtime/import-system/module-exec.svg b/docs/runtime/import-system/module-exec.svg new file mode 100644 index 0000000..dc3a796 --- /dev/null +++ b/docs/runtime/import-system/module-exec.svg @@ -0,0 +1,40 @@ + +加载一个模块的全过程 +import os 首次导入的完整旅程:IMPORT_NAME 取出 builtins.__import__ 并调用;落到 C 的 PyImport_ImportModuleLevelObject,解析出绝对名,查 sys.modules 未命中;转交 importlib._find_and_load;_find_spec 遍历 sys.meta_path,PathFinder 在 sys.path 里找到 os.py 产出带 SourceFileLoader 的 spec;按 spec 造出空模块对象先放进 sys.modules;loader.exec_module 把 os.py 编译成字节码在 os.__dict__ 里执行;模块对象返回,STORE_NAME 绑定到名字。下次再 import 在查缓存这步就返回。 + + + + + + + + + + + +import os 首次导入的完整旅程 + + + ① IMPORT_NAME → __import__ + ② C:解析绝对名,查 sys.modules +   未命中 → 转交 importlib + ③ _find_spec 遍历 meta_path + ④ PathFinder 在 sys.path 找到 os.py + + + + ⑤ 造空模块,先放进 sys.modules + ⑥ exec_module:编译 + 执行  在 os.__dict__(模块帧)里跑 + ⑦ 返回模块,STORE_NAME os + + + + + +下次 import os 在第 ② 步缓存命中即返回 —— 第一次慢,后来快 + diff --git a/docs/runtime/import-system/sys-modules-cache.svg b/docs/runtime/import-system/sys-modules-cache.svg new file mode 100644 index 0000000..b1ea4f9 --- /dev/null +++ b/docs/runtime/import-system/sys-modules-cache.svg @@ -0,0 +1,49 @@ + +sys.modules:导入的第一道缓存 +真正去加载之前,先查 sys.modules 缓存,一个模块名到模块对象的字典。命中就直接返回缓存对象,绝不重复加载,这是模块顶层代码只执行一次、再次 import 拿到同一对象的原因。未命中才去查找并加载。这道缓存也支撑了循环导入:加载时先把半成品模块放进缓存再执行代码,所以 A 导入 B、B 回头导入 A 时拿到还没执行完的 A 半成品,不会无限递归。 + + + + + + + + + + + + +先查 sys.modules,命中即返回 + + +import os +请求导入 + + + +sys.modules +名 → 模块对象 + +{'json': <mod>, …} + + + + +命中 +直接返回,不重复加载 + + + + +未命中 +才去查找并加载 + + +顶层代码只执行一次 · 再次 import 拿到同一对象 +加载时「先登记半成品再执行」→ 循环导入能半工作 + diff --git a/docs/runtime/initialization/bootstrap-order.svg b/docs/runtime/initialization/bootstrap-order.svg new file mode 100644 index 0000000..48aa04c --- /dev/null +++ b/docs/runtime/initialization/bootstrap-order.svg @@ -0,0 +1,40 @@ + +自举的依赖顺序:类型 → sys/builtins → import +阶段一的初始化调用顺序是一条严格的依赖链。类型系统打头,_Py_ReadyTypes 把 int、str、type、object 等所有内建类型就绪,没有类型连一个对象都造不出来。有了类型才能造出 sys、builtins 两个模块对象,填进 print、len、内建异常。前两者就绪,才有底气启动 importlib。任何后者都依赖前者已经就绪。 + + + + + + + + + + +自举依赖链:后者都站在前者肩上 + + +① 类型系统 +_Py_ReadyTypes +int/str/type/object + + +② sys / builtins +print · len +内建异常 + + +③ import +importlib +frozen 自举 + + + + +没有类型,造不出对象;没有 sys/builtins,启动不了 import + diff --git a/docs/runtime/initialization/builtins-namespace.svg b/docs/runtime/initialization/builtins-namespace.svg new file mode 100644 index 0000000..934a6cb --- /dev/null +++ b/docs/runtime/initialization/builtins-namespace.svg @@ -0,0 +1,48 @@ + +builtins 名字空间的来历,LEGB 的 B +LEGB 查找的最后一层内建,就来自这里。阶段一的 _PyBuiltin_Init 造出 builtins 模块,把 print、len、range、内建异常填进去,再把它的 __dict__ 存进 interp->builtins。之后每个帧创建时,f_builtins 都指向这同一份字典。所以任何代码、任何函数里都能直接用 print、len。LEGB 的 B 不是魔法,就是初始化时挂在解释器上、再分发给每个帧的那份 builtins.__dict__。 + + + + + + + + + + +LEGB 的 B 从哪来 + + + +builtins 模块 +_PyBuiltin_Init 造 + +print · len +range · 内建异常 +__dict__ + + + +interp->builtins +挂在解释器上 + + + +帧 A +f_builtins → + +帧 B +f_builtins → + + + + + +每个帧的 f_builtins 都指向同一份 builtins.__dict__ —— 所以 print、len 触手可及 + diff --git a/docs/runtime/initialization/frozen-importlib.svg b/docs/runtime/initialization/frozen-importlib.svg new file mode 100644 index 0000000..42c5da6 --- /dev/null +++ b/docs/runtime/initialization/frozen-importlib.svg @@ -0,0 +1,48 @@ + +frozen importlib 打破鸡生蛋难题 +启动 import 系统有个鸡生蛋难题:importlib 是用 Python 写的,可启动它本身又需要 import。CPython 的破解办法是 frozen 冻结模块:把 importlib._bootstrap 的字节码在编译 CPython 时就固化进可执行文件,存为 Python/importlib.h 里一个巨大的字节数组。启动时无需从磁盘读文件、无需走 import 流程,直接把这段冻结的字节码喂给求值循环执行,import 系统就凭空启动了。 + + + + + + + + + + +鸡生蛋:启动 import 又需要 import + + + +难题 +importlib 用 Python 写 +可加载它本身又得 import + + + +破解:frozen 冻结模块 + + +编译 CPython 时 +importlib._bootstrap + + +固化为字节数组 +Python/importlib.h + + +启动时直接执行 +无需读盘、无需 import + + + + + +把冻结字节码喂给求值循环 → import 凭空启动 + diff --git a/docs/runtime/initialization/index.md b/docs/runtime/initialization/index.md new file mode 100644 index 0000000..190cf17 --- /dev/null +++ b/docs/runtime/initialization/index.md @@ -0,0 +1,216 @@ +# Python 运行环境初始化 + +前四部分,我们把对象、编译、虚拟机一路拆了个透:你写的 `foo.py` 怎样被编译成字节码,又怎样被求值循环一条条执行。但有个问题一直被悄悄略过——**在 `foo.py` 的第一行真正运行之前,是谁把舞台搭好的?** + +`type`、`int`、`str` 这些内建类型从哪来?`print`、`len` 这些内建函数为什么张口就能用?`import` 凭什么能找到模块?`sys.argv` 又是谁填的?答案是:当你敲下 `python foo.py`,CPython 在执行你的代码前,先默默做了一大套**初始化**工作。这一章就跟着源码,从 `main()` 一路走到「你的第一行代码」。 + +## 从 main() 到你的代码:全景 + +启动的主线非常清晰,可以先记住这条流水线: + +![启动流水线](startup-pipeline.svg) + +- **`main()`** —— 可执行文件的入口(`Programs/python.c`),它转手调用 `Py_Main`; +- **`Py_Main`** —— 解析命令行参数(`-c`、`-m`、文件名……),然后做两件大事:先**初始化运行环境**,再**运行你的代码**; +- **`Py_Initialize`**(及其底层的 `_Py_InitializeCore` / `_Py_InitializeMainInterpreter`)—— 把整个 Python 运行环境从无到有搭起来,这是本章的主角; +- **运行代码** —— 视参数不同,跑文件、跑 `-c` 命令、跑 `-m` 模块,或进入交互式 REPL。 + +初始化做完,舞台就绪,你的代码才登场。下面我们钻进「搭舞台」这一步。 + +## 三层状态:运行环境的家底放在哪 + +搭舞台前先得有个「放东西的地方」。CPython 把运行期状态组织成**三个嵌套的层次**,从全局到具体: + +`源文件:`[Include/internal/pystate.h](https://github.com/python/cpython/blob/v3.7.0/Include/internal/pystate.h#L78) + +```c +// Include/internal/pystate.h —— 顶层运行时状态(全局唯一) +typedef struct pyruntimestate { + int initialized; + int core_initialized; // 两个阶段的完成标志(见下文) + PyThreadState *finalizing; + struct pyinterpreters { + PyInterpreterState *head; // 所有解释器串成链表 + PyInterpreterState *main; // 主解释器 + } interpreters; + struct _gc_runtime_state gc; // 垃圾回收状态 + struct _ceval_runtime_state ceval; // 求值循环 / GIL 状态 + struct _gilstate_runtime_state gilstate; +} _PyRuntimeState; +``` + +这三层是: + +- **`_PyRuntimeState`**——进程级、全局唯一的 `_PyRuntime`。装着 GC、GIL 这类「整个进程共享」的东西,以及所有解释器的链表。 +- **`PyInterpreterState`**——解释器级。一个进程里可以有多个解释器(子解释器),各自独立,互不共享模块与名字空间: + +`源文件:`[Include/pystate.h](https://github.com/python/cpython/blob/v3.7.0/Include/pystate.h#L110) + +```c +// Include/pystate.h —— 解释器级状态(节选) +typedef struct _is { + struct _is *next; // 链到下一个解释器 + struct _ts *tstate_head; // 该解释器下的线程链表 + PyObject *modules; // sys.modules:已加载模块表 + PyObject *sysdict; // sys 模块的 __dict__ + PyObject *builtins; // builtins 模块的 __dict__(print、len 都在这) + PyObject *importlib; // import 机制的实现 + _PyFrameEvalFunction eval_frame; // 默认就是 _PyEval_EvalFrameDefault +} PyInterpreterState; +``` + +- **`PyThreadState`**——线程级。每个 Python 线程一个,装着**当前帧** `frame`、递归深度、以及上一章反复出现的异常状态 `curexc_*` / `exc_info`: + +`源文件:`[Include/pystate.h](https://github.com/python/cpython/blob/v3.7.0/Include/pystate.h#L209) + +```c +// Include/pystate.h —— 线程级状态(节选) +typedef struct _ts { + struct _ts *next; + PyInterpreterState *interp; // 我属于哪个解释器 + struct _frame *frame; // 当前正在执行的帧(帧栈的栈顶) + int recursion_depth; + ...... +} PyThreadState; +``` + +![三层状态的嵌套](state-hierarchy.svg) + +记住这张图:**一个 `_PyRuntime`,下面挂若干 `PyInterpreterState`,每个解释器下面挂若干 `PyThreadState`,每个线程状态指着它当前的帧**。初始化,本质就是把这三层从零建起来、再填满内容。 + +## 两阶段初始化:先打地基,再盖房子 + +`Py_Initialize` 内部分成**两个阶段**,这是 3.7 引入的清晰划分: + +![两阶段初始化](two-phase-init.svg) + +**阶段一 `_Py_InitializeCore`**——打地基。建主解释器、第一个线程状态、创建 GIL,把最底层的类型系统和核心模块立起来: + +`源文件:`[Python/pylifecycle.c](https://github.com/python/cpython/blob/v3.7.0/Python/pylifecycle.c#L598) + +```c +// Python/pylifecycle.c —— _Py_InitializeCore(精简,只留主干) +_PyRuntime_Initialize(); // 初始化全局 _PyRuntime +interp = PyInterpreterState_New(); // 建主解释器 +tstate = PyThreadState_New(interp); // 建第一个线程状态 +PyThreadState_Swap(tstate); +PyEval_InitThreads(); // 创建 GIL +_Py_ReadyTypes(); // ★ 把所有内建类型「就绪」 +_PyLong_Init(); _PyFloat_Init(); ... // 小整数池、浮点等 +interp->modules = PyDict_New(); // sys.modules +_PySys_BeginInit(&sysmod); // sys 模块(半成品) +_PyUnicode_Init(); // 字符串实现 +bimod = _PyBuiltin_Init(); // builtins 模块 +interp->builtins = PyModule_GetDict(bimod); +_PyExc_Init(bimod); // 内建异常 +_PyImport_Init(interp); // import 子系统 +initimport(interp, sysmod); // ★ 启动 frozen importlib +_PyRuntime.core_initialized = 1; // 地基完成 +``` + +**阶段二 `_Py_InitializeMainInterpreter`**——盖房子。在地基之上装好「面向用户」的部分:基于文件系统的完整 import、`__main__` 模块、标准输入输出流、信号处理、`site`: + +`源文件:`[Python/pylifecycle.c](https://github.com/python/cpython/blob/v3.7.0/Python/pylifecycle.c#L790) + +```c +// Python/pylifecycle.c —— _Py_InitializeMainInterpreter(精简) +_PySys_EndInit(interp->sysdict, &interp->config); // 补全 sys(argv、path 等) +initexternalimport(interp); // 基于文件系统的 import(importlib._bootstrap_external) +initfsencoding(interp); // 文件系统编码 +initsigs(); // 信号处理 +add_main_module(interp); // ★ 创建 __main__ 模块 +init_sys_streams(interp); // sys.stdin / stdout / stderr +_PyRuntime.initialized = 1; // 完全初始化 +if (!Py_NoSiteFlag) initsite(); // 加载 site(处理 site-packages) +``` + +为什么非要分两阶段?关键在阶段一末尾那个 `initimport`——它要启动 `importlib`,可 `importlib` 本身是用 Python 写的,得先有一套能跑 Python 的基础环境(类型、`sys`、`builtins`)才行。于是:**先用阶段一搭出一个「最小可运行的 Python」,再用它去启动 import 系统,最后阶段二才敢去 import 标准库**。 + +## 自举的核心:先有类型,再有模块,最后有 import + +阶段一里那串初始化调用,顺序不是随便排的,而是一条严格的**依赖链**。任何后者都依赖前者已经就绪: + +![自举的依赖顺序](bootstrap-order.svg) + +- **类型系统打头**:`_Py_ReadyTypes()` 把 `int`、`str`、`type`、`object` 等所有内建类型「就绪」(填好 `tp_dict`、计算 MRO——第二部分类型对象章讲过)。没有类型,连一个对象都造不出来,后面无从谈起。 +- **再立 `sys` 与 `builtins`**:有了类型,才能造出 `sys`、`builtins` 这两个模块对象,把 `print`、`len`、内建异常等填进去。 +- **最后启动 import**:前两者就绪,才有底气启动 `importlib`。 + +这里藏着一个经典的「鸡生蛋」难题:**`importlib` 是用 Python 写的,可启动它本身又需要 `import`**。CPython 的破解办法是 **frozen(冻结)模块**——把 `importlib._bootstrap` 的字节码在**编译 CPython 时**就固化进可执行文件(见 `Python/importlib.h`,那是一个巨大的字节数组)。启动时无需从磁盘读文件、无需走 import 流程,直接把这段冻结的字节码喂给求值循环执行,import 系统就「凭空」启动了: + +![frozen importlib 打破鸡生蛋](frozen-importlib.svg) + +```python +>>> import importlib._bootstrap as b +>>> b.__spec__.origin # 来历:frozen,而非某个 .py 文件 +'frozen' +>>> import sys +>>> 'importlib' in sys.modules # 启动期就已就位 +True +``` + +`initimport` 装好的是**最底层的内建/frozen import**(能 import 内建模块);阶段二的 `initexternalimport` 再装上**基于文件系统的 import**(`importlib._bootstrap_external`,能从磁盘上的 `.py`/`.pyc` 加载)。import 的完整机制,是下一章的主题,这里先知道它是这样被「自举」起来的。 + +## 内建名字空间:LEGB 里的 B 从哪来 + +还记得「一般表达式与名字空间」一章里的 LEGB 查找吗?取一个名字时按**局部 → 闭包 → 全局 → 内建**的顺序找。当时我们说最后一层「内建」就是 `builtins`,但没说它从哪来。现在答案揭晓: + +正是阶段一的 `_PyBuiltin_Init()` 造出 `builtins` 模块,把它的 `__dict__` 存进 `interp->builtins`。之后**每个帧创建时,`f_builtins` 都指向它**——所以任何代码、任何函数里,`print`、`len`、`range` 总是触手可及: + +![builtins 名字空间的来历](builtins-namespace.svg) + +```python +>>> import builtins +>>> builtins.len is len # 你用的 len 就来自 builtins 模块 +True +>>> import sys +>>> ns = sys._getframe().f_builtins # 当前帧的内建名字空间 +>>> ns is builtins.__dict__ # 正是 builtins 的 __dict__ +True +``` + +LEGB 的 B 不是什么魔法,它就是初始化时挂在解释器上、再分发给每个帧的那一份 `builtins.__dict__`。 + +## 运行你的代码:__main__ 登场 + +舞台终于搭好。`Py_Main` 接下来根据命令行参数运行你的代码,而代码运行在哪个名字空间里?答案是 **`__main__` 模块**——阶段二的 `add_main_module` 早已把它建好: + +`源文件:`[Python/pylifecycle.c](https://github.com/python/cpython/blob/v3.7.0/Python/pylifecycle.c#L1455) + +```c +// Python/pylifecycle.c —— add_main_module(精简) +m = PyImport_AddModule("__main__"); // 创建名为 __main__ 的模块 +d = PyModule_GetDict(m); // 它的 __dict__ 就是顶层代码的全局名字空间 +if (PyDict_GetItemString(d, "__builtins__") == NULL) { + PyObject *bimod = PyImport_ImportModule("builtins"); + PyDict_SetItemString(d, "__builtins__", bimod); // 把内建塞进去 +} +``` + +无论你是 `python foo.py`、`python -c "..."` 还是直接进 REPL,顶层代码都在 `__main__` 模块的 `__dict__` 里执行——它就是顶层那层「全局名字空间」。这也解开了那句无处不在的惯用法: + +![__main__ 与代码的运行](run-main.svg) + +```python +# foo.py +print(__name__) # 直接运行:打印 __main__ +if __name__ == "__main__": # “我是被直接运行的,不是被 import 的” + main() +``` + +直接运行的脚本,其模块名被设成 `"__main__"`;而被 `import` 时,模块名是它的文件名。`if __name__ == "__main__"` 这个判断,本质就是在问「我是不是那个 `add_main_module` 建出来的顶层模块」。 + +代码跑完(或抛出未捕获的异常),`Py_Main` 调用 `Py_FinalizeEx` 收尾:执行注册的退出函数、清理模块、回收对象、销毁线程状态与解释器——把初始化建起来的一切,反向拆掉。 + +--- + +小结一下运行环境的初始化: + +- 启动主线是 `main() → Py_Main →`(**初始化** `Py_Initialize` + **运行代码**); +- 运行期状态分**三层**:全局唯一的 **`_PyRuntimeState`**(GC、GIL、解释器链表)、解释器级的 **`PyInterpreterState`**(modules、sys、builtins、importlib)、线程级的 **`PyThreadState`**(当前帧、异常状态); +- 初始化分**两阶段**:`_Py_InitializeCore` 打地基(解释器/线程状态、GIL、类型系统、sys/builtins、frozen import),`_Py_InitializeMainInterpreter` 盖房子(文件系统 import、`__main__`、标准流、signals、site); +- 顺序是一条**自举依赖链**:类型 → sys/builtins → import;其中 `importlib` 用 **frozen 字节码**打破「启动 import 又需要 import」的鸡生蛋难题; +- **LEGB 的 B** 就是 `_PyBuiltin_Init` 造出、挂在 `interp->builtins`、再分发给每个帧 `f_builtins` 的那份字典; +- 你的代码运行在 **`__main__`** 模块的名字空间里,`if __name__ == "__main__"` 即由此而来。 + +初始化时我们反复碰到 `import`——它被自举起来,却还没细看它**怎么工作**:`import numpy` 时,CPython 如何找到、加载、缓存这个模块?下一章就深入 **模块与 import 机制**。 diff --git a/docs/runtime/initialization/run-main.svg b/docs/runtime/initialization/run-main.svg new file mode 100644 index 0000000..3fed097 --- /dev/null +++ b/docs/runtime/initialization/run-main.svg @@ -0,0 +1,46 @@ + +__main__ 模块与代码的运行 +舞台搭好后,无论你是 python foo.py、python -c 还是进 REPL,顶层代码都在 __main__ 模块的 __dict__ 里执行,这就是顶层那层全局名字空间,由阶段二的 add_main_module 早早建好。直接运行的脚本,其模块名被设成 __main__;被 import 时,模块名是它的文件名。所以 if __name__ == __main__ 这个判断,本质就是在问我是不是那个顶层模块,而不是被 import 进来的。 + + + + + + + + + +你的代码运行在 __main__ 的名字空间里 + + + + python foo.py + python -c "..." + REPL + + + + +__main__ 模块 +add_main_module 建好 + +__dict__ = 顶层全局名字空间 + + + + + + + +__name__ +== "__main__" + + +直接运行 → 模块名设为 "__main__";被 import → 模块名是文件名 +if __name__ == "__main__": 即在问「我是不是那个顶层模块」 + diff --git a/docs/runtime/initialization/startup-pipeline.svg b/docs/runtime/initialization/startup-pipeline.svg new file mode 100644 index 0000000..baf7160 --- /dev/null +++ b/docs/runtime/initialization/startup-pipeline.svg @@ -0,0 +1,43 @@ + +Python 启动流水线 +从 main 到你的代码的启动主线。main 是可执行文件入口,调用 Py_Main。Py_Main 解析命令行参数后做两件大事:先初始化运行环境 Py_Initialize,再运行你的代码。Py_Initialize 底层分为 _Py_InitializeCore 和 _Py_InitializeMainInterpreter 两阶段,把运行环境从无到有搭起来。初始化做完,才视参数运行文件、-c 命令、-m 模块或进入 REPL。 + + + + + + + + + + +从 main() 到你的第一行代码 + + +main() +入口 + + +Py_Main +解析参数 + + +Py_Initialize +搭舞台(两阶段) +本章主角 + + +运行你的代码 +文件 / -c / -m / REPL + + + + + +初始化做完,舞台就绪,你的代码才登场 + diff --git a/docs/runtime/initialization/state-hierarchy.svg b/docs/runtime/initialization/state-hierarchy.svg new file mode 100644 index 0000000..e10a230 --- /dev/null +++ b/docs/runtime/initialization/state-hierarchy.svg @@ -0,0 +1,46 @@ + +三层运行期状态的嵌套 +运行期状态分三个嵌套层次。最外层是全局唯一的 _PyRuntimeState,装着 GC、GIL 这类整个进程共享的东西,以及所有解释器的链表。中间是解释器级 PyInterpreterState,一个进程可以有多个,各自有独立的 modules、sysdict、builtins、importlib。最内层是线程级 PyThreadState,每个 Python 线程一个,装着当前帧 frame、递归深度、异常状态。一个 runtime 下挂若干 interpreter,每个 interpreter 下挂若干 thread,每个 thread 指着它当前的帧。 + + + + + + + + + +runtime ⊃ interpreter ⊃ thread + + + +_PyRuntimeState(全局唯一 _PyRuntime) +GC · GIL · ceval · 解释器链表 + + + +PyInterpreterState(解释器,可多个) +modules · sysdict · builtins · importlib + + + +PyThreadState 线程 1 +frame(当前帧) +recursion_depth +curexc_* / exc_info + +→ 帧栈(栈顶) + + +PyThreadState 线程 2 +frame(当前帧) +recursion_depth +curexc_* / exc_info + +→ 帧栈(栈顶) + diff --git a/docs/runtime/initialization/two-phase-init.svg b/docs/runtime/initialization/two-phase-init.svg new file mode 100644 index 0000000..4f97a2f --- /dev/null +++ b/docs/runtime/initialization/two-phase-init.svg @@ -0,0 +1,51 @@ + +两阶段初始化 +Py_Initialize 内部分两阶段。阶段一 _Py_InitializeCore 打地基:建主解释器、第一个线程状态、创建 GIL,把最底层的类型系统就绪,立起 sys 和 builtins 模块,启动 frozen importlib。阶段二 _Py_InitializeMainInterpreter 盖房子:补全 sys,装上基于文件系统的 import,创建 __main__ 模块,建标准输入输出流,装信号处理和 site。分两阶段的原因:阶段一末尾要启动用 Python 写的 importlib,必须先有一个最小可运行的 Python 环境。 + + + + + + + + + +两阶段:先打地基,再盖房子 + + + +① _Py_InitializeCore +打地基 + + 建主解释器 + 线程状态 + PyEval_InitThreads(GIL) + _Py_ReadyTypes(类型系统) + sys(半成品) + builtins + 内建异常 + initimport(frozen import) + +core_initialized = 1 + + + +② _Py_InitializeMainInterpreter +盖房子 + + _PySys_EndInit(argv、path) + initexternalimport(文件系统) + initsigs(信号) + add_main_module(__main__) + init_sys_streams(标准流) + initsite(site-packages) + +initialized = 1 + + + +分两阶段:阶段一先搭出「最小可运行的 Python」,才有底气启动 import、import 标准库 + diff --git a/docs/vm/control-flow/block-stack.svg b/docs/vm/control-flow/block-stack.svg new file mode 100644 index 0000000..2b03dda --- /dev/null +++ b/docs/vm/control-flow/block-stack.svg @@ -0,0 +1,50 @@ + +block 栈:SETUP_LOOP 与 break / continue(Python 3.7) +3.7 里循环用帧的 block 栈来记账。进入循环时 SETUP_LOOP 压一个块,记下循环结束的偏移和当前栈深度。break 不必静态知道循环出口在哪——它顺着 block 栈找到这个块、跳到记录的结束偏移;continue 则跳回循环顶部。循环正常结束时 POP_BLOCK 弹出这个块。 + + + + + + + + + + + +block 栈:break / continue 怎么找到出口(3.7) + + +帧的 f_blockstack + +循环块(SETUP_LOOP 压入) +b_type = SETUP_LOOP +b_handler = 循环结束偏移 +b_level = 入栈时的栈深度 + + + +SETUP_LOOP:进入循环时压入 + +POP_BLOCK:正常结束时弹出 + + + +break → BREAK_LOOP +顺着 block 栈找到循环块, +跳到它记录的「结束偏移」 + + + + +continue → 跳回循环顶部 +重新判断条件 / 取下一项 + +好处:break 无需在编译期算出循环出口的绝对地址,运行期顺着 block 栈即可找到 +(3.8 起改用零开销异常表,SETUP_LOOP 被移除,但思路一脉相承) + diff --git a/docs/vm/control-flow/for-iter.svg b/docs/vm/control-flow/for-iter.svg new file mode 100644 index 0000000..9959723 --- /dev/null +++ b/docs/vm/control-flow/for-iter.svg @@ -0,0 +1,63 @@ + +for 循环靠迭代器协议:GET_ITER 与 FOR_ITER +for x in seq 不直接遍历容器,而是走迭代器协议。GET_ITER 先对可迭代对象调用 iter() 得到一个迭代器,放在栈上。然后 FOR_ITER 反复对迭代器调用 next():拿到值就压栈、交给循环体;某次 next() 抛出 StopIteration 就跳出循环。所以 for 本质是「先 iter,再不断 next 直到 StopIteration」。 + + + + + + + + + + + + + + +for x in seq:先 iter(),再不断 next() + + + +可迭代对象 seq +[1, 2, 3] + + + + +GET_ITER +iter(seq) → 迭代器 + + + + +FOR_ITER +调用迭代器的 next() + + + +拿到值 → 压栈给 x + +循环体(用 x) +STORE_FAST x; ... + + + +JUMP_ABSOLUTE 回到 FOR_ITER + + + + +StopIteration +→ 跳出循环 +JUMPBY 出口 + + +迭代器协议:__iter__ 给出迭代器,__next__ 逐个取值,耗尽时抛 StopIteration +for 对一切可迭代对象(list、dict、文件、生成器…)都用这同一套机制 + diff --git a/docs/vm/control-flow/if-else.svg b/docs/vm/control-flow/if-else.svg new file mode 100644 index 0000000..705ef39 --- /dev/null +++ b/docs/vm/control-flow/if-else.svg @@ -0,0 +1,51 @@ + +if / else 编译成条件跳转 POP_JUMP_IF_FALSE +if x > 0 ... else ... 被编译成:先算出条件压栈,POP_JUMP_IF_FALSE 弹出条件,为真则顺序落入 if 分支,为假则跳到 else 分支。if 分支执行完用 JUMP_FORWARD 跳过 else,避免两段都执行。条件分支的本质就是「按栈顶真假,决定要不要跳过一段指令」。 + + + + + + + + + + + + +if x > 0: y=1 else: y=2 + + + +算条件 x > 0,压栈 +COMPARE_OP + + + +POP_JUMP_IF_FALSE 8 +弹出条件,决定走哪边 + + + + +真 → 顺序落入 + +if 分支:y = 1 +LOAD_CONST 1; STORE + + + +假 → 跳到 8 + +else 分支:y = 2 +LOAD_CONST 2; STORE + + + +if 分支末尾 JUMP_FORWARD 跳过 else → + diff --git a/docs/vm/control-flow/index.md b/docs/vm/control-flow/index.md new file mode 100644 index 0000000..421389a --- /dev/null +++ b/docs/vm/control-flow/index.md @@ -0,0 +1,170 @@ +# 控制流:跳转、循环与迭代器 + +上一章的直线代码,执行起来一条接一条、一往无前。但真实程序有 `if`、`while`、`for`——执行顺序会拐弯、会折返。这一章看虚拟机如何做到这点。 + +答案出奇地简单:**改写「下一条取哪条」**。求值循环里有个指令指针 `next_instr`,正常每执行一条就自动指向下一条;而所有控制流,归根结底都是一类特殊指令——**跳转**——它们直接改写 `next_instr`,让循环下一轮从别处取指令。 + +## 跳转:改写指令指针 + +先看这个核心动作。ceval.c 里取指令靠 `next_instr`,跳转则由两个宏改写它: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L705) + +```c +// Python/ceval.c —— 跳转宏 +#define JUMPTO(x) (next_instr = first_instr + (x) / sizeof(_Py_CODEUNIT)) // 跳到绝对偏移 x +#define JUMPBY(x) (next_instr += (x) / sizeof(_Py_CODEUNIT)) // 相对当前位置跳 x +``` + +`JUMPTO` 把 `next_instr` 设成某个绝对位置,`JUMPBY` 则在当前位置上偏移——前者叫绝对跳转,后者叫相对跳转。无论哪种,效果都是**让下一轮取指令从新位置开始**: + +![跳转改写 next_instr](jump-pointer.svg) + +记住这一点,下面的 `if`、`while`、`for` 就都只是「在合适的时机改写 `next_instr`」的不同套路而已。 + +## if / else:条件跳转 + +`if/else` 用的是**条件跳转**——按栈顶的真假,决定要不要跳。最常见的是 `POP_JUMP_IF_FALSE`:弹出栈顶条件,**为假就跳走,为真就顺序往下**: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2642) + +```c +// Python/ceval.c —— TARGET(POP_JUMP_IF_FALSE)(精简) +PyObject *cond = POP(); // 弹出条件 +if (cond == Py_True) { ...; FAST_DISPATCH(); } // 真:顺序往下(落入 if 分支) +if (cond == Py_False) { ...; JUMPTO(oparg); FAST_DISPATCH(); } // 假:跳到 oparg(else 分支) +err = PyObject_IsTrue(cond); // 一般对象:算真值再决定 +if (err == 0) JUMPTO(oparg); +``` + +把 `if x > 0: y = 1` `else: y = 2` 编译出来,骨架是这样: + +``` + LOAD x; LOAD 0; COMPARE_OP > # 算条件,结果压栈 + POP_JUMP_IF_FALSE →8 # 假 → 跳到偏移 8(else) + LOAD_CONST 1; STORE y # if 分支 + JUMP_FORWARD →10 # 跳过 else + >>8 LOAD_CONST 2; STORE y # else 分支 + >>10 ... # 汇合 +``` + +![if/else 的条件跳转](if-else.svg) + +条件为真时,`POP_JUMP_IF_FALSE` 不跳,自然落入 `if` 分支;执行完用 `JUMP_FORWARD` **跳过** `else`,避免两段都跑。条件为假时,直接跳到偏移 8 的 `else`。一个条件跳转 + 一个无条件跳转,`if/else` 就成了。 + +## while:往回跳形成循环 + +`if` 是往**前**跳(跳过一段)。把跳转方向调转——往**回**跳,就得到了循环。`while x > 0: x -= 1` 的骨架: + +``` + SETUP_LOOP →(循环外) # 进入循环(下一节讲) + >> LOAD x; LOAD 0; COMPARE_OP > # 循环顶部:判断条件 + POP_JUMP_IF_FALSE →(跳出) # 假 → 跳出循环 + LOAD x; LOAD 1; ...; STORE x # 循环体:x -= 1 + JUMP_ABSOLUTE →>> # 往回跳,回到顶部重新判断 + POP_BLOCK +``` + +![while 循环的回跳](while-loop.svg) + +关键就是末尾那条 `JUMP_ABSOLUTE`——它用 `JUMPTO` 把 `next_instr` 改回循环顶部,于是又重新判断条件、再跑一轮。什么时候停?顶部的 `POP_JUMP_IF_FALSE` 一旦发现条件为假,就跳到循环外。**循环 = 条件跳转(决定要不要继续)+ 往回跳(重来一轮)**,再没别的玄机。 + +## for 与迭代器协议 + +`for` 比 `while` 多一层意思:它不靠条件,而是「把一个序列**逐个取完**」。CPython 用**迭代器协议**实现这件事,核心是两条指令 `GET_ITER` 和 `FOR_ITER`。 + +`GET_ITER` 先把可迭代对象变成一个**迭代器**——相当于调用 `iter()`: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2756) + +```c +// Python/ceval.c —— TARGET(GET_ITER) +PyObject *iterable = TOP(); +PyObject *iter = PyObject_GetIter(iterable); // iter(iterable) +SET_TOP(iter); // 用迭代器替换栈顶 +``` + +然后 `FOR_ITER` 反复对这个迭代器调用 `next()`——拿到值就压栈交给循环体,**抛 `StopIteration` 就跳出循环**: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2799) + +```c +// Python/ceval.c —— TARGET(FOR_ITER)(精简) +PyObject *iter = TOP(); +PyObject *next = (*iter->ob_type->tp_iternext)(iter); // 调用迭代器的 __next__ +if (next != NULL) { + PUSH(next); // 取到值 → 压栈(接着 STORE_FAST 给循环变量) + DISPATCH(); +} +if (PyErr_Occurred()) { // 没取到:是 StopIteration 吗 + if (!PyErr_ExceptionMatches(PyExc_StopIteration)) goto error; + PyErr_Clear(); +} +STACKADJ(-1); Py_DECREF(iter); // 迭代正常结束:丢弃迭代器 +JUMPBY(oparg); // 跳到循环出口 +``` + +![for 的迭代器协议](for-iter.svg) + +所以 `for x in seq` 的骨架是:`GET_ITER` 拿到迭代器,`FOR_ITER` 每轮取一个值给 `x`、跑一遍循环体、`JUMP_ABSOLUTE` 跳回 `FOR_ITER` 再取下一个;直到某次 `next()` 抛 `StopIteration`,`FOR_ITER` 就 `JUMPBY` 到循环外。 + +这套协议我们可以在 Python 层完全手动复现——`for` 不过是它的语法糖: + +```python +>>> it = iter([10, 20]) # 对应 GET_ITER +>>> type(it).__name__ +'list_iterator' +>>> next(it), next(it) # 对应 FOR_ITER 每轮取值 +(10, 20) +>>> next(it) # 取尽 → StopIteration(FOR_ITER 据此跳出) +Traceback (most recent call last): + ... +StopIteration +``` + +正因为 `for` 只认「迭代器协议」,任何实现了 `__iter__` / `__next__` 的对象都能被 `for` 遍历——list、dict、文件、生成器,乃至自定义类型,都走这同一套机制: + +```python +>>> class Count: +... def __init__(self, n): self.n = n; self.i = 0 +... def __iter__(self): return self +... def __next__(self): +... if self.i >= self.n: raise StopIteration +... self.i += 1 +... return self.i +... +>>> list(Count(3)) # list() 内部也是 iter + 不断 next +[1, 2, 3] +``` + +## block 栈:break 与 continue 怎么找到出口 + +还剩一个问题:循环体里的 `break` 怎么知道该跳到哪?它要跳到「循环结束之后」,但那个位置在循环体里并不直观可知。3.7 的办法是给帧配一个 **block 栈**(`f_blockstack`)记账。 + +进入循环时,`SETUP_LOOP` 压入一个块,记下**循环结束的偏移**和当前**栈深度**: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2837) + +```c +// Python/ceval.c —— TARGET(SETUP_LOOP) +PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg, STACK_LEVEL()); +// ↑ 循环结束的偏移 ↑ 入栈时的栈深度 +``` + +![block 栈与 break/continue](block-stack.svg) + +有了这个块,`break`(编译成 `BREAK_LOOP`)就不必自己算出口地址——它顺着 block 栈找到循环块,跳到块里记录的「结束偏移」即可;`continue` 则跳回循环顶部。循环正常结束时,`POP_BLOCK` 把这个块弹掉、并把栈深度还原到入栈时的水平(`UNWIND_BLOCK`)。这套 block 栈机制不止服务于循环,下一章的 `try`/`except`/`finally` 也靠它来记录异常处理的落点——所以这里先建立印象。 + +> 这是 3.7 的实现。3.8 起循环不再用 `SETUP_LOOP` 和 block 栈,改用「零开销异常表」管理出口,`SETUP_LOOP` 被移除;但「记下出口、跳过去」的思路是一脉相承的。 + +--- + +小结一下控制流: + +- 一切控制流都归结到一个动作:跳转指令用 `JUMPTO`/`JUMPBY` **改写 `next_instr`**,决定下一条取哪条; +- **`if/else`**:`POP_JUMP_IF_FALSE` 按条件真假决定跳不跳,配合 `JUMP_FORWARD` 跳过另一分支; +- **`while`**:循环顶部条件跳转 + 循环体末尾 `JUMP_ABSOLUTE` **往回跳**; +- **`for`**:走**迭代器协议**——`GET_ITER` 取得迭代器,`FOR_ITER` 反复 `next()` 取值,`StopIteration` 时跳出;任何实现 `__iter__`/`__next__` 的对象都能被遍历; +- **`break`/`continue`**:靠帧的 **block 栈**(`SETUP_LOOP` 压入、`POP_BLOCK` 弹出)记下循环出口,运行期顺着它找到落点。 + +控制流清楚了。但还有一种「跳法」更剧烈——**异常**:它能从深处的函数体,一路炸回到外层的 `try`。下一章就看虚拟机如何用 block 栈处理 `try`/`except`/`finally` 与异常的栈展开。 diff --git a/docs/vm/control-flow/jump-pointer.svg b/docs/vm/control-flow/jump-pointer.svg new file mode 100644 index 0000000..a829a99 --- /dev/null +++ b/docs/vm/control-flow/jump-pointer.svg @@ -0,0 +1,50 @@ + +跳转指令改写 next_instr,决定下一条取哪条 +求值循环正常情况下顺序前进:取完一条指令,指令指针 next_instr 自动指向下一条。跳转指令打破这个顺序——JUMP_ABSOLUTE 用 JUMPTO 把 next_instr 直接改写成某个目标偏移,于是循环下一轮就从那里取指令。条件跳转 POP_JUMP_IF_FALSE 则按栈顶真假决定要不要改写。所有分支与循环都建立在「改写 next_instr」这一个动作上。 + + + + + + + + + + + +跳转 = 改写指令指针 next_instr + + + + 0 LOAD_FAST x + 2 POP_JUMP_IF_FALSE 8 + 4 LOAD_CONST 1 + 6 JUMP_ABSOLUTE 10 + 8 LOAD_CONST 2 + + + + + + +顺序前进 +next_instr += 1 + + + + +跳转:改写 next_instr +JUMPTO(目标偏移) +下一轮从那里取 + + + +假→跳 8 + +分支、循环、break 全都归结到这一个动作:改写「下一条取哪条」 + diff --git a/docs/vm/control-flow/while-loop.svg b/docs/vm/control-flow/while-loop.svg new file mode 100644 index 0000000..fc84fcf --- /dev/null +++ b/docs/vm/control-flow/while-loop.svg @@ -0,0 +1,51 @@ + +while 循环:靠往回跳形成循环 +while x > 0: x -= 1 编译成一个回跳结构。循环顶部先判断条件,POP_JUMP_IF_FALSE 在条件为假时跳出循环;条件为真则执行循环体,循环体末尾用 JUMP_ABSOLUTE 跳回顶部重新判断。循环就是「条件跳转 + 往回跳」两者配合的结果。 + + + + + + + + + + + + + +while x > 0: x -= 1 + + + +判断条件 x > 0 +>> (回跳目标) COMPARE_OP + + + +POP_JUMP_IF_FALSE (跳出) + + + + +循环体:x -= 1 +真 → 执行 + + + + +JUMP_ABSOLUTE 回跳 + + + + +假 → 跳出循环 +POP_BLOCK 后继续 + +循环 = 条件跳转(决定要不要继续)+ JUMP_ABSOLUTE 往回跳(重新来一轮) + diff --git a/docs/vm/exceptions/cross-frame-unwind.svg b/docs/vm/exceptions/cross-frame-unwind.svg new file mode 100644 index 0000000..ee55240 --- /dev/null +++ b/docs/vm/exceptions/cross-frame-unwind.svg @@ -0,0 +1,64 @@ + +跨帧展开与 traceback 逐层累积 +异常在一帧内沿 block 栈展开,若该帧无人接住,求值循环带着 NULL 返回。调用它的上层帧里,CALL 指令拿到 NULL 就 goto error,在自己这一帧重演展开。三帧自下而上:inner 帧 raise,展开无人接,返回 NULL;outer 帧 CALL 拿到 NULL,展开仍无人接,返回 NULL;module 帧 CALL 拿到 NULL,这里有 SETUP_EXCEPT,异常被接住。每经过一帧,该帧的 error 标签都会执行 PyTraceBack_Here 把自己添进 traceback,于是最终的 traceback 正是异常穿过的帧序列 inner、outer、module。帧内沿 block 栈展开,帧间沿 f_back 调用链接力。 + + + + + + + + + + +跨帧展开:沿 f_back 一路炸到外层 + + + + +inner 帧 +raise ValueError +展开 block 栈 → 无人接 → 返回 NULL + + + +outer 帧 +CALL 拿到 NULL → goto error +展开 → 无人接 → 返回 NULL + + + +<module> 帧 +CALL 拿到 NULL → goto error +这里有 SETUP_EXCEPT → 接住! + + + + +返回 NULL +返回 NULL +沿 f_back 调用链 + + +traceback 逐层累积 + +每帧 error 处 PyTraceBack_Here(f): + + +in <module> + +in outer + +in inner ← 起点 + + + +ValueError: boom +异常穿过的帧序列, +一层不落地记下 + diff --git a/docs/vm/exceptions/except-match.svg b/docs/vm/exceptions/except-match.svg new file mode 100644 index 0000000..7ef9563 --- /dev/null +++ b/docs/vm/exceptions/except-match.svg @@ -0,0 +1,58 @@ + +except 子句在求值栈上匹配异常 +展开循环跳到 except handler 时,求值栈顶已被压上异常的三件套 traceback、value、exc(exc 在最上)。handler 的字节码用 DUP_TOP 复制栈顶的异常类型,LOAD 出 except 后写的异常类,COMPARE_OP EXC_MATCH 判断是否匹配(含子类),POP_JUMP_IF_FALSE 据结果分流。匹配则 POP_TOP 三次弹掉三件套、执行 except 体、POP_EXCEPT 清理 EXCEPT_HANDLER 块,异常被消化。不匹配则落到 END_FINALLY,把异常重新抛出,回到 fast_block_end 继续往外层展开。 + + + + + + + + + + + +except:拿栈顶的异常类型去比对 + + +求值栈 +exc(栈顶) +val +tb +展开循环压上来的三件套 + + + +DUP_TOP +LOAD ValueError +COMPARE_OP EXC_MATCH +POP_JUMP_IF_FALSE + + + + +匹配 + +异常被消化 +POP_TOP×3 → except 体 +POP_EXCEPT 收尾 + + + +不匹配 + +END_FINALLY +把异常重新抛出 +回 fast_block_end 继续展开 + + + +往外层 try 找下一个 except + +多个 except 就是「不匹配就重抛、再比下一个」串起来的 + diff --git a/docs/vm/exceptions/exception-overview.svg b/docs/vm/exceptions/exception-overview.svg new file mode 100644 index 0000000..dd7dcb9 --- /dev/null +++ b/docs/vm/exceptions/exception-overview.svg @@ -0,0 +1,63 @@ + +异常机制全景:从抛出到栈展开 +异常发生有两种来源:显式的 raise 语句,或任何指令操作失败。两者都把线程的当前异常 curexc 字段填好,跳到求值循环的 error 标签;error 标签设置 why 为 WHY_EXCEPTION,并调用 PyTraceBack_Here 把当前帧记进 traceback,随后落入 fast_block_end 沿 block 栈逐块展开。展开有两种结局:命中某个 SETUP_EXCEPT 或 SETUP_FINALLY 块,跳到对应 handler,异常被接住;或 block 栈走完仍无人接,本帧返回 NULL,沿 f_back 调用链交给调用者帧重演展开,这就是跨帧穿透。 + + + + + + + + + + + +异常 = 设置当前异常 + 跳 error + 沿 block 栈展开 + + + +raise 语句 +RAISE_VARARGS + +任何操作失败 +goto error + + + + + + +error 标签 +why = WHY_EXCEPTION +PyTraceBack_Here(f) → 记一帧 + + + + + +fast_block_end — 沿 block 栈展开 +逐块弹出,UNWIND_BLOCK 把求值栈清回 b_level +return / break / continue / 异常 同走这一段 + + + + + + +命中 try 块 +SETUP_EXCEPT / SETUP_FINALLY +JUMPTO handler,异常被接住 + + + +块栈走完,无人接 +返回 NULL,沿 f_back 上交 +调用者帧重演展开(跨帧) + +帧内沿 block 栈展开,帧间沿 f_back 接力——异常从最深处一路炸到最外层 + diff --git a/docs/vm/exceptions/finally-why.svg b/docs/vm/exceptions/finally-why.svg new file mode 100644 index 0000000..9688678 --- /dev/null +++ b/docs/vm/exceptions/finally-why.svg @@ -0,0 +1,65 @@ + +finally 借 why_code 记住未竟之事 +finally 的「无论如何都执行」靠一种寄存—续做的机制。进入 finally 体之前,会往求值栈压一个标记,记下「本来要干什么」:try 体正常结束压 None;途中 return 或 continue 压对应的 why 整数和返回值;途中抛异常则压异常本身。finally 体照常执行。执行完,末尾的 END_FINALLY 把标记取回来,据它决定接着干什么:None 就正常往下走;why 整数就完成原来的 return 或 continue;异常就重新抛出、继续往外展开。一个 why_code,把三种情形统一成同一套机制。 + + + + + + + + + + + + +finally:进门前寄存,出门后续做 + + +进 finally 前压入的标记 + +None +try 体正常结束 + +why + retval +return / continue + +异常本身 +途中抛了异常 + + + + + + +finally 体 +无论如何都执行 + + + + +END_FINALLY +取回标记,据它续做 + + + + + + + +见 None +照常往下走 + +见 why 整数 +完成原来的 return/continue + + +见异常 +重新抛出,回 fast_block_end 继续展开 + + diff --git a/docs/vm/exceptions/index.md b/docs/vm/exceptions/index.md new file mode 100644 index 0000000..6caba83 --- /dev/null +++ b/docs/vm/exceptions/index.md @@ -0,0 +1,345 @@ +# 异常机制:block 栈与栈展开 + +上一章的控制流,无论 `if`、`while` 还是 `for`,跳转的落点都在**同一段字节码**里——跳来跳去,终究没出这一帧。可异常不一样:一个 `raise` 能从某个深埋的函数体里发动,**穿过一层层调用**,一路炸回到外层某个 `try`。这是一种更剧烈的「跳法」。 + +这一章就看虚拟机如何驾驭这种剧烈的跳转。会发现它复用了上一章末尾埋下的伏笔——帧里的 **block 栈**——再配上一套叫 **`why_code`** 的「未竟之事」记账法,把 `raise`、`return`、`break`、`continue` 统统纳入了同一套**栈展开**逻辑。 + +## 先从 Python 的视角看异常 + +动手前先用纯 Python 建立直觉。异常机制在语言层面就四件事:`raise` 抛出、`try`/`except` 捕获、`finally` 善后、`traceback` 记录「这一路是从哪炸过来的」。 + +```python +>>> def inner(): +... raise ValueError("boom") # 在最深处抛出 +... +>>> def outer(): +... inner() # 自己不捕获,异常会穿过这一层 +... +>>> try: +... outer() # 在最外层接住 +... except ValueError as e: +... print("caught:", e) +... +caught: boom +``` + +异常在 `inner` 里抛出,`inner` 和 `outer` 都没有 `try`,于是它**逐层向外穿透**,直到最外层的 `except` 才被接住。如果一路都没人接,它会一直炸到顶层,打印出我们再熟悉不过的 traceback: + +```python +>>> outer() +Traceback (most recent call last): + File "", line 1, in + File "", line 2, in outer # 经过 outer + File "", line 2, in inner # 起点 inner +ValueError: boom +``` + +注意这份 traceback 是**逐帧累积**出来的——它如实记下了异常穿过的每一层 `inner ← outer ← `。记住这个「逐层穿透 + 逐帧记账」的画面,本章要做的就是把它落到 C 源码上。整套机制的全景如下: + +![异常机制全景](exception-overview.svg) + +一句话概括:**异常 = 设置「当前错误」+ 跳到求值循环的 `error` 标签 + 沿 block 栈展开**。下面逐块拆开。 + +## 异常是怎么「发生」的 + +先问一个基础问题:虚拟机怎么知道「出异常了」?答案藏在线程状态里。每个线程状态 `PyThreadState` 都有一组字段,专门记录**正在抛出途中的异常**: + +`源文件:`[Include/pystate.h](https://github.com/python/cpython/blob/v3.7.0/Include/pystate.h#L235) + +```c +// Include/pystate.h —— PyThreadState(节选) +/* The exception currently being raised */ +PyObject *curexc_type; // 正在抛出的异常:类型 +PyObject *curexc_value; // 值(异常实例) +PyObject *curexc_traceback; // traceback +``` + +所谓「抛出一个异常」,本质就是把这三个字段填上——这正是 `PyErr_SetString`、`PyErr_SetObject` 这些 C API 在做的事。填好之后,怎么让求值循环注意到?靠两条路汇到同一个落点——求值循环里的 **`error` 标签**: + +![进入 error 标签的两条路](raise-paths.svg) + +**第一条:显式 `raise`。** 源码里的 `raise` 语句编译成 `RAISE_VARARGS` 指令,它调用 `do_raise` 设置好「当前异常」,然后跳到 `error`: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1611) + +```c +// Python/ceval.c —— TARGET(RAISE_VARARGS)(精简) +case 0: + if (do_raise(exc, cause)) { // 设置 curexc_*,区分 raise / raise X / raise X from Y + why = WHY_EXCEPTION; + goto fast_block_end; // 直接进入栈展开 + } +``` + +**第二条:隐式出错。** 这条更常见——任何指令执行失败,都会 `goto error`。回想上一章的 `BINARY_ADD`:相加失败(比如 `1 + "x"`)时 `PyNumber_Add` 返回 `NULL`,分支里就 `goto error`。整个 ceval.c 里这样的 `goto error` 有几百处,它们是异常最主要的来源——绝大多数异常并非你手写 `raise`,而是某个底层操作失败后自动冒出来的。 + +```c +// Python/ceval.c —— 任何指令失败都走这条路(以 BINARY_ADD 为例) +sum = PyNumber_Add(left, right); +SET_TOP(sum); +if (sum == NULL) + goto error; // 操作失败 → 跳到 error 标签 +``` + +两条路最终都汇到 `error` 标签。它做两件事,然后落入真正的主角 `fast_block_end`: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L3330) + +```c +// Python/ceval.c —— error 标签(精简) +error: + why = WHY_EXCEPTION; // ① 标记:本次展开是因为「异常」 + PyTraceBack_Here(f); // ② 把当前帧记进 traceback(逐帧累积就靠这句) + /* 落入下面的 fast_block_end */ +``` + +第 ② 句 `PyTraceBack_Here(f)` 正是开头那份 traceback「逐帧累积」的来历:异常每穿过一帧,就在这里把那一帧添进链条。而第 ① 句把 `why` 设成 `WHY_EXCEPTION`——这个 `why` 是理解整章的钥匙。 + +## why_code:把四种「非正常结束」记成一个理由 + +`why` 的类型是个枚举 `why_code`,它回答一个问题:「为什么要中断正常的顺序执行、去展开 block 栈?」 + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L505) + +```c +// Python/ceval.c —— why_code +enum why_code { + WHY_NOT = 0x0001, // 没事,正常执行 + WHY_EXCEPTION = 0x0002, // 出了异常 + WHY_RETURN = 0x0008, // 执行了 return + WHY_BREAK = 0x0010, // 执行了 break + WHY_CONTINUE = 0x0020, // 执行了 continue + WHY_YIELD = 0x0040, // 执行了 yield + WHY_SILENCED = 0x0080 // 异常被 with 吞掉了 +}; +``` + +这是 CPython 异常机制最精妙的设计:**`return`、`break`、`continue` 和异常,本质是同一类事**——它们都要「中断当前的顺序执行,跳到别处」,途中都可能要跨过若干 `try`/`finally`,都得把求值栈清理干净。CPython 没有为它们各写一套逻辑,而是用一个 `why` 把「中断的理由」记下来,再交给**同一段**栈展开代码统一处理。所以下面讲异常展开时,你看到的其实是一套通吃四种情况的机制。 + +## block 栈:try 在哪、栈该清到哪 + +栈展开要回答两个问题:**异常该交给哪个 `try`?清理时求值栈该退到什么深度?** 这两个答案,在进入 `try` 时就已经记在了帧的 **block 栈**上——也就是上一章循环用过的那个 `f_blockstack`。 + +每个块是一个 `PyTryBlock`,只有三个字段,却刚好回答上面两个问题: + +`源文件:`[Include/frameobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/frameobject.h#L11) + +```c +// Include/frameobject.h +typedef struct { + int b_type; // 块的种类:SETUP_EXCEPT / SETUP_FINALLY / SETUP_LOOP / EXCEPT_HANDLER + int b_handler; // 出事了跳哪去(handler 的字节码偏移) + int b_level; // 入块时的求值栈深度——清理时退回到这里 +} PyTryBlock; +``` + +进入 `try` 时,编译器埋下的 `SETUP_EXCEPT`(带 `except`)或 `SETUP_FINALLY`(带 `finally`)会压入一个这样的块,记下 handler 的位置和当前栈深: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2838) + +```c +// Python/ceval.c —— TARGET(SETUP_EXCEPT) / TARGET(SETUP_FINALLY) +PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg, STACK_LEVEL()); +// ↑ b_type ↑ b_handler=handler 偏移 ↑ b_level=当前栈深 +``` + +![PyTryBlock 与 block 栈](tryblock-stack.svg) + +`b_level` 这个字段格外重要。设想 `try: x = a + b + c` 在算 `b + c` 时抛了异常——此刻求值栈上还压着半截没算完的中间值。展开时若不把它们清掉,栈就乱了。`b_level` 记下的正是「进 `try` 那一刻干净的栈深」,清理时退回这里即可。 + +## 栈展开:fast_block_end 这段 while 循环 + +现在到全章的心脏。`why` 已置位、当前异常已设好,控制流落到 `fast_block_end`——一段 `while` 循环,**顺着 block 栈从栈顶往下逐个弹块**,看谁能接住这次「中断」: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L3351) + +```c +// Python/ceval.c —— fast_block_end(精简,只留异常相关分支) +fast_block_end: + while (why != WHY_NOT && f->f_iblock > 0) { + PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1]; // 看栈顶的块 + f->f_iblock--; // 弹掉它 + + UNWIND_BLOCK(b); // ① 把求值栈退回 b->b_level,多余的中间值全部丢弃 + + // ② 是 try 块、且这次是异常 → 由它接住 + if (why == WHY_EXCEPTION && (b->b_type == SETUP_EXCEPT + || b->b_type == SETUP_FINALLY)) { + // 保存「上一个正在处理的异常」,压入 EXCEPT_HANDLER 块(POP_EXCEPT 时还原) + PyFrame_BlockSetup(f, EXCEPT_HANDLER, -1, STACK_LEVEL()); + PUSH(...旧 exc_info 三件套...); + PyErr_Fetch(&exc, &val, &tb); // 取出当前异常 + PyErr_NormalizeException(&exc, &val, &tb); + ...更新 tstate->exc_info 为本次异常... + PUSH(tb); PUSH(val); PUSH(exc); // ③ 把异常三件套压上求值栈,交给 handler + why = WHY_NOT; // ④ 异常「接住了」,中断到此为止 + JUMPTO(handler); // ⑤ 跳到 except/finally 体 + break; + } + ...(SETUP_FINALLY 处理 return/break,见下一节)... + } + if (why != WHY_NOT) // 块栈走完仍没人接 → 跳出主循环,把异常甩给调用者(跨帧) + break; +``` + +![栈展开循环](unwind-loop.svg) + +逐步看这段循环对一次异常做了什么: + +- **① `UNWIND_BLOCK(b)`**:把求值栈退回 `b_level`,丢掉那些半算完的中间值——这就是 `b_level` 的用处。 +- **② 判断**:弹出的块若是 `SETUP_EXCEPT`/`SETUP_FINALLY`,且 `why == WHY_EXCEPTION`,说明这个 `try` 能接住异常。 +- **③ 交接**:把异常的 `(type, value, traceback)` 三件套压上求值栈,`except` 子句待会儿就从栈上拿它来匹配;同时压入一个 **`EXCEPT_HANDLER`** 块,记住「进入异常处理前的状态」,等 `POP_EXCEPT` 时还原。 +- **④ `why = WHY_NOT`**:异常被接住了,中断结束,循环就此 `break`。 +- **⑤ `JUMPTO(handler)`**:跳到 `b_handler` 记下的 handler 偏移,开始执行 `except` 或 `finally` 体。 + +如果弹到底(`f_iblock` 归零)`why` 仍是 `WHY_EXCEPTION`,说明**这一帧没人能接**——循环退出后那句 `if (why != WHY_NOT) break` 会跳出整个求值主循环,把异常甩给调用者。这就是跨帧穿透,稍后细说。 + +> 顺带一提那两个被压上栈的「异常状态」概念:`curexc_*` 是**正在抛出途中**的异常(`raise` 刚发动、还没被接住);而 `tstate->exc_info` 是**正在被处理**的异常(已经进了某个 `except` 体)——`sys.exc_info()` 读的就是后者。展开时把旧的 `exc_info` 压栈保存、`POP_EXCEPT` 时还原,是为了支持 `except` 里又嵌 `try` 的层层嵌套。 + +## except:在求值栈上匹配异常 + +被 `JUMPTO` 跳到的 handler,是编译器为 `except` 子句生成的一段字节码。它要做的是:拿栈顶的异常类型,去和 `except ValueError` 里写的类比一比,**匹配就处理,不匹配就重新抛出、继续往外找**。 + +把 `try: ... except ValueError: ...` 编出来,骨架是这样(跳转目标用符号表示): + +``` + SETUP_EXCEPT →H # 压入异常块,记下 handler 位置 H + + POP_BLOCK # try 体顺利跑完:弹掉异常块 + JUMP_FORWARD →E # 跳过 except,到汇合点 E + >>H DUP_TOP # 入口:栈顶是异常类型,复制一份来比 + LOAD_GLOBAL ValueError + COMPARE_OP (exception match) # 类型匹配吗? + POP_JUMP_IF_FALSE →N # 不匹配 → 跳到 N + POP_TOP; POP_TOP; POP_TOP # 匹配:弹掉 type/value/traceback 三件套 + + POP_EXCEPT # 清理 EXCEPT_HANDLER 块、还原上一个异常状态 + JUMP_FORWARD →E + >>N END_FINALLY # 没有任何 except 匹配 → 重新抛出,继续展开 + >>E ... # 汇合 +``` + +![except 的匹配](except-match.svg) + +关键是 `>>H` 入口处栈顶那个异常类型——它正是上一节展开循环 `PUSH(exc)` 压上去的。`DUP_TOP` + `COMPARE_OP (exception match)` 做一次「是不是这个异常类(或其子类)」的判断: + +- **匹配**:`POP_TOP` 三次弹掉异常三件套,执行 `except` 体;完事 `POP_EXCEPT` 把 `EXCEPT_HANDLER` 块和异常状态收拾干净。异常到此真正被消化。 +- **不匹配**:落到 `>>N` 的 `END_FINALLY`。它发现栈上是个异常(而非正常标记),就把它**重新抛出**——于是 `why` 又变回 `WHY_EXCEPTION`、`goto fast_block_end`,展开循环继续往外层 block 找下一个 `try`。多个 `except ValueError: ... except KeyError: ...` 也是靠这条「不匹配就重抛、再比下一个」串起来的。 + +## finally:why_code 如何「记住未竟之事」 + +`finally` 的语义是「无论如何都执行」——无论 `try` 体正常结束、抛了异常,还是中途 `return`/`break`。这个「无论如何」用 `why_code` 实现得异常优雅。 + +看展开循环里专门处理 `SETUP_FINALLY` 的另一个分支: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L3421) + +```c +// Python/ceval.c —— fast_block_end 中处理 finally 的分支 +if (b->b_type == SETUP_FINALLY) { + if (why & (WHY_RETURN | WHY_CONTINUE)) + PUSH(retval); // 若是 return/continue,连同返回值一起寄存到栈上 + PUSH(PyLong_FromLong((long)why)); // 把「中断的理由」why 压栈,先记着 + why = WHY_NOT; // 理由暂存好了,先去执行 finally 体 + JUMPTO(b->b_handler); // 跳到 finally + break; +} +``` + +精髓在 `PUSH(why)`:展开经过一个 `finally` 块时,它把**中断的理由**(以及 `return` 的返回值)压到栈上「寄存」,然后把 `why` 清成 `WHY_NOT`、先去执行 `finally` 体。等 `finally` 体跑完,末尾的 `END_FINALLY` 再把寄存的理由取回来,**接着干被打断的事**: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1847) + +```c +// Python/ceval.c —— TARGET(END_FINALLY)(精简) +PyObject *status = POP(); // 取回 finally 入口寄存的「标记」 +if (PyLong_Check(status)) { // 是个 why 整数 → 之前是 return/break/continue + why = (enum why_code) PyLong_AS_LONG(status); + if (why == WHY_RETURN || why == WHY_CONTINUE) + retval = POP(); + goto fast_block_end; // 接着完成原来的 return/break/continue +} +else if (PyExceptionClass_Check(status)) { // 是个异常类 → 之前在抛异常 + PyErr_Restore(status, POP(), POP()); + why = WHY_EXCEPTION; + goto fast_block_end; // 接着把异常往外抛 +} +// status 是 None → try 体本是正常结束,finally 跑完照常往下走 +``` + +![finally 借 why_code 续做未竟之事](finally-why.svg) + +于是 `finally` 的三种情形被统一成一句话——**进 `finally` 前把「本来要干什么」压栈寄存,`finally` 跑完再取回来接着干**: + +- `try` 体**正常结束**:编译器在落入 `finally` 前压了个 `None`,`END_FINALLY` 见 `None`,跑完照常往下; +- `try` 体里 **`return`/`break`/`continue`**:寄存的是对应的 `why`(和返回值),`finally` 跑完接着完成那个 `return`; +- `try` 体里**抛了异常**:寄存的是异常本身,`finally` 跑完把异常重新抛出、继续向外展开。 + +这下就明白 `finally` 为什么「拦得住 `return`」了——`return` 不过是 `why == WHY_RETURN` 的一次展开,途经 `finally` 块时同样会被寄存、暂缓,等 `finally` 执行完才放行。`finally` 里若又写了 `return`,则会覆盖掉寄存的理由,原来的 `return`/异常就被「顶掉」了。一个 `why_code`,把这些边角语义全收进了同一套机制。 + +## 跨帧展开:traceback 是怎么一层层长出来的 + +回到最初那个问题:异常如何穿过 `inner ← outer ← `? + +答案前面已经露头:当一帧的 block 栈**走到底仍没人接**,`fast_block_end` 后那句 `if (why != WHY_NOT) break` 会跳出求值主循环,让 `_PyEval_EvalFrameDefault` **带着 `NULL` 返回**。而调用它的,是调用方帧里的 `CALL_FUNCTION` 之类指令——它拿到 `NULL`,立刻 `goto error`,于是在**调用方这一帧**里,同样的 `error → fast_block_end` 展开又重演一遍: + +![跨帧展开与 traceback 累积](cross-frame-unwind.svg) + +``` +inner 帧:raise → error → 展开 block 栈 → 没人接 → 返回 NULL + │ error 处 PyTraceBack_Here(inner) 记一笔 + ▼ +outer 帧:CALL 拿到 NULL → goto error → 展开 → 没人接 → 返回 NULL + │ error 处 PyTraceBack_Here(outer) 记一笔 + ▼ + 帧:CALL 拿到 NULL → goto error → 展开 → SETUP_EXCEPT 接住! +``` + +每经过一帧,那帧的 `error` 标签都会执行一次 `PyTraceBack_Here(f)`,把自己**添进 traceback 链**。所以最终打印出来的 traceback,正是异常穿过的帧序列——`inner`、`outer`、`` 一层不落。**栈展开在帧内沿 block 栈进行,在帧间沿 `f_back` 调用链进行**,两者接力,异常就这样从最深处一路炸到最外层。若连最外层 `` 帧也没有 `try`,异常返回给最顶层的运行环境,由它打印 traceback 并结束程序——这就是我们见惯的那一幕。 + +## 异常链:`__context__` 与 `__cause__` + +最后补一个 Python 3 引入的细节。在处理一个异常时**又抛出**另一个异常,Python 会把它们「串」起来,traceback 里会出现那句熟悉的「During handling of the above exception, another exception occurred」: + +```python +>>> try: +... 1 / 0 +... except ZeroDivisionError: +... raise ValueError("wrapped") # 处理 ZeroDivisionError 时又抛了 ValueError +... +Traceback (most recent call last): + ... +ZeroDivisionError: division by zero +During handling of the above exception, another exception occurred: + ... +ValueError: wrapped +``` + +这种**隐式**串联记在新异常的 `__context__` 上,源头就是前面反复出现的 `tstate->exc_info`——「当前正在处理的异常」。新异常抛出时,CPython 把 `exc_info` 挂到它的 `__context__`,于是两者连成一串。 + +如果想**显式**表达因果,用 `raise ... from ...`,它落到 `do_raise` 的 `cause` 参数,记在 `__cause__` 上,traceback 改用「The above exception was the direct cause of the following exception」措辞: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L4036) + +```c +// Python/ceval.c —— do_raise 处理 raise X from Y(精简) +if (cause) { // raise X from Y 里的 Y + ... + PyException_SetCause(value, fixed_cause); // 挂到 X.__cause__ +} +``` + +`__context__` 是「处理旧异常时碰巧又出错」,`__cause__` 是「我明确地因为它才抛这个」——前者自动、后者手动,但都只是给异常对象**多挂一个指针**,并不影响前面那套展开逻辑。 + +--- + +小结一下异常机制: + +- **异常 = 设置「当前异常」(`curexc_*`)+ 跳到 `error` 标签 + 沿 block 栈展开**;进入 `error` 有两条路:显式 `RAISE_VARARGS`,和任何指令失败后的 `goto error`(后者才是大多数异常的来源); +- **`why_code`** 是核心抽象:`return`/`break`/`continue`/异常被统一记成一个「中断的理由」`why`,交给**同一段**栈展开代码处理; +- **block 栈**(`PyTryBlock` 的 `b_type`/`b_handler`/`b_level`)在进 `try` 时由 `SETUP_EXCEPT`/`SETUP_FINALLY` 压入,记下 handler 落点与该清理到的栈深; +- **栈展开**(`fast_block_end`)沿 block 栈逐块弹出:`UNWIND_BLOCK` 还原求值栈,命中 `try` 块就把异常三件套压栈、`JUMPTO` 到 handler;`except` 用 `COMPARE_OP (exception match)` 匹配,不中就 `END_FINALLY` 重抛; +- **`finally`** 借 `why_code`「寄存—续做」:进 `finally` 前把未竟之事压栈,跑完由 `END_FINALLY` 取回接着干——这正是它「无论如何都执行」、还拦得住 `return` 的原因; +- **跨帧穿透**:帧内沿 block 栈展开,帧间沿 `f_back` 返回 `NULL` 接力;每帧 `PyTraceBack_Here` 记一笔,traceback 就此逐层长出。 + +异常这种「剧烈跳转」搞清楚了。但我们一直在说「调用一个函数就新建一帧」,这「新建一帧」本身又是怎么回事?参数怎么传进去、默认值和闭包怎么安排?下一章就来拆**函数机制:调用、参数与闭包**。 diff --git a/docs/vm/exceptions/raise-paths.svg b/docs/vm/exceptions/raise-paths.svg new file mode 100644 index 0000000..f0e7c6a --- /dev/null +++ b/docs/vm/exceptions/raise-paths.svg @@ -0,0 +1,48 @@ + +进入 error 标签的两条路 +异常进入求值循环的 error 标签有两条来路。第一条是显式 raise:源码的 raise 语句编译成 RAISE_VARARGS 指令,调用 do_raise 设置当前异常后跳转。第二条是隐式出错:任何指令执行失败,例如 BINARY_ADD 调用 PyNumber_Add 返回 NULL,分支里直接 goto error;整个 ceval.c 里这样的 goto error 有几百处,是异常最主要的来源。两条路都先填好线程状态的 curexc 字段,再汇到同一个 error 标签。 + + + + + + + + + + +两条路,同一个落点 + + +① 显式 raise + +RAISE_VARARGS + +do_raise() 设异常 + + + +② 操作失败(最常见) + +PyNumber_Add → NULL + +goto error + + + + +error 标签 +why=EXCEPTION · 记 traceback · 展开 + + + + +ceval.c 里 +几百处 +goto error + diff --git a/docs/vm/exceptions/tryblock-stack.svg b/docs/vm/exceptions/tryblock-stack.svg new file mode 100644 index 0000000..917dffb --- /dev/null +++ b/docs/vm/exceptions/tryblock-stack.svg @@ -0,0 +1,54 @@ + +PyTryBlock 与帧的 block 栈 +左边是一个 PyTryBlock 的三个字段:b_type 是块的种类(SETUP_EXCEPT / SETUP_FINALLY / SETUP_LOOP / EXCEPT_HANDLER),b_handler 是出事时跳转的 handler 字节码偏移,b_level 是入块时的求值栈深度,清理时退回到这里。右边是帧里的 f_blockstack 数组,进入 try 时 SETUP_EXCEPT 或 SETUP_FINALLY 压入一个块,f_iblock 指向栈顶。栈展开时从 f_iblock 指的栈顶往下逐块弹出。 + + + + + + + + + +block 栈:try 在哪、栈该清到哪 + + +一个 PyTryBlock + + + + +b_type +块的种类 +SETUP_EXCEPT / FINALLY … + +b_handler +出事跳哪去 +handler 字节码偏移 + +b_level +栈该清到哪 +入块时的求值栈深度 + + +帧的 f_blockstack + +SETUP_EXCEPT + +SETUP_LOOP + +… 更外层的块 + + +f_iblock → + +SETUP_EXCEPT 压入 + +展开时从 f_iblock 指的栈顶往下逐块弹 + + diff --git a/docs/vm/exceptions/unwind-loop.svg b/docs/vm/exceptions/unwind-loop.svg new file mode 100644 index 0000000..0c07ff5 --- /dev/null +++ b/docs/vm/exceptions/unwind-loop.svg @@ -0,0 +1,68 @@ + +fast_block_end 的栈展开循环 +栈展开是一段 while 循环。进入时 why 已是 WHY_EXCEPTION。循环条件是 f_iblock 大于 0,即 block 栈还有块。每轮:看栈顶的块 b 并弹出它,f_iblock 减一;执行 UNWIND_BLOCK 把求值栈清回 b 的 b_level,丢弃半算完的中间值;判断 b 是否是 try 块(SETUP_EXCEPT 或 SETUP_FINALLY)且 why 是异常——若是,则把异常的 type、value、traceback 三件套压上求值栈、压入 EXCEPT_HANDLER 块、把 why 置回 WHY_NOT、JUMPTO 到 handler,异常被接住,循环 break;若不是,继续下一轮往外层块找。若 block 栈走到底 why 仍是异常,跳出主循环,本帧返回 NULL 交给调用者。 + + + + + + + + + + + +栈展开:沿 block 栈逐块找接盘的 try + + + +why = WHY_EXCEPTION + + + + +f_iblock > 0 ? +还有块吗 + + + + + +无人接 +返回 NULL → 调用者 + + + + + + +看栈顶块 b,f_iblock-- + + + +UNWIND_BLOCK(b) +求值栈清回 b_level,丢弃中间值 + + + + +b 是 try 块? +SETUP_EXCEPT / FINALLY + + + + +下一块 + + + + + +异常被接住 +压入异常三件套 · why=WHY_NOT · JUMPTO handler · break + diff --git a/docs/vm/expressions-and-names/build-list.svg b/docs/vm/expressions-and-names/build-list.svg new file mode 100644 index 0000000..16ea497 --- /dev/null +++ b/docs/vm/expressions-and-names/build-list.svg @@ -0,0 +1,43 @@ + +BUILD_LIST 3:把栈顶三个值打包成列表 +构建容器也是栈式套路。执行 [a, b, c] 时,先依次把 a、b、c 压入求值栈,再用 BUILD_LIST 3 把栈顶的三个值弹出、打包成一个列表对象,最后把这个列表压回栈顶。参数 3 就是元素个数。 + + + + + + + + + + +BUILD_LIST 3:把栈顶三个值打包成列表(源码 [a, b, c]) + + +栈:已压入 a、b、c + +a +b +c + + + +BUILD_LIST 3 +弹出 3 个,打包成列表 + + + + +栈:列表压回栈顶 + +[a, b, c] +一个 list 对象 + +参数 3 = 元素个数;BUILD_TUPLE、BUILD_MAP 等都是同一套「弹出 → 打包 → 压回」 + diff --git a/docs/vm/expressions-and-names/expr-stack.svg b/docs/vm/expressions-and-names/expr-stack.svg new file mode 100644 index 0000000..f48187b --- /dev/null +++ b/docs/vm/expressions-and-names/expr-stack.svg @@ -0,0 +1,60 @@ + +a + b * c 在求值栈上的执行(设 a=1, b=2, c=3) +表达式 a + b * c 编译成:依次 LOAD_FAST 压入 a、b、c,再 BINARY_MULTIPLY 把栈顶的 b、c 弹出相乘压回,最后 BINARY_ADD 把 a 和乘积相加。乘法先于加法发生,正是因为编译器按语法树结构把乘法指令排在了前面——运算优先级在编译期就固化进了字节码顺序。 + + + + + + + + + +a + b * c 在栈上求值 (设 a=1, b=2, c=3) + + + + + + + +LOAD_FAST a + +1 +压 a=1 + + +LOAD_FAST b + +1 +2 +压 b=2 + + +LOAD_FAST c + +1 +2 +3 +压 c=3 + + +BINARY_MULTIPLY + +1 +6 +2·3 → 6 + + +BINARY_ADD + +7 +1+6 → 7 + +乘法排在加法之前执行——优先级在编译期就固化进了字节码的顺序 + diff --git a/docs/vm/expressions-and-names/fastlocals.svg b/docs/vm/expressions-and-names/fastlocals.svg new file mode 100644 index 0000000..d3298c7 --- /dev/null +++ b/docs/vm/expressions-and-names/fastlocals.svg @@ -0,0 +1,47 @@ + +LOAD_FAST:按下标直接取局部变量 +函数的局部变量存在帧的 f_localsplus 数组里,每个变量占一个固定槽位。LOAD_FAST 的参数就是槽位下标——LOAD_FAST 0 直接取数组第 0 个槽(变量 a),把它的值压入求值栈。整个过程只是一次数组索引,不查任何字典,所以最快。 + + + + + + + + + + +LOAD_FAST 0:按下标取局部变量,不查字典 + + + +LOAD_FAST 0 +参数 0 = 槽位下标 + + +f_localsplus 数组(帧里的局部变量区) + + a = 5[0] + b = 3[1] + c = …[2] + + + + +取第 0 槽 + + +求值栈 + +5 + +压入值 5 + +下标在编译期就由符号表定好(见 co_varnames 的顺序),运行期一次数组索引即可——这就是它快的原因 + diff --git a/docs/vm/expressions-and-names/index.md b/docs/vm/expressions-and-names/index.md new file mode 100644 index 0000000..0f23e56 --- /dev/null +++ b/docs/vm/expressions-and-names/index.md @@ -0,0 +1,201 @@ +# 一般表达式与名字空间 + +上一章我们立起了虚拟机的骨架——求值循环照着字节码一条条执行,操作的是帧里的求值栈。这一章就钻进最常见的那批指令:**取名字**和**算表达式**。一段直线代码(没有分支、没有调用)的执行,基本就是这两件事来回交织。 + +而其中真正有讲头的是「取名字」。`x` 这个名字,运行时到底去哪儿找它的值?答案牵出 Python 著名的 **LEGB** 规则,以及一个容易被忽略的事实:**「去哪找」这件事,一半在编译期就定好了。** + +## 取名字:编译期定指令,运行期做查找 + +回想第三部分讲的符号表——编译时它已经分析出每个名字属于哪类作用域。这个分析结果不会浪费:**编译器据此为每个名字选定一条专门的取值指令**。于是取名字的工作被劈成两半: + +- **编译期**:符号表定作用域 → 选指令。局部变量用 `LOAD_FAST`,函数里引用的全局名用 `LOAD_GLOBAL`,模块顶层和类体里的名字用 `LOAD_NAME`。 +- **运行期**:选定的指令各查各的名字空间。 + +![取名字的编译期与运行期分工](name-resolution.svg) + +亲眼看一下编译器的选择。同一个表达式 `a + g` 里,`a` 是参数(局部)、`g` 是全局变量,编译器给它们配了不同的指令: + +```python +>>> import dis +>>> g = 10 +>>> def f(a): +... return a + g +... +>>> for ins in dis.get_instructions(f): +... if ins.opname.startswith("LOAD"): +... print(f"{ins.opname:14} {ins.argrepr}") +... +LOAD_FAST a +LOAD_GLOBAL g +``` + +`a` 配了 `LOAD_FAST`、`g` 配了 `LOAD_GLOBAL`——**这个区分在编译期就完成了**,运行期不再去猜「`a` 是局部还是全局」。下面逐条看这三种指令在运行期怎么干活。 + +## LOAD_FAST:局部变量按下标取,最快 + +函数里的局部变量,存在帧的 `f_localsplus` 那块数组里(上一章提过,局部变量和求值栈共用它)。`LOAD_FAST` 的参数就是变量在这块数组里的**下标**——直接按下标取,连字典都不用查: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1067) + +```c +// Python/ceval.c —— TARGET(LOAD_FAST) +PyObject *value = GETLOCAL(oparg); // 按下标直接取 +if (value == NULL) { // 槽位还没被赋值 + format_exc_check_arg(PyExc_UnboundLocalError, ...); + goto error; +} +Py_INCREF(value); +PUSH(value); // 压入求值栈 +``` + +![LOAD_FAST 按下标取局部变量](fastlocals.svg) + +这是 Python 里取值最快的路径——一次数组索引而已。这也解释了一个常见报错的由来:如果这个槽位还没被赋值就被读取,`GETLOCAL` 取到 `NULL`,于是抛 **`UnboundLocalError`**: + +```python +>>> def bad(): +... print(x) # x 在下面被赋值,所以整个函数里 x 是局部 → LOAD_FAST +... x = 1 # 但执行到 print 时这个槽位还是空的 +... +>>> bad() +Traceback (most recent call last): + ... +UnboundLocalError: local variable 'x' referenced before assignment +``` + +> 上面是 3.7 的报错文字;新版本措辞略有调整(如「cannot access local variable ...」),含义一致。关键是:**只要函数里某处给 `x` 赋了值,`x` 在整个函数里就是局部的**,读取它走的就是 `LOAD_FAST`——哪怕赋值写在读取之后。 + +## LOAD_GLOBAL:全局 → 内建 + +函数里**只读不写**的外部名(比如调用 `len`、引用模块级变量 `g`),编译器选 `LOAD_GLOBAL`。它先查全局名字空间 `f_globals`,没有再查内建 `f_builtins`: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2101) + +```c +// Python/ceval.c —— TARGET(LOAD_GLOBAL)(快路径) +v = _PyDict_LoadGlobal((PyDictObject *)f->f_globals, // 先全局 + (PyDictObject *)f->f_builtins, // 再内建 + name); +if (v == NULL) { ... // 两处都没有 + format_exc_check_arg(PyExc_NameError, NAME_ERROR_MSG, name); + goto error; +} +``` + +`_PyDict_LoadGlobal` 把「先全局后内建」两步合成一次调用。所以我们能直接用 `len`、`print` 这些**内建函数**而无需导入——它们不在全局里,但 `LOAD_GLOBAL` 会自动回退到内建名字空间找到它们: + +```python +>>> def show(): +... return len # 全局里没有 len,回退到内建找到 +... +>>> show() + +``` + +要是全局和内建都没有这个名字,就抛 **`NameError`**: + +```python +>>> undefined_name +Traceback (most recent call last): + ... +NameError: name 'undefined_name' is not defined +``` + +注意 `LOAD_GLOBAL` 和 `LOAD_FAST` 的报错不同:前者是 `NameError`(名字根本不存在),后者是 `UnboundLocalError`(是局部、但还没赋值)。报错类型直接反映了编译器当初选了哪条指令。 + +## LOAD_NAME:模块层与类体的运行期查找 + +还有第三条:`LOAD_NAME`。它用在**模块顶层代码**和 **`class` 体**里——这些地方的名字,编译期没法像函数那样把局部变量固定成下标(class 体要支持动态、模块层的局部就是全局),于是只能运行期动态查。它依次查**局部 → 全局 → 内建**三个名字空间: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2050) + +```c +// Python/ceval.c —— TARGET(LOAD_NAME)(精简) +v = PyObject_GetItem(f->f_locals, name); // ① 局部 +if (v == NULL) { + v = PyDict_GetItem(f->f_globals, name); // ② 全局 + if (v == NULL) { + v = PyDict_GetItem(f->f_builtins, name); // ③ 内建 + if (v == NULL) { ...NameError... } + } +} +``` + +![LOAD_NAME 运行期依次查三个名字空间](name-lookup.svg) + +可以看到 `LOAD_NAME` 比 `LOAD_GLOBAL` 多查一层局部、比 `LOAD_FAST` 多了字典查找——它最灵活,但也最慢。这正是为什么**函数内部要尽量用局部变量**:函数体里的名字能走 `LOAD_FAST` 的快路径,而模块顶层只能用 `LOAD_NAME`。 + +## LEGB:四类作用域与四条指令 + +把三条指令和大家熟悉的 **LEGB** 规则对起来,整幅图就完整了。LEGB 是取名字的查找顺序——**L**ocal(局部)→ **E**nclosing(外层函数)→ **G**lobal(全局)→ **B**uiltin(内建): + +![LEGB 与对应指令](legb.svg) + +| 作用域 | 指令 | 运行期行为 | +|---|---|---| +| **L** 局部 | `LOAD_FAST` | 按下标取,不查字典 | +| **E** 外层(闭包) | `LOAD_DEREF` | 从 cell 取外层函数的变量 | +| **G** 全局 / **B** 内建 | `LOAD_GLOBAL` | 先全局、再内建 | +| 模块层 / 类体 | `LOAD_NAME` | 局部 → 全局 → 内建 | + +关键在于:**LEGB 这条「链」并不是运行期一节节去试出来的,而是编译期就按符号表把每个名字归好类、配好指令**。运行期各指令只查自己该查的那一两处,查不到才报 `NameError`。其中 `LOAD_DEREF`(E 层,闭包)牵涉 cell 变量,留到「函数机制」一章再展开;这里只要知道它在 LEGB 里占了「外层」这一格。 + +## 表达式求值:运算符在栈上接力 + +名字取到栈上之后,剩下的就是**算**。运算符指令的套路高度一致——上一章的 `BINARY_ADD` 已经示范过:弹出操作数、计算、把结果压回栈顶。乘法 `BINARY_MULTIPLY` 一模一样,只是换成 `PyNumber_Multiply`: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1197) + +```c +// Python/ceval.c —— TARGET(BINARY_MULTIPLY) +PyObject *right = POP(); +PyObject *left = TOP(); +PyObject *res = PyNumber_Multiply(left, right); +...... +SET_TOP(res); // 结果写回栈顶 +``` + +有意思的是**运算优先级是怎么体现的**。看 `a + b * c`(设 `a=1, b=2, c=3`)——上一章讲过,编译器按语法树生成字节码,而 `*` 在树里比 `+` 更深,于是它的指令排得更靠前: + +``` + 0 LOAD_FAST a + 2 LOAD_FAST b + 4 LOAD_FAST c + 6 BINARY_MULTIPLY # 先算 b * c + 8 BINARY_ADD # 再算 a + (b*c) +``` + +在栈上跑一遍就一目了然: + +![a + b * c 在栈上求值](expr-stack.svg) + +三个值依次压栈后,`BINARY_MULTIPLY` 先弹出 `b`、`c` 相乘得 `6` 压回,`BINARY_ADD` 再把 `a` 和 `6` 相加得 `7`。**乘法先于加法发生,纯粹是因为它的指令排在前面**——优先级在编译期就固化进了字节码顺序,运行期的栈只是忠实地按顺序执行,根本不需要懂什么叫优先级。 + +构建容器也是同一个套路。比如 `[a, b, c]` 编译成「依次压入 a、b、c,再用 `BUILD_LIST 3` 把栈顶三个值打包成列表」: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2263) + +```c +// Python/ceval.c —— TARGET(BUILD_LIST) +PyObject *list = PyList_New(oparg); // oparg = 元素个数 +while (--oparg >= 0) { + PyObject *item = POP(); // 从栈顶逐个弹出 + PyList_SET_ITEM(list, oparg, item); // 填进列表(倒着填,顺序正好对) +} +PUSH(list); // 列表压回栈顶 +``` + +![BUILD_LIST 把栈顶值打包成列表](build-list.svg) + +`BUILD_TUPLE`、`BUILD_MAP`(字典)、`COMPARE_OP`(比较)……全是这个模式:操作数已经在栈上备好,指令弹出它们、算出结果、压回栈顶。理解了「**压操作数 → 指令计算 → 压回结果**」这一条,绝大多数表达式字节码都能照着读下来。 + +--- + +小结一下: + +- 取名字分两半:**编译期**由符号表定作用域、选指令;**运行期**指令各查各的名字空间; +- 三条取值指令:**`LOAD_FAST`**(局部,按下标,最快,未赋值则 `UnboundLocalError`)、**`LOAD_GLOBAL`**(全局→内建,缺失则 `NameError`)、**`LOAD_NAME`**(模块层/类体,局部→全局→内建,最灵活也最慢); +- 它们对应 **LEGB** 规则的各层(外层 E 由 `LOAD_DEREF` 负责,留待函数机制章);LEGB 的归类在编译期完成,不是运行期逐层试; +- **表达式求值**统一是栈式接力:压操作数 → 运算符指令弹出计算 → 压回结果;**运算优先级**靠编译期排好的字节码顺序体现,`BUILD_LIST` 等构建指令也是同一套路。 + +直线代码到此清楚了。但真实程序还有 `if`、`while`、`for`——执行不再是一条道走到底。下一章看**控制流**:虚拟机如何靠「跳转」指令改变执行顺序。 diff --git a/docs/vm/expressions-and-names/legb.svg b/docs/vm/expressions-and-names/legb.svg new file mode 100644 index 0000000..920af95 --- /dev/null +++ b/docs/vm/expressions-and-names/legb.svg @@ -0,0 +1,56 @@ + +LEGB:四类作用域与对应的取名字指令 +Python 取名字遵循 LEGB 顺序:先局部 Local,再外层函数 Enclosing,然后全局 Global,最后内建 Builtin。这四类作用域分别由不同指令负责:局部用 LOAD_FAST,外层闭包用 LOAD_DEREF,全局与内建由 LOAD_GLOBAL 一并查(先全局后内建)。编译期就按符号表把名字归到其中一类,选定对应指令。 + + + + + + + + + + + +LEGB:四类作用域,各有对应指令 +查找顺序:Local → Enclosing → Global → Builtin + + + + + LLocal 局部本函数的局部变量 + LOAD_FAST + + + EEnclosing 外层外层函数的变量(闭包) + LOAD_DEREF + + + GGlobal 全局模块级的名字 + LOAD_GLOBAL + + + BBuiltin 内建len、print 等 + (同上,回退到此) + + + + + + +没有↓没有↓没有↓ + + + + +函数里 LOAD_GLOBAL 先查全局、再查内建(两者由它一并负责) + +编译期符号表把名字归到 L/E/G 之一并选好指令;运行期再没找到才报 NameError +(LOAD_DEREF 等闭包细节留到「函数机制」一章) + diff --git a/docs/vm/expressions-and-names/name-lookup.svg b/docs/vm/expressions-and-names/name-lookup.svg new file mode 100644 index 0000000..7ac6874 --- /dev/null +++ b/docs/vm/expressions-and-names/name-lookup.svg @@ -0,0 +1,60 @@ + +LOAD_NAME 与 LOAD_GLOBAL 的运行期字典查找 +LOAD_NAME 在运行期依次查三个名字空间字典:先查局部 f_locals,没有就查全局 f_globals,再没有就查内建 f_builtins,三处都没有才报 NameError。LOAD_GLOBAL 则跳过局部、从全局这一步开始查(全局 → 内建)。与 LOAD_FAST 的一次数组索引相比,它们都要做字典查找,所以更慢。 + + + + + + + + + + + + +运行期查名字空间:依次查字典,查不到才报错 + + + +① 局部 f_locals +{ ... } +没有 → + + +② 全局 f_globals +{ ... } +没有 → + + +③ 内建 f_builtins +len, print … +命中 ✓ + + + + + + + +LOAD_NAME + +从①开始查 + + + +LOAD_GLOBAL + +跳过局部,从②开始 + + + +压入栈 + +三处都没有 → NameError;相比 LOAD_FAST 的一次数组索引,查字典更慢 + diff --git a/docs/vm/expressions-and-names/name-resolution.svg b/docs/vm/expressions-and-names/name-resolution.svg new file mode 100644 index 0000000..dbd93a8 --- /dev/null +++ b/docs/vm/expressions-and-names/name-resolution.svg @@ -0,0 +1,54 @@ + +取名字:编译期定指令,运行期查名字空间 +取一个名字的工作分两半。编译期:符号表已判定名字的作用域,据此选定用哪条指令——局部变量用 LOAD_FAST,函数里的全局引用用 LOAD_GLOBAL,模块顶层与类体用 LOAD_NAME。运行期:每条指令各查不同的名字空间——LOAD_FAST 直接按下标取(不查字典),LOAD_GLOBAL 查全局再查内建,LOAD_NAME 依次查局部、全局、内建。 + + + + + + + + + + +取名字:编译期选指令,运行期查名字空间 + + +编译期:符号表定作用域 → 选指令 +运行期:指令各查不同名字空间 + + + + +局部变量函数内赋值的名字 +LOAD_FAST + +按下标直接取(fastlocals) +不查任何字典,最快 + + + + +全局引用函数里只读的外部名 +LOAD_GLOBAL + +全局 f_globals → +内建 f_builtins + + + + +模块层 / 类体顶层或 class 体内 +LOAD_NAME + +局部 → 全局 → 内建 +运行期依次查三处 + + +作用域在编译期就定了,所以指令一旦选好,运行期不再「猜」名字在哪类作用域 + diff --git a/docs/vm/frame-and-eval-loop/eval-loop.svg b/docs/vm/frame-and-eval-loop/eval-loop.svg new file mode 100644 index 0000000..1938f3d --- /dev/null +++ b/docs/vm/frame-and-eval-loop/eval-loop.svg @@ -0,0 +1,57 @@ + +求值循环:取指令 → 派发 → 执行 → 再取指令 +虚拟机的核心是 ceval.c 里的求值循环。它周而复始地做三件事:先取出下一条字节码指令(NEXTOPARG 解出 opcode 与 oparg),再用一个巨大的 switch 按 opcode 跳转到对应分支,分支里执行该指令、操作求值栈,最后 DISPATCH 回到开头取下一条。如此一条接一条,直到遇到 RETURN_VALUE 返回。 + + + + + + + + + + + + +求值循环:一条接一条地执行字节码 + + + + +① 取下一条指令 +NEXTOPARG() +解出 opcode + oparg + + + +② 按 opcode 派发 +switch (opcode) +跳到对应指令的分支 + + + +③ 执行该指令 +操作求值栈(压/弹/算) +DISPATCH() + + + + + + + + + +DISPATCH:回到 ① 取下一条 —— 周而复始 + + + +遇 RETURN_VALUE → 退出循环 + +返回 + diff --git a/docs/vm/frame-and-eval-loop/frame-object.svg b/docs/vm/frame-and-eval-loop/frame-object.svg new file mode 100644 index 0000000..9370991 --- /dev/null +++ b/docs/vm/frame-and-eval-loop/frame-object.svg @@ -0,0 +1,66 @@ + +帧对象 PyFrameObject:执行一段字节码的现场 +一个帧对象是执行某段字节码的完整现场。它通过 f_code 指向要执行的 code object;通过 f_globals、f_locals、f_builtins 指向取名字要用的三个名字空间;自带一个求值栈(f_valuestack 起点、f_stacktop 栈顶)供指令临时存放操作数;用 f_lasti 记录执行到第几条指令;并通过 f_back 指回调用它的上一个帧,从而把所有帧串成调用链。 + + + + + + + + + + + + +帧对象:执行一段字节码的「现场」 + + + +帧 PyFrameObject + + f_back + f_code + f_globals + f_locals + f_builtins + f_valuestack + + + + +调用者的帧 + +f_back + + + +code object +co_code 字节码 + 各表 + +f_code + + + +三个名字空间 +局部 / 全局 / 内建 +取名字时按序查这三处 + + + + +求值栈 +指令临时存放操作数的地方 +f_stacktop 指向栈顶 + + +f_lasti +记录执行到第几条指令 +(执行进度) + + diff --git a/docs/vm/frame-and-eval-loop/frame-stack.svg b/docs/vm/frame-and-eval-loop/frame-stack.svg new file mode 100644 index 0000000..332013b --- /dev/null +++ b/docs/vm/frame-and-eval-loop/frame-stack.svg @@ -0,0 +1,48 @@ + +帧栈:f_back 把帧串成调用链 +每发生一次函数调用,就新建一个帧并压到帧栈顶;每个帧的 f_back 指回调用它的上一个帧。outer() 调用 inner() 时,inner 的帧在栈顶、f_back 指向 outer 帧,outer 帧再指向模块帧。这条由 f_back 串起来的链就是调用栈,出错时打印的 traceback 正是顺着它回溯的。 + + + + + + + + +帧栈:每次调用压一个帧,f_back 指回调用者 + + + + +<module> 帧 +模块顶层代码,调用链的底部 +f_back = NULL + + + +outer 帧 +局部变量 msg = "hi" + + + +inner 帧 ← 栈顶(正在执行) +sys._getframe() 拿到的就是它 + + + +f_back + +f_back + + +后调用 +先调用 + + +调用链:inner → outer → <module>,traceback 正是顺着 f_back 回溯打印 + diff --git a/docs/vm/frame-and-eval-loop/index.md b/docs/vm/frame-and-eval-loop/index.md new file mode 100644 index 0000000..035486d --- /dev/null +++ b/docs/vm/frame-and-eval-loop/index.md @@ -0,0 +1,145 @@ +# Python 虚拟机框架:帧对象与求值循环 + +第三部分我们把源码编译成了 code object,里面装着字节码。可字节码只是一串静态的指令,谁来执行它?答案就是 **Python 虚拟机**——而虚拟机的核心,是本章的两个主角:**帧对象**和**求值循环**。 + +打个比方:code object 是一张乐谱(写死的音符),帧对象是某次演奏的现场(这次用哪架钢琴、弹到第几小节、手边的临时记号),求值循环则是演奏者本人——照着乐谱一个音一个音地弹下去。 + +## 栈式虚拟机:先建立直觉 + +动手之前先建立一个总印象:**CPython 是一台「基于栈」的虚拟机**。它执行指令时,操作数不放在寄存器里,而是放在一个**求值栈**上——指令从栈顶取操作数、把结果压回栈顶。 + +以 `c = a + b` 为例(设 `a=5`、`b=3`),上一章我们见过它的字节码:`LOAD_FAST a`、`LOAD_FAST b`、`BINARY_ADD`、`STORE_FAST c`。它们在求值栈上是这样接力的: + +![求值栈执行 c = a + b](value-stack.svg) + +- `LOAD_FAST a`:把 `a` 的值 `5` 压栈; +- `LOAD_FAST b`:把 `b` 的值 `3` 压栈; +- `BINARY_ADD`:弹出栈顶两个值相加,把结果 `8` 压回栈顶; +- `STORE_FAST c`:弹出 `8`,存进变量 `c`。栈又空了。 + +每条指令只和栈顶打交道,谁也不必写明「操作数在哪个寄存器」。这就是栈式虚拟机的简洁之处。而这个求值栈,连同执行所需的一切,都装在**帧对象**里。 + +## 帧对象:执行一段字节码的现场 + +执行一段字节码,光有字节码不够——还要知道:局部变量放哪、全局名去哪查、求值栈在哪、执行到第几条了。把这些「执行现场」打包起来的,就是**帧对象**(`PyFrameObject`): + +`源文件:`[Include/frameobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/frameobject.h#L17) + +```c +// Include/frameobject.h —— PyFrameObject(节选) +typedef struct _frame { + PyObject_VAR_HEAD + struct _frame *f_back; // 上一个帧(调用者),串成调用链 + PyCodeObject *f_code; // 要执行的 code object(字节码在此) + PyObject *f_builtins; // 内建名字空间 + PyObject *f_globals; // 全局名字空间 + PyObject *f_locals; // 局部名字空间 + PyObject **f_valuestack; // 求值栈的起点 + PyObject **f_stacktop; // 求值栈的栈顶(下一个空位) + int f_lasti; // 上一条执行的指令位置(执行进度) + ...... + PyObject *f_localsplus[1]; // 局部变量 + 求值栈,动态分配 +} PyFrameObject; +``` + +![帧对象:执行现场](frame-object.svg) + +逐一对应到「执行需要什么」: + +- **执行什么**:`f_code` 指向 code object——字节码和那几张表都在里面。 +- **名字去哪查**:`f_locals`、`f_globals`、`f_builtins` 三个名字空间。取一个名字时按「局部 → 全局 → 内建」的顺序查这三处(细节是下一章的主题)。 +- **临时数据放哪**:`f_valuestack` 是求值栈起点,`f_stacktop` 指向当前栈顶。上一节那些压栈弹栈,操作的就是这里。 +- **执行到哪了**:`f_lasti` 记录刚执行到第几条指令,循环据此知道下一条该取哪条。 +- **谁调用了我**:`f_back` 指向调用者的帧——这把所有帧串了起来。 + +最后那个 `f_localsplus` 值得一提:**局部变量和求值栈其实共用同一块连续内存**(局部变量在前、求值栈在后),一次分配、紧凑高效。这也是为什么 `LOAD_FAST`(取局部变量)特别快——直接按下标访问这块数组。 + +## 帧栈:函数调用串成的链 + +`f_back` 串起来的,正是我们熟悉的**调用栈**。每调用一个函数,就**新建一个帧**压到栈顶;函数返回,帧就弹掉。当前正在执行的,永远是栈顶那个帧。 + +用 `sys._getframe()` 可以拿到当前帧,顺着 `f_back` 往回走就能看到整条调用链: + +```python +>>> import sys +>>> def inner(): +... f = sys._getframe() +... chain = f.f_code.co_name + " <- " + f.f_back.f_code.co_name \ +... + " <- " + f.f_back.f_back.f_code.co_name +... print("当前帧 co_name :", f.f_code.co_name) +... print("调用链 :", chain) +... print("调用者 outer 的局部变量:", f.f_back.f_locals) +... +>>> def outer(): +... msg = "hi" +... inner() +... +>>> outer() +当前帧 co_name : inner +调用链 : inner <- outer <- +调用者 outer 的局部变量: {'msg': 'hi'} +``` + +![帧栈:f_back 串成调用链](frame-stack.svg) + +看最后一行——通过 `inner` 帧的 `f_back`,我们直接读到了 `outer` 帧里的局部变量 `msg`。每个帧都保存着自己那一层的现场,互不干扰;而 `f_back` 让它们连成一条可回溯的链。**程序出错时打印的 traceback,正是顺着 `f_back` 一层层回溯出来的**——「谁调用了谁」全写在这条链上。 + +## 求值循环:虚拟机的心脏 + +有了帧(现场),就该有人照着它把字节码一条条执行下去了。这个执行者,就是 [Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L551) 里的 **`_PyEval_EvalFrameDefault`**——整个 CPython 跑得最频繁、最核心的一段代码。它拿到一个帧,就进入一个**循环**,周而复始地做三件事: + +![求值循环](eval-loop.svg) + +1. **取指令**:从字节码里取出下一条,解出操作码 `opcode` 和参数 `oparg`; +2. **派发**:用一个**巨大的 `switch`** 按 `opcode` 跳到对应分支; +3. **执行**:分支里执行这条指令(多半是操作求值栈),然后回到第 1 步取下一条。 + +源码里这个结构清晰可见(精简后): + +```c +// Python/ceval.c —— _PyEval_EvalFrameDefault 主循环(精简) +for (;;) { + ...... + NEXTOPARG(); // ① 取出 opcode 和 oparg + switch (opcode) { // ② 按 opcode 派发 + TARGET(LOAD_FAST): ... // 各条指令各一个分支 + TARGET(BINARY_ADD): ... + ...... + } + DISPATCH(); // ③ 回到循环开头,取下一条 +} +``` + +这就是虚拟机的全部骨架:**一个大循环 + 一个大 switch**,每种字节码指令在 switch 里有一个分支。把上一节的 `BINARY_ADD` 分支翻出来看,正是栈式操作的真身: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1265) + +```c +// Python/ceval.c —— TARGET(BINARY_ADD) +TARGET(BINARY_ADD) { + PyObject *right = POP(); // 弹出右操作数 + PyObject *left = TOP(); // 取栈顶的左操作数 + ...... + sum = PyNumber_Add(left, right); // 相加 + ...... + SET_TOP(sum); // 结果写回栈顶 + if (sum == NULL) + goto error; + DISPATCH(); // 取下一条 +} +``` + +`POP`、`TOP`、`SET_TOP` 操作的就是当前帧的求值栈;`DISPATCH()` 则回到循环开头。一条指令做完,循环就转一圈;如此一圈圈转下去,直到遇到 `RETURN_VALUE`——它把栈顶的值作为返回值、退出循环,这个帧的使命就结束了,控制权交还给 `f_back` 指向的调用者帧。 + +> 实际源码为了快,用了「计算跳转(computed goto)」等技巧让派发更高效,`DISPATCH`/`FAST_DISPATCH` 就是它的封装。但骨架仍是「取指令 → 派发 → 执行 → 再取指令」这个循环,理解到这一层就够了。 + +--- + +小结一下虚拟机的框架: + +- CPython 是**基于栈**的虚拟机:指令从**求值栈**栈顶取操作数、把结果压回栈顶; +- **帧对象**(`PyFrameObject`)是执行一段字节码的「现场」:`f_code`(执行什么)、三个名字空间(名字去哪查)、求值栈(临时数据)、`f_lasti`(执行进度)、`f_back`(谁调用了我);局部变量与求值栈共用 `f_localsplus` 一块内存; +- `f_back` 把帧串成**调用栈**,traceback 就是顺着它回溯的; +- **求值循环**(`_PyEval_EvalFrameDefault`)是虚拟机的心脏:**一个大循环 + 一个大 switch**,反复「取指令 → 按 opcode 派发 → 执行(操作求值栈)→ 取下一条」,直到 `RETURN_VALUE`。 + +骨架立起来了。但我们还没细看那些指令具体怎么取名字、怎么算表达式、怎么跳转。下一章就从最常见的**一般表达式与名字空间**入手,看看 `LOAD_FAST`、`LOAD_GLOBAL` 这些指令到底如何工作。 diff --git a/docs/vm/frame-and-eval-loop/value-stack.svg b/docs/vm/frame-and-eval-loop/value-stack.svg new file mode 100644 index 0000000..ac1d879 --- /dev/null +++ b/docs/vm/frame-and-eval-loop/value-stack.svg @@ -0,0 +1,54 @@ + +求值栈执行 c = a + b(a=5, b=3) +Python 虚拟机是基于栈的。执行 c = a + b 时:LOAD_FAST a 把 5 压栈,LOAD_FAST b 把 3 压栈,BINARY_ADD 弹出两个值相加、把结果 8 压回栈顶,STORE_FAST c 弹出 8 存进变量 c,栈又空了。每条指令都只跟栈顶打交道,操作数不需要写明寄存器。 + + + + + + + + +求值栈执行 c = a + b (设 a=5, b=3) + + + + + + + + +LOAD_FAST a + +5 +压入 5 + + +LOAD_FAST b + +5 +3 +再压入 3 + + +BINARY_ADD + +8 +弹 3、5,压回 8 + + +STORE_FAST c + +(空) +弹 8 存进 c + + + +c = 8 + + diff --git a/docs/vm/functions/arg-binding.svg b/docs/vm/functions/arg-binding.svg new file mode 100644 index 0000000..56d3e48 --- /dev/null +++ b/docs/vm/functions/arg-binding.svg @@ -0,0 +1,55 @@ + +参数绑定:实参如何落进 fastlocals +以 def f(a, b, c=3, *args, **kw) 调用 f(1, 2, 9, 8, x=5) 为例。_PyEval_EvalCodeEx 把实参填进帧的局部变量区 fastlocals。位置参数 1、2 对号入座填进 a、b 槽;c 没传,用 func_defaults 里的默认值 3 补;多余的位置参数 9、8 打包成元组进 args 槽;关键字参数 x=5 没有形参接住,进 kw 字典。各种参数 TypeError 都诞生在这一绑定过程。 + + + + + + + + + + + +参数绑定:f(1, 2, 9, 8, x=5) +def f(a, b, c=3, *args, **kw) + + +传入的实参 + + 1(位置) + 2(位置) + 9(位置) + 8(位置) + x=5(关键字) + + + +帧的 fastlocals 槽 + + a = 1 + b = 2 + c = 3 (func_defaults 补) + args = (9, 8) + kw = {'x': 5} + + + + + + + + + +多余 → *args + +① 位置对号入座 ② 多余进 *args ③ 关键字按名匹配 ④ 缺位用默认值补 ⑤ 没接住的进 **kw +缺/多/重/不认识 → 各种 TypeError 都生于此 +关键字匹配用裸指针比较加速(形参名多为 interned 字符串) + diff --git a/docs/vm/functions/call-pipeline.svg b/docs/vm/functions/call-pipeline.svg new file mode 100644 index 0000000..e889e6b --- /dev/null +++ b/docs/vm/functions/call-pipeline.svg @@ -0,0 +1,52 @@ + +函数调用流水线:从 CALL_FUNCTION 到新建一帧 +调用一个 Python 函数的链路。字节码 CALL_FUNCTION 把活儿交给 call_function,它按被调对象类型分派,Python 函数走 _PyFunction_FastCallKeywords,最终落到核心 _PyEval_EvalCodeEx。后者新建一个帧、把实参绑定进帧的局部变量区 fastlocals,然后调起上一章的求值循环 _PyEval_EvalFrameDefault 执行字节码。一句话:调用等于新建一帧加绑定参数加跑求值循环。 + + + + + + + + + + +调用 = 新建一帧 + 绑定参数 + 跑求值循环 + + + +CALL_FUNCTION +字节码指令 + + +call_function +按类型分派 + + +_PyFunction_ +FastCallKeywords + + +_PyEval_ +EvalCodeEx + + + + + + + + +_PyEval_EvalCodeEx 的两件大事 +① 新建帧 + 绑定参数 +② 跑求值循环 + + + +_PyEval_EvalFrameDefault(上一章的心脏) + diff --git a/docs/vm/functions/cell-closure.svg b/docs/vm/functions/cell-closure.svg new file mode 100644 index 0000000..8924d6a --- /dev/null +++ b/docs/vm/functions/cell-closure.svg @@ -0,0 +1,48 @@ + +闭包用 cell 盒子共享外层变量 +闭包的物理基础是 cell 对象,一个只有 ob_ref 一个字段的盒子。被内层函数引用的外层变量不作为普通局部变量存放,而是装进 cell。外层函数 make_counter 的 co_cellvars 里的 count,与内层函数 inc 的 co_freevars 里的 count,指向同一个 cell 盒子。外层把 count 装进盒子,内层通过闭包拿到同一个盒子的引用,于是双方读写同一处。哪怕 make_counter 早已返回、它的帧本该销毁,cell 盒子仍被 inc 的闭包引用而存活,这就是闭包记得住还能改的原因。 + + + + + + + + + + +cell:外层与内层共享同一个盒子 + + + +make_counter 帧 +co_cellvars: ('count',) + +count(cellvar) + + + +inc 帧 +co_freevars: ('count',) + +count(freevar) + + + +cell 盒子 + +ob_ref = 3 + + + + +装进盒子 +闭包拿到同一个盒子 + +make_counter 返回后帧虽销毁,盒子仍被 inc 的闭包引用而存活 + diff --git a/docs/vm/functions/closure-assembly.svg b/docs/vm/functions/closure-assembly.svg new file mode 100644 index 0000000..f738c50 --- /dev/null +++ b/docs/vm/functions/closure-assembly.svg @@ -0,0 +1,53 @@ + +闭包的组装与读写 +盒子怎么从外层传到内层函数手里。外层函数在 def inc 之前,用 LOAD_CLOSURE 把 count 的 cell 压栈、打包成闭包元组,MAKE_FUNCTION 带 0x08 标志把它装进 inc 函数对象的 func_closure。于是 inc 一出生就带着外层那个盒子。运行时,对 cell 的读写是专门的两条指令:LOAD_DEREF 透过盒子取值,STORE_DEREF 透过盒子存值。 + + + + + + + + + + +闭包的组装:把盒子传给内层函数 + + + +LOAD_CLOSURE +把 count 的 cell 压栈 + + +BUILD_TUPLE +打包成闭包元组 + + +MAKE_FUNCTION +oparg & 0x08 + + + + + + + +inc 函数对象 +func_closure = (cell,) + + +运行时透过盒子读写 + + + LOAD_DEREFPyCell_GET → 取值 + + STORE_DEREFPyCell_SET → 存值 + + +inc 一出生就带着外层的盒子,之后用 DEREF 指令透过它读写 count + diff --git a/docs/vm/functions/defaults-once.svg b/docs/vm/functions/defaults-once.svg new file mode 100644 index 0000000..3ad2e5c --- /dev/null +++ b/docs/vm/functions/defaults-once.svg @@ -0,0 +1,46 @@ + +默认值在 def 时求值一次,调用间共享 +默认值在 def 执行时求值一次,结果存进函数对象的 func_defaults 元组。之后每次调用只是从这个元组里取来填缺位,所有调用共享同一份默认值对象。若默认值是可变对象比如空列表,这个共享会显形:def append(x, lst=[]) 里的空列表只创建一个,存在 __defaults__ 里,append(1) 和 append(2) 修改的是同一个列表,于是结果累积成 [1, 2]。这就是可变默认值陷阱的根源。 + + + + + + + + + + +默认值 def 时求值一次,被调用共享 +def append(x, lst=[]): lst.append(x); return lst + + + +def 执行时 +[] 求值一次 → 存入 + + + +append.__defaults__ +( [ ] ,) ← 唯一的列表 + + + + +append(1) +→ [1] + +append(2) +→ [1, 2] 累积! + + + +两次调用改的是同一个列表 + +默认值只在 def 时创建一次、挂在函数对象上 —— 可变默认值陷阱的根源 + diff --git a/docs/vm/functions/func-vs-code.svg b/docs/vm/functions/func-vs-code.svg new file mode 100644 index 0000000..08373a6 --- /dev/null +++ b/docs/vm/functions/func-vs-code.svg @@ -0,0 +1,46 @@ + +函数对象与 code 对象的分工 +一个 Python 函数是两样东西的组合。左边是 code object,编译好的静态字节码乐谱,可被多个函数对象共享,在循环里反复 def 时底层是同一个 code object。右边是 PyFunctionObject,运行期对象,背着 func_code 指向那份乐谱,外加随上下文而变的随身道具:func_globals 全局名字空间、func_defaults 默认值、func_closure 闭包、func_kwdefaults 等。静态的可共享,随上下文变的挂在函数对象上。 + + + + + + + + + +一个函数 = 乐谱(code)+ 演奏者(函数对象) + + + + + +PyFunctionObject(运行期) + + func_code→ 乐谱 + func_globals全局名字空间 + func_defaults默认值元组 + func_kwdefaultskw 默认值 + func_closure闭包 cell 元组 + +随上下文而变 · 每次 def 新建一个 + + + +code object +co_code(字节码) +co_consts / co_varnames +静态乐谱 · 编译一次 +可被多个函数对象共享 + + +func_code 指向 + +a.__code__ is b.__code__ → True,但 a is b → False + diff --git a/docs/vm/functions/index.md b/docs/vm/functions/index.md new file mode 100644 index 0000000..9de5d25 --- /dev/null +++ b/docs/vm/functions/index.md @@ -0,0 +1,287 @@ +# 函数机制:调用、参数与闭包 + +前面几章我们一直在说一句话:「调用一个函数,就**新建一个帧**」。可这「新建一帧」到底是怎么发生的?参数怎么从调用处传进帧里?默认值、`*args`、`**kwargs` 在哪一步安排?嵌套函数里的闭包又是怎么记住外层变量的?这一章把这些一次讲清。 + +先建立一个总印象:**一个 Python 函数其实是「两样东西」的组合**——一份静态的 **code object**(编译好的字节码,第三部分讲过),加一个运行期的 **函数对象**(`PyFunctionObject`,记着它的全局名字空间、默认值、闭包)。沿用之前的比喻:code object 是乐谱,函数对象是「拿着这份乐谱、还配好了随身道具的演奏者」。调用,则是为这次演奏**新建一个现场(帧)**。 + +![函数对象 vs code 对象](func-vs-code.svg) + +## def 做了什么:MAKE_FUNCTION 造出函数对象 + +`def` 不是「定义」那么玄,它是一条**会执行的语句**:运行到它时,虚拟机现场**造一个函数对象**出来。负责这件事的指令是 `MAKE_FUNCTION`: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L3196) + +```c +// Python/ceval.c —— TARGET(MAKE_FUNCTION)(精简) +PyObject *qualname = POP(); +PyObject *codeobj = POP(); +PyFunctionObject *func = (PyFunctionObject *) + PyFunction_NewWithQualName(codeobj, f->f_globals, qualname); // 绑定 code + 当前全局名字空间 +if (oparg & 0x08) func->func_closure = POP(); // 有闭包 +if (oparg & 0x04) func->func_annotations = POP(); // 有注解 +if (oparg & 0x02) func->func_kwdefaults = POP(); // 有关键字默认值 +if (oparg & 0x01) func->func_defaults = POP(); // 有默认值 +PUSH((PyObject *)func); +``` + +可以看到,`MAKE_FUNCTION` 把一堆「随身道具」挂到函数对象上——而这些道具,编译器早已用前几条指令在栈上备好(默认值打包成元组、闭包打包成 cell 元组……)。`oparg` 的四个标志位说明这次 `def` 带了哪些。装好的 `PyFunctionObject` 长这样: + +`源文件:`[Include/funcobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/funcobject.h#L21) + +```c +// Include/funcobject.h —— PyFunctionObject(节选) +typedef struct { + PyObject_HEAD + PyObject *func_code; // __code__:那份静态的字节码乐谱 + PyObject *func_globals; // __globals__:到哪查全局名(def 所在模块的全局) + PyObject *func_defaults; // __defaults__:位置参数默认值(一个元组) + PyObject *func_kwdefaults; // __kwdefaults__:keyword-only 参数默认值(一个 dict) + PyObject *func_closure; // __closure__:闭包(一个 cell 元组) + ...... +} PyFunctionObject; +``` + +**关键区别在这里**:字节码(`func_code`)是静态、可共享的;而默认值、全局名字空间、闭包这些**随上下文而变**的东西,全挂在函数对象上。同一份 code object 可以被多个函数对象引用——比如在循环里反复 `def`,每轮造的是**新的函数对象**,但它们底下的 code object 是同一个: + +```python +>>> def make(): +... def f(): pass +... return f +... +>>> a, b = make(), make() +>>> a is b # 两个不同的函数对象 +False +>>> a.__code__ is b.__code__ # 但共享同一份 code object(乐谱只编译一次) +True +>>> a.__globals__ is a.__code__ # 函数对象额外背着全局名字空间等运行期信息 +False +``` + +## 调用:从 CALL_FUNCTION 到新建一帧 + +有了函数对象,`f(1, 2)` 这样的调用编译成 `CALL_FUNCTION`。它在求值栈上的布局很简单:**函数对象在下、参数在上**,`oparg` 就是参数个数。它把活儿交给 `call_function`: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L3114) + +```c +// Python/ceval.c —— TARGET(CALL_FUNCTION) +PyObject **sp = stack_pointer; +res = call_function(&sp, oparg, NULL); // oparg 个位置参数 +stack_pointer = sp; +PUSH(res); +``` + +`call_function` 会按被调对象的类型分派:C 函数走 C 的快速通道,而我们关心的 Python 函数走 `_PyFunction_FastCallKeywords`,它最终落到**整个函数调用的核心** `_PyEval_EvalCodeEx`——上一章见过的求值循环 `_PyEval_EvalFrameDefault`,正是由它建好帧之后调起的。这条链路就是「调用 = 新建一帧 + 绑定参数 + 跑求值循环」的全貌: + +![函数调用流水线](call-pipeline.svg) + +`源文件:`[Objects/call.c](https://github.com/python/cpython/blob/v3.7.0/Objects/call.c#L386) + +```c +// Objects/call.c —— _PyFunction_FastCallKeywords(精简) +PyCodeObject *co = PyFunction_GET_CODE(func); +PyObject *globals = PyFunction_GET_GLOBALS(func); // 函数对象背着的全局名字空间 +PyObject *argdefs = PyFunction_GET_DEFAULTS(func); // 默认值 +// 简单情形(无 kwargs、参数刚好对上)走 function_code_fastcall 抄近路; +// 否则交给通用的 _PyEval_EvalCodeEx 做完整的参数绑定 +return _PyEval_EvalCodeEx(co, globals, ..., argdefs, ..., closure, ...); +``` + +注意 `globals`、`argdefs`、`closure` 都是从**函数对象**里取出来的——这正是上一节强调「随身道具挂在函数对象上」的用处:真正调用时,它们被一并喂给帧。 + +## 参数绑定:实参如何落进 fastlocals + +`_PyEval_EvalCodeEx` 建好帧后,第一件大事就是把调用处传来的实参,按规则填进帧的**局部变量区**(还记得上一章的 `f_localsplus` 吗?局部变量就排在它最前面,`LOAD_FAST` 按下标直取)。这套绑定规则,正是 Python 函数签名所有花样的实现: + +![参数绑定到 fastlocals](arg-binding.svg) + +按源码的顺序,绑定分几步走: + +**① 位置参数对号入座。** 前 `co_argcount` 个形参槽,依次填入传来的位置实参: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L3706) + +```c +// Python/ceval.c —— _PyEval_EvalCodeEx:位置参数(精简) +n = (argcount > co->co_argcount) ? co->co_argcount : argcount; +for (i = 0; i < n; i++) { + Py_INCREF(args[i]); + SETLOCAL(i, args[i]); // 第 i 个实参 → 第 i 个局部槽 +} +``` + +**② 多余的位置参数打进 `*args`。** 若形参里有 `*args`(`CO_VARARGS` 标志),把超出 `co_argcount` 的实参打包成一个元组,放进对应槽: + +```c +// Python/ceval.c —— 多余位置参数 → *args 元组(精简) +if (co->co_flags & CO_VARARGS) { + u = PyTuple_New(argcount - n); + for (i = n; i < argcount; i++) { Py_INCREF(args[i]); PyTuple_SET_ITEM(u, i-n, args[i]); } + SETLOCAL(total_args, u); +} +``` + +**③ 关键字参数按名匹配。** 每个 `key=value`,拿 `key` 去和形参名表 `co_varnames` 比对,命中就填进那个槽。源码这里有个小优化——形参名通常是 interned 字符串,先做**裸指针比较**,几乎总能命中,省下字符串比较: + +```c +// Python/ceval.c —— 关键字参数匹配(精简) +for (j = 0; j < total_args; j++) + if (co_varnames[j] == keyword) goto kw_found; // 裸指针比较,快 +// ……没匹配上:若有 **kwargs 就塞进去,否则报 unexpected keyword argument +``` + +没有任何形参接得住的关键字参数,若函数声明了 `**kwargs`(`CO_VARKEYWORDS`)就塞进那个 dict;否则就是我们熟悉的 `got an unexpected keyword argument`。 + +**④ 缺的位置参数用默认值补。** 走到这一步还空着的位置槽,从函数对象的 `func_defaults` 里取默认值填上;仍填不满,就是 `missing N required positional argument`: + +```c +// Python/ceval.c —— 用默认值补缺(精简) +if (argcount < co->co_argcount) { + Py_ssize_t m = co->co_argcount - defcount; // 没有默认值的形参个数 + ...... + for (; i < defcount; i++) + if (GETLOCAL(m+i) == NULL) { Py_INCREF(defs[i]); SETLOCAL(m+i, defs[i]); } +} +``` + +**⑤ keyword-only 参数**则类似地用 `func_kwdefaults`(一个 dict)补缺。绑定全部做完,`fastlocals` 就备齐了,求值循环可以开跑。我们平时报的那些 `TypeError`——参数多了、少了、重了、名字不认识——全都诞生在这一段: + +```python +>>> def f(a, b, c=3): pass +>>> f(1) +TypeError: f() missing 1 required positional argument: 'b' +>>> f(1, 2, 3, 4) +TypeError: f() takes from 2 to 3 positional arguments but 4 were given +>>> f(1, 2, a=9) +TypeError: f() got multiple values for argument 'a' +``` + +## 默认值的真相:def 时求值一次 + +第 ④ 步藏着一个 Python 老手都该懂的细节:**默认值是在 `def` 执行时求值一次、存进 `func_defaults`**,之后每次调用只是「从这个元组里取来填缺」。也就是说,所有调用**共享同一份默认值对象**。 + +![默认值在 def 时求值一次](defaults-once.svg) + +如果默认值是个可变对象(比如 `[]`),这个「共享」就会显形——著名的可变默认值陷阱: + +```python +>>> def append(x, lst=[]): # [] 只在 def 时创建这一个 +... lst.append(x) +... return lst +... +>>> append(1) +[1] +>>> append(2) # 还是同一个 lst! +[1, 2] +>>> append.__defaults__ # 默认值就存在这里,被反复修改 +([1, 2],) +``` + +看最后一行——`__defaults__` 这个元组从头到尾就是 `def` 时创建的那一个,两次调用改的是同一个列表。理解了「默认值 def 时求值一次、挂在函数对象上」,这个陷阱就不再神秘。 + +## 闭包:用 cell 把外层变量「装进盒子」 + +最后是函数机制里最精巧的一块——**闭包**。嵌套函数能记住外层函数的局部变量,哪怕外层早已返回: + +```python +>>> def make_counter(): +... count = 0 +... def inc(): +... nonlocal count +... count += 1 +... return count +... return inc +... +>>> c = make_counter() +>>> c(); c(); c() +1 +2 +3 +``` + +`make_counter` 早返回了,它的帧本该销毁,可 `inc` 还在不断读写 `count`。秘密在于:这种被内层引用的变量,不会作为普通局部变量存放,而是被装进一个 **cell 对象**——一个只有一个字段的「盒子」: + +`源文件:`[Include/cellobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/cellobject.h#L9) + +```c +// Include/cellobject.h +typedef struct { + PyObject_HEAD + PyObject *ob_ref; // 盒子里装的东西(cell 为空时为 NULL) +} PyCellObject; +``` + +编译器在分析作用域时就分好了类:被内层函数引用的外层变量,记进外层 code 的 **`co_cellvars`**;内层函数从外层捕获来的变量,记进内层 code 的 **`co_freevars`**。建帧时,`_PyEval_EvalCodeEx` 为每个 cellvar 造一个 cell,并把闭包传来的 cell 拷进 freevars 区: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L3852) + +```c +// Python/ceval.c —— 建帧时安排 cell 与 free 变量(精简) +for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) { + // 若该 cell 变量同时是个参数,就把已绑定的实参装进盒子,否则建空盒子 + c = (co->co_cell2arg && ...) ? PyCell_New(GETLOCAL(arg)) : PyCell_New(NULL); + SETLOCAL(co->co_nlocals + i, c); +} +for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i) { + PyObject *o = PyTuple_GET_ITEM(closure, i); // 从函数对象的 func_closure 取来的 cell + Py_INCREF(o); + freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o; +} +``` + +关键在于:**外层的 cellvar 和内层的 freevar 指向同一个 cell 盒子**。外层把 `count` 装进盒子,内层通过闭包拿到的是**同一个盒子的引用**——于是双方读写的是同一处。这就是闭包「记得住、还能改」的物理基础: + +![cell:外层与内层共享同一个盒子](cell-closure.svg) + +对 cell 的读写,是专门的两条指令 `LOAD_DEREF`(透过盒子取值)和 `STORE_DEREF`(透过盒子存值): + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2212) + +```c +// Python/ceval.c —— 透过 cell 读写 +TARGET(LOAD_DEREF) { + PyObject *cell = freevars[oparg]; + PyObject *value = PyCell_GET(cell); // 取盒子里的东西 + ...... + PUSH(value); +} +TARGET(STORE_DEREF) { + PyObject *v = POP(); + PyObject *cell = freevars[oparg]; + PyCell_SET(cell, v); // 把东西放进盒子 +} +``` + +而那个盒子是怎么从外层传到内层函数手里的?靠 `LOAD_CLOSURE` + `MAKE_FUNCTION`:外层在 `def inc` 之前,用 `LOAD_CLOSURE` 把 `count` 的 cell 压栈、打包成 `func_closure` 元组,`MAKE_FUNCTION`(`oparg & 0x08`)把它装进 `inc` 函数对象。于是 `inc` 一出生就带着外层那个盒子: + +![闭包的组装](closure-assembly.svg) + +```python +>>> c.__closure__ # inc 带着的闭包:一个 cell 元组 +(,) +>>> c.__closure__[0].cell_contents # 盒子里现在装着 count 的当前值 +3 +>>> c.__code__.co_freevars # inc 从外层捕获的自由变量名 +('count',) +``` + +这也顺带解开另一个经典困惑——循环里建闭包,捕获的是**盒子**而非当时的值,所以循环结束后大家看到的是同一个最终值: + +```python +>>> fns = [lambda: i for i in range(3)] +>>> [fn() for fn in fns] +[2, 2, 2] # 三个 lambda 共享捕获 i 的同一个盒子,最终都是 2 +``` + +--- + +小结一下函数机制: + +- 一个函数 = 静态的 **code object**(共享的字节码乐谱)+ 运行期的 **`PyFunctionObject`**(背着 `func_globals`、`func_defaults`、`func_closure` 等随身道具);`def` 由 **`MAKE_FUNCTION`** 现场造出后者; +- **调用**走 `CALL_FUNCTION → call_function → _PyFunction_FastCallKeywords → _PyEval_EvalCodeEx`,归结为「**新建一帧 + 绑定参数 + 跑求值循环**」; +- **参数绑定**在 `_PyEval_EvalCodeEx` 里把实参填进 `fastlocals`:位置参数对号入座、多余的进 `*args`、关键字按名匹配(裸指针比较加速)、没接住的进 `**kwargs`、缺的用 `func_defaults`/`func_kwdefaults` 补;各种参数 `TypeError` 都生于此; +- **默认值**在 `def` 时求值一次、存于 `func_defaults`,调用间共享——这是可变默认值陷阱的根源; +- **闭包**用 **cell** 盒子实现:外层 `co_cellvars` 与内层 `co_freevars` 指向同一个 cell,`LOAD_DEREF`/`STORE_DEREF` 透过盒子读写,`LOAD_CLOSURE`/`MAKE_FUNCTION` 负责把盒子传给内层函数。 + +函数调用「建帧—绑参—执行—返回」的闭环到此完整。但有一种函数很特别:它执行到一半 `yield` 一下就「暂停」,把帧**原地冻住**、控制权交还给调用者,下次再从断点继续。这种「可暂停的函数」是怎么做到的?下一章就拆**生成器与协程**。 diff --git a/docs/vm/functions/make-function.svg b/docs/vm/functions/make-function.svg new file mode 100644 index 0000000..1d5d2aa --- /dev/null +++ b/docs/vm/functions/make-function.svg @@ -0,0 +1,50 @@ + +MAKE_FUNCTION 从求值栈组装函数对象 +def 是一条会执行的语句。运行到它时,编译器已用前几条指令在求值栈上备好了随身道具:可选的默认值元组、kwdefaults 字典、注解字典、闭包 cell 元组,以及栈顶的 code object 和 qualname。MAKE_FUNCTION 弹出 code 和 qualname 造出 PyFunctionObject,再按 oparg 的四个标志位 0x01 默认值、0x02 kwdefaults、0x04 注解、0x08 闭包,依次弹栈装到函数对象上,最后把函数对象压回栈顶。 + + + + + + + + + + +def → MAKE_FUNCTION 装配函数对象 + + +求值栈(编译器已备好) +qualname(栈顶) +code object +闭包 cell 元组 ? +注解 dict ? +kwdefaults ? +默认值元组 ? +蓝色按 oparg 标志位可选 + + + +MAKE_ +FUNCTION + + + + + +PyFunctionObject + + func_code / func_globals + func_closureoparg & 0x08 + func_annotationsoparg & 0x04 + func_kwdefaultsoparg & 0x02 + func_defaultsoparg & 0x01 + + +弹栈逐项装配,最后把函数对象压回栈顶 + diff --git a/docs/vm/generators/coroutine-eventloop.svg b/docs/vm/generators/coroutine-eventloop.svg new file mode 100644 index 0000000..b3bd2dd --- /dev/null +++ b/docs/vm/generators/coroutine-eventloop.svg @@ -0,0 +1,51 @@ + +事件循环靠 send 驱动协程树 +协程复用生成器的挂起恢复机制。async def 的 code object 带 CO_COROUTINE 标志,await 等于 GET_AWAITABLE 加 YIELD_FROM。事件循环手握一棵协程构成的调用树,靠 send 一次次驱动它们前进。某个协程 await 一个 IO 时挂起、交出控制权,事件循环转去推进别的协程;IO 就绪后再 send 回去唤醒它。成千上万个协程就这样在单线程里轮流推进,互不阻塞。生成器是数据的暂停,协程是控制权的暂停。 + + + + + + + + + + + +事件循环用 send 驱动协程,await 时让出 + + + +事件循环 +单线程 +轮流 send 驱动 +谁就绪推进谁 + + + +协程 A +运行中 + + +协程 B +await IO → 挂起,让出 + + +协程 C +IO 就绪 → 被唤醒 + + + + + +send +就绪后 send 回去 + +await = GET_AWAITABLE + YIELD_FROM,复用生成器同一套挂起/恢复 +生成器是「数据的暂停」,协程是「控制权的暂停」 + diff --git a/docs/vm/generators/exhaust-stopiteration.svg b/docs/vm/generators/exhaust-stopiteration.svg new file mode 100644 index 0000000..730145e --- /dev/null +++ b/docs/vm/generators/exhaust-stopiteration.svg @@ -0,0 +1,44 @@ + +生成器耗尽:return 变成 StopIteration +生成器函数执行到结尾或显式 return 时,帧走正常退出路径,f_stacktop 被置成 NULL。gen_send_ex 一看 f_stacktop 等于 NULL,就知道这次是返回不是 yield,于是转换成 StopIteration,return 的值挂在异常的 value 上,并释放帧。这让生成器和 for 协议天然咬合:for x in g 不断对 g 调 next,每轮取一个 yield 的值,直到某次 next 抛 StopIteration,循环结束。 + + + + + + + + + + + +耗尽:return → StopIteration,帧释放 + + +函数 return +帧走正常退出 + + +f_stacktop = NULL +标志位:返回了 + + +raise StopIteration +return 值挂在 .value + + + + + + +这正是 for 协议的另一端 +for x in g:不断 next(g),直到 StopIteration 才停 + + +「yield 一个值」对应「next 返回」,「函数返回」对应「StopIteration」 + diff --git a/docs/vm/generators/gen-call-returns-frame.svg b/docs/vm/generators/gen-call-returns-frame.svg new file mode 100644 index 0000000..08bf386 --- /dev/null +++ b/docs/vm/generators/gen-call-returns-frame.svg @@ -0,0 +1,45 @@ + +调用生成器函数:建帧、包壳、返回,不执行 +普通函数调用会建帧、绑参,然后立即跑求值循环执行函数体。生成器函数不同:只要函数体里有 yield,code object 就带 CO_GENERATOR 标志,_PyEval_EvalCodeEx 建好帧、绑好参数后检测到这个标志,就不跑求值循环,而是把这个准备就绪却尚未执行的帧包进一个 PyGenObject 直接返回。所以 g = gen() 只是建一个帧加包个壳,函数体一行都没执行,要等到第一次 next 才真正开跑。 + + + + + + + + + + +g = gen():只建帧包壳,不执行函数体 + + +gen() +调用 + + +_PyEval_EvalCodeEx +建帧 + 绑参 +检测 CO_GENERATOR + + + + + + +PyGenObject(壳) + +gi_frame = 就绪的帧 + +求值循环没有运行 —— 函数体一行都没跑 + + +第一次 next(g) +函数体才真正开跑 + + diff --git a/docs/vm/generators/genobject.svg b/docs/vm/generators/genobject.svg new file mode 100644 index 0000000..9a6f130 --- /dev/null +++ b/docs/vm/generators/genobject.svg @@ -0,0 +1,45 @@ + +生成器对象保管着一个帧 +PyGenObject 是个揣着帧的壳,核心字段只有几个:gi_frame 保管的帧,也就是第一章讲的那个 PyFrameObject,有同样的局部变量区、求值栈、f_lasti 执行进度;gi_running 防重入标志;gi_code 背后的 code object;gi_exc_state 挂起期间保存的异常状态。普通调用结束就丢弃帧,生成器把帧攥在手里不放,于是这个帧能被反复进入和退出。 + + + + + + + + + +生成器 = 一个揣着帧的壳 + + + +PyGenObject + + gi_frame→ 保管的帧 + + gi_running防重入 + + gi_code背后的字节码 + + gi_exc_state挂起期异常 + + + + +PyFrameObject(第一章那个帧) + + f_localsplus(局部+求值栈) + f_stacktop(栈顶) + f_lasti(执行进度) + f_back(调用者) + + + +gi_frame + diff --git a/docs/vm/generators/index.md b/docs/vm/generators/index.md new file mode 100644 index 0000000..69aafd9 --- /dev/null +++ b/docs/vm/generators/index.md @@ -0,0 +1,242 @@ +# 生成器与协程 + +上一章我们看清了普通函数调用的闭环:**建帧 → 绑参 → 执行 → 返回,帧随即销毁**。一次调用从头跑到尾,帧用完即弃。 + +可有一种函数偏偏不守这个规矩——它执行到 `yield` 就「暂停」,把现场**原地冻住**、控制权交还给调用者;下次再从断点继续,局部变量、执行位置全都还在。这就是**生成器**。它是怎么做到「暂停」的?答案出人意料地简单:**调用结束时,不把帧扔掉,而是把它揣进一个对象里留着**。这一章就拆开这件事,再看它如何顺势支撑起**协程**与 `async`/`await`。 + +## 生成器函数被调用时:不执行,只建帧 + +第一个反直觉的事实:**调用一个生成器函数,函数体一行都不会执行**。 + +```python +>>> def gen(): +... print("开始执行") +... yield 1 +... yield 2 +... +>>> g = gen() # 注意:没有打印「开始执行」! +>>> g + +>>> next(g) # 直到第一次 next,函数体才真正开跑 +开始执行 +1 +``` + +只要函数体里出现 `yield`,编译器就给它的 code object 打上 `CO_GENERATOR` 标志。还记得上一章函数调用的核心 `_PyEval_EvalCodeEx` 吗?它建好帧、绑好参数后,会先检查这个标志——若是生成器,就**不跑求值循环**,而是把这个「准备就绪却尚未执行」的帧包进一个生成器对象,直接返回: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L3878) + +```c +// Python/ceval.c —— _PyEval_EvalCodeEx 末尾(精简) +if (co->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) { + Py_CLEAR(f->f_back); // 调用者关系待恢复时再接 + // 把这个就绪的帧包进生成器对象,直接返回——函数体一行都没跑 + gen = PyGen_NewWithQualName(f, name, qualname); + return gen; +} +``` + +![调用生成器函数:建帧,包壳,返回](gen-call-returns-frame.svg) + +所以 `g = gen()` 做的全部事情,就是「建一个帧 + 包个壳」。函数体的执行被推迟到了第一次 `next(g)`。 + +## 生成器对象:一个揣着帧的壳 + +那个「壳」就是 `PyGenObject`。它的字段出奇地少,核心只有一个——**它保管着的那个帧 `gi_frame`**: + +`源文件:`[Include/genobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/genobject.h#L15) + +```c +// Include/genobject.h —— 生成器对象的核心字段(来自 _PyGenObject_HEAD 宏) +struct _frame *gi_frame; // 保管的帧(生成器「冻住的现场」);耗尽后为 NULL +char gi_running; // 是否正在执行(防重入) +PyObject *gi_code; // 背后的 code object +PyObject *gi_name, *gi_qualname; +_PyErr_StackItem gi_exc_state; // 挂起期间保存自己的异常状态 +``` + +这个 `gi_frame`,正是第一章讲的那个 `PyFrameObject`——同样的局部变量区、同样的求值栈、同样的 `f_lasti`(执行进度)。区别只在于:普通调用结束就把帧丢了,而生成器**把帧攥在手里不放**。整个生成器机制,本质就是「一个能被反复进入、退出的帧」: + +```python +>>> g = gen() +>>> g.gi_frame # 壳里揣着一个帧 + +>>> g.gi_frame.f_lasti # 执行进度:还没开始 +-1 +>>> g.gi_code is gen.__code__ # 背后还是那份字节码 +True +``` + +![生成器对象保管着一个帧](genobject.svg) + +## yield:把帧原地冻住 + +`next(g)` 让帧跑起来,一路执行到 `yield`。处理 `yield` 的指令是 `YIELD_VALUE`,它做的事和「暂停」二字严丝合缝: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1810) + +```c +// Python/ceval.c —— TARGET(YIELD_VALUE)(精简) +retval = POP(); // yield 出去的值 +f->f_stacktop = stack_pointer; // ★ 保存求值栈的栈顶——把现场冻住 +why = WHY_YIELD; +goto fast_yield; // 退出求值循环,但不清栈、不销毁帧 +``` + +对比上一章普通函数的 `RETURN_VALUE`:它走的是正常退出路径,会把求值栈清空、`f_stacktop` 置为 `NULL`,帧的使命就此终结。而 `YIELD_VALUE` 偏偏**保存** `f_stacktop`、跳到 `fast_yield` 这条特殊出口——它绕过了清栈,于是**求值栈上的中间值原封不动地留着**。再加上 `f_lasti` 此刻已经指向 `yield` 的**下一条**指令,这个帧就被完整地「定格」在了 yield 这一刻: + +![yield 冻住帧 vs return 销毁帧](yield-vs-return.svg) + +`f_stacktop` 是不是 `NULL`,成了区分两种退出的**标志位**:非 `NULL` 说明是 yield 挂起(帧还能续跑),`NULL` 说明是 return 终结(帧已结束)。这个区别下面恢复和耗尽时都要用到。 + +## send 与恢复:把值塞回冻住的帧 + +控制权回到调用者后,下一次 `next(g)` 或 `g.send(v)` 要让帧从断点续跑。这件事由 `gen_send_ex` 完成,它的逻辑把「恢复一个挂起的帧」讲得明明白白: + +`源文件:`[Objects/genobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/genobject.c#L152) + +```c +// Objects/genobject.c —— gen_send_ex(精简) +PyFrameObject *f = gen->gi_frame; +...... +if (f->f_lasti != -1) { // 不是「刚启动」 + result = arg ? arg : Py_None; + Py_INCREF(result); + *(f->f_stacktop++) = result; // ★ 把 send 进来的值压回帧的求值栈 +} +f->f_back = tstate->frame; // 生成器返回给「当前调用者」,而非创建者 +gen->gi_running = 1; +result = PyEval_EvalFrameEx(f, exc); // ★ 从断点续跑这个保存的帧 +gen->gi_running = 0; +``` + +两个 `★` 是全章的关键。第二个好理解:`PyEval_EvalFrameEx(f)` 拿着保存的帧重新进入求值循环,而 `f_lasti` 指向 yield 之后,于是**自然地从断点继续**。 + +第一个 `★` 则解释了 `send` 的魔法。`x = yield` 这个表达式,挂起前 `yield` 把值送了出去;恢复时 `gen_send_ex` 把 `send(v)` 的实参 `v` **压回帧的求值栈顶**——而求值循环一恢复,正等着从栈上取一个值赋给 `x`。于是 `x` 拿到的就是 `v`: + +![send 把值压回帧的求值栈](send-value.svg) + +```python +>>> def echo(): +... while True: +... got = yield # 恢复时,send 进来的值成为 got +... print("收到", got) +... +>>> e = echo() +>>> next(e) # 先启动到第一个 yield +>>> e.send("hello") # 把 "hello" 塞进去,成为 got +收到 hello +``` + +还有一个细节藏在 `f->f_back = tstate->frame` 里:生成器恢复时,把帧的 `f_back` 接到**当前**调用者,而非最初创建它的地方。所以「谁 `next` 我,我就返回给谁」——这让同一个生成器能在不同地方被驱动,traceback 也总反映当下的调用链。 + +## 耗尽:return 变成 StopIteration + +生成器函数执行到结尾(或显式 `return`)时,帧走正常退出路径——`f_stacktop` 被置成 `NULL`。`gen_send_ex` 拿到结果后一看 `f_stacktop == NULL`,就知道「这次是返回、不是 yield」,于是把它转换成 `StopIteration`,并释放帧: + +`源文件:`[Objects/genobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/genobject.c#L234) + +```c +// Objects/genobject.c —— gen_send_ex 收尾(精简) +if (result && f->f_stacktop == NULL) { // 返回了(不是 yield) + if (result == Py_None) + PyErr_SetNone(PyExc_StopIteration); + else + _PyGen_SetStopIterationValue(result); // return 的值挂到 StopIteration 上 + Py_CLEAR(result); +} +if (!result || f->f_stacktop == NULL) { + gen->gi_frame = NULL; // 帧不能再跑,释放掉 + Py_DECREF(f); +} +``` + +![return → StopIteration,帧释放](exhaust-stopiteration.svg) + +这下就和控制流章里的 `for` 接上了——`for x in g` 的本质,是不断对 `g` 调 `next()`(即 `send(None)`),`FOR_ITER` 每轮取一个 yield 出来的值;直到某次 `next()` 抛出 `StopIteration`,循环就结束。生成器和 `for` 协议天生咬合,正因为「yield 一个值」对应「`next()` 返回」、「函数返回」对应「`StopIteration`」: + +```python +>>> def count(n): +... for i in range(n): +... yield i +... return "done" # 返回值挂在 StopIteration 上 +... +>>> list(count(3)) # for/list 不断 next 到 StopIteration +[0, 1, 2] +>>> g = count(0) +>>> try: next(g) +... except StopIteration as e: print("返回值:", e.value) +返回值: done +``` + +## yield from:把驱动权委托出去 + +`yield from sub` 让当前生成器把驱动权**委托**给另一个子迭代器:子每 yield 一个值,就透传给外层的调用者;外层 `send` 进来的值,也透传给子。它由 `YIELD_FROM` 实现: + +`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1775) + +```c +// Python/ceval.c —— TARGET(YIELD_FROM)(精简) +PyObject *v = POP(); +PyObject *receiver = TOP(); // 被委托的子迭代器,留在栈上 +retval = _PyGen_Send((PyGenObject *)receiver, v); // 把值转发给子 +if (retval == NULL) { // 子耗尽(StopIteration) + _PyGen_FetchStopIterationValue(&val); // 取子的返回值 + SET_TOP(val); // 作为 yield from 表达式的值 + DISPATCH(); // 继续往下执行 +} +f->f_stacktop = stack_pointer; // 子 yield 了值 → 外层也跟着挂起 +why = WHY_YIELD; +f->f_lasti -= sizeof(_Py_CODEUNIT); // 回退一条:恢复时重新执行 YIELD_FROM +goto fast_yield; +``` + +注意那句 `f_lasti -= ...`:它让帧恢复时**重新执行** `YIELD_FROM`,于是外层会一直「卡」在这条指令上反复转发,直到子迭代器耗尽——这正是「委托」的实现。子的 `return` 值,则通过 `StopIteration` 被取出,成为 `yield from` 表达式的结果。 + +## 协程:把同一套机制用于「等待」 + +理解了生成器,**协程几乎是免费的**。`async def` 定义的协程,编译出的 code object 带 `CO_COROUTINE` 标志,走的是和生成器**完全相同**的挂起/恢复机制——对应的 `PyCoroObject` 与 `PyGenObject` 共享同一个对象头(`_PyGenObject_HEAD`),字段一模一样: + +`源文件:`[Include/genobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/genobject.h#L52) + +```c +// Include/genobject.h —— 协程对象,与生成器同构 +typedef struct { + _PyGenObject_HEAD(cr) // 和 PyGenObject 一样:揣着一个帧 cr_frame + PyObject *cr_origin; +} PyCoroObject; +``` + +区别只在「暂停的含义」:生成器的 `yield` 是**为了产出数据**——「我算出一个值,先交出去」;协程的 `await` 是**为了等待**——「这件事还没好,我先让出控制权,等好了再叫我」。而 `await x` 在字节码层面正是 `GET_AWAITABLE` + `YIELD_FROM`:把自己挂起、委托给被等待对象,等它有结果了再恢复。 + +谁来「等好了再叫我」?**事件循环**。它手握一棵协程构成的调用树,靠 `send` 一次次驱动它们前进;某个协程 `await` 一个 IO 时挂起、交出控制权,事件循环转去推进别的协程;IO 就绪后再 `send` 回去唤醒它。成千上万个协程就这样在**单线程**里轮流推进,互不阻塞: + +![事件循环靠 send 驱动协程树](coroutine-eventloop.svg) + +```python +>>> async def coro(): +... return 42 +... +>>> c = coro() +>>> c # 和生成器一样,调用不执行,只得到对象 + +>>> try: c.send(None) # 事件循环就是这样驱动它的 +... except StopIteration as e: print("结果:", e.value) +结果: 42 +``` + +可以看到,协程被 `send(None)` 驱动、以 `StopIteration` 携带返回值结束——和生成器如出一辙。`asyncio` 那套看似复杂的异步,根基就是这个「可暂停的帧」。 + +--- + +小结一下生成器与协程: + +- 普通调用「建帧—执行—弃帧」,生成器打破最后一步:**把帧留下来,反复进入、退出**; +- 调用生成器函数**不执行函数体**,`_PyEval_EvalCodeEx` 检测到 `CO_GENERATOR` 就把就绪的帧包进 `PyGenObject` 返回; +- **`yield`**(`YIELD_VALUE`)保存 `f_stacktop`、走 `fast_yield` 特殊出口——不清栈、不销毁帧,把现场连同 `f_lasti` 冻住;`f_stacktop` 是否为 `NULL` 区分「yield 挂起」与「return 终结」; +- **`send`/恢复**(`gen_send_ex`)把传入值压回帧的求值栈(这是 `x = yield` 取到 send 值的原因),重连 `f_back`,再 `PyEval_EvalFrameEx` 从断点续跑; +- 函数**返回**时 `f_stacktop` 置 `NULL`,被转换成 **`StopIteration`**(return 值挂在其上)并释放帧——这让生成器与 `for` 协议天然咬合; +- **`yield from`** 把驱动权委托给子迭代器(靠 `f_lasti` 回退反复转发); +- **协程**复用同一套挂起/恢复机制(`PyCoroObject` 与 `PyGenObject` 同构),`await` = `GET_AWAITABLE` + `YIELD_FROM`,由**事件循环**用 `send` 驱动——这就是 `asyncio` 单线程并发的根基。 + +至此,第四部分「虚拟机」就完整了:从帧与求值循环,到表达式、控制流、异常、函数,再到这一章可暂停的生成器与协程,我们已经看清虚拟机如何执行字节码。但虚拟机不会凭空运转——解释器是怎么启动的?`import` 一个模块时发生了什么?多线程下那把著名的 GIL 又是如何工作的?下一部分「运行时」,就从 **Python 运行环境的初始化** 讲起。 diff --git a/docs/vm/generators/send-value.svg b/docs/vm/generators/send-value.svg new file mode 100644 index 0000000..9135be1 --- /dev/null +++ b/docs/vm/generators/send-value.svg @@ -0,0 +1,44 @@ + +send 把值压回帧的求值栈,成为 yield 表达式的值 +got = yield 这个表达式的魔法在于:挂起前 yield 把值送出去,恢复时 gen_send_ex 把 send 的实参压回保存的帧的求值栈顶。而求值循环一恢复,正等着从栈上取一个值赋给 got。于是 send("hello") 传入的 hello 被压上栈,成为 got 的值。这就是为什么 got = yield 求值为 send 进来的值。 + + + + + + + + + + +got = yield:send 的值从栈上来 + + + +g.send("hello") +实参 "hello" + + +帧的求值栈 + +"hello" ← 压入栈顶 + +挂起前留下的中间值… + + +gen_send_ex 压回 + + + +got = (取栈顶) +got == "hello" + + +求值循环恢复后 +正等着取一个值赋给 got + diff --git a/docs/vm/generators/suspend-resume.svg b/docs/vm/generators/suspend-resume.svg new file mode 100644 index 0000000..970887e --- /dev/null +++ b/docs/vm/generators/suspend-resume.svg @@ -0,0 +1,46 @@ + +生成器的挂起与恢复循环 +生成器在调用者和帧之间反复切换。next 或 send 进入帧,从 f_lasti 指向的断点续跑;遇到 yield,YIELD_VALUE 保存现场、把值交出、控制权回到调用者,帧冻在原地;下次 next 或 send 再进入,从上次断点继续。如此往复,直到函数返回,帧被释放、抛出 StopIteration。同一个帧被反复进入和退出,这就是可暂停的函数。 + + + + + + + + + + +挂起与恢复:同一个帧反复进出 + + + +调用者 +for x in g +/ g.send(v) +拿到 yield 的值 + + + +生成器的帧 +局部变量都还在 +从 f_lasti 续跑 +冻在上次 yield 处 + + + +next / send → 恢复 +把 send 的值压回帧的求值栈 + + + +yield → 挂起 + +往复,直到函数返回 → 帧释放、抛 StopIteration +控制权在调用者与帧之间来回交接,帧始终不被销毁 + diff --git a/docs/vm/generators/yield-vs-return.svg b/docs/vm/generators/yield-vs-return.svg new file mode 100644 index 0000000..cd3190a --- /dev/null +++ b/docs/vm/generators/yield-vs-return.svg @@ -0,0 +1,46 @@ + +yield 冻住帧 vs return 销毁帧 +两种退出求值循环的方式对比。YIELD_VALUE 保存 f_stacktop 等于当前栈顶,把求值栈上的中间值原封不动留着,走 fast_yield 特殊出口,不清栈不销毁帧,加上 f_lasti 已指向 yield 的下一条指令,帧被完整定格,还能续跑。RETURN_VALUE 走正常退出路径,清空求值栈,把 f_stacktop 置为 NULL,帧的使命终结。f_stacktop 是否为 NULL 成了区分两者的标志位:非 NULL 是 yield 挂起,NULL 是 return 终结。 + + + + + + + + +两种退出:yield 冻住,return 终结 + + + +YIELD_VALUE(挂起) + + f_stacktop = stack_pointer + why = WHY_YIELD + goto fast_yield + +不清栈 · 不销毁帧 + +f_stacktop ≠ NULL +帧定格,还能续跑 + + + +RETURN_VALUE(终结) + + retval = POP() + why = WHY_RETURN + 清空求值栈 + +正常退出路径 + +f_stacktop = NULL +帧的使命终结 + +f_stacktop 是否为 NULL —— 区分「yield 挂起」与「return 终结」的标志位 + diff --git a/objects/PyListStructure.png b/objects/PyListStructure.png deleted file mode 100644 index 9e357bd..0000000 Binary files a/objects/PyListStructure.png and /dev/null differ diff --git a/objects/PyObject.jpg b/objects/PyObject.jpg deleted file mode 100644 index ad6bbba..0000000 Binary files a/objects/PyObject.jpg and /dev/null differ diff --git a/objects/PyVarObject.jpg b/objects/PyVarObject.jpg deleted file mode 100644 index 58f2c2c..0000000 Binary files a/objects/PyVarObject.jpg and /dev/null differ diff --git a/objects/dict-object.md b/objects/dict-object.md deleted file mode 100644 index f1912d3..0000000 --- a/objects/dict-object.md +++ /dev/null @@ -1,724 +0,0 @@ -# Python字典 - -Dictionary object implementation using a hash table ,通过描述可知,python的字典就是实现了一个hash表。 - -## Python字典概述 -在python的字典中,一个键值对的对应保存就是PyDictEntry类型来保存; - -`源文件:`[Include/dict-common.h](https://github.com/python/cpython/blob/v3.7.0/Objects/dict-common.h#L1) - -```c -// Objects/dict-common.h -typedef struct { - /* Cached hash code of me_key. */ - Py_hash_t me_hash; - PyObject *me_key; - PyObject *me_value; /* This field is only meaningful for combined tables */ -} PyDictKeyEntry;  -``` - -其中,me_hash就是哈希生成的值,me_key就是对应的key值,me_value就是对应的值。 -在python中,在一个PyDictObject对象的变化过程中,entry的状态会在不同的状态间转换。基本上在如下四种状态中转换:Unused、Active、Dummy和Pending。 - -1. Unused:没有插入任何一个获取的key与value,并且在次之前也没有存储任何的key,value,每一个entry在初始化的时候都会处于这种状态,并且Unused会被里面切换到Active态,当有key插入,这是就是entry初始化的状态。 -2. Active:当index>=0时,me_key不为空并且me_value不为空,保存了一个键值对,Active可以转变为Dummy或者Pending状态,当一个健被删除的时候,这只会在me_value不为空的时候出现。 -3. Dummy:先前保存了一个Active的键值对,但是这个键值对被删除了并且一个活跃的键值对还没有填入该位置,Dummy可以转变为Active当删除的时候,Dummy的位置不能被重新使用,一旦发生碰撞,探针序列就无法知道这对键值对曾是活跃的键值对。 -4. Pending:索引>=0,键!=空,值=空(仅拆分),尚未插入到拆分表中。 - - -## 字典的两种类型 - -python的字典类型中包含了两种联合字典(split-table dictionaries)与分离字典(combined-table dictonaries)。详细的信息可查看有关dict的描述[pep-0412]()。 - -### split-table dictionaries - -当被创建的字典是用来保存object的\_\_dict\_\_属性时,该字典才会创建为一个split-table,它们的健表都被缓存在类型属性中,并且允许所有该类型的实例都可以共享该keys。当出现一个事件讲字典的属性值进行改变的时候,个别字典讲慢慢的转化成组合表的形式。这就保证了在大部分的应用场景下很高的内存利用效率,并保证了在各个场景下的正确性。当split-dict重新改变大小,它会立马改变为一个combined-table,如果重置大小作为保存实例属性的结果,并且只有一个该object的实例,字典会立马再变为一个split-table。如果从split-table中删除一个key, value,它不会删除keys tables中对应的该值,而只是将values数值中移除了该value。 - -### combined-table dictionaries - -直接通过dict內建函数与{}生成的字典,模块和大部分其他字典都会创建为combined-table字典,一个combined-table不会改变为一个split-table字典,该字典的行为方式与最初的字典的行为方式大致相同。 - - -## 容器的相关数据结构 - -字典对象是通过PyDictObject来实现数据的,详情如下; - -`源文件:`[Include/dictobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/dictobject.h#L17) - -```c -// Include/dictobject.h -typedef struct _dictkeysobject PyDictKeysObject; - -/* The ma_values pointer is NULL for a combined table - * or points to an array of PyObject* for a split table - */ -typedef struct { - PyObject_HEAD - - /* Number of items in the dictionary */ - Py_ssize_t ma_used;  // 使用的keys个数 - - /* Dictionary version: globally unique, value change each time - the dictionary is modified */ - uint64_t ma_version_tag; - - PyDictKeysObject *ma_keys;     // 如果有则是保存的keys数据 - - /* If ma_values is NULL, the table is "combined": keys and values - are stored in ma_keys. - - If ma_values is not NULL, the table is splitted: - keys are stored in ma_keys and values are stored in ma_values */ - PyObject **ma_values;  // 如果不为空则保存的是values -} PyDictObject; -``` - -其中,PyDictKeysObject的定义如下; - -`源文件:`[Include/dict-common.h](https://github.com/python/cpython/blob/v3.7.0/Objects/dict-common.h#L20) - -```c -// Objects/dict-common.h -/* See dictobject.c for actual layout of DictKeysObject */ -struct _dictkeysobject { - Py_ssize_t dk_refcnt;                  // 引用计数 - - /* Size of the hash table (dk_indices). It must be a power of 2. */ - Py_ssize_t dk_size;                   // hash table 的大小必须是2的倍数 - - /* Function to lookup in the hash table (dk_indices): - - - lookdict(): general-purpose, and may return DKIX_ERROR if (and - only if) a comparison raises an exception. - - - lookdict_unicode(): specialized to Unicode string keys, comparison of - which can never raise an exception; that function can never return - DKIX_ERROR. - - - lookdict_unicode_nodummy(): similar to lookdict_unicode() but further - specialized for Unicode string keys that cannot be the value. - - - lookdict_split(): Version of lookdict() for split tables. */ - dict_lookup_func dk_lookup; // 哈希查找函数 - - /* Number of usable entries in dk_entries. */ - Py_ssize_t dk_usable; // 可用的entry数量 - - /* Number of used entries in dk_entries. */  - Py_ssize_t dk_nentries;          // 已经使用的entry数量 - - /* Actual hash table of dk_size entries. It holds indices in dk_entries, - or DKIX_EMPTY(-1) or DKIX_DUMMY(-2). - - Indices must be: 0 <= indice < USABLE_FRACTION(dk_size). - - The size in bytes of an indice depends on dk_size: - - - 1 byte if dk_size <= 0xff (char*) - - 2 bytes if dk_size <= 0xffff (int16_t*) - - 4 bytes if dk_size <= 0xffffffff (int32_t*) - - 8 bytes otherwise (int64_t*) - - Dynamically sized, SIZEOF_VOID_P is minimum. */ - char dk_indices[]; /* char is required to avoid strict aliasing. */   // 存入的entries - - /* "PyDictKeyEntry dk_entries[dk_usable];" array follows: - see the DK_ENTRIES() macro */ -}; -``` - -相关数据结构的内存布局为; -![python_dict_mem](./python_dict_mem.png) - -## Python字典示例 - -本次示例脚本如下: - -```python -d = {} -d['1']='2' -d['1']='e' -d.pop('1') - -``` - -通过Python的反汇编工具获取字节码; - -```shell -python -m dis dict_test.py -``` - -输出的字节码如下; - -```shell - 2 0 BUILD_MAP 0 - 2 STORE_NAME 0 (d) - - 3 4 LOAD_CONST 0 ('2') - 6 LOAD_NAME 0 (d) - 8 LOAD_CONST 1 ('1') - 10 STORE_SUBSCR - - 4 12 LOAD_CONST 2 ('e') - 14 LOAD_NAME 0 (d) - 16 LOAD_CONST 1 ('1') - 18 STORE_SUBSCR - - 5 20 LOAD_NAME 0 (d) - 22 LOAD_METHOD 1 (pop) - 24 LOAD_CONST 1 ('1') - 26 CALL_METHOD 1 - 28 POP_TOP - 30 LOAD_CONST 3 (None) - 32 RETURN_VALUE -``` - -通过字节码指令可知,首先调用了BUILD_MAP来创建一个新的字典,接着就对新建的字典d进行了赋值操作与更新操作,最后调用了pop方法删除一个key。接下来就详细分析一下相关流程。 - -## 字典的初始化流程 - -通过查找BUILD_MAP的虚拟机执行函数; - -`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L2357) - -```c -// Python/ceval.c -switch (opcode) { - ... - - TARGET(BUILD_MAP) { - Py_ssize_t i; - PyObject *map = _PyDict_NewPresized((Py_ssize_t)oparg); // 新建并初始化一个字典 - if (map == NULL) - goto error;  // 如果新建失败则报错 - for (i = oparg; i > 0; i--) {   // 检查在新建的过程中是否通过参数传值 - int err; - PyObject *key = PEEK(2*i); - PyObject *value = PEEK(2*i - 1); - err = PyDict_SetItem(map, key, value);      // 找到对应的值并讲该值设置到map中 - if (err != 0) {                        // 检查是否报错 - Py_DECREF(map); - goto error;                        // 如果错误就报错处理 - } - } - - while (oparg--) { - Py_DECREF(POP());                       // 弹出栈上输入参数的引用 - Py_DECREF(POP()); - } - PUSH(map);                              // 讲生成的map压栈 - DISPATCH();                             // 检查是否需要执行下一条字节码指令 - } -} -``` - -从该函数的执行可知,初始化的函数是从_PyDict_NewPresized开始,该函数就是生成并初始化一个字典; - -`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L1240) - -```c -// Objects/dictobject.c - -PyObject * -_PyDict_NewPresized(Py_ssize_t minused) -{ - const Py_ssize_t max_presize = 128 * 1024;  // 字典最大的容量 - Py_ssize_t newsize; - PyDictKeysObject *new_keys; - - /* There are no strict guarantee that returned dict can contain minused - * items without resize. So we create medium size dict instead of very - * large dict or MemoryError. - */ - if (minused > USABLE_FRACTION(max_presize)) { // 检查传入的数量是否超过最大值 - newsize = max_presize; - } - else { - Py_ssize_t minsize = ESTIMATE_SIZE(minused); // 获取最小的值,在新建一个空的字典的时候该值为0 - newsize = PyDict_MINSIZE; // 设置字典的最小值 为8 - while (newsize < minsize) { // 如果传入的值大于最小值则调整newsize 大小 - newsize <<= 1; - } - } - assert(IS_POWER_OF_2(newsize)); - - new_keys = new_keys_object(newsize); // 生成并初始化一个PyDictKeysObject对象 - if (new_keys == NULL) - return NULL; - return new_dict(new_keys, NULL); // 生成一个新的对象并返回 -} -``` - -首先,先计算出需要生成的字典的大小,然后再初始化一个PyDictKeysObject,最后就生成一个PyDictObject返回。继续查看new_keys_object的执行流程; - -`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L503) - -```c -// Objects/dictobject.c - -static PyDictKeysObject *new_keys_object(Py_ssize_t size) -{ - PyDictKeysObject *dk; - Py_ssize_t es, usable; - - assert(size >= PyDict_MINSIZE); // 检查size是否大于最小size - assert(IS_POWER_OF_2(size)); // 检查是否是2的倍数 - - usable = USABLE_FRACTION(size); // 检查是否可用  根据经验在1/2和2/3之间效果最好 - if (size <= 0xff) { - es = 1; - } - else if (size <= 0xffff) { - es = 2; - } -#if SIZEOF_VOID_P > 4 - else if (size <= 0xffffffff) { - es = 4; - } -#endif - else { - es = sizeof(Py_ssize_t); - } - - if (size == PyDict_MINSIZE && numfreekeys > 0) {      // 是否有缓存,如果有缓存就选择缓存中的dk - dk = keys_free_list[--numfreekeys]; - } - else { - dk = PyObject_MALLOC(sizeof(PyDictKeysObject) - + es * size - + sizeof(PyDictKeyEntry) * usable); // 没有缓存可使用的字典则申请内存生成一个 - if (dk == NULL) { - PyErr_NoMemory(); - return NULL; - } - } - DK_DEBUG_INCREF dk->dk_refcnt = 1; // 设置引用计数 - dk->dk_size = size; // 设置大小 - dk->dk_usable = usable; // 设置是否可用 - dk->dk_lookup = lookdict_unicode_nodummy; // 设置查找函数 - dk->dk_nentries = 0; - memset(&dk->dk_indices[0], 0xff, es * size); // 将申请的内存置空 - memset(DK_ENTRIES(dk), 0, sizeof(PyDictKeyEntry) * usable); - return dk; -} -``` - -主要就是通过传入的size,检查是否超过设置的大小,检查是否有缓存的字典数据可用,如果没有则申请内存重新生成一个dk,最后进行申请到的内存讲内容清空。接着就会进行new_dict初始化数据; - -`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L568) - -```c -// Objects/dictobject.c - -/* Consumes a reference to the keys object */ -static PyObject * -new_dict(PyDictKeysObject *keys, PyObject **values) -{ - PyDictObject *mp; - assert(keys != NULL); - if (numfree) {                            // 判断缓冲池是否有 - mp = free_list[--numfree]; - assert (mp != NULL); - assert (Py_TYPE(mp) == &PyDict_Type);  - _Py_NewReference((PyObject *)mp);              // 使用缓冲池对象     - } - else { - mp = PyObject_GC_New(PyDictObject, &PyDict_Type);    // 缓冲池没有则申请新的对象并初始化 - if (mp == NULL) { - DK_DECREF(keys); - free_values(values); - return NULL; - } - } - mp->ma_keys = keys; - mp->ma_values = values; - mp->ma_used = 0;                           // 设置ma_used为0 - mp->ma_version_tag = DICT_NEXT_VERSION(); - assert(_PyDict_CheckConsistency(mp)); - return (PyObject *)mp; -} -``` - -new_dict就是根据keys,values设置到从缓冲池或者新生成一个dict对象,最后返回。至此,dict的创建工作已经完成。 - -## 字典的插入与查找 - -通过字节码的指令STORE_SUBSCR可知,该命令就是讲'1'作为key, '2'作为value插入到d中,此时查看该执行函数; - -`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/v3.7.0/Python/ceval.c#L1561) - -```c -// Python/ceval.c -switch (opcode) { - ... - - TARGET(STORE_SUBSCR) { - PyObject *sub = TOP(); // 第一个值为key - PyObject *container = SECOND(); // 该为字典对象 - PyObject *v = THIRD(); // 该为value - int err; - STACKADJ(-3); - /* container[sub] = v */ - err = PyObject_SetItem(container, sub, v); // 调用该方法设置值 - Py_DECREF(v); - Py_DECREF(container); - Py_DECREF(sub); - if (err != 0) - goto error; - DISPATCH(); - } -} -``` - -此时,从栈中取出相关参数,并将这些值传入PyObject_SetItem函数进行处理设置值; - -`源文件:`[Objects/abstract.c](https://github.com/python/cpython/blob/v3.7.0/Objects/abstract.c#L186) - -```c -// Objects/abstract.c -int -PyObject_SetItem(PyObject *o, PyObject *key, PyObject *value) -{ - PyMappingMethods *m; - - if (o == NULL || key == NULL || value == NULL) {           // 检查是否为空如果任一为空则报错 - null_error(); - return -1; - } - m = o->ob_type->tp_as_mapping;                      // 获取类型的tp_as_mapping方法集      - if (m && m->mp_ass_subscript)                       // 如果有设置该类型 - return m->mp_ass_subscript(o, key, value); // 调用该mp_ass_subscript方法 - - if (o->ob_type->tp_as_sequence) { // 获取作为队列的操作集 - if (PyIndex_Check(key)) {                       // 检查key是否是索引 - Py_ssize_t key_value; - key_value = PyNumber_AsSsize_t(key, PyExc_IndexError);  - if (key_value == -1 && PyErr_Occurred()) - return -1; - return PySequence_SetItem(o, key_value, value);       // 调用索引插入 - } - else if (o->ob_type->tp_as_sequence->sq_ass_item) { - type_error("sequence index must be " - "integer, not '%.200s'", key); - return -1; - } - } - - type_error("'%.200s' object does not support item assignment", o);   // 则该类型对象不支持设置 - return -1; -} -``` - -其中就调用了字典的tp_as_mapping的方法集,并调用了该方法集的mp_ass_subscript方法;此时我们分析一下,dict的tp_as_mapping的方法集。此时就调用了tp_as_mapping的mp_ass_subscript方法,此时就是调用dict的dict_ass_sub方法; - -`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L2040) - -```c -// Objects/dictobject.c -static int -dict_ass_sub(PyDictObject *mp, PyObject *v, PyObject *w) -{ - if (w == NULL) - return PyDict_DelItem((PyObject *)mp, v); - else - return PyDict_SetItem((PyObject *)mp, v, w); -} -``` - -可知,删除一个key就是PyDict_DelItem,设置一个key就是PyDict_SetItem; - -`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L1433) - -```c -// Objects/dictobject.c -int -PyDict_SetItem(PyObject *op, PyObject *key, PyObject *value) -{ - PyDictObject *mp; - Py_hash_t hash; - if (!PyDict_Check(op)) {            // 检查是否是字典类型 - PyErr_BadInternalCall(); - return -1; - } - assert(key); - assert(value); - mp = (PyDictObject *)op; - if (!PyUnicode_CheckExact(key) || - (hash = ((PyASCIIObject *) key)->hash) == -1)  // 检查传入的key是否hash为-1 - { - hash = PyObject_Hash(key); // 生成hash调用key对应的tp_hash方法,在本例中传入的是str类型,则调用str类型的tp_hash方法 - if (hash == -1) - return -1; - } - - /* insertdict() handles any resizing that might be necessary */ - return insertdict(mp, key, hash, value); // 生成hash调用key对应的tp_hash方法 -} - -``` - -insertdict方法就是将生成的方法,插入到字典中去; - -`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L987) - -```c -// Objects/dictobject.c -static int -insertdict(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value) -{ - PyObject *old_value; - PyDictKeyEntry *ep; - - Py_INCREF(key); - Py_INCREF(value); - if (mp->ma_values != NULL && !PyUnicode_CheckExact(key)) { - if (insertion_resize(mp) < 0) // 重新设置mp的大小 如果ma_values有值 - goto Fail; - } - - Py_ssize_t ix = mp->ma_keys->dk_lookup(mp, key, hash, &old_value);     // 调用查找方法 - if (ix == DKIX_ERROR) - goto Fail; - - assert(PyUnicode_CheckExact(key) || mp->ma_keys->dk_lookup == lookdict); - MAINTAIN_TRACKING(mp, key, value); // 检查mp key values是否需要加入垃圾回收 - - /* When insertion order is different from shared key, we can't share - * the key anymore. Convert this instance to combine table. - */ - if (_PyDict_HasSplitTable(mp) && - ((ix >= 0 && old_value == NULL && mp->ma_used != ix) || - (ix == DKIX_EMPTY && mp->ma_used != mp->ma_keys->dk_nentries))) {  // 检查是否是分离表,如果没查找到旧值并且 - if (insertion_resize(mp) < 0)                         // 重新设置该字典大小 - goto Fail; - ix = DKIX_EMPTY; - } - - if (ix == DKIX_EMPTY) { - /* Insert into new slot. */ - assert(old_value == NULL); - if (mp->ma_keys->dk_usable <= 0) {                      // 如果可用的值小于0 - /* Need to resize. */ - if (insertion_resize(mp) < 0)                       // 需要重新扩展字典大小 - goto Fail; - } - Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash);         // 查找一个可用的hash位置 - ep = &DK_ENTRIES(mp->ma_keys)[mp->ma_keys->dk_nentries];         // 获取存取的地址 - dk_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries);      // 设置该值 - ep->me_key = key;                                 // 保存key - ep->me_hash = hash; // 保存计算得出的hash值 - if (mp->ma_values) {                               // 如果mp的ma_values有值 - assert (mp->ma_values[mp->ma_keys->dk_nentries] == NULL); - mp->ma_values[mp->ma_keys->dk_nentries] = value;           // 设置该key对应的value - } - else { - ep->me_value = value; // 直接讲value设置到entry上面 - } - mp->ma_used++;                                   // 使用个数加1 - mp->ma_version_tag = DICT_NEXT_VERSION();   - mp->ma_keys->dk_usable--;                            // 可用减1 - mp->ma_keys->dk_nentries++; - assert(mp->ma_keys->dk_usable >= 0); - assert(_PyDict_CheckConsistency(mp)); - return 0; - } - - if (_PyDict_HasSplitTable(mp)) { // 如果是分离的 - mp->ma_values[ix] = value; // 直接设置ma_values对应的ix到values中 - if (old_value == NULL) { - /* pending state */ - assert(ix == mp->ma_used); - mp->ma_used++;                               // 使用加1 - } - } - else { - assert(old_value != NULL); - DK_ENTRIES(mp->ma_keys)[ix].me_value = value; - } - - mp->ma_version_tag = DICT_NEXT_VERSION(); - Py_XDECREF(old_value); /* which **CAN** re-enter (see issue #22653) */ - assert(_PyDict_CheckConsistency(mp)); - Py_DECREF(key); - return 0; - -Fail: - Py_DECREF(value); - Py_DECREF(key); - return -1; -} -``` - -首先会调用相关的查找方法,去查找待搜索的值是否已经存在字典中,如果当前字典数据已经满了则会按照增长大小的函数生成一个新的字典,并把旧数据设置到新的字典中,当找到的字典匹配时则返回。 - -其中dk_lookup对应的方法,在初始化之后对应的是lookdict_unicode_nodummy; - -`源文件:`[Objects/dictobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/dictobject.c#L813) - -```c -// Objects/dictobject.c - -/* Faster version of lookdict_unicode when it is known that no keys - * will be present. */ -static Py_ssize_t _Py_HOT_FUNCTION -lookdict_unicode_nodummy(PyDictObject *mp, PyObject *key, - Py_hash_t hash, PyObject **value_addr) -{ - assert(mp->ma_values == NULL); - /* Make sure this function doesn't have to handle non-unicode keys, - including subclasses of str; e.g., one reason to subclass - unicodes is to override __eq__, and for speed we don't cater to - that here. */ - if (!PyUnicode_CheckExact(key)) {                     // 检查如果不是unicode则直接调用lookdict方法查找 - mp->ma_keys->dk_lookup = lookdict; - return lookdict(mp, key, hash, value_addr); - } - - PyDictKeyEntry *ep0 = DK_ENTRIES(mp->ma_keys);             // 获取keys的首个元素地址 - size_t mask = DK_MASK(mp->ma_keys);                    // 获取大小 - size_t perturb = (size_t)hash; - size_t i = (size_t)hash & mask;                       // 获取生成的最终的值                  - - for (;;) { - Py_ssize_t ix = dk_get_index(mp->ma_keys, i); // 便利ma_keys key列表 - assert (ix != DKIX_DUMMY);                     // 判断不能为空 - if (ix == DKIX_EMPTY) { // 如果为空则证明找到一个可以使用的 - *value_addr = NULL;                       // 讲key对应的value设置为空 - return DKIX_EMPTY;                        // 返回 - } - PyDictKeyEntry *ep = &ep0[ix];             // 获取该位置元素值 - assert(ep->me_key != NULL); - assert(PyUnicode_CheckExact(ep->me_key)); - if (ep->me_key == key || - (ep->me_hash == hash && unicode_eq(ep->me_key, key))) {  // 如果key相同 hash值也相同 - *value_addr = ep->me_value;                    // 将该值赋值 - return ix; - } - perturb >>= PERTURB_SHIFT;                      // 偏移 - i = mask & (i*5 + perturb + 1);                   // 获取下一个位置 - } - Py_UNREACHABLE(); -} -``` - -该函数的主要工作就是查找,字典中是否有空余的值,或者如果找到了满足hash值与key相同的就将value设置为找到的值(这也是字典查找的核心逻辑)。至此,字典的插入的大致流程已经分析完毕。 - - -## Python字典的操作测试 - -现在我们动手观看一下具体的操作实例,首先声明,该例子仅供调试使用,目前调试的字典的key与value都是float类型并且不能del或者pop其中的key。操作字典如下所示; - -```python -d = {20000:2} -d[1] = 2 -d[3] = 2 -``` - -首先,讲如下代码插入到dictobject.c的1060行; - -```c -// 测试代码 -PyObject* key1 = PyLong_FromLong(20000); -Py_hash_t hash1 = PyObject_Hash(key1); -PyObject* old_value1; -Py_ssize_t ix1 = mp->ma_keys->dk_lookup(mp, key1, hash1, &old_value1); -if (ix1 == 0){ - PyLongObject* give; - give = (PyLongObject* )key1; - printf("found value : %ld\n", give->ob_digit[0]); - PyDictKeyEntry *ep01 = DK_ENTRIES(mp->ma_keys); - int i, count; - count = mp->ma_used; - int size_count, j; - size_count = mp->ma_keys->dk_size; - printf("%s ", mp->ma_keys->dk_indices); - int8_t *indices = (int8_t*)(mp->ma_keys->dk_indices); - printf("indices index values :"); - for (j=0; jme_key; - printf("size : %d ", mp->ma_keys->dk_size); - printf("found value while  key : %ld ", give->ob_digit[0]); - give = (PyLongObject* )ep01->me_value; - printf("value : %ld\n", give->ob_digit[0]); - ep01++; - } -} -``` - -然后编译运行; - -```python -Python 3.7.3 (default, May 22 2019, 16:17:57) -[GCC 7.3.0] on linux -Type "help", "copyright", "credits" or "license" for more information. ->>> d = {20000:2} -found value : 20000 - indices index values :0 -1 -1 -1 -1 -1 -1 -1 -size : 8 found value while  key : 20000 value : 2 -``` - -其中为什么初始化的时候输入20000,是根据代码找到相关的key值,因为字典也被python自身实现的结构中引用了多次,所以我们就设置了一个特殊值来跟踪我们想要的字典;当d初始化的时候,就输出如上所示内容;我们接下来继续操作; - -```python ->>> d = {20000:2} -found value : 20000 - indices index values :0 -1 -1 -1 -1 -1 -1 -1 -size : 8 found value while  key : 20000 value : 2 ->>> d[2] = 3 -found value : 20000 - indices index values :0 -1 1 -1 -1 -1 -1 -1 -size : 8 found value while  key : 20000 value : 2 -size : 8 found value while  key : 2 value : 3 ->>> d[3] = 4 -found value : 20000 - indices index values :0 -1 1 2 -1 -1 -1 -1 -size : 8 found value while  key : 20000 value : 2 -size : 8 found value while  key : 2 value : 3 -size : 8 found value while  key : 3 value : 4 ->>> d[5] = 6 -found value : 20000 - indices index values :0 -1 1 2 -1 3 -1 -1 -size : 8 found value while  key : 20000 value : 2 -size : 8 found value while  key : 2 value : 3 -size : 8 found value while  key : 3 value : 4 -size : 8 found value while  key : 5 value : 6 ->>> d[7] = 8 -found value : 20000 - indices index values :0 -1 1 2 -1 3 -1 4 -size : 8 found value while  key : 20000 value : 2 -size : 8 found value while  key : 2 value : 3 -size : 8 found value while  key : 3 value : 4 -size : 8 found value while  key : 5 value : 6 -size : 8 found value while  key : 7 value : 8 -``` - -此后我们一直添加值进d,从输出信息可知,index就是记录了PyDictKeyEntry的索引值,-1就表示该处未使用。 -当我们继续向d中添加内容时; - -```python ->>> d[9] = 10 -found value : 20000 - indices index values :0 -1 1 2 -1 3 -1 4 -1 5 -1 -1 -1 -1 -1 -1 -size : 16 found value while  key : 20000 value : 2 -size : 16 found value while  key : 2 value : 3 -size : 16 found value while  key : 3 value : 4 -size : 16 found value while  key : 5 value : 6 -size : 16 found value while  key : 7 value : 8 -size : 16 found value while  key : 9 value : 10 ->>> d[10] = 11 -found value : 20000 - indices index values :0 -1 1 2 -1 3 -1 4 -1 5 6 -1 -1 -1 -1 -1 -size : 16 found value while  key : 20000 value : 2 -size : 16 found value while  key : 2 value : 3 -size : 16 found value while  key : 3 value : 4 -size : 16 found value while  key : 5 value : 6 -size : 16 found value while  key : 7 value : 8 -size : 16 found value while  key : 9 value : 10 -size : 16 found value while  key : 10 value : 11 -``` - -从输出内容可知,字典的大小随之改变了,这也说明了python字典的最佳大小容量限定在1/2到2/3之间,如果超过这个阈值则字典就会自动扩容,扩容的策略大家可详细查看源码。 diff --git a/objects/list-object.md b/objects/list-object.md deleted file mode 100644 index 76b6fb2..0000000 --- a/objects/list-object.md +++ /dev/null @@ -1,414 +0,0 @@ -# Python List 对象 - -在Python中的list可以存放任何类型的数据,查看`PyListObject`可以发现,list实际存放的是PyObject* 指针 - -## PyListObject - -`源文件:`[Include/listobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/listobject.h#L23) - -```c -// listobject.h - -typedef struct { - PyObject_VAR_HEAD - /* Vector of pointers to list elements. list[0] is ob_item[0], etc. */ - PyObject **ob_item; - - /* ob_item contains space for 'allocated' elements. The number - * currently in use is ob_size. - * Invariants: - * 0 <= ob_size <= allocated - * len(list) == ob_size - * ob_item == NULL implies ob_size == allocated == 0 - * list.sort() temporarily sets allocated to -1 to detect mutations. - * - * Items must normally not be NULL, except during construction when - * the list is not yet visible outside the function that builds it. - */ - - // 可容纳元素的总数 - Py_ssize_t allocated; -} PyListObject; -``` - -示例 -```python -lst = [] -lst.append(1) -``` - -其存储结构如下图 - -![PyList structure](PyListStructure.png) - - - -## PyListObject对象的一些操作 - -- 创建PyListObject PyList_New -- 对象赋值 PyList_SetItem -- 获取元素 PyList_GetItem -- 插入元素 PyList_Insert -- 追加元素 PyList_Append -- 移除元素 list_remove -- 调整list大小 list_resize - -### PyList_New 创建对象 - -为了避免频繁的申请内存空间,创建PyListObject的时候会先检查缓冲池是否有可用空间 - -`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L136) - -```c -// listobject.c - -PyObject * -PyList_New(Py_ssize_t size) -{ - PyListObject *op; -#ifdef SHOW_ALLOC_COUNT - static int initialized = 0; - if (!initialized) { - Py_AtExit(show_alloc); - initialized = 1; - } -#endif - - // size 合法性检查 - if (size < 0) { - PyErr_BadInternalCall(); - return NULL; - } - - // PyListObject对象缓冲池是否有可用空间 - if (numfree) { - numfree--; - op = free_list[numfree]; - _Py_NewReference((PyObject *)op); -#ifdef SHOW_ALLOC_COUNT - count_reuse++; -#endif - } else { - // 缓冲池满只能向系统申请内存 - op = PyObject_GC_New(PyListObject, &PyList_Type); - if (op == NULL) - return NULL; -#ifdef SHOW_ALLOC_COUNT - count_alloc++; -#endif - } - if (size <= 0) - op->ob_item = NULL; - else { - op->ob_item = (PyObject **) PyMem_Calloc(size, sizeof(PyObject *)); - if (op->ob_item == NULL) { - Py_DECREF(op); - return PyErr_NoMemory(); - } - } - Py_SIZE(op) = size; - op->allocated = size; - _PyObject_GC_TRACK(op); - return (PyObject *) op; -} -``` - -PyListObject缓冲池默认大小为80 `源文件:`[Include/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L101) - -```c -// listobject.c - -/* Empty list reuse scheme to save calls to malloc and free */ -#ifndef PyList_MAXFREELIST -#define PyList_MAXFREELIST 80 -#endif -static PyListObject *free_list[PyList_MAXFREELIST]; -static int numfree = 0; -``` - -### PyList_SetItem 元素赋值 - -`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L215) - -```c -// listobject.c - -int -PyList_SetItem(PyObject *op, Py_ssize_t i, - PyObject *newitem) -{ - PyObject **p; - if (!PyList_Check(op)) { - Py_XDECREF(newitem); - PyErr_BadInternalCall(); - return -1; - } - if (i < 0 || i >= Py_SIZE(op)) { - Py_XDECREF(newitem); - PyErr_SetString(PyExc_IndexError, - "list assignment index out of range"); - return -1; - } - p = ((PyListObject *)op) -> ob_item + i; - Py_XSETREF(*p, newitem); - return 0; -} -``` - -元素赋值的示例 - -```python -lst = [0, 1, 2] -lst[0] = 3 -# 这里 lst[0] = 3 会调用 PyList_SetItem 函数 -``` - - -### PyList_GetItem 获取元素 - -`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L195) - -```c -// Objects/listobject.c - -PyObject * -PyList_GetItem(PyObject *op, Py_ssize_t i) -{ - if (!PyList_Check(op)) { - PyErr_BadInternalCall(); - return NULL; - } - if (i < 0 || i >= Py_SIZE(op)) { - if (indexerr == NULL) { - indexerr = PyUnicode_FromString( - "list index out of range"); - if (indexerr == NULL) - return NULL; - } - PyErr_SetObject(PyExc_IndexError, indexerr); - return NULL; - } - return ((PyListObject *)op) -> ob_item[i]; -} -``` - -获取元素的示例 - -```python -lst = [1, 2, 3, 4] -print(lst[3]) -# lst[3] 实际调用的就是 PyList_GetItem -# 根据索引返回对应的元素 -``` - - -### PyList_Append 追加元素 - -PyList_Append 调用 app1 - -```c -int -PyList_Append(PyObject *op, PyObject *newitem) -{ - if (PyList_Check(op) && (newitem != NULL)) - return app1((PyListObject *)op, newitem); - PyErr_BadInternalCall(); - return -1; -} -``` - -`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L279) - -```c -// Objects/listobject.c - -static int -app1(PyListObject *self, PyObject *v) -{ - Py_ssize_t n = PyList_GET_SIZE(self); - - assert (v != NULL); - if (n == PY_SSIZE_T_MAX) { - PyErr_SetString(PyExc_OverflowError, - "cannot add more objects to list"); - return -1; - } - - if (list_resize(self, n+1) < 0) - return -1; - - Py_INCREF(v); - PyList_SET_ITEM(self, n, v); - return 0; -} -``` - -从`app1`代码可以看出追加元素操作大致流程如下 -- 调用list_resize,将list大小加一 -- 将元素插入list尾部 - -### PyList_Insert 插入元素 - -PyList_Insert 调用 ins1 - -```c -int -PyList_Insert(PyObject *op, Py_ssize_t where, PyObject *newitem) -{ - if (!PyList_Check(op)) { - PyErr_BadInternalCall(); - return -1; - } - return ins1((PyListObject *)op, where, newitem); -} -``` - -`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L236) - -```c -// Objects/listobject.c - -static int -ins1(PyListObject *self, Py_ssize_t where, PyObject *v) -{ - Py_ssize_t i, n = Py_SIZE(self); - PyObject **items; - if (v == NULL) { - PyErr_BadInternalCall(); - return -1; - } - if (n == PY_SSIZE_T_MAX) { - PyErr_SetString(PyExc_OverflowError, - "cannot add more objects to list"); - return -1; - } - - if (list_resize(self, n+1) < 0) - return -1; - - if (where < 0) { - where += n; - if (where < 0) - where = 0; - } - if (where > n) - where = n; - items = self->ob_item; - for (i = n; --i >= where; ) - items[i+1] = items[i]; - Py_INCREF(v); - items[where] = v; - return 0; -} -``` - -从`ins1`代码可以看出插入元素操作大致流程如下 -- 调用list_resize,将list大小加一 -- 将要插入的位置的元素都往后移一个位置 -- 将元素插入指定位置 - -### list_remove 移除元素 - -`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L2546) - -```c -// listobject.c - -static PyObject * -list_remove(PyListObject *self, PyObject *value) -/*[clinic end generated code: output=f087e1951a5e30d1 input=2dc2ba5bb2fb1f82]*/ -{ - Py_ssize_t i; - - for (i = 0; i < Py_SIZE(self); i++) { - int cmp = PyObject_RichCompareBool(self->ob_item[i], value, Py_EQ); - if (cmp > 0) { - if (list_ass_slice(self, i, i+1, - (PyObject *)NULL) == 0) - Py_RETURN_NONE; - return NULL; - } - else if (cmp < 0) - return NULL; - } - PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); - return NULL; -} -``` - -移除元素示例 - -```python -lst = [0, 2, 4, 3] -lst.remove(3) -""" -lst.remove(3) 会调用 list_remove函数, -list_remove函数会遍历列表,使用PyObject_RichCompareBool与目标值进行比较, -相同则调用list_ass_slice进行移除,当遍历完列表还未找到则报错 -""" -``` - -### list_resize 调整list存储空间 - -随着list元素的增加,list的存储空间可能会不够用,这个时候就需要扩大list的存储空间。 -随着list元素的减少,list的存储空间可能存在冗余,这个时候就需要缩小list的存储空间。 -函数`list_resize`就是用于调节list存储空间大小的 - -`源文件:`[Objects/listobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/listobject.c#L19) - -```c -// listobject.c - -static int -list_resize(PyListObject *self, Py_ssize_t newsize) -{ - PyObject **items; - size_t new_allocated, num_allocated_bytes; - Py_ssize_t allocated = self->allocated; - - /* Bypass realloc() when a previous overallocation is large enough - to accommodate the newsize. If the newsize falls lower than half - the allocated size, then proceed with the realloc() to shrink the list. - */ - if (allocated >= newsize && newsize >= (allocated >> 1)) { - assert(self->ob_item != NULL || newsize == 0); - Py_SIZE(self) = newsize; - return 0; - } - - /* This over-allocates proportional to the list size, making room - * for additional growth. The over-allocation is mild, but is - * enough to give linear-time amortized behavior over a long - * sequence of appends() in the presence of a poorly-performing - * system realloc(). - * The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ... - * Note: new_allocated won't overflow because the largest possible value - * is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t. - */ - new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6); - if (new_allocated > (size_t)PY_SSIZE_T_MAX / sizeof(PyObject *)) { - PyErr_NoMemory(); - return -1; - } - - if (newsize == 0) - new_allocated = 0; - num_allocated_bytes = new_allocated * sizeof(PyObject *); - items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes); - if (items == NULL) { - PyErr_NoMemory(); - return -1; - } - self->ob_item = items; - Py_SIZE(self) = newsize; - self->allocated = new_allocated; - return 0; -} -``` - -当 `allocated/2 <= newsize <= allocated` 时,list_resize只会改变 ob_size不会改变allocated。 -其他情况则需要调用`PyMem_Realloc`函数分配新的空间存储列表元素。 - -列表allocated的增长模式是 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ... - -其公式为 `new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6)` diff --git a/objects/long-object.md b/objects/long-object.md deleted file mode 100644 index 6722b52..0000000 --- a/objects/long-object.md +++ /dev/null @@ -1,637 +0,0 @@ -# Python 整数对象 - -CPython2 的整数对象 有 `PyIntObject` 和 `PyLongObject` 这两种类型, -CPython3 只保留了 `PyLongObject` - -在 `源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3) -的第三行有这么一句话 `XXX The functional organization of this file is terrible` - -可见这个变化不是一蹴而就的,有比较艰辛的过程,大家有兴趣可以去挖掘一下 - -## PyLongObject - -`源文件:`[Include/longobject.h](https://github.com/python/cpython/blob/v3.7.0/Include/longobject.h#L10) - -```c -// longobject.h - -typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */ -``` - - -`源文件:`[Include/longintrepr.h](https://github.com/python/cpython/blob/v3.7.0/Include/longintrepr.h#L85) - -```c -// longintrepr.h -/* Long integer representation. - The absolute value of a number is equal to - 一个数的绝对值等价于下面的表达式 - SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i) - - Negative numbers are represented with ob_size < 0; - 负数表示为 ob_size < 0 - - zero is represented by ob_size == 0. - 整数0 用 ob_size == 0表示 - - In a normalized number, ob_digit[abs(ob_size)-1] (the most significant - digit) is never zero. Also, in all cases, for all valid i, - - 在一个规范的数字ob_digit[abs(ob_size)-1]()永不为0。而且,所有有效的 i 都满足以下要求 - 0 <= ob_digit[i] <= MASK. - - The allocation function takes care of allocating extra memory - so that ob_digit[0] ... ob_digit[abs(ob_size)-1] are actually available. - - CAUTION: Generic code manipulating subtypes of PyVarObject has to - aware that ints abuse ob_size's sign bit. - - 警告: 通用代码操作 PyVarObject 的子类型必须注意 ob_size的符号滥用问题。 -*/ - -struct _longobject { - PyObject_VAR_HEAD - digit ob_digit[1]; -}; -``` - -从源码可以看出 PyLongObject 是变长对象 - - -## 类型对象 PyLong_Type - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5379) - -```c -// Objects/longobject.c - -PyTypeObject PyLong_Type = { - PyVarObject_HEAD_INIT(&PyType_Type, 0) - "int", /* tp_name */ - offsetof(PyLongObject, ob_digit), /* tp_basicsize */ - sizeof(digit), /* tp_itemsize */ - long_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_reserved */ - long_to_decimal_string, /* tp_repr */ - &long_as_number, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - (hashfunc)long_hash, /* tp_hash */ - 0, /* tp_call */ - long_to_decimal_string, /* tp_str */ - PyObject_GenericGetAttr, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | - Py_TPFLAGS_LONG_SUBCLASS, /* tp_flags */ - long_doc, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - long_richcompare, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - long_methods, /* tp_methods */ - 0, /* tp_members */ - long_getset, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - long_new, /* tp_new */ - PyObject_Del, /* tp_free */ -}; -``` - -## 创建整数对象 - -从 PyLong_Type 可以看出,创建一个整数对象的入口函数为 long_new - -`源文件:`[Objects/clinic/longobject.c.h](https://github.com/python/cpython/blob/v3.7.0/Objects/clinic/longobject.c.h#L0) - -```c -// Objects/clinic/longobject.c.h -/*[clinic input] -preserve -[clinic start generated code]*/ - -static PyObject * -long_new_impl(PyTypeObject *type, PyObject *x, PyObject *obase); - -static PyObject * -long_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) -{ - PyObject *return_value = NULL; - static const char * const _keywords[] = {"", "base", NULL}; - static _PyArg_Parser _parser = {"|OO:int", _keywords, 0}; - PyObject *x = NULL; - PyObject *obase = NULL; - - if (!_PyArg_ParseTupleAndKeywordsFast(args, kwargs, &_parser, - &x, &obase)) { - goto exit; - } - return_value = long_new_impl(type, x, obase); - -exit: - return return_value; -} -``` - -具体实现在 long_new_impl `源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L4785) - -```c -// Objects/longobject.c - -/*[clinic input] -@classmethod -int.__new__ as long_new - x: object(c_default="NULL") = 0 - / - base as obase: object(c_default="NULL") = 10 -[clinic start generated code]*/ - -static PyObject * -long_new_impl(PyTypeObject *type, PyObject *x, PyObject *obase) -/*[clinic end generated code: output=e47cfe777ab0f24c input=81c98f418af9eb6f]*/ -{ - Py_ssize_t base; - - if (type != &PyLong_Type) - return long_subtype_new(type, x, obase); /* Wimp out */ - if (x == NULL) { - if (obase != NULL) { - PyErr_SetString(PyExc_TypeError, - "int() missing string argument"); - return NULL; - } - return PyLong_FromLong(0L); - } - if (obase == NULL) - return PyNumber_Long(x); - - base = PyNumber_AsSsize_t(obase, NULL); - if (base == -1 && PyErr_Occurred()) - return NULL; - if ((base != 0 && base < 2) || base > 36) { - PyErr_SetString(PyExc_ValueError, - "int() base must be >= 2 and <= 36, or 0"); - return NULL; - } - - if (PyUnicode_Check(x)) - return PyLong_FromUnicodeObject(x, (int)base); - else if (PyByteArray_Check(x) || PyBytes_Check(x)) { - char *string; - if (PyByteArray_Check(x)) - string = PyByteArray_AS_STRING(x); - else - string = PyBytes_AS_STRING(x); - return _PyLong_FromBytes(string, Py_SIZE(x), (int)base); - } - else { - PyErr_SetString(PyExc_TypeError, - "int() can't convert non-string with explicit base"); - return NULL; - } -} -``` - -从 long_new_impl 函数可以看出有如下几种情况 - -- x == NULL 且 obase != NULL 调用 PyLong_FromLong -- obase 为NULL 调用 PyNumber_Long -- x 和 obase 都不为 NULL - - PyUnicode 调用PyLong_FromUnicodeObject,最终调用PyLong_FromString - - PyByteArray/PyBytes 调用_PyLong_FromBytes,最终调用PyLong_FromString - -## 小整数对象 - -一些整数在一开始就会被初始化一直留存,当再次使用直接从小整数对象池中获取,不用频繁的申请内存。 - -默认的小整数范围是 [-5, 257) `源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L17) - -```c -// Objects/longobject.c - -#ifndef NSMALLPOSINTS -#define NSMALLPOSINTS 257 -#endif -#ifndef NSMALLNEGINTS -#define NSMALLNEGINTS 5 -#endif - -#if NSMALLNEGINTS + NSMALLPOSINTS > 0 -/* Small integers are preallocated in this array so that they - can be shared. - The integers that are preallocated are those in the range - -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive). */ -static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS]; -#ifdef COUNT_ALLOCS -Py_ssize_t quick_int_allocs, quick_neg_int_allocs; -#endif - -static PyObject * -get_small_int(sdigit ival) -{ - PyObject *v; - assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS); - v = (PyObject *)&small_ints[ival + NSMALLNEGINTS]; - Py_INCREF(v); -#ifdef COUNT_ALLOCS - if (ival >= 0) - quick_int_allocs++; - else - quick_neg_int_allocs++; -#endif - return v; -} -#define CHECK_SMALL_INT(ival) \ - do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \ - return get_small_int((sdigit)ival); \ - } while(0) -``` - -宏 **CHECK_SMALL_INT** 会检查传入的数是否在小整数范围内,如果是直接返回。 -可以在创建或复制整数对象等函数中找到 **CHECK_SMALL_INT** 的身影,以下只列出了 -**PyLong_FromLong**,就不一一列举了 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L239) - -```c -// Object/longobject.c - -PyObject * -PyLong_FromLong(long ival) -{ - PyLongObject *v; - unsigned long abs_ival; - unsigned long t; /* unsigned so >> doesn't propagate sign bit */ - int ndigits = 0; - int sign; - - CHECK_SMALL_INT(ival); - - ... -} -``` - -### 小整数初始化 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5462) - -```c -// Objects/longobject.c - -int -_PyLong_Init(void) -{ -#if NSMALLNEGINTS + NSMALLPOSINTS > 0 - int ival, size; - PyLongObject *v = small_ints; - - for (ival = -NSMALLNEGINTS; ival < NSMALLPOSINTS; ival++, v++) { - size = (ival < 0) ? -1 : ((ival == 0) ? 0 : 1); - if (Py_TYPE(v) == &PyLong_Type) { - /* The element is already initialized, most likely - * the Python interpreter was initialized before. - */ - Py_ssize_t refcnt; - PyObject* op = (PyObject*)v; - - refcnt = Py_REFCNT(op) < 0 ? 0 : Py_REFCNT(op); - _Py_NewReference(op); - /* _Py_NewReference sets the ref count to 1 but - * the ref count might be larger. Set the refcnt - * to the original refcnt + 1 */ - Py_REFCNT(op) = refcnt + 1; - assert(Py_SIZE(op) == size); - assert(v->ob_digit[0] == (digit)abs(ival)); - } - else { - (void)PyObject_INIT(v, &PyLong_Type); - } - Py_SIZE(v) = size; - v->ob_digit[0] = (digit)abs(ival); - } -#endif - _PyLong_Zero = PyLong_FromLong(0); - if (_PyLong_Zero == NULL) - return 0; - _PyLong_One = PyLong_FromLong(1); - if (_PyLong_One == NULL) - return 0; - - /* initialize int_info */ - if (Int_InfoType.tp_name == NULL) { - if (PyStructSequence_InitType2(&Int_InfoType, &int_info_desc) < 0) - return 0; - } - - return 1; -} -``` - -## 整数的存储结构 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L1581) - -在 **long_to_decimal_string_internal**中添加如下代码并重新编译安装 -```c -// Objects/longobject.c -static int -long_to_decimal_string_internal(PyObject *aa, - PyObject **p_output, - _PyUnicodeWriter *writer, - _PyBytesWriter *bytes_writer, - char **bytes_str) -{ - PyLongObject *scratch, *a; - PyObject *str = NULL; - Py_ssize_t size, strlen, size_a, i, j; - digit *pout, *pin, rem, tenpow; - int negative; - int d; - enum PyUnicode_Kind kind; - - a = (PyLongObject *)aa; - - // 添加打印代码 - printf("ob_size = %d\n", Py_SIZE(a)); - for (int index = 0; index < Py_SIZE(a); ++index) { - printf("ob_digit[%d] = %d\n", index, a->ob_digit[index]); - } - - ... -} -``` - -编译安装后进入python解释器输入如下代码 - -```python -num = 9223372043297226753 -print(num) - -# output ->>> ob_size = 3 ->>> ob_digit[0] = 1 ->>> ob_digit[1] = 6 ->>> ob_digit[2] = 8 ->>> 9223372043297226753 -``` - -如下图所示 - -![longobject storage](longobject_storage.png) - -注:这里的 30 是由 **PyLong_SHIFT** 决定的,64位系统中,**PyLong_SHIFT** 为30,否则 **PyLong_SHIFT** 为15 - -## 整数对象的数值操作 - -可以看到整数对象的数值操作较多,由于篇幅限制无法一一分析,这里只分析整数的部分操作 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5341) - -```c -// Objects/longobject.c - -static PyNumberMethods long_as_number = { - (binaryfunc)long_add, /*nb_add 加法 */ - (binaryfunc)long_sub, /*nb_subtract 减法 */ - (binaryfunc)long_mul, /*nb_multiply 乘法 */ - long_mod, /*nb_remainder 取余 */ - long_divmod, /*nb_divmod */ - long_pow, /*nb_power 求幂 */ - (unaryfunc)long_neg, /*nb_negative */ - (unaryfunc)long_long, /*tp_positive */ - (unaryfunc)long_abs, /*tp_absolute 绝对值 */ - (inquiry)long_bool, /*tp_bool 求bool值 */ - (unaryfunc)long_invert, /*nb_invert 反转 */ - long_lshift, /*nb_lshift 逻辑左移 */ - (binaryfunc)long_rshift, /*nb_rshift 逻辑右移 */ - long_and, /*nb_and 与操作 */ - long_xor, /*nb_xor 异或 */ - long_or, /*nb_or 或操作 */ - long_long, /*nb_int*/ - 0, /*nb_reserved*/ - long_float, /*nb_float*/ - 0, /* nb_inplace_add */ - 0, /* nb_inplace_subtract */ - 0, /* nb_inplace_multiply */ - 0, /* nb_inplace_remainder */ - 0, /* nb_inplace_power */ - 0, /* nb_inplace_lshift */ - 0, /* nb_inplace_rshift */ - 0, /* nb_inplace_and */ - 0, /* nb_inplace_xor */ - 0, /* nb_inplace_or */ - long_div, /* nb_floor_divide */ - long_true_divide, /* nb_true_divide */ - 0, /* nb_inplace_floor_divide */ - 0, /* nb_inplace_true_divide */ - long_long, /* nb_index */ -}; -``` - -### 整数相加 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3081) - -```c -// Objects/longobject.c - -static PyObject * -long_add(PyLongObject *a, PyLongObject *b) -{ - PyLongObject *z; - - CHECK_BINOP(a, b); - - if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) { - return PyLong_FromLong(MEDIUM_VALUE(a) + MEDIUM_VALUE(b)); - } - if (Py_SIZE(a) < 0) { - if (Py_SIZE(b) < 0) { - z = x_add(a, b); - if (z != NULL) { - /* x_add received at least one multiple-digit int, - and thus z must be a multiple-digit int. - That also means z is not an element of - small_ints, so negating it in-place is safe. */ - assert(Py_REFCNT(z) == 1); - Py_SIZE(z) = -(Py_SIZE(z)); - } - } - else - z = x_sub(b, a); - } - else { - if (Py_SIZE(b) < 0) - z = x_sub(a, b); - else - z = x_add(a, b); - } - return (PyObject *)z; -} -``` - -可以看到整数的加法运算函数long_add根据 a、b的ob_size 又细分为两个函数 (x_add 和 x_sub) 做处理 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L2991) - -```c -// Objects/longobject.c - -/* Add the absolute values of two integers. */ -static PyLongObject * -x_add(PyLongObject *a, PyLongObject *b) -{ - Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b)); - PyLongObject *z; - Py_ssize_t i; - digit carry = 0; - - /* Ensure a is the larger of the two: */ - // 确保 a 大于 b - if (size_a < size_b) { - { PyLongObject *temp = a; a = b; b = temp; } - { Py_ssize_t size_temp = size_a; - size_a = size_b; - size_b = size_temp; } - } - z = _PyLong_New(size_a+1); - if (z == NULL) - return NULL; - for (i = 0; i < size_b; ++i) { - carry += a->ob_digit[i] + b->ob_digit[i]; - z->ob_digit[i] = carry & PyLong_MASK; - carry >>= PyLong_SHIFT; - } - for (; i < size_a; ++i) { - carry += a->ob_digit[i]; - z->ob_digit[i] = carry & PyLong_MASK; - carry >>= PyLong_SHIFT; - } - z->ob_digit[i] = carry; - return long_normalize(z); -} -``` - -加法运算函数 x_add 从 ob_digit 数组的低位开始依次按位相加,carry做进位处理,然后处理a对象的高位数字,最后使用 long_normalize 函数调整 ob_size,确保ob_digit[abs(ob_size)-1]不为零,这与普通四则运算的加法运算相同,只不过进位单元不同而已 - -![longobject x_add](longobject_x_add.png) - - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3025) - -```c -// Objects/longobject.c - -/* Subtract the absolute values of two integers. */ - -static PyLongObject * -x_sub(PyLongObject *a, PyLongObject *b) -{ - Py_ssize_t size_a = Py_ABS(Py_SIZE(a)), size_b = Py_ABS(Py_SIZE(b)); - PyLongObject *z; - Py_ssize_t i; - int sign = 1; - digit borrow = 0; - - /* Ensure a is the larger of the two: */ - // 确保 a 大于 b - if (size_a < size_b) { - sign = -1; - { PyLongObject *temp = a; a = b; b = temp; } - { Py_ssize_t size_temp = size_a; - size_a = size_b; - size_b = size_temp; } - } - else if (size_a == size_b) { - /* Find highest digit where a and b differ: */ - // 找到最高位 a 与 b的差异 - i = size_a; - while (--i >= 0 && a->ob_digit[i] == b->ob_digit[i]) - ; - if (i < 0) - return (PyLongObject *)PyLong_FromLong(0); - if (a->ob_digit[i] < b->ob_digit[i]) { - sign = -1; - { PyLongObject *temp = a; a = b; b = temp; } - } - size_a = size_b = i+1; - } - z = _PyLong_New(size_a); - if (z == NULL) - return NULL; - for (i = 0; i < size_b; ++i) { - /* The following assumes unsigned arithmetic - works module 2**N for some N>PyLong_SHIFT. */ - borrow = a->ob_digit[i] - b->ob_digit[i] - borrow; - z->ob_digit[i] = borrow & PyLong_MASK; - borrow >>= PyLong_SHIFT; - borrow &= 1; /* Keep only one sign bit */ - } - for (; i < size_a; ++i) { - borrow = a->ob_digit[i] - borrow; - z->ob_digit[i] = borrow & PyLong_MASK; - borrow >>= PyLong_SHIFT; - borrow &= 1; /* Keep only one sign bit */ - } - assert(borrow == 0); - if (sign < 0) { - Py_SIZE(z) = -Py_SIZE(z); - } - return long_normalize(z); -} -``` - -与普通四则运算减法相同,数不够大则向高一位借位, -减法运算函数 x_sub 的示例图如下,注:PyLong_SHIFT为30 - -![longobject x_sub](longobject_x_sub.png) - -### 整数相乘 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3547) - -```c -// Objects/longobject.c -static PyObject * -long_mul(PyLongObject *a, PyLongObject *b) -{ - PyLongObject *z; - - CHECK_BINOP(a, b); - - /* fast path for single-digit multiplication */ - if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) { - stwodigits v = (stwodigits)(MEDIUM_VALUE(a)) * MEDIUM_VALUE(b); - return PyLong_FromLongLong((long long)v); - } - - z = k_mul(a, b); - /* Negate if exactly one of the inputs is negative. */ - if (((Py_SIZE(a) ^ Py_SIZE(b)) < 0) && z) { - _PyLong_Negate(&z); - if (z == NULL) - return NULL; - } - return (PyObject *)z; -} -``` - -k_mul函数是一种快速乘法 [源文件]( -https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L3268) - -> Karatsuba的算法主要是用于两个大数的乘法,极大提高了运算效率,相较于普通乘法降低了复杂度,并在其中运用了递归的思想。 -> 基本的原理和做法是将位数很多的两个大数x和y分成位数较少的数,每个数都是原来x和y位数的一半。 -> 这样处理之后,简化为做三次乘法,并附带少量的加法操作和移位操作。 - -具体可以看wiki [Karatsuba算法](https://www.wikiwand.com/zh-hans/Karatsuba算法)的实现 diff --git a/objects/longobject_storage.png b/objects/longobject_storage.png deleted file mode 100644 index 5d9b664..0000000 Binary files a/objects/longobject_storage.png and /dev/null differ diff --git a/objects/longobject_x_add.png b/objects/longobject_x_add.png deleted file mode 100644 index 63cb695..0000000 Binary files a/objects/longobject_x_add.png and /dev/null differ diff --git a/objects/longobject_x_sub.png b/objects/longobject_x_sub.png deleted file mode 100644 index e500b12..0000000 Binary files a/objects/longobject_x_sub.png and /dev/null differ diff --git a/objects/object.md b/objects/object.md deleted file mode 100644 index 4eb8c5c..0000000 --- a/objects/object.md +++ /dev/null @@ -1,378 +0,0 @@ -# Python 对象初探 - -在Python的世界一切皆对象,不论是整数,还是字符串,甚至连类型、函数等都是一种对象。 - -## 对象的分类 - -以下是Python对象的大致的一个分类 - -- Fundamental 对象: 类型对象 -- Numeric 对象: 数值对象 -- Sequence 对象: 容纳其他对象的序列集合对象 -- Mapping 对象: 类似 C++中的 map 的关联对象 -- Internal 对象: Python 虚拟机在运行时内部使用的对象 - -![object category](object_category.jpg) - -## 对象机制的基石 PyObject - -对于初学者来说这么多类型的对象怎么学?别着急,我们后续章节会解答。 - -在开始我们的学习之旅之前,我们要先认识一个结构体**PyObject**,可以说Python的对象机制就是基于**PyObject**拓展开来的,所以我们先看看**PyObject** 到底长什么样。 - -`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L106) - -```c -// Include/object.h -#define _PyObject_HEAD_EXTRA \ - struct _object *_ob_next; \ - struct _object *_ob_prev; - -typedef struct _object { - _PyObject_HEAD_EXTRA // 双向链表 垃圾回收 需要用到 - Py_ssize_t ob_refcnt; // 引用计数 - struct _typeobject *ob_type; // 指向类型对象的指针,决定了对象的类型 -} PyObject; -``` - -Python中的所有对象都拥有一些相同的内容,而这些内容就定义在**PyObject**中, - -**PyObject** 包含 一个用于垃圾回收的双向链表,一个引用计数变量 `ob_refcnt` 和 一个类型对象指针`ob_type` - -![PyObject](PyObject.jpg) - -## 定长对象与变长对象 - -Python对象除了前面提到的那种分类方法外,还可以分为定长对象和变长对象这两种形式。 - -变长对象都拥有一个相同的内容 **PyVarObject**,而 **PyVarObject**也是基于**PyObject**扩展的。 - -从代码中可以看出**PyVarObject**比**PyObject**多出了一个用于存储元素个数的变量*ob_size*。 - -`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L106) - -```c -// Include/object.h -typedef struct _object { - _PyObject_HEAD_EXTRA - Py_ssize_t ob_refcnt; - struct _typeobject *ob_type; -} PyObject; - -typedef struct { - PyObject ob_base; - Py_ssize_t ob_size; /* Number of items in variable part */ -} PyVarObject; -``` - - -![PyVarObject](PyVarObject.jpg) - -## 类型对象 - -前面我们提到了**PyObject** 的 对象类型指针`struct _typeobject *ob_type`,它指向的类型对象就决定了一个对象是什么类型的。 - -这是一个非常重要的结构体,它不仅仅决定了一个对象的类型,还包含大量的`元信息`, -包括创建对象需要分配多少内存,对象都支持哪些操作等等。 - -接下来我们看一下`struct _typeobject`代码 - -在 **PyTypeObject** 的定义中包含许多信息,主要分类以下几类: -- 类型名, tp_name, 主要用于 Python 内部调试用 -- 创建该类型对象时分配的空间大小信息,即 `tp_basicsize` 和 `tp_itemsize` -- 与该类型对象相关的操作信息(如 `tp_print` 这样的函数指针) -- 一些对象属性 - -`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L346) - -```c -// Include/object.h -typedef struct _typeobject { - PyObject_VAR_HEAD - const char *tp_name; /* For printing, in format "." */ // 类型名 - Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */ - // 创建该类型对象分配的内存空间大小 - - // 一堆方法定义,函数和指针 - /* Methods to implement standard operations */ - destructor tp_dealloc; - printfunc tp_print; - 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; - - ...... - -} PyTypeObject; -``` - - -## 类型的类型 - -在 **PyTypeObjet** 定义开始有一个宏`PyOject_VAR_HEAD`,查看源码可知 **PyTypeObjet** 是一个变长对象 - -`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L98) - -```c -// Include/object.h -#define PyObject_VAR_HEAD PyVarObject ob_base; -``` - -对象的类型是由该对象指向的 类型对象 决定的,那么类型对象的类型是由谁决定的呢? -对于其他对象,可以通过与其关联的类型对象确定其类型,那么通过什么来确定一个对象是类型对象呢? -答案就是 `PyType_Type` - -`源文件:`[Objects/typeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/typeobject.c#L3540) - -```c -// Objects/typeobject.c -PyTypeObject PyType_Type = { - PyVarObject_HEAD_INIT(&PyType_Type, 0) - "type", /* tp_name */ - sizeof(PyHeapTypeObject), /* tp_basicsize */ - sizeof(PyMemberDef), /* tp_itemsize */ - - ...... -}; -``` - -`PyType_Type` 在类型机制中至关重要,所有用户自定义 `class` 所 -对应的 `PyTypeObject` 对象都是通过 `PyType_Type`创建的 - - -接下来我们看 `PyLong_Type` 是怎么与 `PyType_Type` 建立联系的。 -前面提到,在Python中,每一个对象都将自己的引用计数、类型信息保存在开始的部分中。 -为了方便对这部分内存初始化,Python中提供了几个有用的宏: - -`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L69) - -```c -// Include/object.h -#ifdef Py_TRACE_REFS - #define _PyObject_EXTRA_INIT 0, 0, -#else - #define _PyObject_EXTRA_INIT -#endif - -#define PyObject_HEAD_INIT(type) \ - { _PyObject_EXTRA_INIT \ - 1, type }, -``` - -这些宏在各种内建类型对象的初始化中被大量使用。 -以`PyLong_Type`为例,可以清晰的看到一般的类型对象和`PyType_Type`之间的关系 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5379) - -```c -// Objects/longobject.c - -PyTypeObject PyLong_Type = { - PyVarObject_HEAD_INIT(&PyType_Type, 0) - "int", /* tp_name */ - offsetof(PyLongObject, ob_digit), /* tp_basicsize */ - sizeof(digit), /* tp_itemsize */ - - ...... -}; -``` - -下图是对象运行时的图像表现 - -![](object_runtime_relation.jpg) - - -## 对象的创建 - -Python创建对象有两种方式 - -### 范型API 或称为 AOL (Abstract Object Layer) - -这类API通常形如`PyObject_XXX`这样的形式。可以应用在任何Python对象上, -如`PyObject_New`。创建一个整数对象的方式 - -```c -PyObject* longobj = PyObject_New(Pyobject, &PyLong_Type); -``` - -### 与类型相关的API 或称为 COL (Concrete Object Layer) - -这类API 通常只能作用于某一种类型的对象上,对于每一种内建对象 -Python都提供了这样一组API。例如整数对象,我们可以利用如下的API创建 -```c -PyObject *longObj = PyLong_FromLong(10); -``` - -## 对象的行为 - -在 **PyTypeObject** 中定义了大量的函数指针。这些函数指针可以视为类型对象中 -所定义的操作,这些操作直接决定着一个对象在运行时所表现出的行为,比如 **PyTypeObject** 中的 `tp_hash` 指明了该类型对象如何生成其`hash`值。 - -在**PyTypeObject**的代码中,我们还可以看到非常重要的三组操作族 -- `PyNumberMethods *tp_as_number` -- `PySequenceMethods *tp_as_sequence` -- `PyMappingMethods *tp_as_mapping` - - -**PyNumberMethods** 的代码如下 - -`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L240) - -```c -// Include/object.h -typedef PyObject * (*binaryfunc)(PyObject *, PyObject *); - -typedef struct { - binaryfunc nb_matrix_multiply; - binaryfunc nb_inplace_matrix_multiply; - - ...... -} PyNumberMethods; -``` - -**PyNumberMethods** 定义了一个数值对象该支持的操作。一个数值对象如 整数对象,那么它的类型对象 `PyLong_Type`中`tp_as_number.nb_add` -就指定了它进行加法操作时的具体行为。 - -在以下代码中可以看出`PyLong_Type`中的`tp_as_number`项指向的是`long_as_number` - -`源文件:`[Objects/longobject.h](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5342) - -```c -// Objects/longobject.c -static PyNumberMethods long_as_number = { - (binaryfunc)long_add, /*nb_add*/ - (binaryfunc)long_sub, /*nb_subtract*/ - (binaryfunc)long_mul, /*nb_multiply*/ - - ...... -}; - -PyTypeObject PyLong_Type = { - PyVarObject_HEAD_INIT(&PyType_Type, 0) - "int", /* tp_name */ - offsetof(PyLongObject, ob_digit), /* tp_basicsize */ - sizeof(digit), /* tp_itemsize */ - long_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_reserved */ - long_to_decimal_string, /* tp_repr */ - &long_as_number, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - - ...... -}; -``` - -`PySequenceMethods *tp_as_sequence` 和 `PyMappingMethods *tp_as_mapping`的分析与`PyNumberMethods *tp_as_number` 相同,大家可以自行查阅源码 - - -## 对象的多态性 - -Python创建一个对象比如 **PyLongObject** 时,会分配内存进行初始化,然后 -Python内部会用 `PyObject*` 变量来维护这个对象,其他对象也与此类似 - -所以在 Python 内部各个函数之间传递的都是一种范型指针 `PyObject*` -我们不知道这个指针所指的对象是什么类型,只能通过所指对象的 `ob_type` 域 -动态进行判断,而Python正是通过 `ob_type` 实现了多态机制 - -考虑以下的 calc_hash 函数 - -```c -Py_hash_t -calc_hash(PyObject* object) -{ - Py_hash_t hash = object->ob_type->tp_hash(object); - return hash; -} -``` - -如果传递给 calc_hash 函数的指针是一个 `PyLongObject*`,那么它会调用 PyLongObject 对象对应的类型对象中定义的 hash操作`tp_hash`,`tp_hash`可以在**PyTypeObject**中找到, -而具体赋值绑定我们可以在 `PyLong_Type` 初始化代码中看到绑定的是`long_hash`函数 - -`源文件:`[Objects/longobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/longobject.c#L5379) - -```c -// Objects/longobject.c -PyTypeObject PyLong_Type = { - PyVarObject_HEAD_INIT(&PyType_Type, 0) - "int", /* tp_name */ - ... - - (hashfunc)long_hash, /* tp_hash */ - - ... -}; -``` - -如果指针是一个 `PyUnicodeObject*`,那么就会调用 PyUnicodeObject 对象对应的类型对象中定义的hash操作,查看源码可以看到 实际绑定的是 `unicode_hash`函数 - -`源文件:`[Objects/unicodeobject.c](https://github.com/python/cpython/blob/v3.7.0/Objects/unicodeobject.c#L15066) - -```c -// Objects/unicodeobject.c -PyTypeObject PyUnicode_Type = { - PyVarObject_HEAD_INIT(&PyType_Type, 0) - "str", /* tp_name */ - - ... - - (hashfunc) unicode_hash, /* tp_hash*/ - - ... -}; -``` - -## 引用计数 - -Python 通过引用计数来管理维护对象在内存中的存在与否 - -Python 中的每个东西都是一个对象, 都有`ob_refcnt` 变量,这个变量维护对象的引用计数,从而最终决定该对象的创建与销毁 - -在Python中,主要通过 `Py_INCREF(op)`与`Py_DECREF(op)` 这两个宏 -来增加和减少对一个对象的引用计数。当一个对象的引用计数减少到0之后, -`Py_DECREF`将调用该对象的`tp_dealloc`来释放对象所占用的内存和系统资源; - -但这并不意味着最终一定会调用 `free` 释放内存空间。因为频繁的申请、释放内存会大大降低Python的执行效率。因此Python中大量采用了内存对象池的技术,使得对象释放的空间归还给内存池而不是直接`free`,后续使用可先从对象池中获取 - -`源文件:`[Include/object.h](https://github.com/python/cpython/blob/v3.7.0/Include/object.h#L777) - -```c -// Include/object.h -#define _Py_NewReference(op) ( \ - _Py_INC_TPALLOCS(op) _Py_COUNT_ALLOCS_COMMA \ - _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \ - Py_REFCNT(op) = 1) - -#define Py_INCREF(op) ( \ - _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA \ - ((PyObject *)(op))->ob_refcnt++) - -#define Py_DECREF(op) \ - do { \ - PyObject *_py_decref_tmp = (PyObject *)(op); \ - if (_Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA \ - --(_py_decref_tmp)->ob_refcnt != 0) \ - _Py_CHECK_REFCNT(_py_decref_tmp) \ - else \ - _Py_Dealloc(_py_decref_tmp); \ - } while (0) -``` diff --git a/objects/object_category.jpg b/objects/object_category.jpg deleted file mode 100644 index 4a8e7db..0000000 Binary files a/objects/object_category.jpg and /dev/null differ diff --git a/objects/object_runtime_relation.jpg b/objects/object_runtime_relation.jpg deleted file mode 100644 index ed94950..0000000 Binary files a/objects/object_runtime_relation.jpg and /dev/null differ diff --git a/objects/python_dict_mem.png b/objects/python_dict_mem.png deleted file mode 100644 index 0d14a37..0000000 Binary files a/objects/python_dict_mem.png and /dev/null differ diff --git a/objects/python_set.png b/objects/python_set.png deleted file mode 100644 index ffd7628..0000000 Binary files a/objects/python_set.png and /dev/null differ diff --git a/objects/set-object.md b/objects/set-object.md deleted file mode 100644 index d34f01b..0000000 --- a/objects/set-object.md +++ /dev/null @@ -1,596 +0,0 @@ -# python集合 - -set是无序且不重复的集合,是可变的,通常用来从列表中删除重复项以及计算数学运算,如交集、并集、差分和对称差分等集合操作。set 支持 x in set, len(set),和 for x in set。作为一个无序的集合,set不记录元素位置或者插入点。因此,sets不支持 indexing, 或其它类序列的操作。 - -## python集合概述 - -在set中,对应的set的值的存储是通过结构setentry来保存数据值的; - -`源文件:`[include/setobject.h](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Include/setobject.h#L26) - -```c -typedef struct { - PyObject *key; - Py_hash_t hash; /* Cached hash code of the key */ -} setentry; -``` - -key就是保存的数据,hash就是保存的数据的hash,便于查找,set也是基于hash表来实现。对应的setentry所对应的set的数据结构如下; - -`源文件:`[include/setobject.h](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Include/setobject.h#L42) - -```c -typedef struct { - PyObject_HEAD - - Py_ssize_t fill; /* Number active and dummy entries*/ // 包括已经使用的entry与空entry值的总和 - Py_ssize_t used; /* Number active entries */ // 已经使用可用的总量 - - /* The table contains mask + 1 slots, and that's a power of 2. - * We store the mask instead of the size because the mask is more - * frequently needed. - */ - Py_ssize_t mask;                                // 与hash求和的mask - - /* The table points to a fixed-size smalltable for small tables - * or to additional malloc'ed memory for bigger tables. - * The table pointer is never NULL which saves us from repeated - * runtime null-tests. - */ - setentry *table; // 保存数据的数组数组指针 - Py_hash_t hash; /* Only used by frozenset objects */ - Py_ssize_t finger; /* Search finger for pop() */ - - setentry smalltable[PySet_MINSIZE]; // 保存数据的数组 默认初始化为8个元素,通过table指向 - PyObject *weakreflist; /* List of weak references */ -} PySetObject; -``` - -一个set就对应一个PySetObject类型数据,set会根据保存的元素自动调整大小。相关的内存布局如下; - -![内存图片](./python_set.png) - -## python集合(set)示例 - -示例脚本如下: - -```python -set_a = {1,2}  -set_a.add(3) -set_a.add(4) -set_a.remove(1) -set_a.update({3,}) -set_a.union({1,5}) -``` - -通过python反汇编获取该脚本的字节码; - -``` -python -m dis set_test.py -``` - -输出的字节码如下所示; - -```shell - 1 0 LOAD_CONST 0 (1) - 3 LOAD_CONST 1 (2) - 6 BUILD_SET 2 - 9 STORE_NAME 0 (set_a) - - 2 12 LOAD_NAME 0 (set_a) - 15 LOAD_ATTR 1 (add) - 18 LOAD_CONST 2 (3) - 21 CALL_FUNCTION 1 - 24 POP_TOP - - 3 25 LOAD_NAME 0 (set_a) - 28 LOAD_ATTR 1 (add) - 31 LOAD_CONST 3 (4) - 34 CALL_FUNCTION 1 - 37 POP_TOP - - 4 38 LOAD_NAME 0 (set_a) - 41 LOAD_ATTR 2 (remove) - 44 LOAD_CONST 0 (1) - 47 CALL_FUNCTION 1 - 50 POP_TOP - - 5 51 LOAD_NAME 0 (set_a) - 54 LOAD_ATTR 3 (update) - 57 LOAD_CONST 2 (3) - 60 BUILD_SET 1 - 63 CALL_FUNCTION 1 - 66 POP_TOP - - 6 67 LOAD_NAME 0 (set_a) - 70 LOAD_ATTR 4 (union) - 73 LOAD_CONST 0 (1) - 76 LOAD_CONST 4 (5) - 79 BUILD_SET 2 - 82 CALL_FUNCTION 1 - 85 POP_TOP - 86 LOAD_CONST 5 (None) - 89 RETURN_VALUE -``` - - -通过该字节码指令可知,创建set调用了BUILD_SET指令,初始化完成之后,就调用set的add方法添加元素,调用remove删除元素,调用update来更新集合,通过union来合并集合。接下来就详细分析一下相关的操作流程。 - -## set的创建与初始化 - -查找BUILD_SET的虚拟机执行函数如下; - -`源文件:`[Python/ceval.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Python/ceval.c#L2318) - -```c -// Python/ceval.c - - TARGET(BUILD_SET) { - PyObject *set = PySet_New(NULL); // 新建并初始化一个set - int err = 0; - int i; - if (set == NULL) - goto error; - for (i = oparg; i > 0; i--) { // 将传入初始化的参数传入 - PyObject *item = PEEK(i); - if (err == 0) - err = PySet_Add(set, item); // 并依次对set进行添加操作 - Py_DECREF(item); - } - STACKADJ(-oparg);                // 移动弹栈 - if (err != 0) { - Py_DECREF(set); - goto error; - } - PUSH(set);                     // 讲set压栈 - DISPATCH();                    // 执行下一条指令 - } - -``` - -此时继续查看PySet_New函数的执行流程; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L2286) - - -```c -PyObject * -PySet_New(PyObject *iterable) -{ - return make_new_set(&PySet_Type, iterable); -} - -... - - -static PyObject * -make_new_set(PyTypeObject *type, PyObject *iterable) -{ - PySetObject *so; - - so = (PySetObject *)type->tp_alloc(type, 0); // 申请该元素的内存 - if (so == NULL) // 内存申请失败则返回为空 - return NULL; - - so->fill = 0; // 初始化的时候都为0 - so->used = 0; - so->mask = PySet_MINSIZE - 1; // PySet_MINSIZE默认我8,mask为7 - so->table = so->smalltable; // 将保存数据的头指针指向table - so->hash = -1; // 设置hash值为-1 - so->finger = 0; - so->weakreflist = NULL; - - if (iterable != NULL) { // 如果有迭代器 - if (set_update_internal(so, iterable)) { // 将内容更新到so中 - Py_DECREF(so); - return NULL; - } - } - - return (PyObject *)so; // 返回初始化完成的set -} -``` - -从PySet_New的执行流程可知,字典的初始化过程就是初始化相关数据结构。 - -## set的插入 - -在本例的初始化过程中,由于传入了初始值1,2,所以会在执行字节码指令的时候,执行PySet_Add,该函数的本质与set_a.add(3)本质都调用了更底层set_add_key函数; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L2338) - -```c - -int -PySet_Add(PyObject *anyset, PyObject *key) -{ - if (!PySet_Check(anyset) && - (!PyFrozenSet_Check(anyset) || Py_REFCNT(anyset) != 1)) { - PyErr_BadInternalCall(); - return -1; - } - return set_add_key((PySetObject *)anyset, key); // 向字典中添加key; -} -``` - -继续查看set_add_key函数的执行过程; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L419) - -```c -static int -set_add_key(PySetObject *so, PyObject *key) -{ - Py_hash_t hash; - - if (!PyUnicode_CheckExact(key) || - (hash = ((PyASCIIObject *) key)->hash) == -1) { - hash = PyObject_Hash(key); // 获取传入值的hash值 - if (hash == -1) // 如果不能hash则返回-1 - return -1; - } - return set_add_entry(so, key, hash); // 计算完成后添加值 -} -``` - -该函数主要就是检查传入的key是否能够被hash,如果能够被hash则直接返回,如果能被hash则继续调用set_add_entry函数将值加入到set中; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L136) - -```c - -static int -set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash) -{ - setentry *table; - setentry *freeslot; - setentry *entry; - size_t perturb; - size_t mask; - size_t i; /* Unsigned for defined overflow behavior */ - size_t j; - int cmp; - - /* Pre-increment is necessary to prevent arbitrary code in the rich - comparison from deallocating the key just before the insertion. */ - Py_INCREF(key); // 提高key的引用计数 - - restart: - - mask = so->mask;  // 获取so->mask - i = (size_t)hash & mask;  // 通过传入的hash与mask求索引下标 - - entry = &so->table[i];    // 获取索引对应的值 - if (entry->key == NULL) // 如果获取索引的值没有被使用则直接跳转到found_unused处执行 - goto found_unused; - - freeslot = NULL; - perturb = hash;    // perturb设置为当前hash值 -  - while (1) { - if (entry->hash == hash) { // 如果当前hash值相等 - PyObject *startkey = entry->key;                      // 获取当前key - /* startkey cannot be a dummy because the dummy hash field is -1 */ - assert(startkey != dummy); // 检查key是否为dummy - if (startkey == key) // 如果找到的值与传入需要设置的值相同则跳转到found_active处执行 - goto found_active; - if (PyUnicode_CheckExact(startkey) - && PyUnicode_CheckExact(key) - && _PyUnicode_EQ(startkey, key)) // 如果是unicode,通过类型转换检查两个key的内容是否相同,如果不相同则跳转到found_active处 - goto found_active; - table = so->table; // 如果没有找到,则获取当前table的头部节点 - Py_INCREF(startkey); - cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);          // 如果是其他类型的对象则调用比较方法去比较两个key是否相同 - Py_DECREF(startkey); - if (cmp > 0) /* likely */ // 如果找到则跳转到found_active - goto found_active; - if (cmp < 0) - goto comparison_error; // 如果小于0,则是两个类型对比失败 - /* Continuing the search from the current entry only makes - sense if the table and entry are unchanged; otherwise, - we have to restart from the beginning */ - if (table != so->table || entry->key != startkey) // 如果set改变了则重新开始查找 - goto restart; - mask = so->mask; /* help avoid a register spill */    - } - else if (entry->hash == -1) - freeslot = entry;    // 如果不能hash 则设置freeslot - - if (i + LINEAR_PROBES <= mask) {               // 检查当前索引值加上 9小于当前mask - for (j = 0 ; j < LINEAR_PROBES ; j++) { // 循环9次 - entry++;     // 向下一个位置 - if (entry->hash == 0 && entry->key == NULL)              // 如果找到当前hash为空或者key为空的则跳转到found_unused_or_dummy处执行 - goto found_unused_or_dummy; - if (entry->hash == hash) {   // 如果找到的hash值相同 - PyObject *startkey = entry->key; // 获取该值 - assert(startkey != dummy); // 检查是否为dummy - if (startkey == key) // 如果key相同则跳转到found_active处执行 - goto found_active; - if (PyUnicode_CheckExact(startkey) - && PyUnicode_CheckExact(key) - && _PyUnicode_EQ(startkey, key)) // 检查是否为unicode,并比较如果不相同则跳转到found_active - goto found_active; - table = so->table; // 调用key本身的方法比较 - Py_INCREF(startkey); - cmp = PyObject_RichCompareBool(startkey, key, Py_EQ); - Py_DECREF(startkey); - if (cmp > 0) - goto found_active; - if (cmp < 0) - goto comparison_error; - if (table != so->table || entry->key != startkey) - goto restart; - mask = so->mask; - } - else if (entry->hash == -1) - freeslot = entry; - } - } - - perturb >>= PERTURB_SHIFT; // 如果没有找到则获取下一个索引值 - i = (i * 5 + 1 + perturb) & mask; // 右移5位 加上 索引值*5 加1与mask求余获取下一个索引值 - - entry = &so->table[i]; // 获取下一个元素 - if (entry->key == NULL)               // 如果找到为空则直接跳转到found_unused_or_dummy处 - goto found_unused_or_dummy; - } - - found_unused_or_dummy: - if (freeslot == NULL)                                  // 检查freeslot是否为空如果为空则跳转到found_unused处执行即找到了dummy位置 - goto found_unused; - so->used++;                       // 使用数加1 - freeslot->key = key;                                   // 设置key与hash值 - freeslot->hash = hash; - return 0; - - found_unused: - so->fill++;                                        // 使用总数加1 - so->used++;                                        // 使用总数加1  - entry->key = key;                                     // 设置key与hash值 - entry->hash = hash; - if ((size_t)so->fill*5 < mask*3)                           // 检查已经使用的值是否是总数的3/5 - return 0; - return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);    // 如果已使用的总数大于3/5则重新调整table,如果set使用的总数超过了50000则扩展为以前的2倍否则就是四倍 - - found_active: - Py_DECREF(key);                                      // 如果找到了该值 则什么也不做 - return 0; - - comparison_error: - Py_DECREF(key);                                      // 如果比较失败则返回-1 - return -1; -} -``` - -此时基本的流程就是通过传入的hash值,如果计算出的索引值,没有值,则直接将该值存入对应的entry中,如果相同则不插入,如果索引对应的值且值不同,则遍历从该索引往后9个位置的值,依次找到有空余位置的值,并将该值设置进去。如果设置该值之后使用的数量占总的申请数量超过了3/5则重新扩充set,扩充的原则就是如果当前的set->used>50000就进行两倍扩充否则就进行四倍扩充。 - -插入的概述如下,默认s初始化为空; - -```python -s.add(1) // index = 1 & 7 = 1 -``` - -![插入1](./set_insert_one.png) - -```python -s.add(2) // index = 2 & 7 = 2 -``` - -![插入2](./set_insert_two.png) - -```python -s.add(7) // index = 9 & 7 = 1 -``` - -![插入9](./set_insert_nine.png) - -大致的set的插入过程执行完毕。 - -## set的删除 - -set的删除操作主要集中在set_remove()函数上,如下示例; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L1921) - -```c - -static PyObject * -set_remove(PySetObject *so, PyObject *key) -{ - PyObject *tmpkey; - int rv; - - rv = set_discard_key(so, key); // 将该key设置为dummy - if (rv < 0) { - if (!PySet_Check(key) || !PyErr_ExceptionMatches(PyExc_TypeError)) // 检查是否为set类型 - return NULL; - PyErr_Clear(); - tmpkey = make_new_set(&PyFrozenSet_Type, key);             // 对该值重新初始化为forzenset - if (tmpkey == NULL) - return NULL; - rv = set_discard_key(so, tmpkey);                     // 设置该key为空 - Py_DECREF(tmpkey); - if (rv < 0) - return NULL; - } - - if (rv == DISCARD_NOTFOUND) { // 如果没有找到则报错 - _PyErr_SetKeyError(key); - return NULL; - } - Py_RETURN_NONE; -} -``` - -此时就会调用set_discard_key方法来讲对应的entry设置为dummy;set_discard_key方法如下; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L447) - -```c - -static int -set_discard_key(PySetObject *so, PyObject *key) -{ - Py_hash_t hash; - - if (!PyUnicode_CheckExact(key) || - (hash = ((PyASCIIObject *) key)->hash) == -1) { - hash = PyObject_Hash(key);  // 检查是否可用hash如果可用则调用set_discard_entry方法 - if (hash == -1) - return -1; - } - return set_discard_entry(so, key, hash); -} -``` - -该函数主要就是做了检查key是否可用hash的检查,此时如果可用hash则调用set_discard_entry方法; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L400) - -```c - -static int -set_discard_entry(PySetObject *so, PyObject *key, Py_hash_t hash) -{ - setentry *entry; - PyObject *old_key; - - entry = set_lookkey(so, key, hash);      // 查找该值 set_lookkey该方法与插入的逻辑类似大家可自行查看 - if (entry == NULL)                 // 如果没有找到则返回-1 - return -1; - if (entry->key == NULL) - return DISCARD_NOTFOUND;           // 找到entry而key为空则返回notfound - old_key = entry->key; // 找到正常值则讲该值对应的entry设置为dummy - entry->key = dummy; - entry->hash = -1; // hash值为-1 - so->used--; // 使用数量减1 但是fill数量未变 - Py_DECREF(old_key);                 // 减少该对象引用 - return DISCARD_FOUND;                // 返回返现 -} -``` - -此时就是查找该值,如果找到该值并将该值设置为dummy,并且将used值减1,此处没有减去fill的数量,从此处可知,fill包括所有曾经申请过的数量。 - -## set的resize - -set的resize主要依靠set_table_reseize函数来实现; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L302) - -```c -static int -set_table_resize(PySetObject *so, Py_ssize_t minused) -{ - setentry *oldtable, *newtable, *entry; - Py_ssize_t oldmask = so->mask; // 设置旧的mask - size_t newmask; - int is_oldtable_malloced; - setentry small_copy[PySet_MINSIZE]; // 最小的拷贝数组 - - assert(minused >= 0); - - /* Find the smallest table size > minused. */ - /* XXX speed-up with intrinsics */ - size_t newsize = PySet_MINSIZE; - while (newsize <= (size_t)minused) { - newsize <<= 1; // The largest possible value is PY_SSIZE_T_MAX + 1.  // 查找位于minused最大的PySet_MINSIZE的n次方的值 - } - - /* Get space for a new table. */ - oldtable = so->table;                   // 先获取旧的table - assert(oldtable != NULL); - is_oldtable_malloced = oldtable != so->smalltable; - - if (newsize == PySet_MINSIZE) {                  // 如果获取的新大小与PySet_MINSIZE的大小相同 - /* A large table is shrinking, or we can't get any smaller. */ - newtable = so->smalltable;                  // 获取新table的地址 - if (newtable == oldtable) {                 // 如果相同 - if (so->fill == so->used) {              // 如果使用的相同则什么都不做 - /* No dummies, so no point doing anything. */ - return 0; - } - /* We're not going to resize it, but rebuild the - table anyway to purge old dummy entries. - Subtle: This is *necessary* if fill==size, - as set_lookkey needs at least one virgin slot to - terminate failing searches. If fill < size, it's - merely desirable, as dummies slow searches. */ - assert(so->fill > so->used); - memcpy(small_copy, oldtable, sizeof(small_copy)); // 将数据拷贝到set_lookkey中 - oldtable = small_copy;                   - } - } - else { - newtable = PyMem_NEW(setentry, newsize); // 新申请内存 - if (newtable == NULL) {                     // 如果为空则申请内存失败报错 - PyErr_NoMemory(); - return -1; - } - } - - /* Make the set empty, using the new table. */ - assert(newtable != oldtable); // 检查新申请的与就table不同 - memset(newtable, 0, sizeof(setentry) * newsize);        // 新申请的内存置空 - so->mask = newsize - 1; // 设置新的size - so->table = newtable; // 重置table指向新table - - /* Copy the data over; this is refcount-neutral for active entries; - dummy entries aren't copied over, of course */ - newmask = (size_t)so->mask; // 获取新的mask - if (so->fill == so->used) { // 如果使用的与曾经使用的数量相同 - for (entry = oldtable; entry <= oldtable + oldmask; entry++) { - if (entry->key != NULL) { - set_insert_clean(newtable, newmask, entry->key, entry->hash);  // 如果值不为空则插入到新的table中 - } - } - } else { - so->fill = so->used;                        // 如果不相同则重置fill为used的值 - for (entry = oldtable; entry <= oldtable + oldmask; entry++) { - if (entry->key != NULL && entry->key != dummy) {     // 检查如果不为dummy并且key不为空的情况下 - set_insert_clean(newtable, newmask, entry->key, entry->hash);  // 重新插入该列表该值 - } - } - } - - if (is_oldtable_malloced)                       // 如果两个表相同则删除旧table - PyMem_DEL(oldtable); - return 0; // 返回0 -} - -``` - -主要是检查是否table相同并且需要重新resize的值,然后判断是否fill与used相同,如果相同则全部插入,如果不同,则遍历旧table讲不为空并且不为dummy的值插入到新表中; - -`源文件:`[Objects/setobject.c](https://github.com/python/cpython/blob/1bf9cc509326bc42cd8cb1650eb9bf64550d817e/Objects/setobject.c#L267) - -```c -static void -set_insert_clean(setentry *table, size_t mask, PyObject *key, Py_hash_t hash) -{ - setentry *entry; - size_t perturb = hash; - size_t i = (size_t)hash & mask;         // 计算索引 - size_t j; - - while (1) { - entry = &table[i]; // 获取当前entry - if (entry->key == NULL) // 如果为空则跳转值found_null设置key与hash - goto found_null; - if (i + LINEAR_PROBES <= mask) { // 如果没有找到空值则通过该索引偏移9位去查找空余位置 - for (j = 0; j < LINEAR_PROBES; j++) { - entry++; - if (entry->key == NULL) // 如果为空则跳转到found_null - goto found_null; - } - } - perturb >>= PERTURB_SHIFT; // 计算下一个索引值继续寻找 - i = (i * 5 + 1 + perturb) & mask; - } - found_null: - entry->key = key; - entry->hash = hash; -} -``` - -set的resize的操作基本如上所述。 - diff --git a/objects/set_insert_nine.png b/objects/set_insert_nine.png deleted file mode 100644 index d08e521..0000000 Binary files a/objects/set_insert_nine.png and /dev/null differ diff --git a/objects/set_insert_one.png b/objects/set_insert_one.png deleted file mode 100644 index 15ae74d..0000000 Binary files a/objects/set_insert_one.png and /dev/null differ diff --git a/objects/set_insert_two.png b/objects/set_insert_two.png deleted file mode 100644 index 6e90dab..0000000 Binary files a/objects/set_insert_two.png and /dev/null differ diff --git a/objects/simple-implementation.md b/objects/simple-implementation.md deleted file mode 100644 index 2fbab7c..0000000 --- a/objects/simple-implementation.md +++ /dev/null @@ -1 +0,0 @@ -# 实现简版 Python diff --git a/objects/string-object.md b/objects/string-object.md deleted file mode 100644 index cb10692..0000000 --- a/objects/string-object.md +++ /dev/null @@ -1 +0,0 @@ -# Python 字符串 对象 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a224c8a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2635 @@ +{ + "name": "python3-source-code-analysis", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "markdown-it-task-lists": "^2.1.1", + "vitepress": "^1.6.4" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.21.0.tgz", + "integrity": "sha512-kGvHfBa9oQCvZh0YXeguSToBD9GNJ+gzUZQ9KPTg+KSsM36obYcsKPoX0NnlJtPflHXu7RkMaIi44xs9meR6Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.55.0.tgz", + "integrity": "sha512-Zt2GjIm7vsaf7K23tk5JmtcVNc38G9p0C2L2Lrm06miyLE/NL2etHtHInvuLc1DjxTp7Y2nId4X/tzwo372K8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.55.0.tgz", + "integrity": "sha512-7BueMuWYg/KBA2EX9zsQ+3OAleEyrJcB+SV5Al/9pLjMQq5mXB/8M5HaUPqZwN812g5kLzj9j43VThlZgWq0hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.55.0.tgz", + "integrity": "sha512-pJZIyhvUrs+B7c5Lw0iP5yP/NsqJMda7pKRYbfG4KtfGIVSMcAalZhdqL5UX8Z9DOC4KxO9tKV5RDeVjZU0VfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.55.0.tgz", + "integrity": "sha512-RydkKDhx0GWTYuw0ndTXHGM8hD8hgwftKE65FfnJZb5bPc9CevOqv3qNPUQiviAwkqT9hQNH31uDGeV3yZkgfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.55.0.tgz", + "integrity": "sha512-XiS7gdFq/COWiwdWXZ8+RHuewfEo03TkGESk44zU8zTc/Z6R8fm4DNmV52swJKkeB2N9iC7NKpgpM22OOkcgTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.55.0.tgz", + "integrity": "sha512-LBEJ/q+hn1nJ0aYg5IcWgLNCPjWHTahWmpHNx1qUZMho+9CyWM6LaEnhac45UHjQm/j0m374HP685VrpL133lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.55.0.tgz", + "integrity": "sha512-2/9jUXKH4IcdU5qxH6cbDH46ZBe46G7xr+MrcHwgEXZcUfdAvUgLSH53MAWuMgxvw0G5yoqiWMifHc62Os0fiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.55.0.tgz", + "integrity": "sha512-80tKsQgxXWo+jK0v4YGCHqyTEXawhAKYyr3kOdN51ElfRqUFjZNPVhZk6vRiqSqXfvrH85ytacT3cbJR6+qolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.55.0.tgz", + "integrity": "sha512-4UjmAL8ywGW4rCfK6Qmgw3wIjbrO2wl2s4Eq56JTiN40L2t0XTv0HZkYAmr6nfeiXO0he/2crvZRX6SATSepag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.55.0.tgz", + "integrity": "sha512-LMpJPtIkfDsHIx5Ga+baNr22ntYbY+e2wT7MSIc/FjAnu9wnBFhx1H/GfhmP/c5/IvbThDX+3ilxPRjSfCI8aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.55.0.tgz", + "integrity": "sha512-tDymJ7nFOAoUuecma3usK6o94dp8m4HYFDGh4ByYQXWkv14cpmDn+nWdylmcZO0Qvco107vqDo4+Anksnl8w1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.55.0.tgz", + "integrity": "sha512-6IDSB5o5dkDPQ4LdOW0Yuw/qy5MdWlO2xDHgPVZgW4YDjbxvnX5PAiV7/WWZdWyVObScZZnnHpPbiqfYs/zBLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.55.0.tgz", + "integrity": "sha512-Yyyne4l//vDSdg4MhYJkaVne+KEPi833eCj3/T/87ernTwrvP6j9biXXZELsN8sLI/f2ndV/vugDIy2jdJQB6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.86", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.86.tgz", + "integrity": "sha512-t3jck5qPQuK1qy+bRn9eCoDQhIB7XSazKz1Fjp8hcan3XOAsTI5Mq/s3F0ekOKSvMQqkVORYK6ns6o6T9f5EMA==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz", + "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.38", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", + "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", + "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.38", + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", + "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz", + "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz", + "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/shared": "3.5.38" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", + "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.38", + "@vue/runtime-core": "3.5.38", + "@vue/shared": "3.5.38", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz", + "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "vue": "3.5.38" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz", + "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.55.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.55.0.tgz", + "integrity": "sha512-af+rI+tUVeS9KWHPAZQHIHPOIC3StPRR6IwQu2nz1aQoTL6Gs5Ty3KsHCgbXMHOpoh9QqSjq8F3KJ8xmaCZSBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.21.0", + "@algolia/client-abtesting": "5.55.0", + "@algolia/client-analytics": "5.55.0", + "@algolia/client-common": "5.55.0", + "@algolia/client-insights": "5.55.0", + "@algolia/client-personalization": "5.55.0", + "@algolia/client-query-suggestions": "5.55.0", + "@algolia/client-search": "5.55.0", + "@algolia/ingestion": "1.55.0", + "@algolia/monitoring": "1.55.0", + "@algolia/recommend": "5.55.0", + "@algolia/requester-browser-xhr": "5.55.0", + "@algolia/requester-fetch": "5.55.0", + "@algolia/requester-node-http": "5.55.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", + "dev": true, + "license": "ISC" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.38", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz", + "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.38", + "@vue/compiler-sfc": "3.5.38", + "@vue/runtime-dom": "3.5.38", + "@vue/server-renderer": "3.5.38", + "@vue/shared": "3.5.38" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..31ed8a2 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "scripts": { + "serve": "vitepress dev", + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "markdown-it-task-lists": "^2.1.1", + "vitepress": "^1.6.4" + }, + "overrides": { + "vite": "^6.4.3", + "esbuild": "^0.25.0" + } +}