2019-08-12-1901100244-自学训练营-Python入门-Day05-学习心得

学员信息

  • 学号:1901100244
  • 操作系统:Windows 10 version 1903 (64-bit)
  • 学习内容:字符串的基本处理,词频统计,数组操作,进制转换
  • 学习用时:6 小时

收获总结

  1. 了解了字符串相关的替换、删除、大小写转换等工作的方法;
  2. 了解了由字符串分词生成列表的方法;
  3. 了解了列表的排序方法;
  4. 了解了字典的生成和排序方法;
  5. 明确了列表、元组、字典的联系和区别;
  6. 了解了切片的使用方法;
  7. 了解了数字类型和字符串类型的转换方法;
  8. 了解了由列表拼接字符串的方法;
  9. 了解了十进制数到二进制数、八进制数、十六进制数的转换方法。

遇到的难点与问题

1. 字符串样本的形式

任务手册中给出的字符串样本是著名的《Python 之禅》(The Zen of Python),其最早由蒂姆·彼得斯(Tim Peters)在 Python 邮件列表中发表,它包含了影响 Python 编程语言设计的 19 条程序编写原则。

《Python 之禅》被内置到了 Python 的 this 模块中,只需要运行 import this 就会被输出。

但手册中所呈现的字符串样本又与内置的内容有所区别,表现在:

  • 开头多了一个空行;
  • 标题和正文间多了一个空行;
  • 有错别字(“ambiguity”写成了“ambxiguity”);
  • 结尾多了一个空行。

另外,原始的文本由于种种原因,使用了“打字机风格”,具体表现在:

  • 单引号(‘、’)和缩略号(’)使用了直引号(’)进行替代;
  • 破折号(—)使用了两个连字符(–)进行替代;
  • 强调使用了成对星号(*)进行表达。

原始文本还有令人纠结的一点,就是破折号左右的空格。破折号左右要不要留空格是个见仁见智的问题,但至少应当全文一致,可原始文本的三个破折号用法各异,这就难以解释了。

这些问题虽然从完成任务的角度来说无需关心,但从排版的角度来说,足够让人产生如鲠在喉的感觉,况且 Python 3.x 默认使用的编码已经是 Unicode,完全可以在文本上解决引号、破折号等问题。

本着这样的想法,我在原始文本的基础上,考虑了排版规则,结合维基百科的 The Zen of Python 页面的内容,生成了作为练习使用的字符串文本。

此外,我顺着《Python 之禅》找到了一系列的《Python 增强提案》(Python Enhancement Proposals,缩写为 PEPs),这其中的《Python 编码规范》(Style Guide for Python Code,编号为 PEP 8)应该是学习 Python 编程的必读文本,其他的提案也可以选择性地读一读。

另可参见:
维基百科的 Python 之禅页面
蛇宗三字经(The Zen of Python)
写在最前面:The Zen of Python
Python 的众多 PEP 之中,除了 PEP 8,还有哪一些是值得阅读的?
维基百科的英文破折号(—)页面
别再用「六个点」当省略号了,这些标点都有更规范的输入方式
中文排版需求
Mathematical Alphanumeric Symbols
VS Code 写 Python 时的代码错误提醒和自动格式化
Linting Python in Visual Studio Code

2. 英文分词

分词是个大问题。

简单的文本,可以通过空格分词,但稍微复杂一点的文本,处理起来就很耗神。

怎么叫“复杂”?包括但不限于:

  • 小数(1.25之类);
  • 复杂词形($10、10% 之类);
  • 缩略语(U.S.A. 之类);
  • 包含连字符的词汇(self-teaching 之类);
  • 特殊符号(…、– 之类)
  • 转义符(\n 之类)。

还好,本次练习要处理的文本,不算太复杂。

不过要完成的目标(去掉文本中包含“ea”的单词),还是对分词有些要求的。举例来说,“idea—let’s”这样的部分,就得把“idea”去掉,别的内容保留,之后还要处理破折号两侧可能出现的多余空格。

