学员信息
- 学号:1901100244
- 操作系统:Windows 10 version 1903 (64-bit)
- 学习内容:字符串的基本处理,词频统计,数组操作,进制转换
- 学习用时:6 小时
收获总结
- 了解了字符串相关的替换、删除、大小写转换等工作的方法;
- 了解了由字符串分词生成列表的方法;
- 了解了列表的排序方法;
- 了解了字典的生成和排序方法;
- 明确了列表、元组、字典的联系和区别;
- 了解了切片的使用方法;
- 了解了数字类型和字符串类型的转换方法;
- 了解了由列表拼接字符串的方法;
- 了解了十进制数到二进制数、八进制数、十六进制数的转换方法。
遇到的难点与问题
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 多页的大部头《精通正则表达式》和余晟撰写的《正则指引》,还可以通过使用 Regester 和 RegexBuddy 等软件进行练习和测试。
无论如何,正则表达式的语法都很难读写,令人头疼,可为了它能实现的功能,只能认了。
上文已经说过,在 Python 里使用正则表达式需要加载正则表达式模块(re
),不过在我用的时候遭遇了这么几个问题:
- 后向引用
我认为包含撇号(’)的单词(如 ’s、’re、don’t 等),应该单独特别处理,而非简单地和它周围的词混在一起。举例来说,“Let’s”应该断成“Let”和“’s”,“don’t”就应该是一个整体。
好在要处理的文本只有 3 个撇号,都是断开就行的,我就简化处理了——在撇号之前加空格把它和关联单词分开。
完成这个任务需要使用“后向引用”,其默认的表达方式应该是类似“\1
”、“\2
”这样的,但我调了数次都报错,又作了一番功课才明白在这里应该用“\\1
”、“\\2
”。
捎带要提一下的是,VS Code 里的查找/替换功能也可以用正则表达式,但它的后向引用要写成“$1
”、“$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”部分。 - 特殊字符
正则表达式里的“\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. 字典的统计和排序
生成字典不难,有这么几个值得记录的点:
使用“
not in
”而非“not...in
”
更具体地说,if w not in text
和if not w in text
是等价的,但前一种写法更易读。
现在 Flake8 会把“not...in
”标记出来(E713)并给出修改为“not in
”的建议,同理,“is not
”和“not...is
”也有同样的问题(E714)。PEP 8 对此也有说明。另可参考《Python 中 not 的用法》一文。字典、元组、列表
字典不能直接排序,需要用item()
函数把字典转成一个由元组构成的列表,然后再用sorted()
函数排序。
这里可能出现的波折是混淆了sort()
和sorted()
。简单说,由字典转成的列表,不能用sort()
排序,只能用sorted()
排序。更多的信息可以参考《Python 中列表、元组及字典的排序》、《Python 中方法 sort() 和函数 sorted() 的区别》、《Python 中排序函数 sort() 和 sorted() 的区别》等文。lambda 表达式
使用sorted()
排序会用到 lambda 表达式,这个稍稍有些不易理解。
lambda 表达式定义的是一个“匿名函数”,是为了简化代码而使用的。
举例来说,1
lambda x: x[1]
和
1
2def g(x):
return x[1]的功能是一样的。
更进一步说,1
sorted(text, key=lambda x: x[1])
和
1
2
3
4
5def 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
Pythondict()
函数
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 快速自学资源整理
总结
- 巩固了一些英文标点的用法
- 字符串内容的查找、替换、删除
- 字符串的大小写转换
- 英文单词词频统计与输出
- 数组的基础操作
- 不同进制数字的转换