Python 与内存管理,第1部分:对象

发布: (2026年2月8日 GMT+8 05:06)
9 分钟阅读
原文: Dev.to

Source: Dev.to

值相等 – ==

== 检查两个对象是否包含相同的数据。当你写 a == b 时,Python 会比较对象中 存储的值。这可能涉及调用特殊方法,如 __eq__

# == compares values
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)          # True – same content

在内部,== 可能触发更复杂的操作:

  • 对于列表,它会逐个比较元素。
  • 对于自定义对象,它会调用对象的 __eq__ 方法。
class SensorValue:
    def __init__(self, value, tolerance):
        self.value = int(value)
        self.tolerance = int(tolerance)

    def __eq__(self, other):
        # Equality within tolerance
        return abs(self.value - other.value)

注意 (Python 3.14+) – 你可以通过 sys._is_immortal(obj)(私有 API)查询对象是否是永生的。

7. 回顾

运算符 / 函数检查内容常见使用场景
==值相等性(__eq__比较数据
is身份(相同内存地址)单例检查(is None
id()对象的“地址”(整数)调试 / 反射

理解这些区别可以解释为什么:

  • 小整数会被缓存,看起来是同一个对象。
  • 大整数只有在显式绑定到多个名称时才会共享。
  • 引用计数驱动内存回收,但不朽对象会绕过它。

掌握这些知识后,你可以编写更可预测、对内存更敏感的 Python 代码——并避免因混淆相等性和身份而产生的细微错误。

字符串驻留

Python 对某些字符串进行特殊优化:它 缓存 这些字符串,并将缓存的对象存储为 驻留 字符串。驻留字符串表现为“永生”对象——在解释器运行期间它们永不被释放。请参阅官方文档中的 sys.intern 描述。

注意 – Python 隐式地驻留许多字符串(例如标识符、短字面量)。因此,在比较看似相等但实际不是同一对象的字符串时,is 运算符可能会产生令人惊讶的结果。

小整数内存布局

id() 返回对象的内存地址。当我们查看连续小整数的地址时,会看到一个规律的模式:

for i in range(5):
    print(f"id({i+1}) - id({i}) = {id(i+1) - id(i)}")

差值是 32 字节。这就是 Python 整数对象的大小吗?

import sys
print(f"{sys.getsizeof(1)=}")      # sys.getsizeof(1)=28
  • sys.getsizeof(1) 报告 28 字节 —— 对象数据的实际大小。
  • 当打印 id 差值时看到的 32 字节间隔 来自 CPython 在 连续数组 中分配小整数的方式;每个槽位占用 32 字节,尽管对象本身只使用了 28 字节。

那 32 字节里都有什么?

偏移大小含义
0 – 78 B引用计数 (ob_refcnt)。对于“永生”的小整数,这里存放魔法值 0xC0000000
8 – 158 B指向类型对象的指针 (ob_type)。
16 – 238 B大小字段 —— 组成整数的 digit(位)数量(Python 整数具有任意精度)。
24 – 274 B第一个 digit (ob_digit[0])。更大的数字会分配额外的 digit。
28 – 314 B为了 8 字节对齐而填充的空间。

CPython 对象布局(C 级别)

每个 Python 对象都以 PyObject 头部 开始:

typedef struct _object {
    Py_ssize_t ob_refcnt;   // reference count (or immortal flag)
    PyTypeObject *ob_type; // pointer to type object
} PyObject;

整数(long)对象定义

在 CPython 源码(cpython/Include/cpython/longintrepr.h)中:

typedef struct _PyLongValue {
    uintptr_t lv_tag;          // number of digits, sign, flags
    digit ob_digit[1];         // array of 30‑bit “digits” (uint32)
} _PyLongValue;

struct _longobject {           // the public integer object
    PyObject_HEAD               // expands to the PyObject header above
    _PyLongValue long_value;   // the actual numeric data
};

该头部被内存管理器用于对象检索和垃圾回收。

引用计数

CPython 内存管理的核心是 引用计数。每个对象都保存一个计数器,用来记录有多少引用指向它。当计数器降至零时,内存会被释放。

你可以使用 sys.getrefcount() 查看引用计数(该函数会为你传入的参数额外添加 一个 引用)。

import sys

x = [1, 2, 3]
print(f"Initial refcount: {sys.getrefcount(x)=}")   # 2 (x + argument)

y = x
print(f"Assigning y <- x: {sys.getrefcount(x)=}")   # 3

z = x
print(f"Assigning z <- x: {sys.getrefcount(x)=}")   # 4

del y
print(f"De‑assigning y: {sys.getrefcount(x)=}")     # 3

z = None
print(f"De‑assigning z: {sys.getrefcount(x)=}")     # 2

del x
print("Deleted x")

当引用计数降至 0 时,CPython 会立即释放该对象。

弱引用与循环引用

弱引用 不会 增加引用计数,即使仍然存在指向它们的弱引用,对象也可以被回收。

import sys
import weakref

class Node:
    def __init__(self, child):
        self.child = child

a = Node(None)          # refcnt = 1
b = Node(None)

a_ref = weakref.ref(a)   # weak reference, no refcnt increase
a.child = b
b.child = a              # creates a cycle, refcnt = 2 for each

print(f"All refs a, b, and a_ref assigned: {sys.getrefcount(a_ref())=}")  # 3 (2 + argument)

del a
print(f"After variable A deleted: {sys.getrefcount(a_ref())=}")            # 3 (still alive because of the cycle)

del b
print(f"After variable B deleted: {sys.getrefcount(a_ref())=}")            # 2 (object freed, but weakref still returns None)

为什么仅靠引用计数无法打破循环

当对象相互引用(形成循环)时,它们的引用计数永远不会降到零,导致内存泄漏。为此,CPython 添加了一个分代垃圾收集器,能够检测并回收此类循环。

代际垃圾收集器

收集器基于 代际假设

  • 大多数对象寿命短(约 80‑90 %)。
  • 老对象很少引用年轻对象

CPython 维护 三个代

运行时机描述
0gc.get_threshold()[0] 次分配(默认 700)新对象。
1gc.get_threshold()[1] 次第 0 代的收集(默认 10)在一次第 0 代收集后仍存活的对象。
2gc.get_threshold()[2] 次第 1 代的收集(默认 10)长期存活的对象。
import gc
print(f"GC generations thresholds: {gc.get_threshold()}")
# (700, 10, 10) by default

由于第 0 代的收集只有在达到一定的分配次数后才会触发,刚创建的对象(或弱引用对象)即使所有强引用已经消失,也可能在短时间内仍然存活。这解释了上面示例中,删除 ab 后,弱引用 a_ref() 仍然可以访问——循环垃圾尚未被收集。

TL;DR

  • 字符串驻留 缓存特定字符串,使它们实际上是永久的。
  • 小整数 存放在预分配的连续数组中;每个槽占用 32 字节,虽然对象本身只使用 28 字节。
  • 每个 CPython 对象都以 PyObject 头部开始(ob_refcnt + ob_type)。
  • 引用计数 在计数降至零时自动释放对象。
  • 弱引用 允许持有指向对象的非拥有指针。
  • 循环引用代际垃圾收集器 处理,收集器会根据分配阈值定期运行。
0 浏览
Back to Blog

相关文章

阅读更多 »

Python–TypeScript 合约

《The Python–TypeScript Contract》封面图片 https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to...

问题 13:分组字母异位词

概述:任务是编写一个函数,将给定字符串列表中相互是变位词的单词进行分组。变位词是由…构成的单词或短语。