关于 Python 字符串默认编码下的字节数问题

2020-02-03 00:38:33 +08:00
 peterliu502

python 中字符串默认的是 Unicode 编码,数字字符、英文字符和中文字符在 Unicode 编码中不是都是 2 字节么?
为什么我用 getsizeof()函数来看字符串的内存大小,发现 1 个汉字字符是 2 字节没错,但 1 个数字字符却只有 1 字节?
我用 utf-8 编码检验了一下,没发现问题。1 个数字字符 1 字节,一个汉字 3 字节。

print('Unicode_123 =', sys.getsizeof('123'))  
print('Unicode_12 =', sys.getsizeof('12'))
# Unicode_123-Unicode_12 可得字符串默认编码下 1 个数字字符所占内存
print('Unicode_一二三 =', sys.getsizeof('一二三'))  
print('Unicode_一二 =', sys.getsizeof('一二'))
# Unicode_一二三-Unicode_一二可得字符串默认编码下 1 个汉字字符所占内存
print('Utf-8_123 =', sys.getsizeof('123'.encode('utf-8')))  
print('Utf-8_12 =', sys.getsizeof('12'.encode('utf-8')))
# Utf-8_123-Utf-8_12 可得 utf-8 编码下 1 个数字字符所占内存
print('Utf-8_一二三 =', sys.getsizeof('一二三'.encode('utf-8')))  
print('Utf-8_一二 =', sys.getsizeof('一二'.encode('utf-8')))
# Utf-8_一二三-Utf-8_一二可得 utf-8 编码下 1 个汉字字符所占内存

显示结果如下:
Unicode_123 = 52
Unicode_12 = 51
Unicode_一二三 = 80
Unicode_一二 = 78
Utf-8_123 = 36
Utf-8_12 = 35
Utf-8_一二三 = 42
Utf-8_一二 = 39

但如果字符串是数字或英文字母混合汉字时,新增 1 个数字字符是增加 2 字节

print('Unicode_一 =', sys.getsizeof('一'))
print('Unicode_1 一 =', sys.getsizeof('1 一'))
print('Unicode_a 一 =', sys.getsizeof('a 一'))
print('Unicode_一二 =', sys.getsizeof('一二'))

显示结果如下: Unicode_一 = 76
Unicode_1 一 = 78
Unicode_a 一 = 78
Unicode_一二 = 78
这种情况是为什么呢?

2511 次点击
所在节点    Python
8 条回复
justou
2020-02-03 22:20:58 +08:00
'1 一' 'a 一' 是不是多插入了一个空格?
peterliu502
2020-02-04 12:03:38 +08:00
@justou 好像还真是……
peterliu502
2020-02-04 12:04:24 +08:00
@justou 但还是没明白为何 ASCII 字符是 1 字节
justou
2020-02-04 14:46:31 +08:00
Unicode 字符串即 py3 的 str 底层是一个 Py_UNICODE 的数组,其实就是一个 wchar_t 数组( wchar_t 大小平台相关,或 16bit 或 32bit ),str 就是一个对底层 wchar_t 数组的封装。

按照你的例子,str 增加一个 ascii 字符,大小增加 2 字节的话,那么底层的 wchar_t 大小应该是 16bit ( 2 字节)

https://docs.python.org/3/c-api/unicode.html#unicode-type
peterliu502
2020-02-05 12:39:51 +08:00
@justou str 增加一个 ascii 字符是 1 字节,非 ascii 字符才是 2 字节
chenstack
2020-02-05 15:05:35 +08:00
Python3 字符串 encode 默认编码是 utf-8,中文一般占 3 字节,ascii 字符是 1 字节
下面例子是计算 bytes 的大小
>>> print('Utf-8_一二三 =', sys.getsizeof('一二三'.encode('utf-8')))
>>> print('Utf-8_一二 =', sys.getsizeof('一二'.encode('utf-8')))
Utf-8_一二三 = 42
Utf-8_一二 = 39
相差 3


下面例子是计算 str 的大小,每个字符都是 2 字节,也就是 @justou #4 说的
>>> print('Unicode_a 一 =', sys.getsizeof('a 一'))
>>> print('Unicode_一二 =', sys.getsizeof('一二'))
Unicode_a 一 = 78
Unicode_一二 = 78
justou
2020-02-05 15:35:28 +08:00
我上面的回复不全面,可以用以下函数来探索下:

from sys import getsizeof

def probe_size_increment(init_str="", code_points=range(2, 300)):
for i in code_points:
s1 = init_str + ''.join(chr(n) for n in range(i))
s2 = init_str + ''.join(chr(n) for n in range(i+1))
print(f"{i}~{i+1} {getsizeof(s2) - getsizeof(s1)}")

1. probe_size_increment(init_str=""); 可以用一个字节来表示的码点最大为 255,可以看到,在这之前都是 1 的增长,除了 255~256 分配了一定空间,后面都是 2 的增长。

2. probe_size_increment(init_str="", code_points=range(2**16 - 10, 2**16 - 1));都是 2 的增长。

3. probe_size_increment(init_str="", code_points=range(2**16 + 10, 2**16 + 20)); 都是 4 的增长。

首先意识到 str 下面管理的是一个单一类型的 c 数组,可以有以下推论:当所有字符都可以用 1 字节表示时,这个 c 数组是 Py_UCS1[], 所以在 255 之前增长都是 1 ;同理,2 字节增长的是 Py_UCS2[],4 字节增长的是 Py_UCS4[]

找了下源码,应该是这个位置 https://github.com/python/cpython/blob/3.8/Objects/unicodeobject.c#L2322 跟我们的推论一致

Python 的一些实现用了很多优化手段,本身就是 C 语言搭建起来的一个框架。想探究其原理建议直接看 C 代码吧。之前有人发过解读 Python 源码的帖子,你找找。
peterliu502
2020-02-05 18:47:59 +08:00
@justou 谢谢,你这个答案我觉得算是解答了我的这个疑问

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/641735

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX