Chap 4 文本和字节序列
4.1 字符问题
一个字符串是一个字符序列。
2015年,字符的最佳定义是Unicode字符。
Unicode标准将字符的标识和具体的字节表述做了明确区分:
- 字符的标识,即码位 在Unicode标准中以4-6个十六进制数字表示,并且加前缀“U+”
- 字符的具体表述取决于所用的编码。编码是在码位和字符序列之间转换时使用的算法。
把码位转为字节序列是编码, 反之是解码。
4.2 字节概要
Python 3引入不可变bytes类型,2.6添加可变bytearray类型
bytes或bytearray对象的各个元素是介于0-255(含)之间的整数。
二进制序列的切片始终是同一类型的二进制序列
各个字节的值会以三种不同的方式显示:
- 可打印的ASCII范围内的字节(从空格到~),使用ASCII本身
- 制表符、换行符、回车符和\对应的字节,使用转义序列\t,\n,\t,\
- 其他字节的值,使用十六进制转义序列
构建bytes和bytearray实例可以调用各自的构造方法,传入参数:
- 一个str对象和一个encoding关键字参数
- 一个可迭代对象,提供0-255之间的数值
- 一个整数,使用空字节创建对应长度的二进制序列。(3.6删除)
- 一个实现了缓冲协议的对象,如bytes, bytearray, memoryview, array.array,此时把原对象中的字节序列复制到新建的二进制序列中
结构体和内存视图
memoryview不是用于创建或存储字节序列的,而是共享内存,可以访问其他二进制序列、打包的数组和缓冲中的数据切片,而无需复制字节序列。他的切片是一个新的memoryview对象,而且不会复制字节序列。
4.3 基本的编解码器
4.4 了解编解码问题
处理UnicodeEncodeError
编码问题
把文本转换为字节序列时,如果目标编码中没有定义的某个字符,就会抛出UnicodeEncodeError
。除非用errors参数传给编码方法或函数,对错误进行特殊处理。1
2
3
4text.encode('cp437', errors='ignore|replace|xmlcharrefreplace')
# ignore 会跳过无法编码的字符
# replace 会用?替换无法编码的字符
# xmlcharrefreplace 将无法编码的字符替换成XML实体
处理UnicodeDecodeError
不是每个字节都包含有效的ASCII字符,也不是每个字符序列都是有效的UTF8或16,在将二进制序列转换成文本时,假设是这两个编码中的一种,遇到无法转换的字节序列会抛出UnicodeDecodeError
而陈旧的8位编码,比如cp1252等能编码任何字节序列流而不抛出错误
使用预期之外的编码加载模块时抛出的SyntaxError
Python 3默认使用UTF-8编码源码,而2.5开始则默认使用ASCII
如果加载的.py模块中包含UTF8之外的数据,而且没有声明编码,会得到SyntaxError
可在文件顶部添coding注释:1
# coding: cp1252
BOM的问题
BOM是字节序标记,Unicode码点U+FEFF,放在UTF16文件的开头,如果字节序是FEFF则是大字节序,FFFE则是小字节序。
UTF8由于编码特性与字节序无关,所以不需要BOM。但是微软习惯在UTF8中使用BOM。
锟斤拷的问题:一些语言体系在转为Unicode时,有些字符不存在,Unicode规定以U+FFFD
作为占位符表示这些字符,用UTF8编码为EF BF BD
, 连续多个EF BF BD
会被GB编码程序以两个字节一个汉字的形式编码,就会出现“锟斤拷”。 为了避免这个问题可以减少程序的编码转换。
参考 http://jimliu.net/2015/03/07/something-about-encoding-extra/
4.5 处理文本文件
处理文本的最佳实践:
- 解码输入的字节序列
- 只处理文本
- 编码输出的文本
Python 3 内置的open
函数会在读取文件时作必要的解码,以文本模式写入文件时还会做必要的编码,所以调用my_file.read()
方法得到的以及传给my_file.write(text)
方法都是字符串对象。
编码默认值
如果打开文件时没有指定encoding参数,默认值由locale.getpreferredencoding()
提供,这个值也是重定向到文件的sys.stdout/stdin/stderr
的默认编码。但是,用户的编号设置在不同系统中的设定方式不同,而且在某些系统中可能无法通过编程方式设置,因此这个函数返回的只是猜测的编码。
所以,不要依赖默认值!!!
4.6 为了正确比较而规范化Unicode字符串
Unicode有组合字符,字符串的比较会比较复杂。
比如,café有两种构成方式:
- café
- cafe\u0301
因为它们的码位不同,所以比较结果是不等同的。
U+0301是COMBINING ACUTE ACCENT, 加在e后得到é。 在Unicode标准中,’café’和’e\u0301’称为标准等价物(canonical equivalent)。Python判断的是码位序列,因此判为不等。
这种问题的解决方式是:使用unicodedata.normalize
函数提供的Unicode规范化。这个函数的第一个参数有四种选择:
- NFC,Normalization Form C,使用最少的码位构成等价的字符串。
西方键盘通常能输出组合字符,因此用户输入的文本默认是NFC形式,但是还是在保存文本之前使用normalize('NFC', text)
清洗文本。
使用NFC时,有些单字符会被规范为另一个单字符。比如电阻单位欧姆和希腊字母大写的欧米茄。二者在视觉上一样,但是比较时并不相等。 NFD,把组合字符分解成基字符和单独的组合字符
1
2
3
4
5
6
7
8
9
10
11from unicodedata import normalize
s1 = 'café'
s2 = 'cafe\u0301'
print(len(normalize('NFC', s1)), len(normalize('NFC', s2)))
# 4, 4
print(len(normalize('NFD', s1)), len(normalize('NFD', s2)))
# 5, 5
print(normalize('NFC', s1) == normalize('NFC', s2))
# true
print(normalize('NFD', s1) == normalize('NFD', s2))
# trueNFKC, K表示“compatibility”,兼容性,是比较严格的规范化形式,对“兼容字符”有影响。
在NFKC和NFKD中,兼容字符会被替换为一个或多个“兼容分解字符”,可能会损失或曲解信息,但是可以为搜索和索引提供便利的中间表述。- NFKD
大小写折叠
大小写折叠将所有文本变成小写,再做些其他转换。
对于只包含latin1的字符的字符串s, s.casefold()
和s.lower()
一样,只有两个例外:
- 微符号会变成小写希腊字母μ
- 德语ß会变成ss
Python 3.4起s.casefold()
和s.lower()
不同结果的有116个码位,占Unicode字符的0.11%
规范化文本匹配实用函数
NFC和NFC可以合理比较Unicode字符串,对于大部分应用来说,NFC是最好的最规范化形式。
不区分大小写的应该使用str.casefold()
处理多语言文本:
- nfc_equal
大小写敏感 - fold_equal
存在大小写折叠的问题
4.7 Unicode文本排序
不同地区采用的排序规则有所不同。
在Python, 非ASCII文本的排序标准是使用locale.strxfrm
函数,这个函数会把字符串转换为适合所在区域进行比较的形式。
在使用locale.strxfrm
之前,必须先为应用设定合适的区域设置,locale.setlocale(LC_COLLATE, <YOUR_LOCALE>)
(先import locale)
区域设置是全局的!因此不推荐在库中调用setlocale
,应用或框架应在进程启动时设定区域设置,而且此后不要再修改。
使用Unicode排序算法排序
因为上述方法存在一些注意事项,James Tauber开发了Unicode排序算法,Unicode Collation Algorithm。1
2
3
4import pyuca
coll = pyuca.Collator()
fruits = [ ... ]
sorted_fruits = sorted(fruits, key=coll.sort_key)
PyUCA默认使用项目自带的allkeys.txt,没有考虑区域设置。
4.8 Unicode数据库
Unicode标准提供了一个完整的数据库,不仅包括码位与字符名称之间的映射,还有各个字符的元数据,以及字符之间的关系
4.9 支持字符串和字符序列的双模式API
正则表达式中的字符串和字符序列
如果使用字符序列构建正则表达式,\d和\s等模式只能匹配ASCII字符;
如果使用字符串模式,就能匹配ASCII之外的Unicode数字或字母
os函数中的字符串和字符序列
os模块中的所有函数、文件名和路径名参数既能使用字符串,也能使用字节序列。
字符串好多琐碎的知识啊(:з)∠)
Chap 5 一等函数
一等对象满足:
- 在运行时创建
- 能赋值给变量或数据结构中的元素
- 能作为参数传给函数
- 能作为函数的返回结果
Python中,整数、字符串、字典都是一等对象。 函数也是。
5.1 把函数视作对象
5.2 高阶函数
接收函数为参数,或者把函数作为结果返回的函数是高阶函数。
常见的有map
, 内置函数sorted
也是,可选的key参数用于提供一个函数,任何单函数参数都能作为key的值。
在函数式编程范式里,最为人熟知的高阶函数有map
, filter
, reduce
和apply
。apply
在Python 3 中移除了。
map, filter和reduce的替代品
1 | list(map(factorial, range(6))) |
在Python 3 中,map
和filter
返回生成器,所以替代品是生成器表达式。 在Python 2 中返回的是列表,所以替代品是列表推导。
Python 2 中reduce
是内置函数, Python 3 中放入了functools
模块中了。 常用于求和,最好使用内置的sum
函数1
2
3
4
5from functools import reduce
from operator import add
reduce(add, range(100))
# 等同于
sum(range(100))
sum
和reduce
的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把系列值归约成一个值。还有以下内置归约函数:
- all(iterable) 如果每个元素都是真值,返回True, all([])返回True
- any(iterable) 只要元素中有真值,返回True, any([])返回True
5.3 匿名函数
lambda
关键字在Python表达式里创建匿名函数
但是,lambda函数的定义体只能使用纯表达式: 不能赋值、不能使用while和try等Python语句。
在参数列表中最适合使用匿名函数。
除了作为参数传给高阶函数外,Python很少使用匿名函数。
lambda句法只是语法糖(指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。)和def语句一样,lambda表达式会创建函数对象。
5.4 可调用对象
判断对象能否被调用,可以使用内置的callable()
函数。 Python数据模型文档列出了7中可调用对象:
- 用户定义的函数
使用def语句或lambda表达式创建 - 内置函数
使用C语言(CPython)实现的函数,如len
或time.strftime
- 内置方法
使用C语言实现的方法,如dict.get
- 方法
在类的定义体中定义的函数 - 类
调用类时会允许类的__new__
方法创建一个实例,然后运行__init__
方法,初始化实例,最后把实例返回给调用方。 - 类的实例
如果类定义了__call__
方法,它的实例可以作为函数调用 - 生成器函数
使用yield
关键字的函数或方法。调用生成器函数返回的是生成器对象
5.5 用户定义的可调用类型
任何Python对象都可以表现得像函数,只需实现实例方法__call__
实现__call__
方法的类是创建函数类对象的简便方法,此时必须在内部维护一个状态。让他在调用之间可用。
装饰器就是这样,它必须是函数,而且有时要在多次调用之间记住某些事。
创建保有内部状态的函数,还可以使用闭包。
5.6 函数内省
内省是指在运行时确定对象类型的能力。
用dir()
函数可以查看某函数具有的属性。1
dir(factorial)
返回的属性大多数是Python对象共有的,可以通过求差集得到用户定义函数特有的属性。
用户定义的函数的属性
| 名称 | 类型 | 说明 |
| —– | —– | —-|
| annotations | dict | 参数和返回值的注解 |
| call | method-wrapper | 实现()运算符,即可调用对象协议 |
| closure | tuple | 函数闭包,即自由变量的绑定 |
| code | code | 编译成字节码的函数元数据和函数定义体 |
| defaults | tuple | 形式参数的默认值 |
| get | method-wrapper | 实现只读描述符协议 |
| globals | dict | 函数所在模块中的全局变量 |
| kwdefaults | dict | 仅限关键字形式参数的默认值 |
| name | str | 函数名称 |
| qualname | str | 函数的限定名称 |
5.7 从定位参数到仅限关键字参数
仅限关键字 是Python 3新特性,指参数只能通过关键字参数制定,一定不会捕获未命名的定位参数,它们在函数定义时放在前面有的参数后面。
调用函数是使用 和 * 展开可迭代对象,映射到单个参数。
捕获该位置起的任意个参数。
仅限关键字参数不一定要有默认值。
5.10 函数式编程
operator模块
operator模块为多个算数运算符提供了对应的函数。如add
, mul
等。itemgetter
可以用来根据元素的某个字段给元组列表排序, 如果传入多个参数可返回提取的值构成的元组。attrgetter
创建的函数根据名称提取对象的属性。
使用functools.partial 冻结参数
functools.partial
用于部分应用于一个函数,基于一个函数创建新的可调用对象,把某些参数固定。