#### Python编年史
下面是 Python 语言发展过程中的一些重要时间点:
1. 1989年12月:吉多·范罗苏姆决心开发一个新的脚本语言及其解释器来打发无聊的圣诞节,新语言将作为 ABC 语言的继承者,主要用来替代 Unix shell 和 C 语言实现系统管理。由于吉多本人是 BBC 电视剧《*Monty Python's Flying Circus*》的忠实粉丝,所以他选择了 Python 这个词作为新语言的名字。
2. 1991年02月:吉多·范罗苏姆在 alt.sources 新闻组上发布了 Python 解释器的最初代码,标记为版本0.9.0。
3. 1994年01月:Python 1.0发布,梦开始的地方。
4. 2000年10月:Python 2.0发布,Python 的整个开发过程更加透明,生态圈开始慢慢形成。
5. 2008年12月:Python 3.0发布,引入了诸多现代编程语言的新特性,但并不完全向下兼容。
6. 2011年04月:pip 首次发布,Python 语言有了自己的包管理工具。
7. 2018年07月:吉多·范罗苏姆宣布从“终身仁慈独裁者”(开源项目社区出现争议时拥有最终决定权的人)的职位上“永久休假”。
8. 2020年01月:在 Python 2和 Python 3共存了11年之后,官方停止了对 Python 2的更新和维护,希望用户尽快切换到 Python 3。
9. 目前:Python 在大模型(GPT-3、GPT-4、BERT等)、计算机视觉(图像识别、目标检测、图像生成等)、智能推荐(YouTube、Netflix、字节跳动等)、自动驾驶(Waymo、Apollo等)、语音识别、数据科学、量化交易、自动化测试、自动化运维等领域都得到了广泛的应用,Python 语言的生态圈也是相当繁荣。
> **说明**:大多数软件的版本号一般分为三段,形如A.B.C,其中A表示大版本号,当软件整体重写升级或出现不向后兼容的改变时,才会增加A;B表示功能更新,出现新功能时增加B;C表示小的改动(例如:修复了某个Bug),只要有修改就增加C。
#### Python优缺点
Python 语言的优点很多,简单为大家列出几点。
1. **简单优雅**,跟其他很多编程语言相比,Python **更容易上手**。
2. 能用更少的代码做更多的事情,**提升开发效率**。
3. 开放源代码,拥有**强大的社区和生态圈**。
4. **能够做的事情非常多**,有极强的适应性。
5. **胶水语言**,能够黏合其他语言开发的东西。
6. 解释型语言,更容易**跨平台**,能够在多种操作系统上运行。
Python 最主要的缺点是**执行效率低**(解释型语言的通病),如果更看重代码的执行效率,C、C++ 或 Go 可能是你更好的选择。
### 安装Python环境
工欲善其事,必先利其器。想要开始你的 Python 编程之旅,首先得在计算机上安装 Python 环境,简单的说就是安装运行 Python 程序需要的 Python 解释器。我们推荐大家安装官方的 Python 3 解释器,它是用 C 语言编写的,我们通常也称之为 CPython,它可能是你目前最好的选择。首先,我们需要从官方网站的[下载页面](https://www.python.org/downloads/)找到下载链接,点击“Download”按钮进入下载页面后,需要根据自己的操作系统选择合适的 Python 3安装程序,如下图所示。
进入下载页面后,有些 Python 版本并没有提供 Windows 和 macOS 系统的安装程序,只提供了源代码的压缩文件,对于熟悉 Linux 系统的小伙伴,我们可以通过源代码构建安装;对于使用 Windows 或 macOS 系统的小伙伴,我们还是**强烈建议**使用安装程序。例如,你想安装 Python 3.10,选择 Python 3.10.10 或 Python 3.10.11 就能找到 Windows 或 macOS 系统的安装包,而其他版本可能只有源代码,如下图所示。
#### Windows环境
下面我们以 Windows 11为例,讲解如何在 Windows 操作系统上安装 Python 环境。双击运行从官网下载的安装程序,会打开一个安装向导,如下图所示。
首先,一定要记得勾选“Add python.exe to PATH”选项,它会帮助我们将 Python 解释器添加到 Windows 系统的 PATH 环境变量中(不理解没关系,勾上就对了);其次,“Use admin privileges when installing py.exe”是为了在安装过程中获得管理员权限,建议勾选。然后,我们选择“Customize Installation”,使用自定义安装的模式,这是专业人士的选择,而你就(假装)是那个专业人士,不建议使用“Install Now”(默认安装)。
接下来,安装向导会提示你勾选需要的“Optional Features”(可选特性),这里咱们可以直接全选。值得一提的是其中的第2项,它是 Python 的包管理工具 pip,可以帮助我们安装三方库和三方工具,所以一定要记得勾选它,然后点击“Next”进入下一环节。
接下来是对“Advanced Options”(高级选项)的选择,这里我们建议大家只勾选“Add Python to environment variables”和“Precompile standard library”这两个选项,前者会帮助我们自动配置好环境变量,后者会预编译标准库(生成`.pyc`文件),这样在使用时就无需临时编译了。还是那句话,不理解没关系,勾上就对了。下面的“Customize install location”(自定义安装路径)**强烈建议**修改为自定义的路径,这个路径中不应该包含中文、空格或其他特殊字符,注意这一点会为你将来减少很多不必要的麻烦。设置完成后,点击“Install”开始安装。
安装成功会出现如下图所示的画面,安装成功的关键词是“successful”,如果安装失败,这里的单词会变成“failed”。
安装完成后可以打开 Windows 的“命令行提示符”或 PowerShell,然后输入`python --version`或`python -V`来检查安装是否成功,这个命令是查看 Python 解释器的版本号。如果看到如下所示的画面,那么恭喜你,Python 环境已经安装成功了。这里我们建议再检查一下 Python 的包管理工具 pip 是否可用,对应的命令是`pip --version`或`pip -V`。
> **说明**:如果安装过程报错或提示安装失败,很有可能是你的 Windows 系统缺失了一些动态链接库文件或缺少必要的构建工具导致的。可以在[微软官网](https://visualstudio.microsoft.com/zh-hans/downloads/)下载“Visual Studio 2022 生成工具”进行修复,如下图所示。如果不方便在微软官网下载的,也可以使用下面的百度云盘链接来获取修复工具,链接: https://pan.baidu.com/s/1iNDnU5UVdDX5sKFqsiDg5Q 提取码: cjs3。
>
>
>
> 上面下载的“Visual Studio 2022 生成工具”需要联网才能运行,运行后会出现如下图所示的画面,大家可以参考下图勾选对应的选项进行修复。修复过程需要联网下载对应的软件包,这个过程可能会比较耗时间,修复成功后可能会要求重启你的操作系统。
>
>
#### macOS环境
macOS 安装 Python 环境相较于 Windows 系统更为简单,我们从官方下载的安装包是一个`pkg`文件,双击运行之后不断的点击“继续”就安装成功了,几乎不用做任何的设置和勾选,如下图所示。
安装完成后,可以在 macOS 的“终端”工具中输入`python3 --version`命令来检查是否安装成功,注意这里的命令是`python3`不是`python`!!!然后我们再检查一下包管理工具,输入命令`pip3 --version`,如下图所示。
#### 其他安装方式
有人可能会推荐新手直接安装 [Anaconda](https://www.anaconda.com/download/success),因为 Anaconda 会帮助我们安装 Python 解释器以及一些常用的三方库,除此之外还提供了一些便捷的工具,特别适合萌新小白。我个人并不推荐这种方式,因为在安装 Anaconda 时你会莫名其妙安装了一大堆有用没用的三方库(占用比较多的硬盘空间),然后你的终端或命令提示符会被 Anaconda 篡改(每次启动自动激活虚拟环境),这些并不符合软件设计的**最小惊讶原则**。其他关于 Anaconda 的小毛病此处就不再赘述了,如果你非要使用 Anaconda,推荐安装 Miniconda,它跟 Anaconda 在同一个下载页面。
还有萌新小白经常会听到或说出,“我要写 Python 程序,安装一个 PyCharm 不就可以了吗?”。这里简单科普一下,PyCharm 只是一个辅助写 Python 代码的工具,它本身并不具备运行 Python 代码的能力,运行 Python 代码靠的是我们上面安装的 Python 解释器。当然,有些 PyCharm 版本在创建 Python 项目时,如果检测不到你电脑上的 Python 环境,也会提示你联网下载 Python 解释器。PyCharm 的安装和使用我们放在了下一课。
### 总结
总结一下我们学到的东西:
1. Python 语言很强大,可以做很多的事情,所以值得我们去学习。
2. 要使用 Python语言,首先得安装 Python 环境,也就是运行 Python 程序所需的 Python 解释器。
3. Windows 系统可以在命令提示符或 PowerShell 中输入`python --version`检查 Python 环境是否安装成功;macOS 系统可以在终端中输入`python3 --version`进行检查。
================================================
FILE: Day01-20/02.第一个Python程序.md
================================================
## 第一个Python程序
在上一课中,我们对 Python 语言的过去现在有了一些了解,我们准备好了运行 Python 程序所需要的解释器环境。相信大家已经迫不及待的想开始自己的 Python 编程之旅了,但是新问题来了,我们应该在什么地方书写 Python 程序,然后又怎么运行它呢?
### 编写代码的工具
下面我们为大家讲解几种可以编写和运行 Python 代码的工具,大家可以根据自己的需求来选择合适的工具。当然,对于初学者,我个人比较推荐使用 PyCharm,因为它不需要太多的配置也非常的强大,对新手还是很友好的。如果你也听说过或者喜欢 PyCharm,可以直接跳过下面对其他工具的介绍,直接快进到讲解 PyCharm 的地方。
#### 默认的交互式环境
我们打开 Windows 的“命令提示符”或“PowerShell”工具,输入`python`然后按下`Enter`键,这个命令会把我们带到一个交互式环境中。所谓交互式环境,就是我们输入一行代码并按下`Enter`键,代码马上会被执行,如果代码有产出结果,那么结果会被显示在窗口中,如下所示。
```Bash
Python 3.10.10
Type "help", "copyright", "credits" or "license" for more information.
>>> 2 * 3
6
>>> 2 + 3
5
>>>
```
> **说明**:使用 macOS 系统的用户需要打开“终端”工具,输入`python3`进入交互式环境。
如果希望退出交互式环境,可以在交互式环境中输入`quit()`,如下所示。
```Bash
>>> quit()
```
#### 更好的交互式环境 - IPython
上面说的交互式环境用户体验并不怎么好,大家使用一下就能感受到。我们可以用 IPython 来替换掉它,因为 IPython 提供了更为强大的编辑和交互功能。我们可以在命令提示符或终端中使用 Python 的包管理工具`pip`来安装 IPython,如下所示。
```bash
pip install ipython
```
> **提示**:在使用上面的命令安装 IPython 之前,可以先通过`pip config set global.index-url https://pypi.doubanio.com/simple`命令或`pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple/`将下载源修改为国内的豆瓣镜像或清华镜像,否则下载安装的过程可能会非常的缓慢。
接下来可以使用下面的命令启动 IPython,进入交互式环境。
```bash
ipython
```
> **说明**:还有一个网页版的 IPython 名叫 Jupyter,我们在用得着它的地方再为大家介绍。
#### 文本编辑神器 - Visual Studio Code
Visual Studio Code 是由微软开发能够在 Windows、 Linux 和 macOS 等操作系统上运行的代码编辑神器。它支持语法高亮、自动补全、多点编辑、运行调试等一系列便捷功能,而且能够支持多种编程语言。如果大家要选择一款高级文本编辑工具,强烈推荐 Visual Studio Code,关于它的[下载](https://code.visualstudio.com/)、安装和使用,有兴趣的读者可以自行研究。
#### 集成开发环境 - PyCharm
如果用 Python 语言开发商业项目,我们推荐大家使用更为专业的工具 PyCharm。PyCharm 是由捷克一家名为 [JetBrains](https://www.jetbrains.com/) 的公司针对 Python 语言提供的集成开发环境(IDE)。所谓集成开发环境,通常是指提供了编写代码、运行代码、调试代码、分析代码、版本控制等一系列强大功能和便捷操作的开发工具,因此特别适合用于商业项目的开发。我们可以在 JetBrains 公司的官方网站上找到 PyCharm 的[下载链接](
官方提供了两个 PyCharm 的版本,一个是免费的社区版(Community Edition),功能相对弱小,但对于初学者来说是完全够用的;另一个是付费的专业版(Professional Edition),功能非常强大,但需要按年或按月支付费用,新用户可以免费试用30天时间。PyCharm 的安装没有任何难度,运行下载的安装程序,几乎全部使用默认设置进行安装就可以了。对于使用 Windows 系统的小伙伴,其中有一个步骤可以按照下图所示勾选“创建桌面快捷方式”和“在右键菜单中添加"Open Folder as Project"”就可以了。
第一次运行 PyCharm 时,在提示你导入 PyCharm 设置的界面上直接选择“Do not import settings”,然后我们就可以看到如下图所示的欢迎界面。此处,我们可以先点击“Customize”选项对 PyCharm 做一些个性化的设置。
接下来,我们可以在“Projects”选项中点击“New Project”来创建一个新的项目,此处还可以“打开已有项目”或“从版本控制服务器(VCS)获取项目”,如下图所示。
创建项目的时候需要指定项目的路径并创建”虚拟环境“,我们建议每个 Python 都在自己专属的虚拟环境中运行。如果你的系统上还没 Python 环境,那么 PyCharm 会提供官网的下载链接,当你点击“Create”按钮创建项目时,它会联网下载 Python 解释器,如下图所示。
当然,我们并不推荐这么做,因为我们在上一课已经安装过 Python 环境了。在系统有 Python 环境的情况下,PyCharm 通常会自动发现 Python 解释器的位置并以此为基础创建虚拟环境,所以大家看到的画面应该如下图所示。
> **说明**:上面的截图来自于 Windows 系统,如果使用 macOS 系统,你看到的项目路径和 Python 解释器路径会跟上面有所不同。
创建好项目后会出现如下图所示的画面,我们可以通过在项目文件夹上点击鼠标右键,选择“New”菜单下的“Python File”来创建一个 Python 文件,在给文件命名时建议使用英文字母和下划线的组合,创建好的 Python 文件会自动打开,进入可编辑的状态。
接下来,我们可以在代码窗口编写我们的 Python 代码。写好代码后,可以在窗口中点击鼠标右键,选择“Run”菜单项来运行代码,下面的“Run”窗口会显示代码的执行结果,如下图所示。
到这里,我们的第一个 Python 程序已经运转起来了,很酷吧!对了,PyCharm 有一个叫“每日小贴士”的弹窗,会教给你一些使用 PyCharm 的小技巧,如下图所示。如果不需要,直接关闭就可以了;如果不希望它再次出现,在关闭前可以勾选“Don't show tips on startup”。
### 你好世界
按照行业惯例,我们学习任何一门编程语言写的第一个程序都是输出`hello, world`,因为这段代码是伟大的丹尼斯·里奇(C 语言之父,和肯·汤普森一起开发了 Unix 操作系统)和布莱恩·柯尼汉(awk 语言的发明者)在他们的不朽著作《*The C Programming Language*》中写的第一段代码,下面是对应的 Python 语言的版本。
```python
print('hello, world')
```
> **注意**:上面代码中的圆括号、单引号都是在英文输入法状态下输入的,如果不小心写成了中文的圆括号或单引号,运行代码时会出现`SyntaxError: invalid character '(' (U+FF08)`或`SyntaxError: invalid character '‘' (U+2018)`这样的错误提示。
上面的代码只有一个语句,在这个语句中,我们用到了一个名为`print`的函数,它可以帮助我们输出指定的内容;`print`函数圆括号中的`'hello, world'`是一个字符串,它代表了一段文本内容;在 Python 语言中,我们可以用单引号或双引号来表示一个字符串。不同于 C、C++ 或 Java 这样的编程语言,Python 代码中的语句不需要用分号来表示结束,也就是说,如果我们想再写一条语句,只需要回车换行即可,代码如下所示。此外,Python 代码也不需要通过编写名为`main`的入口函数来使其运行,提供入口函数是编写可执行的 C、C++ 或 Java 代码必须要做的事情,这一点很多程序员都不陌生,但是在 Python 语言中它并不是必要的。
```python
print('hello, world')
print('goodbye, world')
```
如果不使用 PyCharm 这样的集成开发环境,我们也可以直接调用 Python 解释器来运行 Python 程序。我们可以将上面的代码保存成一个名为`example01.py`的文件,对于Windows 系统,我们假设该文件在`C:\code`目录下,我们打开“命令提示符”或“PowerShell”并输入下面的命令就可以运行它。
```powershell
python C:\code\example01.py
```
对于 macOS 系统,假设我们的文件在`/Users/Hao`目录下,那么可以在终端中输入下面的命令来运行程序。
```Bash
python3 /Users/Hao/example01.py
```
> **提示**:如果路径比较长,不愿意手动输入,我们可以通过拖拽的方式将文件直接拖到“命令提示符”或“终端”中,这样会自动输入完整的文件路径。
大家可以试着修改上面的代码,比如将单引号中的`hello, world`换成其他内容或者多写几个这样的语句,看看会运行出怎样的结果。需要提醒大家的是,写 Python 代码时,最好每一行只写一条语句。虽然,我们可以使用`;`作为分隔将多个语句写在一行中,但是这样做会让代码变得非常难看,不再具备良好的可读性。
### 注释你的代码
注释是编程语言的一个重要组成部分,用于在代码中解释代码的作用,从而达到增强代码可读性的目标。当然,我们也可以将代码中暂时不需要运行的代码段通过添加注释来去掉,这样当你需要重新使用这些代码的时候,去掉注释符号就可以了。简单的说,**注释会让代码更容易看懂但不会影响代码的执行结果**。
Python 中有两种形式的注释:
1. 单行注释:以`#`和空格开头,可以注释掉从`#`开始后面一整行的内容。
2. 多行注释:三个引号(通常用双引号)开头,三个引号结尾,通常用于添加多行说明性内容。
```python
"""
第一个Python程序 - hello, world
Version: 1.0
Author: 骆昊
"""
# print('hello, world')
print("你好,世界!")
```
### 总结
到此,我们已经把第一个 Python 程序运行起来了,是不是很有成就感?!只要你坚持学习下去,再过一段时间,我们就可以用 Python 语言做更多更酷的事情。今时今日,编程就跟英语一样,对很多人来说都是一项必须要掌握的技能。
================================================
FILE: Day01-20/03.Python语言中的变量.md
================================================
## Python语言中的变量
对于想学习编程的新手来说,有两个问题可能是他们很想知道的,其一是“什么是(计算机)程序”,其二是“写(计算机)程序能做什么”。先说说我对这两个问题的理解:**程序是数据和指令的有序集合**,**写程序就是用数据和指令控制计算机做我们想让它做的事情**。今时今日,为什么有那么多人选择用 Python 语言来写程序,因为 Python 语言足够简单和强大。相较于 C、C++、Java 这样的编程语言,Python 对初学者和非专业人士更加友好,很多问题在 Python 语言中都能找到简单优雅的解决方案。接下来,我们就从最基础的语言元素开始,带大家认识和使用 Python 语言。
### 一些常识
在开始系统的学习 Python 编程之前,我们先来科普一些计算机的基础知识。计算机的硬件系统通常由五大部件构成,包括:**运算器**、**控制器**、**存储器**、**输入设备**和**输出设备**。其中,运算器和控制器放在一起就是我们常说的**中央处理器**(CPU),它的功能是执行各种运算和控制指令。刚才我们提到过,程序是指令的集合,写程序就是将一系列的指令按照某种方式组织到一起,然后通过这些指令去控制计算机做我们想让它做的事情。存储器可以分为**内部存储器**和**外部存储器**,前者就是我们常说的内存,它是中央处理器可以直接寻址的存储空间,程序在执行的过程中,对应的数据和指令需要加载到内存中。输入设备和输出设备经常被统称为 I/O 设备,键盘、鼠标、麦克风、摄像头是典型的输入设备,而显示器、打印机、扬声器等则是典型的输出设备。目前,我们使用的计算机基本大多是遵循“冯·诺依曼体系结构”的计算机,这种计算机有两个关键点:一是**将存储器与中央处理器分开**;二是**将数据以二进制方式编码**。
二进制是一种“逢二进一”的计数法,跟人类使用的“逢十进一”的计数法本质是一样的。人类因为有十根手指,所以使用了十进制计数法,在计数时十根手指用完之后,就只能用进位的方式来表示更大的数值。当然凡事都有例外,玛雅人可能是因为长年光着脚的原因,把脚趾头也都用上了,于是他们使用了二十进制的计数法。基于这样的计数方式,玛雅人使用的历法跟我们平常使用的历法就产生了差异。按照玛雅人的历法,2012 年是上一个所谓的“太阳纪”的最后一年,而 2013 年则是新的“太阳纪”的开始。后来这件事情还被以讹传讹的方式误传为“2012 年是玛雅人预言的世界末日”的荒诞说法。今天有很多人猜测,玛雅文明之所以发展缓慢跟使用了二十进制是有关系的。对于计算机来说,二进制在物理器件上最容易实现的,因为可以用高电压表示 1,用低电压表示 0。不是所有写程序的人都需要熟悉二进制,熟悉十进制与二进制、八进制、十六进制的转换,大多数时候我们即便不了解这些知识也能写程序。但是,我们必须知道,计算机是使用二进制计数的,不管什么样的数据,到了计算机内存中都是以二进制形态存在的。
> **说明**:关于二进制计数法以及它与其他进制如何相互转换,大家可以翻翻名为《计算机导论》或《计算机文化》的书,都能找到相应的知识,此处就不再进行赘述了,不清楚的读者可以自行研究。
### 变量和类型
要想在计算机的内存中保存数据,首先得说一说变量这个概念。在编程语言中,**变量是数据的载体**,简单的说就是一块用来保存数据的内存空间,**变量的值可以被读取和修改**,这是所有运算和控制的基础。计算机能处理的数据有很多种类型,最常见的就是数值,除了数值之外还有文本、图像、音频、视频等各种各样的数据类型。虽然数据在计算机中都是以二进制形态存在的,但是我们可以用不同类型的变量来表示数据类型的差异。Python 语言中预设了多种数据类型,也允许我们自定义新的数据类型,这一点在后面会讲到。我们首先来了解几种 Python 中最为常用的数据类型。
1. 整型(`int`):Python 中可以处理任意大小的整数,而且支持二进制(如`0b100`,换算成十进制是4)、八进制(如`0o100`,换算成十进制是64)、十进制(`100`)和十六进制(`0x100`,换算成十进制是256)的表示法。运行下面的代码,看看会输出什么。
```python
print(0b100) # 二进制整数
print(0o100) # 八进制整数
print(100) # 十进制整数
print(0x100) # 十六进制整数
```
2. 浮点型(`float`):浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,浮点数除了数学写法(如`123.456`)之外还支持科学计数法(如`1.23456e2`,表示$\small{1.23456 \times 10^{2}}$)。运行下面的代码,看看会输出什么。
```python
print(123.456) # 数学写法
print(1.23456e2) # 科学计数法
```
3. 字符串型(`str`):字符串是以单引号或双引号包裹起来的任意文本,比如`'hello'`和`"hello"`。
4. 布尔型(`bool`):布尔型只有`True`、`False`两种值,要么是`True`,要么是`False`,可以用来表示现实世界中的“是”和“否”,命题的“真”和“假”,状况的“好”与“坏”,水平的“高”与“低”等等。如果一个变量的值只有两种状态,我们就可以使用布尔型。
### 变量命名
对于每个变量,我们都需要给它取一个名字,就如同我们每个人都有自己的名字一样。在 Python 中,变量命名需要遵循以下的规则和惯例。
- 规则部分:
- 规则1:变量名由**字母**、**数字**和**下划线**构成,数字不能开头。需要说明的是,这里说的字母指的是 Unicode 字符,Unicode 称为万国码,囊括了世界上大部分的文字系统,这也就意味着中文、日文、希腊字母等都可以作为变量名中的字符,但是一些特殊字符(如:`!`、`@`、`#`等)是不能出现在变量名中的。我们强烈建议大家把这里说的字母理解为**尽可能只使用英文字母**。
- 规则2:Python 是**大小写敏感**的编程语言,简单的说就是大写的`A`和小写的`a`是两个不同的变量,这一条其实并不算规则,而是需要大家注意的地方。
- 规则3:变量名**不要跟 Python 的关键字重名**,**尽可能避开 Python 的保留字**。这里的关键字是指在 Python 程序中有特殊含义的单词(如:`is`、`if`、`else`、`for`、`while`、`True`、`False`等),保留字主要指 Python 语言内置函数、内置模块等的名字(如:`int`、`print`、`input`、`str`、`math`、`os`等)。
- 惯例部分:
- 惯例1:变量名通常使用**小写英文字母**,**多个单词用下划线进行连接**。
- 惯例2:受保护的变量用单个下划线开头。
- 惯例3:私有的变量用两个下划线开头。
惯例2和惯例3大家暂时不用管,讲到后面自然会明白的。当然,作为一个专业的程序员,给变量命名时做到**见名知意**也是非常重要,这彰显了一个程序员的专业气质,很多开发岗位的面试也非常看重这一点。
### 变量的使用
下面通过例子来说明变量的类型和变量的使用。
```python
"""
使用变量保存数据并进行加减乘除运算
Version: 1.0
Author: 骆昊
"""
a = 45 # 定义变量a,赋值45
b = 12 # 定义变量b,赋值12
print(a, b) # 45 12
print(a + b) # 57
print(a - b) # 33
print(a * b) # 540
print(a / b) # 3.75
```
在 Python 中可以使用`type`函数对变量的类型进行检查。程序设计中函数的概念跟数学上函数的概念非常类似,数学上的函数相信大家并不陌生,它包括了函数名、自变量和因变量。如果暂时不理解函数这个概念也不要紧,我们会在后续的内容中专门讲解函数的定义和使用。
```python
"""
使用type函数检查变量的类型
Version: 1.0
Author: 骆昊
"""
a = 100
b = 123.45
c = 'hello, world'
d = True
print(type(a)) #
下面,我们用`for-in`循环实现从 1 到 100 的整数求和,即 $\small{\sum_{n=1}^{100}{n}}$ 。
```python
"""
从1到100的整数求和
Version: 1.0
Author: 骆昊
"""
total = 0
for i in range(1, 101):
total += i
print(total)
```
上面的代码中,变量`total`的作用是保存累加的结果。在循环的过程中,循环变量`i`的值会从 1 一直取到 100。对于变量`i`的每个取值,我们都执行了`total += i`,它相当于`total = total + i`,这条语句实现了累加操作。所以,当循环结束,我们输出变量`total` 的值,它的值就是从 1 累加到 100 的结果 5050。注意,`print(total)`这条语句前是没有缩进的,它不受`for-in`循环的控制,不会重复执行。
我们再来写一个从1到100偶数求和的代码,如下所示。
```python
"""
从1到100的偶数求和
Version: 1.0
Author: 骆昊
"""
total = 0
for i in range(1, 101):
if i % 2 == 0:
total += i
print(total)
```
> **说明**:上面的`for-in`循环中我们使用了分支结构来判断循环变量`i`是不是偶数。
我们也可以修改`range`函数的参数,将起始值和跨度修改为`2`,用更为简单的代码实现从 1 到 100 的偶数求和。
```python
"""
从1到100的偶数求和
Version: 1.1
Author: 骆昊
"""
total = 0
for i in range(2, 101, 2):
total += i
print(total)
```
当然, 更为简单的办法是使用 Python 内置的`sum`函数求和,这样我们连循环结构都省掉了。
```python
"""
从1到100的偶数求和
Version: 1.2
Author: 骆昊
"""
print(sum(range(2, 101, 2)))
```
### while循环
如果要构造循环结构但是又不能确定循环重复的次数,我们推荐使用`while`循环。`while`循环通过布尔值或能产生布尔值的表达式来控制循环,当布尔值或表达式的值为`True`时,循环体(`while`语句下方保持相同缩进的代码块)中的语句就会被重复执行,当表达式的值为`False`时,结束循环。
下面我们用`while`循环来实现从 1 到 100 的整数求和,代码如下所示。
```python
"""
从1到100的整数求和
Version: 1.1
Author: 骆昊
"""
total = 0
i = 1
while i <= 100:
total += i
i += 1
print(total)
```
相较于`for-in`循环,上面的代码我们在循环开始前增加了一个变量`i`,我们使用这个变量来控制循环,所以`while`后面给出了`i <= 100`的条件。在`while`的循环体中,我们除了做累加,还需要让变量`i`的值递增,所以我们添加了`i += 1`这条语句,这样`i`的值就会依次取到1、2、3、……,直到 101。当`i`变成 101 时,`while`循环的条件不再成立,代码会离开`while`循环,此时我们输出变量`total`的值,它就是从 1 到 100 求和的结果 5050。
如果要实现从 1 到 100 的偶数求和,我们可以对上面的代码稍作修改。
```python
"""
从1到100的偶数求和
Version: 1.3
Author: 骆昊
"""
total = 0
i = 2
while i <= 100:
total += i
i += 2
print(total)
```
### break和continue
如果把`while`循环的条件设置为`True`,即让条件恒成立会怎么样呢?我们看看下面的代码,还是使用`while`构造循环结构,计算 1 到 100 的偶数和。
```python
"""
从1到100的偶数求和
Version: 1.4
Author: 骆昊
"""
total = 0
i = 2
while True:
total += i
i += 2
if i > 100:
break
print(total)
```
上面的代码中使用`while True`构造了一个条件恒成立的循环,也就意味着如果不做特殊处理,循环是不会结束的,这就是我们常说的“死循环”。为了在`i`的值超过 100 后让循环停下来,我们使用了`break`关键字,它的作用是终止循环结构的执行。需要注意的是,`break`只能终止它所在的那个循环,这一点在使用嵌套循环结构时需要引起注意,后面我们会讲到什么是嵌套的循环结构。除了`break`之外,还有另一个在循环结构中可以使用的关键字`continue`,它可以用来放弃本次循环后续的代码直接让循环进入下一轮,代码如下所示。
```python
"""
从1到100的偶数求和
Version: 1.5
Author: 骆昊
"""
total = 0
for i in range(1, 101):
if i % 2 != 0:
continue
total += i
print(total)
```
> **说明**:上面的代码使用`continue`关键字跳过了`i`是奇数的情况,只有在`i`是偶数的前提下,才会执行到`total += i`。
### 嵌套的循环结构
和分支结构一样,循环结构也是可以嵌套的,也就是说在循环结构中还可以构造循环结构。下面的例子演示了如何通过嵌套的循环来输出一个乘法口诀表(九九表)。
```python
"""
打印乘法口诀表
Version: 1.0
Author: 骆昊
"""
for i in range(1, 10):
for j in range(1, i + 1):
print(f'{i}×{j}={i * j}', end='\t')
print()
```
上面的代码中,`for-in`循环的循环体中又用到了`for-in`循环,外面的循环用来控制产生`i`行的输出,而里面的循环则用来控制在一行中输出`j`列。显然,里面的`for-in`循环的输出就是乘法口诀表中的一整行。所以在里面的循环完成时,我们用了一个`print()`来实现换行的效果,让下面的输出重新另起一行,最后的输出如下所示。
```
1×1=1
2×1=2 2×2=4
3×1=3 3×2=6 3×3=9
4×1=4 4×2=8 4×3=12 4×4=16
5×1=5 5×2=10 5×3=15 5×4=20 5×5=25
6×1=6 6×2=12 6×3=18 6×4=24 6×5=30 6×6=36
7×1=7 7×2=14 7×3=21 7×4=28 7×5=35 7×6=42 7×7=49
8×1=8 8×2=16 8×3=24 8×4=32 8×5=40 8×6=48 8×7=56 8×8=64
9×1=9 9×2=18 9×3=27 9×4=36 9×5=45 9×6=54 9×7=63 9×8=72 9×9=81
```
### 循环结构的应用
#### 例子1:判断素数
要求:输入一个大于 1 的正整数,判断它是不是素数。
> **提示**:素数指的是只能被 1 和自身整除的大于 1 的整数。例如对于正整数 $\small{n}$,我们可以通过在 2 到 $\small{n - 1}$ 之间寻找有没有 $\small{n}$ 的因子,来判断它到底是不是一个素数。当然,循环不用从 2 开始到 $\small{n - 1}$ 结束,因为对于大于 1 的正整数,因子应该都是成对出现的,所以循环到 $\small{\sqrt{n}}$ 就可以结束了。
```python
"""
输入一个大于1的正整数判断它是不是素数
Version: 1.0
Author: 骆昊
"""
num = int(input('请输入一个正整数: '))
end = int(num ** 0.5)
is_prime = True
for i in range(2, end + 1):
if num % i == 0:
is_prime = False
break
if is_prime:
print(f'{num}是素数')
else:
print(f'{num}不是素数')
```
> **说明**:上面的代码中我们用了布尔型的变量`is_prime`,我们先将它赋值为`True`,假设`num`是一个素数;接下来,我们在 2 到`num ** 0.5`的范围寻找`num`的因子,如果找到了`num`的因子,那么它一定不是素数,此时我们将`is_prime`赋值为`False`,同时使用`break`关键字终止循环结构;最后,我们根据`is_prime`的值是`True`还是`False`来给出不同的输出。
#### 例子2:最大公约数
要求:输入两个大于 0 的正整数,求两个数的最大公约数。
> **提示**:两个数的最大公约数是两个数的公共因子中最大的那个数。
```python
"""
输入两个正整数求它们的最大公约数
Version: 1.0
Author: 骆昊
"""
x = int(input('x = '))
y = int(input('y = '))
for i in range(x, 0, -1):
if x % i == 0 and y % i == 0:
print(f'最大公约数: {i}')
break
```
> **说明**:上面代码中`for-in`循环的循环变量值是从大到小的,这样我们找到的能够同时整除`x`和`y`的因子`i`,就是`x`和`y`的最大公约数,此时我们用`break`终止循环。如果`x`和`y`互质,那么循环会执行到`i`变成 1,因为 1 是所有正整数的因子,此时`x`和`y`的最大公约数就是 1。
用上面代码的找最大公约数在执行效率是有问题的。假如`x`的值是`999999999998`,`y`的值是`999999999999`,很显然两个数是互质的,最大公约数为 1。但是我们使用上面的代码,循环会重复`999999999998`次,这通常是难以接受的。我们可以使用欧几里得算法来找最大公约数,它能帮我们更快的得到想要的结果,代码如下所示。
```python
"""
输入两个正整数求它们的最大公约数
Version: 1.1
Author: 骆昊
"""
x = int(input('x = '))
y = int(input('y = '))
while y % x != 0:
x, y = y % x, x
print(f'最大公约数: {x}')
```
> **说明**:解决问题的方法和步骤可以称之为算法,对于同一个问题,我们可以设计出不同的算法,不同的算法在存储空间的占用和执行效率上都会存在差别,而这些差别就代表了算法的优劣。大家可以对比上面的两段待会,体会一下为什么我们说欧几里得算法是更好的选择。上面的代码中`x, y = y % x, x`语句表示将`y % x`的值赋给`x`,将`x` 原来的值赋给`y`。
#### 例子3:猜数字游戏
要求:计算机出一个 1 到 100 之间的随机数,玩家输入自己猜的数字,计算机给出对应的提示信息“大一点”、“小一点”或“猜对了”,如果玩家猜中了数字,计算机提示用户一共猜了多少次,游戏结束,否则游戏继续。
```python
"""
猜数字小游戏
Version: 1.0
Author: 骆昊
"""
import random
answer = random.randrange(1, 101)
counter = 0
while True:
counter += 1
num = int(input('请输入: '))
if num < answer:
print('大一点.')
elif num > answer:
print('小一点.')
else:
print('猜对了.')
break
print(f'你一共猜了{counter}次.')
```
> **说明**:上面的代码使用`import random`导入了 Python 标准库的`random`模块,该模块的`randrange`函数帮助我们生成了 1 到 100 范围的随机数(不包括 100)。变量`counter`用来记录循环执行的次数,也就是用户一共猜了几次,每循环一次`counter`的值都会加 1。
### 总结
学会了 Python 中的分支结构和循环结构,我们就可以解决很多实际的问题了。通过这节课的学习,大家应该已经知道了可以用`for`和`while`关键字来构造循环结构。**如果事先知道循环结构重复的次数,我们通常使用**`for`**循环**;**如果循环结构的重复次数不能确定,可以用**`while`**循环**。此外,我们可以在循环结构中**使用**`break`**终止循环**,**也可以在循环结构中使用**`continue`**关键字让循环结构直接进入下一轮次**。
================================================
FILE: Day01-20/07.分支和循环结构实战.md
================================================
## 分支和循环结构实战
通过前面两节课的学习,大家对 Python 中的分支结构和循环结构已经有了初步的认知。**分支结构和循环结构是构造程序逻辑的基础**,它们的重要性不言而喻,但是对于初学者来说这也是比较困难的部分。很多人对分支结构和循环结构的语法是能够理解的,但是遇到实际问题的时候又无法下手;**看懂别人的代码很容易,但是要自己写出类似的代码却又很难**。如果你也有同样的问题和困惑,千万不要沮丧,这只是因为你的编程之旅才刚刚开始,**你的练习量还没有达到让你可以随心所欲写出代码的程度**,只要加强编程练习,通过量的积累来产生质的变化,这个问题迟早都会解决的。
### 例子1:100以内的素数
> **说明**:素数指的是只能被 1 和自身整除的正整数(不包括 1),之前我们写过判断素数的代码,这里相当于是一个升级版本。
```python
"""
输出100以内的素数
Version: 1.0
Author: 骆昊
"""
for num in range(2, 100):
is_prime = True
for i in range(2, int(num ** 0.5) + 1):
if num % i == 0:
is_prime = False
break
if is_prime:
print(num)
```
### 例子2:斐波那契数列
要求:输出斐波那契数列中的前 20 个数。
> **说明**:斐波那契数列(Fibonacci sequence),通常也被称作黄金分割数列,是意大利数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)在《计算之书》中研究理想假设条件下兔子成长率问题而引入的数列,因此这个数列也常被戏称为“兔子数列”。斐波那契数列的特点是数列的前两个数都是 1,从第三个数开始,每个数都是它前面两个数的和。按照这个规律,斐波那契数列的前 10 个数是:`1, 1, 2, 3, 5, 8, 13, 21, 34, 55`。斐波那契数列在现代物理、准晶体结构、化学等领域都有直接的应用。
```python
"""
输出斐波那契数列中的前20个数
Version: 1.0
Author: 骆昊
"""
a, b = 0, 1
for _ in range(20):
a, b = b, a + b
print(a)
```
> **说明**:上面循环中的`a, b = b, a + b`表示将变量`b`的值赋给`a`,把`a + b`的值赋给`b`。通过这个递推公式,我们可以依次获得斐波那契数列中的数。
### 例子3:寻找水仙花数
要求:找出 100 到 999 范围内的所有水仙花数。
> **提示**:在数论中,水仙花数(narcissistic number)也被称为超完全数字不变数、自恋数、自幂数、阿姆斯特朗数,它是一个 $\small{N}$ 位非负整数,其各位数字的 $\small{N}$ 次方和刚好等于该数本身,例如: $\small{153 = 1^{3} + 5^{3} + 3^{3}}$ ,所以 153 是一个水仙花数; $\small{1634 = 1^{4} + 6^{4} + 3^{4} + 4^{4}}$ ,所以 1634 也是一个水仙花数。对于三位数,解题的关键是将它拆分为个位、十位、百位,再判断是否满足水仙花数的要求,这一点利用 Python 中的`//`和`%`运算符其实很容易做到。
```python
"""
找出100到999范围内的水仙花数
Version: 1.0
Author: 骆昊
"""
for num in range(100, 1000):
low = num % 10
mid = num // 10 % 10
high = num // 100
if num == low ** 3 + mid ** 3 + high ** 3:
print(num)
```
上面利用`//`和`%`拆分一个数的小技巧在写代码的时候还是很常用的。我们要将一个不知道有多少位的正整数进行反转,例如将 12389 变成 98321,也可以利用这两个运算来实现,代码如下所示。
```python
"""
正整数的反转
Version: 1.0
Author: 骆昊
"""
num = int(input('num = '))
reversed_num = 0
while num > 0:
reversed_num = reversed_num * 10 + num % 10
num //= 10
print(reversed_num)
```
### 例子4:百钱百鸡问题
> **说明**:百钱百鸡是我国古代数学家张丘建在《算经》一书中提出的数学问题:鸡翁一值钱五,鸡母一值钱三,鸡雏三值钱一。百钱买百鸡,问鸡翁、鸡母、鸡雏各几何?翻译成现代文是:公鸡 5 元一只,母鸡 3 元一只,小鸡 1 元三只,用 100 块钱买一百只鸡,问公鸡、母鸡、小鸡各有多少只?
```python
"""
百钱百鸡问题
Version: 1.0
Author: 骆昊
"""
for x in range(0, 21):
for y in range(0, 34):
for z in range(0, 100, 3):
if x + y + z == 100 and 5 * x + 3 * y + z // 3 == 100:
print(f'公鸡: {x}只, 母鸡: {y}只, 小鸡: {z}只')
```
上面使用的方法叫做**穷举法**,也称为**暴力搜索法**,这种方法通过一项一项的列举备选解决方案中所有可能的候选项,并检查每个候选项是否符合问题的描述,最终得到问题的解。上面的代码中,我们使用了嵌套的循环结构,假设公鸡有`x`只,显然`x`的取值范围是 0 到 20,假设母鸡有`y`只,它的取值范围是 0 到 33,假设小鸡有`z`只,它的取值范围是 0 到 99 且取值是 3 的倍数。这样,我们设置好 100 只鸡的条件`x + y + z == 100`,设置好 100 块钱的条件`5 * x + 3 * y + z // 3 == 100`,当两个条件同时满足时,就是问题的正确答案,我们用`print`函数输出它。这种方法看起来比较笨拙,但对于运算能力非常强大的计算机来说,通常都是一个可行的甚至是不错的选择,只要问题的解存在就能够找到它。
事实上,上面的代码还有更好的写法,既然我们已经假设公鸡有`x`只,母鸡有`y`只,那么小鸡的数量就应该是`100 - x - y`,这样减少一个条件,我们就可以把上面三层嵌套的`for-in`循环改写为两层嵌套的`for-in`循环。循环次数减少了,代码的执行效率就有了显著的提升,如下所示。
```python
"""
百钱百鸡问题
Version: 1.1
Author: 骆昊
"""
for x in range(0, 21):
for y in range(0, 34):
z = 100 - x - y
if z % 3 == 0 and 5 * x + 3 * y + z // 3 == 100:
print(f'公鸡: {x}只, 母鸡: {y}只, 小鸡: {z}只')
```
> **说明**:上面代码中的`z % 3 == 0`是为了确保小鸡的数量是 3 的倍数。
### 例子5:CRAPS赌博游戏
> **说明**:CRAPS又称花旗骰,是美国拉斯维加斯非常受欢迎的一种的桌上赌博游戏。该游戏使用两粒骰子,玩家通过摇两粒骰子获得点数进行游戏。简化后的规则是:玩家第一次摇骰子如果摇出了 7 点或 11 点,玩家胜;玩家第一次如果摇出 2 点、3 点或 12 点,庄家胜;玩家如果摇出其他点数则游戏继续,玩家重新摇骰子,如果玩家摇出了 7 点,庄家胜;如果玩家摇出了第一次摇的点数,玩家胜;其他点数玩家继续摇骰子,直到分出胜负。为了增加代码的趣味性,我们设定游戏开始时玩家有 1000 元的赌注,每局游戏开始之前,玩家先下注,如果玩家获胜就可以获得对应下注金额的奖励,如果庄家获胜,玩家就会输掉自己下注的金额。游戏结束的条件是玩家破产(输光所有的赌注)。
```python
"""
Craps赌博游戏
Version: 1.0
Author: 骆昊
"""
import random
money = 1000
while money > 0:
print(f'你的总资产为: {money}元')
# 下注金额必须大于0且小于等于玩家的总资产
while True:
debt = int(input('请下注: '))
if 0 < debt <= money:
break
# 用两个1到6均匀分布的随机数相加模拟摇两颗色子得到的点数
first_point = random.randrange(1, 7) + random.randrange(1, 7)
print(f'\n玩家摇出了{first_point}点')
if first_point == 7 or first_point == 11:
print('玩家胜!\n')
money += debt
elif first_point == 2 or first_point == 3 or first_point == 12:
print('庄家胜!\n')
money -= debt
else:
# 如果第一次摇色子没有分出胜负,玩家需要重新摇色子
while True:
current_point = random.randrange(1, 7) + random.randrange(1, 7)
print(f'玩家摇出了{current_point}点')
if current_point == 7:
print('庄家胜!\n')
money -= debt
break
elif current_point == first_point:
print('玩家胜!\n')
money += debt
break
print('你破产了, 游戏结束!')
```
### 总结
分支结构和循环结构都非常重要,是构造程序逻辑的基础,**一定要通过大量的练习来达到融会贯通**。我们可以用上面讲的花旗骰游戏作为一个标准,如果你能够很顺利的完成这段代码,那么分支结构和循环结构的知识你就已经很好的掌握了。
================================================
FILE: Day01-20/08.常用数据结构之列表-1.md
================================================
## 常用数据结构之列表-1
在开始本节课的内容之前,我们先给大家一个编程任务,将一颗色子掷 6000 次,统计每种点数出现的次数。这个任务对大家来说应该是非常简单的,我们可以用 1 到 6 均匀分布的随机数来模拟掷色子,然后用 6 个变量分别记录每个点数出现的次数,相信通过前面的学习,大家都能比较顺利的写出下面的代码。
```python
"""
将一颗色子掷6000次,统计每种点数出现的次数
Author: 骆昊
Version: 1.0
"""
import random
f1 = 0
f2 = 0
f3 = 0
f4 = 0
f5 = 0
f6 = 0
for _ in range(6000):
face = random.randrange(1, 7)
if face == 1:
f1 += 1
elif face == 2:
f2 += 1
elif face == 3:
f3 += 1
elif face == 4:
f4 += 1
elif face == 5:
f5 += 1
else:
f6 += 1
print(f'1点出现了{f1}次')
print(f'2点出现了{f2}次')
print(f'3点出现了{f3}次')
print(f'4点出现了{f4}次')
print(f'5点出现了{f5}次')
print(f'6点出现了{f6}次')
```
上面的代码非常有多么“丑陋”相信就不用我多说了。当然,更为可怕的是,如果我们要掷两颗或者掷更多的色子,然后统计每种点数出现的次数,那就需要定义更多的变量,写更多的分支结构,大家想想都会感到恶心。讲到这里,相信大家心中已经有一个疑问了:有没有办法用一个变量来保存多个数据,有没有办法用统一的代码对多个数据进行操作?答案是肯定的,在 Python 语言中我们可以通过容器型变量来保存和操作多个数据,我们首先为大家介绍列表(`list`)这种新的数据类型。
### 创建列表
在 Python 中,**列表是由一系列元素按特定顺序构成的数据序列**,这就意味着如果我们定义一个列表类型的变量,**可以用它来保存多个数据**。在 Python 中,可以使用`[]`字面量语法来定义列表,列表中的多个元素用逗号进行分隔,代码如下所示。
```python
items1 = [35, 12, 99, 68, 55, 35, 87]
items2 = ['Python', 'Java', 'Go', 'Kotlin']
items3 = [100, 12.3, 'Python', True]
print(items1) # [35, 12, 99, 68, 55, 35, 87]
print(items2) # ['Python', 'Java', 'Go', 'Kotlin']
print(items3) # [100, 12.3, 'Python', True]
```
> **说明**:列表中可以有重复元素,例如`items1`中的`35`;列表中可以有不同类型的元素,例如`items3`中有`int`类型、`float`类型、`str`类型和`bool`类型的元素,但是我们通常并不建议将不同类型的元素放在同一个列表中,主要是操作起来极为不便。
我们可以使用`type`函数来查看变量的类型,有兴趣的小伙伴可以自行查看上面的变量`items1`到底是什么类型。因为列表可以保存多个元素,它是一种容器型的数据类型,所以我们在给列表类型的变量起名字时,变量名通常用复数形式的单词。
除此以外,还可以通过 Python 内置的`list`函数将其他序列变成列表。准确的说,`list`并不是一个普通的函数,它是创建列表对象的构造器,后面的课程会为大家介绍对象和构造器这些概念。
```python
items4 = list(range(1, 10))
items5 = list('hello')
print(items4) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(items5) # ['h', 'e', 'l', 'l', 'o']
```
> **说明**:`range(1, 10)`会产生`1`到`9`的整数序列,给到`list`构造器中,会创建出由`1`到`9`的整数构成的列表。字符串是字符构成的序列,上面的`list('hello')`用字符串`hello`的字符作为列表元素,创建了列表对象。
### 列表的运算
我们可以使用`+`运算符实现两个列表的拼接,拼接运算会将两个列表中的元素连接起来放到一个列表中,代码如下所示。
```python
items5 = [35, 12, 99, 45, 66]
items6 = [45, 58, 29]
items7 = ['Python', 'Java', 'JavaScript']
print(items5 + items6) # [35, 12, 99, 45, 66, 45, 58, 29]
print(items6 + items7) # [45, 58, 29, 'Python', 'Java', 'JavaScript']
items5 += items6
print(items5) # [35, 12, 99, 45, 66, 45, 58, 29]
```
我们可以使用`*`运算符实现列表的重复运算,`*`运算符会将列表元素重复指定的次数,我们在上面的代码中增加两行,如下所示。
```python
print(items6 * 3) # [45, 58, 29, 45, 58, 29, 45, 58, 29]
print(items7 * 2) # ['Python', 'Java', 'JavaScript', 'Python', 'Java', 'JavaScript']
```
我们可以使用`in`或`not in`运算符判断一个元素在不在列表中,我们在上面的代码代码中再增加两行,如下所示。
```python
print(29 in items6) # True
print(99 in items6) # False
print('C++' not in items7) # True
print('Python' not in items7) # False
```
由于列表中有多个元素,而且元素是按照特定顺序放在列表中的,所以当我们想操作列表中的某个元素时,可以使用`[]`运算符,通过在`[]`中指定元素的位置来访问该元素,这种运算称为索引运算。需要说明的是,`[]`的元素位置可以是`0`到`N - 1`的整数,也可以是`-1`到`-N`的整数,分别称为正向索引和反向索引,其中`N`代表列表元素的个数。对于正向索引,`[0]`可以访问列表中的第一个元素,`[N - 1]`可以访问最后一个元素;对于反向索引,`[-1]`可以访问列表中的最后一个元素,`[-N]`可以访问第一个元素,代码如下所示。
```python
items8 = ['apple', 'waxberry', 'pitaya', 'peach', 'watermelon']
print(items8[0]) # apple
print(items8[2]) # pitaya
print(items8[4]) # watermelon
items8[2] = 'durian'
print(items8) # ['apple', 'waxberry', 'durian', 'peach', 'watermelon']
print(items8[-5]) # 'apple'
print(items8[-4]) # 'waxberry'
print(items8[-1]) # watermelon
items8[-4] = 'strawberry'
print(items8) # ['apple', 'strawberry', 'durian', 'peach', 'watermelon']
```
在使用索引运算的时候要避免出现索引越界的情况,对于上面的`items8`,如果我们访问`items8[5]`或`items8[-6]`,就会引发`IndexError`错误,导致程序崩溃,对应的错误信息是:*list index out of range*,翻译成中文就是“数组索引超出范围”。因为对于只有五个元素的列表`items8`,有效的正向索引是`0`到`4`,有效的反向索引是`-1`到`-5`。
如果希望一次性访问列表中的多个元素,我们可以使用切片运算。切片运算是形如`[start:end:stride]`的运算符,其中`start`代表访问列表元素的起始位置,`end`代表访问列表元素的终止位置(终止位置的元素无法访问),而`stride`则代表了跨度,简单的说就是位置的增量,比如我们访问的第一个元素在`start`位置,那么第二个元素就在`start + stride`位置,当然`start + stride`要小于`end`。我们给上面的代码增加下面的语句,来使用切片运算符访问列表元素。
```python
print(items8[1:3:1]) # ['strawberry', 'durian']
print(items8[0:3:1]) # ['apple', 'strawberry', 'durian']
print(items8[0:5:2]) # ['apple', 'durian', 'watermelon']
print(items8[-4:-2:1]) # ['strawberry', 'durian']
print(items8[-2:-6:-1]) # ['peach', 'durian', 'strawberry', 'apple']
```
> **提醒**:大家可以看看上面代码中的最后一行,想一想当跨度为负数时,切片运算是如何访问元素的。
如果`start`值等于`0`,那么在使用切片运算符时可以将其省略;如果`end`值等于`N`,`N`代表列表元素的个数,那么在使用切片运算符时可以将其省略;如果`stride`值等于`1`,那么在使用切片运算符时也可以将其省略。所以,下面的代码跟上面的代码作用完全相同。
```python
print(items8[1:3]) # ['strawberry', 'durian']
print(items8[:3:1]) # ['apple', 'strawberry', 'durian']
print(items8[::2]) # ['apple', 'durian', 'watermelon']
print(items8[-4:-2]) # ['strawberry', 'durian']
print(items8[-2::-1]) # ['peach', 'durian', 'strawberry', 'apple']
```
事实上,我们还可以通过切片操作修改列表中的元素,例如我们给上面的代码再加上一行,大家可以看看这里的输出。
```python
items8[1:3] = ['x', 'o']
print(items8) # ['apple', 'x', 'o', 'peach', 'watermelon']
```
两个列表还可以做关系运算,我们可以比较两个列表是否相等,也可以给两个列表比大小,代码如下所示。
```python
nums1 = [1, 2, 3, 4]
nums2 = list(range(1, 5))
nums3 = [3, 2, 1]
print(nums1 == nums2) # True
print(nums1 != nums2) # False
print(nums1 <= nums3) # True
print(nums2 >= nums3) # False
```
> **说明**:上面的`nums1`和`nums2`对应元素完全相同,所以`==`运算的结果是`True`。`nums2`和`nums3`的比较,由于`nums2`的第一个元素`1`小于`nums3`的第一个元素`3`,所以`nums2 >= nums3`比较的结果是`False`。两个列表的关系运算在实际工作并不那么常用,如果实在不理解就跳过吧,不用纠结。
### 元素的遍历
如果想逐个取出列表中的元素,可以使用`for-in`循环的,有以下两种做法。
方法一:在循环结构中通过索引运算,遍历列表元素。
```python
languages = ['Python', 'Java', 'C++', 'Kotlin']
for index in range(len(languages)):
print(languages[index])
```
输出:
```
Python
Java
C++
Kotlin
```
> **说明**:上面的`len`函数可以获取列表元素的个数`N`,而`range(N)`则构成了从`0`到`N-1`的范围,刚好可以作为列表元素的索引。
方法二:直接对列表做循环,循环变量就是列表元素的代表。
```python
languages = ['Python', 'Java', 'C++', 'Kotlin']
for language in languages:
print(language)
```
输出:
```
Python
Java
C++
Kotlin
```
### 总结
讲到这里,我们可以用列表的知识来重构上面“掷色子统计每种点数出现次数”的代码。
```python
"""
将一颗色子掷6000次,统计每种点数出现的次数
Author: 骆昊
Version: 1.1
"""
import random
counters = [0] * 6
# 模拟掷色子记录每种点数出现的次数
for _ in range(6000):
face = random.randrange(1, 7)
counters[face - 1] += 1
# 输出每种点数出现的次数
for face in range(1, 7):
print(f'{face}点出现了{counters[face - 1]}次')
```
上面的代码中,我们用`counters`列表中的六个元素分别表示 1 到 6 点出现的次数,最开始的时候六个元素的值都是 0。接下来,我们用 1 到 6 均匀分布的随机数模拟掷色子,如果摇出 1 点,`counters[0]`的值加 1,如果摇出 2 点,`counters[1]`的值加 1,以此类推。大家感受一下,由于使用了列表类型加上循环结构,我们对数据的处理是批量性的,这就使得修改后的代码比之前的代码要简单优雅得多。
================================================
FILE: Day01-20/09.常用数据结构之列表-2.md
================================================
## 常用数据结构之列表-2
### 列表的方法
列表类型的变量拥有很多方法可以帮助我们操作一个列表,假设我们有名为`foos`的列表,列表有名为`bar`的方法,那么使用列表方法的语法是:`foos.bar()`,这是一种通过对象引用调用对象方法的语法。后面我们讲面向对象编程的时候,还会对这种语法进行详细的讲解,这种语法也称为给对象发消息。
#### 添加和删除元素
列表是一种可变容器,可变容器指的是我们可以向容器中添加元素、可以从容器移除元素,也可以修改现有容器中的元素。我们可以使用列表的`append`方法向列表中追加元素,使用`insert`方法向列表中插入元素。追加指的是将元素添加到列表的末尾,而插入则是在指定的位置添加新元素,大家可以看看下面的代码。
```python
languages = ['Python', 'Java', 'C++']
languages.append('JavaScript')
print(languages) # ['Python', 'Java', 'C++', 'JavaScript']
languages.insert(1, 'SQL')
print(languages) # ['Python', 'SQL', 'Java', 'C++', 'JavaScript']
```
我们可以用列表的`remove`方法从列表中删除指定元素,需要注意的是,如果要删除的元素并不在列表中,会引发`ValueError`错误导致程序崩溃,所以建议大家在删除元素时,先用之前讲过的成员运算做一个判断。我们还可以使用`pop`方法从列表中删除元素,`pop`方法默认删除列表中的最后一个元素,当然也可以给一个位置,删除指定位置的元素。在使用`pop`方法删除元素时,如果索引的值超出了范围,会引发`IndexError`异常,导致程序崩溃。除此之外,列表还有一个`clear`方法,可以清空列表中的元素,代码如下所示。
```python
languages = ['Python', 'SQL', 'Java', 'C++', 'JavaScript']
if 'Java' in languages:
languages.remove('Java')
if 'Swift' in languages:
languages.remove('Swift')
print(languages) # ['Python', 'SQL', C++', 'JavaScript']
languages.pop()
temp = languages.pop(1)
print(temp) # SQL
languages.append(temp)
print(languages) # ['Python', C++', 'SQL']
languages.clear()
print(languages) # []
```
> **说明**:`pop`方法删除元素时会得到被删除的元素,上面的代码中,我们将`pop`方法删除的元素赋值给了名为`temp`的变量。当然如果你愿意,还可以把这个元素再次加入到列表中,正如上面的代码`languages.append(temp)`所做的那样。
这里还有一个小问题,例如`languages`列表中有多个`'Python'`,那么我们用`languages.remove('Python')`是删除所有的`'Python'`,还是删除第一个`'Python'`,大家可以先猜一猜,然后再自己动手尝试一下。
从列表中删除元素其实还有一种方式,就是使用 Python 中的`del`关键字后面跟要删除的元素,这种做法跟使用`pop`方法指定索引删除元素没有实质性的区别,但后者会返回删除的元素,前者在性能上略优,因为`del`对应的底层字节码指令是`DELETE_SUBSCR`,而`pop`对应的底层字节码指令是`CALL_METHOD`和`POP_TOP`,如果不理解就不用管它了。
```python
items = ['Python', 'Java', 'C++']
del items[1]
print(items) # ['Python', 'C++']
```
#### 元素位置和频次
列表的`index`方法可以查找某个元素在列表中的索引位置,如果找不到指定的元素,`index`方法会引发`ValueError`错误;列表的`count`方法可以统计一个元素在列表中出现的次数,代码如下所示。
```python
items = ['Python', 'Java', 'Java', 'C++', 'Kotlin', 'Python']
print(items.index('Python')) # 0
# 从索引位置1开始查找'Python'
print(items.index('Python', 1)) # 5
print(items.count('Python')) # 2
print(items.count('Kotlin')) # 1
print(items.count('Swfit')) # 0
# 从索引位置3开始查找'Java'
print(items.index('Java', 3)) # ValueError: 'Java' is not in list
```
#### 元素排序和反转
列表的`sort`操作可以实现列表元素的排序,而`reverse`操作可以实现元素的反转,代码如下所示。
```python
items = ['Python', 'Java', 'C++', 'Kotlin', 'Swift']
items.sort()
print(items) # ['C++', 'Java', 'Kotlin', 'Python', 'Swift']
items.reverse()
print(items) # ['Swift', 'Python', 'Kotlin', 'Java', 'C++']
```
### 列表生成式
在 Python 中,列表还可以通过一种特殊的字面量语法来创建,这种语法叫做生成式。下面,我们通过例子来说明使用列表生成式创建列表到底有什么好处。
场景一:创建一个取值范围在`1`到`99`且能被`3`或者`5`整除的数字构成的列表。
```python
items = []
for i in range(1, 100):
if i % 3 == 0 or i % 5 == 0:
items.append(i)
print(items)
```
使用列表生成式做同样的事情,代码如下所示。
```python
items = [i for i in range(1, 100) if i % 3 == 0 or i % 5 == 0]
print(items)
```
场景二:有一个整数列表`nums1`,创建一个新的列表`nums2`,`nums2`中的元素是`nums1`中对应元素的平方。
```python
nums1 = [35, 12, 97, 64, 55]
nums2 = []
for num in nums1:
nums2.append(num ** 2)
print(nums2)
```
使用列表生成式做同样的事情,代码如下所示。
```python
nums1 = [35, 12, 97, 64, 55]
nums2 = [num ** 2 for num in nums1]
print(nums2)
```
场景三: 有一个整数列表`nums1`,创建一个新的列表`nums2`,将`nums1`中大于`50`的元素放到`nums2`中。
```python
nums1 = [35, 12, 97, 64, 55]
nums2 = []
for num in nums1:
if num > 50:
nums2.append(num)
print(nums2)
```
使用列表生成式做同样的事情,代码如下所示。
```python
nums1 = [35, 12, 97, 64, 55]
nums2 = [num for num in nums1 if num > 50]
print(nums2)
```
使用列表生成式创建列表不仅代码简单优雅,而且性能上也优于使用`for-in`循环和`append`方法向空列表中追加元素的方式。为什么说生成式有更好的性能呢,那是因为 Python 解释器的字节码指令中有专门针对生成式的指令(`LIST_APPEND`指令);而`for`循环是通过方法调用(`LOAD_METHOD`和`CALL_METHOD`指令)的方式为列表添加元素,方法调用本身就是一个相对比较耗时的操作。对这一点不理解也没有关系,记住“**强烈建议用生成式语法来创建列表**”这个结论就可以了。
### 嵌套列表
Python 语言没有限定列表中的元素必须是相同的数据类型,也就是说一个列表中的元素可以任意的数据类型,当然也包括列表本身。如果列表中的元素也是列表,那么我们可以称之为嵌套的列表。嵌套的列表可以用来表示表格或数学上的矩阵,例如:我们想保存5个学生3门课程的成绩,可以用如下所示的列表。
```python
scores = [[95, 83, 92], [80, 75, 82], [92, 97, 90], [80, 78, 69], [65, 66, 89]]
print(scores[0])
print(scores[0][1])
```
对于上面的嵌套列表,每个元素相当于就是一个学生3门课程的成绩,例如`[95, 83, 92]`,而这个列表中的`83`代表了这个学生某一门课的成绩,如果想访问这个值,可以使用两次索引运算`scores[0][1]`,其中`scores[0]`可以得到`[95, 83, 92]`这个列表,再次使用索引运算`[1]`就可以获得该列表中的第二个元素。
如果想通过键盘输入的方式来录入5个学生3门课程的成绩并保存在列表中,可以使用如下所示的代码。
```python
scores = []
for _ in range(5):
temp = []
for _ in range(3):
score = int(input('请输入: '))
temp.append(score)
scores.append(temp)
print(scores)
```
如果想通过产生随机数的方式来生成5个学生3门课程的成绩并保存在列表中,我们可以使用列表生成式,代码如下所示。
```python
import random
scores = [[random.randrange(60, 101) for _ in range(3)] for _ in range(5)]
print(scores)
```
> **说明**:上面的代码`[random.randrange(60, 101) for _ in range(3)] `可以产生由3个随机整数构成的列表,我们把这段代码又放在了另一个列表生成式中作为列表的元素,这样的元素一共生成5个,最终得到了一个嵌套列表。
### 列表的应用
下面我们通过一个双色球随机选号的例子为大家讲解列表的应用。双色球是由中国福利彩票发行管理中心发售的乐透型彩票,每注投注号码由`6`个红色球和`1`个蓝色球组成。红色球号码从`1`到`33`中选择,蓝色球号码从`1`到`16`中选择。每注需要选择`6`个红色球号码和`1`个蓝色球号码,如下所示。
> **提示**:知乎上有一段对国内各种形式的彩票本质的论述相当精彩,这里分享给大家:“**虚构一个不劳而获的人,去忽悠一群想不劳而获的人,最终养活一批真正不劳而获的人**”。很多对概率没有概念的人,甚至认为彩票中与不中的概率都是 50%;还有很多人认为如果中奖的概率是 1%,那么买 100 次就一定可以中奖,这些都是非常荒唐的想法。所以,**珍爱生命,远离赌博,尤其是你对概率一无所知的情况下**!
下面,我们通过 Python 程序来生成一组随机号码。
```python
"""
双色球随机选号程序
Author: 骆昊
Version: 1.0
"""
import random
red_balls = list(range(1, 34))
selected_balls = []
# 添加6个红色球到选中列表
for _ in range(6):
# 生成随机整数代表选中的红色球的索引位置
index = random.randrange(len(red_balls))
# 将选中的球从红色球列表中移除并添加到选中列表
selected_balls.append(red_balls.pop(index))
# 对选中的红色球排序
selected_balls.sort()
# 输出选中的红色球
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
# 随机选择1个蓝色球
blue_ball = random.randrange(1, 17)
# 输出选中的蓝色球
print(f'\033[034m{blue_ball:0>2d}\033[0m')
```
> **说明**:上面代码中`print(f'\033[0m...\033[0m')`是为了控制输出内容的颜色,红色球输出成红色,蓝色球输出成蓝色。其中省略号代表我们要输出的内容,`\033[0m`是一个控制码,表示关闭所有属性,也就是说之前的控制码将会失效,你也可以将其简单的理解为一个定界符,`m`前面的`0`表示控制台的显示方式为默认值,`0`可以省略,`1`表示高亮,`5`表示闪烁,`7`表示反显等。在`0`和`m`的中间,我们可以写上代表颜色的数字,比如`30`代表黑色,`31`代表红色,`32`代表绿色,`33`代表黄色,`34`代表蓝色等。
我们还可以利用`random`模块提供的`sample`和`choice`函数来简化上面的代码,前者可以实现无放回随机抽样,后者可以实现随机抽取一个元素,修改后的代码如下所示。
```python
"""
双色球随机选号程序
Author: 骆昊
Version: 1.1
"""
import random
red_balls = [i for i in range(1, 34)]
blue_balls = [i for i in range(1, 17)]
# 从红色球列表中随机抽出6个红色球(无放回抽样)
selected_balls = random.sample(red_balls, 6)
# 对选中的红色球排序
selected_balls.sort()
# 输出选中的红色球
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
# 从蓝色球列表中随机抽出1个蓝色球
blue_ball = random.choice(blue_balls)
# 输出选中的蓝色球
print(f'\033[034m{blue_ball:0>2d}\033[0m')
```
如果要实现随机生成`N`注号码,我们只需要将上面的代码放到一个`N`次的循环中,如下所示。
```python
"""
双色球随机选号程序
Author: 骆昊
Version: 1.2
"""
import random
n = int(input('生成几注号码: '))
red_balls = [i for i in range(1, 34)]
blue_balls = [i for i in range(1, 17)]
for _ in range(n):
# 从红色球列表中随机抽出6个红色球(无放回抽样)
selected_balls = random.sample(red_balls, 6)
# 对选中的红色球排序
selected_balls.sort()
# 输出选中的红色球
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
# 从蓝色球列表中随机抽出1个蓝色球
blue_ball = random.choice(blue_balls)
# 输出选中的蓝色球
print(f'\033[034m{blue_ball:0>2d}\033[0m')
```
我们在 PyCharm 中运行上面的代码,输入`5`,运行效果如下图所示。
这里顺便给大家介绍一个名为 rich 的 Python 三方库,它可以帮助我们用最简单的方式产生最漂亮的输出,你可以在终端中使用 Python 包管理工具 pip 来安装这个三方库,对于使用 PyCharm 的用户,当然要在 PyCharm 的终端窗口使用 pip 命令将 rich 安装到项目的虚拟环境中,命令如下所示。
```bash
pip install rich
```
如上图所示,rich 安装成功后,我们可以用如下所示的代码来控制输出。
```python
"""
双色球随机选号程序
Author: 骆昊
Version: 1.3
"""
import random
from rich.console import Console
from rich.table import Table
# 创建控制台
console = Console()
n = int(input('生成几注号码: '))
red_balls = [i for i in range(1, 34)]
blue_balls = [i for i in range(1, 17)]
# 创建表格并添加表头
table = Table(show_header=True)
for col_name in ('序号', '红球', '蓝球'):
table.add_column(col_name, justify='center')
for i in range(n):
selected_balls = random.sample(red_balls, 6)
selected_balls.sort()
blue_ball = random.choice(blue_balls)
# 向表格中添加行(序号,红色球,蓝色球)
table.add_row(
str(i + 1),
f'[red]{" ".join([f"{ball:0>2d}" for ball in selected_balls])}[/red]',
f'[blue]{blue_ball:0>2d}[/blue]'
)
# 通过控制台输出表格
console.print(table)
```
> **说明**:上面代码第 31 行使用了列表生成式语法将红色球号码处理成字符串并保存在一个列表中,`" ".join([...])`是将列表中的多个字符串用空格拼接成一个完整的字符串,如果不理解可以先放放。字符串中的`[red]...[/red]`用来设置输出颜色为红色,第 32 行的`[blue]...[/blue]`用来设置输出颜色为蓝色。更多关于 rich 库的知识,可以参考[官方文档](https://github.com/textualize/rich/blob/master/README.cn.md)。
最终的输出如下图所示,看着这样的输出,是不是心情更美好了一些。
### 总结
Python 中的列表底层是一个可以动态扩容的数组,列表元素在计算机内存中是连续存储的,所以可以实现随机访问(通过一个有效的索引获取对应的元素且操作时间与列表元素个数无关)。我们可以暂时不去触碰这些底层的存储细节,也不需要大家理解列表每个方法的渐近时间复杂度(执行方法耗费的时间跟列表元素个数之间的关系),大家先学会用列表解决工作中的问题,我想这一点更为重要。
================================================
FILE: Day01-20/10.常用数据结构之元组.md
================================================
## 常用数据结构之元组
前面的两节课,我们为大家讲解了 Python 中的列表,它是一种容器型的数据类型,通过列表类型的变量,我们可以保存多个数据并通过循环实现对数据的批量操作。当然,Python 中还有其他容器型的数据类型,接下来我们就为大家讲解另一种容器型的数据类型,它的名字叫元组(tuple)。
### 元组的定义和运算
在 Python 语言中,元组也是多个元素按照一定顺序构成的序列。元组和列表的不同之处在于,**元组是不可变类型**,这就意味着元组类型的变量一旦定义,其中的元素不能再添加或删除,而且元素的值也不能修改。如果试图修改元组中的元素,将引发`TypeError`错误,导致程序崩溃。定义元组通常使用形如`(x, y, z)`的字面量语法,元组类型支持的运算符跟列表是一样的,我们可以看看下面的代码。
```python
# 定义一个三元组
t1 = (35, 12, 98)
# 定义一个四元组
t2 = ('骆昊', 45, True, '四川成都')
# 查看变量的类型
print(type(t1)) #
随着时间的推移,虽然数值运算仍然是计算机日常工作中最为重要的组成部分,但是今天的计算机还要处理大量的以文本形式存在的信息。如果我们希望通过 Python 程序来操作本这些文本信息,就必须要先了解字符串这种数据类型以及与它相关的运算和方法。
### 字符串的定义
所谓**字符串**,就是**由零个或多个字符组成的有限序列**,一般记为:
$$
s = a_1a_2 \cdots a_n \,\,\,\,\, (0 \le n \le \infty)
$$
在 Python 程序中,我们把单个或多个字符用单引号或者双引号包围起来,就可以表示一个字符串。字符串中的字符可以是特殊符号、英文字母、中文字符、日文的平假名或片假名、希腊字母、Emoji 字符(如:💩、🐷、🀄️)等。
```python
s1 = 'hello, world!'
s2 = "你好,世界!❤️"
s3 = '''hello,
wonderful
world!'''
print(s1)
print(s2)
print(s3)
```
#### 转义字符
我们可以在字符串中使用`\`(反斜杠)来表示转义,也就是说`\`后面的字符不再是它原来的意义,例如:`\n`不是代表字符`\`和字符`n`,而是表示换行;`\t`也不是代表字符`\`和字符`t`,而是表示制表符。所以如果字符串本身又包含了`'`、`"`、`\`这些特殊的字符,必须要通过`\`进行转义处理。例如要输出一个带单引号或反斜杠的字符串,需要用如下所示的方法。
```python
s1 = '\'hello, world!\''
s2 = '\\hello, world!\\'
print(s1)
print(s2)
```
#### 原始字符串
Python 中有一种以`r`或`R`开头的字符串,这种字符串被称为原始字符串,意思是字符串中的每个字符都是它本来的含义,没有所谓的转义字符。例如,在字符串`'hello\n'`中,`\n`表示换行;而在`r'hello\n'`中,`\n`不再表示换行,就是字符`\`和字符`n`。大家可以运行下面的代码,看看会输出什么。
```python
s1 = '\it \is \time \to \read \now'
s2 = r'\it \is \time \to \read \now'
print(s1)
print(s2)
```
> **说明**:上面的变量`s1`中,`\t`、`\r`和`\n`都是转义字符。`\t`是制表符(table),`\n`是换行符(new line),`\r`是回车符(carriage return)相当于让输出回到了行首。对比一下两个`print`函数的输出,看看到底有什么区别!
#### 字符的特殊表示
Python 中还允许在`\`后面还可以跟一个八进制或者十六进制数来表示字符,例如`\141`和`\x61`都代表小写字母`a`,前者是八进制的表示法,后者是十六进制的表示法。另外一种表示字符的方式是在`\u`后面跟 Unicode 字符编码,例如`\u9a86\u660a`代表的是中文“骆昊”。运行下面的代码,看看输出了什么。
```python
s1 = '\141\142\143\x61\x62\x63'
s2 = '\u9a86\u660a'
print(s1)
print(s2)
```
### 字符串的运算
Python 语言为字符串类型提供了非常丰富的运算符,有很多运算符跟列表类型的运算符作用类似。例如,我们可以使用`+`运算符来实现字符串的拼接,可以使用`*`运算符来重复一个字符串的内容,可以使用`in`和`not in`来判断一个字符串是否包含另外一个字符串,我们也可以用`[]`和`[:]`运算符从字符串取出某个字符或某些字符。
#### 拼接和重复
下面的例子演示了使用`+`和`*`运算符来实现字符串的拼接和重复操作。
```python
s1 = 'hello' + ', ' + 'world'
print(s1) # hello, world
s2 = '!' * 3
print(s2) # !!!
s1 += s2
print(s1) # hello, world!!!
s1 *= 2
print(s1) # hello, world!!!hello, world!!!
```
用`*`实现字符串的重复是非常有意思的一个运算符,在很多编程语言中,要表示一个有10个`a`的字符串,你只能写成`'aaaaaaaaaa'`,但是在 Python 中,你可以写成`'a' * 10`。你可能觉得`'aaaaaaaaaa'`这种写法也没有什么不方便的,但是请想一想,如果字符`a`要重复100次或者1000次又会如何呢?
#### 比较运算
对于两个字符串类型的变量,可以直接使用比较运算符来判断两个字符串的相等性或比较大小。需要说明的是,因为字符串在计算机内存中也是以二进制形式存在的,那么字符串的大小比较比的是每个字符对应的编码的大小。例如`A`的编码是`65`, 而`a`的编码是`97`,所以`'A' < 'a'`的结果相当于就是`65 < 97`的结果,这里很显然是`True`;而`'boy' < 'bad'`,因为第一个字符都是`'b'`比不出大小,所以实际比较的是第二个字符的大小,显然`'o' < 'a'`的结果是`False`,所以`'boy' < 'bad'`的结果是`False`。如果不清楚两个字符对应的编码到底是多少,可以使用`ord`函数来获得,之前我们有提到过这个函数。例如`ord('A')`的值是`65`,而`ord('昊')`的值是`26122`。下面的代码展示了字符串的比较运算,请大家仔细看看。
```python
s1 = 'a whole new world'
s2 = 'hello world'
print(s1 == s2) # False
print(s1 < s2) # True
print(s1 == 'hello world') # False
print(s2 == 'hello world') # True
print(s2 != 'Hello world') # True
s3 = '骆昊'
print(ord('骆')) # 39558
print(ord('昊')) # 26122
s4 = '王大锤'
print(ord('王')) # 29579
print(ord('大')) # 22823
print(ord('锤')) # 38180
print(s3 >= s4) # True
print(s3 != s4) # True
```
#### 成员运算
Python 中可以用`in`和`not in`判断一个字符串中是否包含另外一个字符或字符串,跟列表类型一样,`in`和`not in`称为成员运算符,会产生布尔值`True`或`False`,代码如下所示。
```python
s1 = 'hello, world'
s2 = 'goodbye, world'
print('wo' in s1) # True
print('wo' not in s2) # False
print(s2 in s1) # False
```
#### 获取字符串长度
获取字符串长度跟获取列表元素个数一样,使用内置函数`len`,代码如下所示。
```python
s = 'hello, world'
print(len(s)) # 12
print(len('goodbye, world')) # 14
```
#### 索引和切片
字符串的索引和切片操作跟列表、元组几乎没有区别,因为字符串也是一种有序序列,可以通过正向或反向的整数索引访问其中的元素。但是有一点需要注意,因为**字符串是不可变类型**,所以**不能通过索引运算修改字符串中的字符**。
```python
s = 'abc123456'
n = len(s)
print(s[0], s[-n]) # a a
print(s[n-1], s[-1]) # 6 6
print(s[2], s[-7]) # c c
print(s[5], s[-4]) # 3 3
print(s[2:5]) # c12
print(s[-7:-4]) # c12
print(s[2:]) # c123456
print(s[:2]) # ab
print(s[::2]) # ac246
print(s[::-1]) # 654321cba
```
需要再次提醒大家注意的是,在进行索引运算时,如果索引越界,会引发`IndexError`异常,错误提示信息为:`string index out of range`(字符串索引超出范围)。
### 字符的遍历
如果希望遍历字符串中的每个字符,可以使用`for-in`循环,有如下所示的两种方式。
方式一:
```python
s = 'hello'
for i in range(len(s)):
print(s[i])
```
方式二:
```python
s = 'hello'
for elem in s:
print(elem)
```
### 字符串的方法
在 Python 中,我们可以通过字符串类型自带的方法对字符串进行操作和处理,假设我们有名为`foo`的字符串,字符串有名为`bar`的方法,那么使用字符串方法的语法是:`foo.bar()`,这是一种通过对象引用调用对象方法的语法,跟前面使用列表方法的语法是一样的。
#### 大小写相关操作
下面的代码演示了和字符串大小写变换相关的方法。
```python
s1 = 'hello, world!'
# 字符串首字母大写
print(s1.capitalize()) # Hello, world!
# 字符串每个单词首字母大写
print(s1.title()) # Hello, World!
# 字符串变大写
print(s1.upper()) # HELLO, WORLD!
s2 = 'GOODBYE'
# 字符串变小写
print(s2.lower()) # goodbye
# 检查s1和s2的值
print(s1) # hello, world
print(s2) # GOODBYE
```
> **说明**:由于字符串是不可变类型,使用字符串的方法对字符串进行操作会产生新的字符串,但是原来变量的值并没有发生变化。所以上面的代码中,当我们最后检查`s1`和`s2`两个变量的值时,`s1`和`s2` 的值并没有发生变化。
#### 查找操作
如果想在一个字符串中从前向后查找有没有另外一个字符串,可以使用字符串的`find`或`index`方法。在使用`find`和`index`方法时还可以通过方法的参数来指定查找的范围,也就是查找不必从索引为`0`的位置开始。
```python
s = 'hello, world!'
print(s.find('or')) # 8
print(s.find('or', 9)) # -1
print(s.find('of')) # -1
print(s.index('or')) # 8
print(s.index('or', 9)) # ValueError: substring not found
```
>**说明**:`find`方法找不到指定的字符串会返回`-1`,`index`方法找不到指定的字符串会引发`ValueError`错误。
`find`和`index`方法还有逆向查找(从后向前查找)的版本,分别是`rfind`和`rindex`,代码如下所示。
```python
s = 'hello world!'
print(s.find('o')) # 4
print(s.rfind('o')) # 7
print(s.rindex('o')) # 7
# print(s.rindex('o', 8)) # ValueError: substring not found
```
#### 性质判断
可以通过字符串的`startswith`、`endswith`来判断字符串是否以某个字符串开头和结尾;还可以用`is`开头的方法判断字符串的特征,这些方法都返回布尔值,代码如下所示。
```python
s1 = 'hello, world!'
print(s1.startswith('He')) # False
print(s1.startswith('hel')) # True
print(s1.endswith('!')) # True
s2 = 'abc123456'
print(s2.isdigit()) # False
print(s2.isalpha()) # False
print(s2.isalnum()) # True
```
> **说明**:上面的`isdigit`用来判断字符串是不是完全由数字构成的,`isalpha`用来判断字符串是不是完全由字母构成的,这里的字母指的是 Unicode 字符但不包含 Emoji 字符,`isalnum`用来判断字符串是不是由字母和数字构成的。
#### 格式化
在 Python 中,字符串类型可以通过`center`、`ljust`、`rjust`方法做居中、左对齐和右对齐的处理。如果要在字符串的左侧补零,也可以使用`zfill`方法。
```python
s = 'hello, world'
print(s.center(20, '*')) # ****hello, world****
print(s.rjust(20)) # hello, world
print(s.ljust(20, '~')) # hello, world~~~~~~~~
print('33'.zfill(5)) # 00033
print('-33'.zfill(5)) # -0033
```
我们之前讲过,在用`print`函数输出字符串时,可以用下面的方式对字符串进行格式化。
```python
a = 321
b = 123
print('%d * %d = %d' % (a, b, a * b))
```
当然,我们也可以用字符串的`format`方法来完成字符串的格式,代码如下所示。
```python
a = 321
b = 123
print('{0} * {1} = {2}'.format(a, b, a * b))
```
从 Python 3.6 开始,格式化字符串还有更为简洁的书写方式,就是在字符串前加上`f`来格式化字符串,在这种以`f`打头的字符串中,`{变量名}`是一个占位符,会被变量对应的值将其替换掉,代码如下所示。
```python
a = 321
b = 123
print(f'{a} * {b} = {a * b}')
```
如果需要进一步控制格式化语法中变量值的形式,可以参照下面的表格来进行字符串格式化操作。
| 变量值 | 占位符 | 格式化结果 | 说明 |
| ----------- | ---------- | ------------- | ---- |
| `3.1415926` | `{:.2f}` | `'3.14'` | 保留小数点后两位 |
| `3.1415926` | `{:+.2f}` | `'+3.14'` | 带符号保留小数点后两位 |
| `-1` | `{:+.2f}` | `'-1.00'` | 带符号保留小数点后两位 |
| `3.1415926` | `{:.0f}` | `'3'` | 不带小数 |
| `123` | `{:0>10d}` | `'0000000123'` | 左边补`0`,补够10位 |
| `123` | `{:x<10d}` | `'123xxxxxxx'` | 右边补`x` ,补够10位 |
| `123` | `{:>10d}` | `' 123'` | 左边补空格,补够10位 |
| `123` | `{:<10d}` | `'123 '` | 右边补空格,补够10位 |
| `123456789` | `{:,}` | `'123,456,789'` | 逗号分隔格式 |
| `0.123` | `{:.2%}` | `'12.30%'` | 百分比格式 |
| `123456789` | `{:.2e}` | `'1.23e+08'` | 科学计数法格式 |
#### 修剪操作
字符串的`strip`方法可以帮我们获得将原字符串修剪掉左右两端指定字符之后的字符串,默认是修剪空格字符。这个方法非常有实用价值,可以用来将用户输入时不小心键入的头尾空格等去掉,`strip`方法还有`lstrip`和`rstrip`两个版本,相信从名字大家已经猜出来这两个方法是做什么用的。
```python
s1 = ' jackfrued@126.com '
print(s1.strip()) # jackfrued@126.com
s2 = '~你好,世界~'
print(s2.lstrip('~')) # 你好,世界~
print(s2.rstrip('~')) # ~你好,世界
```
#### 替换操作
如果希望用新的内容替换字符串中指定的内容,可以使用`replace`方法,代码如下所示。`replace`方法的第一个参数是被替换的内容,第二个参数是替换后的内容,还可以通过第三个参数指定替换的次数。
```python
s = 'hello, good world'
print(s.replace('o', '@')) # hell@, g@@d w@rld
print(s.replace('o', '@', 1)) # hell@, good world
```
#### 拆分与合并
可以使用字符串的`split`方法将一个字符串拆分为多个字符串(放在一个列表中),也可以使用字符串的`join`方法将列表中的多个字符串连接成一个字符串,代码如下所示。
```python
s = 'I love you'
words = s.split()
print(words) # ['I', 'love', 'you']
print('~'.join(words)) # I~love~you
```
需要说明的是,`split`方法默认使用空格进行拆分,我们也可以指定其他的字符来拆分字符串,而且还可以指定最大拆分次数来控制拆分的效果,代码如下所示。
```python
s = 'I#love#you#so#much'
words = s.split('#')
print(words) # ['I', 'love', 'you', 'so', 'much']
words = s.split('#', 2)
print(words) # ['I', 'love', 'you#so#much']
```
#### 编码和解码
Python 中除了字符串`str`类型外,还有一种表示二进制数据的字节串类型(`bytes`)。所谓字节串,就是**由零个或多个字节组成的有限序列**。通过字符串的`encode`方法,我们可以按照某种编码方式将字符串编码为字节串,我们也可以使用字节串的`decode`方法,将字节串解码为字符串,代码如下所示。
```python
a = '骆昊'
b = a.encode('utf-8')
c = a.encode('gbk')
print(b) # b'\xe9\xaa\x86\xe6\x98\x8a'
print(c) # b'\xc2\xe6\xea\xbb'
print(b.decode('utf-8')) # 骆昊
print(c.decode('gbk')) # 骆昊
```
注意,如果编码和解码的方式不一致,会导致乱码问题(无法再现原始的内容)或引发`UnicodeDecodeError`错误,导致程序崩溃。
#### 其他方法
对于字符串类型来说,还有一个常用的操作是对字符串进行匹配检查,即检查字符串是否满足某种特定的模式。例如,一个网站对用户注册信息中用户名和邮箱的检查,就属于模式匹配检查。实现模式匹配检查的工具叫做正则表达式,Python 语言通过标准库中的`re`模块提供了对正则表达式的支持,我们会在后续的课程中为大家讲解这个知识点。
### 总结
知道如何表示和操作字符串对程序员来说是非常重要的,因为我们经常需要处理文本信息,Python 中操作字符串可以用拼接、索引、切片等运算符,也可以使用字符串类型提供的非常丰富的方法。
================================================
FILE: Day01-20/12.常用数据结构之集合.md
================================================
## 常用数据结构之集合
在学习了列表和元组之后,我们再来学习一种容器型的数据类型,它的名字叫集合(set)。说到集合这个词大家一定不会陌生,在数学课本上就有这个概念。如果我们**把一定范围的、确定的、可以区别的事物当作一个整体来看待**,那么这个整体就是集合,集合中的各个事物称为集合的**元素**。通常,集合需要满足以下要求:
1. **无序性**:一个集合中,每个元素的地位都是相同的,元素之间是无序的。
2. **互异性**:一个集合中,任何两个元素都是不相同的,即元素在集合中只能出现一次。
3. **确定性**:给定一个集合和一个任意元素,该元素要么属这个集合,要么不属于这个集合,二者必居其一,不允许有模棱两可的情况出现。
Python 程序中的集合跟数学上的集合没有什么本质区别,需要强调的是上面所说的无序性和互异性。无序性说明集合中的元素并不像列中的元素那样存在某种次序,可以通过索引运算就能访问任意元素,**集合并不支持索引运算**。另外,集合的互异性决定了**集合中不能有重复元素**,这一点也是集合区别于列表的地方,我们无法将重复的元素添加到一个集合中。集合类型必然是支持`in`和`not in`成员运算的,这样就可以确定一个元素是否属于集合,也就是上面所说的集合的确定性。**集合的成员运算在性能上要优于列表的成员运算**,这是集合的底层存储特性决定的,此处我们暂时不做讨论,大家记住这个结论即可。
> **说明**:集合底层使用了哈希存储(散列存储),对哈希存储不了解的读者可以先看看“Hello 算法”网站对[哈希表](https://www.hello-algo.com/chapter_hashing/)的讲解,感谢作者的开源精神。
### 创建集合
在 Python 中,创建集合可以使用`{}`字面量语法,`{}`中需要至少有一个元素,因为没有元素的`{}`并不是空集合而是一个空字典,字典类型我们会在下一节课中为大家介绍。当然,也可以使用 Python 内置函数`set`来创建一个集合,准确的说`set`并不是一个函数,而是创建集合对象的构造器,这个知识点会在后面讲解面向对象编程的地方为大家介绍。我们可以使用`set`函数创建一个空集合,也可以用它将其他序列转换成集合,例如:`set('hello')`会得到一个包含了`4`个字符的集合(重复的字符`l`只会在集合中出现一次)。除了这两种方式,还可以使用生成式语法来创建集合,就像我们之前用生成式语法创建列表那样。
```python
set1 = {1, 2, 3, 3, 3, 2}
print(set1)
set2 = {'banana', 'pitaya', 'apple', 'apple', 'banana', 'grape'}
print(set2)
set3 = set('hello')
print(set3)
set4 = set([1, 2, 2, 3, 3, 3, 2, 1])
print(set4)
set5 = {num for num in range(1, 20) if num % 3 == 0 or num % 7 == 0}
print(set5)
```
需要提醒大家,集合中的元素必须是`hashable`类型,所谓`hashable`类型指的是能够计算出哈希码的数据类型,通常不可变类型都是`hashable`类型,如整数(`int`)、浮点小数(`float`)、布尔值(`bool`)、字符串(`str`)、元组(`tuple`)等。可变类型都不是`hashable`类型,因为可变类型无法计算出确定的哈希码,所以它们不能放到集合中。例如:我们不能将列表作为集合中的元素;同理,由于集合本身也是可变类型,所以集合也不能作为集合中的元素。我们可以创建出嵌套列表(列表的元素也是列表),但是我们不能创建出嵌套的集合,这一点在使用集合的时候一定要引起注意。
> **温馨提示**:如果不理解上面提到的哈希码、哈希存储这些概念,可以先放放,因为它并不影响你继续学习和使用 Python 语言。当然,如果是计算机专业的小伙伴,不理解哈希存储是很难被原谅的,要赶紧去补课了。
### 元素的遍历
我们可以通过`len`函数来获得集合中有多少个元素,但是我们不能通过索引运算来遍历集合中的元素,因为集合元素并没有特定的顺序。当然,要实现对集合元素的遍历,我们仍然可以使用`for-in`循环,代码如下所示。
```python
set1 = {'Python', 'C++', 'Java', 'Kotlin', 'Swift'}
for elem in set1:
print(elem)
```
> **提示**:大家看看上面代码的运行结果,通过单词输出的顺序体会一下集合的无序性。
### 集合的运算
Python 为集合类型提供了非常丰富的运算,主要包括:成员运算、交集运算、并集运算、差集运算、比较运算(相等性、子集、超集)等。
#### 成员运算
可以通过成员运算`in`和`not in `检查元素是否在集合中,代码如下所示。
```python
set1 = {11, 12, 13, 14, 15}
print(10 in set1) # False
print(15 in set1) # True
set2 = {'Python', 'Java', 'C++', 'Swift'}
print('Ruby' in set2) # False
print('Java' in set2) # True
```
#### 二元运算
集合的二元运算主要指集合的交集、并集、差集、对称差等运算,这些运算可以通过运算符来实现,也可以通过集合类型的方法来实现,代码如下所示。
```python
set1 = {1, 2, 3, 4, 5, 6, 7}
set2 = {2, 4, 6, 8, 10}
# 交集
print(set1 & set2) # {2, 4, 6}
print(set1.intersection(set2)) # {2, 4, 6}
# 并集
print(set1 | set2) # {1, 2, 3, 4, 5, 6, 7, 8, 10}
print(set1.union(set2)) # {1, 2, 3, 4, 5, 6, 7, 8, 10}
# 差集
print(set1 - set2) # {1, 3, 5, 7}
print(set1.difference(set2)) # {1, 3, 5, 7}
# 对称差
print(set1 ^ set2) # {1, 3, 5, 7, 8, 10}
print(set1.symmetric_difference(set2)) # {1, 3, 5, 7, 8, 10}
```
通过上面的代码可以看出,对两个集合求交集,`&`运算符和`intersection`方法的作用是完全相同的,使用运算符的方式显然更直观且代码也更简短。需要说明的是,集合的二元运算还可以跟赋值运算一起构成复合赋值运算,例如:`set1 |= set2`相当于`set1 = set1 | set2`,跟`|=`作用相同的方法是`update`;`set1 &= set2`相当于`set1 = set1 & set2`,跟`&=`作用相同的方法是`intersection_update`,代码如下所示。
```python
set1 = {1, 3, 5, 7}
set2 = {2, 4, 6}
set1 |= set2
# set1.update(set2)
print(set1) # {1, 2, 3, 4, 5, 6, 7}
set3 = {3, 6, 9}
set1 &= set3
# set1.intersection_update(set3)
print(set1) # {3, 6}
set2 -= set1
# set2.difference_update(set1)
print(set2) # {2, 4}
```
#### 比较运算
两个集合可以用`==`和`!=`进行相等性判断,如果两个集合中的元素完全相同,那么`==`比较的结果就是`True`,否则就是`False`。如果集合`A`的任意一个元素都是集合`B`的元素,那么集合`A`称为集合`B`的子集,即对于 $\small{\forall{a} \in {A}}$ ,均有 $\small{{a} \in {B}}$ ,则 $\small{{A} \subseteq {B}}$ ,`A`是`B`的子集,反过来也可以称`B`是`A`的超集。如果`A`是`B`的子集且`A`不等于`B`,那么`A`就是`B`的真子集。Python 为集合类型提供了判断子集和超集的运算符,其实就是我们非常熟悉的`<`、`<=`、`>`、`>=`这些运算符。当然,我们也可以通过集合类型的方法`issubset`和`issuperset`来判断集合之间的关系,代码如下所示。
```python
set1 = {1, 3, 5}
set2 = {1, 2, 3, 4, 5}
set3 = {5, 4, 3, 2, 1}
print(set1 < set2) # True
print(set1 <= set2) # True
print(set2 < set3) # False
print(set2 <= set3) # True
print(set2 > set1) # True
print(set2 == set3) # True
print(set1.issubset(set2)) # True
print(set2.issuperset(set1)) # True
```
> **说明**:上面的代码中,`set1 < set2`判断`set1`是不是`set2`的真子集,`set1 <= set2`判断`set1`是不是`set2`的子集,`set2 > set1`判断`set2`是不是`set1`的超集。当然,我们也可以通过`set1.issubset(set2)`判断`set1`是不是`set2`的子集;通过`set2.issuperset(set1)`判断`set2`是不是`set1`的超集。
### 集合的方法
刚才我们说过,Python 中的集合是可变类型,我们可以通过集合的方法向集合添加元素或从集合中删除元素。
```python
set1 = {1, 10, 100}
# 添加元素
set1.add(1000)
set1.add(10000)
print(set1) # {1, 100, 1000, 10, 10000}
# 删除元素
set1.discard(10)
if 100 in set1:
set1.remove(100)
print(set1) # {1, 1000, 10000}
# 清空元素
set1.clear()
print(set1) # set()
```
> **说明**:删除元素的`remove`方法在元素不存在时会引发`KeyError`错误,所以上面的代码中我们先通过成员运算判断元素是否在集合中。集合类型还有一个`pop`方法可以从集合中随机删除一个元素,该方法在删除元素的同时会返回(获得)被删除的元素,而`remove`和`discard`方法仅仅是删除元素,不会返回(获得)被删除的元素。
集合类型还有一个名为`isdisjoint`的方法可以判断两个集合有没有相同的元素,如果没有相同元素,该方法返回`True`,否则该方法返回`False`,代码如下所示。
```python
set1 = {'Java', 'Python', 'C++', 'Kotlin'}
set2 = {'Kotlin', 'Swift', 'Java', 'Dart'}
set3 = {'HTML', 'CSS', 'JavaScript'}
print(set1.isdisjoint(set2)) # False
print(set1.isdisjoint(set3)) # True
```
### 不可变集合
Python 中还有一种不可变类型的集合,名字叫`frozenset`。`set`跟`frozenset`的区别就如同`list`跟`tuple`的区别,`frozenset`由于是不可变类型,能够计算出哈希码,因此它可以作为`set`中的元素。除了不能添加和删除元素,`frozenset`在其他方面跟`set`是一样的,下面的代码简单的展示了`frozenset`的用法。
```python
fset1 = frozenset({1, 3, 5, 7})
fset2 = frozenset(range(1, 6))
print(fset1) # frozenset({1, 3, 5, 7})
print(fset2) # frozenset({1, 2, 3, 4, 5})
print(fset1 & fset2) # frozenset({1, 3, 5})
print(fset1 | fset2) # frozenset({1, 2, 3, 4, 5, 7})
print(fset1 - fset2) # frozenset({7})
print(fset1 < fset2) # False
```
### 总结
Python 中的**集合类型是一种无序容器**,**不允许有重复运算**,由于底层使用了哈希存储,集合中的元素必须是`hashable`类型。集合与列表最大的区别在于**集合中的元素没有顺序**、所以**不能够通过索引运算访问元素**、但是集合可以执行交集、并集、差集等二元运算,也可以通过关系运算符检查两个集合是否存在超集、子集等关系。
================================================
FILE: Day01-20/13.常用数据结构之字典.md
================================================
## 常用数据结构之字典
迄今为止,我们已经为大家介绍了 Python 中的三种容器型数据类型(列表、元组、集合),但是这些数据类型仍然不足以帮助我们解决所有的问题。例如,我们需要一个变量来保存一个人的多项信息,包括:姓名、年龄、身高、体重、家庭住址、本人手机号、紧急联系人手机号,此时你会发现,我们之前学过的列表、元组和集合类型都不够好使。
```python
person1 = ['王大锤', 55, 168, 60, '成都市武侯区科华北路62号1栋101', '13122334455', '13800998877']
person2 = ('王大锤', 55, 168, 60, '成都市武侯区科华北路62号1栋101', '13122334455', '13800998877')
person3 = {'王大锤', 55, 168, 60, '成都市武侯区科华北路62号1栋101', '13122334455', '13800998877'}
```
集合肯定是最不合适的,因为集合中不能有重复元素,如果一个人的年龄和体重刚好相同,那么集合中就会少一项信息;同理,如果这个人的手机号和紧急联系人手机号是相同的,那么集合中又会少一项信息。另一方面,虽然列表和元组可以把一个人的所有信息都保存下来,但是当你想要获取这个人的手机号或家庭住址时,你得先知道他的手机号是列表或元组中的第几个元素。总之,在遇到上述的场景时,列表、元组、集合都不是最合适的选择,此时我们需要字典(dictionary)类型,这种数据类型最适合把相关联的信息组装到一起,可以帮助我们解决 Python 程序中为真实事物建模的问题。
说到字典这个词,大家一定不陌生,读小学的时候,每个人手头基本上都有一本《新华字典》,如下图所示。
Python 程序中的字典跟现实生活中的字典很像,它以键值对(键和值的组合)的方式把数据组织到一起,我们可以通过键找到与之对应的值并进行操作。就像《新华字典》中,每个字(键)都有与它对应的解释(值)一样,每个字和它的解释合在一起就是字典中的一个条目,而字典中通常包含了很多个这样的条目。
### 创建和使用字典
Python 中创建字典可以使用`{}`字面量语法,这一点跟上一节课讲的集合是一样的。但是字典的`{}`中的元素是以键值对的形式存在的,每个元素由`:`分隔的两个值构成,`:`前面是键,`:`后面是值,代码如下所示。
```python
xinhua = {
'麓': '山脚下',
'路': '道,往来通行的地方;方面,地区:南~货,外~货;种类:他俩是一~人',
'蕗': '甘草的别名',
'潞': '潞水,水名,即今山西省的浊漳河;潞江,水名,即云南省的怒江'
}
print(xinhua)
person = {
'name': '王大锤',
'age': 55,
'height': 168,
'weight': 60,
'addr': '成都市武侯区科华北路62号1栋101',
'tel': '13122334455',
'emergence contact': '13800998877'
}
print(person)
```
通过上面的代码,相信大家已经看出来了,用字典来保存一个人的信息远远优于使用列表或元组,因为我们可以用`:`前面的键来表示条目的含义,而`:`后面就是这个条目所对应的值。
当然,如果愿意,我们也可以使用内置函数`dict`或者是字典的生成式语法来创建字典,代码如下所示。
```python
# dict函数(构造器)中的每一组参数就是字典中的一组键值对
person = dict(name='王大锤', age=55, height=168, weight=60, addr='成都市武侯区科华北路62号1栋101')
print(person) # {'name': '王大锤', 'age': 55, 'height': 168, 'weight': 60, 'addr': '成都市武侯区科华北路62号1栋101'}
# 可以通过Python内置函数zip压缩两个序列并创建字典
items1 = dict(zip('ABCDE', '12345'))
print(items1) # {'A': '1', 'B': '2', 'C': '3', 'D': '4', 'E': '5'}
items2 = dict(zip('ABCDE', range(1, 10)))
print(items2) # {'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5}
# 用字典生成式语法创建字典
items3 = {x: x ** 3 for x in range(1, 6)}
print(items3) # {1: 1, 2: 8, 3: 27, 4: 64, 5: 125}
```
想知道字典中一共有多少组键值对,仍然是使用`len`函数;如果想对字典进行遍历,可以用`for`循环,但是需要注意,`for`循环只是对字典的键进行了遍历,不过没关系,在学习了字典的索引运算后,我们可以通过字典的键访问它对应的值。
```python
person = {
'name': '王大锤',
'age': 55,
'height': 168,
'weight': 60,
'addr': '成都市武侯区科华北路62号1栋101'
}
print(len(person)) # 5
for key in person:
print(key)
```
### 字典的运算
对于字典类型来说,成员运算和索引运算肯定是很重要的,前者可以判定指定的键在不在字典中,后者可以通过键访问对应的值或者向字典中添加新的键值对。值得注意的是,字典的索引不同于列表的索引,列表中的元素因为有属于自己有序号,所以列表的索引是一个整数;字典中因为保存的是键值对,所以字典需要用键去索引对应的值。需要**特别提醒**大家注意的是,**字典中的键必须是不可变类型**,例如整数(`int`)、浮点数(`float`)、字符串(`str`)、元组(`tuple`)等类型,这一点跟集合类型对元素的要求是一样的;很显然,之前我们讲的列表(`list`)和集合(`set`)不能作为字典中的键,字典类型本身也不能再作为字典中的键,因为字典也是可变类型,但是列表、集合、字典都可以作为字典中的值,例如:
```python
person = {
'name': '王大锤',
'age': 55,
'height': 168,
'weight': 60,
'addr': ['成都市武侯区科华北路62号1栋101', '北京市西城区百万庄大街1号'],
'car': {
'brand': 'BMW X7',
'maxSpeed': '250',
'length': 5170,
'width': 2000,
'height': 1835,
'displacement': 3.0
}
}
print(person)
```
大家可以看看下面的代码,了解一下字典的成员运算和索引运算。
```python
person = {'name': '王大锤', 'age': 55, 'height': 168, 'weight': 60, 'addr': '成都市武侯区科华北路62号1栋101'}
# 成员运算
print('name' in person) # True
print('tel' in person) # False
# 索引运算
print(person['name'])
print(person['addr'])
person['age'] = 25
person['height'] = 178
person['tel'] = '13122334455'
person['signature'] = '你的男朋友是一个盖世垃圾,他会踏着五彩祥云去迎娶你的闺蜜'
print(person)
# 循环遍历
for key in person:
print(f'{key}:\t{person[key]}')
```
需要注意,在通过索引运算获取字典中的值时,如指定的键没有在字典中,将会引发`KeyError`异常。
### 字典的方法
字典类型的方法基本上都跟字典的键值对操作相关,其中`get`方法可以通过键来获取对应的值。跟索引运算不同的是,`get`方法在字典中没有指定的键时不会产生异常,而是返回`None`或指定的默认值,代码如下所示。
```python
person = {'name': '王大锤', 'age': 25, 'height': 178, 'addr': '成都市武侯区科华北路62号1栋101'}
print(person.get('name')) # 王大锤
print(person.get('sex')) # None
print(person.get('sex', True)) # True
```
如果需要获取字典中所有的键,可以使用`keys`方法;如果需要获取字典中所有的值,可以使用`values`方法。字典还有一个名为`items`的方法,它会将键和值组装成二元组,通过该方法来遍历字典中的元素也是非常方便的。
```python
person = {'name': '王大锤', 'age': 25, 'height': 178}
print(person.keys()) # dict_keys(['name', 'age', 'height'])
print(person.values()) # dict_values(['王大锤', 25, 178])
print(person.items()) # dict_items([('name', '王大锤'), ('age', 25), ('height', 178)])
for key, value in person.items():
print(f'{key}:\t{value}')
```
字典的`update`方法实现两个字典的合并操作。例如,有两个字典`x`和`y`,当执行`x.update(y)`操作时,`x`跟`y`相同的键对应的值会被`y`中的值更新,而`y`中有但`x`中没有的键值对会直接添加到`x`中,代码如下所示。
```python
person1 = {'name': '王大锤', 'age': 55, 'height': 178}
person2 = {'age': 25, 'addr': '成都市武侯区科华北路62号1栋101'}
person1.update(person2)
print(person1) # {'name': '王大锤', 'age': 25, 'height': 178, 'addr': '成都市武侯区科华北路62号1栋101'}
```
如果使用 Python 3.9 及以上的版本,也可以使用`|`运算符来完成同样的操作,代码如下所示。
```python
person1 = {'name': '王大锤', 'age': 55, 'height': 178}
person2 = {'age': 25, 'addr': '成都市武侯区科华北路62号1栋101'}
person1 |= person2
print(person1) # {'name': '王大锤', 'age': 25, 'height': 178, 'addr': '成都市武侯区科华北路62号1栋101'}
```
可以通过`pop`或`popitem`方法从字典中删除元素,前者会返回(获得)键对应的值,但是如果字典中不存在指定的键,会引发`KeyError`错误;后者在删除元素时,会返回(获得)键和值组成的二元组。字典的`clear`方法会清空字典中所有的键值对,代码如下所示。
```python
person = {'name': '王大锤', 'age': 25, 'height': 178, 'addr': '成都市武侯区科华北路62号1栋101'}
print(person.pop('age')) # 25
print(person) # {'name': '王大锤', 'height': 178, 'addr': '成都市武侯区科华北路62号1栋101'}
print(person.popitem()) # ('addr', '成都市武侯区科华北路62号1栋101')
print(person) # {'name': '王大锤', 'height': 178}
person.clear()
print(person) # {}
```
跟列表一样,从字典中删除元素也可以使用`del`关键字,在删除元素的时候如果指定的键索引不到对应的值,一样会引发`KeyError`错误,具体的做法如下所示。
```python
person = {'name': '王大锤', 'age': 25, 'height': 178, 'addr': '成都市武侯区科华北路62号1栋101'}
del person['age']
del person['addr']
print(person) # {'name': '王大锤', 'height': 178}
```
### 字典的应用
我们通过几个简单的例子来看看如何使用字典类型解决一些实际的问题。
**例子1**:输入一段话,统计每个英文字母出现的次数,按出现次数从高到低输出。
```python
sentence = input('请输入一段话: ')
counter = {}
for ch in sentence:
if 'A' <= ch <= 'Z' or 'a' <= ch <= 'z':
counter[ch] = counter.get(ch, 0) + 1
sorted_keys = sorted(counter, key=counter.get, reverse=True)
for key in sorted_keys:
print(f'{key} 出现了 {counter[key]} 次.')
```
输入:
```
Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.
```
输出:
```
e 出现了 27 次.
n 出现了 21 次.
a 出现了 18 次.
i 出现了 18 次.
s 出现了 16 次.
t 出现了 16 次.
o 出现了 14 次.
h 出现了 13 次.
r 出现了 10 次.
d 出现了 9 次.
l 出现了 9 次.
g 出现了 6 次.
u 出现了 6 次.
f 出现了 6 次.
c 出现了 6 次.
y 出现了 5 次.
b 出现了 5 次.
m 出现了 4 次.
p 出现了 3 次.
w 出现了 2 次.
v 出现了 2 次.
M 出现了 1 次.
k 出现了 1 次.
x 出现了 1 次.
```
**例子2**:在一个字典中保存了股票的代码和价格,找出股价大于100元的股票并创建一个新的字典。
> **说明**:可以用字典的生成式语法来创建这个新字典。
```python
stocks = {
'AAPL': 191.88,
'GOOG': 1186.96,
'IBM': 149.24,
'ORCL': 48.44,
'ACN': 166.89,
'FB': 208.09,
'SYMC': 21.29
}
stocks2 = {key: value for key, value in stocks.items() if value > 100}
print(stocks2)
```
输出:
```
{'AAPL': 191.88, 'GOOG': 1186.96, 'IBM': 149.24, 'ACN': 166.89, 'FB': 208.09}
```
### 总结
Python 程序中的字典跟现实生活中字典非常像,允许我们**以键值对的形式保存数据**,再**通过键访问对应的值**。字典是一种非常**有利于数据检索**的数据类型,但是需要再次提醒大家,**字典中的键必须是不可变类型**,列表、集合、字典等类型的数据都不能作为字典的键。
================================================
FILE: Day01-20/14.函数和模块.md
================================================
## 函数和模块
在讲解本节课的内容之前,我们先来研究一道数学题,请说出下面的方程有多少组正整数解。
$$
x_{1} + x_{2} + x_{3} + x_{4} = 8
$$
你可能已经想到了,这个问题其实等同于将 8 个苹果分成四组且每组至少一个苹果有多少种方案,也等价于在分隔 8 个苹果的 7 个间隙之间放入三个隔断将苹果分成四组有多少种方案,所以答案是 $\small{C_{7}^{3} = 35}$ ,其中 $\small{C_{7}^{3}}$ 代表 7 选 3 的组合数,其计算公式如下所示。
$$
C_m^n = \frac {m!} {n!(m-n)!}
$$
根据之前学习的知识,我们可以用循环做累乘的方式分别计算出 $\small{m!}$ 、 $\small{n!}$ 和 $\small{(m-n)!}$ ,然后再通过除法运算得到组合数 $\small{C_{m}^{n}}$ ,代码如下所示。
```python
"""
输入m和n,计算组合数C(m,n)的值
Version: 1.0
Author: 骆昊
"""
m = int(input('m = '))
n = int(input('n = '))
# 计算m的阶乘
fm = 1
for num in range(1, m + 1):
fm *= num
# 计算n的阶乘
fn = 1
for num in range(1, n + 1):
fn *= num
# 计算m-n的阶乘
fk = 1
for num in range(1, m - n + 1):
fk *= num
# 计算C(M,N)的值
print(fm // fn // fk)
```
输入:
```
m = 7
n = 3
```
输出:
```
35
```
不知大家是否注意到,上面的代码中我们做了三次求阶乘的操作,虽然 $\small{m}$ 、 $\small{n}$ 、 $\small{m - n}$ 的值各不相同,但是三段代码并没有实质性的区别,属于重复代码。世界级的编程大师*Martin Fowler*曾经说过:“**代码有很多种坏味道,重复是最坏的一种!**”。要写出高质量的代码,首先就要解决重复代码的问题。对于上面的代码来说,我们可以将求阶乘的功能封装到一个称为“函数”的代码块中,在需要计算阶乘的地方,我们只需“调用函数”即可实现对求阶乘功能的复用。
### 定义函数
数学上的函数通常形如 $\small{y = f(x)}$ 或者 $\small{z = g(x, y)}$ 这样的形式,在 $\small{y = f(x)}$ 中, $\small{f}$ 是函数的名字, $\small{x}$ 是函数的自变量, $\small{y}$ 是函数的因变量;而在 $\small{z = g(x, y)}$ 中, $\small{g}$ 是函数名, $\small{x}$ 和 $\small{y}$ 是函数的自变量, $\small{z}$ 是函数的因变量。Python 中的函数跟这个结构是一致的,每个函数都有自己的名字、自变量和因变量。我们通常把 Python 函数的自变量称为函数的参数,而因变量称为函数的返回值。
Python 中可以使用`def`关键字来定义函数,和变量一样每个函数也应该有一个漂亮的名字,命名规则跟变量的命名规则是一样的(大家赶紧想想我们之前讲过的变量的命名规则)。在函数名后面的圆括号中可以设置函数的参数,也就是我们刚才说的函数的自变量,而函数执行完成后,我们会通过`return`关键字来返回函数的执行结果,这就是我们刚才说的函数的因变量。如果函数中没有`return`语句,那么函数会返回代表空值的`None`。另外,函数也可以没有自变量(参数),但是函数名后面的圆括号是必须有的。一个函数要做的事情(要执行的代码),是通过代码缩进的方式放到函数定义行之后,跟之前分支和循环结构的代码块类似,如下图所示。
下面,我们将之前代码中求阶乘的操作放到一个函数中,通过这种方式来重构上面的代码。**所谓重构,是在不影响代码执行结果的前提下对代码的结构进行调整**,重构之后的代码如下所示。
```python
"""
输入m和n,计算组合数C(m,n)的值
Version: 1.1
Author: 骆昊
"""
# 通过关键字def定义求阶乘的函数
# 自变量(参数)num是一个非负整数
# 因变量(返回值)是num的阶乘
def fac(num):
result = 1
for n in range(2, num + 1):
result *= n
return result
m = int(input('m = '))
n = int(input('n = '))
# 计算阶乘的时候不需要写重复的代码而是直接调用函数
# 调用函数的语法是在函数名后面跟上圆括号并传入参数
print(fac(m) // fac(n) // fac(m - n))
```
大家可以感受下,上面的代码是不是比之前的版本更加简单优雅。更为重要的是,我们定义的求阶乘函数`fac`还可以在其他需要求阶乘的代码中重复使用。所以,**使用函数可以帮助我们将功能上相对独立且会被重复使用的代码封装起来**,当我们需要这些的代码,不是把重复的代码再编写一遍,而是**通过调用函数实现对既有代码的复用**。事实上,Python 标准库的`math`模块中,已经有一个名为`factorial`的函数实现了求阶乘的功能,我们可以直接用`import math`导入`math`模块,然后使用`math.factorial`来调用求阶乘的函数;我们也可以通过`from math import factorial`直接导入`factorial`函数来使用它,代码如下所示。
```python
"""
输入m和n,计算组合数C(m,n)的值
Version: 1.2
Author: 骆昊
"""
from math import factorial
m = int(input('m = '))
n = int(input('n = '))
print(factorial(m) // factorial(n) // factorial(m - n))
```
将来我们使用的函数,要么是自定义的函数,要么是 Python 标准库或者三方库中提供的函数,如果已经有现成的可用的函数,我们就没有必要自己去定义,“**重复发明轮子**”是一件非常糟糕的事情。对于上面的代码,如果你觉得`factorial`这个名字太长,书写代码的时候不是特别方便,我们在导入函数的时候还可以通过`as`关键字为其别名。在调用函数的时候,我们可以用函数的别名,而不再使用它之前的名字,代码如下所示。
```python
"""
输入m和n,计算组合数C(m,n)的值
Version: 1.3
Author: 骆昊
"""
from math import factorial as f
m = int(input('m = '))
n = int(input('n = '))
print(f(m) // f(n) // f(m - n))
```
### 函数的参数
#### 位置参数和关键字参数
我们再来写一个函数,根据给出的三条边的长度判断是否可以构成三角形,如果可以构成三角形则返回`True`,否则返回`False`,代码如下所示。
```python
def make_judgement(a, b, c):
"""判断三条边的长度能否构成三角形"""
return a + b > c and b + c > a and a + c > b
```
上面`make_judgement`函数有三个参数,这种参数叫做位置参数,在调用函数时通常按照从左到右的顺序依次传入,而且传入参数的数量必须和定义函数时参数的数量相同,如下所示。
```python
print(make_judgement(1, 2, 3)) # False
print(make_judgement(4, 5, 6)) # True
```
如果不想按照从左到右的顺序依次给出`a`、`b`、`c` 三个参数的值,也可以使用关键字参数,通过“参数名=参数值”的形式为函数传入参数,如下所示。
```python
print(make_judgement(b=2, c=3, a=1)) # False
print(make_judgement(c=6, b=4, a=5)) # True
```
在定义函数时,我们可以在参数列表中用`/`设置**强制位置参数**(*positional-only arguments*),用`*`设置**命名关键字参数**。所谓强制位置参数,就是调用函数时只能按照参数位置来接收参数值的参数;而命名关键字参数只能通过“参数名=参数值”的方式来传递和接收参数,大家可以看看下面的例子。
```python
# /前面的参数是强制位置参数
def make_judgement(a, b, c, /):
"""判断三条边的长度能否构成三角形"""
return a + b > c and b + c > a and a + c > b
# 下面的代码会产生TypeError错误,错误信息提示“强制位置参数是不允许给出参数名的”
# TypeError: make_judgement() got some positional-only arguments passed as keyword arguments
# print(make_judgement(b=2, c=3, a=1))
```
> **说明**:强制位置参数是 Python 3.8 引入的新特性,在使用低版本的 Python 解释器时需要注意。
```python
# *后面的参数是命名关键字参数
def make_judgement(*, a, b, c):
"""判断三条边的长度能否构成三角形"""
return a + b > c and b + c > a and a + c > b
# 下面的代码会产生TypeError错误,错误信息提示“函数没有位置参数但却给了3个位置参数”
# TypeError: make_judgement() takes 0 positional arguments but 3 were given
# print(make_judgement(1, 2, 3))
```
#### 参数的默认值
Python 中允许函数的参数拥有默认值,我们可以把之前讲过的一个例子“CRAPS赌博游戏”(《第07课:分支和循环结构的应用》)中摇色子获得点数的功能封装到函数中,代码如下所示。
```python
from random import randrange
# 定义摇色子的函数
# 函数的自变量(参数)n表示色子的个数,默认值为2
# 函数的因变量(返回值)表示摇n颗色子得到的点数
def roll_dice(n=2):
total = 0
for _ in range(n):
total += randrange(1, 7)
return total
# 如果没有指定参数,那么n使用默认值2,表示摇两颗色子
print(roll_dice())
# 传入参数3,变量n被赋值为3,表示摇三颗色子获得点数
print(roll_dice(3))
```
我们再来看一个更为简单的例子。
```python
def add(a=0, b=0, c=0):
"""三个数相加求和"""
return a + b + c
# 调用add函数,没有传入参数,那么a、b、c都使用默认值0
print(add()) # 0
# 调用add函数,传入一个参数,该参数赋值给变量a, 变量b和c使用默认值0
print(add(1)) # 1
# 调用add函数,传入两个参数,分别赋值给变量a和b,变量c使用默认值0
print(add(1, 2)) # 3
# 调用add函数,传入三个参数,分别赋值给a、b、c三个变量
print(add(1, 2, 3)) # 6
```
需要注意的是,**带默认值的参数必须放在不带默认值的参数之后**,否则将产生`SyntaxError`错误,错误消息是:`non-default argument follows default argument`,翻译成中文的意思是“没有默认值的参数放在了带默认值的参数后面”。
#### 可变参数
Python 语言中可以通过星号表达式语法让函数支持可变参数。所谓可变参数指的是在调用函数时,可以向函数传入`0`个或任意多个参数。将来我们以团队协作的方式开发商业项目时,很有可能要设计函数给其他人使用,但有的时候我们并不知道函数的调用者会向该函数传入多少个参数,这个时候可变参数就能派上用场。
下面的代码演示了如何使用可变位置参数实现对任意多个数求和的`add`函数,调用函数时传入的参数会保存到一个元组,通过对该元组的遍历,可以获取传入函数的参数。
```python
# 用星号表达式来表示args可以接收0个或任意多个参数
# 调用函数时传入的n个参数会组装成一个n元组赋给args
# 如果一个参数都没有传入,那么args会是一个空元组
def add(*args):
total = 0
# 对保存可变参数的元组进行循环遍历
for val in args:
# 对参数进行了类型检查(数值型的才能求和)
if type(val) in (int, float):
total += val
return total
# 在调用add函数时可以传入0个或任意多个参数
print(add()) # 0
print(add(1)) # 1
print(add(1, 2, 3)) # 6
print(add(1, 2, 'hello', 3.45, 6)) # 12.45
```
如果我们希望通过“参数名=参数值”的形式传入若干个参数,具体有多少个参数也是不确定的,我们还可以给函数添加可变关键字参数,把传入的关键字参数组装到一个字典中,代码如下所示。
```python
# 参数列表中的**kwargs可以接收0个或任意多个关键字参数
# 调用函数时传入的关键字参数会组装成一个字典(参数名是字典中的键,参数值是字典中的值)
# 如果一个关键字参数都没有传入,那么kwargs会是一个空字典
def foo(*args, **kwargs):
print(args)
print(kwargs)
foo(3, 2.1, True, name='骆昊', age=43, gpa=4.95)
```
输出:
```
(3, 2.1, True)
{'name': '骆昊', 'age': 43, 'gpa': 4.95}
```
### 用模块管理函数
不管用什么样的编程语言来写代码,给变量、函数起名字都是一个让人头疼的问题,因为我们会遇到**命名冲突**这种尴尬的情况。最简单的场景就是在同一个`.py`文件中定义了两个同名的函数,如下所示。
```python
def foo():
print('hello, world!')
def foo():
print('goodbye, world!')
foo() # 大家猜猜调用foo函数会输出什么
```
当然上面的这种情况我们很容易就能避免,但是如果项目是团队协作多人开发的时候,团队中可能有多个程序员都定义了名为`foo`的函数,这种情况下怎么解决命名冲突呢?答案其实很简单,Python 中每个文件就代表了一个模块(module),我们在不同的模块中可以有同名的函数,在使用函数的时候,我们通过`import`关键字导入指定的模块再使用**完全限定名**(`模块名.函数名`)的调用方式,就可以区分到底要使用的是哪个模块中的`foo`函数,代码如下所示。
`module1.py`
```python
def foo():
print('hello, world!')
```
`module2.py`
```python
def foo():
print('goodbye, world!')
```
`test.py`
```python
import module1
import module2
# 用“模块名.函数名”的方式(完全限定名)调用函数,
module1.foo() # hello, world!
module2.foo() # goodbye, world!
```
在导入模块时,还可以使用`as`关键字对模块进行别名,这样我们可以使用更为简短的完全限定名。
`test.py`
```python
import module1 as m1
import module2 as m2
m1.foo() # hello, world!
m2.foo() # goodbye, world!
```
上面两段代码,我们导入的是定义函数的模块,我们也可以使用`from...import...`语法从模块中直接导入需要使用的函数,代码如下所示。
`test.py`
```python
from module1 import foo
foo() # hello, world!
from module2 import foo
foo() # goodbye, world!
```
但是,如果我们如果从两个不同的模块中导入了同名的函数,后面导入的函数会替换掉之前的导入,就像下面的代码,调用`foo`会输出`goodbye, world!`,因为我们先导入了`module1`的`foo`,后导入了`module2`的`foo` 。如果两个`from...import...`反过来写,那就是另外一番光景了。
`test.py`
```python
from module1 import foo
from module2 import foo
foo() # goodbye, world!
```
如果想在上面的代码中同时使用来自两个模块的`foo`函数还是有办法的,大家可能已经猜到了,还是用`as`关键字对导入的函数进行别名,代码如下所示。
`test.py`
```python
from module1 import foo as f1
from module2 import foo as f2
f1() # hello, world!
f2() # goodbye, world!
```
### 标准库中的模块和函数
Python 标准库中提供了大量的模块和函数来简化我们的开发工作,我们之前用过的`random`模块就为我们提供了生成随机数和进行随机抽样的函数;而`time`模块则提供了和时间操作相关的函数;我们之前用到过的`math`模块中还包括了计算正弦、余弦、指数、对数等一系列的数学函数。随着我们深入学习 Python 语言,我们还会用到更多的模块和函数。
Python 标准库中还有一类函数是不需要`import`就能够直接使用的,我们将其称之为**内置函数**,这些内置函数不仅有用而且还很常用,下面的表格列出了一部分的内置函数。
| 函数 | 说明 |
| ------- | ------------------------------------------------------------ |
| `abs` | 返回一个数的绝对值,例如:`abs(-1.3)`会返回`1.3`。 |
| `bin` | 把一个整数转换成以`'0b'`开头的二进制字符串,例如:`bin(123)`会返回`'0b1111011'`。 |
| `chr` | 将Unicode编码转换成对应的字符,例如:`chr(8364)`会返回`'€'`。 |
| `hex` | 将一个整数转换成以`'0x'`开头的十六进制字符串,例如:`hex(123)`会返回`'0x7b'`。 |
| `input` | 从输入中读取一行,返回读到的字符串。 |
| `len` | 获取字符串、列表等的长度。 |
| `max` | 返回多个参数或一个可迭代对象中的最大值,例如:`max(12, 95, 37)`会返回`95`。 |
| `min` | 返回多个参数或一个可迭代对象中的最小值,例如:`min(12, 95, 37)`会返回`12`。 |
| `oct` | 把一个整数转换成以`'0o'`开头的八进制字符串,例如:`oct(123)`会返回`'0o173'`。 |
| `open` | 打开一个文件并返回文件对象。 |
| `ord` | 将字符转换成对应的Unicode编码,例如:`ord('€')`会返回`8364`。 |
| `pow` | 求幂运算,例如:`pow(2, 3)`会返回`8`;`pow(2, 0.5)`会返回`1.4142135623730951`。 |
| `print` | 打印输出。 |
| `range` | 构造一个范围序列,例如:`range(100)`会产生`0`到`99`的整数序列。 |
| `round` | 按照指定的精度对数值进行四舍五入,例如:`round(1.23456, 4)`会返回`1.2346`。 |
| `sum` | 对一个序列中的项从左到右进行求和运算,例如:`sum(range(1, 101))`会返回`5050`。 |
| `type` | 返回对象的类型,例如:`type(10)`会返回`int`;而` type('hello')`会返回`str`。 |
### 总结
**函数是对功能相对独立且会重复使用的代码的封装**。学会使用定义和使用函数,就能够写出更为优质的代码。当然,Python 语言的标准库中已经为我们提供了大量的模块和常用的函数,用好这些模块和函数就能够用更少的代码做更多的事情;如果这些模块和函数不能满足我们的要求,可能就需要自定义函数,然后再通过模块的概念来管理这些自定义函数。
================================================
FILE: Day01-20/15.函数应用实战.md
================================================
## 函数应用实战
### 例子1:随机验证码
设计一个生成随机验证码的函数,验证码由数字和英文大小写字母构成,长度可以通过参数设置。
```python
import random
import string
ALL_CHARS = string.digits + string.ascii_letters
def generate_code(*, code_len=4):
"""
生成指定长度的验证码
:param code_len: 验证码的长度(默认4个字符)
:return: 由大小写英文字母和数字构成的随机验证码字符串
"""
return ''.join(random.choices(ALL_CHARS, k=code_len))
```
> **说明1**:`string`模块的`digits`代表0到9的数字构成的字符串`'0123456789'`,`string`模块的`ascii_letters`代表大小写英文字母构成的字符串`'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'`。
>
> **说明2**:`random`模块的`sample`和`choices`函数都可以实现随机抽样,`sample`实现无放回抽样,这意味着抽样取出的元素是不重复的;`choices`实现有放回抽样,这意味着可能会重复选中某些元素。这两个函数的第一个参数代表抽样的总体,而参数`k`代表样本容量,需要说明的是`choices`函数的参数`k`是一个命名关键字参数,在传参时必须指定参数名。
可以用下面的代码生成5组随机验证码来测试上面的函数。
```python
for _ in range(5):
print(generate_code())
```
输出:
```
59tZ
QKU5
izq8
IBBb
jIfX
```
或者
```python
for _ in range(5):
print(generate_code(code_len=6))
```
输出:
```
FxJucw
HS4H9G
0yyXfz
x7fohf
ReO22w
```
> **说明**:我们设计的`generate_code`函数的参数是命名关键字参数,由于它有默认值,可以不给它传值,使用默认值4。如果需要给函数传入参数,必须指定参数名`code_len`。
### 例子2:判断素数
设计一个判断给定的大于1的正整数是不是质数的函数。质数是只能被1和自身整除的正整数(大于1),如果一个大于 1 的正整数 $\small{N}$ 是质数,那就意味着在 2 到 $\small{N-1}$ 之间都没有它的因子。
```python
def is_prime(num: int) -> bool:
"""
判断一个正整数是不是质数
:param num: 大于1的正整数
:return: 如果num是质数返回True,否则返回False
"""
for i in range(2, int(num ** 0.5) + 1):
if num % i == 0:
return False
return True
```
> **说明1**:上面`is_prime`函数的参数`num`后面的`: int`用来标注参数的类型,虽然它对代码的执行结果不产生任何影响,但是很好的增强了代码的可读性。同理,参数列表后面的`-> bool`用来标注函数返回值的类型,它也不会对代码的执行结果产生影响,但是却让我们清楚的知道,调用函数会得到一个布尔值,要么是`True`,要么是`False`。
>
> **说明2**:上面的循环并不需要从 2 循环到 $\small{N-1}$ ,因为如果循环进行到 $\small{\sqrt{N}}$ 时,还没有找到$\small{N}$的因子,那么 $\small{\sqrt{N}}$ 之后也不会出现 $\small{N}$ 的因子,大家可以自己想一想这是为什么。
### 例子3:最大公约数和最小公倍数
设计计算两个正整数最大公约数和最小公倍数的函数。 $\small{x}$ 和 $\small{y}$ 的最大公约数是能够同时整除 $\small{x}$ 和 $\small{y}$ 的最大整数,如果 $\small{x}$ 和 $\small{y}$ 互质,那么它们的最大公约数为 1; $\small{x}$ 和 $\small{y}$ 的最小公倍数是能够同时被 $\small{x}$ 和 $\small{y}$ 整除的最小正整数,如果 $\small{x}$ 和 $\small{y}$ 互质,那么它们的最小公倍数为 $\small{x \times y}$ 。需要提醒大家注意的是,计算最大公约数和最小公倍数是两个不同的功能,应该设计成两个函数,而不是把两个功能放到同一个函数中。
```python
def lcm(x: int, y: int) -> int:
"""求最小公倍数"""
return x * y // gcd(x, y)
def gcd(x: int, y: int) -> int:
"""求最大公约数"""
while y % x != 0:
x, y = y % x, x
return x
```
> **说明**:函数之间可以相互调用,上面求最小公倍数的`lcm`函数调用了求最大公约数的`gcd`函数,通过 $\frac{x \times y}{ gcd(x, y)}$ 来计算最小公倍数。
### 例子4:数据统计
假设样本数据保存一个列表中,设计计算样本数据描述性统计信息的函数。描述性统计信息通常包括:算术平均值、中位数、极差(最大值和最小值的差)、方差、标准差、变异系数等,计算公式如下所示。
样本均值(sample mean):
$$
\bar{x} = \frac{\sum_{i=1}^{n}x_{i}}{n} = \frac{x_{1}+x_{2}+\cdots +x_{n}}{n}
$$
样本方差(sample variance):
$$
s^2 = \frac {\sum_{i=1}^{n}(x_i - \bar{x})^2} {n-1}
$$
样本标准差(sample standard deviation):
$$
s = \sqrt{\frac{\sum_{i=1}^{n}(x_i - \bar{x})^2}{n-1}}
$$
变异系数(coefficient of sample variation):
$$
CV = \frac{s}{\bar{x}}
$$
```python
def ptp(data):
"""极差(全距)"""
return max(data) - min(data)
def mean(data):
"""算术平均"""
return sum(data) / len(data)
def median(data):
"""中位数"""
temp, size = sorted(data), len(data)
if size % 2 != 0:
return temp[size // 2]
else:
return mean(temp[size // 2 - 1:size // 2 + 1])
def var(data, ddof=1):
"""方差"""
x_bar = mean(data)
temp = [(num - x_bar) ** 2 for num in data]
return sum(temp) / (len(temp) - ddof)
def std(data, ddof=1):
"""标准差"""
return var(data, ddof) ** 0.5
def cv(data, ddof=1):
"""变异系数"""
return std(data, ddof) / mean(data)
def describe(data):
"""输出描述性统计信息"""
print(f'均值: {mean(data)}')
print(f'中位数: {median(data)}')
print(f'极差: {ptp(data)}')
print(f'方差: {var(data)}')
print(f'标准差: {std(data)}')
print(f'变异系数: {cv(data)}')
```
> **说明1**:中位数是将数据按照升序或降序排列后位于中间的数,它描述了数据的中等水平。中位数的计算分两种情况:当数据体量$n$为奇数时,中位数是位于 $\frac{n + 1}{2}$ 位置的元素;当数据体量 $\small{n}$ 为偶数时,中位数是位于 $\frac{n}{2}$ 和 $\frac{n}{2} + 1$ 两个位置元素的均值。
>
> **说明2**:计算方差和标准差的函数中有一个名为`ddof`的参数,它代表了可以调整的自由度,默认值为 1。在计算样本方差和样本标准差时,需要进行自由度校正;如果要计算总体方差和总体标准差,可以将`ddof`参数赋值为 0,即不需要进行自由度校正。
>
> **说明3**:`describe`函数将上面封装好的统计函数组装到一起,用于输出数据的描述性统计信息。事实上,Python 标准库中有一个名为`statistics`的模块,它已经把获取描述性统计信息的函数封装好了,有兴趣的读者可以自行了解。
### 例子5:双色球随机选号
我们用函数重构之前讲过的双色球随机选号的例子(《第09课:常用数据结构之列表-2》),将生成随机号码和输出一组号码的功能分别封装到两个函数中,然后通过调用函数实现机选`N`注号码的功能。
```python
"""
双色球随机选号程序
Author: 骆昊
Version: 1.3
"""
import random
RED_BALLS = [i for i in range(1, 34)]
BLUE_BALLS = [i for i in range(1, 17)]
def choose():
"""
生成一组随机号码
:return: 保存随机号码的列表
"""
selected_balls = random.sample(RED_BALLS, 6)
selected_balls.sort()
selected_balls.append(random.choice(BLUE_BALLS))
return selected_balls
def display(balls):
"""
格式输出一组号码
:param balls: 保存随机号码的列表
"""
for ball in balls[:-1]:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
print(f'\033[034m{balls[-1]:0>2d}\033[0m')
n = int(input('生成几注号码: '))
for _ in range(n):
display(choose())
```
> **说明**:大家看看`display(choose())`这行代码,这里我们先通过`choose`函数获得一组随机号码,然后把`choose`函数的返回值作为`display`函数的参数,通过`display`函数将选中的随机号码显示出来。重构之后的代码逻辑非常清晰,代码的可读性更强了。如果有人为你封装了这两个函数,你仅仅是函数的调用者,其实你根本不用关心`choose`函数和`display`函数的内部实现,你只需要知道调用`choose`函数可以生成一组随机号码,而调用`display`函数传入一个列表,就可以输出这组号码。将来我们使用各种各样的 Python 三方库时,我们也根本不关注它们的底层实现,我们需要知道的仅仅是调用哪个函数可以解决问题。
### 总结
在写代码尤其是开发商业项目的时候,一定要有意识的**将相对独立且重复使用的功能封装成函数**,这样不管是自己还是团队的其他成员都可以通过调用函数的方式来使用这些功能,减少工作中那些重复且乏味的劳动。
================================================
FILE: Day01-20/16.函数使用进阶.md
================================================
## 函数使用进阶
我们继续探索定义和使用函数的相关知识。通过前面的学习,我们知道了函数有自变量(参数)和因变量(返回值),自变量可以是任意的数据类型,因变量也可以是任意的数据类型,那么这里就有一个小问题,我们能不能用函数作为函数的参数,用函数作为函数的返回值?这里我们先说结论:**Python 中的函数是“一等函数”**,所谓“一等函数”指的就是函数可以赋值给变量,函数可以作为函数的参数,函数也可以作为函数的返回值。把一个函数作为其他函数的参数或返回值的用法,我们通常称之为“高阶函数”。
### 高阶函数
我们回到之前讲过的一个例子,设计一个函数,传入任意多个参数,对其中`int`类型或`float`类型的元素实现求和操作。我们对之前的代码稍作调整,让整个代码更加紧凑一些,如下所示。
```python
def calc(*args, **kwargs):
items = list(args) + list(kwargs.values())
result = 0
for item in items:
if type(item) in (int, float):
result += item
return result
```
如果我们希望上面的`calc`函数不仅仅可以做多个参数的求和,还可以实现更多的甚至是自定义的二元运算,我们该怎么做呢?上面的代码只能求和是因为函数中使用了`+=`运算符,这使得函数跟加法运算形成了耦合关系,如果能解除这种耦合关系,函数的通用性和灵活性就会更好。解除耦合的办法就是将`+`运算符变成函数调用,并将其设计为函数的参数,代码如下所示。
```python
def calc(init_value, op_func, *args, **kwargs):
items = list(args) + list(kwargs.values())
result = init_value
for item in items:
if type(item) in (int, float):
result = op_func(result, item)
return result
```
注意,上面的函数增加了两个参数,其中`init_value`代表运算的初始值,`op_func`代表二元运算函数,为了调用修改后的函数,我们先定义做加法和乘法运算的函数,代码如下所示。
```python
def add(x, y):
return x + y
def mul(x, y):
return x * y
```
如果要做求和的运算,我们可以按照下面的方式调用`calc`函数。
```python
print(calc(0, add, 1, 2, 3, 4, 5)) # 15
```
如果要做求乘积运算,我们可以按照下面的方式调用`calc`函数。
```python
print(calc(1, mul, 1, 2, 3, 4, 5)) # 120
```
上面的`calc`函数通过将运算符变成函数的参数,实现了跟加法运算的解耦合,这是一种非常高明和实用的编程技巧,但对于最初学者来说可能会觉得难以理解,建议大家细品一下。需要注意上面的代码中,将函数作为参数传入其他函数和直接调用函数是有显著的区别的,调用函数需要在函数名后面跟上圆括号,而把函数作为参数时只需要函数名即可**。
如果我们没有提前定义好`add`和`mul`函数,也可以使用 Python 标准库中的`operator`模块提供的`add`和`mul`函数,它们分别代表了做加法和做乘法的二元运算,我们拿过来直接使用即可,代码如下所示。
```python
import operator
print(calc(0, operator.add, 1, 2, 3, 4, 5)) # 15
print(calc(1, operator.mul, 1, 2, 3, 4, 5)) # 120
```
Python 内置函数中有不少高阶函数,我们前面提到过的`filter`和`map`函数就是高阶函数,前者可以实现对序列中元素的过滤,后者可以实现对序列中元素的映射,例如我们要去掉一个整数列表中的奇数,并对所有的偶数求平方得到一个新的列表,就可以直接使用这两个函数来做到,具体的做法是如下所示。
```python
def is_even(num):
"""判断num是不是偶数"""
return num % 2 == 0
def square(num):
"""求平方"""
return num ** 2
old_nums = [35, 12, 8, 99, 60, 52]
new_nums = list(map(square, filter(is_even, old_nums)))
print(new_nums) # [144, 64, 3600, 2704]
```
当然,要完成上面代码的功能,也可以使用列表生成式,列表生成式的做法更为简单优雅。
```python
old_nums = [35, 12, 8, 99, 60, 52]
new_nums = [num ** 2 for num in old_nums if num % 2 == 0]
print(new_nums) # [144, 64, 3600, 2704]
```
我们再来讨论一个内置函数`sorted`,它可以实现对容器型数据类型(如:列表、字典等)元素的排序。我们之前讲过`list`类型的`sort`方法,它实现了对列表元素的排序,`sorted`函数从功能上来讲跟列表的`sort`方法没有区别,但它会返回排序后的列表对象,而不是直接修改原来的列表,这一点我们称为**函数的无副作用设计**,也就是说调用函数除了产生返回值以外,不会对程序的状态或外部环境产生任何其他的影响。使用`sorted`函数排序时,可以通过高阶函数的形式自定义排序的规则,我们通过下面的例子加以说明。
```python
old_strings = ['in', 'apple', 'zoo', 'waxberry', 'pear']
new_strings = sorted(old_strings)
print(new_strings) # ['apple', 'in', 'pear', waxberry', 'zoo']
```
上面的代码对大家来说并不陌生,但是如果希望根据字符串的长度而不是字母表顺序对列表元素排序,我们可以向`sorted`函数传入一个名为`key`的参数,将`key`参数赋值为获取字符串长度的函数`len`,这个函数我们在之前的课程中讲到过,代码如下所示。
```python
old_strings = ['in', 'apple', 'zoo', 'waxberry', 'pear']
new_strings = sorted(old_strings, key=len)
print(new_strings) # ['in', 'zoo', 'pear', 'apple', 'waxberry']
```
> **说明**:列表类型的`sort`方法也有同样的`key`参数,有兴趣的读者可以自行尝试。
### Lambda函数
在使用高阶函数的时候,如果作为参数或者返回值的函数本身非常简单,一行代码就能够完成,也不需要考虑对函数的复用,那么我们可以使用 lambda 函数。Python 中的 lambda 函数是没有的名字函数,所以很多人也把它叫做**匿名函数**,lambda 函数只能有一行代码,代码中的表达式产生的运算结果就是这个匿名函数的返回值。之前的代码中,我们写的`is_even`和`square`函数都只有一行代码,我们可以考虑用 lambda 函数来替换掉它们,代码如下所示。
```python
old_nums = [35, 12, 8, 99, 60, 52]
new_nums = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, old_nums)))
print(new_nums) # [144, 64, 3600, 2704]
```
通过上面的代码可以看出,定义 lambda 函数的关键字是`lambda`,后面跟函数的参数,如果有多个参数用逗号进行分隔;冒号后面的部分就是函数的执行体,通常是一个表达式,表达式的运算结果就是 lambda 函数的返回值,不需要写`return` 关键字。
前面我们说过,Python 中的函数是“一等函数”,函数是可以直接赋值给变量的。在学习了 lambda 函数之后,前面我们写过的一些函数就可以用一行代码来实现它们了,大家可以看看能否理解下面的求阶乘和判断素数的函数。
```python
import functools
import operator
# 用一行代码实现计算阶乘的函数
fac = lambda n: functools.reduce(operator.mul, range(2, n + 1), 1)
# 用一行代码实现判断素数的函数
is_prime = lambda x: all(map(lambda f: x % f, range(2, int(x ** 0.5) + 1)))
# 调用Lambda函数
print(fac(6)) # 720
print(is_prime(37)) # True
```
> **提示1**:上面使用的`reduce`函数是 Python 标准库`functools`模块中的函数,它可以实现对一组数据的归约操作,类似于我们之前定义的`calc`函数,第一个参数是代表运算的函数,第二个参数是运算的数据,第三个参数是运算的初始值。很显然,`reduce`函数也是高阶函数,它和`filter`函数、`map`函数一起构成了处理数据中非常关键的三个动作:**过滤**、**映射**和**归约**。
>
> **提示2**:上面判断素数的 lambda 函数通过`range`函数构造了从 2 到 $\small{\sqrt{x}}$ 的范围,检查这个范围有没有`x`的因子。`all`函数也是 Python 内置函数,如果传入的序列中所有的布尔值都是`True`,`all`函数返回`True`,否则`all`函数返回`False`。
### 偏函数
偏函数是指固定函数的某些参数,生成一个新的函数,这样就无需在每次调用函数时都传递相同的参数。在 Python 语言中,我们可以使用`functools`模块的`partial`函数来创建偏函数。例如,`int`函数在默认情况下可以将字符串视为十进制整数进行类型转换,如果我们修修改它的`base`参数,就可以定义出三个新函数,分别用于将二进制、八进制、十六进制字符串转换为整数,代码如下所示。
```python
import functools
int2 = functools.partial(int, base=2)
int8 = functools.partial(int, base=8)
int16 = functools.partial(int, base=16)
print(int('1001')) # 1001
print(int2('1001')) # 9
print(int8('1001')) # 513
print(int16('1001')) # 4097
```
不知大家是否注意到,`partial`函数的第一个参数和返回值都是函数,它将传入的函数处理成一个新的函数返回。通过构造偏函数,我们可以结合实际的使用场景将原函数变成使用起来更为便捷的新函数,不知道大家有没有觉得这波操作很有意思。
### 总结
Python 中的函数是一等函数,可以赋值给变量,也可以作为函数的参数和返回值,这也就意味着我们可以在 Python 中使用高阶函数。高阶函数的概念对新手并不友好,但它却带来了函数设计上的灵活性。如果我们要定义的函数非常简单,只有一行代码,而且不需要函数名来复用它,我们可以使用 lambda 函数。
================================================
FILE: Day01-20/17.函数高级应用.md
================================================
## 函数高级应用
在上一个章节中,我们探索了 Python 中的高阶函数,相信大家对函数的定义和应用有了更深刻的认知。本章我们继续为大家讲解函数相关的知识,一个是 Python 中的特色语法装饰器,一个是函数的递归调用。
### 装饰器
Python 语言中,装饰器是“**用一个函数装饰另外一个函数并为其提供额外的能力**”的语法现象。装饰器本身是一个函数,它的参数是被装饰的函数,它的返回值是一个带有装饰功能的函数。通过前面的描述,相信大家已经听出来了,装饰器是一个高阶函数,它的参数和返回值都是函数。但是,装饰器的概念对编程语言的初学者来说,还是让人头疼的,下面我们先通过一个简单的例子来说明装饰器的作用。假设有名为`downlaod`和`upload`的两个函数,分别用于文件的下载和上传,如下所示。
```python
import random
import time
def download(filename):
"""下载文件"""
print(f'开始下载{filename}.')
time.sleep(random.random() * 6)
print(f'{filename}下载完成.')
def upload(filename):
"""上传文件"""
print(f'开始上传{filename}.')
time.sleep(random.random() * 8)
print(f'{filename}上传完成.')
download('MySQL从删库到跑路.avi')
upload('Python从入门到住院.pdf')
```
> **说明**:上面的代码用休眠一段随机时间的方式模拟了下载和上传文件需要花费一定的时间,并没有真正的联网上传下载文件。用 Python 语言实现联网上传下载文件也非常简单,后面我们会讲到相关的知识。
现在有一个新的需求,我们希望知道调用`download`和`upload`函数上传下载文件到底用了多少时间,这应该如何实现呢?相信很多小伙伴已经想到了,我们可以在函数开始执行的时候记录一个时间,在函数调用结束后记录一个时间,两个时间相减就可以计算出下载或上传的时间,代码如下所示。
```python
start = time.time()
download('MySQL从删库到跑路.avi')
end = time.time()
print(f'花费时间: {end - start:.2f}秒')
start = time.time()
upload('Python从入门到住院.pdf')
end = time.time()
print(f'花费时间: {end - start:.2f}秒')
```
通过上面的代码,我们可以在下载和上传文件时记录下耗费的时间,但不知道大家是否注意到,上面记录时间、计算和显示执行时间的代码都是重复代码。有编程经验的人都知道,**重复的代码是万恶之源**,那么有没有办法在不写重复代码的前提下,用一种简单优雅的方式记录下函数的执行时间呢?在 Python 语言中,装饰器就是解决这类问题的最佳选择。通过装饰器语法,我们可以把跟原来的业务(上传和下载)没有关系计时功能的代码封装到一个函数中,如果`upload`和`download`函数需要记录时间,我们直接把装饰器作用到这两个函数上即可。既然上面提到了,装饰器是一个高阶函数,它的参数和返回值都是函数,我们将记录时间的装饰器姑且命名为`record_time`,那么它的整体结构应该如下面的代码所示。
```python
def record_time(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
```
相信大家注意到了,`record_time`函数的参数`func`代表了一个被装饰的函数,函数里面定义的`wrapper`函数是带有装饰功能的函数,它会执行被装饰的函数`func`,它还需要返回在最后产生函数执行的返回值。不知大家是否留意到,上面的代码我在第4行和第6行留下了两个空行,这意味着我们可以这些地方添加代码来实现额外的功能。`record_time`函数最终会返回这个带有装饰功能的函数`wrapper`并通过它替代原函数`func`,当原函数`func`被`record_time`函数装饰后,我们调用它时其实调用的是`wrapper`函数,所以才获得了额外的能力。`wrapper`函数的参数比较特殊,由于我们要用`wrapper`替代原函数`func`,但是我们又不清楚原函数`func`会接受哪些参数,所以我们就通过可变参数和关键字参数照单全收,然后在调用`func`的时候,原封不动的全部给它。这里还要强调一下,Python 语言支持函数的嵌套定义,就像上面,我们可以在`record_time`函数中定义`wrapper`函数,这个操作在很多编程语言中并不被支持。
看懂这个结构后,我们就可以把记录时间的功能写到这个装饰器中,代码如下所示。
```python
import time
def record_time(func):
def wrapper(*args, **kwargs):
# 在执行被装饰的函数之前记录开始时间
start = time.time()
# 执行被装饰的函数并获取返回值
result = func(*args, **kwargs)
# 在执行被装饰的函数之后记录结束时间
end = time.time()
# 计算和显示被装饰函数的执行时间
print(f'{func.__name__}执行时间: {end - start:.2f}秒')
# 返回被装饰函数的返回值
return result
return wrapper
```
写装饰器虽然颇费周折,但是这是个一劳永逸的骚操作,将来再有记录函数执行时间的需求时,我们只需要添加上面的装饰器即可。使用上面的装饰器函数有两种方式,第一种方式就是直接调用装饰器函数,传入被装饰的函数并获得返回值,我们可以用这个返回值直接替代原来的函数,那么在调用时就已经获得了装饰器提供的额外的能力(记录执行时间),大家试试下面的代码就明白了。
```python
download = record_time(download)
upload = record_time(upload)
download('MySQL从删库到跑路.avi')
upload('Python从入门到住院.pdf')
```
在 Python 中,使用装饰器还有更为便捷的**语法糖**(编程语言中添加的某种语法,这种语法对语言的功能没有影响,但是使用更加便捷,代码的可读性也更强,我们将其称之为“语法糖”或“糖衣语法”),可以用`@装饰器函数`将装饰器函数直接放在被装饰的函数上,效果跟上面的代码相同。我们把完整的代码为大家罗列出来,大家可以再看看我们是如何定义和使用装饰器的。
```python
import random
import time
def record_time(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__}执行时间: {end - start:.2f}秒')
return result
return wrapper
@record_time
def download(filename):
print(f'开始下载{filename}.')
time.sleep(random.random() * 6)
print(f'{filename}下载完成.')
@record_time
def upload(filename):
print(f'开始上传{filename}.')
time.sleep(random.random() * 8)
print(f'{filename}上传完成.')
download('MySQL从删库到跑路.avi')
upload('Python从入门到住院.pdf')
```
上面的代码,我们通过装饰器语法糖为`download`和`upload`函数添加了装饰器,被装饰后的`download`和`upload`函数其实就是我们在装饰器中返回的`wrapper`函数,调用它们其实就是在调用`wrapper`函数,所以才有了记录函数执行时间的功能。
如果在代码的某些地方,我们想去掉装饰器的作用执行原函数,那么在定义装饰器函数的时候,需要做一点点额外的工作。Python 标准库`functools`模块的`wraps`函数也是一个装饰器,我们将它放在`wrapper`函数上,这个装饰器可以帮我们保留被装饰之前的函数,这样在需要取消装饰器时,可以通过被装饰函数的`__wrapped__`属性获得被装饰之前的函数。
```python
import random
import time
from functools import wraps
def record_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f'{func.__name__}执行时间: {end - start:.2f}秒')
return result
return wrapper
@record_time
def download(filename):
print(f'开始下载{filename}.')
time.sleep(random.random() * 6)
print(f'{filename}下载完成.')
@record_time
def upload(filename):
print(f'开始上传{filename}.')
time.sleep(random.random() * 8)
print(f'{filename}上传完成.')
# 调用装饰后的函数会记录执行时间
download('MySQL从删库到跑路.avi')
upload('Python从入门到住院.pdf')
# 取消装饰器的作用不记录执行时间
download.__wrapped__('MySQL必知必会.pdf')
upload.__wrapped__('Python从新手到大师.pdf')
```
**装饰器函数本身也可以参数化**,简单的说就是装饰器也是可以通过调用者传入的参数来进行定制的,这个知识点我们在后面用到的时候再为大家讲解。
### 递归调用
Python 中允许函数嵌套定义,也允许函数之间相互调用,而且一个函数还可以直接或间接的调用自身。函数自己调用自己称为递归调用,那么递归调用有什么用处呢?现实中,有很多问题的定义本身就是一个递归定义,例如我们之前讲到的阶乘,非负整数`N`的阶乘是`N`乘以`N-1`的阶乘,即 $\small{N! = N \times (N-1)!}$ ,定义的左边和右边都出现了阶乘的概念,所以这是一个递归定义。既然如此,我们可以使用递归调用的方式来写一个求阶乘的函数,代码如下所示。
```python
def fac(num):
if num in (0, 1):
return 1
return num * fac(num - 1)
```
上面的代码中,`fac`函数中又调用了`fac`函数,这就是所谓的递归调用。代码第2行的`if`条件叫做递归的收敛条件,简单的说就是什么时候要结束函数的递归调用,在计算阶乘时,如果计算到`0`或`1`的阶乘,就停止递归调用,直接返回`1`;代码第4行的`num * fac(num - 1)`是递归公式,也就是阶乘的递归定义。下面,我们简单的分析下,如果用`fac(5)`计算`5`的阶乘,整个过程会是怎样的。
```python
# 递归调用函数入栈
# 5 * fac(4)
# 5 * (4 * fac(3))
# 5 * (4 * (3 * fac(2)))
# 5 * (4 * (3 * (2 * fac(1))))
# 停止递归函数出栈
# 5 * (4 * (3 * (2 * 1)))
# 5 * (4 * (3 * 2))
# 5 * (4 * 6)
# 5 * 24
# 120
print(fac(5)) # 120
```
注意,函数调用会通过内存中称为“栈”(stack)的数据结构来保存当前代码的执行现场,函数调用结束后会通过这个栈结构恢复之前的执行现场。栈是一种先进后出的数据结构,这也就意味着最早入栈的函数最后才会返回,而最后入栈的函数会最先返回。例如调用一个名为`a`的函数,函数`a`的执行体中又调用了函数`b`,函数`b`的执行体中又调用了函数`c`,那么最先入栈的函数是`a`,最先出栈的函数是`c`。每进入一个函数调用,栈就会增加一层栈帧(stack frame),栈帧就是我们刚才提到的保存当前代码执行现场的结构;每当函数调用结束后,栈就会减少一层栈帧。通常,内存中的栈空间很小,因此递归调用的次数如果太多,会导致栈溢出(stack overflow),所以**递归调用一定要确保能够快速收敛**。我们可以尝试执行`fac(5000)`,看看是不是会提示`RecursionError`错误,错误消息为:`maximum recursion depth exceeded in comparison`(超出最大递归深度),其实就是发生了栈溢出。
如果我们使用官方的 Python 解释器(CPython),默认将函数调用的栈结构最大深度设置为`1000`层。如果超出这个深度,就会发生上面说的`RecursionError`。当然,我们可以使用`sys`模块的`setrecursionlimit`函数来改变递归调用的最大深度,但是我们不建议这样做,因为让递归快速收敛才是我们应该做的事情,否则就应该考虑使用循环递推而不是递归。
再举一个之前讲过的生成斐波那契数列的例子,因为斐波那契数列前两个数都是`1`,从第三个数开始,每个数是前两个数相加的和,可以记为`f(n) = f(n - 1) + f(n - 2)`,很显然这又是一个递归的定义,所以我们可以用下面的递归调用函数来计算第`n`个斐波那契数。
```python
def fib1(n):
if n in (1, 2):
return 1
return fib1(n - 1) + fib1(n - 2)
for i in range(1, 21):
print(fib1(i))
```
需要提醒大家,上面计算斐波那契数的代码虽然看起来非常简单明了,但执行性能是比较糟糕的。大家可以试一试,把上面代码`for`循环中`range`函数的第二个参数修改为`51`,即输出前50个斐波那契数,看看需要多长时间,也欢迎大家在评论区留下你的代码执行时间。至于为什么这么慢,大家可以自己思考一下原因。很显然,直接使用循环递推的方式获得斐波那契数列是更好的选择,代码如下所示。
```python
def fib2(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
```
除此以外,我们还可以使用 Python 标准库中`functools`模块的`lru_cache`函数来优化上面的递归代码。`lru_cache`函数是一个装饰器函数,我们将其置于上面的函数`fib1`之上,它可以缓存该函数的执行结果从而避免在递归调用的过程中产生大量的重复运算,这样代码的执行性能就有“飞一般”的提升。大家可以尝试输出前50个斐波那契数,看看加上装饰器以后代码需要执行多长时间,评论区见!
```python
from functools import lru_cache
@lru_cache()
def fib1(n):
if n in (1, 2):
return 1
return fib1(n - 1) + fib1(n - 2)
for i in range(1, 51):
print(i, fib1(i))
```
> **提示**:`lru_cache`函数是一个带参数的装饰器,所以上面第4行代码使用装饰器语法糖时,`lru_cache`后面要跟上圆括号。`lru_cache`函数有一个非常重要的参数叫`maxsize`,它可以用来定义缓存空间的大小,默认值是128。
### 总结
装饰器是 Python 语言中的特色语法,**可以通过装饰器来增强现有的函数**,这是一种非常有用的编程技巧。另一方面,通过函数递归调用,可以在代码层面将一些复杂的问题简单化,但是**递归调用一定要注意收敛条件和递归公式**,找到递归公式才有机会使用递归调用,而收敛条件则确保了递归调用能停下来。函数调用通过内存中的栈空间来保存现场和恢复现场,栈空间通常都很小,所以**递归如果不能迅速收敛,很可能会引发栈溢出错误,从而导致程序的崩溃**。
================================================
FILE: Day01-20/18.面向对象编程入门.md
================================================
## 面向对象编程入门
面向对象编程是一种非常流行的**编程范式**(programming paradigm),所谓编程范式就是**程序设计的方法论**,简单的说就是程序员对程序的认知和理解以及他们编写代码的方式。
在前面的课程中,我们说过“**程序是指令的集合**”,运行程序时,程序中的语句会变成一条或多条指令,然后由CPU(中央处理器)去执行。为了简化程序的设计,我们又讲到了函数,**把相对独立且经常重复使用的代码放置到函数中**,在需要使用这些代码的时候调用函数即可。如果一个函数的功能过于复杂和臃肿,我们又可以进一步**将函数进一步拆分为多个子函数**来降低系统的复杂性。
不知大家是否发现,编程其实是写程序的人按照计算机的工作方式通过代码控制机器完成任务。但是,计算机的工作方式与人类正常的思维模式是不同的,如果编程就必须抛弃人类正常的思维方式去迎合计算机,编程的乐趣就少了很多。这里,我想说的并不是我们不能按照计算机的工作方式去编写代码,但是当我们需要开发一个复杂的系统时,这种方式会让代码过于复杂,从而导致开发和维护工作都变得举步维艰。
随着软件复杂性的增加,编写正确可靠的代码会变成了一项极为艰巨的任务,这也是很多人都坚信“软件开发是人类改造世界所有活动中最为复杂的活动”的原因。如何用程序描述复杂系统和解决复杂问题,就成为了所有程序员必须要思考和直面的问题。诞生于上世纪70年代的 Smalltalk 语言让软件开发者看到了希望,因为它引入了一种新的编程范式叫面向对象编程。在面向对象编程的世界里,程序中的**数据和操作数据的函数是一个逻辑上的整体**,我们称之为**对象**,**对象可以接收消息**,解决问题的方法就是**创建对象并向对象发出各种各样的消息**;通过消息传递,程序中的多个对象可以协同工作,这样就能构造出复杂的系统并解决现实中的问题。当然,面向对象编程的雏形还可以向前追溯到更早期的Simula语言,但这不是我们要讨论的重点。
> **说明:** 今天我们使用的很多高级程序设计语言都支持面向对象编程,但是面向对象编程也不是解决软件开发中所有问题的“银弹”,或者说在软件开发这个行业目前还没有所谓的“银弹”。关于这个问题,大家可以参考 IBM360 系统之父弗雷德里克·布鲁克斯所发表的论文《没有银弹:软件工程的本质性与附属性工作》或软件工程的经典著作《人月神话》一书。
### 类和对象
如果要用一句话来概括面向对象编程,我认为下面的说法是相当精辟和准确的。
> **面向对象编程**:把一组数据和处理数据的方法组成**对象**,把行为相同的对象归纳为**类**,通过**封装**隐藏对象的内部细节,通过**继承**实现类的特化和泛化,通过**多态**实现基于对象类型的动态分派。
这句话对初学者来说可能不那么容易理解,但是我可以先为大家圈出几个关键词:**对象**(object)、**类**(class)、**封装**(encapsulation)、**继承**(inheritance)、**多态**(polymorphism)。
我们先说说类和对象这两个词。在面向对象编程中,**类是一个抽象的概念,对象是一个具体的概念**。我们把同一类对象的共同特征抽取出来就是一个类,比如我们经常说的人类,这是一个抽象概念,而我们每个人就是人类的这个抽象概念下的实实在在的存在,也就是一个对象。简而言之,**类是对象的蓝图和模板,对象是类的实例,是可以接受消息的实体**。
在面向对象编程的世界中,**一切皆为对象**,**对象都有属性和行为**,**每个对象都是独一无二的**,而且**对象一定属于某个类**。对象的属性是对象的静态特征,对象的行为是对象的动态特征。按照上面的说法,如果我们把拥有共同特征的对象的属性和行为都抽取出来,就可以定义出一个类。
### 定义类
在 Python 语言中,我们可以使用`class`关键字加上类名来定义类,通过缩进我们可以确定类的代码块,就如同定义函数那样。在类的代码块中,我们需要写一些函数,我们说过类是一个抽象概念,那么这些函数就是我们对一类对象共同的动态特征的提取。写在类里面的函数我们通常称之为**方法**,方法就是对象的行为,也就是对象可以接收的消息。方法的第一个参数通常都是`self`,它代表了接收这个消息的对象本身。
```python
class Student:
def study(self, course_name):
print(f'学生正在学习{course_name}.')
def play(self):
print(f'学生正在玩游戏.')
```
### 创建和使用对象
在我们定义好一个类之后,可以使用构造器语法来创建对象,代码如下所示。
```python
stu1 = Student()
stu2 = Student()
print(stu1) # <__main__.Student object at 0x10ad5ac50>
print(stu2) # <__main__.Student object at 0x10ad5acd0>
print(hex(id(stu1)), hex(id(stu2))) # 0x10ad5ac50 0x10ad5acd0
```
在类的名字后跟上圆括号就是所谓的构造器语法,上面的代码创建了两个学生对象,一个赋值给变量`stu1`,一个赋值给变量`stu2`。当我们用`print`函数打印`stu1`和`stu2`两个变量时,我们会看到输出了对象在内存中的地址(十六进制形式),跟我们用`id`函数查看对象标识获得的值是相同的。现在我们可以告诉大家,我们定义的变量其实保存的是一个对象在内存中的逻辑地址(位置),通过这个逻辑地址,我们就可以在内存中找到这个对象。所以`stu3 = stu2`这样的赋值语句并没有创建新的对象,只是用一个新的变量保存了已有对象的地址。
接下来,我们尝试给对象发消息,即调用对象的方法。刚才的`Student`类中我们定义了`study`和`play`两个方法,两个方法的第一个参数`self`代表了接收消息的学生对象,`study`方法的第二个参数是学习的课程名称。Python中,给对象发消息有两种方式,请看下面的代码。
```python
# 通过“类.方法”调用方法
# 第一个参数是接收消息的对象
# 第二个参数是学习的课程名称
Student.study(stu1, 'Python程序设计') # 学生正在学习Python程序设计.
# 通过“对象.方法”调用方法
# 点前面的对象就是接收消息的对象
# 只需要传入第二个参数课程名称
stu1.study('Python程序设计') # 学生正在学习Python程序设计.
Student.play(stu2) # 学生正在玩游戏.
stu2.play() # 学生正在玩游戏.
```
### 初始化方法
大家可能已经注意到了,刚才我们创建的学生对象只有行为没有属性,如果要给学生对象定义属性,我们可以修改`Student`类,为其添加一个名为`__init__`的方法。在我们调用`Student`类的构造器创建对象时,首先会在内存中获得保存学生对象所需的内存空间,然后通过自动执行`__init__`方法,完成对内存的初始化操作,也就是把数据放到内存空间中。所以我们可以通过给`Student`类添加`__init__`方法的方式为学生对象指定属性,同时完成对属性赋初始值的操作,正因如此,`__init__`方法通常也被称为初始化方法。
我们对上面的`Student`类稍作修改,给学生对象添加`name`(姓名)和`age`(年龄)两个属性。
```python
class Student:
"""学生"""
def __init__(self, name, age):
"""初始化方法"""
self.name = name
self.age = age
def study(self, course_name):
"""学习"""
print(f'{self.name}正在学习{course_name}.')
def play(self):
"""玩耍"""
print(f'{self.name}正在玩游戏.')
```
修改刚才创建对象和给对象发消息的代码,重新执行一次,看看程序的执行结果有什么变化。
```python
# 调用Student类的构造器创建对象并传入初始化参数
stu1 = Student('骆昊', 44)
stu2 = Student('王大锤', 25)
stu1.study('Python程序设计') # 骆昊正在学习Python程序设计.
stu2.play() # 王大锤正在玩游戏.
```
### 面向对象的支柱
面向对象编程有三大支柱,就是我们之前给大家划重点的时候圈出的三个词:**封装**、**继承**和**多态**。后面两个概念在下一节课中会详细说明,这里我们先说一下什么是封装。我自己对封装的理解是:**隐藏一切可以隐藏的实现细节,只向外界暴露简单的调用接口**。我们在类中定义的对象方法其实就是一种封装,这种封装可以让我们在创建对象之后,只需要给对象发送一个消息就可以执行方法中的代码,也就是说我们在只知道方法的名字和参数(方法的外部视图),不知道方法内部实现细节(方法的内部视图)的情况下就完成了对方法的使用。
举一个例子,假如要控制一个机器人帮我倒杯水,如果不使用面向对象编程,不做任何的封装,那么就需要向这个机器人发出一系列的指令,如站起来、向左转、向前走5步、拿起面前的水杯、向后转、向前走10步、弯腰、放下水杯、按下出水按钮、等待10秒、松开出水按钮、拿起水杯、向右转、向前走5步、放下水杯等,才能完成这个简单的操作,想想都觉得麻烦。按照面向对象编程的思想,我们可以将倒水的操作封装到机器人的一个方法中,当需要机器人帮我们倒水的时候,只需要向机器人对象发出倒水的消息就可以了,这样做不是更好吗?
在很多场景下,面向对象编程其实就是一个三步走的问题。第一步定义类,第二步创建对象,第三步给对象发消息。当然,有的时候我们是不需要第一步的,因为我们想用的类可能已经存在了。之前我们说过,Python内置的`list`、`set`、`dict`其实都是类,如果要创建列表、集合、字典对象,我们就不用自定义类了。当然,有的类并不是 Python 标准库中直接提供的,它可能来自于第三方的代码,如何安装和使用三方代码在后续课程中会进行讨论。在某些特殊的场景中,我们会用到名为“内置对象”的对象,所谓“内置对象”就是说上面三步走的第一步和第二步都不需要了,因为类已经存在而且对象已然创建过了,直接向对象发消息就可以了,这也就是我们常说的“开箱即用”。
### 面向对象案例
#### 例子1:时钟
> **要求**:定义一个类描述数字时钟,提供走字和显示时间的功能。
```python
import time
# 定义时钟类
class Clock:
"""数字时钟"""
def __init__(self, hour=0, minute=0, second=0):
"""初始化方法
:param hour: 时
:param minute: 分
:param second: 秒
"""
self.hour = hour
self.min = minute
self.sec = second
def run(self):
"""走字"""
self.sec += 1
if self.sec == 60:
self.sec = 0
self.min += 1
if self.min == 60:
self.min = 0
self.hour += 1
if self.hour == 24:
self.hour = 0
def show(self):
"""显示时间"""
return f'{self.hour:0>2d}:{self.min:0>2d}:{self.sec:0>2d}'
# 创建时钟对象
clock = Clock(23, 59, 58)
while True:
# 给时钟对象发消息读取时间
print(clock.show())
# 休眠1秒钟
time.sleep(1)
# 给时钟对象发消息使其走字
clock.run()
```
#### 例子2:平面上的点
> **要求**:定义一个类描述平面上的点,提供计算到另一个点距离的方法。
```python
class Point:
"""平面上的点"""
def __init__(self, x=0, y=0):
"""初始化方法
:param x: 横坐标
:param y: 纵坐标
"""
self.x, self.y = x, y
def distance_to(self, other):
"""计算与另一个点的距离
:param other: 另一个点
"""
dx = self.x - other.x
dy = self.y - other.y
return (dx * dx + dy * dy) ** 0.5
def __str__(self):
return f'({self.x}, {self.y})'
p1 = Point(3, 5)
p2 = Point(6, 9)
print(p1) # 调用对象的__str__魔法方法
print(p2)
print(p1.distance_to(p2))
```
### 总结
面向对象编程是一种非常流行的编程范式,除此之外还有**指令式编程**、**函数式编程**等编程范式。由于现实世界是由对象构成的,而对象是可以接收消息的实体,所以**面向对象编程更符合人类正常的思维习惯**。类是抽象的,对象是具体的,有了类就能创建对象,有了对象就可以接收消息,这就是面向对象编程的基础。定义类的过程是一个抽象的过程,找到对象公共的属性属于数据抽象,找到对象公共的方法属于行为抽象。抽象的过程是一个仁者见仁智者见智的过程,对同一类对象进行抽象可能会得到不同的结果,如下图所示。
> **说明:** 本节课的插图来自于 Grady Booc 等撰写的《面向对象分析与设计》一书,该书是讲解面向对象编程的经典著作,有兴趣的读者可以购买和阅读这本书来了解更多的面向对象的相关知识。
================================================
FILE: Day01-20/19.面向对象编程进阶.md
================================================
## 面向对象编程进阶
前面我们讲解了 Python 面向对象编程的一些基础知识,本节我们继续讨论面向对象编程相关的内容。
### 可见性和属性装饰器
在很多面向对象编程语言中,对象的属性通常会被设置为私有(private)或受保护(protected)的成员,简单的说就是不允许直接访问这些属性;对象的方法通常都是公开的(public),因为公开的方法是对象能够接受的消息,也是对象暴露给外界的调用接口,这就是所谓的访问可见性。在 Python 中,可以通过给对象属性名添加前缀下划线的方式来说明属性的访问可见性,例如,可以用`__name`表示一个私有属性,`_name`表示一个受保护属性,代码如下所示。
```python
class Student:
def __init__(self, name, age):
self.__name = name
self.__age = age
def study(self, course_name):
print(f'{self.__name}正在学习{course_name}.')
stu = Student('王大锤', 20)
stu.study('Python程序设计')
print(stu.__name) # AttributeError: 'Student' object has no attribute '__name'
```
上面代码的最后一行会引发`AttributeError`(属性错误)异常,异常消息为:`'Student' object has no attribute '__name'`。由此可见,以`__`开头的属性`__name`相当于是私有的,在类的外面无法直接访问,但是类里面的`study`方法中可以通过`self.__name`访问该属性。需要说明的是,大多数使用 Python 语言的人在定义类时,通常不会选择让对象的属性私有或受保护,正如有一句名言说的:“**We are all consenting adults here**”(大家都是成年人),成年人可以为自己的行为负责,而不需要通过 Python 语言本身来限制访问可见性。事实上,大多数的程序员都认为**开放比封闭要好**,把对象的属性私有化并非必不可少的东西,所以 Python 语言并没有从语义上做出最严格的限定,也就是说上面的代码如果你愿意,用`stu._Student__name`的方式仍然可以访问到私有属性`__name`,有兴趣的读者可以自己试一试。
### 动态属性
Python 语言属于动态语言,维基百科对动态语言的解释是:“在运行时可以改变其结构的语言,例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化”。动态语言非常灵活,目前流行的 Python 和 JavaScript 都是动态语言,除此之外,诸如 PHP、Ruby 等也都属于动态语言,而 C、C++ 等语言则不属于动态语言。
在 Python 中,我们可以动态为对象添加属性,这是 Python 作为动态类型语言的一项特权,代码如下所示。需要提醒大家的是,对象的方法其实本质上也是对象的属性,如果给对象发送一个无法接收的消息,引发的异常仍然是`AttributeError`。
```python
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student('王大锤', 20)
stu.sex = '男' # 给学生对象动态添加sex属性
```
如果不希望在使用对象时动态的为对象添加属性,可以使用 Python 语言中的`__slots__`魔法。对于`Student`类来说,可以在类中指定`__slots__ = ('name', 'age')`,这样`Student`类的对象只能有`name`和`age`属性,如果想动态添加其他属性将会引发异常,代码如下所示。
```python
class Student:
__slots__ = ('name', 'age')
def __init__(self, name, age):
self.name = name
self.age = age
stu = Student('王大锤', 20)
# AttributeError: 'Student' object has no attribute 'sex'
stu.sex = '男'
```
### 静态方法和类方法
之前我们在类中定义的方法都是对象方法,换句话说这些方法都是对象可以接收的消息。除了对象方法之外,类中还可以有静态方法和类方法,这两类方法是发给类的消息,二者并没有实质性的区别。在面向对象的世界里,一切皆为对象,我们定义的每一个类其实也是一个对象,而静态方法和类方法就是发送给类对象的消息。那么,什么样的消息会直接发送给类对象呢?
举一个例子,定义一个三角形类,通过传入三条边的长度来构造三角形,并提供计算周长和面积的方法。计算周长和面积肯定是三角形对象的方法,这一点毫无疑问。但是在创建三角形对象时,传入的三条边长未必能构造出三角形,为此我们可以先写一个方法来验证给定的三条边长是否可以构成三角形,这种方法很显然就不是对象方法,因为在调用这个方法时三角形对象还没有创建出来。我们可以把这类方法设计为静态方法或类方法,也就是说这类方法不是发送给三角形对象的消息,而是发送给三角形类的消息,代码如下所示。
```python
class Triangle(object):
"""三角形"""
def __init__(self, a, b, c):
"""初始化方法"""
self.a = a
self.b = b
self.c = c
@staticmethod
def is_valid(a, b, c):
"""判断三条边长能否构成三角形(静态方法)"""
return a + b > c and b + c > a and a + c > b
# @classmethod
# def is_valid(cls, a, b, c):
# """判断三条边长能否构成三角形(类方法)"""
# return a + b > c and b + c > a and a + c > b
def perimeter(self):
"""计算周长"""
return self.a + self.b + self.c
def area(self):
"""计算面积"""
p = self.perimeter() / 2
return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5
if Triangle.is_valid(3, 4, 5):
t = Triangle(3, 4, 5)
print(f'周长: {t.perimeter()}')
print(f'面积: {t.area()}')
else:
print('无效的边长!!!')
```
上面的代码使用`staticmethod`装饰器声明了`is_valid`方法是`Triangle`类的静态方法,如果要声明类方法,可以使用`classmethod`装饰器(如上面的代码15~18行所示)。可以直接使用`类名.方法名`的方式来调用静态方法和类方法,二者的区别在于,类方法的第一个参数是类对象本身,而静态方法则没有这个参数。简单的总结一下,**对象方法、类方法、静态方法都可以通过“类名.方法名”的方式来调用,区别在于方法的第一个参数到底是普通对象还是类对象,还是没有接受消息的对象**。静态方法通常也可以直接写成一个独立的函数,因为它并没有跟特定的对象绑定。
这里做一个补充说明,我们可以给上面计算三角形周长和面积的方法添加一个`property`装饰器(Python 内置类型),这样三角形类的`perimeter`和`area`就变成了两个属性,不再通过调用方法的方式来访问,而是用对象访问属性的方式直接获得,修改后的代码如下所示。
```python
class Triangle(object):
"""三角形"""
def __init__(self, a, b, c):
"""初始化方法"""
self.a = a
self.b = b
self.c = c
@staticmethod
def is_valid(a, b, c):
"""判断三条边长能否构成三角形(静态方法)"""
return a + b > c and b + c > a and a + c > b
@property
def perimeter(self):
"""计算周长"""
return self.a + self.b + self.c
@property
def area(self):
"""计算面积"""
p = self.perimeter / 2
return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5
if Triangle.is_valid(3, 4, 5):
t = Triangle(3, 4, 5)
print(f'周长: {t.perimeter}')
print(f'面积: {t.area}')
else:
print('无效的边长!!!')
```
### 继承和多态
面向对象的编程语言支持在已有类的基础上创建新类,从而减少重复代码的编写。提供继承信息的类叫做父类(超类、基类),得到继承信息的类叫做子类(派生类、衍生类)。例如,我们定义一个学生类和一个老师类,我们会发现他们有大量的重复代码,而这些重复代码都是老师和学生作为人的公共属性和行为,所以在这种情况下,我们应该先定义人类,再通过继承,从人类派生出老师类和学生类,代码如下所示。
```python
class Person:
"""人"""
def __init__(self, name, age):
self.name = name
self.age = age
def eat(self):
print(f'{self.name}正在吃饭.')
def sleep(self):
print(f'{self.name}正在睡觉.')
class Student(Person):
"""学生"""
def __init__(self, name, age):
super().__init__(name, age)
def study(self, course_name):
print(f'{self.name}正在学习{course_name}.')
class Teacher(Person):
"""老师"""
def __init__(self, name, age, title):
super().__init__(name, age)
self.title = title
def teach(self, course_name):
print(f'{self.name}{self.title}正在讲授{course_name}.')
stu1 = Student('白元芳', 21)
stu2 = Student('狄仁杰', 22)
tea1 = Teacher('武则天', 35, '副教授')
stu1.eat()
stu2.sleep()
tea1.eat()
stu1.study('Python程序设计')
tea1.teach('Python程序设计')
stu2.study('数据科学导论')
```
继承的语法是在定义类的时候,在类名后的圆括号中指定当前类的父类。如果定义一个类的时候没有指定它的父类是谁,那么默认的父类是`object`类。`object`类是 Python 中的顶级类,这也就意味着所有的类都是它的子类,要么直接继承它,要么间接继承它。Python 语言允许多重继承,也就是说一个类可以有一个或多个父类,关于多重继承的问题我们在后面会有更为详细的讨论。在子类的初始化方法中,我们可以通过`super().__init__()`来调用父类初始化方法,`super`函数是 Python 内置函数中专门为获取当前对象的父类对象而设计的。从上面的代码可以看出,子类除了可以通过继承得到父类提供的属性和方法外,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力。在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,也叫做“里氏替换原则”(Liskov Substitution Principle)。
子类继承父类的方法后,还可以对方法进行重写(重新实现该方法),不同的子类可以对父类的同一个方法给出不同的实现版本,这样的方法在程序运行时就会表现出多态行为(调用相同的方法,做了不同的事情)。多态是面向对象编程中最精髓的部分,当然也是对初学者来说最难以理解和灵活运用的部分,我们会在下一个章节用专门的例子来讲解这个知识点。
### 总结
Python 是动态类型语言,Python 中的对象可以动态的添加属性,对象的方法其实也是属性,只不过和该属性对应的是一个可以调用的函数。在面向对象的世界中,**一切皆为对象**,我们定义的类也是对象,所以**类也可以接收消息**,对应的方法是类方法或静态方法。通过继承,我们**可以从已有的类创建新类**,实现对已有类代码的复用。
================================================
FILE: Day01-20/20.面向对象编程应用.md
================================================
## 面向对象编程应用
面向对象编程对初学者来说不难理解但很难应用,虽然我们为大家总结过面向对象的三步走方法(定义类、创建对象、给对象发消息),但是说起来容易做起来难。**大量的编程练习**和**阅读优质的代码**可能是这个阶段最能够帮助到大家的两件事情。接下来我们还是通过经典的案例来剖析面向对象编程的知识,同时也通过这些案例把我们之前学过的 Python 知识都串联起来。
### 例子1:扑克游戏。
> **说明**:简单起见,我们的扑克只有52张牌(没有大小王),游戏需要将 52 张牌发到 4 个玩家的手上,每个玩家手上有 13 张牌,按照黑桃、红心、草花、方块的顺序和点数从小到大排列,暂时不实现其他的功能。
使用面向对象编程方法,首先需要从问题的需求中找到对象并抽象出对应的类,此外还要找到对象的属性和行为。当然,这件事情并不是特别困难,我们可以从需求的描述中找出名词和动词,名词通常就是对象或者是对象的属性,而动词通常是对象的行为。扑克游戏中至少应该有三类对象,分别是牌、扑克和玩家,牌、扑克、玩家三个类也并不是孤立的。类和类之间的关系可以粗略的分为 **is-a关系(继承)**、**has-a关系(关联)**和 **use-a关系(依赖)**。很显然扑克和牌是 has-a 关系,因为一副扑克有(has-a)52 张牌;玩家和牌之间不仅有关联关系还有依赖关系,因为玩家手上有(has-a)牌而且玩家使用了(use-a)牌。
牌的属性显而易见,有花色和点数。我们可以用 0 到 3 的四个数字来代表四种不同的花色,但是这样的代码可读性会非常糟糕,因为我们并不知道黑桃、红心、草花、方块跟 0 到 3 的数字的对应关系。如果一个变量的取值只有有限多个选项,我们可以使用枚举。与 C、Java 等语言不同的是,Python 中没有声明枚举类型的关键字,但是可以通过继承`enum`模块的`Enum`类来创建枚举类型,代码如下所示。
```python
from enum import Enum
class Suite(Enum):
"""花色(枚举)"""
SPADE, HEART, CLUB, DIAMOND = range(4)
```
通过上面的代码可以看出,定义枚举类型其实就是定义符号常量,如`SPADE`、`HEART`等。每个符号常量都有与之对应的值,这样表示黑桃就可以不用数字 0,而是用`Suite.SPADE`;同理,表示方块可以不用数字 3, 而是用`Suite.DIAMOND`。注意,使用符号常量肯定是优于使用字面常量的,因为能够读懂英文就能理解符号常量的含义,代码的可读性会提升很多。Python 中的枚举类型是可迭代类型,简单的说就是可以将枚举类型放到`for-in`循环中,依次取出每一个符号常量及其对应的值,如下所示。
```python
for suite in Suite:
print(f'{suite}: {suite.value}')
```
接下来我们可以定义牌类。
```python
class Card:
"""牌"""
def __init__(self, suite, face):
self.suite = suite
self.face = face
def __repr__(self):
suites = '♠♥♣♦'
faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
return f'{suites[self.suite.value]}{faces[self.face]}' # 返回牌的花色和点数
```
可以通过下面的代码来测试下`Card`类。
```python
card1 = Card(Suite.SPADE, 5)
card2 = Card(Suite.HEART, 13)
print(card1) # ♠5
print(card2) # ♥K
```
接下来我们定义扑克类。
```python
import random
class Poker:
"""扑克"""
def __init__(self):
self.cards = [Card(suite, face)
for suite in Suite
for face in range(1, 14)] # 52张牌构成的列表
self.current = 0 # 记录发牌位置的属性
def shuffle(self):
"""洗牌"""
self.current = 0
random.shuffle(self.cards) # 通过random模块的shuffle函数实现随机乱序
def deal(self):
"""发牌"""
card = self.cards[self.current]
self.current += 1
return card
@property
def has_next(self):
"""还有没有牌可以发"""
return self.current < len(self.cards)
```
可以通过下面的代码来测试下`Poker`类。
```python
poker = Poker()
print(poker.cards) # 洗牌前的牌
poker.shuffle()
print(poker.cards) # 洗牌后的牌
```
定义玩家类。
```python
class Player:
"""玩家"""
def __init__(self, name):
self.name = name
self.cards = [] # 玩家手上的牌
def get_one(self, card):
"""摸牌"""
self.cards.append(card)
def arrange(self):
"""整理手上的牌"""
self.cards.sort()
```
创建四个玩家并将牌发到玩家的手上。
```python
poker = Poker()
poker.shuffle()
players = [Player('东邪'), Player('西毒'), Player('南帝'), Player('北丐')]
# 将牌轮流发到每个玩家手上每人13张牌
for _ in range(13):
for player in players:
player.get_one(poker.deal())
# 玩家整理手上的牌输出名字和手牌
for player in players:
player.arrange()
print(f'{player.name}: ', end='')
print(player.cards)
```
执行上面的代码会在`player.arrange()`那里出现异常,因为`Player`的`arrange`方法使用了列表的`sort`对玩家手上的牌进行排序,排序需要比较两个`Card`对象的大小,而`<`运算符又不能直接作用于`Card`类型,所以就出现了`TypeError`异常,异常消息为:`'<' not supported between instances of 'Card' and 'Card'`。
为了解决这个问题,我们可以对`Card`类的代码稍作修改,使得两个`Card`对象可以直接用`<`进行大小的比较。这里用到技术叫**运算符重载**,Python 中要实现对`<`运算符的重载,需要在类中添加一个名为`__lt__`的魔术方法。很显然,魔术方法`__lt__`中的`lt`是英文单词“less than”的缩写,以此类推,魔术方法`__gt__`对应`>`运算符,魔术方法`__le__`对应`<=`运算符,`__ge__`对应`>=`运算符,`__eq__`对应`==`运算符,`__ne__`对应`!=`运算符。
修改后的`Card`类代码如下所示。
```python
class Card:
"""牌"""
def __init__(self, suite, face):
self.suite = suite
self.face = face
def __repr__(self):
suites = '♠♥♣♦'
faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
return f'{suites[self.suite.value]}{faces[self.face]}'
def __lt__(self, other):
if self.suite == other.suite:
return self.face < other.face # 花色相同比较点数的大小
return self.suite.value < other.suite.value # 花色不同比较花色对应的值
```
>**说明:** 大家可以尝试在上面代码的基础上写一个简单的扑克游戏,如 21 点游戏(Black Jack),游戏的规则可以自己在网上找一找。
### 例子2:工资结算系统。
> **要求**:某公司有三种类型的员工,分别是部门经理、程序员和销售员。需要设计一个工资结算系统,根据提供的员工信息来计算员工的月薪。其中,部门经理的月薪是固定 15000 元;程序员按工作时间(以小时为单位)支付月薪,每小时 200 元;销售员的月薪由 1800 元底薪加上销售额 5% 的提成两部分构成。
通过对上述需求的分析,可以看出部门经理、程序员、销售员都是员工,有相同的属性和行为,那么我们可以先设计一个名为`Employee`的父类,再通过继承的方式从这个父类派生出部门经理、程序员和销售员三个子类。很显然,后续的代码不会创建`Employee` 类的对象,因为我们需要的是具体的员工对象,所以这个类可以设计成专门用于继承的抽象类。Python 语言中没有定义抽象类的关键字,但是可以通过`abc`模块中名为`ABCMeta` 的元类来定义抽象类。关于元类的概念此处不展开讲解,当然大家不用纠结,照做即可。
```python
from abc import ABCMeta, abstractmethod
class Employee(metaclass=ABCMeta):
"""员工"""
def __init__(self, name):
self.name = name
@abstractmethod
def get_salary(self):
"""结算月薪"""
pass
```
在上面的员工类中,有一个名为`get_salary`的方法用于结算月薪,但是由于还没有确定是哪一类员工,所以结算月薪虽然是员工的公共行为但这里却没有办法实现。对于暂时无法实现的方法,我们可以使用`abstractmethod`装饰器将其声明为抽象方法,所谓**抽象方法就是只有声明没有实现的方法**,**声明这个方法是为了让子类去重写这个方法**。接下来的代码展示了如何从员工类派生出部门经理、程序员、销售员这三个子类以及子类如何重写父类的抽象方法。
```python
class Manager(Employee):
"""部门经理"""
def get_salary(self):
return 15000.0
class Programmer(Employee):
"""程序员"""
def __init__(self, name, working_hour=0):
super().__init__(name)
self.working_hour = working_hour
def get_salary(self):
return 200 * self.working_hour
class Salesman(Employee):
"""销售员"""
def __init__(self, name, sales=0):
super().__init__(name)
self.sales = sales
def get_salary(self):
return 1800 + self.sales * 0.05
```
上面的`Manager`、`Programmer`、`Salesman`三个类都继承自`Employee`,三个类都分别重写了`get_salary`方法。**重写就是子类对父类已有的方法重新做出实现**。相信大家已经注意到了,三个子类中的`get_salary`各不相同,所以这个方法在程序运行时会产生**多态行为**,多态简单的说就是**调用相同的方法**,**不同的子类对象做不同的事情**。
我们通过下面的代码来完成这个工资结算系统,由于程序员和销售员需要分别录入本月的工作时间和销售额,所以在下面的代码中我们使用了 Python 内置的`isinstance`函数来判断员工对象的类型。我们之前讲过的`type`函数也能识别对象的类型,但是`isinstance`函数更加强大,因为它可以判断出一个对象是不是某个继承结构下的子类型,你可以简单的理解为`type`函数是对对象类型的精准匹配,而`isinstance`函数是对对象类型的模糊匹配。
```python
emps = [Manager('刘备'), Programmer('诸葛亮'), Manager('曹操'), Programmer('荀彧'), Salesman('张辽')]
for emp in emps:
if isinstance(emp, Programmer):
emp.working_hour = int(input(f'请输入{emp.name}本月工作时间: '))
elif isinstance(emp, Salesman):
emp.sales = float(input(f'请输入{emp.name}本月销售额: '))
print(f'{emp.name}本月工资为: ¥{emp.get_salary():.2f}元')
```
### 总结
面向对象编程思想非常的好,也符合人类的正常思维习惯,但是要想灵活运用面向对象编程中的抽象、封装、继承、多态需要长时间的积累和沉淀,这件事情无法一蹴而就,因为知识的积累本就是涓滴成河的过程。
================================================
FILE: Day21-30/21.文件读写和异常处理.md
================================================
## 文件读写和异常处理
实际开发中常常会遇到对数据进行持久化的场景,所谓持久化是指将数据从无法长久保存数据的存储介质(通常是内存)转移到可以长久保存数据的存储介质(通常是硬盘)中。实现数据持久化最直接简单的方式就是通过**文件系统**将数据保存到**文件**中。
计算机的**文件系统**是一种存储和组织计算机数据的方法,它使得对数据的访问和查找变得容易,文件系统使用**文件**和**树形目录**的抽象逻辑概念代替了硬盘、光盘、闪存等物理设备的数据块概念,用户使用文件系统来保存数据时,不必关心数据实际保存在硬盘的哪个数据块上,只需要记住这个文件的路径和文件名。在写入新数据之前,用户不必关心硬盘上的哪个数据块没有被使用,硬盘上的存储空间管理(分配和释放)功能由文件系统自动完成,用户只需要记住数据被写入到了哪个文件中。
### 打开和关闭文件
有了文件系统,我们可以非常方便的通过文件来读写数据;在 Python 中要实现文件操作是非常简单的。我们可以使用 Python 内置的`open`函数来打开文件,在使用`open`函数时,我们可以通过函数的参数指定**文件名**、**操作模式**和**字符编码**等信息,接下来就可以对文件进行读写操作了。这里所说的操作模式是指要打开什么样的文件(字符文件或二进制文件)以及做什么样的操作(读、写或追加),具体如下表所示。
| 操作模式 | 具体含义 |
| -------- | -------------------------------- |
| `'r'` | 读取 (默认) |
| `'w'` | 写入(会先截断之前的内容) |
| `'x'` | 写入,如果文件已经存在会产生异常 |
| `'a'` | 追加,将内容写入到已有文件的末尾 |
| `'b'` | 二进制模式 |
| `'t'` | 文本模式(默认) |
| `'+'` | 更新(既可以读又可以写) |
下图展示了如何根据程序的需要来设置`open`函数的操作模式。
在使用`open`函数时,如果打开的文件是字符文件(文本文件),可以通过`encoding`参数来指定读写文件使用的字符编码。如果对字符编码和字符集这些概念不了解,可以看看[《字符集和字符编码》](https://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html)一文,此处不再进行赘述。
使用`open`函数打开文件成功后会返回一个文件对象,通过这个对象,我们就可以实现对文件的读写操作;如果打开文件失败,`open`函数会引发异常,稍后会对此加以说明。如果要关闭打开的文件,可以使用文件对象的`close`方法,这样可以在结束文件操作时释放掉这个文件。
### 读写文本文件
用`open`函数打开文本文件时,需要指定文件名并将文件的操作模式设置为`'r'`,如果不指定,默认值也是`'r'`;如果需要指定字符编码,可以传入`encoding`参数,如果不指定,默认值是`None`,那么在读取文件时使用的是操作系统默认的编码。需要提醒大家,如果不能保证保存文件时使用的编码方式与`encoding`参数指定的编码方式是一致的,那么就可能因无法解码字符而导致读取文件失败。
下面的例子演示了如何读取一个纯文本文件(一般指只有字符原生编码构成的文件,与富文本相比,纯文本不包含字符样式的控制元素,能够被最简单的文本编辑器直接读取)。
```python
file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
file.close()
```
> **说明**:《致橡树》是舒婷老师在 1977 年 3 月创作的爱情诗,也是我最喜欢的现代诗之一。内容如下:
>
> 我如果爱你
> 绝不像攀援的凌霄花,
> 借你的高枝炫耀自己;
> 我如果爱你
> 绝不学痴情的鸟儿,
> 为绿荫重复单调的歌曲;
> 也不止像泉源,
> 常年送来清凉的慰藉;
> 也不止像险峰,
> 增加你的高度,衬托你的威仪。
> 甚至日光,
> 甚至春雨。
>
> 不,这些都还不够!
> 我必须是你近旁的一株木棉,
> 作为树的形象和你站在一起。
> 根,紧握在地下;
> 叶,相触在云里。
> 每一阵风过,
> 我们都互相致意,
> 但没有人,
> 听懂我们的言语。
> 你有你的铜枝铁干,
> 像刀,像剑,也像戟;
> 我有我红硕的花朵,
> 像沉重的叹息,
> 又像英勇的火炬。
>
> 我们分担寒潮、风雷、霹雳;
> 我们共享雾霭、流岚、虹霓。
> 仿佛永远分离,
> 却又终身相依。
> 这才是伟大的爱情,
> 坚贞就在这里:
> 爱
> 不仅爱你伟岸的身躯,
> 也爱你坚持的位置,
> 足下的土地。
除了使用文件对象的`read`方法读取文件之外,还可以使用`for-in`循环逐行读取或者用`readlines`方法将文件按行读取到一个列表容器中,代码如下所示。
```python
file = open('致橡树.txt', 'r', encoding='utf-8')
for line in file:
print(line, end='')
file.close()
file = open('致橡树.txt', 'r', encoding='utf-8')
lines = file.readlines()
for line in lines:
print(line, end='')
file.close()
```
如果要向文件中写入内容,可以在打开文件时使用`w`或者`a`作为操作模式,前者会截断之前的文本内容写入新的内容,后者是在原来内容的尾部追加新的内容。
```python
file = open('致橡树.txt', 'a', encoding='utf-8')
file.write('\n标题:《致橡树》')
file.write('\n作者:舒婷')
file.write('\n时间:1977年3月')
file.close()
```
### 异常处理机制
请注意上面的代码,如果`open`函数指定的文件并不存在或者无法打开,那么将引发异常状况导致程序崩溃。为了让代码具有健壮性和容错性,我们可以**使用 Python 的异常机制对可能在运行时发生状况的代码进行适当的处理**。Python 中和异常相关的关键字有五个,分别是`try`、`except`、`else`、`finally`和`raise`,我们先看看下面的代码,再来为大家介绍这些关键字的用法。
```python
file = None
try:
file = open('致橡树.txt', 'r', encoding='utf-8')
print(file.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')
finally:
if file:
file.close()
```
在 Python 中,我们可以将运行时会出现状况的代码放在`try`代码块中,在`try`后面可以跟上一个或多个`except`块来捕获异常并进行相应的处理。例如,在上面的代码中,文件找不到会引发`FileNotFoundError`,指定了未知的编码会引发`LookupError`,而如果读取文件时无法按指定编码方式解码文件会引发`UnicodeDecodeError`,所以我们在`try`后面跟上了三个`except`分别处理这三种不同的异常状况。在`except`后面,我们还可以加上`else`代码块,这是`try` 中的代码没有出现异常时会执行的代码,而且`else`中的代码不会再进行异常捕获,也就是说如果遇到异常状况,程序会因异常而终止并报告异常信息。最后我们使用`finally`代码块来关闭打开的文件,释放掉程序中获取的外部资源。由于`finally`块的代码不论程序正常还是异常都会执行,甚至是调用了`sys`模块的`exit`函数终止 Python 程序,`finally`块中的代码仍然会被执行(因为`exit`函数的本质是引发了`SystemExit`异常),因此我们把`finally`代码块称为“总是执行代码块”,它最适合用来做释放外部资源的操作。
Python 中内置了大量的异常类型,除了上面代码中用到的异常类型以及之前的课程中遇到过的异常类型外,还有许多的异常类型,其继承结构如下所示。
```
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
```
从上面的继承结构可以看出,Python 中所有的异常都是`BaseException`的子类型,它有四个直接的子类,分别是:`SystemExit`、`KeyboardInterrupt`、`GeneratorExit`和`Exception`。其中,`SystemExit`表示解释器请求退出,`KeyboardInterrupt`是用户中断程序执行(按下`Ctrl+c`),`GeneratorExit`表示生成器发生异常通知退出,不理解这些异常没有关系,继续学习就好了。值得一提的是`Exception`类,它是常规异常类型的父类型,很多的异常都是直接或间接的继承自`Exception`类。如果 Python 内置的异常类型不能满足应用程序的需要,我们可以自定义异常类型,而自定义的异常类型也应该直接或间接继承自`Exception`类,当然还可以根据需要重写或添加方法。
在 Python 中,可以使用`raise`关键字来引发异常(抛出异常对象),而调用者可以通过`try...except...`结构来捕获并处理异常。例如在函数中,当函数的执行条件不满足时,可以使用抛出异常的方式来告知调用者问题的所在,而调用者可以通过捕获处理异常来使得代码从异常中恢复,定义异常和抛出异常的代码如下所示。
```python
class InputError(ValueError):
"""自定义异常类型"""
pass
def fac(num):
"""求阶乘"""
if num < 0:
raise InputError('只能计算非负整数的阶乘')
if num in (0, 1):
return 1
return num * fac(num - 1)
```
调用求阶乘的函数`fac`,通过`try...except...`结构捕获输入错误的异常并打印异常对象(显示异常信息),如果输入正确就计算阶乘并结束程序。
```python
flag = True
while flag:
num = int(input('n = '))
try:
print(f'{num}! = {fac(num)}')
flag = False
except InputError as err:
print(err)
```
### 上下文管理器语法
对于`open`函数返回的文件对象,还可以使用`with`上下文管理器语法在文件操作完成后自动执行文件对象的`close`方法,这样可以让代码变得更加简单优雅,因为不需要再写`finally`代码块来执行关闭文件释放资源的操作。需要提醒大家的是,并不是所有的对象都可以放在`with`上下文语法中,只有符合**上下文管理器协议**(有`__enter__`和`__exit__`魔术方法)的对象才能使用这种语法,Python 标准库中的`contextlib`模块也提供了对`with`上下文语法的支持,后面用到的时候再为大家进行讲解。
用`with`上下文语法改写后的代码如下所示。
```python
try:
with open('致橡树.txt', 'r', encoding='utf-8') as file:
print(file.read())
except FileNotFoundError:
print('无法打开指定的文件!')
except LookupError:
print('指定了未知的编码!')
except UnicodeDecodeError:
print('读取文件时解码错误!')
```
### 读写二进制文件
读写二进制文件跟读写文本文件的操作类似,但是需要注意,在使用`open`函数打开文件时,如果要进行读操作,操作模式是`'rb'`,如果要进行写操作,操作模式是`'wb'`。还有一点,读写文本文件时,`read`方法的返回值以及`write`方法的参数是`str`对象(字符串),而读写二进制文件时,`read`方法的返回值以及`write`方法的参数是`bytes-like`对象(字节串)。下面的代码实现了将当前路径下名为`guido.jpg`的图片文件复制到`吉多.jpg`文件中的操作。
```python
try:
with open('guido.jpg', 'rb') as file1:
data = file1.read()
with open('吉多.jpg', 'wb') as file2:
file2.write(data)
except FileNotFoundError:
print('指定的文件无法打开.')
except IOError:
print('读写文件时出现错误.')
print('程序执行结束.')
```
如果要复制的图片文件很大,一次将文件内容直接读入内存中可能会造成非常大的内存开销,为了减少对内存的占用,可以为`read`方法传入`size`参数来指定每次读取的字节数,通过循环读取和写入的方式来完成上面的操作,代码如下所示。
```python
try:
with open('guido.jpg', 'rb') as file1, open('吉多.jpg', 'wb') as file2:
data = file1.read(512)
while data:
file2.write(data)
data = file1.read()
except FileNotFoundError:
print('指定的文件无法打开.')
except IOError:
print('读写文件时出现错误.')
print('程序执行结束.')
```
### 总结
通过读写文件的操作,我们可以实现数据持久化。在 Python 中可以通过`open`函数来获得文件对象,可以通过文件对象的`read`和`write`方法实现文件读写操作。程序在运行时可能遭遇无法预料的异常状况,可以使用 Python 的异常机制来处理这些状况。Python 的异常机制主要包括`try`、`except`、`else`、`finally`和`raise`这五个核心关键字。`try`后面的`except`语句不是必须的,`finally`语句也不是必须的,但是二者必须要有一个;`except`语句可以有一个或多个,多个`except`会按照书写的顺序依次匹配指定的异常,如果异常已经处理就不会再进入后续的`except`语句;`except`语句中还可以通过元组同时指定多个异常类型进行捕获;`except`语句后面如果不指定异常类型,则默认捕获所有异常;捕获异常后可以使用`raise`要再次抛出,但是不建议捕获并抛出同一个异常;不建议在不清楚逻辑的情况下捕获所有异常,这可能会掩盖程序中严重的问题。最后强调一点,**不要使用异常机制来处理正常业务逻辑或控制程序流程**,简单的说就是不要滥用异常机制,这是初学者常犯的错误。
================================================
FILE: Day21-30/22.对象的序列化和反序列化.md
================================================
## 对象的序列化和反序列化
### JSON 概述
通过上面的讲解,我们已经知道如何将文本数据和二进制数据保存到文件中,那么这里还有一个问题,如果希望把一个列表或者一个字典中的数据保存到文件中又该怎么做呢?在 Python 中,我们可以将程序中的数据以 JSON 格式进行保存。JSON 是“JavaScript Object Notation”的缩写,它本来是 JavaScript 语言中创建对象的一种字面量语法,现在已经被广泛的应用于跨语言跨平台的数据交换。使用 JSON 的原因非常简单,因为它结构紧凑而且是纯文本,任何操作系统和编程语言都能处理纯文本,这就是**实现跨语言跨平台数据交换**的前提条件。目前 JSON 基本上已经取代了 XML(可扩展标记语言)作为**异构系统间交换数据的事实标准**。可以在[ JSON 的官方网站](https://www.json.org/json-zh.html)找到更多关于 JSON 的知识,这个网站还提供了每种语言处理 JSON 数据格式可以使用的工具或三方库。
```JavaScript
{
name: "骆昊",
age: 40,
friends: ["王大锤", "白元芳"],
cars: [
{"brand": "BMW", "max_speed": 240},
{"brand": "Benz", "max_speed": 280},
{"brand": "Audi", "max_speed": 280}
]
}
```
上面是 JSON 的一个简单例子,大家可能已经注意到了,它跟 Python 中的字典非常类似而且支持嵌套结构,就好比 Python 字典中的值可以是另一个字典。我们可以尝试把下面的代码输入浏览器的控制台(对于 Chrome 浏览器,可以通过“更多工具”菜单找到“开发者工具”子菜单,就可以打开浏览器的控制台),浏览器的控制台提供了一个运行 JavaScript 代码的交互式环境(类似于 Python 的交互式环境),下面的代码会帮我们创建出一个 JavaScript 的对象,我们将其赋值给名为`obj`的变量。
```JavaScript
let obj = {
name: "骆昊",
age: 40,
friends: ["王大锤", "白元芳"],
cars: [
{"brand": "BMW", "max_speed": 240},
{"brand": "Benz", "max_speed": 280},
{"brand": "Audi", "max_speed": 280}
]
}
```
上面的`obj`就是 JavaScript 中的一个对象,我们可以通过`obj.name`或`obj["name"]`两种方式获取到`name`对应的值,如下图所示。可以注意到,`obj["name"]`这种获取数据的方式跟 Python 字典通过键获取值的索引操作是完全一致的,而 Python 中也通过名为`json`的模块提供了字典与 JSON 双向转换的支持。
我们在 JSON 中使用的数据类型(JavaScript 数据类型)和 Python 中的数据类型也是很容易找到对应关系的,大家可以看看下面的两张表。
表1:JavaScript 数据类型(值)对应的 Python 数据类型(值)
| JSON | Python |
| ------------ | ------------ |
| `object` |`dict`|
| `array` |`list`|
| `string` | `str` |
| `number ` |`int` / `float`|
| `number` (real) |`float`|
| `boolean` (`true` / `false`) | `bool` (`True` / `False`) |
| `null` | `None` |
表2:Python 数据类型(值)对应的JavaScript数据类型(值)
| Python | JSON |
| --------------------------- | ---------------------------- |
| `dict` | `object` |
| `list` / `tuple` | `array` |
| `str` | `string` |
| `int` / `float` | `number` |
| `bool` (`True` / `False`) | `boolean` (`true` / `false`) |
| `None` | `null` |
### 读写 JSON 格式的数据
在 Python 中,如果要将字典处理成 JSON 格式(以字符串形式存在),可以使用`json`模块的`dumps`函数,代码如下所示。
```python
import json
my_dict = {
'name': '骆昊',
'age': 40,
'friends': ['王大锤', '白元芳'],
'cars': [
{'brand': 'BMW', 'max_speed': 240},
{'brand': 'Audi', 'max_speed': 280},
{'brand': 'Benz', 'max_speed': 280}
]
}
print(json.dumps(my_dict))
```
运行上面的代码,输出如下所示,可以注意到中文字符都是用 Unicode 编码显示的。
```JSON
{"name": "\u9a86\u660a", "age": 40, "friends": ["\u738b\u5927\u9524", "\u767d\u5143\u82b3"], "cars": [{"brand": "BMW", "max_speed": 240}, {"brand": "Audi", "max_speed": 280}, {"brand": "Benz", "max_speed": 280}]}
```
如果要将字典处理成 JSON 格式并写入文本文件,只需要将`dumps`函数换成`dump`函数并传入文件对象即可,代码如下所示。
```python
import json
my_dict = {
'name': '骆昊',
'age': 40,
'friends': ['王大锤', '白元芳'],
'cars': [
{'brand': 'BMW', 'max_speed': 240},
{'brand': 'Audi', 'max_speed': 280},
{'brand': 'Benz', 'max_speed': 280}
]
}
with open('data.json', 'w') as file:
json.dump(my_dict, file)
```
执行上面的代码,会创建`data.json`文件,文件的内容跟上面代码的输出是一样的。
`json`模块有四个比较重要的函数,分别是:
- `dump` - 将 Python 对象按照 JSON 格式序列化到文件中
- `dumps` - 将 Python 对象处理成 JSON 格式的字符串
- `load` - 将文件中的 JSON 数据反序列化成对象
- `loads` - 将字符串的内容反序列化成 Python 对象
这里出现了两个概念,一个叫序列化,一个叫反序列化,[维基百科](https://zh.wikipedia.org/)上的解释是:“序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换为可以存储或传输的形式,这样在需要的时候能够恢复到原先的状态,而且通过序列化的数据重新获取字节时,可以利用这些字节来产生原始对象的副本(拷贝)。与这个过程相反的动作,即从一系列字节中提取数据结构的操作,就是反序列化(deserialization)”。
我们可以通过下面的代码,读取上面创建的`data.json`文件,将 JSON 格式的数据还原成 Python 中的字典。
```python
import json
with open('data.json', 'r') as file:
my_dict = json.load(file)
print(type(my_dict))
print(my_dict)
```
### 包管理工具 pip
Python 标准库中的`json`模块在数据序列化和反序列化时性能并不是非常理想,为了解决这个问题,可以使用三方库`ujson`来替换`json`。所谓三方库,是指非公司内部开发和使用的,也不是来自于官方标准库的 Python 模块,这些模块通常由其他公司、组织或个人开发,所以被称为三方库。虽然 Python 语言的标准库虽然已经提供了诸多模块来方便我们的开发,但是对于一个强大的语言来说,它的生态圈一定也是非常繁荣的。
之前安装 Python 解释器时,默认情况下已经勾选了安装 pip,大家可以在命令提示符或终端中通过`pip --version`来确定是否已经拥有了 pip。Pip 是 Python 的包管理工具,通过 pip 可以查找、安装、卸载、更新 Python 的三方库或工具,例如要安装替代`json`模块的`ujson`,可以使用下面的命令。
```Bash
pip install ujson
```
在默认情况下,pip 会访问`https://pypi.org/simple/`来获得三方库相关的数据,但是国内访问这个网站的速度并不是十分理想,因此国内用户可以使用豆瓣网或清华大学提供的镜像来替代这个默认的下载源,再执行安装三方库的操作,命令如下所示。
```Bash
pip config set global.index-url https://pypi.doubanio.com/simple
pip install ujson
```
可以通过`pip search`命令根据名字查找需要的三方库,可以通过`pip list`命令来查看已经安装过的三方库。如果想更新某个三方库,可以使用`pip install -U`或`pip install --upgrade`;如果要删除某个三方库,可以使用`pip uninstall`命令。
搜索`ujson`三方库。
```Bash
pip search ujson
micropython-cpython-ujson (0.2) - MicroPython module ujson ported to CPython
pycopy-cpython-ujson (0.2) - Pycopy module ujson ported to CPython
ujson (3.0.0) - Ultra fast JSON encoder and decoder for Python
ujson-bedframe (1.33.0) - Ultra fast JSON encoder and decoder for Python
ujson-segfault (2.1.57) - Ultra fast JSON encoder and decoder for Python. Continuing
development.
ujson-ia (2.1.1) - Ultra fast JSON encoder and decoder for Python (Internet
Archive fork)
ujson-x (1.37) - Ultra fast JSON encoder and decoder for Python
ujson-x-legacy (1.35.1) - Ultra fast JSON encoder and decoder for Python
drf_ujson (1.2) - Django Rest Framework UJSON Renderer
drf-ujson2 (1.6.1) - Django Rest Framework UJSON Renderer
ujsonDB (0.1.0) - A lightweight and simple database using ujson.
fast-json (0.3.2) - Combines best parts of json and ujson for fast serialization
decimal-monkeypatch (0.4.3) - Python 2 performance patches: decimal to cdecimal, json to
ujson for psycopg2
```
查看已经安装的三方库。
```Bash
pip list
Package Version
----------------------------- ----------
aiohttp 3.5.4
alipay 0.7.4
altgraph 0.16.1
amqp 2.4.2
... ...
```
更新`ujson`三方库。
```Bash
pip install -U ujson
```
删除`ujson`三方库。
```Bash
pip uninstall -y ujson
```
> **提示**:如果要更新`pip`自身,对于 macOS 系统来说,可以使用命令`pip install -U pip`。在 Windows 系统上,可以将命令替换为`python -m pip install -U --user pip`。
### 使用网络API获取数据
如果想在我们自己的程序中显示天气、路况、航班等信息,这些信息我们自己没有能力提供,所以必须使用网络数据服务。目前绝大多数的网络数据服务(或称之为网络 API)都是基于 [HTTP](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE) 或 HTTPS 提供 JSON 格式的数据,我们可以通过 Python 程序发送 HTTP 请求给指定的 URL(统一资源定位符),这个 URL 就是所谓的网络 API,如果请求成功,它会返回 HTTP 响应,而 HTTP 响应的消息体中就有我们需要的 JSON 格式的数据。关于 HTTP 的相关知识,可以看看阮一峰的[《HTTP协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)一文。
国内有很多提供网络 API 接口的网站,例如[聚合数据](https://www.juhe.cn/)、[百度智能云](https://apis.baidu.com/)等,这些网站上有免费的和付费的数据接口,国外的[{API}Search](http://apis.io/)网站也提供了类似的功能,有兴趣的可以自行研究。下面的例子演示了如何使用[`requests`](http://docs.python-requests.org/zh_CN/latest/)库(基于 HTTP 进行网络资源访问的三方库)访问网络 API 获取国内新闻并显示新闻标题和链接。在这个例子中,我们使用了名为[天聚数行](https://www.tianapi.com/)的网站提供的国内新闻数据接口,其中的身份标识(API Key)需要自己到网站上注册申请。在天聚数行网站注册账号后会自动分配 API Key,但是要访问接口获取数据,需要绑定验证邮箱或手机,然后还要申请需要使用的接口,如下图所示。
Python 通过 URL 接入网络,我们推荐大家使用`requests`三方库,它简单且强大,但需要自行安装。
```Bash
pip install requests
```
获取国内新闻并显示新闻标题和链接。
```python
import requests
resp = requests.get('http://api.tianapi.com/guonei/?key=APIKey&num=10')
if resp.status_code == 200:
data_model = resp.json()
for news in data_model['newslist']:
print(news['title'])
print(news['url'])
print('-' * 60)
```
上面的代码通过`requests`模块的`get`函数向天聚数行的国内新闻接口发起了一次请求,如果请求过程没有出现问题,`get`函数会返回一个`Response`对象,通过该对象的`status_code`属性表示 HTTP 响应状态码,如果不理解没关系,你只需要关注它的值,如果值等于`200`或者其他`2`字头的值,那么我们的请求是成功的。通过`Response`对象的`json()`方法可以将返回的 JSON 格式的数据直接处理成 Python 字典,非常方便。天聚数行国内新闻接口返回的 JSON 格式的数据(部分)如下图所示。
> **提示**:上面代码中的`APIKey`需要换成自己在天聚数行网站申请的API Key。天聚数行网站上还有提供了很多非常有意思的 API 接口,例如:垃圾分类、周公解梦等,大家可以仿照上面的代码来调用这些接口。每个接口都有对应的接口文档,文档中有关于如何使用接口的详细说明。
### 总结
Python 中实现序列化和反序列化除了使用`json`模块之外,还可以使用`pickle`和`shelve`模块,但是这两个模块是使用特有的序列化协议来序列化数据,因此序列化后的数据只能被 Python 识别,关于这两个模块的相关知识,有兴趣的读者可以自己查找网络上的资料。处理 JSON 格式的数据很显然是程序员必须掌握的一项技能,因为不管是访问网络 API 接口还是提供网络 API 接口给他人使用,都需要具备处理 JSON 格式数据的相关知识。
================================================
FILE: Day21-30/23.Python读写CSV文件.md
================================================
## Python读写CSV文件
### CSV 文件介绍
CSV(Comma Separated Values)全称逗号分隔值文件是一种简单、通用的文件格式,被广泛的应用于应用程序(数据库、电子表格等)数据的导入和导出以及异构系统之间的数据交换。因为 CSV 是纯文本文件,不管是什么操作系统和编程语言都是可以处理纯文本的,而且很多编程语言中都提供了对读写 CSV 文件的支持,因此 CSV 格式在数据处理和数据科学中被广泛应用。
CSV 文件有以下特点:
1. 纯文本,使用某种字符集(如 [ASCII](https://zh.wikipedia.org/wiki/ASCII)、[Unicode](https://zh.wikipedia.org/wiki/Unicode)、[GB2312](https://zh.wikipedia.org/wiki/GB2312)等);
2. 由一条条的记录组成(典型的是每行一条记录);
3. 每条记录被分隔符(如逗号、分号、制表符等)分隔为字段(列);
4. 每条记录都有同样的字段序列。
CSV 文件可以使用文本编辑器或类似于 Excel 电子表格这类工具打开和编辑,当使用 Excel 这类电子表格打开 CSV 文件时,你甚至感觉不到 CSV 和 Excel 文件的区别。很多数据库系统都支持将数据导出到 CSV 文件中,当然也支持从 CSV 文件中读入数据保存到数据库中,这些内容并不是现在要讨论的重点。
### 将数据写入 CSV 文件
现有五个学生三门课程的考试成绩需要保存到一个 CSV 文件中,要达成这个目标,可以使用 Python 标准库中的`csv`模块,该模块的`writer`函数会返回一个`csvwriter`对象,通过该对象的`writerow`或`writerows`方法就可以将数据写入到 CSV 文件中,具体的代码如下所示。
```python
import csv
import random
with open('scores.csv', 'w') as file:
writer = csv.writer(file)
writer.writerow(['姓名', '语文', '数学', '英语'])
names = ['关羽', '张飞', '赵云', '马超', '黄忠']
for name in names:
scores = [random.randrange(50, 101) for _ in range(3)]
scores.insert(0, name)
writer.writerow(scores)
```
生成的 CSV 文件的内容。
```
姓名,语文,数学,英语
关羽,98,86,61
张飞,86,58,80
赵云,95,73,70
马超,83,97,55
黄忠,61,54,87
```
需要说明的是上面的`writer`函数,除了传入要写入数据的文件对象外,还可以`dialect`参数,它表示 CSV 文件的方言,默认值是`excel`。除此之外,还可以通过`delimiter`、`quotechar`、`quoting`参数来指定分隔符(默认是逗号)、包围值的字符(默认是双引号)以及包围的方式。其中,包围值的字符主要用于当字段中有特殊符号时,通过添加包围值的字符可以避免二义性。大家可以尝试将上面第5行代码修改为下面的代码,然后查看生成的 CSV 文件。
```python
writer = csv.writer(file, delimiter='|', quoting=csv.QUOTE_ALL)
```
生成的 CSV 文件的内容。
```
"姓名"|"语文"|"数学"|"英语"
"关羽"|"88"|"64"|"65"
"张飞"|"76"|"93"|"79"
"赵云"|"78"|"55"|"76"
"马超"|"72"|"77"|"68"
"黄忠"|"70"|"72"|"51"
```
### 从 CSV 文件读取数据
如果要读取刚才创建的 CSV 文件,可以使用下面的代码,通过`csv`模块的`reader`函数可以创建出`csvreader`对象,该对象是一个迭代器,可以通过`next`函数或`for-in`循环读取到文件中的数据。
```python
import csv
with open('scores.csv', 'r') as file:
reader = csv.reader(file, delimiter='|')
for data_list in reader:
print(reader.line_num, end='\t')
for elem in data_list:
print(elem, end='\t')
print()
```
> **注意**:上面的代码对`csvreader`对象做`for`循环时,每次会取出一个列表对象,该列表对象包含了一行中所有的字段。
### 总结
将来如果大家使用Python做数据分析,很有可能会用到名为`pandas`的三方库,它是Python数据分析的神器之一。`pandas`中封装了名为`read_csv`和`to_csv`的函数用来读写 CSV 文件,其中`read_csv`会将读取到的数据变成一个`DataFrame`对象,而`DataFrame`就是`pandas`库中最重要的类型,它封装了一系列用于数据处理的方法(清洗、转换、聚合等);而`to_csv`会将`DataFrame`对象中的数据写入 CSV 文件,完成数据的持久化。`read_csv`函数和`to_csv`函数远远比原生的`csvreader`和`csvwriter`强大。
================================================
FILE: Day21-30/24.Python读写Excel文件-1.md
================================================
## Python读写Excel文件-1
### Excel简介
Excel 是 Microsoft(微软)为使用 Windows 和 macOS 操作系统开发的一款电子表格软件。Excel 凭借其直观的界面、出色的计算功能和图表工具,再加上成功的市场营销,一直以来都是最为流行的个人计算机数据处理软件。当然,Excel 也有很多竞品,例如 Google Sheets、LibreOffice Calc、Numbers 等,这些竞品基本上也能够兼容 Excel,至少能够读写较新版本的 Excel 文件,当然这些不是我们讨论的重点。掌握用 Python 程序操作 Excel 文件,可以让日常办公自动化的工作更加轻松愉快,而且在很多商业项目中,导入导出 Excel 文件都是特别常见的功能。
Python 操作 Excel 需要三方库的支持,如果要兼容 Excel 2007 以前的版本,也就是`xls`格式的 Excel 文件,可以使用三方库`xlrd`和`xlwt`,前者用于读 Excel 文件,后者用于写 Excel 文件。如果使用较新版本的 Excel,即`xlsx`格式的 Excel 文件,可以使用`openpyxl`库,当然这个库不仅仅可以操作Excel,还可以操作其他基于 Office Open XML 的电子表格文件。
本章我们先讲解基于`xlwt`和`xlrd`操作 Excel 文件,大家可以先使用下面的命令安装这两个三方库以及配合使用的工具模块`xlutils`。
```Bash
pip install xlwt xlrd xlutils
```
### 读Excel文件
例如在当前文件夹下有一个名为“阿里巴巴2020年股票数据.xls”的 Excel 文件,如果想读取并显示该文件的内容,可以通过如下所示的代码来完成。
```python
import xlrd
# 使用xlrd模块的open_workbook函数打开指定Excel文件并获得Book对象(工作簿)
wb = xlrd.open_workbook('阿里巴巴2020年股票数据.xls')
# 通过Book对象的sheet_names方法可以获取所有表单名称
sheetnames = wb.sheet_names()
print(sheetnames)
# 通过指定的表单名称获取Sheet对象(工作表)
sheet = wb.sheet_by_name(sheetnames[0])
# 通过Sheet对象的nrows和ncols属性获取表单的行数和列数
print(sheet.nrows, sheet.ncols)
for row in range(sheet.nrows):
for col in range(sheet.ncols):
# 通过Sheet对象的cell方法获取指定Cell对象(单元格)
# 通过Cell对象的value属性获取单元格中的值
value = sheet.cell(row, col).value
# 对除首行外的其他行进行数据格式化处理
if row > 0:
# 第1列的xldate类型先转成元组再格式化为“年月日”的格式
if col == 0:
# xldate_as_tuple函数的第二个参数只有0和1两个取值
# 其中0代表以1900-01-01为基准的日期,1代表以1904-01-01为基准的日期
value = xlrd.xldate_as_tuple(value, 0)
value = f'{value[0]}年{value[1]:>02d}月{value[2]:>02d}日'
# 其他列的number类型处理成小数点后保留两位有效数字的浮点数
else:
value = f'{value:.2f}'
print(value, end='\t')
print()
# 获取最后一个单元格的数据类型
# 0 - 空值,1 - 字符串,2 - 数字,3 - 日期,4 - 布尔,5 - 错误
last_cell_type = sheet.cell_type(sheet.nrows - 1, sheet.ncols - 1)
print(last_cell_type)
# 获取第一行的值(列表)
print(sheet.row_values(0))
# 获取指定行指定列范围的数据(列表)
# 第一个参数代表行索引,第二个和第三个参数代表列的开始(含)和结束(不含)索引
print(sheet.row_slice(3, 0, 5))
```
> **提示**:上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xls”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。
相信通过上面的代码,大家已经了解到了如何读取一个 Excel 文件,如果想知道更多关于`xlrd`模块的知识,可以阅读它的[官方文档](https://xlrd.readthedocs.io/en/latest/)。
### 写Excel文件
写入 Excel 文件可以通过`xlwt` 模块的`Workbook`类创建工作簿对象,通过工作簿对象的`add_sheet`方法可以添加工作表,通过工作表对象的`write`方法可以向指定单元格中写入数据,最后通过工作簿对象的`save`方法将工作簿写入到指定的文件或内存中。下面的代码实现了将5 个学生 3 门课程的考试成绩写入 Excel 文件的操作。
```python
import random
import xlwt
student_names = ['关羽', '张飞', '赵云', '马超', '黄忠']
scores = [[random.randrange(50, 101) for _ in range(3)] for _ in range(5)]
# 创建工作簿对象(Workbook)
wb = xlwt.Workbook()
# 创建工作表对象(Worksheet)
sheet = wb.add_sheet('一年级二班')
# 添加表头数据
titles = ('姓名', '语文', '数学', '英语')
for index, title in enumerate(titles):
sheet.write(0, index, title)
# 将学生姓名和考试成绩写入单元格
for row in range(len(scores)):
sheet.write(row + 1, 0, student_names[row])
for col in range(len(scores[row])):
sheet.write(row + 1, col + 1, scores[row][col])
# 保存Excel工作簿
wb.save('考试成绩表.xls')
```
### 调整单元格样式
在写Excel文件时,我们还可以为单元格设置样式,主要包括字体(Font)、对齐方式(Alignment)、边框(Border)和背景(Background)的设置,`xlwt`对这几项设置都封装了对应的类来支持。要设置单元格样式需要首先创建一个`XFStyle`对象,再通过该对象的属性对字体、对齐方式、边框等进行设定,例如在上面的例子中,如果希望将表头单元格的背景色修改为黄色,可以按照如下的方式进行操作。
```python
header_style = xlwt.XFStyle()
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN
# 0 - 黑色、1 - 白色、2 - 红色、3 - 绿色、4 - 蓝色、5 - 黄色、6 - 粉色、7 - 青色
pattern.pattern_fore_colour = 5
header_style.pattern = pattern
titles = ('姓名', '语文', '数学', '英语')
for index, title in enumerate(titles):
sheet.write(0, index, title, header_style)
```
如果希望为表头设置指定的字体,可以使用`Font`类并添加如下所示的代码。
```python
font = xlwt.Font()
# 字体名称
font.name = '华文楷体'
# 字体大小(20是基准单位,18表示18px)
font.height = 20 * 18
# 是否使用粗体
font.bold = True
# 是否使用斜体
font.italic = False
# 字体颜色
font.colour_index = 1
header_style.font = font
```
> **注意**:上面代码中指定的字体名(`font.name`)应当是本地系统有的字体,例如在我的电脑上有名为“华文楷体”的字体。
如果希望表头垂直居中对齐,可以使用下面的代码进行设置。
```python
align = xlwt.Alignment()
# 垂直方向的对齐方式
align.vert = xlwt.Alignment.VERT_CENTER
# 水平方向的对齐方式
align.horz = xlwt.Alignment.HORZ_CENTER
header_style.alignment = align
```
如果希望给表头加上黄色的虚线边框,可以使用下面的代码来设置。
```python
borders = xlwt.Borders()
props = (
('top', 'top_colour'), ('right', 'right_colour'),
('bottom', 'bottom_colour'), ('left', 'left_colour')
)
# 通过循环对四个方向的边框样式及颜色进行设定
for position, color in props:
# 使用setattr内置函数动态给对象指定的属性赋值
setattr(borders, position, xlwt.Borders.DASHED)
setattr(borders, color, 5)
header_style.borders = borders
```
如果要调整单元格的宽度(列宽)和表头的高度(行高),可以按照下面的代码进行操作。
```python
# 设置行高为40px
sheet.row(0).set_style(xlwt.easyxf(f'font:height {20 * 40}'))
titles = ('姓名', '语文', '数学', '英语')
for index, title in enumerate(titles):
# 设置列宽为200px
sheet.col(index).width = 20 * 200
# 设置单元格的数据和样式
sheet.write(0, index, title, header_style)
```
### 公式计算
对于前面打开的“阿里巴巴2020年股票数据.xls”文件,如果要统计全年收盘价(Close字段)的平均值以及全年交易量(Volume字段)的总和,可以使用Excel的公式计算即可。我们可以先使用`xlrd`读取Excel文件夹,然后通过`xlutils`三方库提供的`copy`函数将读取到的Excel文件转成`Workbook`对象进行写操作,在调用`write`方法时,可以将一个`Formula`对象写入单元格。
实现公式计算的代码如下所示。
```python
import xlrd
import xlwt
from xlutils.copy import copy
wb_for_read = xlrd.open_workbook('阿里巴巴2020年股票数据.xls')
sheet1 = wb_for_read.sheet_by_index(0)
nrows, ncols = sheet1.nrows, sheet1.ncols
wb_for_write = copy(wb_for_read)
sheet2 = wb_for_write.get_sheet(0)
sheet2.write(nrows, 4, xlwt.Formula(f'average(E2:E{nrows})'))
sheet2.write(nrows, 6, xlwt.Formula(f'sum(G2:G{nrows})'))
wb_for_write.save('阿里巴巴2020年股票数据汇总.xls')
```
> **说明**:上面的代码有一些小瑕疵,有兴趣的读者可以自行探索并思考如何解决。
### 总结
掌握了 Python 程序操作 Excel 的方法,可以解决日常办公中很多繁琐的处理 Excel 电子表格工作,最常见就是将多个数据格式相同的 Excel 文件合并到一个文件以及从多个 Excel 文件或表单中提取指定的数据。当然,如果要对表格数据进行处理,使用 Python 数据分析神器之一的 pandas 库可能更为方便。
================================================
FILE: Day21-30/25.Python读写Excel文件-2.md
================================================
## Python读写Excel文件-2
### Excel简介
Excel 是 Microsoft(微软)为使用 Windows 和 macOS 操作系统开发的一款电子表格软件。Excel 凭借其直观的界面、出色的计算功能和图表工具,再加上成功的市场营销,一直以来都是最为流行的个人计算机数据处理软件。当然,Excel 也有很多竞品,例如 Google Sheets、LibreOffice Calc、Numbers 等,这些竞品基本上也能够兼容 Excel,至少能够读写较新版本的 Excel 文件,当然这些不是我们讨论的重点。掌握用 Python 程序操作 Excel 文件,可以让日常办公自动化的工作更加轻松愉快,而且在很多商业项目中,导入导出 Excel 文件都是特别常见的功能。
本章我们继续讲解基于另一个三方库`openpyxl`如何进行 Excel 文件操作,首先需要先安装它。
```Bash
pip install openpyxl
```
`openpyxl`的优点在于,当我们打开一个 Excel 文件后,既可以对它进行读操作,又可以对它进行写操作,而且在操作的便捷性上是优于`xlwt`和`xlrd`的。此外,如果要进行样式编辑和公式计算,使用`openpyxl`也远比上一个章节我们讲解的方式更为简单,而且`openpyxl`还支持数据透视和插入图表等操作,功能非常强大。有一点需要再次强调,`openpyxl`并不支持操作 Office 2007 以前版本的 Excel 文件。
### 读取Excel文件
例如在当前文件夹下有一个名为“阿里巴巴2020年股票数据.xlsx”的 Excel 文件,如果想读取并显示该文件的内容,可以通过如下所示的代码来完成。
```python
import datetime
import openpyxl
# 加载一个工作簿 ---> Workbook
wb = openpyxl.load_workbook('阿里巴巴2020年股票数据.xlsx')
# 获取工作表的名字
print(wb.sheetnames)
# 获取工作表 ---> Worksheet
sheet = wb.worksheets[0]
# 获得单元格的范围
print(sheet.dimensions)
# 获得行数和列数
print(sheet.max_row, sheet.max_column)
# 获取指定单元格的值
print(sheet.cell(3, 3).value)
print(sheet['C3'].value)
print(sheet['G255'].value)
# 获取多个单元格(嵌套元组)
print(sheet['A2:C5'])
# 读取所有单元格的数据
for row_ch in range(2, sheet.max_row + 1):
for col_ch in 'ABCDEFG':
value = sheet[f'{col_ch}{row_ch}'].value
if type(value) == datetime.datetime:
print(value.strftime('%Y年%m月%d日'), end='\t')
elif type(value) == int:
print(f'{value:<10d}', end='\t')
elif type(value) == float:
print(f'{value:.4f}', end='\t')
else:
print(value, end='\t')
print()
```
> **提示**:上面代码中使用的Excel文件“阿里巴巴2020年股票数据.xlsx”可以通过后面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。
需要提醒大家一点,`openpyxl`获取指定的单元格有两种方式,一种是通过`cell`方法,需要注意,该方法的行索引和列索引都是从`1`开始的,这是为了照顾用惯了 Excel 的人的习惯;另一种是通过索引运算,通过指定单元格的坐标,例如`C3`、`G255`,也可以取得对应的单元格,再通过单元格对象的`value`属性,就可以获取到单元格的值。通过上面的代码,相信大家还注意到了,可以通过类似`sheet['A2:C5']`或`sheet['A2':'C5']`这样的切片操作获取多个单元格,该操作将返回嵌套的元组,相当于获取到了多行多列。
### 写Excel文件
下面我们使用`openpyxl`来进行写 Excel 操作。
```python
import random
import openpyxl
# 第一步:创建工作簿(Workbook)
wb = openpyxl.Workbook()
# 第二步:添加工作表(Worksheet)
sheet = wb.active
sheet.title = '期末成绩'
titles = ('姓名', '语文', '数学', '英语')
for col_index, title in enumerate(titles):
sheet.cell(1, col_index + 1, title)
names = ('关羽', '张飞', '赵云', '马超', '黄忠')
for row_index, name in enumerate(names):
sheet.cell(row_index + 2, 1, name)
for col_index in range(2, 5):
sheet.cell(row_index + 2, col_index, random.randrange(50, 101))
# 第四步:保存工作簿
wb.save('考试成绩表.xlsx')
```
### 调整样式和公式计算
在使用`openpyxl`操作 Excel 时,如果要调整单元格的样式,可以直接通过单元格对象(`Cell`对象)的属性进行操作。单元格对象的属性包括字体(`font`)、对齐(`alignment`)、边框(`border`)等,具体的可以参考`openpyxl`的[官方文档](https://openpyxl.readthedocs.io/en/stable/index.html)。在使用`openpyxl`时,如果需要做公式计算,可以完全按照 Excel 中的操作方式来进行,具体的代码如下所示。
```python
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side
# 对齐方式
alignment = Alignment(horizontal='center', vertical='center')
# 边框线条
side = Side(color='ff7f50', style='mediumDashed')
wb = openpyxl.load_workbook('考试成绩表.xlsx')
sheet = wb.worksheets[0]
# 调整行高和列宽
sheet.row_dimensions[1].height = 30
sheet.column_dimensions['E'].width = 120
sheet['E1'] = '平均分'
# 设置字体
sheet.cell(1, 5).font = Font(size=18, bold=True, color='ff1493', name='华文楷体')
# 设置对齐方式
sheet.cell(1, 5).alignment = alignment
# 设置单元格边框
sheet.cell(1, 5).border = Border(left=side, top=side, right=side, bottom=side)
for i in range(2, 7):
# 公式计算每个学生的平均分
sheet[f'E{i}'] = f'=average(B{i}:D{i})'
sheet.cell(i, 5).font = Font(size=12, color='4169e1', italic=True)
sheet.cell(i, 5).alignment = alignment
wb.save('考试成绩表.xlsx')
```
### 生成统计图表
通过`openpyxl`库,可以直接向 Excel 中插入统计图表,具体的做法跟在 Excel 中插入图表大体一致。我们可以创建指定类型的图表对象,然后通过该对象的属性对图表进行设置。当然,最为重要的是为图表绑定数据,即横轴代表什么,纵轴代表什么,具体的数值是多少。最后,可以将图表对象添加到表单中,具体的代码如下所示。
```python
from openpyxl import Workbook
from openpyxl.chart import BarChart, Reference
wb = Workbook(write_only=True)
sheet = wb.create_sheet()
rows = [
('类别', '销售A组', '销售B组'),
('手机', 40, 30),
('平板', 50, 60),
('笔记本', 80, 70),
('外围设备', 20, 10),
]
# 向表单中添加行
for row in rows:
sheet.append(row)
# 创建图表对象
chart = BarChart()
chart.type = 'col'
chart.style = 10
# 设置图表的标题
chart.title = '销售统计图'
# 设置图表纵轴的标题
chart.y_axis.title = '销量'
# 设置图表横轴的标题
chart.x_axis.title = '商品类别'
# 设置数据的范围
data = Reference(sheet, min_col=2, min_row=1, max_row=5, max_col=3)
# 设置分类的范围
cats = Reference(sheet, min_col=1, min_row=2, max_row=5)
# 给图表添加数据
chart.add_data(data, titles_from_data=True)
# 给图表设置分类
chart.set_categories(cats)
chart.shape = 4
# 将图表添加到表单指定的单元格中
sheet.add_chart(chart, 'A10')
wb.save('demo.xlsx')
```
运行上面的代码,打开生成的 Excel 文件,效果如下图所示。
### 总结
掌握了 Python 程序操作 Excel 的方法,可以解决日常办公中很多繁琐的处理 Excel 电子表格工作,最常见就是将多个数据格式相同的Excel 文件合并到一个文件以及从多个 Excel 文件或表单中提取指定的数据。如果数据体量较大或者处理数据的方式比较复杂,我们还是推荐大家使用 Python 数据分析神器之一的 pandas 库。
================================================
FILE: Day21-30/26.Python操作Word和PowerPoint文件.md
================================================
## Python操作Word和PowerPoint文件
在日常工作中,有很多简单重复的劳动其实完全可以交给 Python 程序,比如根据样板文件(模板文件)批量的生成很多个 Word 文件或 PowerPoint 文件。Word 是微软公司开发的文字处理程序,相信大家都不陌生,日常办公中很多正式的文档都是用 Word 进行撰写和编辑的,目前使用的 Word 文件后缀名一般为`.docx`。PowerPoint 是微软公司开发的演示文稿程序,是微软的 Office 系列软件中的一员,被商业人士、教师、学生等群体广泛使用,通常也将其称之为“幻灯片”。在 Python 中,可以使用名为`python-docx` 的三方库来操作 Word,可以使用名为`python-pptx`的三方库来生成 PowerPoint。
### 操作Word文档
我们可以先通过下面的命令来安装`python-docx`三方库。
```bash
pip install python-docx
```
按照[官方文档](https://python-docx.readthedocs.io/en/latest/)的介绍,我们可以使用如下所示的代码来生成一个简单的 Word 文档。
```python
from docx import Document
from docx.shared import Cm, Pt
from docx.document import Document as Doc
# 创建代表Word文档的Doc对象
document = Document() # type: Doc
# 添加大标题
document.add_heading('快快乐乐学Python', 0)
# 添加段落
p = document.add_paragraph('Python是一门非常流行的编程语言,它')
run = p.add_run('简单')
run.bold = True
run.font.size = Pt(18)
p.add_run('而且')
run = p.add_run('优雅')
run.font.size = Pt(18)
run.underline = True
p.add_run('。')
# 添加一级标题
document.add_heading('Heading, level 1', level=1)
# 添加带样式的段落
document.add_paragraph('Intense quote', style='Intense Quote')
# 添加无序列表
document.add_paragraph(
'first item in unordered list', style='List Bullet'
)
document.add_paragraph(
'second item in ordered list', style='List Bullet'
)
# 添加有序列表
document.add_paragraph(
'first item in ordered list', style='List Number'
)
document.add_paragraph(
'second item in ordered list', style='List Number'
)
# 添加图片(注意路径和图片必须要存在)
document.add_picture('resources/guido.jpg', width=Cm(5.2))
# 添加分节符
document.add_section()
records = (
('骆昊', '男', '1995-5-5'),
('孙美丽', '女', '1992-2-2')
)
# 添加表格
table = document.add_table(rows=1, cols=3)
table.style = 'Dark List'
hdr_cells = table.rows[0].cells
hdr_cells[0].text = '姓名'
hdr_cells[1].text = '性别'
hdr_cells[2].text = '出生日期'
# 为表格添加行
for name, sex, birthday in records:
row_cells = table.add_row().cells
row_cells[0].text = name
row_cells[1].text = sex
row_cells[2].text = birthday
# 添加分页符
document.add_page_break()
# 保存文档
document.save('demo.docx')
```
> **提示**:上面代码第7行中的注释`# type: Doc`是为了在PyCharm中获得代码补全提示,因为如果不清楚对象具体的数据类型,PyCharm 无法在后续代码中给出`Doc`对象的代码补全提示。
执行上面的代码,打开生成的 Word 文档,效果如下图所示。
对于一个已经存在的 Word 文件,我们可以通过下面的代码去遍历它所有的段落并获取对应的内容。
```python
from docx import Document
from docx.document import Document as Doc
doc = Document('resources/离职证明.docx') # type: Doc
for no, p in enumerate(doc.paragraphs):
print(no, p.text)
```
> **提示**:如果需要上面代码中的 Word 文件,可以通过下面的百度云盘地址进行获取。链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g 提取码:e7b4。
读取到的内容如下所示。
```
0
1 离 职 证 明
2
3 兹证明 王大锤 ,身份证号码: 100200199512120001 ,于 2018 年 8 月 7 日至 2020 年 6 月 28 日在我单位 开发部 部门担任 Java开发工程师 职务,在职期间无不良表现。因 个人 原因,于 2020 年 6 月 28 日起终止解除劳动合同。现已结清财务相关费用,办理完解除劳动关系相关手续,双方不存在任何劳动争议。
4
5 特此证明!
6
7
8 公司名称(盖章):成都风车车科技有限公司
9 2020 年 6 月 28 日
```
讲到这里,相信很多读者已经想到了,我们可以把上面的离职证明制作成一个模板文件,把姓名、身份证号、入职和离职日期等信息用占位符代替,这样通过对占位符的替换,就可以根据实际需要写入对应的信息,这样就可以批量的生成 Word 文档。
按照上面的思路,我们首先编辑一个离职证明的模板文件,如下图所示。
接下来我们读取该文件,将占位符替换为真实信息,就可以生成一个新的 Word 文档,如下所示。
```python
from docx import Document
from docx.document import Document as Doc
# 将真实信息用字典的方式保存在列表中
employees = [
{
'name': '骆昊',
'id': '100200198011280001',
'sdate': '2008年3月1日',
'edate': '2012年2月29日',
'department': '产品研发',
'position': '架构师',
'company': '成都华为技术有限公司'
},
{
'name': '王大锤',
'id': '510210199012125566',
'sdate': '2019年1月1日',
'edate': '2021年4月30日',
'department': '产品研发',
'position': 'Python开发工程师',
'company': '成都谷道科技有限公司'
},
{
'name': '李元芳',
'id': '2102101995103221599',
'sdate': '2020年5月10日',
'edate': '2021年3月5日',
'department': '产品研发',
'position': 'Java开发工程师',
'company': '同城企业管理集团有限公司'
},
]
# 对列表进行循环遍历,批量生成Word文档
for emp_dict in employees:
# 读取离职证明模板文件
doc = Document('resources/离职证明模板.docx') # type: Doc
# 循环遍历所有段落寻找占位符
for p in doc.paragraphs:
if '{' not in p.text:
continue
# 不能直接修改段落内容,否则会丢失样式
# 所以需要对段落中的元素进行遍历并进行查找替换
for run in p.runs:
if '{' not in run.text:
continue
# 将占位符换成实际内容
start, end = run.text.find('{'), run.text.find('}')
key, place_holder = run.text[start + 1:end], run.text[start:end + 1]
run.text = run.text.replace(place_holder, emp_dict[key])
# 每个人对应保存一个Word文档
doc.save(f'{emp_dict["name"]}离职证明.docx')
```
执行上面的代码,会在当前路径下生成三个 Word 文档,如下图所示。
### 生成PowerPoint
首先我们需要安装名为`python-pptx`的三方库,命令如下所示。
```Bash
pip install python-pptx
```
用 Python 操作 PowerPoint 的内容,因为实际应用场景不算很多,我不打算在这里进行赘述,有兴趣的读者可以自行阅读`python-pptx`的[官方文档](https://python-pptx.readthedocs.io/en/latest/),下面仅展示一段来自于官方文档的代码。
```python
from pptx import Presentation
# 创建幻灯片对象
pres = Presentation()
# 选择母版添加一页
title_slide_layout = pres.slide_layouts[0]
slide = pres.slides.add_slide(title_slide_layout)
# 获取标题栏和副标题栏
title = slide.shapes.title
subtitle = slide.placeholders[1]
# 编辑标题和副标题
title.text = "Welcome to Python"
subtitle.text = "Life is short, I use Python"
# 选择母版添加一页
bullet_slide_layout = pres.slide_layouts[1]
slide = pres.slides.add_slide(bullet_slide_layout)
# 获取页面上所有形状
shapes = slide.shapes
# 获取标题和主体
title_shape = shapes.title
body_shape = shapes.placeholders[1]
# 编辑标题
title_shape.text = 'Introduction'
# 编辑主体内容
tf = body_shape.text_frame
tf.text = 'History of Python'
# 添加一个一级段落
p = tf.add_paragraph()
p.text = 'X\'max 1989'
p.level = 1
# 添加一个二级段落
p = tf.add_paragraph()
p.text = 'Guido began to write interpreter for Python.'
p.level = 2
# 保存幻灯片
pres.save('test.pptx')
```
运行上面的代码,生成的 PowerPoint 文件如下图所示。
### 总结
用 Python 程序解决办公自动化的问题真的非常酷,它可以将我们从繁琐乏味的劳动中解放出来。写这类代码就是去做一件一劳永逸的事情,写代码的过程即便不怎么愉快,使用这些代码的时候应该是非常开心的。
================================================
FILE: Day21-30/27.Python操作PDF文件.md
================================================
## Python操作PDF文件
PDF 是 Portable Document Format 的缩写,这类文件通常使用`.pdf`作为其扩展名。在日常开发工作中,最容易遇到的就是从 PDF 中读取文本内容以及用已有的内容生成PDF文档这两个任务。
### 从PDF中提取文本
在 Python 中,可以使用名为`PyPDF2`的三方库来读取 PDF 文件,可以使用下面的命令来安装它。
```Bash
pip install PyPDF2
```
`PyPDF2`没有办法从 PDF 文档中提取图像、图表或其他媒体,但它可以提取文本,并将其返回为 Python 字符串。
```python
import PyPDF2
reader = PyPDF2.PdfReader('test.pdf')
for page in reader.pages:
print(page.extract_text())
```
> **提示**:本章代码使用到的 PDF 文件都可以通过下面的百度云盘地址进行获取,链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g,提取码:e7b4。
当然,`PyPDF2`并不是什么样的 PDF 文档都能提取出文字来,这个问题就我所知并没有什么特别好的解决方法,尤其是在提取中文的时候。网上也有很多讲解从 PDF 中提取文字的文章,推荐大家自行阅读[《三大神器助力Python提取pdf文档信息》](https://cloud.tencent.com/developer/article/1395339)一文进行了解。
要从 PDF 文件中提取文本也可以直接使用三方的命令行工具,具体的做法如下所示。
```Bash
pip install pdfminer.six
pdf2text.py test.pdf
```
### 旋转和叠加页面
上面的代码中通过创建`PdfFileReader`对象的方式来读取 PDF 文档,该对象的`getPage`方法可以获得PDF文档的指定页并得到一个`PageObject`对象,通过`PageObject`对象的`rotateClockwise`和`rotateCounterClockwise`方法可以实现页面的顺时针和逆时针方向旋转,通过`PageObject`对象的`addBlankPage`方法可以添加一个新的空白页,代码如下所示。
```python
reader = PyPDF2.PdfReader('XGBoost.pdf')
writer = PyPDF2.PdfWriter()
for no, page in enumerate(reader.pages):
if no % 2 == 0:
new_page = page.rotate(-90)
else:
new_page = page.rotate(90)
writer.add_page(new_page)
with open('temp.pdf', 'wb') as file_obj:
writer.write(file_obj)
```
### 加密PDF文件
使用`PyPDF2`中的`PdfFileWrite`对象可以为PDF文档加密,如果需要给一系列的PDF文档设置统一的访问口令,使用Python程序来处理就会非常的方便。
```python
import PyPDF2
reader = PyPDF2.PdfReader('XGBoost.pdf')
writer = PyPDF2.PdfWriter()
for page in reader.pages:
writer.add_page(page)
writer.encrypt('foobared')
with open('temp.pdf', 'wb') as file_obj:
writer.write(file_obj)
```
### 批量添加水印
上面提到的`PageObject`对象还有一个名为`mergePage`的方法,可以两个 PDF 页面进行叠加,通过这个操作,我们很容易实现给PDF文件添加水印的功能。例如要给上面的“XGBoost.pdf”文件添加一个水印,我们可以先准备好一个提供水印页面的 PDF 文件,然后将包含水印的`PageObject`读取出来,然后再循环遍历“XGBoost.pdf”文件的每个页,获取到`PageObject`对象,然后通过`mergePage`方法实现水印页和原始页的合并,代码如下所示。
```python
reader1 = PyPDF2.PdfReader('XGBoost.pdf')
reader2 = PyPDF2.PdfReader('watermark.pdf')
writer = PyPDF2.PdfWriter()
watermark_page = reader2.pages[0]
for page in reader1.pages:
page.merge_page(watermark_page)
writer.add_page(page)
with open('temp.pdf', 'wb') as file_obj:
writer.write(file_obj)
```
如果愿意,还可以让奇数页和偶数页使用不同的水印,大家可以自己思考下应该怎么做。
### 创建PDF文件
创建 PDF 文档需要三方库`reportlab`的支持,安装的方法如下所示。
```Bash
pip install reportlab
```
下面通过一个例子为大家展示`reportlab`的用法。
```python
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
pdf_canvas = canvas.Canvas('resources/demo.pdf', pagesize=A4)
width, height = A4
# 绘图
image = canvas.ImageReader('resources/guido.jpg')
pdf_canvas.drawImage(image, 20, height - 395, 250, 375)
# 显示当前页
pdf_canvas.showPage()
# 注册字体文件
pdfmetrics.registerFont(TTFont('Font1', 'resources/fonts/Vera.ttf'))
pdfmetrics.registerFont(TTFont('Font2', 'resources/fonts/青呱石头体.ttf'))
# 写字
pdf_canvas.setFont('Font2', 40)
pdf_canvas.setFillColorRGB(0.9, 0.5, 0.3, 1)
pdf_canvas.drawString(width // 2 - 120, height // 2, '你好,世界!')
pdf_canvas.setFont('Font1', 40)
pdf_canvas.setFillColorRGB(0, 1, 0, 0.5)
pdf_canvas.rotate(18)
pdf_canvas.drawString(250, 250, 'hello, world!')
# 保存
pdf_canvas.save()
```
上面的代码如果不太理解也没有关系,等真正需要用 Python 创建 PDF 文档的时候,再好好研读一下`reportlab`的[官方文档](https://www.reportlab.com/docs/reportlab-userguide.pdf)就可以了。
> **提示**:上面代码中用到的图片和字体可以通过下面的百度云盘链接获取,链接:https://pan.baidu.com/s/1rQujl5RQn9R7PadB2Z5g_g,提取码:e7b4。
### 总结
在学习完上面的内容之后,相信大家已经知道像合并多个 PDF 文件这样的工作应该如何用 Python 代码来处理了,赶紧自己动手试一试吧。
================================================
FILE: Day21-30/28.Python处理图像.md
================================================
## Python处理图像
### 入门知识
1. 颜色。如果你有使用颜料画画的经历,那么一定知道混合红、黄、蓝三种颜料可以得到其他的颜色,事实上这三种颜色就是美术中的三原色,它们是不能再分解的基本颜色。在计算机中,我们可以将红、绿、蓝三种色光以不同的比例叠加来组合成其他的颜色,因此这三种颜色就是色光三原色。在计算机系统中,我们通常会将一个颜色表示为一个 RGB 值或 RGBA 值(其中的 A 表示 Alpha 通道,它决定了透过这个图像的像素,也就是透明度)。
| 名称 | RGB值 | 名称 | RGB值 |
| :---------: | :-------------: | :----------: | :-----------: |
| White(白) | (255, 255, 255) | Red(红) | (255, 0, 0) |
| Green(绿) | (0, 255, 0) | Blue(蓝) | (0, 0, 255) |
| Gray(灰) | (128, 128, 128) | Yellow(黄) | (255, 255, 0) |
| Black(黑) | (0, 0, 0) | Purple(紫) | (128, 0, 128) |
2. 像素。对于一个由数字序列表示的图像来说,最小的单位就是图像上单一颜色的小方格,这些小方块都有一个明确的位置和被分配的色彩数值,而这些一小方格的颜色和位置决定了该图像最终呈现出来的样子,它们是不可分割的单位,我们通常称之为像素(pixel)。每一个图像都包含了一定量的像素,这些像素决定图像在屏幕上所呈现的大小,大家如果爱好拍照或者自拍,对像素这个词就不会陌生。
### 用Pillow处理图像
Pillow 是由从著名的 Python 图像处理库 PIL 发展出来的一个分支,通过 Pillow 可以实现图像压缩和图像处理等各种操作。可以使用下面的命令来安装 Pillow。
```Shell
pip install pillow
```
Pillow 中最为重要的是`Image`类,可以通过`Image`模块的`open`函数来读取图像并获得`Image`类型的对象。
1. 读取和显示图像
```python
from PIL import Image
# 读取图像获得Image对象
image = Image.open('guido.jpg')
# 通过Image对象的format属性获得图像的格式
print(image.format) # JPEG
# 通过Image对象的size属性获得图像的尺寸
print(image.size) # (500, 750)
# 通过Image对象的mode属性获取图像的模式
print(image.mode) # RGB
# 通过Image对象的show方法显示图像
image.show()
```
2. 剪裁图像
```python
# 通过Image对象的crop方法指定剪裁区域剪裁图像
image.crop((80, 20, 310, 360)).show()
```
3. 生成缩略图
```python
# 通过Image对象的thumbnail方法生成指定尺寸的缩略图
image.thumbnail((128, 128))
image.show()
```
4. 缩放和黏贴图像
```python
# 读取骆昊的照片获得Image对象
luohao_image = Image.open('luohao.png')
# 读取吉多的照片获得Image对象
guido_image = Image.open('guido.jpg')
# 从吉多的照片上剪裁出吉多的头
guido_head = guido_image.crop((80, 20, 310, 360))
width, height = guido_head.size
# 使用Image对象的resize方法修改图像的尺寸
# 使用Image对象的paste方法将吉多的头粘贴到骆昊的照片上
luohao_image.paste(guido_head.resize((int(width / 1.5), int(height / 1.5))), (172, 40))
luohao_image.show()
```
5. 旋转和翻转
```python
image = Image.open('guido.jpg')
# 使用Image对象的rotate方法实现图像的旋转
image.rotate(45).show()
# 使用Image对象的transpose方法实现图像翻转
# Image.FLIP_LEFT_RIGHT - 水平翻转
# Image.FLIP_TOP_BOTTOM - 垂直翻转
image.transpose(Image.FLIP_TOP_BOTTOM).show()
```
6. 操作像素
```python
for x in range(80, 310):
for y in range(20, 360):
# 通过Image对象的putpixel方法修改图像指定像素点
image.putpixel((x, y), (128, 128, 128))
image.show()
```
7. 滤镜效果
```python
from PIL import ImageFilter
# 使用Image对象的filter方法对图像进行滤镜处理
# ImageFilter模块包含了诸多预设的滤镜也可以自定义滤镜
image.filter(ImageFilter.CONTOUR).show()
```
### 使用Pillow绘图
Pillow 中有一个名为`ImageDraw`的模块,该模块的`Draw`函数会返回一个`ImageDraw`对象,通过`ImageDraw`对象的`arc`、`line`、`rectangle`、`ellipse`、`polygon`等方法,可以在图像上绘制出圆弧、线条、矩形、椭圆、多边形等形状,也可以通过该对象的`text`方法在图像上添加文字。
要绘制如上图所示的图像,完整的代码如下所示。
```python
import random
from PIL import Image, ImageDraw, ImageFont
def random_color():
"""生成随机颜色"""
red = random.randint(0, 255)
green = random.randint(0, 255)
blue = random.randint(0, 255)
return red, green, blue
width, height = 800, 600
# 创建一个800*600的图像,背景色为白色
image = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255))
# 创建一个ImageDraw对象
drawer = ImageDraw.Draw(image)
# 通过指定字体和大小获得ImageFont对象
font = ImageFont.truetype('Kongxin.ttf', 32)
# 通过ImageDraw对象的text方法绘制文字
drawer.text((300, 50), 'Hello, world!', fill=(255, 0, 0), font=font)
# 通过ImageDraw对象的line方法绘制两条对角直线
drawer.line((0, 0, width, height), fill=(0, 0, 255), width=2)
drawer.line((width, 0, 0, height), fill=(0, 0, 255), width=2)
xy = width // 2 - 60, height // 2 - 60, width // 2 + 60, height // 2 + 60
# 通过ImageDraw对象的rectangle方法绘制矩形
drawer.rectangle(xy, outline=(255, 0, 0), width=2)
# 通过ImageDraw对象的ellipse方法绘制椭圆
for i in range(4):
left, top, right, bottom = 150 + i * 120, 220, 310 + i * 120, 380
drawer.ellipse((left, top, right, bottom), outline=random_color(), width=8)
# 显示图像
image.show()
# 保存图像
image.save('result.png')
```
> **注意**:上面代码中使用的字体文件需要根据自己准备,可以选择自己喜欢的字体文件并放置在代码目录下。
### 总结
使用 Python 语言做开发,除了可以用 Pillow 来处理图像外,还可以使用更为强大的 OpenCV 库来完成图形图像的处理,OpenCV(**Open** Source **C**omputer **V**ision Library)是一个跨平台的计算机视觉库,可以用来开发实时图像处理、计算机视觉和模式识别程序。在我们的日常工作中,有很多繁琐乏味的任务其实都可以通过 Python 程序来处理,编程的目的就是让计算机帮助我们解决问题,减少重复乏味的劳动。通过本章节的学习,相信大家已经感受到了使用 Python 程序绘图改图的乐趣,其实 Python 能做的事情还远不止这些,继续你的学习吧。
================================================
FILE: Day21-30/29.Python发送邮件和短信.md
================================================
## Python发送邮件和短信
在前面的课程中,我们已经教会大家如何用 Python 程序自动的生成 Excel、Word、PDF 文档,接下来我们还可以更进一步,就是通过邮件将生成好的文档发送给指定的收件人,然后用短信告知对方我们发出了邮件。这些事情利用 Python 程序也可以轻松愉快的解决。
### 发送电子邮件
在即时通信软件如此发达的今天,电子邮件仍然是互联网上使用最为广泛的应用之一,公司向应聘者发出录用通知、网站向用户发送一个激活账号的链接、银行向客户推广它们的理财产品等几乎都是通过电子邮件来完成的,而这些任务应该都是由程序自动完成的。
我们可以用 HTTP(超文本传输协议)来访问网站资源,HTTP 是一个应用级协议,它建立在 TCP(传输控制协议)之上,TCP 为很多应用级协议提供了可靠的数据传输服务。如果要发送电子邮件,需要使用 SMTP(简单邮件传输协议),它也是建立在 TCP 之上的应用级协议,规定了邮件的发送者如何跟邮件服务器进行通信的细节。Python 通过名为`smtplib`的模块将这些操作简化成了`SMTP_SSL`对象,通过该对象的`login`和`send_mail`方法,就能够完成发送邮件的操作。
我们先尝试一下发送一封极为简单的邮件,该邮件不包含附件、图片以及其他超文本内容。发送邮件首先需要接入邮件服务器,我们可以自己架设邮件服务器,这件事情对新手并不友好,但是我们可以选择使用第三方提供的邮件服务。例如,我在
用手机扫码上面的二维码可以通过发送短信的方式来获取授权码,短信发送成功后,点击“我已发送”就可以获得授权码。授权码需要妥善保管,因为一旦泄露就会被其他人冒用你的身份来发送邮件。接下来,我们就可以编写发送邮件的代码了,如下所示。
```python
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# 创建邮件主体对象
email = MIMEMultipart()
# 设置发件人、收件人和主题
email['From'] = 'xxxxxxxxx@126.com'
email['To'] = 'yyyyyy@qq.com;zzzzzz@1000phone.com'
email['Subject'] = Header('上半年工作情况汇报', 'utf-8')
# 添加邮件正文内容
content = """据德国媒体报道,当地时间9日,德国火车司机工会成员进行了投票,
定于当地时间10日起进行全国性罢工,货运交通方面的罢工已于当地时间10日19时开始。
此后,从11日凌晨2时到13日凌晨2时,德国全国范围内的客运和铁路基础设施将进行48小时的罢工。"""
email.attach(MIMEText(content, 'plain', 'utf-8'))
# 创建SMTP_SSL对象(连接邮件服务器)
smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465)
# 通过用户名和授权码进行登录
smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码')
# 发送邮件(发件人、收件人、邮件内容(字符串))
smtp_obj.sendmail(
'xxxxxxxxx@126.com',
['yyyyyy@qq.com', 'zzzzzz@1000phone.com'],
email.as_string()
)
```
如果要发送带有附件的邮件,只需要将附件的内容处理成 BASE64 编码,那么它就和普通的文本内容几乎没有什么区别。BASE64 是一种基于 64 个可打印字符来表示二进制数据的表示方法,常用于某些需要表示、传输、存储二进制数据的场合,电子邮件就是其中之一。对这种编码方式不理解的同学,推荐阅读[《Base64笔记》](http://www.ruanyifeng.com/blog/2008/06/base64.html)一文。在之前的内容中,我们也提到过,Python 标准库的`base64`模块提供了对 BASE64 编解码的支持。
下面的代码演示了如何发送带附件的邮件。
```python
import smtplib
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from urllib.parse import quote
# 创建邮件主体对象
email = MIMEMultipart()
# 设置发件人、收件人和主题
email['From'] = 'xxxxxxxxx@126.com'
email['To'] = 'zzzzzzzz@1000phone.com'
email['Subject'] = Header('请查收离职证明文件', 'utf-8')
# 添加邮件正文内容(带HTML标签排版的内容)
content = """亲爱的前同事:
你需要的离职证明在附件中,请查收!
祝,好!
孙美丽 即日
""" email.attach(MIMEText(content, 'html', 'utf-8')) # 读取作为附件的文件 with open(f'resources/王大锤离职证明.docx', 'rb') as file: attachment = MIMEText(file.read(), 'base64', 'utf-8') # 指定内容类型 attachment['content-type'] = 'application/octet-stream' # 将中文文件名处理成百分号编码 filename = quote('王大锤离职证明.docx') # 指定如何处置内容 attachment['content-disposition'] = f'attachment; filename="{filename}"' # 创建SMTP_SSL对象(连接邮件服务器) smtp_obj = smtplib.SMTP_SSL('smtp.126.com', 465) # 通过用户名和授权码进行登录 smtp_obj.login('xxxxxxxxx@126.com', '邮件服务器的授权码') # 发送邮件(发件人、收件人、邮件内容(字符串)) smtp_obj.sendmail( 'xxxxxxxxx@126.com', 'zzzzzzzz@1000phone.com', email.as_string() ) ``` 为了方便大家用 Python 实现邮件发送,我将上面的代码封装成了函数,使用的时候大家只需要调整邮件服务器域名、端口、用户名和授权码就可以了。 ```python import smtplib from email.header import Header from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from urllib.parse import quote # 邮件服务器域名(自行修改) EMAIL_HOST = 'smtp.126.com' # 邮件服务端口(通常是465) EMAIL_PORT = 465 # 登录邮件服务器的账号(自行修改) EMAIL_USER = 'xxxxxxxxx@126.com' # 开通SMTP服务的授权码(自行修改) EMAIL_AUTH = '邮件服务器的授权码' def send_email(*, from_user, to_users, subject='', content='', filenames=[]): """发送邮件 :param from_user: 发件人 :param to_users: 收件人,多个收件人用英文分号进行分隔 :param subject: 邮件的主题 :param content: 邮件正文内容 :param filenames: 附件要发送的文件路径 """ email = MIMEMultipart() email['From'] = from_user email['To'] = to_users email['Subject'] = subject message = MIMEText(content, 'plain', 'utf-8') email.attach(message) for filename in filenames: with open(filename, 'rb') as file: pos = filename.rfind('/') display_filename = filename[pos + 1:] if pos >= 0 else filename display_filename = quote(display_filename) attachment = MIMEText(file.read(), 'base64', 'utf-8') attachment['content-type'] = 'application/octet-stream' attachment['content-disposition'] = f'attachment; filename="{display_filename}"' email.attach(attachment) smtp = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT) smtp.login(EMAIL_USER, EMAIL_AUTH) smtp.sendmail(from_user, to_users.split(';'), email.as_string()) ``` ### 发送短信 发送短信也是项目中常见的功能,网站的注册码、验证码、营销信息基本上都是通过短信来发送给用户的。发送短信需要三方平台的支持,下面我们以[螺丝帽平台](https://luosimao.com/)为例,为大家介绍如何用 Python 程序发送短信。注册账号和购买短信服务的细节我们不在这里进行赘述,大家可以咨询平台的客服。
接下来,我们可以通过`requests`库向平台提供的短信网关发起一个 HTTP 请求,通过将接收短信的手机号和短信内容作为参数,就可以发送短信,代码如下所示。
```python
import random
import requests
def send_message_by_luosimao(tel, message):
"""发送短信(调用螺丝帽短信网关)"""
resp = requests.post(
url='http://sms-api.luosimao.com/v1/send.json',
auth=('api', 'key-注册成功后平台分配的KEY'),
data={
'mobile': tel,
'message': message
},
timeout=10,
verify=False
)
return resp.json()
def gen_mobile_code(length=6):
"""生成指定长度的手机验证码"""
return ''.join(random.choices('0123456789', k=length))
def main():
code = gen_mobile_code()
message = f'您的短信验证码是{code},打死也不能告诉别人哟!【Python小课】'
print(send_message_by_luosimao('13500112233', message))
if __name__ == '__main__':
main()
```
上面请求螺丝帽的短信网关`http://sms-api.luosimao.com/v1/send.json`会返回JSON格式的数据,如果返回`{'error': 0, 'msg': 'OK'}`就说明短信已经发送成功了,如果`error`的值不是`0`,可以通过查看官方的[开发文档](https://luosimao.com/docs/api/)了解到底哪个环节出了问题。螺丝帽平台常见的错误类型如下图所示。
目前,大多数短信平台都会要求短信内容必须附上签名,下图是我在螺丝帽平台配置的短信签名“【Python小课】”。有些涉及到敏感内容的短信,还需要提前配置短信模板,有兴趣的读者可以自行研究。一般情况下,平台为了防范短信被盗用,还会要求设置“IP 白名单”,不清楚如何配置的可以咨询平台客服。
当然国内的短信平台很多,读者可以根据自己的需要进行选择(通常会考虑费用预算、短信达到率、使用的难易程度等指标),如果需要在商业项目中使用短信服务建议购买短信平台提供的套餐服务。
### 总结
其实,发送邮件和发送短信一样,也可以通过调用三方服务来完成,在实际的商业项目中,建议自己架设邮件服务器或购买三方服务来发送邮件,这个才是比较靠谱的选择。
================================================
FILE: Day21-30/30.正则表达式的应用.md
================================================
## 正则表达式的应用
### 正则表达式相关知识
在编写处理字符串的程序时,经常会遇到在一段文本中查找符合某些规则的字符串的需求,正则表达式就是用于描述这些规则的工具,换句话说,我们可以使用正则表达式来定义字符串的匹配模式,即如何检查一个字符串是否有跟某种模式匹配的部分或者从一个字符串中将与模式匹配的部分提取出来或者替换掉。
举一个简单的例子,如果你在 Windows 操作系统中使用过文件查找并且在指定文件名时使用过通配符(`*`和`?`),那么正则表达式也是与之类似的用 来进行文本匹配的工具,只不过比起通配符正则表达式更强大,它能更精确地描述你的需求,当然你付出的代价是书写一个正则表达式比使用通配符要复杂得多,因为任何给你带来好处的东西都需要你付出对应的代价。
再举一个例子,我们从某个地方(可能是一个文本文件,也可能是网络上的一则新闻)获得了一个字符串,希望在字符串中找出手机号和座机号。当然我们可以设定手机号是 11 位的数字(注意并不是随机的 11 位数字,因为你没有见过“25012345678”这样的手机号),而座机号则是类似于“区号-号码”这样的模式,如果不使用正则表达式要完成这个任务就会比较麻烦。最初计算机是为了做数学运算而诞生的,处理的信息基本上都是数值,而今天我们在日常工作中处理的信息很多都是文本数据,我们希望计算机能够识别和处理符合某些模式的文本,正则表达式就显得非常重要了。今天几乎所有的编程语言都提供了对正则表达式操作的支持,Python 通过标准库中的`re`模块来支持正则表达式操作。
关于正则表达式的相关知识,大家可以阅读一篇非常有名的博文叫[《正则表达式30分钟入门教程》](https://deerchao.net/tutorials/regex/regex.htm),读完这篇文章后你就可以看懂下面的表格,这是我们对正则表达式中的一些基本符号进行的扼要总结。
| 符号 | 解释 | 示例 | 说明 |
| -------------- | -------------------------------- | ------------------ | ------------------------------------------------------------ |
| `.` | 匹配任意字符 | `b.t` | 可以匹配bat / but / b#t / b1t等 |
| `\w` | 匹配字母/数字/下划线 | `b\wt` | 可以匹配bat / b1t / b_t等
```python
import re
# 创建正则表达式对象,使用了前瞻和回顾来保证手机号前后不应该再出现数字
pattern = re.compile(r'(?<=\D)1[34578]\d{9}(?=\D)')
sentence = '''重要的事情说8130123456789遍,我的手机号是13512346789这个靓号,
不是15600998765,也不是110或119,王大锤的手机号才是15600998765。'''
# 方法一:查找所有匹配并保存到一个列表中
tels_list = re.findall(pattern, sentence)
for tel in tels_list:
print(tel)
print('--------华丽的分隔线--------')
# 方法二:通过迭代器取出匹配对象并获得匹配的内容
for temp in pattern.finditer(sentence):
print(temp.group())
print('--------华丽的分隔线--------')
# 方法三:通过search函数指定搜索位置找出所有匹配
m = pattern.search(sentence)
while m:
print(m.group())
m = pattern.search(sentence, m.end())
```
> **说明:** 上面匹配国内手机号的正则表达式并不够好,因为像 14 开头的号码只有 145 或 147,而上面的正则表达式并没有考虑这种情况,要匹配国内手机号,更好的正则表达式的写法是:`(?<=\D)(1[38]\d{9}|14[57]\d{8}|15[0-35-9]\d{8}|17[678]\d{8})(?=\D)`,国内好像已经有 19 和 16 开头的手机号了,但是这个暂时不在我们考虑之列。
#### 例子3:替换字符串中的不良内容
```python
import re
sentence = 'Oh, shit! 你是傻逼吗? Fuck you.'
purified = re.sub('fuck|shit|[傻煞沙][比笔逼叉缺吊碉雕]',
'*', sentence, flags=re.IGNORECASE)
print(purified) # Oh, *! 你是*吗? * you.
```
> **说明:**` re`模块的正则表达式相关函数中都有一个`flags`参数,它代表了正则表达式的匹配标记,可以通过该标记来指定匹配时是否忽略大小写、是否进行多行匹配、是否显示调试信息等。如果需要为`flags`参数指定多个值,可以使用[按位或运算符](http://www.runoob.com/python/python-operators.html#ysf5)进行叠加,如`flags=re.I | re.M`。
#### 例子4:拆分长字符串
```python
import re
poem = '窗前明月光,疑是地上霜。举头望明月,低头思故乡。'
sentences_list = re.split(r'[,。]', poem)
sentences_list = [sentence for sentence in sentences_list if sentence]
for sentence in sentences_list:
print(sentence)
```
### 总结
正则表达式在字符串的处理和匹配上真的非常强大,通过上面的例子相信大家已经感受到了正则表达式的魅力,当然写一个正则表达式对新手来说并不是那么容易,但是很多事情都是熟能生巧,大胆的去尝试就行了,有一个在线的[正则表达式测试工具](https://c.runoob.com/front-end/854)相信能够在一定程度上帮到大家。
================================================
FILE: Day31-35/31.Python语言进阶.md
================================================
## Python语言进阶
### 重要知识点
- 生成式(推导式)的用法
```Python
prices = {
'AAPL': 191.88,
'GOOG': 1186.96,
'IBM': 149.24,
'ORCL': 48.44,
'ACN': 166.89,
'FB': 208.09,
'SYMC': 21.29
}
# 用股票价格大于100元的股票构造一个新的字典
prices2 = {key: value for key, value in prices.items() if value > 100}
print(prices2)
```
> 说明:生成式(推导式)可以用来生成列表、集合和字典。
- 嵌套的列表的坑
```Python
names = ['关羽', '张飞', '赵云', '马超', '黄忠']
courses = ['语文', '数学', '英语']
# 录入五个学生三门课程的成绩
# 错误 - 参考http://pythontutor.com/visualize.html#mode=edit
# scores = [[None] * len(courses)] * len(names)
scores = [[None] * len(courses) for _ in range(len(names))]
for row, name in enumerate(names):
for col, course in enumerate(courses):
scores[row][col] = float(input(f'请输入{name}的{course}成绩: '))
print(scores)
```
[Python Tutor](http://pythontutor.com/) - VISUALIZE CODE AND GET LIVE HELP
- `heapq`模块(堆排序)
```Python
"""
从列表中找出最大的或最小的N个元素
堆结构(大根堆/小根堆)
"""
import heapq
list1 = [34, 25, 12, 99, 87, 63, 58, 78, 88, 92]
list2 = [
{'name': 'IBM', 'shares': 100, 'price': 91.1},
{'name': 'AAPL', 'shares': 50, 'price': 543.22},
{'name': 'FB', 'shares': 200, 'price': 21.09},
{'name': 'HPQ', 'shares': 35, 'price': 31.75},
{'name': 'YHOO', 'shares': 45, 'price': 16.35},
{'name': 'ACME', 'shares': 75, 'price': 115.65}
]
print(heapq.nlargest(3, list1))
print(heapq.nsmallest(3, list1))
print(heapq.nlargest(2, list2, key=lambda x: x['price']))
print(heapq.nlargest(2, list2, key=lambda x: x['shares']))
```
- `itertools`模块
```Python
"""
迭代工具模块
"""
import itertools
# 产生ABCD的全排列
itertools.permutations('ABCD')
# 产生ABCDE的五选三组合
itertools.combinations('ABCDE', 3)
# 产生ABCD和123的笛卡尔积
itertools.product('ABCD', '123')
# 产生ABC的无限循环序列
itertools.cycle(('A', 'B', 'C'))
```
- `collections`模块
常用的工具类:
- `namedtuple`:命令元组,它是一个类工厂,接受类型的名称和属性列表来创建一个类。
- `deque`:双端队列,是列表的替代实现。Python中的列表底层是基于数组来实现的,而deque底层是双向链表,因此当你需要在头尾添加和删除元素时,deque会表现出更好的性能,渐近时间复杂度为$O(1)$。
- `Counter`:`dict`的子类,键是元素,值是元素的计数,它的`most_common()`方法可以帮助我们获取出现频率最高的元素。`Counter`和`dict`的继承关系我认为是值得商榷的,按照CARP原则,`Counter`跟`dict`的关系应该设计为关联关系更为合理。
- `OrderedDict`:`dict`的子类,它记录了键值对插入的顺序,看起来既有字典的行为,也有链表的行为。
- `defaultdict`:类似于字典类型,但是可以通过默认的工厂函数来获得键对应的默认值,相比字典中的`setdefault()`方法,这种做法更加高效。
```Python
"""
找出序列中出现次数最多的元素
"""
from collections import Counter
words = [
'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around',
'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes',
'look', 'into', 'my', 'eyes', "you're", 'under'
]
counter = Counter(words)
print(counter.most_common(3))
```
### 数据结构和算法
- 算法:解决问题的方法和步骤
- 评价算法的好坏:渐近时间复杂度和渐近空间复杂度。
- 渐近时间复杂度的大O标记:
-
#### 结构
- html
- head
- title
- meta
- body
#### 文本
- 标题(heading)和段落(paragraph)
- h1 ~ h6
- p
- 上标(superscript)和下标(subscript)
- sup
- sub
- 空白(白色空间折叠)
- 折行(break)和水平标尺(horizontal ruler)
- br
- hr
- 语义化标签
- 加粗和强调 - strong
- 引用 - blockquote
- 缩写词和首字母缩写词 - abbr / acronym
- 引文 - cite
- 所有者联系信息 - address
- 内容的修改 - ins / del
#### 列表(list)
- 有序列表(ordered list)- ol / li
- 无序列表(unordered list)- ul / li
- 定义列表(definition list)- dl / dt / dd
#### 链接(anchor)
- 页面链接
- 锚链接
- 功能链接
#### 图像(image)
- 图像存储位置

- 图像及其宽高
- 选择正确的图像格式
- JPEG
- GIF
- PNG
- 矢量图
- 语义化标签 - figure / figcaption
#### 表格(table)
- 基本的表格结构 - table / tr / td / th
- 表格的标题 - caption
- 跨行和跨列 - rowspan属性 / colspan属性
- 长表格 - thead / tbody / tfoot
#### 表单(form)
- 重要属性 - action / method / enctype
- 表单控件(input)- type属性
- 文本框 - `text` / 密码框 - `password` / 数字框 - `number`
- 邮箱 - `email` / 电话 - `tel` / 日期 - `date` / 滑条 - `range` / URL - `url` / 搜索 - `search`
- 单选按钮 - `radio` / 复选按钮 - `checkbox`
- 文件上传 - `file` / 隐藏域 - `hidden`
- 提交按钮 - `submit` / 图像按钮 - `image` / 重置按钮 - `reset`
- 下拉列表 - select / option
- 文本域(多行文本)- textarea
- 组合表单元素 - fieldset / legend
#### 音视频(audio / video)
- 视频格式和播放器
- 视频托管服务
- 添加视频的准备工作
- video标签和属性 - autoplay / controls / loop / muted / preload / src
- audio标签和属性 - autoplay / controls / loop / muted / preload / src / width / height / poster
#### 窗口(frame)
- 框架集(过时,不建议使用) - frameset / frame
- 内嵌窗口 - iframe
#### 其他
- 文档类型
```HTML
```
```HTML
```
```HTML
```
- 注释
```HTML
```
- 属性
- id:唯一标识
- class:元素所属的类,用于区分不同的元素
- title:元素的额外信息(鼠标悬浮时会显示工具提示文本)
- tabindex:Tab键切换顺序
- contenteditable:元素是否可编辑
- draggable:元素是否可拖拽
- 块级元素 / 行级元素
- 字符实体(实体替换符)

### 使用CSS渲染页面
#### 简介
- CSS的作用
- CSS的工作原理
- 规则、属性和值

- 常用选择器

#### 颜色(color)
- 如何指定颜色
- 颜色术语和颜色对比
- 背景色
#### 文本(text / font)
- 文本的大小和字型(font-size / font-family)


- 粗细、样式、拉伸和装饰(font-weight / font-style / font-stretch / text-decoration)

- 行间距(line-height)、字母间距(letter-spacing)和单词间距(word-spacing)
- 对齐(text-align)方式和缩进(text-ident)
- 链接样式(:link / :visited / :active / :hover)
- CSS3新属性
- 阴影效果 - text-shadow
- 首字母和首行文本(:first-letter / :first-line)
- 响应用户
#### 盒子(box model)
- 盒子大小的控制(width / height)

- 盒子的边框、外边距和内边距(border / margin / padding)

- 盒子的显示和隐藏(display / visibility)
- CSS3新属性
- 边框图像(border-image)
- 投影(border-shadow)
- 圆角(border-radius)
#### 列表、表格和表单
- 列表的项目符号(list-style)
- 表格的边框和背景(border-collapse)
- 表单控件的外观
- 表单控件的对齐
- 浏览器的开发者工具
#### 图像
- 控制图像的大小(display: inline-block)
- 对齐图像
- 背景图像(background / background-image / background-repeat / background-position)
#### 布局
- 控制元素的位置(position / z-index)
- 普通流
- 相对定位
- 绝对定位
- 固定定位
- 浮动元素(float / clear)
- 网站布局
- HTML5布局

- 适配屏幕尺寸
- 固定宽度布局
- 流体布局
- 布局网格
### 使用JavaScript控制行为
#### JavaScript基本语法
- 语句和注释
- 变量和数据类型
- 声明和赋值
- 简单数据类型和复杂数据类型
- 变量的命名规则
- 表达式和运算符
- 赋值运算符
- 算术运算符
- 比较运算符
- 逻辑运算符:`&&`、`||`、`!`
- 分支结构
- `if...else...`
- `switch...cas...default...`
- 循环结构
- `for`循环
- `while`循环
- `do...while`循环
- 数组
- 创建数组
- 操作数组中的元素
- 函数
- 声明函数
- 调用函数
- 参数和返回值
- 匿名函数
- 立即调用函数
#### 面向对象
- 对象的概念
- 创建对象的字面量语法
- 访问成员运算符
- 创建对象的构造函数语法
- `this`关键字
- 添加和删除属性
- `delete`关键字
- 标准对象
- `Number` / `String` / `Boolean` / `Symbol` / `Array` / `Function`
- `Date` / `Error` / `Math` / `RegExp` / `Object` / `Map` / `Set`
- `JSON` / `Promise` / `Generator` / `Reflect` / `Proxy`
#### BOM
- `window`对象的属性和方法
- `history`对象
- `forward()` / `back()` / `go()`
- `location`对象
- `navigator`对象
- `screen`对象
#### DOM
- DOM树
- 访问元素
- `getElementById()` / `querySelector()`
- `getElementsByClassName()` / `getElementsByTagName()` / `querySelectorAll()`
- `parentNode` / `previousSibling` / `nextSibling` / `children` / `firstChild` / `lastChild`
- 操作元素
- `nodeValue`
- `innerHTML` / `textContent` / `createElement()` / `createTextNode()` / `appendChild()` / `insertBefore()` / `removeChild()`
- `className` / `id` / `hasAttribute()` / `getAttribute()` / `setAttribute()` / `removeAttribute()`
- 事件处理
- 事件类型
- UI事件:`load` / `unload` / `error` / `resize` / `scroll`
- 键盘事件:`keydown` / `keyup` / `keypress`
- 鼠标事件:`click` / `dbclick` / `mousedown` / `mouseup` / `mousemove` / `mouseover` / `mouseout`
- 焦点事件:`focus` / `blur`
- 表单事件:`input` / `change` / `submit` / `reset` / `cut` / `copy` / `paste` / `select`
- 事件绑定
- HTML事件处理程序(不推荐使用,因为要做到标签与代码分离)
- 传统的DOM事件处理程序(只能附加一个回调函数)
- 事件监听器(旧的浏览器中不被支持)
- 事件流:事件捕获 / 事件冒泡
- 事件对象(低版本IE中的window.event)
- `target`(有些浏览器使用srcElement)
- `type`
- `cancelable`
- `preventDefault()`
- `stopPropagation()`(低版本IE中的cancelBubble)
- 鼠标事件 - 事件发生的位置
- 屏幕位置:`screenX`和`screenY`
- 页面位置:`pageX`和`pageY`
- 客户端位置:`clientX`和`clientY`
- 键盘事件 - 哪个键被按下了
- `keyCode`属性(有些浏览器使用`which`)
- `String.fromCharCode(event.keyCode)`
- HTML5事件
- `DOMContentLoaded`
- `hashchange`
- `beforeunload`
#### JavaScript API
- 客户端存储 - `localStorage`和`sessionStorage`
```JavaScript
localStorage.colorSetting = '#a4509b';
localStorage['colorSetting'] = '#a4509b';
localStorage.setItem('colorSetting', '#a4509b');
```
- 获取位置信息 - `geolocation`
```JavaScript
navigator.geolocation.getCurrentPosition(function(pos) {
console.log(pos.coords.latitude)
console.log(pos.coords.longitude)
})
```
- 从服务器获取数据 - Fetch API
- 绘制图形 - `