Python 字符串是不可变的,那么为什么 s.split( ) return 是一个新字符串列表

Python Strings are immutable so why does s.split( ) return a list of new strings

通过查看 CPython 实现,字符串 split() 的 return 值似乎是新分配的字符串列表。但是,由于字符串是不可变的,因此似乎可以通过指向偏移量从原始字符串中生成子字符串。

我是否正确理解了 CPython 的当前行为?是否有理由不选择此 space 优化?我能想到的一个原因是父字符串在其所有子字符串都被释放之前无法被释放。

没有 crystal 球,我无法告诉你为什么 CPython 会那样做。但是,出于某些原因您可能会选择这样做。

问题是一个小字符串可能包含对更大后备数组的引用。例如,假设我读入一个 8 GB 的 HTTP 访问日志文件来分析哪些用户代理访问我的文件最多,我只是通过 fp.read() 然后 运行 对整个文件的正则表达式来做到这一点一次而不是一次一行。

我想知道前 10 个最常见的用户代理,所以我把它放在一个列表中。

然后我想对其他 100 个文件进行相同的分析,以查看前 10 个用户代理随时间的变化情况。繁荣!我的程序试图使用 800 GB 内存并被终止。为什么?我该如何调试?

Java 在 Java 7 之前使用了这种共享技术,所以同样的推理也适用。参见 Java 7 String - substring complexityJDK-4513622:(str) 保留字段的子字符串可防止 object.

的 GC

另请注意,让字符串共享内存需要您遵循从字符串 object 到字符串数据的指针。在 CPython 中,字符串数据通常直接放在内存中的 header 之后,因此不需要跟随指针。这减少了所需的分配次数并减少了读取字符串时的数据依赖性。

在当前的 CPython 实现中,字符串是引用计数的;假定字符串不能包含对其他对象的引用,因为字符串不是容器。这意味着垃圾回收不需要检查或跟踪字符串对象(因为它们完全被引用计数覆盖)。但实际上比这更糟:Python 的旧版本根本没有跟踪垃圾收集器; GC 为 new in 2.0。在此之前,任何循环垃圾都会简单地泄漏。

有效实现的子串到偏移算法不应形成循环。所以理论上,循环垃圾收集器不是这个的先决条件。但是,因为我们进行的是引用计数而不是跟踪,所以子对象会在生命周期结束时负责 Py_DECREF()ing 它们的父对象。否则父泄漏。这意味着您不能在整个字符串达到生命周期结束时将其扔进空闲列表;你必须检查它是否是一个子字符串,并且 branching is potentially expensive。 Python 在历史上被设计为进行字符串处理(类似于 Perl,但语法更好),这意味着创建和销毁大量字符串。此外,所有变量名称在内部都存储为字符串,因此即使用户不进行字符串处理,解释器也会进行。将字符串重新分配过程放慢一点都可能对性能产生严重影响。

CPython 除了存储长度外,还在内部使用以 NUL 结尾的字符串。这是一个非常早期的设计选择,自 Python 的第一个版本以来就存在,并且在最新版本中仍然如此。

您可以在 Include/unicodeobject.h 中看到 PyASCIIObject 表示 "wchar_t representation (null-terminated)" 和 PyCompactUnicodeObject 表示 "UTF-8 representation (null-terminated)"。 (最近的 CPython 实现 select 来自 4 种后端字符串类型之一,具体取决于 Unicode 编码需要。)

许多 Python 扩展模块需要一个以 NUL 结尾的字符串。很难将子字符串实现为更大字符串的切片并保留低级 C API。并非不可能,因为可以使用 copy-on-C-API-access 来完成。或者 Python 可以要求所有扩展作者使用新的子切片友好 API。但是考虑到从其他实现子切片引用的语言的经验中发现的问题,这种复杂性是不值得的,正如 Dietrich Epp 所描述的。

我在 Kevin 的回答中几乎看不到适用于这个问题的内容。该决定与 Python 2.0 之前缺乏循环垃圾收集无关,也不可能。子串切片是用非循环数据结构实现的。 'Competently-implemented' 不是相关要求,因为将其转变为循环数据结构需要一种反常的无能或恶意。

解除分配器中也不一定会有额外的分支开销。如果源字符串是一种类型而子字符串切片是另一种类型,那么 Python 的普通类型调度程序将自动使用正确的释放器,而不会产生额外的开销。即使有一个额外的分支,我们知道这种情况下的分支开销不是 "expensive"。 Python 3.3(因为 PEP 393)有这 4 种后端 Unicode 类型,并根据分支决定要做什么。字符串访问比解除分配更频繁地发生,因此由于分支导致的任何分配开销都会在噪音中丢失。

在 CPython "variable names are internally stored as strings" 中大部分是正确的。 (例外情况是局部变量作为索引存储到局部数组中。)但是,这些名称也使用 PyUnicode_InternInPlace() 驻留在全局字典中。因此没有释放开销,因为这些字符串没有被释放,在涉及使用非驻留字符串的动态调度的情况之外,比如通过 getattr()。