笔记

您所在的位置:网站首页 python系列教程 笔记

笔记

#笔记 | 来源: 网络整理| 查看: 265

本文章主要以黑马程序员的「传智播客 Python 就业班 (ij6g)」、「 Python 从入门到精通教程 」和「 廖雪峰的 Python 教程 」为主线,输出学习笔记,目的是检验自己的学习效果和日常复习之需。作为入门 Python 的参考资料,除了视频的基础内容外,文章还会补充视频中讲解不详细或遗漏的必要知识点。

文章的内容和知识框架,与「廖雪峰的 Python 教程」和「传智播客的视频」大体保持一致:

文章以模块分块阐述:Linux 基础、Python 基础、Python 面向对象、项目实战 ( 实战部分以爬虫、数据分析为主的项目实战 )。

每个模块按知识点区分: 1) Linux 基础部分参考 传智播客 Python 从入门到精通教程; 2) Python 基础部分参考 廖雪峰 Python 教程; 3) 项目实践,即数据分析部分参考书籍 利用 Python 进行数据分析 $^{[5]}$;

最后,正如 Bruce Eckel 所述 Life is short, you need python,Python 的高效只有切身体验才会深有体会,期待您早日加入 Python 队伍中来。

更新进度 2018.09.03:完成初稿,且完成 Linux 基础部分的内容; 2018.09.18:更新 Python 基础部分内容「语言基础、函数、高级特性」; 2018.09.21:更新 Python 基础部分内容「函数式编程」; 2018.10.10:更新 Python 基础部分内容「模块、面向对象编程」; 2018.10.12:更新 Python 基础部分内容「面向对象高级编程」; 2018.10.13:更新 Python 基础部分内容「错误/调试/测试」; 2018.10.14:更新 Python 基础部分内容「面向 I/O 编程」; 2018.11.05:更新 Python 基础部分内容「装饰器」; 参考书目 Python 基础 📖 | 埃里克·马瑟斯.《 Python 编程:从入门到实践 》:豆瓣评分 📖 | Albert Sweigart.《 Python 编程快速上手 》:豆瓣评分 Python 进阶 📖 | David M. Beazley / Brian K. Jones.《 Python Cookbook 》:中文版 | 英文版 📖 | Luciano Ramalho. 《 Fluent Python 》:中文版 | 英文版 Python 实践 📖 | Wes Mckinney.《 利用 Python 进行数据分析 》:豆瓣评分 📖 | Clinton W. Brownley.《 Python数据分析基础 》:豆瓣评分 教学资源 📖 | 文章 | 廖雪峰. Python 3 教程. liaoxuefeng.com 📺 | 视频 | 黑马程序员. Python 从入门到精通教程. 2017. bilibili.com

📺 | 视频 | 传智播客. Python 就业班. 2017. BaiduCloud | Pwd: ij6g

本文章的学习笔记是基于此系列教学视频所得的。

Linux 基础Linux 常用终端命令

仅列举一些项目中常用的命令。

LS 命令与通配符

*:代表任意个数个字符。 ?:代表任意一个字符。 []:表示可匹配字符组中任意一个。 [abc]:匹配 a、b、c 中的任意一个字符。

[a-f]:匹配从 a 到 f 范围内的任意一个字符。

常使用 ls -al 显示当前文件目录所有文件的详细信息。

CD 命令与切换目录

相对路径:最前面不是 / 或 ~,表示相对 当前目录 所在的目录位置。

绝对路径:最前面是 / 或 ~,表示从 根目录 / Home 目录 开始的具体目录位置。

12345# 相对路径:返回上两级目录cd ../../ # 绝对路径:相当于 cd /Users/your username/cd ~

Tree 命令:以树状结构显示文件目录结构,若 tree -d 则显示目录,不显示文件。

查看文件内容

cat 文件名:查看文件内容、创建文件、文件合并、追加文件内容等功能。 more 文件名:分屏显示文件内容。 grep 搜索文本的文件名:搜索文件文件内容。 例如搜索包含单词 “hello” 的文本,即 grep "hello" sample.txt。 选项参数:-n 显示匹配行号;-v 显示不包含匹配文本的所有行;-i 忽略大小写。

Echo 命令与重定向

echo 命令:在终端中显示参数指定的文字。 重定向 > 和 >>: > 表示输出,会覆盖文件原有内容。 >> 表示追加,会将内容追加到已有文件的末尾。

echo 命令常结合 重定向 使用:

12# 将字符串 "Hello World" 追加到echo "Hello World" >> sample.txt

管道符 |

Linux 允许将一个命令的输出通过管道作为另一个命令的输入。

ls 命令与 grep 命令的结合使用,如从 Home 目录下搜索包含 “python” 关键字的文件或者文件夹:

12# 从 Home 目录下搜索包含 "python" 关键字的文件或者文件夹ls -al ~ | grep python

Ifconfig 命令与 Ping 命令

ifconfig 命令可查看/配置计算机当前的网卡配置。

ping 命令一般用于检测当前计算机到目标计算机之间的网络是否畅通。

12# 快速查看网卡对应的 IP 地址ifconfig | grep inet 远程登录和复制文件远程登录 远程登录即通过 SSH 客户端 连接运行了 SSH 服务器 的远程机器上。 SSH 是目前较可靠,专为 远程登录会话 和 其他网络服务 提供安全性协议。 有效防止远程管理过程中的信息泄露。 对所有传输的数据进行加密,也能防止 DNS 欺骗和 IP 欺骗。 SSH 客户端是一种使用 Secure Shell 协议连接到远程计算机的软件程序。 SSH 客户端简单使用访问服务器:ssh [-p port] user@remote user 是远程机器上的用户名。 remote 是远程机器地址,可为 IP、域名或别名。 port 是 SSH 服务器监听的端口,若不指定端口默认为 22。 复制文件

SCP 即 Secure Copy,是一个在 Linux 下用来进行 远程拷贝文件 的命令。

12345# 从本地复制文件到远程机器桌面上scp -P sample.py user@remote:Desktop/sample.py# 从远程机器桌面上复制文件夹到本地上scp -P port -r user@remote:Desktop/sample ~/Desktop/sample SSH 高级用法免密码登录

免密码登录:即客户端访问服务端时,需要密码验证身份登录。

Step.01. 配置公钥:执行 ssh-keygen 即生成 SSH 密钥。

Step.02. 上传公钥到服务器:执行 ssh-copy-id -p port user@remote,让远程服务器记住我们的 公钥。

1) 有关 SSH 配置信息都保存在 /Home/your username/.ssh 目录下。2) 免密登录使用的是非对称加密算法 ( RSA ),即使用公钥加密的数据,需要使用私钥解密;使用私钥加密的数据,需要使用公钥解密。若有兴趣了解 RSA 算法 的原理及计算,可参考引用文章 [1]、[2]。

图5-2-1免密码登录实现原理图

图 5-2-1 免密码登录实现原理图 配置别名

配置别名:每次输入 ssh -p port user@remote 是非常繁琐重复的工作,配置别名的方式以替代上述这么一串命令代码。

在 /.ssh/config 文件下追加以下内容 ( 需建立 Config 文件 ):

1234Host macHostName 192.168.10.1User userPort 22

命令输入 ssh mac 即可实现远程登录操作 ( SCP 同样原理 )。

123# 若配置别名后,待验证命令的格式:# 是否为: scp -r ~/Desktop/Sample mac:Desktop/Sample# 还是: scp -P 22 -r ~/Desktop/Sample mac:Desktop/Sample 用户和权限基本概念 在 Linux 中,可指定每一用户针对不同的文件或者目录的不同权限。 对文件 / 目录包含的权限有: 表 5-3-1 文件/目录权限属性说明 权限 英文 缩写 数字代号 读 read r 4 写 write w 2 执行 excute x 1 组 为方便用户管理,提出组的概念。在实际开发中,可预先针对组设置好权限,然后将不同的用户添加到对应组中,从而不用依次为每个用户设置权限。 权限名称 权限参数 说明 user u 文件的拥有者,一般指当前用户 group g 文件所属的群组 other o 其他用户 LL 命令 LL 命令即 LS 命令的扩展用法 ls -al。 LL 命令可查看文件夹下文件的详细信息,从左往右依次是: 权限:第一个字符是 d,表示目录;- 表示文件; 硬链接数:通俗理解即有多少种方式可访问到当前目录 / 文件; 拥有者:当前用户; 组:当前用户所属的组; 文件大小,修改时间,文件 / 目录名称. 表 5-3-2 "ls -al" 查看文件的权限信息说明 目录 拥有者权限 组权限 其他用户权限 备注 - r w - r w - r - - 文件权限示例 d r w x r w x r - x 目录权限示例 Chmod 命令

Chmod 命令:可修改 用户/组 对 文件/目录 的权限。

1234567# 一次性修改拥有者/组的权限chmod +/-rwx 文件名/目录名# 一次性定制给某拥有者/组赋予某文件或者目录权限# 给予当前用户及所属群组对于该文件可读、可写、可执行权限chmod ug=rwx 文件名# 给予当前用户及所属群组对于该目录下所有文件可读、可写、可执行权限chmod ug=rwx -R 目录名 Sudo 命令

Sudo 命令:使用预设 ( root, 系统管理员 ) 的身份来执行命令。

Linux 系统中,通常使用标准用户登录及使用系统,通常 sudo 命令临时获得权限用于系统的维护与和管理。在执行一些模块的安装过程或者配置过程中,你会经常用到它的。

系统信息相关命令 查询时间和日期 date:查看系统时间。 cal:查看当月日历,cal -y 查看当年的日历。 磁盘和目录空间 df:df -h,Disk Free 显示磁盘剩余空间。 du:du -h,Disk Usage 显示目录下的文件大小。

进程信息

ps:ps aux,即 Process Status,查看进程的详细状况。 top:动态显示运行中的进程并排序。

kill:kill [-9] 进程代号,-9 表示强行终止,终止指定代号的进程。

使用 kill 命令时,最好终止当前用户开启的进程,而不是终止 root 身份开启的进程。

其他终端命令查找文件

查找文件:find 命令功能非常强大,通常在特定目录下搜索符合条件的文件。

若省略路径,表示在当前文件夹下查找。

find 命令可结合 通配符 一起使用。

1find [路径] -name "*.py" 软链接

软链接:建立文件的软链接,通俗理解即 PC/MacOS 上的 快捷方式。

源文件要使用绝对路径,即便于移动链接文件 (快捷方式) 仍能正常使用。

没有 -s 选项是建立一个硬链接文件。

1ln -s 被链接的源文件 快捷方式的名称

在 Linux 中,文件名和文件的数据是分开储存的。

图5-5-1软、硬链接访问文件数据

图 5-5-1 软、硬链接访问文件数据 打包压缩

tar 是 Linux 中最常用的备份工具 ( 打包并不压缩 ),其命令格式如下:

12345678910# 选项 c:生成档案文件 (.tar)# 选项 x:解开档案文件# 选项 v:列出归档/解档的详细过程,显示进程# 选项 f:指定档案文件名称,选项 f 后应该紧跟 .tar 文件# 打包文件:打包放于同一目录下tar -cvf 打包文件.tar. 被打包文件路径# 解包文件tar - xvf 打包文件 [-C 目标路径]

tar 与 gzip 命令结合可实现文件 打包和压缩,即 tar 只负责打包文件, gzip 负责压缩文件。

12345678# 压缩文件:压缩文件放于同一目录下tar - zcvf 打包文件.tar.gz 被压缩文件路径# 解压缩文件tar -zxvf 打包文件.tar.gz# 解压缩文件到指定路径tar -zxvf 打包文件.tar.gz [-C 目标路径] Python 基础引入Python 优缺点 Python 是面向对象 / 过程的语言 ( 对象和过程语言各有自己的优缺点 ): 面向对象:由 数据 和 功能组合而成的对象 构建而成的程序。 面向过程:由 过程 或仅仅是 可重用代码 构建起来的程序。 Python 应用场景 Web 端程序: mod_wsgi 模块:Apache 可运行用 Python 编写 Web 程序。 常见 Web 框架:Django、TurboGears、Web2py、Zope 等。 操作系统管理:服务器运维的自动化脚本。 科学计算:NumPy、SciPy、Matplotlib 等。 桌面端程序:PyQt、PySide、wxPython、PyGTK 等。 服务端程序:Twisted ( 支持异步网络编程和多数标准的网络协议,包括客户端和服务端 )。 Python 解释器

当我们编写 Python 代码时,我们得到的是一个包含 Python 代码的以 .py 为扩展名的文本文件。要运行代码,就需要 Python 解释器去执行 xxx.py 文件。

CPython

当我们从 Python 官方网站下载 并安装好 Python 3.x 后,我们就直接获得了一个官方版本的解释器:CPython ( C 语言开发的 )。 在命令行下运行 python 就是启动 CPython 解释器。

iPython

iPython 是基于 CPython 之上的一个交互式解释器,即 iPython 只是在交互方式上有所增强,但是执行 Python 代码的功能和 CPython 是完全一样的。 在命令行下运行 ipython 即可启动 iPython 交互式解释器。

CPython 用 >>> 作为提示符,而 IPython 用 In [序号]: 作为提示符。

图6-1-1Python与iPython提示符表现形式

图 6-1-1 Python 与 iPython 提示符表现形式

PyCharm

工欲善其事,必先利其器。为帮助开发者更便捷、更高效来开发 Python 程序,一款集成开发编辑器 ( IDE ) 显得格外重要。IDE 除了快捷键、插件外,重要的是它还支持 调试程序。

当然,支持 Python 程序开发的 IDE 还有很多优秀的产品:如:Eclipse with PyDev

第一个程序

新建并运行 python 程序:vi python_sample.py 开始编写程序;通过 python python_sample.py 执行程序。以下为简单的 Python 示例:

12345678910111213141516# 声明部分# 取机器 Path 中指定的第一个 python 来执行脚本#!/usr/bin/env python# python.py 文件中包含中文字符,Python2 在文件头加入以下语句 ( Python3 是默认支持的 ):# -*- coding=utf-8 -*- # 代码部分print("Life is short, you need python.")a = 100A = 200if a >= 100: # 冒号 ":" 结尾,缩进的语句即为代码块 print(a)else: print(-A) # Python 是大小写敏感的 语言基础注释

行注释、块注释:行注释的风格与 Linux 中 Shell 脚本的注释相同,即以 # 开头的注释;块注释使用三个单引号 ' 或三个双引号 " 包裹实现。

123456789101112131415# 行注释# line 1...# line 2...'''' 单引号块注释' line 1' line 2'''"""" 双引号块注释" line 1" line 2""" 数据类型 整型:可处理 任意大小 的整数,当然包括 负整数。例如 0,1,100,-8080 等。

浮点型:即含有小数点的数,如 1.23,1.23e9 ( 1.23x10$^9$ ),1.23e-5 ( 1.23x10$^{-5}$ )

1) 整数和浮点数在计算机内部存储的方式是不同的;2) 整数运算永远是精确的,而浮点数运算则可能会有四舍五入的误差。

字符型:以单引号 ' 或双引号 " ( 表示方式不同而已 ) 括起来的任意文本。例如 '(1+2)\%3 == 0',或者 "The 'a' is a lowercase letter of 'A'"。

布尔型:True / Flase 两种值。 布尔运算:and、or、not,例如 (3 > 2) and (1 > 2),输出 Flase。 空值:None,注意 None 不能理解为 0,因为 0 是有意义的,而 None 是一个特殊的空值。

Python 中的数据类型是没有大小限制的,若想定义无限大,可定义为无限大,即 inf。

常量变量常量 常量:例如定义 PI = 3.14159,其实际也是变量,只是约定俗成罢了。 变量 形如 param = value 的形式赋予变量值,但不用赋变量数据类型。

变量的输入与输出:

123high = int( input(Please enter your high:) )# input() 默认输出 String 类型print("Your high is: %d" % high); 字符编码 一个字节,表示的最大的整数就是 255,即十进制为 255,二进制为 11111111。若想表示更大的整数则需要更多的字节。

ASCII:127 个字符编码,即大小写字母、数字和一些特殊字符。例如大些字母 A,对应的 ASCII 为 65。

但处理中文显然一个字节是不够的 ( 至少两个字节 ),且还不能与 ASCII 编码冲突,所以中国制定了GB2312 编码。

然而,世界有上百种语言,日本把日文编到 Shift_JIS 里,韩国把韩文编到 Euc-kr 里,各国有各国的标准,就会不可避免地出现冲突,结果就是,在多语言混合的文本中,显示出来会有乱码。

因此,Unicode 应运而生 $^{[3]}$。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。

Unicode:2 字节及以上。

为节约空间,把 Unicode 编码转化为“可变长编码”的 UTF-8 编码。

UTF-8:根据数字大小编写 1 ~ 6 字节,英文字母 1 字节,汉字 3 字节 ( 生僻字符用到 4 ~ 6 字节 )。

ACSII、Unicode 与 UTF-8 的关系

表 6-2-1 ACSII、Unicode 与 UTF-8 的关系 字符 ASCII Unicode UTF-8 A 0100 0001 00000000 01000001 01000001 中 — 01001110 00101101 11100100 10111000 1010 1101

启示:计算机系统通用的字符编码工作方式,如图 6-2-1 所示。

用记事本编辑时,从文件读取的 UTF-8 字符被转换为 Unicode 字符到内存里,当保存的时再把 Unicode 转换为 UTF-8 保存到文件;

浏览网页时,服务器会把动态生成的 Unicode 内容转换为 UTF-8 再传输到浏览器。

图6-2-1计算机系统通用的字符编码工作方式

图 6-2-1 计算机系统通用的字符编码工作方式 字符串/列表/元组/字典字符串 Str

Python 3 中,字符串是以 Unicode 编码的。

Python 的字符串类型为 String,内存中以 Unicode 表示。若在网络中传输,则可以把 string 类型的数据变成以字节为单位的 bytes。

encode() 与 decode():

英文字符可用 ASCII 编码 Bytes,即 "ABC".encode("ascii")。

中文字符可用 UTF-8 编码,即 "中国".encode("utf-8")。

含有中文的 str 无法用 ASCII 编码,因中文编码的范围超过了 ASCII 编码的范围。强制编码会抛出异常:'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)。

常用数据类型转换,见表 6-2-2 所示:

表 6-2-2 常用数据类型转换说明表 函数格式 使用示例 描述 int(x [,base]) int(“8”) 或 int(‘A’, base = 16) 可转换的包括 String 类型和其他数字类型,但高精度转换会丢失精度 float(x) float(1) 或 float(“1”) 可转换 String 和其他数字类型,不足的位数用 0 补齐,例如 1 会变成 1.0 comple(real,imag) complex(“1”) 或 complex(1,2) 第一个参数可以是 String 或者数字,第二个参数只能为数字类型,第二个参数没有时默认为 0 str(x) str(1) 将数字转化为 String repr(x) repr(Object) 返回一个对象的 String 格式 eval(str) eval(“12+23”) 执行一个字符串表达式,返回计算的结果,如例子中返回 35 tuple(seq) tuple((1,2,3,4)) 参数可以是元组、列表或字典。若为字典时,返回字典的 key 组成的集合 list(s) list((1,2,3,4)) 将序列转变成一个列表,参数可为元组、字典、列表。若为字典时,返回字典的 key 组成的集合 set(s) set([‘b’, ‘r’, ‘u’, ‘o’, ‘n’])或者set(“asdfg”) 将一个可迭代对象转变为可变集合且去重复,返回结果可以用来计算差集 x - y、并集 x l y、交集 x & y frozenset(s) frozenset([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) 将一个可迭代对象转变成不可变集合,参数为元组、字典、列表等 chr(x) chr(0x30) chr() 用一个范围在 range (0~255) 内的整数作参数,返回一个对应的字符。返回值是当前整数对应的 ASCII 字符。 ord(x) ord(‘a’) 返回对应的 ASCII 数值,或者 Unicode 数值 hex(x) hex(12) 把整数 x 转换为 16 进制字符串 oct(x) oct(12) 把整数 x 转换为 8 进制字符串

字符串输入和输出:

123name = input("Enter your name:")age = int( input("Enter your age:") )print("name: %s, age: %d" % (name, age))

组成字符串的方式:

12345678str1 = "Hello"str2 = "World"# str3 组装成 "HelloWorld"str3 = str1 + str2# 组装成 "===HelloWorld===",此方式常用拼凑字符串"===%s===" % (str1 + str2)

字符串下标与取值:

12345678910111213141516array = "ABCDE"print( array[0] ) # 输出 Aprint( array[4] ) # 输出 Eprint( array[-1] ) # 输出 E# 切片print( array[0:3] ) # 输出 ABCprint( array[0:-1] ) # 输出 ABCDprint( array[0:] ) # 输出 ABCDE# 即以 2 为步进距离,从下标 0 开始取值至末尾,输出 ACEprint( array[0::2] )# 即以 -1 为步进距离,从末尾开始取值至开端,逆序输出print( array[-1::-1] )

字符串常见操作

find(s) 与 index(s):从目标字符串中寻找子串,找到会返回子串的起始下标;若找不到则返回 -1。index() 找到目标的情况和 find() 相同,找不到目标则会抛出异常。

当然还有 rfind(s) 和 rindex(),即从右端开始寻找子字符串。

count(str, start, end):即在目标字符串 myStr,求得 str 在位置 start 和 end 之间出现的次数。

例如:myStr.count(str, start = 0, end = len(myStr)),

replace(原始字符串, 目标字符串) 或 replace(原始字符串, 目标字符串,替代次数):

例如:myStr.replace("world", "python")

split(str):根据 str 把原字符串切开。

splitlines(str):将字符串中的每一行切割开来。

re.split(正则表达式, 目标字符串),根据正则表达式切割字符。

capitalize() 与 title():前者是把字符串中的第一个字符转为大写字母,后者是把字符串中每个单词的首字母转为大写。

startsWith(str) 与 endsWith(str):前者是判断目标字符是否以字符串 str 开头,后者则是判断目标字符是否以字符串 str 结尾。 lower() 与 upper():前者是将目标字符串全转为小写字母,后者是将字符串全转为大写字母。 rstrip()、lstrip() 与 strip():去除字符串左边、右边或者两端的空白字符。 partition(str):以 str 为中心,将目标字符串划分成左、中 ( str 本身 )、右三部分的字符串。 isalpha()、isdigit() 与 isalnum():分别用于判断是否为字符,是否为数字和是否全为数字。

join():例如 str.join(array),即使用 str 将列表 array 的内容拼接起来。

1234array = ['A', 'B', 'C']str1 = '&'# str2 被组装成 A&B&C,即将 str1 组装到字符数组中str2 = str1.join(array) 列表 List 定义一个列表:list = ['A', 'B', 'C', 'D'] 或者 student = ['lucy', 25, 'female']。 列表的增删改查 : 增加:1) 在列表尾部追加元素:list.append('D')2) 自定义插入位置:list.insert(位置,添加的内容)3) 往一列表中添加另一个列表:student + list 或者 student.extend(list) 删除:1) 出栈:list.pop() / 入栈:list.append()2) 根据下标来删除:del list[0],清空列表 del list[0::1] 查询:1) ('B' in list) 结果为 Ture2) ('D' not in list) 结果为 Ture 元组 Tuple

有序列表元组 ( Tuple ),与 List 不同,Tuple 一旦初始化就不能修改。

定义一些常量参数时可用 Tuple。

定义:tuples = ('A', 'B', 'C')。

歧义:tuple = (1) 相当于 tuple = 1;tuple(-1, ) 才是元组列表。

事实: Tuple 中存储的是 引用。

123456tuple = ('a', 'b', ['A', 'B'])tuple[2][0] = 'X'tuple[2][1] = 'Y'# 事实上,'A' 和 'B' 被改变为 'X' 和 'Y'# 即 Tuple 定义是不变的,只是 Tuple 上存储的 List 为引用

再议不可变对象:replace() 并没有改变字符串的内容,我们理解 str 是变量,abc 是字符串对象。replace() 相当于创建了新的字符串对象 Abc。

123str = 'abc'print( str.replace('a', 'A') ) # 输出 Abcprint(str) # 输出 abc 字典 Dict 字典 ( Dict ),其他语言中又称 Map,使用键值 ( key-value ) 存储。 定义:dict = {'name': 'Lucy', 'age':25, 'gender': 'female}。 字典的增删改查: 增加:dict['high'] = 175,若对应键值存在即修改的效果。 删除:dict.pop('high') / del dict['high'] 查询:dict.get('name'),若找不到对应键值则抛出异常。 集合 Set Set 与 Dict 类似,是一组 key 的集合,但不存储 value。

Set 可看成数学意义上的 无序 和 无重复 元素的集合。

1print( set([1, 1, 2, 3, 4, 4, 5]) ) # 输出 [1, 2, 3, 4, 5] 条件判断

标准条件判断语句:

1234567if : elif : else: if : # if 嵌套

三元表达式:在 Python 中,可将 if-else 语句放到一行里,语法如下:

12345678# true-expr 或 false-expr 可以是任何 Python 代码value = true-expr if condition else false-expr# 上述三元表达式等同于标准条件判断语句的写法if condition: value = true-exprelse: value = false-expr 循环结构

For 循环与 While 循环

1234567891011# For 循环names = ['LiMing', 'ZhangWei']for name in names print(name) # While 循环sum = 0i = 0while( i 0): n = n - 1 s = s * x return s print( power(5) ) # 输出 25print( power(5, 3) ) # 计算 5 的 3 次方,输出 125

可变参数:顾名思义,可变参数就是传入的参数个数是可变的。

123456789# def calculator(numbers),即理解 numbers 为一个 tupledef calculator(*numbers): sum = 0 for n in numbers: sum = sum + n ** 2 return sum# 等价于 calculator( (1, 3, 5, 7) )print( calculator(1, 3, 5, 7) ) # 输出 84

关键字参数: 可变参数 允许你传入 0 个或任意个参数,这些参数在函数调用时自动组装为一个 元组 ( Tuple )。 关键字参数 允许你传入 0 个或任意个参数,这些关键字参数在函数内部自动组装成为一个 词典 ( Dict )。

123456789101112def person(name, age, **kw): print(' name:', name, ' age:', age, ' others:', kw)person('Lucy', 35, city = 'Guangzhou', gender = 'M')# 输出 name: Lucy age: 35 others: {'city': 'Guangzhou', 'gender': 'M'}# 当然,我们可先组装词典 dict,然后把该 dict 转换为关键字参数传进去extra = {'city': 'Guangzhou', 'gender': 'M'} # 将字典中的元素,拆分成独立的 Key-Value 键值,引用时前缀也要加 "**"person('Jack Ma', 50, **extra)# 输出 name: Jack Ma age: 50 others: {'city': 'Guangzhou', 'gender': 'M'}

参数组合:Python 中定义函数,可多种参数组合使用,但必须满足一下参数定义顺序:必选参数、默认参数、可变参数、命名关键字 和 关键字参数。

12345def func(a, b, c = 0, *args, **kw): print(' a=', a, ' b=', b, ' c=', c, ' args=', args, ' kw=', kw)# 输出 a=1 b=2 c=3 args=('a', 'b') kw={'x'=99}func(1, 2, 3, 'a', 'b', 'x'=99)

结合 tuple 和 dict:即通过类似 func(*args, **kw) 形式调用函数。参数虽可自由组合使用,但不要组合太复杂,以造成可理解性较差的结果。

123args = (1, 2, 3)kw = {'x' = 5, 'y' = 6}func(*args, **kw) 递归函数

函数内部可以调用其他函数。若一个函数内部调用了其自身,即该函数为 递归函数。

1234def fact(n): if n == 1: return 1 return n * fact(n - 1) 递归的过深调用会导致栈溢出。可通过 尾递归 优化。

尾递归优化:解决递归调用栈溢出的方法,即函数返回时调用本身,并且 return 语句不能包含表达式。

区别上述的 fact(n) 函数,由于 return n * fact(n - 1) 引入了乘法表达式,即非尾递归。 而 return fact_iter(num - 1, num * product) 仅仅返回函数本身。

这样,编译器 / 解释器就可对尾递归做优化,即使递归本身调用 n 次,都只占用一个栈帧,不会出现栈溢出的情况。

1234567def fact(): return fact_iter(n, 1) def fact_iter(num, product): if num == 1: return product return fact_iter(num -1, num * product) 高级特性切片

切片操作符:在 List 中指定 索引范围 的操作。 索引范围具体为: 起始位置:结束位置:步进 ,注意步进数 ( 默认为 1,不能为 0 )。

1234list = [11, 22, 33, 44, 55]# 输出 [11, 22, 33],即从小标为 0 开始,步进为 1,取前 3 个元素print( list[0:3:1] )

倒数切片:

1234list = ['A', 'B', 'C', 'D', 'E']# 输出 ['A', 'B', 'C', 'D'],即从下标为 0 开始,切片至倒数第一个元素 (不含其本身)print( list[0:-1] )

字符串切片:

1234str = 'ABCDE'# 输出 ACE,即对字符串中所有字符作用,每隔两位取值print( str[::2] ) 注意:Tuple 也是一种 List,唯一不同的是 Tuple 不可变,因此 Tuple 不可用切片操作。 迭代

迭代:给定一个 List 或 Tuple,通过 For 循环遍历这个 List 或 Tuple。

1234list = ['A', 'B', 'C', 'D', 'E']for str in list: print(str) # 输出 ABCDE

enumerate 函数可以把一个 list 变成 索引-元素树,这样就可以在 For 循环中同时迭代 索引 和 元素本身。

1234list = ['A', 'B', 'C', 'D', 'E']for i, value in enumerate(list): print(i, value) 列表生成式

列表生成式:List Comprehensions,用于创建 List 的生成式。

1234567891011121314151617181920212223242526272829list1 = []list1 = [x**2 for num in range(1, 10)]# 输出 1x1,2x2,3x3, ..., 9x9print(list1)'''等价于:for num in range(1, 10): list1.append(num ** 2)''' # for 循环与 if 判断配合,例如取得 10 以内的偶数,求其平方数list2 = [ num**2 for num in range(1, 10) if num%2 == 0 ]# 输出 2x2, 4x4, 6x6, 8x8print(list2)# 两层 for 循环list3 = [ m+str(n) for m in 'ABC' for n in range(1,4) ]# 输出 ['A1', 'A2', 'A3', 'B1', 'B2', 'B3', 'C1', 'C2', 'C3']print(list3)list4 = [ m*n for m in 'ABC' for n in range(1,4) ]# 输出 ['A', 'AA', 'AAA', 'B', 'BB', 'BBB', 'C', 'CC', 'CCC']print(list4)# 列出当前目录下所有文件和目录名import os # 导入 os 模块list = [d for d in os.listdir('.')] 生成器 引入:列表生成式,可直接创建一个列表。但受到内存限制,列表容量肯定是有限的。例如:我们需要一个包含 100 万个元素的列表 ( 列表中的元素按照某种算法推算出来 ),直接创建是不太现实的,那么我们是否可通过某种过程,实现 动态推算 并 输出元素 ? Generator:生成器,即不用一步到位创建 list 对象,而是通过循环过程中不断推算出后续的元素。在 Python中,把这种一边循环一边计算的机制称作 Generator。

创建 Generator:把列表生成式的 [] 改成 () 即可。

123456789101112131415161718192021222324252627282930313233# 受到内存限制,运行过程中可能会崩掉list = [ x for x in range(1, int(10e10)) ]# 简单生成器generator = ( x for x in range(1, int(10e10)) )for n in generator: print(n) # 简单示例:带 yield 的 Generator 函数# 1) 在每次循环时都执行,遇到 yield 语句返回# 2) 再次执行时,从上次返回的 yield 语句处继续执行def odd(): print('First Return: ') yield [1, 2, 3] print('Second Return:') yield (1, 2, 3) print('Third Return:') yield {'key': 'value'} for n in odd(): print(n) # Fibonacci 数列def fibonacci(times): n, a, b = 0, 0, 1 while n < times: yield b (a, b) = (b, a+b) n = n + 1 return 'done' for n in fibonacci(10): print(n) 迭代器 可用于 for 循环的数据类型: 集合数据类型:list、tuple、dict (字典)、set、str (字符串) Generator 生成器和带 yield 的 Generator 函数

可用于 for 循环的对象统称为可迭代对象 Iterable。

123456# 使用 isinstance() 判断一个对象是否为 Iterable 对象form collections import Iterableisinstance([], Iterable) # Trueisinstance((x for x in range(1, 10)), Iterable) # Trueisinstance(100, Iterable) # False

生成器是 Iterator 对象;List、Dict、Str 虽然是 Iterable 对象,但却不是 Iterator。 我们可以通过 iter() 函数,把 List、Dict、Str 等 Iterable 转换达成 Iterator。

Python 的迭代器 ( Iterator ) 对象表示的是一个数据流,即 Iterator 对象可被 next() 函数调用并不断返回下一个数据,直至没有数据时抛出 StopIteration 异常。

12isinstance(iter([]), Iterator) # Trueisinstance(iter('abc'), Iterator) # True 函数式编程 函数: 模块化编程,即把大段功能代码拆分、封装成模块,通过层层调用,把复杂任务解构成简单任务。 这种分解称之为 面向过程 的程序设计。 函数是面向过程程序设计的 基本单元。 函数式编程: 就是一种抽象程序很高的 编程范式; 纯粹的函数式编程语言编写的函数没有变量; 函数式编程的特点:允许函数作为 参数,作为另一函数的 输入。 高阶函数

变量可指向函数:

12345678# 直接调用函数x = abs(-10)# 变量可指向函数f = absx = f(-10)# x 的结果都为 10

函数名也是变量:函数名其实就是指向函数的变量。

注意:1) 而在实际编码当中,绝对不能这样写,只是为了说明函数名也是变量。2) 若需恢复 abs 函数,请重启 Python 交互环境。

12345678abs = 10abs(-1)# 抛出异常# 即 abs 已指向一个整数 10,而不是指向求绝对值的函数。 Traceback (most recent call last): File "", line 1, in TypeError: 'int' object is not callble

传入函数:一个函数接收另一个函数作为参数,称为 高阶函数。

12345678def add(x, y, f): return f(x) + f(y) # 调用 add(-5, 6 abs) 时,计算的过程为:# x = -5# y = -6# f = abs# f(x) + f(y) MapReduce Python 内建了 map() 和 reduce() 函数。

Map / Reduce 的概念 :

MapReduce 是一种编程模型,是 处理 和 生成 大型数据集的相关实现。

用户指定一映射函数 map() 处理键/值对,以生成一组中间键/值对;同时也指定 reduce() 函数用以 合并 含相同中间键所关联的所有中间值。

为了更加透彻理解 MapReduce,可研读 Google 关于 MapReduce 的论文:MapReduce: Simplified Data Processing on Large Clusters $^{[4]}$。

Map 函数

map() 函数:其接收 两个参数,第一个是 函数,第二个是 Iterable。即 map 将传入的 函数 依次 作用 到序列的 每个元素,并把结果作为新的 Iterator 返回。

12345678910111213141516# 例 1:有一个函数 f(x) = x*x,将其作用于一个 list = [1, 2, 3, 4, 5]def f(x): return x ** 2# 1) map() 函数r = map(f, [1, 2, 3, 4, 5])print(list(r)) # 输出 [1, 4, 9, 16, 25]# 2) 不需要 map() 函数的等价写法list = []for n in [1, 2, 3, 4, 5] list.append( f(n) )print(list) # 输出 [1, 4, 9, 16, 25]# 例 2:map 作为高阶函数,事实上它把运算规则抽象了,如把 list 中数字转字符串list( map(str, [1, 2, 3, 4, 5]) ) # 输出 ['1', '2', '3', '4', '5'] Reduce 函数

reduce() 函数:其接收 两个参数,第一个是 函数,第二个是 Iterable。即 reduce 把结果继续和序列的 下一个元素 做 累积计算。

reduce(f, [x1, x2, x3, x4]) 等价于 f( f( f(x1, x2), x3 ), x4 )

12345from functools import reducedef add(x, y): return x + yprint( reduce(add, [1, 2, 3, 4, 5]) )

当然,上述的实例只是为了描述原理而设定,下面将结合 map() 与 reduce() 举例:

12345678910111213from functools import reduce# 定义一计算公式def fn(x, y): return x * 10 + y# 定义一字符转数字的函数def char2num(s): digits = {'0': 10, '1': 20, '2': 30, '3': 40} return digits[s] # map/reduce 实现处理与计算的功能print( reduce(fn, map(char2num, '0123')) ) Filter Python 内建了 filter() 函数,用于过滤序列。

filter() 函数:接收 两个参数,一个是 函数,另一个是 序列。即 filter 把传入的函数作用于每个元素,然后根据返回值是 True/False 决定是否 保留/丢弃 该元素。

filter() 函数返回的是一个 Iterator,即一个惰性序列,故需要强迫 filter() 完成计算结果,如 list() 函数获得所有结果。

12345678910111213# 在一个 list 中,删掉偶数,只保留奇数def isOdd(n): return n % 2 == 1 # 输出 [1, 3, 5]list( filter(isOdd, [1, 2, 3, 4, 5]) )# 把一个序列中的空字符剔除def rejectBlankStr(s): return s and s.strip() # 输出 ABClist( filter(rejectBlankStr, ['A', 'B', '', None, 'C']) ) Sorted

排序算法:排序的核心是 比较两元素的大小。若是数字则直接比较;但比较的若是字符串或两个字典,则比较过程需通过函数抽象实现。

12# 输出 [-6, 2, 12, 24, 36]print( sorted( [36, 24, -6, 12, 2] ) )

sorted() 也是一高阶函数,可接收一个 key 函数来自定义排序:

123456789# 输出 [2, -6, 12, 24, 36]print( sorted([36, 24, -6, 12, 2], key = abs) )# 忽略大小写,实现字符串排序# 实现字符串的比较是根据 ASCII 实现比较的print( sorted(['Bob', 'Lucy', 'Zoo', 'Danny'], key = str.lower) ) # 进行反向排序,可传入第三个参数实现print( sorted(['Bob', 'Lucy', 'Zoo', 'Danny'], key = str.lower, reverse = True) ) 返回函数函数作为返回值

函数作为返回值:高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

1234567891011121314151617181920212223242526# 通常情况实现一个可变参数的求和def calcSum(*args): ax = 0 for n in args: ax = ax + n return ax # 若不想立刻求和,可不返回求和结果,而是求和函数def lazySum(*args): def sum(): ax = 0 for n in args: ax = ax + n return ax return sum# 调用 lazySum() 时,返回函数而不是结果f = lazySum(1, 3, 5, 7, 9)# 调用 f,才真正计算求和的结果f()# 当每次调用 lazySum() 时,都会返回一个新的函数,既使传入参数相同f1 = lazySum(1, 3, 5, 7, 9)f2 = lazySum(1, 3, 5, 7, 9)print( f1 == f2 ) # 输出 False 闭包 注意到上述例子返回的函数在其定义内部引用了局部变量 args,故当一个函数返回一个函数后,其内部的局部变量还被新函数引用。

注意返回的函数并没有立刻执行,而是调用了 f() 才执行。

123456789101112131415161718192021222324252627282930313233def count(): fs = [] for i in range(1, 4): def f(): return i ** 2 fs.append(f) return fs f1, f2, f3 = count()# 输出 9::9::9print( str(f1()) + '::' + str(f2()) + '::' + str(f3()) ) """" 实际结果为:f1() --> 9,f2() --> 9, f3() --> 9" 全部结果都为 9,原因在于返回的函数引用了变量 i,但它并非立刻执行" 需等到 3 个函数都返回时,它们所引用的变量 i 已经变成了 3,故最终结果是 9"""# 若需引用循环的变量def count(): def f(j): def g(): return j * j return g fs = [] for i in range(1, 4): fs.append( f(i) ) # f(i) 立刻执行,i 的当前值被传入 f() return fs f1, f2, f3 = count()# 输出 1::4::9print( str(f1()) + '::' + str(f2()) + '::' + str(f3()) )

返回闭包时牢记一点:返回函数不要引用任何循环变量,或后续会发生变化的变量。

匿名函数 当函数作为 传入参数 时,我们不需要显式地定义函数,直接传入匿名函数更便捷。

关键字 lambda 表示匿名函数,冒号前面表示传入参数,后面为返回值 ( 一般为表达式运算后的结果 ),如 lambda x, y : x+y

1234567# 以 map() 函数为例# 输出 [1, 4, 9, 16, 25]print( list(map(lambda x : x ** 2, [1, 2, 3, 4, 5])) )# 匿名函数实际为:def f(x): return x ** 2

匿名函数有一好处,即不必担心 函数名冲突。此外,匿名函数也是一个函数对象,可把匿名函数赋值给一个变量,再利用变量来调用。