简单的分词用 split() 函数就行,但本次用作分词的分隔符不只一个,故而选择通过加载正则表达式模块(re),通过 re.split() 函数完成分词。这里有两个要注意的地方:

  • 间隔符表达式如果带括号((…)),生成的列表里就包括分隔符,反之则不包括;
  • 正则表达式字符串建议在前面加上标记 r,举例来说,我使用的表达式为 r'(\s|,|\.|—)'

更复杂的分词可以通过加载其他的分词工具完成,具体可以参见《Python 文本数据分析初学指南》的“4.2 英文分词及词性标注”部分。

3. 正则表达式

正则表达式(Regular Expression,常简写为 regex、regexp 或 RE)是计算机科学的一个概念,其使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。^Wiki_cs_RE

简单地说,如果需要查找或替换某个具体的字词,使用普通的查找或替换工具就可以了,但如果需要查找或替换的,是符合某些特定规则的字符串,就需要依靠正则表达式了。

快速地了解一下正则表达式,可以阅读陆超编写的《正则表达式30分钟入门教程》,深入学习,可以参考杰弗里·弗里德尔(Jeffrey E. F. Friedl)撰写的 500 多页的大部头《精通正则表达式》和余晟撰写的《正则指引》,还可以通过使用 RegesterRegexBuddy 等软件进行练习和测试。

无论如何,正则表达式的语法都很难读写,令人头疼,可为了它能实现的功能,只能认了。

上文已经说过,在 Python 里使用正则表达式需要加载正则表达式模块(re),不过在我用的时候遭遇了这么几个问题:

  1. 后向引用
    我认为包含撇号(’)的单词(如 ’s、’re、don’t 等),应该单独特别处理,而非简单地和它周围的词混在一起。举例来说,“Let’s”应该断成“Let”和“’s”,“don’t”就应该是一个整体。
    好在要处理的文本只有 3 个撇号,都是断开就行的,我就简化处理了——在撇号之前加空格把它和关联单词分开。
    完成这个任务需要使用“后向引用”,其默认的表达方式应该是类似“\1”、“\2”这样的,但我调了数次都报错,又作了一番功课才明白在这里应该用“\\1”、“\\2”。
    捎带要提一下的是,VS Code 里的查找/替换功能也可以用正则表达式,但它的后向引用要写成“$1”、“$2”这种形式。
  2. 转义序列
    “转义(escape)”是指用多个字符的有序组合来表示原本需要的字符的手段,“转义序列(escape sequence)”则指在转义时使用的有序字符组合。^Wiki_cs_Escape_Sequence
    转义序列通常用作表示那些“无法直接输入”的字符,比如回车符、换行符、制表符等等。
    在 Python 里,转义序列是用“\”开头的一系列字符组合,常用的有:换行符“\n”,制表符“\t”。
    但在正则表达式里,也会用到转义序列,正则表达式的转义序列还比 Python 多些,如:空白符“\s”,数字“\d”,单词边缘“\b”等。
    转义序列很有用,可也会带来麻烦,比如要输入 'C:\new\' 这个路径的字符串,其中的 \n 就会被转义,要避免这一问题,有两种方式:一,使用“\\”来表示不需要转义的反斜线(\),也即写为 'C:\\new\',二,使用“r”标记告诉 Python 此处不转义,也即写为 r'C:\new\'
    当在 Python 里用正则表达式时,问题就更严重了,因为 Python 本身会处理一层转义,正则表达式又会处理一层转义,故而要向正则表达式里传递一个不转义的“\”,字符串要写成“'\\\\'”,这实在是很麻烦,在这种情况下,建议更多地使用“r”标记来处理正则表达式。
    Python 官方发布的静态代码检查工具 Flake8 现在已经把所有 Python 不支持的转义序列标记成错误,提示“invalid escape sequence ‘x’ (W605)”,解决的办法也是在相应字符串加“r”标记。
    除了“r”外,字符串可用的标记还有“u”、“f”等,具体使用可以参见《The Python Language Reference》的“2.4.1. String and Bytes literals”和“2.4.3. Formatted string literals”部分。
  3. 特殊字符
    正则表达式里的“\s”困惑了我一阵子。我一开始用它来匹配空格了,发现不好用。搜索了一下才明确,“\s”可以匹配任意的空白符,包括制表符(\t,即'\u0009')、换行符(\n,即'\u000A')、回车符(\r,即'\u000D')、换页符(\f,即'\u000C')、竖向制表符(\v,即'\u000B')、半角空格(“ ”,即'\u0020'),如果是 Unicode 的正则表达式,\s 还可以匹配全角空格(“ ”,即'\u3000')。
    半角空格没有转义序列,要怎么匹配呢?查了好一会儿我才反应过来,直接输入“ ”就可以了……
    顺便我还了解了一下各种不同编码的转义序列表达法,前面“\u”开头的那些字符串,就是 Unicode 编码的转义序列表达法,具体细节可以参见《JavaScript 转义字符》(JavaScript character escape sequences),《字符编码笔记:ASCII,Unicode 和 UTF-8》,《Unicode 与 JavaScript 详解》等文,具体字符的转换可以用站长之家提供的Unicode编码转换工具。

4. 字典的统计和排序

生成字典不难,有这么几个值得记录的点:

  1. 使用“not in”而非“not...in
    更具体地说,if w not in textif not w in text 是等价的,但前一种写法更易读。
    现在 Flake8 会把“not...in”标记出来(E713)并给出修改为“not in”的建议,同理,“is not”和“not...is”也有同样的问题(E714)。PEP 8 对此也有说明。另可参考《Python 中 not 的用法》一文。

  2. 字典、元组、列表
    字典不能直接排序,需要用 item() 函数把字典转成一个由元组构成的列表,然后再用 sorted() 函数排序。
    这里可能出现的波折是混淆了 sort()sorted()。简单说,由字典转成的列表,不能用 sort() 排序,只能用 sorted() 排序。更多的信息可以参考《Python 中列表、元组及字典的排序》、《Python 中方法 sort() 和函数 sorted() 的区别》、《Python 中排序函数 sort() 和 sorted() 的区别》等文。

  3. lambda 表达式
    使用 sorted() 排序会用到 lambda 表达式,这个稍稍有些不易理解。
    lambda 表达式定义的是一个“匿名函数”,是为了简化代码而使用的。
    举例来说,

    1
    lambda x: x[1]

    1
    2
    def g(x):
    return x[1]

    的功能是一样的。
    更进一步说,

    1
    sorted(text, key=lambda x: x[1])

    1
    2
    3
    4
    5
    def g(x):
    return x[1]


    sorted(text, key=g)

    的功能也是一样的。
    显然,在使用 sorted() 的时候,lambda 表达式可以实现更简练的代码。不过,lambda 表达式会在一定程度上降低程序的可读性,所以要慎用。
    什么时候用 lambda 表达式,Goodpy 在《Python lambda 介绍》一文给了些很好的建议:

    • lambda 并不会带来程序运行效率的提高,只会使代码更简洁。

    • 如果可以使用 for...in...if 来完成的,坚决不用 lambda。

    • 如果使用 lambda,lambda 内不要包含循环,如果有,我宁愿定义函数来完成,使代码获得可重用性和更好的可读性。

    另可参考:
    Python: 使用 lambda 应对各种复杂情况的排序,包括 list 嵌套 dict
    Python dict() 函数

5. 切片的使用

切片是从字符串、列表、元组中抽取部分内容的操作。它很常用,不过有些地方会让人有些困惑,记录如下。

首先要明确每一个元素的位置编号,以列表 li = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 为例——

列表元素 ‘A’ ‘B’ ‘C’ ‘D’ ‘E’ ‘F’ ‘G’
正序编号 0 1 2 3 4 5 6
逆序编号 -7 -6 -5 -4 -3 -2 -1

注意正序编号是从 0 开始的,逆序编号是从 -1 开始的。

切片的语法是 list[start:stop:step],即 名称[开始:结束:步伐]。这其中:

  • 步伐可以省略,默认值为“1”;
  • 开始和结束的值可以省略,默认值为列表的两端(不是第一个元素和最后一个元素);
  • 开始和结束中间的冒号不可省略;
  • 步伐既可以为正值,也可以为负值,正值表示正序取值,负值表示逆序取值;
  • 步伐为正值时,开始位置应在结束位置左侧,步伐为负值时,开始位置应在结束位置右侧,否则不会取出任何元素;
  • 开始和结束的值既可以用正序编号,也可以用逆序编号,两种编号可以混用;
  • 开始位置的元素会取出,结束位置的元素不会取出;
  • 所用位置编号可以超出列表的元素位置编号;
  • 所有的冒号两侧都没有空格。

还以列表 li = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 为例——

行号 代码      结果                   注释
1 li[:] ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 从列表左端取到列表右端,即取了整个列表
2 li[::1] ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 从列表左端取到列表右端,步伐为“1”,即取了整个列表
3 li[::2] ['A', 'C', 'E', 'G'] 从列表左端取到列表右端,步伐为“2”,即每 2 个元素取 1 个元素,取出第 0、2、4、6 号元素
4 li[::3] ['A', 'D', 'G'] 从列表左端取到列表右端,步伐为“3”,即每 3 个元素取 1 个元素,取出第 0、3、6 号元素
5 li[::-1] ['G', 'F', 'E', 'D', 'C', 'B', 'A'] 从列表左端取到列表右端,步伐为“-1”,把整个列表逆序
6 li[::-2] ['G', 'E', 'C', 'A'] 从列表右端取到列表左端,步伐为“-2”,取出第 -1、-3、-5、-7 号元素
7 li[2:5] ['C', 'D', 'E'] 从编号为 2 的元素(含)取到编号为 5 的元素(不含),步伐为默认值“1”,取出第 2、3、4 号元素
8 li[2:5:1] ['C', 'D', 'E'] 从编号为 2 的元素(含)取到编号为 5 的元素(不含),步伐为“1”,取出第 2、3、4 号元素
9 li[2:5:-1] [] 从编号为 2 的元素(含)取到编号为 5 的元素(不含),步伐为“-1”,没有取出元素
10 li[5:2:-1] ['F', 'E', 'D'] 从编号为 5 的元素(含)取到编号为 2 的元素(不含),步伐为“-1”,取出第 5、4、3 号元素
11 li[0:6] ['A', 'B', 'C', 'D', 'E', 'F'] 从编号为 0 的元素(含)取到编号为 6 的元素(不含),步伐为默认值“1”,取出第 0、1、2、3、4、5 号元素
12 li[0:-1] ['A', 'B', 'C', 'D', 'E', 'F'] 从编号为 0 的元素(含)取到编号为 -1 的元素(不含),步伐为默认值“1”,取出第 0、1、2、3、4、5 号元素
13 li[0:] ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 从编号为 0 的元素(含)取到列表右端,步伐为默认值“1”,取了整个列表
14 li[0:9] ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 从编号为 0 的元素(含)取到编号为 9 的元素(不含),步伐为默认值“1”,由于最大元素编号为 6,取了整个列表
15 li[9:0:-1] ['G', 'F', 'E', 'D', 'C', 'B'] 从编号为 9 的元素(含)取到编号为 0 的元素(不含),步伐为默认值“-1”,由于最大元素编号为 6,取出第 6、5、4、3、2、1 号元素
16 li[9:-9:-1] ['G', 'F', 'E', 'D', 'C', 'B', 'A'] 从编号为 9 的元素(含)取到编号为 - 的元素(不含),步伐为默认值“-1”,由于最大元素编号为 6,最小元素编号为 -7,取出第 6、5、4、3、2、1、0 号元素
17 li[0::-1] ['A'] 从编号为 0 的元素(含)取到列表左端,步伐为“-1”,取出第 0 号元素
18 li[:0:-1] ['G', 'F', 'E', 'D', 'C', 'B'] 从列表右侧取到编号为 0 的元素(不含),步伐为“-1”,取出第 -1、-2、-3、-4、-5、-6 号元素
19 li[1:-2:-1] [] 从编号为 1 的元素(含)取到编号为 -2 的元素(不含),步伐为“-1”,没有取出元素
20 li[-2:1:-1] ['F', 'E', 'D', 'C'] 从编号为 -2 的元素(含)取到编号为 1 的元素(不含),步伐为“-1”,取出第 -2、-3、-4、-5 号元素
21 li[0:3:-1] [] 从编号为 0 的元素(含)取到编号为 3 的元素(不含),步伐为“-1”,没有取出元素
22 li[:3:-1] ['G', 'F', 'E'] 从列表右端取到编号为 3 的元素(不含),步伐为“-1”,取出第 -1、-2、-3 号元素

