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每次处理固定个数的字符的方法总结
Jan 29 Python
举例讲解Python中装饰器的用法
Apr 27 Python
Python获取运行目录与当前脚本目录的方法
Jun 01 Python
关于Python中Inf与Nan的判断问题详解
Feb 08 Python
Python3.6简单操作Mysql数据库
Sep 12 Python
利用Django内置的认证视图实现用户密码重置功能详解
Nov 24 Python
python实现用户管理系统
Jan 10 Python
Python中如何导入类示例详解
Apr 17 Python
Python实现的对一个数进行因式分解操作示例
Jun 27 Python
pytorch自定义初始化权重的方法
Aug 17 Python
pyspark给dataframe增加新的一列的实现示例
Apr 24 Python
Python基于template实现字符串替换
Nov 27 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
用Zend Encode编写开发PHP程序
2010/02/21 PHP
PHP设计模式 注册表模式(多个类的注册)
2012/02/05 PHP
PHP读取CSV大文件导入数据库的实例
2017/07/24 PHP
[原创]PHP实现SQL语句格式化功能的方法
2017/07/28 PHP
thinkPHP中U方法加密传递参数功能示例
2018/05/29 PHP
JS获取各种浏览器窗口大小的方法
2014/01/14 Javascript
js实现回放拖拽轨迹从过程上进行分析
2014/06/26 Javascript
jQuery实现跟随鼠标运动图层效果的方法
2015/02/02 Javascript
JavaScript移除数组内重复元素的方法
2015/03/18 Javascript
JS控制div跳转到指定的位置的几种解决方案总结
2016/11/05 Javascript
javascript实现多张图片左右无缝滚动效果
2017/03/22 Javascript
JS实现给json数组动态赋值的方法示例
2020/03/19 Javascript
详解Vue学习笔记进阶篇之列表过渡及其他
2017/07/17 Javascript
使用D3.js创建物流地图的示例代码
2018/01/27 Javascript
vue.js,ajax渲染页面的实例
2018/02/11 Javascript
vue2 v-model/v-text 中使用过滤器的方法示例
2019/05/09 Javascript
JavaScript实现页面中录音功能的方法
2019/06/04 Javascript
在vue-cli3中使用axios获取本地json操作
2020/07/30 Javascript
[03:03]DOTA2 2017国际邀请赛开幕战队入场仪式
2017/08/09 DOTA
[02:37]2018DOTA2亚洲邀请赛赛前采访-EG篇
2018/04/03 DOTA
Python中的对象,方法,类,实例,函数用法分析
2015/01/15 Python
python 接口返回的json字符串实例
2018/03/27 Python
python从子线程中获得返回值的方法
2019/01/30 Python
Python常用的json标准库
2019/02/19 Python
Django实现微信小程序的登录验证功能并维护登录态
2019/07/04 Python
python 判断字符串中是否含有汉字或非汉字的实例
2019/07/15 Python
浅析python,PyCharm,Anaconda三者之间的关系
2019/11/27 Python
python基于opencv检测程序运行效率
2019/12/28 Python
vscode调试django项目的方法
2020/08/06 Python
Speedo美国:澳大利亚顶尖泳衣制造商
2016/08/03 全球购物
美的官方商城:Midea
2016/09/14 全球购物
自荐信的禁忌和要点
2013/10/15 职场文书
幼儿教师辞职信
2015/02/27 职场文书
SpringBoot实现异步事件驱动的方法
2021/06/28 Java/Android
「月刊Comic Alive」2022年5月号封面公开
2022/03/21 日漫
Python使用socket去实现TCP客户端和TCP服务端
2022/04/12 Python