12f = lambda x : x ** 2print( f(5) ) # 输出 25

匿名函数作为返回值返回:

12def build(x, y): return lambda: x * x + y * y 装饰器

提示:对于装饰器,除了廖雪峰老师的教程外 ( 侧重原理讲解 ),还可参考程序员大咖的推文 Python 装饰器的诞生过程 ( 侧重具体实现讲解 )。

引例:假设我们有 time() 函数,我们要增强 time() 函数的功能,比如在函数调用前后自动打印日志,但又不希望修改 time() 函数的定义。

这种在代码运行期间动态增加功能的方式,称之为 装饰器 (Decorator)。

123456789def log(func): def wrapper(*args, **kw): print('call %s():' % func.__name__) return func(*args, **kw) return wrapper @logdef time(): print('2018-11-11 23:11')

那么装饰器是如何实现的?在实现装饰器之前,我们有必要回顾函数的特性:

函数作为变量传递:函数作为变量来传递,代表的是一个函数对象。若函数不加括号,是不会执行的; 函数作为参数传递:一个函数可以接受另一个函数对象作为自己的参数,并对函数对象进行处理; 函数作为返回值:一个函数的返回值可以是另一个函数对象。

函数嵌套及跨域访问:一个函数 (主函数) 内部是可以嵌套另一个函数 (子函数) 的;

1234567891011121314151617181920212223242526# 函数作为变量传递def add(x): return x + 1a = add # 作为变量# 函数作为参数传递def add(x): return x + 1def excute(f): return f(3)excute(add) # 作为参数# 函数作为返回值def add(x): return x + 1def get_add(): return add # 作为返回值# 函数嵌套及跨域访问def outer(): x = 1 def inner(): print(x) # 被嵌套函数 inner 内部的 x 变量可以到封装域去获取 inner() outer()

Python 中的装饰器是通过闭包实现的,即闭包就是引用了外部变量的内部函数,而闭包的实现正是利用了以上函数特性。具体实现:

问题:观察打印结果,从 func() 到 closure(),func 变成了closure,具体是怎么装饰的呢?

解释:closure 实际上是 outer(func),func 作为参数传进 outer,outer 的子函数 inner 对 func 返回的结果进行了一番装饰,返回了一个装饰后的结果,最后 outer 返回 inner,可以说 inner 就是装饰后的 func,这就是一个函数被装饰的过程,重点在于执行 outer(func) 这个步骤,即执行 closure()。

1234567891011121314151617def func(): return '函数 func'def outer(x): def inner(): # 函数嵌套 return '戴了 inner 帽子的' + x() # 跨域访问,引用了外部变量 x return inner # 函数作为返回值# 函数 func 作为 outer 的参数,函数作为变量赋给 closureclosure = outer(func) print( func() ) # 执行原始函数print( closure() ) # 执行闭包# 执行结果:# 函数 func# 戴了 inner 帽子的函数 func

装饰器语法糖 @:Python 给我们提供了语法糖 @,我们想执行 outer(func),只需要把 outer 函数 @ 到 func 函数的上即可。具体实现:

12345678910def outer(x): def inner(): return '戴了 inner 帽子的' + x() return inner@outerdef func(): return '函数 func'print( func() ) # 输出:戴了 inner 帽子的函数 func 偏函数

例:int() 函数可把字符串转为整数,当且仅当传入字符串时,int() 函数默认按照 10 进制转换。

1234567891011121314print( int('12345') ) # 输出 12345# int() 函数提供额外 base 参数,默认值为 10# 若传入 base 参数即可做 N 进制转换 ( N 进制转到 10 进制 )print( int('10', base = 8) ) # 输出 8print( int('A', base = 16) ) # 输出 10# 若我们要转换大量二进制字符串,则可通过定义函数def int2(x, base = 2): return int(x, base) # 这样转换二进制就非常便捷了print( int2('10000000') ) # 输出 128print( int2('10101010') ) # 输出 170

其实 functools.partial 就是帮助我们创建一个偏函数,即其作用就是把一个函数的某些参数固定住 ( 设置默认值 ),返回一个新函数。

12import functoolsint2 = functools.partial(int, base = 2)

创建偏函数时,实际可接收 函数对象、*args 和 **kw 这三个参数。

123456int2 = functools.partial(int, base = 2)# 相当于:args = '10001000'kw = {'base': 2} int(*args, **kw) 模块基本概念

一个 .py 文件称之为一个模块 (Module),模块可避免函数名和变量名冲突。

⚠️ 尽量不与 Python 内置函数名称相冲突,详细可参考 Python 标准函数库 $^{[6]}$。

按目录来组织模块的方法,称为包 (Package),可避免模块名称的冲突。

⚠️ 创建模块的名称不能和 Python 自带的模块名称相冲突。例如系统自带 sys 模块。

__init__.py 该文件必须存在,否则 Python 就把当前 目录当作普通目录,而不是一个包了。 __init__.py 可以是空文件,也可含有代码。 samplye.py 的模块名称为 mypython.sample。

__init__.py 的模块名称为 mypython。

1234mypython ├─ __init__.py ├─ sample.py └─ example.py 使用模块

以内建的 sys 模块为例,编写 sample 模块:

123456789import sysdef test(): args = sys.argv if len(args) > 2: for str in args: print('%s' % str) else: print('Empty paramter')

作用域:在一模块中,我们可能会定义很多函数和变量。或许我们有这样的需求:有的函数和变量仅希望是在模块内部使用。Python 中通过 _ 前缀实现的。

正常的函数和变量名是公开的 (public),可被直接引用。例如,abc、x1、PI 等。

非公开的函数和变量 (private),不应该被直接引用。例如 _xxx、__xxx。

不应该 被直接引用,而不是不能被直接引用,因为 Python 并没有一种方法可以完全限制访问 private 函数或者变量。

使用 private 函数,实现代码封装和抽象的方法:

12345678def __sayHello(name): print('Hello' + name)def greeting(name): __sayHello(name)# 调用函数greeting('Bob') 第三方模块 Python 中,安装第三方模块是通过包管理工具 pip 完成的。 若是 Mac/Linux 用户,可跳过安装 pip 的步骤。 若是 Windows 用户,则需要安装 pip 工具。( 安装方法自行搜索或参考 [7] ) 安装完包管理工具 pip,可通过 pip install Pillow (Python 2.x) 或 pip3 install Pillow (Python 3.x) 命令安装 Python Imaging Library (处理图像的工具库)。

当然,Python 使用过程中需要安装和使用大量的第三方库,若通过上述方式安装未免太过繁琐,故我们可考虑直接安装 Anaconda。

Anaconda,其是一个基于 Python 的数据处理和科学计算的平台,他已经内置了许多非常有用的第三方库。在完整完 Anaconda 后,重新在命令行中键入 python,出现以下信息即安装成功,可正常导库使用:

123pythonpython 3.x.x | Anconda, Inc. | ... on darwin>>> import numpy # 直接倒入第三方模块即可 模块搜索路径

当我们试图搜索某一模块,若找不到会报错。

1234>>> import mymoduleTraceback (most recent call last) File "", line 1, in ModuleNotFoundError: No module named 'mymodule'

默认情况,Python 解释器会搜索当前目录,所有已安装内置模块和第三方模块,搜索路径 存放在 sys 模块 的 path 变量 中:

1234# 若需要添加搜索目录import syssys.path.append('/User/kofe/mypython')print(sys.path) # 查看是否已添加 面向对象编程

面向对象编程,Object Oriented Programming,简称 OOP。是一种程序设计思想。其把对象作为 程序基本单元,且对象中包含了 数据 和 操作的函数。

面向对象的程序设计把计算机程序视为一组对象集合,而每个对象都可接收其他对象发送的消息并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

面向对象的程序可理解为:程序 = 对象 + 对象;对象 = 成员变量 + 成员函数。

对比 面向过程编程,即把计算机程序视为一系列的命令集合,或可理解为一组函数的顺序执行。

面向过程的程序可理解:程序 = 函数 + 算法。

在 Python 中,所有数据都可视为对象。当然,可以通过类来自定义对象的数据类型。例如,我们定义一个 Student 类型来代表学生的范畴:

123456789101112131415class Student(object): # __xxx__ 为特殊变量或方法,有特殊用途,会在后面章节详细讲解 def __init__(self, name, score): self.name = name self.score = score def printScore(self): print('name: %s, score: %s' % (self.name, self.score)) stu1 = Student('Lucy', 80)stu2 = Student('Danny', 90)# 给对象发送消息实际就是调用对应的关联函数stu1.printScore()stu2.printScore() 类和实例 面向对象的核心概念是 类 (Class) 和 实例 (Instance),牢记类是抽象的模板。例如,上述的 Student 类,实例即根据类创建出一个个具体的对象 stu1、stu2。

通过 class 关键字定义类:

123# 若没有合适的继承类,则默认使用 object 类,这是所有类最终都会继承的类class Student(object): pass

创建类的实例,如 stu = Student()

由于类起到模板的作用,因此可在创建实例时,通过特殊方法 __init__(),把属性绑定进去。

1234567class Student(object): def __init__(self, name, score): self.name = name self.score = score#创建实例时,不需要传入 self,即实例本身stu = Student('Lucy', 95)

数据封装:访问实例本身的数据,不通过外部函数访问,而是通过类的内部定义访问数据的函数,这样实现数据封装。

123class Student(object): def printInfo(self): print('name: %s, score: %s' % (self.name, self.score)) 访问限制

私有变量:让内部属性不被外部访问,在 Python 中,通过双下划线 __ 开头,变量变成私有变量。

12345678910111213141516171819202122232425262728293031323334class Student(object): def __init__(self, name, score): self.__name = name self.__score = score # Getter 方法 def getName(self): return self.__name def getScore(self): return self.__score # Setter 方法 def setName(self, name): self.__name = name def setScore(self, score): # Setter 方法修改属性值的好处,可定义规则约束有效值 if 0 Dog,则有:a = Animal() => isinstance(a, Animal) =>Trued = Dog() => isinstance(d, Animal) =>Trued = Dog() => isinstance(d, Dog) =>True

dir() 函数:若要获得一个对象的 所有属性和方法,可使用该函数。它返回一个包含字符串的 list。例如,获得一个 str 对象的所有属性和方法。

