Python 存储字符串时节省空间的方法


Posted in Python onApril 23, 2019

从 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 = ':snake:'
>>> sys.getsizeof(string3 + ':computer:') - 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(':snake:')
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 相关文章推荐
神经网络理论基础及Python实现详解
Dec 15 Python
详解python多线程、锁、event事件机制的简单使用
Apr 27 Python
Flask实现跨域请求的处理方法
Sep 27 Python
python实现字符串和字典的转换
Sep 29 Python
如何运行.ipynb文件的图文讲解
Jun 27 Python
用Python实现BP神经网络(附代码)
Jul 10 Python
python字符串替换re.sub()方法解析
Sep 18 Python
推荐8款常用的Python GUI图形界面开发框架
Feb 23 Python
Django实现从数据库中获取到的数据转换为dict
Mar 27 Python
python进行参数传递的方法
May 12 Python
python字符串常规操作大全
May 02 Python
Django页面数据的缓存与使用的具体方法
Apr 23 #Python
Python切片操作去除字符串首尾的空格
Apr 22 #Python
详解python中的hashlib模块的使用
Apr 22 #Python
Python 中包/模块的 `import` 操作代码
Apr 22 #Python
python定时检测无响应进程并重启的实例代码
Apr 22 #Python
django query模块
Apr 20 #Python
不到20行代码用Python做一个智能聊天机器人
Apr 19 #Python
You might like
3种平台下安装php4经验点滴
2006/10/09 PHP
深入PHP5中的魔术方法详解
2013/06/17 PHP
解析php入库和出库
2013/06/25 PHP
ThinkPHP实现将本地文件打包成zip下载
2014/06/26 PHP
jQuery 使用手册(一)
2009/09/23 Javascript
javascript string字符串优化问题
2011/07/31 Javascript
jQuery实现切换页面布局使用介绍
2011/10/09 Javascript
深入理解JavaScript系列(14) 作用域链介绍(Scope Chain)
2012/04/12 Javascript
解析Javascript中难以理解的11个问题
2013/12/09 Javascript
原生js实现淘宝首页点击按钮缓慢回到顶部效果
2014/04/06 Javascript
jQuery寻找n以内完全数的方法
2015/06/24 Javascript
JS实现新浪博客左侧的Blog管理菜单效果代码
2015/10/22 Javascript
Angular.JS中指令ng-if的注意事项小结
2017/06/21 Javascript
微信小程序 确认框的实现(附代码)
2019/07/23 Javascript
[00:36]DOTA2上海特级锦标赛 Alliance战队宣传片
2016/03/04 DOTA
Python中的groupby分组功能的实例代码
2018/07/11 Python
python+opencv+caffe+摄像头做目标检测的实例代码
2018/08/03 Python
Python中常用的内置方法
2019/01/28 Python
Python使用numpy模块实现矩阵和列表的连接操作方法
2019/06/26 Python
python自动化工具之pywinauto实例详解
2019/08/26 Python
git查看、创建、删除、本地、远程分支方法详解
2020/02/18 Python
Django表单提交后实现获取相同name的不同value值
2020/05/14 Python
使用Keras 实现查看model weights .h5 文件的内容
2020/06/09 Python
使用keras时input_shape的维度表示问题说明
2020/06/29 Python
Django Form设置文本框为readonly操作
2020/07/03 Python
AVON雅芳官网:世界上最大的美容化妆品公司之一
2016/11/02 全球购物
美国卡车、吉普车和SUV零件网站:4 Wheel Parts
2016/11/24 全球购物
Fox Racing英国官网:越野摩托车和山地自行车服装
2020/02/26 全球购物
Myprotein亚太地区:欧洲第一在线运动营养品牌
2020/12/20 全球购物
Java中有几种方法可以实现一个线程?用什么关键字修饰同步方法?stop()和suspend()方法为何不推荐使用?
2015/08/04 面试题
国际贸易专业个人求职信范文分享
2013/12/14 职场文书
教师岗位聘任书范文
2014/03/29 职场文书
内乡县衙导游词
2015/02/05 职场文书
七一活动主持词
2015/06/29 职场文书
MySQL中一条update语句是如何执行的
2022/03/16 MySQL
SQL语句多表联合查询的方法示例
2022/04/18 MySQL