Python 是怎么存储字符串的?

文摘   2024-07-10 08:30   中国台湾  

楔子



上一篇文章我们介绍了字符集,它是一系列字符组成的集合,但不同的字符集所能容纳的字符是有限的。于是为了能将全世界的字符统一起来,便诞生了 unicode。

unicode 字符集对世界上出现的所有字符都进行了系统的整理,包括各种 emoji,不管是哪个国家的语言,都可以使用 unicode 字符集。

print(ord("a"))  # 97
print(ord("憨"))  # 25000
print(ord("て"))  # 12390

不管什么文字,都可以用一个 unicode 来表示,它们在字符集中对应一个唯一的码点。所谓码点,就是字符在字符集中的索引,或者说唯一编号。

但是问题来了,unicode 能表示这么多的字符,占用的内存一定不低吧。的确,根据当时的编码,一个 unicode 字符最高会占用到 4 字节,因此对西方人来说就有点苦不堪言了,明明一个字符就够用了,为啥需要那么多。

于是又出现了 utf-8,它是为 unicode 提供的一个新的编码规则,具有可变长的功能。不同种类的字符占用的大小不同,比如英文字符使用一个字节存储,汉字使用 3 个字节存储,Emoji 使用 4 个字节存储。

但 Python 在表示 unicode 字符串时,使用的却不是 utf-8 编码,至于原因我们下面来分析一下。



unicode 的三种编码



从 Python3 开始,字符串使用的是 unicode。而根据编码的不同,unicode 的每个字符最大可以占到 4 字节,从内存的角度来说,这种编码有时会比较昂贵。

为了减少内存消耗并且提高性能,Python 的内部使用了三种编码方式来表示 unicode。

  • Latin-1 编码:每个字符占 1 字节;

  • UCS2 编码:每个字符占 2 字节;

  • UCS4 编码:每个字符占 4 字节;


在 Python 编程中,所有字符串的行为都是一致的,而且大多数时候我们都没有注意到差异。然而在处理大文本的时候,这种差异就会变得异常显著,甚至有些让人出乎意料。

为了看到内部表示的差异,我们看一下字符串所占的内存大小。

>>> sys.getsizeof("a")
42
>>> sys.getsizeof("憨")
60
>>> sys.getsizeof("😂")
64

我们看到都是一个字符,但它们占用的内存却是不一样的。因为 Python 面对不同的字符会采用不同的编码,进而导致大小不同。


但需要注意的是,Python 的每一个字符串都需要额外占用至少 41 个字节,因为要存储一些元数据,比如:公共的头部、哈希、长度、字节长度、编码类型等等。

import sys

# 对于 ASCII 字符,一个占 1 字节,显然此时编码是 Latin-1 编码
print(sys.getsizeof("ab") - sys.getsizeof("a"))  # 1

# 对于汉字,日文等等,一个占用 2 字节,此时是 UCS2 编码
print(sys.getsizeof("憨憨") - sys.getsizeof("憨"))  # 2
print(sys.getsizeof("です") - sys.getsizeof("で"))  # 2

# 像 Emoji,则是一个占 4 字节 ,此时是 UCS4 编码
print(sys.getsizeof("😂😂") - sys.getsizeof("😂"))  # 4

而采用不同的编码,那么底层结构体实例的元数据也会占用不同大小的内存。

# 所以一个空字符串占用 41 个字节
# 此时会采用占用内存最小的 Latin-1 编码
print(sys.getsizeof(""))  # 41
# 此时使用 UCS2
print(sys.getsizeof("憨") - 2)  # 58
# UCS4
print(sys.getsizeof("🍌") - 4)  # 60

如果编码是 Latin-1,那么这个结构体实例的元数据会占 41 个字节;编码是 UCS2,占 58 个字节;编码是 UCS4,占 60 个字节。然后字符串所占的字节数就等于:元数据 + 字符个数 * 单个字符所占的字节



为什么不使用 utf-8 编码



上面提到的三种编码,是 Python 在底层所使用的,但我们知道 unicode 还有一个 utf-8 编码,那 Python 为啥不用呢?

古明地觉的编程教室
Python、Rust 程序猿,你感兴趣的内容我都会写,点个关注吧(#^.^#)
 最新文章