为什么Python 2和Python 3中的Unicode字符串占用的内存大小不同?

4
在Python 2中,一个空字符串占用确切的37个字节。
>>>> print sys.getsizeof('')
37

在Python 3.6中,相同的调用将输出49个字节。
>>>> print(sys.getsizeof(''))
49

我原以为这是因为在Python 3中,所有字符串都是Unicode编码。但是,令我惊讶的是,下面是一些令人困惑的输出:

Python 2.7

>>>> print sys.getsizeof(u'')
52
>>>> print sys.getsizeof(u'1')
56

Python 3.6

>>>>print(sys.getsizeof(''))
49
>>>>print(sys.getsizeof('1'))
50
  1. 空字符串大小不一样。
  2. 在Python 2中添加一个字符需要额外4个字节,在Python 3中只需要一个字节。

为什么这两个版本的内存占用不同呢?

编辑

由于不同的Python 3版本之间存在差异,因此我指定了我的Python环境的确切版本。


https://dev59.com/U18e5IYBdhLWcg3wHXaJ - Josh Lee
2个回答

4
当然,有原因,但对于任何实际目的来说,这都不应该有影响。如果您有一个Python系统,需要在内存中保留如此多的字符串以接近系统内存,则应通过(1)尝试惰性加载/创建内存中的字符串或(2)使用面向字节的高效二进制结构来处理数据,例如Numpy提供的那些,或Python自己的bytearray
空字符串文字(来自Py2的unicode文字)的更改可能是由于您正在查看的版本之间的任何实现细节,即使编写与Python字符串直接交互的C代码也不应该有影响:即使这些代码也只能通过API触及字符串。
现在,Python 3中的字符串仅增加1个字节的大小,而Python 2中的字符串增加4个字节的具体原因是PEP 393
在Python 3.3之前,Python中的任何(unicode)字符串都会为每个字符使用固定的2个字节或4个字节内存 - Python解释器和使用本地代码的Python模块必须编译为只使用其中一种。即使版本匹配,您也可能会有不兼容的Python二进制文件,这是由于在构建时选择了字符串宽度选项 - 构建被称为“窄构建”和“宽构建”。通过上述PEP 391,Python字符串在实例化时确定其字符大小,取决于它包含的最宽的unicode代码点的大小。包含在前256个代码点中的点(等同于Latin-1字符集)的字符串仅使用每个字符1个字节。

我同意你的观点,坦白地说,当我发现这个问题时,我正在处理字符串。我的问题没有特定的用例,只是出于纯粹的好奇心。但是,如果他们实际上付出了努力来实现一个使用1字节而不是固定2或4字节编码系统,我认为应该有一个原因。我的意思是,这意味着它在某种程度上是相关的,我错了吗? - scharette
@scharette 这是一种优化,可以使许多程序受益,即使程序员不必知道它的存在。就像您不必了解 PyPy 的 JIT 如何工作才能使代码运行更快一样,您也不必了解 CPython 3.7 的 Unicode 存储方式如何工作才能使代码占用更少的内存。这些信息已经记录在文档中,供那些确实需要了解的人使用,但大多数人从未需要过。 (事实上,我怀疑大多数人只是出于好奇或因为在 CPython 上进行黑客攻击而了解它,而不是因为他们需要为 Python 程序学习它。) - abarnert
确实,即使使用C代码,您也应该仅通过API来处理字符串。但不幸的是,3.3不得不更改C API,弃用大多数旧函数,以使优化工作高效顺畅,并且这些更改会引用PEP 393,因此它并不像理想情况下那样隐形。 - abarnert
@abarnert 没错,我只是认为这些问题应该在SO上有它们的位置,因为它们有时可以带来对复杂概念的更好理解。 - scharette
@scharette 当然,这就是我写下这种可怕模块的原因(https://github.com/abarnert/superhackyinternals)并回答相关问题。我当然_希望_没有人会真正用它来做除了更好地理解CPython之外的任何事情。 - abarnert
好的,抱歉如果我的回答让人觉得不应该玩弄和询问问题 - 当然,摆弄和理解是非常重要的。我可以告诉你,当我起草答案时,我担心你的疑虑是因为你试图为生产代码解决这个差异。 - jsbueno

3

Python 3内部现在使用四种不同的编码方式来存储字符串,并为每个字符串选择不同的编码方式。这些编码方式是ASCII、LATIN-1、UCS-2和UTF-32。每种编码方式都能表示不同的Unicode字符子集,并且具有一个有用的属性,即索引i处的元素也是索引i处的Unicode代码点。

In [1]: import sys

In [2]: sys.getsizeof('\xFF')
Out[2]: 74

In [3]: sys.getsizeof('X\xFF')
Out[3]: 75

In [4]: sys.getsizeof('\u0100')
Out[4]: 76

In [5]: sys.getsizeof('X\u0100')
Out[5]: 78

In [6]: sys.getsizeof('\U00010000')
Out[6]: 80

In [7]: sys.getsizeof('X\U00010000')
Out[7]: 84

你可以看到,在字符串中添加一个额外的字符,比如这里的'X',会导致该字符串占用更多的空间,具体取决于其余部分所包含的值。
这个系统是在Python 3.3中实现的,提出于PEP-0393。早期版本的Python使用较旧的unicode表示形式,它总是使用2或4个字节来表示每个元素(我不敢说“字符”),这取决于编译时选项,并且不能混合使用。

值得一提的是,相比之下,Python 2(以及早期的Python 3)将“unicode”字符串存储为UTF-32。(或者,如果您使用“窄版”,它会将它们存储为UTF-16,并且像代理字符是单独的字符一样处理。) - abarnert
@abarnert:Python 3 的早期版本也会这样做。 - Dietrich Epp
是的,这就是为什么我说“(还有早期的Python 3)”。 - abarnert
1
我的天啊!不过那个信息已经包含在答案中了。 - Dietrich Epp
顺便说一句,核心开发人员也犹豫不决是否使用“字符”这个词。大约在 Python 2.1 左右引入 Unicode 2.0 支持并引入窄版构建时,他们从 C API 文档中删除了“字符”一词,并确保明确将它们称为“Py_UNICODE 值”。(但是,奇怪的是,他们仍然在 3.2 版本之前一直使用 UCS2 而不是 UTF-16。) - abarnert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接