12# 输出:['__add__', '__class__', ... 'zfill']dir('abc')

类似 __xxx___ 的属性和方法在 Python 中都有特殊用途。如 __len__() 方法返回长度。调用 len() 函数,在函数内部实际是它自动地去调用该对象的 __len__() 方法,故下面代码是等价的。

1len('abc') == 'abc'.__len__() # 输出 True

仅仅把属性和方法列出来是不够的,配合 getattr()、setattr() 及 hasattr(),我们可直接操作一个 对象的状态。

12345678910111213141516class Retangle(object): def __init__(self): self.x = x self.y = y def area(self): return self.x * self.yrectangle = Rectangle(5, 10)hasattr(rectangle, 'z') # 是否含有属性 zsetattr(rectangle, 'z', 1) # 设置一个属性 z,令其等于 1getattr(rectangle, 'z') # 获取属性 z# 也可以获得对象方法if hasattr(rectangle, 'area'): fn = getattr(rectangle, 'area') 实例属性和类属性

给实例绑定属性的方法是通过 实例变量 赋值,或通过 self 变量 赋值。

123456789class Student(object): # self 变量赋值 def __init__(self, name, score): self.name = name self.score = scorestu = Student('Bob', 80)# 实例变量赋值stu.gender = 'male'

给类绑定属性,直接在 class 中定义属性即可。

Tips:编写程序时,不要对 实例属性 和 类属性 使用相同名称,若含有相同名称的实例属性,将屏蔽掉同名称的类属性。

12345class Student(object): grade = 'postgraduate' stu = Student('Lucy', 95)print( stu.grade ) # 与 print(Student.grade()) 效果相同 面向对象高级编程使用 @property

引入:在「访问限制」章节中,我们通过 setScore() 和 getScore() 方法实现修改数据和获取数据,以实现数据封装。

那么本节提及 @property 属性,到底是何意图?先看看原始的 Setter 和 Getter 使用方法:

12345678910111213141516class Student(object): # Getter 方法 def getScore(self): return self.__score # Setter 方法 # setXXX() 方法还可书写规则以约束输入数据或检查数据 def setScore(self, score): if 0 100000: # 退出循环的条件 raise StopIteration() return self.a # 返回下一个值# Fib 实例作用于 For 循环: for n in Fib(): print(n)

对于定制类,我们让其实现了 __iter__() 和 __next__() 方法,那么它就是一个 Iterator 类型的,这正是动态语言的特性。

这种特性称为 动态语言 的 鸭子类型,动态语言并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

__call__

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,使用 instance.method() 来调用。能不能直接在实例本身上调用呢?

答案是可以的。任何类,只需要定义一个 __call__() 方法,就可以直接对实例进行调用。

123456789class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s.' % self.name)# 调用方式stu = Student('Bob')stu() # 输出 My name is Bob. 更多定制 Python的 class 允许定义许多定制方法,让我们非常方便地生成特定的类。更多的定制方法请参考 Python 的官方文档:Special Method Names。 使用枚举类 在 Python 中,我们定义常量是采用 约定俗成 的方法来定义的,例如:PI = 3.14159。但其本质仍然是 变量。

而本节介绍的枚举类,通过 Enum 定义一个 class 类型,然后,每个常量都是 class 的一个 唯一实例。例如:定义 Month 类型的枚举类。

1234567891011121314151617181920212223242526272829from enum import EnumMonth = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))for name, member in Month.__members__.items(): # value 属性:则是自动赋给成员的 int 常量,默认从 1 开始计数 print(name, '=>', member, ',', member.value)'''' 输出结果:' Jan => Month.Jan , 1' Feb => Month.Feb , 2' Mar => Month.Mar , 3' Apr => Month.Apr , 4' May => Month.May , 5' Jun => Month.Jun , 6' Jul => Month.Jul , 7' Aug => Month.Aug , 8' Sep => Month.Sep , 9' Oct => Month.Oct , 10' Nov => Month.Nov , 11' Dec => Month.Dec , 12'''# 当然,我们还可以这样访问枚举类print( Month.Jan ) # 输出 Month.Janprint( Month(1) ) # 输出 Month.Janprint( Month['Jan'] ) # 输出 Month.Janprint( Month.Jan.value ) # 输出 1

若有需求,我们可精确地控制枚举类型,即从 Enum 派生出自定义类:

123456789101112from enum import Enum, unique@unique# @unique 装饰器可以帮助我们检查保证没有重复值class Weekday(Enum): Sun = 0 Mon = 1 Tue = 2 Wed = 3 Thu = 4 Fri = 5 Sat = 6 错误/调试/测试错误处理返回错误码

在操作系统提供的调用中,返回错误码非常常见。比如打开文件的函数 open(),成功时返回文件描述符 (就是一个整数),出错时返回 -1。同理,我们设计函数时,也可相仿地设置返回代码。

1234567RESULT_OK = 0RESULT_FALSE = -1def test(): if false: return RESULT_FALSE return RESULT_OK 异常错误

高级语言通常都内置了一套 try...except...finally... 的错误处理机制,Python 也不例外,使用方法见实例:

1234567891011121314151617try: result = 10 / int('2') # result = 10 / 0 print('result:', result)except ValueError as e: # 抛出非数值异常错误 print('ValueError:', e)except ZeroDivisionError as e: # 抛出被除数为零的异常错误 print('ZeroDivisionError:', e)else: # 若没有错误发生可在 except 语句块后加一个 else # 当没有错误发生时,会自动执行else语句 print('no error!')finally: # 若设置了 finally 则一定会被执行,但可不设置 finally 语句 print('finally...')

Python 的异常类型其实也是 class,所有的异常类型都继承自 BaseException,常见的错误类型和继承关系见:Python. Exception hierarchy

故在使用 except 时需要注意的是:它不但捕获该类型的错误,还把其子类也 “一网打尽”,例如:

12345678910try: foo()except ValueError as e: print('ValueError')except UnicodeError as e: print('UnicodeError') # 假设 foo() 函数运行错误,则输出 "ValueError"# 第二个 except 永远也捕获不到 UnicodeError# 因为 UnicodeError 是 ValueError 的子类,即异常被第一个 except 给捕获了 调用栈

在函数嵌套调用中,若错误没有被捕获,它就会一直往上抛,直至被 Python 解释器捕获,并打印一个错误信息然后程序退出。因此当发生错误时,一定要分析错误的 调用栈 信息,定位错误的位置,找出 错误根源。

12345678910111213141516171819202122# err.py# 定义函数def foo(src): return 10 / int(src)def bar(src): return foo(src) * 2def main(): bar('0')main() # 调用函数# 抛出异常错误,错误的跟踪信息如下:Traceback (most recent call last):File "err.py", line 11, in main()File "err.py", line 9, in main bar('0')File "err.py", line 6, in bar return foo(s) * 2File "err.py", line 3, in foo return 10 / int(s)ZeroDivisionError: division by zero 抛出异常

异常类型属于 class,捕获一个异常就是捕获到该 class 的一个实例。因此,异常并不是凭空产生而是 有意 创建并抛出的。Python 的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出异常。

如果要抛出错误,首先根据需要定义一个异常的 class,并选择好继承关系,然后用 raise 语句抛出一个异常实例:

123456789101112# 只有在必要的时候才定义我们自己的错误类型# 尽量使用 Python 内置的错误类型,例如 ValueError,TypeErrorclass FooError(ValueError): passdef foo(s): n = int(s) if 0 == n: raise FooError('invalid value: %s' % s) return 10 / nfoo('0') 调试 推荐 IDE 调试,即设置断点、单步执行,就需要一个支持调试功能的 IDE。目前比较好的 Python IDE 有: PyCharm 和 Eclipse vs pyDev。 单元测试 单元测试是用来对一个 模块、函数 或 类 来进行正确性检验的 测试 工作。 这种以 测试驱动 的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的 测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。

为了编写单元测试,我们需要引入 Python 自带的 unittest 模块。以下为一个 单元测试 的示例:

1234567891011121314151617181920212223242526272829303132import unittest# 继承unittest.TestCaseclass MyTest(unittest.TestCase): # 每个测试用例执行之后做操作 def tearDown(self): print('After each testcase...') # 每个测试用例执行之前做操作 def setUp(self): print('Before each testcase...') @classmethod # 必须使用 @classmethod 装饰器,所有 test 运行完后运行一次 def tearDownClass(self): print('After all testing...') @classmethod # 必须使用 @classmethod 装饰器,所有 test 运行前运行一次 def setUpClass(self): print('Before all testing...') def testTestcaseA(self): self.assertEqual(1, 1) # 测试用例 def testTestcaseB(self, elem1 = 'a', elem2 = 'A'): self.assertEqual(elem1, elem2) # 测试用例 # 一旦编写好单元测试就可运行单元测试,最简单的运行方式是在最后加上两行代码:if __name__ == '__main__': unittest.main()

下面是一些常用的断言,也就是校验结果:

12345678assertEqual(a, b) # a == bassertNotEqual(a, b) # a != bassertTrue(x) # bool(x) is TrueassertFalse(x) # bool(x) is FalseassertIsNone(x) # x is NoneassertIsNotNone(x) # x is not NoneassertIn(a, b) # a in bassertNotIn(a, b) # a not in b 面向 I/O 编程

I/O:即输入/输出 ( Input/Output )。

I/O 接口:是 主机 与 被控对象 进行 信息交换 的纽带。例如,程序运行时数据是在内存中驻留的,由 CPU 来执行计算、控制,其中涉及到的数据交换则由磁盘、网络等实现。具体地,I/O 接口的功能就是负责选址、传送命令、传送数据等。

I/O 编程:操作 I/O 是由 操作系统 完成的,且操作系统会提供低级 C 接口,即对 I/O 操作进行 封装,高级语言通过调用 (函数) 的方式实现操作 I/O 的目的。Python 也不例外,即面向 I/O 接口编程。

程序完成 I/O 操作会有 Input 和 Output 两个 数据流:

Stream (流) 是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。 Input Stream 就是数据从外面 (磁盘、网络) 流进内存,Output Stream 就是数据从内存流到外面去。

需要知道的是,CPU 的速度远远快于磁盘、网络等 I/O。因此,代码操作 I/O 接口时速度是会产生不匹配的问题,而同步和异步的区别就在于是否等待 I/O 执行的结果,故 I/O 编程有分 同步模型 和 异步模型。

同步 I/O:在一个线程中,CPU 执行代码的速度极快,然而,一旦遇到 I/O 操作,如读写文件、发送网络数据时,就需要等待 I/O 操作完成才能继续进行下一步操作。

引用廖老师的例子,同步 I/O 指:去麦当劳点餐,你说 “来个汉堡”,服务员告诉你,对不起,汉堡要现做需等 5 分钟,于是你站在收银台前面等了 5 分钟,当拿到汉堡再去逛商场。

异步 I/O:当代码需要执行一个耗时的 I/O 操作时,它只发出 I/O 指令并不等待 I/O 结果,然后去执行其他代码。一段时间后,当 I/O 返回结果时,再通知 CPU 进行处理。

异步 I/O 指:你说“来个汉堡”,服务员告诉你,汉堡需要等 5 分钟,你可以先去逛商场,等做好了我们再通知你,这样你可以立刻去干别的事情 (逛商场),这是异步 I/O。

同步 I/O 与 异步 I/O 模型的实现原理如图 6-10-1 所示:

图 6-10-1 同步 I/O 与 异步 I/O 模型的实现原理

图 6-10-1 同步 I/O 与 异步 I/O 模型的实现原理 存/取本地数据 读写文件是最常见的 I/O 操作,Python 内置了读写文件的函数,用法和 C 是兼容的。 在磁盘上读写文件的功能都是由操作系统提供的,即读写文件就是请求操作系统打开一个 文件对象 (通常称为文件描述符),通过操作系统提供的接口从这个文件对象中读取数据 (读文件),或把数据写入这个文件对象 (写文件)。 读文件

open() 函数,传入文件名和标示符:

1234567891011121314151617181920212223242526try: # 以只读方式读入 test.txt 文件 file = open('/Users/kofe/test.txt', 'r', encoding='utf-8') # 若文件不存在,则抛出 IOError 的错误 # Traceback (most recent call last): # File "", line 1, in # FileNotFoundError: [Errno 2] No such file or directory: '...' # 若文件打开成功,调用 read() 方法可一次读取文件的全部内容 # Python 把内容读到内存,用一个 str 对象表示** str = f.read()finally: if file: f.close() # 文件使用完毕后必须关闭 # 当然,try...finally... 的写法实在太繁琐,故 Python 引入了 with 语句写法:with open('/Users/kofe/test.txt', 'r') as file: print( file.read() )# 读取文件的方式:# read():适合文件较小,可一次性读取文件# read(size):若不能确定文件大小,通过反复调用读取文件# readlines():若是读取配置文件,行读取最为方便for line in f.readlines(): print(line.strip()) # 把末尾的 '\n' 删掉 File-like Object 要想操纵一个文件你需要使用 open() 函数打开文件,open() 函数返回一个 类文件对象 (File-like Object),这就是这个文件在 python 中的抽象表示。除了 File 外,还可以是内存的字节流,网络流,自定义流等。 File-like Object 不要求从特定类继承,就如 定制类.iter 章节所提及的 鸭子类型,只要我们让 class 实现 read() 方法,它就是 File-like Object。 二进制文件

前面的操作是读取文本文件,且是 UTF-8 编码的文本文件。要读取二进制文件,例如图片、视频等,用 rb 模式打开文件即可:

12345file = open('/Users/kofe/test.jpg', 'rb')print( file.read() )# 输出十六进制表示的字节:b'\xff\xd8\xff\x18Exif\x00...' 写文件

写文件和读文件是一样的,唯一区别是调用 open() 函数时,传入标识符 w 或者 wb 表示写 文本文件 或 写二进制文件:

1234# 写入文件后,务必要调用 f.close() 来关闭文件# 使用 Try...finally... ,或 With 语句的写法:with open('/Users/kofe/test.txt', 'w') as file:file.write('Hello, world!')

所有模式的定义及含义可参考 Python 官方文档:Built-in Functions.open()

表 6-10-1 open() 函数操作文件的模式 标识符 描述 r 只读模式 (默认) w 写入模式 (覆盖原文件) a 追加模式 (文件存在则在文件尾部追加,反之则建立) b 二进制格式 + 刷新打开的磁盘文件 (读与写) StringIO/BytesIO

StringIO 和 BytesIO 是在内存中操作 str 和 bytes 的方法。

StringIO 数据读写不一定是文件,也可以在内存中读写。

StringIO 顾名思义就是在内存中读写 str。要把 str 写入 StringIO,我们需要先创建一个 StringIO,然后像文件一样写入:

12345678910111213from io import StringIOfile = StringIO()file.write('Welcome to\n Python World!')while True: s = file.readline() if '' == s: break print( s.strip() ) # 输出结果:Welcome toPython World! BytesIO StringIO 操作的只能是 str,如果要操作二进制数据,就需要使用 BytesIO。

BytesIO 实现了在内存中读写 bytes。

1234567from io import BytesIOfile = BytesIO()file.write( '中文'.encode('utf-8') )print( file.getvalue() )# 写入的不是 str,而是经过 UTF-8 编码的 bytes:b'\xe4\xb8\xad\xe6\x96\x87' 操作文件和目录

若我们要操作文件、目录,可在命令行下面输入操作系统提供的各种命令来完成。例如 dir、cp 等命令。

若要在 Python 程序中执行这些目录和文件的操作怎么办?其实 Python 内置的 os 模块,可以直接调用操作系统提供的 接口函数。

123456import os# 现实操作系统类型# posix:Linux、Unix 或 Mac OS X# nt:Windowsprint( os.name ) 环境变量

在操作系统中定义的环境变量,全部保存在 os.environ 这个变量中,可直接查看:

1234567import os# 操作系统中定义的环境变量,全部保存在os.environ 变量中os.environ# 获取某个环境变量的值:os.environ.get('key')os.environ.get('PATH') 操作文件和目录 操作文件和目录的函数一部分放在 os 模块中,一部分放在 os.path 模块中。

查看、创建和删除目录可以这么调用:

12345678910111213141516171819# 查看当前目录的绝对路径os.path.abspath('.')# 在某个目录下创建一个新目录 (首先把新目录的完整路径表示出来)os.path.join('/Users/kofe', 'testdir')# 然后创建一个目录os.mkdir('/Users/michael/testdir')# 删掉一个目录os.rmdir('/Users/kofe/testdir')# 把两个路径合成一个时,不要直接拼字符串,而要通过 os.path.join() 函数# 同理,要拆分路径时,也不要直接去拆字符串,而要通过 os.path.split() 函数os.path.split('/Users/kofe/testdir/file.txt')('/Users/kofe/testdir', 'file.txt') # 返回一个元组 Tuple # 例如:os.path.splitext() 可直接让你得到文件扩展名os.path.splitext('/path/to/file.txt')('/path/to/file', '.txt') # 返回一个元组 Tuple

文件操作:

12345678# 对文件重命名os.rename('test.txt', 'test.py')# 删掉文件os.remove('test.py')# 然而在 os 模块中没有关于复制的函数# 借助 shutil 模块提供了copyfile() 的函数实现复制 (os 模块的补充)

利用 Python 的特性操作文件或目录:

12# 列出当前目录下的所有目录[x for x in os.listdir('.') if os.path.isdir(x)] 序列化 在程序运行期间,变量都是在内存中存放的。当 程序结束,变量所占用的内存将被操作系统 全部回收。若在程序运行期间,有需求保存 变量 或者 对象 的 数据 和 状态信息,以待下次启动程序时可直接加载该变量或对象。 序列化:将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。 反序列化:可通过从存储区中读取或反序列化对象的状态,重新创建该对象。

在 Python 中,序列化称为 pickling,在其他语言中也被称为 serialization、marshalling、flattening 等。Python 提供了 pickle 模块来实现序列化。

12345678910111213141516171819import pickle dict = {'name': 'Bob', 'age': 25, 'score': 90}### Case.01. 对象/变量 => Bytes => File 文件# dump() 将序列化后的对象 obj 以二进制形式写入文件 file 中with open('./dump.txt', 'wb') as file: pickle.dump(dict, file)# load() 将序列化的对象从文件 file 中读取出来with open('./dump.txt', 'rb') as file: dict = pickle.load(file)### Case.02. 对象/变量 => Bytes# dumps() 方法不需要写入文件中,可直接返回一个序列化的 bytes 对象dump = pickle.dumps(dict)# loads() 则可直接读取一个序列化的 bytes 对象dict_sub = pickle.loads(dump) JSON 基础

引入:Pickle 的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于 Python,且可能不同版本的 Python 彼此都不兼容。

若我们要在不同的编程语言之间传递对象,就必须把 对象序列化为标准格式,例如 XML,但 XML 需要解析读取。但更好的方法是序列化为 JSON,因为 JSON 表示出来就是一个 字符串,可以被所有语言读取,且方便地存储到磁盘或者通过网络传输。

JSON 表示的对象就是标准的 JavaScript 语言的对象,JSON 和 Python 内置的数据类型对应如下:

表 6-10-2 JSON 类型与 Python 类型的数据类型对应表 JSON 类型 Python 类型 {} dict [] list “String” str 10 / 3.14159 int / float true / false True / False null None

Python 内置的 json 模块提供了非常完善的 Python 对象到 JSON 格式的转换:

1234567891011import jsondict = {'name': 'Bob', 'age': 25, 'score': 90}# dump() 方法可以直接把 JSON 写入一个 File-like Object# dumps() 方法返回一个字符串,内容就是标准的 JSONjson_str = json.dumps(dict)# load() 方法从一个 File-like Object 中直接反序列化出对象# loads() 把 JSON 的字符串反序列化为 Python 对象json_str = '{"name": "Bob", "age": 25, "score": 90}'json.loads(json_str) 由于 JSON 标准规定 JSON 编码是 UTF-8,所以我们总是能正确地在 Python 的 str 与 JSON 的字符串间转换。 JSON 进阶

Python 的 dict = {'key': value} 对象可直接序列化为 JSON 的 {"key": value}。但一般情况,我们常用 class 表示对象 ( 例如 Student 类 ),再序列化该对象:

1234567891011import jsonclass Student(object): def __init__(self, name, age, score): self.name = name self.age = age self.score = score# 运行代码,将会报 TypeError 错误stu = Student('Bob', 25, 90)print(json.dumps(stu))

造成上述错误的原因是:Student 对象不是一个可序列化为 JSON 的对象。

