0%

Fluent Python 读书笔记(2)

包括文本(第4章)和一等函数(第5章)

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
4
text.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 处理文本文件

处理文本的最佳实践:

  1. 解码输入的字节序列
  2. 只处理文本
  3. 编码输出的文本

Python 3 内置的open函数会在读取文件时作必要的解码,以文本模式写入文件时还会做必要的编码,所以调用my_file.read()方法得到的以及传给my_file.write(text)方法都是字符串对象。

编码默认值

如果打开文件时没有指定encoding参数,默认值由locale.getpreferredencoding()提供,这个值也是重定向到文件的sys.stdout/stdin/stderr的默认编码。但是,用户的编号设置在不同系统中的设定方式不同,而且在某些系统中可能无法通过编程方式设置,因此这个函数返回的只是猜测的编码。
所以,不要依赖默认值!!!

4.6 为了正确比较而规范化Unicode字符串

Unicode有组合字符,字符串的比较会比较复杂。
比如,café有两种构成方式:

  1. café
  2. cafe\u0301

因为它们的码位不同,所以比较结果是不等同的。
U+0301是COMBINING ACUTE ACCENT, 加在e后得到é。 在Unicode标准中,’café’和’e\u0301’称为标准等价物(canonical equivalent)。Python判断的是码位序列,因此判为不等。

这种问题的解决方式是:使用unicodedata.normalize函数提供的Unicode规范化。这个函数的第一个参数有四种选择:

  1. NFC,Normalization Form C,使用最少的码位构成等价的字符串。
    西方键盘通常能输出组合字符,因此用户输入的文本默认是NFC形式,但是还是在保存文本之前使用normalize('NFC', text)清洗文本。
    使用NFC时,有些单字符会被规范为另一个单字符。比如电阻单位欧姆和希腊字母大写的欧米茄。二者在视觉上一样,但是比较时并不相等。
  2. NFD,把组合字符分解成基字符和单独的组合字符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from 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))
    # true
  3. NFKC, K表示“compatibility”,兼容性,是比较严格的规范化形式,对“兼容字符”有影响。
    在NFKC和NFKD中,兼容字符会被替换为一个或多个“兼容分解字符”,可能会损失或曲解信息,但是可以为搜索和索引提供便利的中间表述。

  4. NFKD

大小写折叠

大小写折叠将所有文本变成小写,再做些其他转换。

对于只包含latin1的字符的字符串s, s.casefold()s.lower()一样,只有两个例外:

  1. 微符号会变成小写希腊字母μ
  2. 德语ß会变成ss

Python 3.4起s.casefold()s.lower()不同结果的有116个码位,占Unicode字符的0.11%

规范化文本匹配实用函数

NFC和NFC可以合理比较Unicode字符串,对于大部分应用来说,NFC是最好的最规范化形式。
不区分大小写的应该使用str.casefold()
处理多语言文本:

  1. nfc_equal
    大小写敏感
  2. 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
4
import 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 一等函数

一等对象满足:

  1. 在运行时创建
  2. 能赋值给变量或数据结构中的元素
  3. 能作为参数传给函数
  4. 能作为函数的返回结果

Python中,整数、字符串、字典都是一等对象。 函数也是。

5.1 把函数视作对象

5.2 高阶函数

接收函数为参数,或者把函数作为结果返回的函数是高阶函数
常见的有map, 内置函数sorted也是,可选的key参数用于提供一个函数,任何单函数参数都能作为key的值。

在函数式编程范式里,最为人熟知的高阶函数有map, filter, reduceapply
apply在Python 3 中移除了。

map, filter和reduce的替代品

1
2
3
4
5
6
7
list(map(factorial, range(6)))
# 等同于
[factorial(n) for n in range(6)]

list(map(factorial, filter(lambda n : n % 2, range(6))))
# 等同于
[factorial(n) for n in range(6) if n % 2]

在Python 3 中,mapfilter返回生成器,所以替代品是生成器表达式。 在Python 2 中返回的是列表,所以替代品是列表推导。

Python 2 中reduce是内置函数, Python 3 中放入了functools模块中了。 常用于求和,最好使用内置的sum函数

1
2
3
4
5
from functools import reduce
from operator import add
reduce(add, range(100))
# 等同于
sum(range(100))

sumreduce的通用思想是把某个操作连续应用到序列的元素上,累计之前的结果,把系列值归约成一个值。还有以下内置归约函数:

  • 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中可调用对象:

  1. 用户定义的函数
    使用def语句或lambda表达式创建
  2. 内置函数
    使用C语言(CPython)实现的函数,如lentime.strftime
  3. 内置方法
    使用C语言实现的方法,如dict.get
  4. 方法
    在类的定义体中定义的函数

  5. 调用类时会允许类的__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实例返回给调用方。
  6. 类的实例
    如果类定义了__call__方法,它的实例可以作为函数调用
  7. 生成器函数
    使用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 用于部分应用于一个函数,基于一个函数创建新的可调用对象,把某些参数固定。