Python字符串的实现
1. 字符串编码的概念
编码指将字符串按照一定的模式(按照双射表的转换)转换成二进制数字,再进行显示或者存储在计算机。
与编码相反,字符串解码就是把二进制数据按照相反模式转换成字符串。
当读取文件的时候,计算机会先判断应该使用哪个双射表(不同语言或者系统会使用不同的双射表)来把二进制数据还原为字符串。不过如果计算机不知道使用其他双射表来打开这个文件的话,最终可能会报错,也可能是乱码,大部分的乱码都是因为这样而出现。
一开始只有ASCII 7 位,后面西欧扩展成了8位支持更多的字符,但还是不够用,Unicode为世界上所有的字符映射了一个数字(统一标识符号)。这个数字的存储形式没有被具体定义,所以才会有那么多的编码方案。
2. 运行的Python如何在内存中存储字符串
在3.3之前(包括2.x中的unicode)都是以wchar_t数组的形式存在。当然编译的时候可以选择是2字节还是4字节。4字节就可以存下所有的字符但内存开销大,2字节的话只能存bmp的,超出的部分用surrogate pairs表示,这就意味着代码中需要额外的逻辑处理,内存减少了但逻辑更复杂,而且一些操作比如len,index都会有问题。(大概是这样,老版本代码没仔细看过。)
从3.3开始内存就变成了“按需”分配了。不再是一刀切的2字节或4字节,而是看你的字符串中最大的字符需要几个字节去表示。对于ASCII和Latin1,就是一个1字节的数组。如果有超出Latin1的,那么就是2字节数组,有超出bmp的就是4字节数组。这样就保证Python的string能存下所有Unicode字符,并且不需要surrogate pairs。这样一个很大的好处就是可以节约很多内存,因为毕竟西方世界还是主流,很多代码并不需要处理超出Latin1的字符。当然代码中的一部分逻辑需要根据不同的类型进行不同的处理,还需要兼容之前的类型(C扩展会带来以前布局的字符串)。鉴于代码质量很高且高度优化过,这不是什么问题。
Python的用户字符串类型
然而,关键点在于编码过程大部分与文件传输有关;在Py的代码书写过程中,定义了一个Python字符串,就没有任何“编码”的概念,这些由Python处理,除非你需要文件和流。
Python2.x
- str表示8位文本和二进制数据
- Unicode表示解码的Unicode文本
Python3.x
- str表示解码的Unicode文本(包括ASCII)
- bytes表示二进制数据(包括编码的文本)
- bytearray, 一种可变的bytes类型。
为什么会有不同的字符串类型
对于2.X,将二进制数据用str类型来表示的初衷,仅仅是因为二进制字符串只是字节的序列,用8位字节来表示也无可厚非;而对于富文本文本,就不能用一般的8位文本来处理了,所以就多出了一种专门处理Unicode文本的字符串。
对于3.X,其主要目标是, 把Python2.X中的普通字符串类型和Unicode字符串类型合并到一个单独的字符串类型中, 以支持简单文本和Unicode文本。开发人员想要消除2.X中文本串二分的局面。考虑到ASCII文本和其他的8位文本都是简单的Unicode类型,这种融合也很符合逻辑。Python3.x形式上将str字符串定义为Unicode码点序列而非字节序列,以明确它用来处理Unicode。并把非文本文件单独划分出来(Unicode无法去处理他们),称为bytes类型。值得一提的是,3.X有了bytearray这种专门修改原位置的可变字符串类型。这对真正的二进制数据和简单的文本类型都是有用的。
普通字符串和原始字符串
坦白来讲,我想先说结论:对于晦涩难懂而很少用到的东西,应当想尽一切方法避免使用它,而非去弄懂原理并编写稍不留神就出错的代码。
"""
普通字符串值得注意的点:
\u转义字符:在python2.x,只能在unicode串中被识别;在python3.x,在str中被识别。
r''被称为原始字符串,其中的转义字符不会被py转义;值得注意的是,原始字符串有一个Bug或者特性,那就是它不能以奇数个\结尾。
"""
字符串转换工具
str() 与 repr()
关于str()
和repr()
的相同点区别:他们都接收一个对象,并按照这个对象制作一个字符串类型的对象返回。二者的区别在于,str()
内置类型我是__str__()
,旨在转换成用户友好型的字符串,而repr()
的内置调用的是__repr__()
,它更多的是尽可能的显示出对象的属性,用于调试。详细的区别请百度,实际上这基本用不到(至少对我来说如此)。
解码 与 编码
str()
在只传入一个bytes类型的时候,表现的行为是返回一个bytes对象的打印字符串。
这是很自然的事情,但str()
拥有另一个用途:解码字符串。
- 编码:
str.encode()
和bytes(S, encoding)
- 解码:
bytes.decode()
和str(S, decoding)
一个明智的选择是,如果要解码或者编码字符串,在传参的时候编码类型一定要写,而不依赖默认参数。这可以避免二义性,并且让代码行为和平台无关。
类比python3.x的str()
和bytes
函数,python2.x的编码和解码发生在编码的str和解码的unicode之间,这仅仅是因为在2.x中二进制串在str中而富字符集字符串在unicode字符串中的实现机制。
ord() 与 chr()
关于转换,如果一个字符串只有单个字符,那么他们可以被ord()
函数转换成对应的ASCII码。反过来,一个ASCII码可以被chr()
转换成响应的单个字符的字符串。
3. Python3.x文本文件和二进制文件的处理策略
这里并不讨论任何2.X的实现。
由于3.X的字符串设计目标,导致3.X的文件处理策略也紧跟潮流。3.X将文件处理方案分为:
文本文件
以文本模式打开。读取内容会自动根据平台的默认名称或者提供的编码名称进行解码;如果文件头部存在BOM
二进制文件
以二进制模式打开(通过在内置函数open调用的模式字符串参数中添加一个小写的b)。直接读取数据,不会以任何编码方式解码它,返回一个bytes对象。二进制模式文件也接收一个bytearray对象作为写入到文件的内容。
4. 实际用字符串时
1. 简单or花招
python中为普通字符串提供了很多方法;并且字符串是序列,所以它具备序列的一系列操作。具体的方法详见上册p217. 在这之中,格式化表达式和格式化方法十分强大,以至于显得过分臃肿。我们基本用不到高级的格式化方法。
2. 源文件字符集编码声明
python默认的识别字面字符串的编码类型为utf-8
。在python文件的第一行或第二行可以显式的改变这一规则:
# -*- coding: latin-1 -*-
...coding anything...
3. 使用bypes对象
3.x的bytes对象本质是一个小整数序列,每个整数都位于0~255之间(也就是每8个字节为一段),并且在显示的时候也确实是打印ASCII字符。
bypes虽然也是字符串,但python不允许在操作中和普通字符串混合使用。例如,对不同类型的字符串相加显然意义不明。此外,使用时它支持字符串的大多数方法,但不支持格式化表达式与格式化方法。
从实际使用的角度,bytes通常出现形式是普通字符串编码时返回的对象:str.encode()
或者bytes(S, encoding=...)
时。后者实际上也是bypes的构造函数。
4. 使用bytesarray对象
这个类型在python2.6和2.7中被加入,作为3.x的一个向后兼容的功能。但它不像3.x中那样实施严格的文本/二进制区分。
从技术上来讲,它和bytes唯一的区别仅仅在于它是可变序列。
5. 实际使用中
- 对文本数据使用str;
- 对二进制数据使用bytes;
- 对想要原处修改的二进制数据使用bypearray.