其实,仔细观察 dumps() 的参数列表,可以发现除了第一个必须的 obj 参数外,dumps() 方法还提供了一大堆的 可选参数,这些可选参数可让我们来定制 JSON 序列化:

1234567891011# 默认情况下 dumps() 不知道如何将 Student 实例变为 JSON 的 {"key": value}# 我们只需为 Student 实例专门写一个转换 (组装) 函数def student2dict(std): return { 'name': std.name, 'age': std.age, 'score': std.score }# 这样,Student 实例首先被 student2dict() 函数转换成 dict,再被序列化为 JSONprint( json.dumps(stu, default = student2dict) )

当然,若我们遇到一个 Teacher 类的实例,照样无法序列化为 JSON。其实可以通过一种 通用方法 将任意 class 的实例变为 dict。

通常 class 的实例都有一个 __dict__ 属性,它本身一个 dict,用来存储实例变量。也有少数例外,比如定义了 __slots__ 的 class。

1print( json.dumps(s, default = lambda obj: obj.__dict__) )

同理,我们需要把 JSON 反序列化为一个 Student 实例对象,loads() 方法首先转换出一个 dict 对象,然后我们传入的 object_hook 函数,其负责把 dict 转换为 Student 实例对象。

12345def dict2student(d): return Student( d['name'], d['age'], d['score' ]) json_str = '{"age": 25, "score": 90, "name": "Bob"}'print( json.loads(json_str, object_hook = dict2student) ) 同步 I/O

在本章引言部分已讲述 同步 I/O 与 异步 I/O 模型的区别,同步模型即按普通顺序写执行代码:

12345678do_some_code()file = open('/path/file.txt', 'r')# 线程停在此处等待 I/O 操作结果r = file.read() # I/O 操作完成后线程才能继续执行do_some_code(r) 异步 I/O

在 I/O 操作过程中,由于一个 I/O 操作阻塞了当前线程,导致其他代码无法执行,故我们可使用多线程或者多进程来 并发 执行代码。然而,我们通过 多线程和多进程 的模型解决了 并发 问题,但现实情况是系统不能无上限地增加线程,因为系统切换线程的开销很大,一旦线程数量过多,CPU 花在线程切换上的时间就增多,则导致性能严重下降的结果。

CPU 高速执行能力和 I/O 设备的读写速度严重不匹配导致线程阻塞。多线程和多进程只是解决这一问题的一种方法,而另一种解决 I/O 问题的方法就是异步 I/O。

异步 I/O 模型 需要一个 消息循环。在消息循环中,主线程不断地重复 读取消息 / 处理消息 这一过程:

1234loop = get_event_loop()while True: event = loop.get_event() process_event(event) 协程

在开始异步 I/O 模型学习前,我们先来了解 协程 的概念。

协程 ( Coroutine ),又称微线程、纤程。协程不是进程或线程,其执行过程更 类似于 子程序,或者说 不带返回值的函数调用。

例如:A 调用 B,B 中又调用了 C。C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕。

子程序调用是通过栈实现的,一个线程 就是执行 一个子程序。子程序调用总是 一个入口,一次返回,且 调用顺序是明确的。

而协程的调用和子程序是不同的。协程看上去也是子程序,但在执行过程中,调用顺序不固定,在子程序内部可中断转而执行别的子程序,在适当的时再返回来接着执行原子程序。

1234567891011121314def A(): print('1') print('2')def B(): print('x') print('y') # 若由协程执行,在执行 A 的过程中可随时中断去执行 B# B 也可能在执行过程中中断再去执行 A,则执行结果有:1xy2

从上述例子结果可看出,A、B 的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

协程极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制。因此,没有线程切换的开销,和多线程相比,线程数量越多协程的性能优势就越明显。

不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,故执行效率相对多线程要高。

因为协程是一个线程执行,是否可利用多核 CPU 获得更高的性能,若方案可行的话如何操作?最简单的方法是 多进程 + 协程,既充分利用多核,又充分发挥协程的高效率。

Python 对协程的支持是通过 generator 实现的,例如:

传统 生产者 - 消费者 模型是:一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但有很大机率出现 死锁。

若改用协程,生产者生产消息后,直接通过 yield 跳转到消费者开始执行,待消费者执行完毕后切换回生产者继续生产。

123456789101112131415161718192021222324252627282930313233343536def consumer(): result = '' while True: # Step.03. consumer 通过 yield 取消息并处理,再通过 yield 把结果回传 n = yield result print('[CONSUMER] Consuming %s...' % n) result = 'OK:' + str(n) # result 可能是 I/O 操作或耗时任务def produce(c): # Step.01. 首先调用 c.send(None) 启动生成器 c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) # Step.02. 当产生了东西后,通过 c.send(n) 切换到 consumer 执行 result = c.send(n) # Step.04. produce 拿到 consumer 的处理结果,(或) 继续生产下条消息 print('[PRODUCER] Consumer return: %s' % result) # Step.05. produce 决定不生产了,通过 c.close() 关闭 consumer,整个过程结束 c.close()# 函数调用cons = consumer()produce(cons)# 输出结果[PRODUCER] Producing 1...[CONSUMER] Consuming 1...[PRODUCER] Consumer return: OK:1[PRODUCER] Producing 2...[CONSUMER] Consuming 2...[PRODUCER] Consumer return: OK:2[PRODUCER] Producing 3...[CONSUMER] Consuming 3...[PRODUCER] Consumer return: OK:3 Asyncio asyncio 是 Python 3.4 版本引入的标准库,直接内置了对异步 I/O 的支持。

asyncio 的编程模型是一个 消息循环。我们从 asyncio 模块中直接获取一个 EventLoop 的引用,然后把需要执行的协程扔到 EventLoop 中执行,就实现了异步 I/O,具体操作实例:

@asyncio.coroutine 把一个 generator 标记为 coroutine 类型,然后,我们就把这个 coroutine 扔到 EventLoop 中执行。

hello() 会首先打印出 Hello world!。然后,yield from 语法可以让我们方便地调用另一个 generator。由于 asyncio.sleep() 也是一个 coroutine,所以线程不会等待 asyncio.sleep(),而是直接中断并执行下一个消息循环。当 asyncio.sleep() 返回时,线程就可以从 yield from 拿到返回值 ( 此处是 None ),然后接着执行下一行语句。

若我们把 asyncio.sleep(1) 看成是一个耗时一秒的 I/O 操作。在此期间,主线程并未等待,而是去执行 EventLoop 中其他可以执行的 coroutine,因此实现了 并发执行。

1234567891011121314import [email protected] hello(): print("Hello world!") # 异步调用 asyncio.sleep(1): r = yield from asyncio.sleep(1) print("Hello again!")# 获取 EventLooploop = asyncio.get_event_loop()# 执行 coroutineloop.run_until_complete(hello())loop.close()

举一反三:我们尝试用 Task 封装两个 coroutine:

123456789101112131415161718192021import threadingimport [email protected] hello(): print('Hello world! (%s)' % threading.currentThread()) yield from asyncio.sleep(1) print('Hello again! (%s)' % threading.currentThread())loop = asyncio.get_event_loop()tasks = [hello(), hello()]loop.run_until_complete(asyncio.wait(tasks))loop.close()# 输出结果# 由打印当前线程名称可看出,两个 coroutine 由同一个线程并发执行的Hello world! ()Hello world! ()(暂停约 1 秒)Hello again! ()Hello again! ()

具体应用场景:我们 asyncio 的异步网络连接来获取 sina、sohu 的首页。

12345678910111213141516171819202122232425262728293031323334import [email protected] wget(host): print('wget %s...' % host) connect = asyncio.open_connection(host, 80) reader, writer = yield from connect header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host writer.write(header.encode('utf-8')) yield from writer.drain() while True: line = yield from reader.readline() if line == b'\r\n': break print('%s header > %s' % (host, line.decode('utf-8').rstrip())) # Ignore the body, close the socket writer.close()loop = asyncio.get_event_loop()tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com']loop.run_until_complete(asyncio.wait(tasks))loop.close()# 输出结果wget www.sohu.com...wget www.sina.com.cn...(打印出sohu的header)www.sohu.com header > HTTP/1.1 200 OKwww.sohu.com header > Content-Type: text/html...(打印出 sina 的 header)www.sina.com.cn header > HTTP/1.1 200 OKwww.sina.com.cn header > Date: Wed, 20 May 2015 04:56:33 GMT... Async / Await 用 asyncio 提供的 @asyncio.coroutine 可把一个 generator 标记为 coroutine 类型,然后在 coroutine 内部用 yield from 调用另一个 coroutine 实现异步操作。

为简化并更好地标识异步 I/O,从 Python 3.5 开始引入了新语法 async 和 await,可以让 coroutine 的代码更简洁易读。请注意,async 和 await 是针对 coroutine 的新语法,即只需要做两步简单的替换:

把 @asyncio.coroutine 替换为 async;

把 yield from 替换为 await.

1234567891011121314# asyncio 原语法书写@asyncio.coroutinedef hello(): print("Hello world!") result = yield from asyncio.sleep(1) print("Hello again!") # 用新语法重新编写async def hello(): print("Hello world!") result = await asyncio.sleep(1) print("Hello again!") # 剩下的代码保持不变 Aiohttp

asyncio 可实现单线程并发 I/O 操作。若仅用在客户端,发挥的威力不大。若在服务器端,例如Web服务器,由于HTTP连接就是 I/O 操作,因此可以用 单线程 + coroutine 实现多用户的高并发支持。

asyncio 实现了 TCP、UDP、SSL 等协议,aiohttp 则是基于 asyncio 实现的 HTTP 框架。

参考资料 [1] Eddie Woo. The RSA Encryption Algorithm. 2017. bilibili.com [2] John cui. 轻松学习RSA加密算法原理. 2018. jianshu.com [3] 廖雪峰. Python 教程. 2018. liaoxuefeng.com [4] Dean J, Ghemawat S. MapReduce: simplified data processing on large clusters [J].Communications of the ACM, 2008, 51(1): 107-113. [5] Wes McKinney. 利用 Python 进行数据分析 [M]. 机械工业出版社, 2013 [6] Python. The Python Standard Library. python.org [7] 渐行渐远silence. Windows 下多版本 Python 安装与 pip 安装和 pip 使用. 2017. csdn.net


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3