Python 存储字符串时是如何节省空间的?
本文为翻译文章,已得到 @rushter 的许可
原文链接:rushter.com/blog/python…
转载请注明出处
从 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 ctypesclass 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 的 unicode 对象的实现(https://github.com/python/cpython/blob/master/Objects/unicodeobject.c
)大约有 16,000 行 C 代码,其中有很多小优化在本文中未提及。如果你想更多的了解 Python 中的 Unicode,推荐你去看一下字符串相关的 PEPs(https://www.python.org/dev/peps/
),同时查看下 unicode 对象的源码。
本文首发于公众号「小小后端」。
Python 存储字符串时是如何节省空间的?相关推荐
- python 判断字符串时是否是json格式方法
在实际工作中,有时候需要对判断字符串是否为合法的json格式 解决方法使用json.loads,这样更加符合'Pythonic'写法 代码示例: Python import json def is_j ...
- c语言单链表存储字符串,字符串的三种存储方式
@[TOC] 在数据结构中,字符串要单独用一种存储结构来存储,称为串存储结构.这里的串指的就是字符串.无论学习哪种编程语言,操作最多的总是字符串.我们平常使用最多的存储结构无疑是利用定长数组存储.但是 ...
- c语言单链表存储字符串,串的块链存储结构(C语言)详解
,指的是使用 本节实现串的块链存储使用的是无头节点的单链表.当然根据实际需要,你也可以自行决定所用链表的结构(shujujiegou,该链表各个节点中可存储 1 个字符: 图 1 各节点仅存储 1 个 ...
- 【Python】字符串 - 集大成篇
目录 1.不同语言的字符串比较 1.1.C 语言 1.2.C ++ 语言 1.2.1 C 风格字符串 1.2.2 C ++ 风格字符串 1.3.JAVA 1.4.Python 2.Python ...
- md5后得到的32位字符串存储到mysql中太占空间了_好看!快收藏:非常完整的 MySQL 规范...
一.数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来) 数据库对象的命名要能做到见名 ...
- md5后得到的32位字符串存储到mysql中太占空间了_面试官:你对MySQL高性能优化有什么规范建议?...
推荐阅读:吊打面试官!MySQL灵魂100问,你能答出多少? 文章篇幅较长,建议先收藏再找个合适的时间阅读 数据库命令规范 所有数据库对象名称必须使用小写字母并用下划线分割 所有数据库对象名称禁止使用 ...
- 已知线性表最多可能有20个元素,存储每个元素需要8字节,存储每个指针需要4字节。当元素个数为( )时使用单链表比使用数组存储此线性表更加节约空间。
已知线性表最多可能有20个元素,存储每个元素需要8字节,存储每个指针需要4字节.当元素个数为( 大于等于13 )时使用单链表比使用数组存储此线性表更加节约空间. 使用数组存储线性表需要提前分配好数组空 ...
- python列表换行输出_Python从列表转换为字符串时处理换行符
我有一个关于换行符和返回字符的问题.呃,这很难解释,但我会尽力的. 我有列表形式的数据.列表的成员中有换行符,因此. 1 2 3example_list = ["I've always lo ...
- 你真的知道Python的字符串是什么吗?
在<详解Python拼接字符串的七种方式>这篇推文里,我提到过,字符串是程序员离不开的事情.后来,我看到了一个英文版本的说法: There are few guarantees in li ...
最新文章
- 三角数字(某年南理工研究生入学上机试题)
- 设置Collection 或 Map 只读
- uclinux内核线程的创建(转)
- 一张图来看看.NETCore和前后端技术的演进之路
- 几种常用的排序方法7--希尔排序
- 从门户网站看内容传播的开放式革命
- 关于 asp.net 服务器控件几个 ID 的说明
- [opencv] Unsupported depth of input image
- 《精通软件性能测试与LoadRunner最佳实战》—第1章1.1节软件测试基础
- Shell脚本修改Nginx upstream配置文件
- 动态设置样式 calc计算
- python机器学习库sklearn——神经网络
- asscalar()函数
- Fedora七年风雨路:寻访14个版本的足迹
- BS7799系列讲座:HTP模型图及构建(转载)
- 插值(五)Bicubic interpolation(双三次插值)
- Valve在Game Dev Conference上,OUYA上的1000场比赛等等
- java面试详解-总有你能碰得到的
- 账号注册及登录具体流程
- Excel对多个sheet页进行相同操作
热门文章
- outlook正在与服务器联系以获取信息,Outlook 2016点击邮件显示正在与服务器联系以获取信息...
- python计算学习_跟老齐学Python之用Python计算
- 关于对 linux系统的物理内存访问 /dev/mem
- 嵌入式开发都需要会些什么
- zabbix linux网卡流量,如何使用zabbix3.2监控Windows网卡流量
- php字符串中有特殊符号怎么处理,PHP字符串中特殊符号的过滤方法介绍
- java公平所与非公平所_一张图读懂Java非公平锁与公平锁
- 关于按键消抖以及LED灯控制的一个实例
- 【Verilog HDL 训练】第 09 天(按键消抖)
- 【FPGA】Buffer专题介绍(一)