> For the complete documentation index, see [llms.txt](https://gists.lanlance.cn/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gists.lanlance.cn/cs/py.md).

# Python

## 1. 列表和元组的内部实现

list 本质上是一个 over-allocate 的 array。tuple 和 list 相似，本质也是一个 array，但是空间大小固定。不同于一般 array，Python 的 tuple 做了许多优化，来提升在程序中的效率。

举个例子，当 tuple 的大小不超过 20 时（对应 CPython 源码中的 PyTuple\_MAXSAVESIZE=20，Python 3.7～3.14 均如此），Python 就会把它缓存在内部的一个 free list 中，且每种大小的 tuple 最多缓存 2000 个（缓存上限的常量：Python 3.13 及以前叫 `PyTuple_MAXFREELIST`，3.14 起重构 freelist 后更名为 `Py_tuple_MAXFREELIST`）。这样，如果你以后需要再去创建同样大小的 tuple，Python 就可以直接从缓存中载入，提高了程序运行效率。

## 2. Python 中对象的比较和拷贝

* 比较操作符'=='表示比较对象间的值是否相等，而'is'表示比较对象的标识是否相等，即它们是否指向同一个内存地址。
* 比较操作符'is'效率优于'=='，因为'is'操作符无法被重载，执行'is'操作只是简单的获取对象的 ID，并进行比较；而'=='操作符则会递归地遍历对象的所有值，并逐一比较（对 list、dict 等容器类型而言；自定义类型则取决于其 **eq** 的实现，默认回退为身份比较）。
* 浅拷贝中的元素，是原对象中子对象的引用，因此，如果原对象中的元素是可变的，改变其也会影响拷贝后的对象，存在一定的副作用。
* 深度拷贝则会递归地拷贝原对象中的每一个子对象，因此拷贝后的对象和原对象互不相关。另外，深度拷贝中会维护一个字典，记录已经拷贝的对象及其 ID，来提高效率并防止无限递归的发生。

## 3. 变量及其赋值的基本原理

Python 中参数的传递既不是值传递，也不是引用传递，而是赋值传递，或者是叫对象的引用传递。

需要注意的是，这里的赋值或对象的引用传递，实质是让变量名绑定到一个具体的对象（CPython 中 `id(obj)` 返回的就是对象的内存地址；变量本身只是名字到对象的一个绑定，存放在命名空间字典或栈帧的局部变量槽里）。

* 如果对象是可变的，当其改变时，所有指向这个对象的变量都会改变。
* 如果对象不可变，简单的赋值只能改变其中一个变量的值，其余变量则不受影响。

清楚了这一点，如果你想通过一个函数来改变某个变量的值，通常有两种方法。一种是直接将可变数据类型（比如列表，字典，集合）当作参数传入，直接在其上修改；第二种则是创建一个新变量，来保存修改后的值，然后将其返回给原变量。在实际工作中，我们更倾向于使用后者，因为其表达清晰明了，不易出错。

## 4. 装饰器的使用场景

所谓的装饰器，其实就是通过装饰器函数，来修改原函数的一些功能，使得原函数不需要修改。

而实际工作中，装饰器通常运用在身份认证、日志记录、输入合理性检查以及缓存等多个领域中。合理使用装饰器，往往能极大地提高程序的可读性以及运行效率。

## 5. 迭代器和生成器

* 容器是可迭代对象，可迭代对象调用 iter() 函数，可以得到一个迭代器。迭代器可以通过 next() 函数来得到下一个元素，从而支持遍历。
* 生成器是一种特殊的迭代器（注意这个逻辑关系反之不成立）。使用生成器，你可以写出来更加清晰的代码；合理使用生成器，可以降低内存占用、优化程序结构、提高程序速度。
* 生成器在 Python 2 的版本上，是协程的一种重要实现方式；而 Python 3.5 引入 async await 语法糖后，生成器实现协程的方式就已经落后了（基于生成器的协程自 Python 3.8 起被弃用，`@asyncio.coroutine` 装饰器已在 Python 3.11 中被移除）。

## 6. Python 的协程

* 协程和多线程的区别，主要在于两点，一是协程为单线程；二是协程由用户决定，在哪些地方交出控制权，切换到下一个任务。
* 协程的写法更加简洁清晰，把 async / await 语法和 create\_task 结合来用，对于中小级别的并发需求已经毫无压力。
* 写协程程序的时候，你的脑海中要有清晰的事件循环概念，知道程序在什么时候需要暂停、等待 I/O，什么时候需要一并执行到底。

## 7. Asyncio 解析

不同于多线程，Asyncio 是单线程的，但其内部 event loop 的机制，可以让它并发地运行多个不同的任务，并且比多线程享有更大的自主控制权。

Asyncio 中的任务，在运行过程中不会被打断（单线程事件循环、协作式调度），因此不会出现线程间抢占式切换导致的 data race。但注意，这不代表完全没有 race condition：协程在 await 处会让出控制权，多个任务在 await 点交错执行时仍可能产生逻辑上的竞态（如先检查后修改共享状态），共享状态仍需加锁或使用 asyncio.Lock 等同步原语（官方文档亦如此建议）。尤其是在 I/O 操作 heavy 的场景下，Asyncio 比多线程的运行效率更高。因为 Asyncio 内部任务切换的损耗，远比线程切换的损耗要小；并且 Asyncio 可以开启的任务数量，也比多线程中的线程数量多得多。

## 8. CPython 引入 GIL 的原因

* 一是设计者为了规避类似于内存管理这样的复杂的竞争风险问题（race condition）；
* 二是因为 CPython 大量使用 C 语言库，但大部分 C 语言库都不是原生线程安全的（线程安全会降低性能和增加复杂度）。
* 版本说明：以上是 GIL 存在的历史原因。Python 3.13 起（PEP 703）支持 free-threaded 构建，可在编译时通过 `--disable-gil` 禁用 GIL，使多线程真正并行利用多核（运行时也可用 `PYTHON_GIL=0` 或 `-X gil=0` 强制关闭）；该特性自 Python 3.14 起已不再是实验状态——PEP 779 被指导委员会接受，free-threaded 构建进入「官方支持但仍然可选」的 Phase II，单线程性能损失约 5%\~10%——默认构建（GIL 版）依然启用 GIL，也不会自动替换。

## 9. Python GC

* 垃圾回收是 Python 自带的机制，用于自动释放不会再用到的内存空间；
* 引用计数是其中最简单的实现，不过切记，仅有引用计数并不足够（它不是充分条件），因为循环引用无法仅靠引用计数回收，需要通过不可达判定来确定是否可以回收；
* Python 的自动回收算法包括标记清除和分代收集，主要针对的是循环引用的垃圾收集；

## 10. dict 的实现原理

* dict 底层是哈希表：Python 3.6 起改为「紧凑 dict」（索引数组 + 按插入顺序排列的 entries 数组），3.7 起把「保持插入顺序」写进了语言规范，这就是它看起来「有序」的原因。
* 查找、插入、删除的平均时间复杂度是 O(1)，最坏 O(n)（所有键哈希值都相同时）；键必须可哈希（即支持 `__hash__` 和 `__eq__`），所以 list / dict / set 不能当键。
* 装载率（已用条目数 / 哈希表长度）超过 2/3 时触发扩容，扩容会重新哈希全部键，是 O(n) 的操作（CPython 源码中 `USABLE_FRACTION` 即 2/3，`GROWTH_RATE` 为已用条目数 × 3）。

## 11. 小整数池和字符串驻留

* CPython 预创建了 -5\~256 的整数对象（源码中 `_PY_NSMALLNEGINTS=5`、`_PY_NSMALLPOSINTS=257`），这个范围内复用同一批对象，因此 `a = 256; b = 256; a is b` 为 True；超出范围就可能为 False（同一代码块内的常量还会被合并，进一步干扰判断）。
* ASCII 且形如标识符（只含字母、数字、下划线）的字符串字面量会被驻留（intern），同一运行内复用同一对象，也可以用 `sys.intern()` 手动驻留。
* 这些都是 CPython 的实现细节而非语言保证：判断值相等一律用 `==`，`is` 只用来和 None 这类单例对象比较（呼应第 2 题）。

## 12. 可变默认参数的陷阱

* `def f(x, lst=[])` 中的默认值只在函数定义时求值一次，保存在函数对象的 `__defaults__` 里，之后每次调用共享同一个列表，于是上一次调用的数据会累积到下一次。
* 正确写法是把默认值设为 None、在函数体内再创建：`def f(x, lst=None): lst = [] if lst is None else lst`。
* 同一机制还有另一个坑：默认值在定义时就求值完毕，所以不要把「期望每次调用重新计算」的表达式（如 `time.time()`、读环境变量）写进默认值。

## 13. 多线程、多进程与协程的选型

* 默认构建受 GIL 限制，多线程跑 CPU 密集的字节码只能串行；要真正吃满多核，用多进程（`multiprocessing`、`concurrent.futures.ProcessPoolExecutor`）。
* I/O 密集型（网络、磁盘）用多线程或 asyncio 都可以：等待 I/O 时都会交出控制权。区别在于线程栈是 MB 级、协程只有 KB 级，成千上万并发连接时 asyncio 的内存优势明显；但协程跑在单个事件循环上，一段 CPU 密集代码就会阻塞整个循环，得丢给线程池 / 进程池。
* 已经在 C 层释放 GIL 的库（numpy、hashlib、部分 C 扩展）照样能并行，这类计算放线程池同样有效。
* 想彻底绕开 GIL：用 Python 3.13+ 的 free-threaded 构建（见第 8 题），或把热点用 C / Rust 扩展（Cython、numpy）实现。

## 14. MRO 与 super() 的原理

* 多继承下方法查找顺序由 C3 线性化算法计算，可通过 `Cls.__mro__`（或 `Cls.mro()`）查看；它保证子类排在父类之前、且不破坏各基类的相对顺序。
* `super()` 并不是「调用父类」，而是「在实例的 MRO 上，从当前类之后继续往下找」：菱形继承（A → B、C → D）中若每一层都用 `super().__init__()`，A 只会被初始化一次，`D.__mro__` 为 `[D, B, C, A, object]`。
* 因此在协作式多继承中应当层层都用 `super()`；直接写 `Base.__init__(self)` 会跳过 MRO 上的其他类，并可能造成重复初始化。

## 15. 闭包与延迟绑定

* 闭包是内层函数引用了外层函数的变量，外层函数返回后这些变量依然存活（存在 cell 对象中，可通过 `f.__closure__` 查看）。
* 内层函数是调用时按名字取值（late binding），所以循环中创建的一批闭包共享同一个变量、最后都取到循环结束时的值；要固定当时的值，用默认参数 `lambda i=i: i`、`functools.partial`，或再套一层函数。
* 内层函数对外层变量重新赋值必须声明 `nonlocal`（模块级变量用 `global`），否则会被当成内层的新局部变量。