字符串和元组的操作与列表类似。

要翻转(逆序)一个名为 li 的列表有两种途径,一种是用 li.reverse(),另一种是用 li[::-1]。但要翻转字符串和元组就只能用切片了。这样来看,切片的方法比较通用。

字典不能切片,不过可以用把字典的关键字转成列表,对列表切片,再用查询重新生成字典的方法实现切片,具体的操作可以参考李阳良的《Python 字典切片》一文。

6. 不同类型的转换

由数字构成的列表不能直接拼接成字符串,得把列表的每个元素都转成字符串才行。

类型转换调用相应的函数就能完成,遍历列表的每个元素可以使用 for...in...if 语句。

Python 里可用的转换函数有这些:

函数           说明
int(x[, base]) x 转换为一个整数,强制类型转换
long(x[, base]) x 转换为一个长整数
float(x) x 转换到一个浮点数,强制类型转换
complex(real[, imag]) 创建一个复数
str(x) 将对象 x 转换为字符串,强制类型转换
repr(x) 将对象 x 转换为表达式字符串
eval(str) 用来计算在字符串中的有效 Python 表达式,并返回一个对象
list(s) 将序列 s 转换为一个列表,强制类型转换
tuple(s) 将序列 s 转换为一个元组,强制类型转换
set(s) 将序列 s 转换为一个集合,强制类型转换
chr(x) 将一个整数转换为一个字符,Python 3.x 可以用其处理 Unicode 字符了
unichr(x) 将一个整数转换为 Unicode 字符,Python 3.x 将其合并进了 chr(x)
ord(str) 将一个字符转换为它的整数值
bin(x) 将一个整数转换为一个二进制字符串
oct(x) 将一个整数转换为一个八进制字符串
hex(x) 将一个整数转换为一个十六进制字符串

这里的 eval(str) 在第 3 天做计算器的时候也提到过。str(x)repr(x) 有联系也有区别,具体可参见叶俊贤的《Python 中 str()repr() 函数的区别》一文。还有一些数据类型转换的细节,可以参考范桂飓的《Python基本语法——强制数据类型转换》一文和 Davidham 的《Python 中的强制类型转换》一文。

其他参考资料:
Python进阶》(Intermediate Python),穆罕默德·耶苏布·乌拉·哈立德(Muhammad Yasoob Ullah Khalid)著,刘宇(@liuyu),老高(@spawnris),大牙码特(@suqi),明源(@muxueqz)等译
给非 Python 开发的 Python 快速自学资源整理

总结

  • 巩固了一些英文标点的用法
  • 字符串内容的查找、替换、删除
  • 字符串的大小写转换
  • 英文单词词频统计与输出
  • 数组的基础操作
  • 不同进制数字的转换