快捷搜索:  MTU2MjE4NzQ3MA`

Python 存储字符串时是如何节省空间的?

image

从 Python 3 开始,str 类型代表着 Unicode 字符串。取决于编码的类型,一个 Unicode 字符可能会占 4 个字节,这个有些时刻有点挥霍内存。

出于内存占用以及机能方面的斟酌,Python 内部采纳下面 3 种要领来存储 Unicode 字符:

一个字符占一个字节(Latin-1 编码)

一个字符占二个字节(UCS-2 编码)

一个字符占四个字节(UCS-4 编码)

应用 Python 进行开拓的时刻,我们会感觉字符串的处置惩罚都很类似,很多时刻根本不必要留意这些区别。可是,当碰着大年夜量的字符处置惩罚的时刻,这些细节就要分外留意了。

我们可以做一些小实验来体会下上面三种要领的区别。措施 sys.getsizeof 用来获取一个工具所占用的字节,这里我们会用到。

>>> import sys

>>> string = 'hello'

>>> sys.getsizeof(string)

54

>>> # 1-byte encoding

... sys.getsizeof(string + '!') - sys.getsizeof(string)

1

>>> # 2-byte encoding

... string2 = '你'

>>> sys.getsizeof(string2 + '好') - sys.getsizeof(string2)

2

>>> sys.getsizeof(string2)

76

>>> # 4-byte encoding

... string3 = ''

>>> sys.getsizeof(string3 + '') - sys.getsizeof(string3)

4

>>> sys.getsizeof(string3)

80

如上所示,当字符串的内容不应时,所采纳的编码也会不合。必要留意的是,Python 中每个字符串都邑别的占用 49-80 字节的空间,用于存储额外的一些信息,比如哈希、字符串长度、字符串字节数和字符串标识。这么一来,一个空字符串会占用 49 个字节,也就好理解了。

我们可以经由过程 cbytes 直接获取一个工具的编码类型:

import ctypes

class PyUnicodeObject(ctypes.Structure):

# internal fields of the string object

_fields_ = [("ob_refcnt", ctypes.c_long),

("ob_type", ctypes.c_void_p),

("length", ctypes.c_ssize_t),

("hash", ctypes.c_ssize_t),

("interned", ctypes.c_uint, 2),

("kind", ctypes.c_uint, 3),

("compact", ctypes.c_uint, 1),

("ascii", ctypes.c_uint, 1),

("ready", ctypes.c_uint, 1),

# ...

# ...

]

def get_string_kind(string):

return PyUnicodeObject.from_address(id(string)).kind

然后测试

>>> get_string_kind('Hello')

1

>>> get_string_kind('你好')

2

>>> get_string_kind('')

4

假如一个字符串中的所有字符都能用 ASCII 表示,那么 Python 会应用 Latin-1 编码。简单说下,Latin-1 用于表示前 256 个 Unicode 字符。它能支持很多拉丁说话,比如英语、瑞典语、意大年夜利语等。不过,假如是汉语、日语、西伯尔语等非拉丁说话,Latin-1 编码就行不通了。由于这些说话的翰墨的码位值(编码值)跨越了 1 个字节的范围(0-255)。

>>> ord('a')

97

>>> ord('你')

20320

>>> ord('!')

33

大年夜部分说话翰墨应用 2 个字节(UCS-2)来编码就已经足够了。4 个字节(UCS-4)的编码在保存特殊符号、emoji 神色或者少见的说话翰墨的时刻会用到。

设想有一个 10GB 的 ASCII 文本文件,我们筹备将其读到内存里面去。假如你插入一个 emoji 神色到文件中,文件占用空间将会达到 4 倍。假如你处置惩罚 NLP 问题较多的话,这种区别你应该能常常体会到。

Python 内部为什么不直接应用 UTF-8 编码

最常见的 Unicode 编码是 UTF-8,然则 Python 内部并没有应用它。

UTF-8 编码字符的时刻,取决于字符的内容,占的空间在 1-4 个字节内发生变更。这是一种分外省空间的存储要领,但正由于这种变长的存储要领,导致字符串不能经由过程下标直接进行随机读取,只能遍历进行查找。比如,假如采纳的是 UTF-8 编码的话,Python 获取 string[5] 只能一个一个字符的进行扫描,直至找到目标字符。假如是定长编码的话也就没有问题了,要用一个下标定位一个字符,只必要用下标乘以指定长度(1、2 或者 4)就能确定。

字符串驻留

Python 中的空字符串和 ASCII 字符都邑应用到字符串驻留(string interning)技巧。怎么理解?你就把这些字符(串)看作是单例的就行。也便是说,两个相同内容的字符串假如应用了驻留的技巧,那么内存里面着实就只开辟了一个空间。

>>> a = 'hello'

>>> b = 'world'

>>> a[4],b[1]

('o', 'o')

>>> id(a[4]), id(b[1]), a[4] is b[1]

(4567926352, 4567926352, True)

>>> id('')

4545673904

>>> id('')

4545673904

正如你看到的那样,a 中的字符 o 和 b 中的字符 o 有着同样的内存地址。Python 中的字符串是弗成改动的,以是提前为某些字符分配好位置便于后面应用也是可行的。

应用到字符串驻留的除了 ASCII 字符、空窜之外,字符长度不跨越 20 的串也应用到了同样的技巧,条件是这些串的内容在编译的时刻就能确定。

这包括:

措施名、类型

变量名

参数名

常量(代码中定义的字符串)

字典的键

属性名

当你在交互式敕令行中编写代码的时刻,语句同样也会先被编译成字节码。以是说,交互式敕令行中的短字符串也会被驻留。

>>> a = 'teststring'

>>> b = 'teststring'

>>> id(a), id(b), a is b

(4569487216, 4569487216, True)

>>> a = 'test'*5

>>> b = 'test'*5

>>> len(a), id(a), id(b), a is b

(20, 4569499232, 4569499232, True)

>>> a = 'test'*6

>>> b = 'test'*6

>>> len(a), id(a), id(b), a is b

(24, 4569479328, 4569479168, False)

由于必须是常量字符串会应用到驻留,以是下面的例子不能达到驻留的效果:

>>> open('test.txt','w').write('hello')

5

>>> open('test.txt','r').read()

'hello'

>>> a = open('test.txt','r').read()

>>> b = open('test.txt','r').read()

>>> id(a), id(b), a is b

(4384934576, 4384934688, False)

>>> len(a), id(a), id(b), a is b

(5, 4384934576, 4384934688, False)

字符串驻留技巧,削减了大年夜量的重复字符串的内存分配。Python 底层经由过程字典实现的这种技巧,这些暂存的字符串作为字典的键。假如想要知道某个字符串是否已经驻留,应用字典的查找操作就能确定。

假如你对Python编程感兴趣,那么记得来小编的Python进修扣群:1017759557,这里有资本共享,技巧解答,大年夜家可以在一路交流Python编程履历,还有小编收拾的一份Python进修教程,盼望能赞助大年夜家更好的进修python。

image

您可能还会对下面的文章感兴趣: