笔记 | Python 3 入门系列教程

序言

  • 本文章主要以黑马程序员的「传智播客 Python 就业班 (ij6g)」、「 Python 从入门到精通教程 」和「 廖雪峰的 Python 教程 」为主线,输出学习笔记,目的是检验自己的学习效果和日常复习之需。
  • 本文章也可作为入门 Python 的参考资料,除了视频的基础内容外,文章还会补充视频中讲解不详细或遗漏的必要知识点。
  • 文章的内容和知识框架,与「廖雪峰的 Python 教程」和「传智播客的视频」大体保持一致:
    • 文章以模块分块阐述:Linux 基础Python 基础Python 面向对象项目实战 ( 实战部分以爬虫、数据分析为主的项目实战 )。
    • 每个模块按知识点区分:
      • Linux 基础部分参考 传智播客 Python 从入门到精通教程
      • Python 基础部分参考 廖雪峰 Python 教程
      • 项目实践,即数据分析部分参考书籍 利用 Python 进行数据分析 $^{[5]}$;
  • 最后,引用 Bruce Eckel 的原话作为开篇,Python 的高效只有切身体验才会深有体会。期待您早日加入 Python 队伍中来。

    Life is short, you need 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 进阶
  • Python 实践
    • 📖 | Wes Mckinney.《 利用 Python 进行数据分析 》:豆瓣评分
    • 📖 | Clinton W. Brownley.《 Python数据分析基础 》:豆瓣评分

教学资源

Linux 基础

Linux 常用终端命令

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

  • LS 命令与通配符

    • *:代表任意个数个字符。
    • ?:代表任意一个字符。
    • []:表示可匹配字符组中任意一个。
    • [abc]:匹配 a、b、c 中的任意一个字符。
    • [a-f]:匹配从 a 到 f 范围内的任意一个字符。

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

  • CD 命令与切换目录

    • 相对路径:最前面不是 /~,表示相对 当前目录 所在的目录位置。
    • 绝对路径:最前面是 /~,表示从 根目录 / Home 目录 开始的具体目录位置。

      1
      2
      3
      4
      5
      # 相对路径:返回上两级目录
      cd ../../

      # 绝对路径:相当于 cd /Users/your username/
      cd ~
  • Tree 命令:以树状结构显示文件目录结构,若 tree -d 则显示目录,不显示文件。

  • 查看文件内容

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

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

      1
      2
      # 将字符串 "Hello World" 追加到
      echo "Hello World" >> sample.txt
  • 管道符 |

    • Linux 允许将一个命令的输出通过管道作为另一个命令的输入。
    • ls 命令与 grep 命令的结合使用,如从 Home 目录下搜索包含 “python” 关键字的文件或者文件夹:

      1
      2
      # 从 Home 目录下搜索包含 "python" 关键字的文件或者文件夹
      ls -al ~ | grep python
  • Ifconfig 命令与 Ping 命令

    • ifconfig 命令可查看/配置计算机当前的网卡配置。
    • ping 命令一般用于检测当前计算机到目标计算机之间的网络是否畅通。

      1
      2
      # 快速查看网卡对应的 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 下用来进行 远程拷贝文件 的命令。

    1
    2
    3
    4
    5
    # 从本地复制文件到远程机器桌面上
    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 文件 ):

    1
    2
    3
    4
    Host mac
    HostName 192.168.10.1
    User user
    Port 22
  • 命令输入 ssh mac 即可实现远程登录操作 ( SCP 同样原理 )。

    1
    2
    3
    # 若配置别名后,待验证命令的格式:
    # 是否为: 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

  • 为方便用户管理,提出组的概念。在实际开发中,可预先针对组设置好权限,然后将不同的用户添加到对应组中,从而不用依次为每个用户设置权限。

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 命令:可修改 用户/组文件/目录 的权限。

    1
    2
    # 一次性修改拥有者/组的权限
    chmod +/-rwx 文件名/目录名

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 命令可结合 通配符 一起使用。

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

软链接

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

  • 源文件要使用绝对路径,即便于移动链接文件 (快捷方式) 仍能正常使用。
  • 没有 -s 选项是建立一个硬链接文件。

    1
    ln -s 被链接的源文件 快捷方式的名称
  • 在 Linux 中,文件名和文件的数据是分开储存的。

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

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

打包压缩

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 选项 c:生成档案文件 (.tar)
    # 选项 x:解开档案文件
    # 选项 v:列出归档/解档的详细过程,显示进程
    # 选项 f:指定档案文件名称,选项 f 后应该紧跟 .tar 文件

    # 打包文件:打包放于同一目录下
    tar -cvf 打包文件.tar. 被打包文件路径

    # 解包文件
    tar - xvf 打包文件 [-C 目标路径]
  • targzip 命令结合可实现文件 打包和压缩,即 tar 只负责打包文件, gzip 负责压缩文件。

    1
    2
    3
    4
    5
    6
    7
    8
    # 压缩文件:压缩文件放于同一目录下
    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 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 声明部分
    # 取机器 Path 中指定的第一个 python 来执行脚本
    #!/usr/bin/env python
    # python.py 文件中包含中文字符,Python2 在文件头加入以下语句 ( Python3 是默认支持的 ):
    # -*- coding=utf-8 -*-

    # 代码部分
    print("Life is short, you need python.")

    a = 100
    A = 200

    if a >= 100: # 冒号 ":" 结尾,缩进的语句即为代码块
    print(a)
    else:
    print(-A) # Python 是大小写敏感的

语言基础

注释

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 行注释
    # 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 的形式赋予变量值,但不用赋变量数据类型。
  • 变量的输入与输出:

    1
    2
    3
    high = 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 进制字符串
  • 字符串输入和输出:

    1
    2
    3
    name = input("Enter your name:")
    age = int( input("Enter your age:") )
    print("name: %s, age: %d" % (name, age))
  • 组成字符串的方式:

    1
    2
    3
    4
    5
    6
    7
    8
    str1 = "Hello"
    str2 = "World"

    # str3 组装成 "HelloWorld"
    str3 = str1 + str2

    # 组装成 "===HelloWorld===",此方式常用拼凑字符串
    "===%s===" % (str1 + str2)
  • 字符串下标与取值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    array = "ABCDE"

    print( array[0] ) # 输出 A
    print( array[4] ) # 输出 E
    print( array[-1] ) # 输出 E

    # 切片
    print( array[0:3] ) # 输出 ABC
    print( array[0:-1] ) # 输出 ABCD
    print( array[0:] ) # 输出 ABCDE

    # 即以 2 为步进距离,从下标 0 开始取值至末尾,输出 ACE
    print( 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 的内容拼接起来。

      1
      2
      3
      4
      array = ['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) 结果为 Ture
      2) ('D' not in list) 结果为 Ture
元组 Tuple
  • 有序列表元组 ( Tuple ),与 List 不同,Tuple 一旦初始化就不能修改

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

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

  • 歧义:tuple = (1) 相当于 tuple = 1tuple(-1, ) 才是元组列表。
  • 事实: Tuple 中存储的是 引用

    1
    2
    3
    4
    5
    6
    tuple = ('a', 'b', ['A', 'B'])
    tuple[2][0] = 'X'
    tuple[2][1] = 'Y'

    # 事实上,'A' 和 'B' 被改变为 'X' 和 'Y'
    # 即 Tuple 定义是不变的,只是 Tuple 上存储的 List 为引用
  • 再议不可变对象:replace() 并没有改变字符串的内容,我们理解 str 是变量,abc 是字符串对象。replace() 相当于创建了新的字符串对象 Abc

    1
    2
    3
    str = 'abc'
    print( str.replace('a', 'A') ) # 输出 Abc
    print(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 可看成数学意义上的 无序无重复 元素的集合。

    1
    print( set([1, 1, 2, 3, 4, 4, 5]) ) # 输出 [1, 2, 3, 4, 5]

条件判断

  • 标准条件判断语句:

    1
    2
    3
    4
    5
    6
    7
    if <condition 1>:
    <action 1>
    elif <condition 2>:
    <action 2>
    else:
    if <condition 3>: # if 嵌套
    <action 3>
  • 三元表达式:在 Python 中,可将 if-else 语句放到一行里,语法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    # true-expr 或 false-expr 可以是任何 Python 代码
    value = true-expr if condition else false-expr

    # 上述三元表达式等同于标准条件判断语句的写法
    if condition:
    value = true-expr
    else:
    value = false-expr

循环结构

  • For 循环与 While 循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # For 循环
    names = ['LiMing', 'ZhangWei']
    for name in names
    print(name)

    # While 循环
    sum = 0
    i = 0
    while( i<100 ):
    sum += 1
    i += 1
  • BreakContinue

    • Break:终止 ( 跳出 ) 循环。
    • Continue:中断本次循环。

函数

定义函数

  • 定义函数使用 def 语句,依次写 函数名括号、( 还可以包括 参数 )、冒号。然后是 函数体 ( 需缩进编写 )。

    1
    2
    3
    def FuncName(param):
    <action>
    return [返回参数]
  • 空函数:模块化设计,即先架构后编码。

    1
    2
    def FuncName(param):   
    pass # 占位符:暂不书写代码逻辑
  • 返回多个值:

    1
    2
    3
    4
    5
    6
    def move(x, y):
    x = x + 1
    y = y + 1
    return x, y

    x, y = move(100, 100) # 其实返回的是一个 Tuple,即 (x, y)

函数参数

  • 默认参数 ( 缺省参数 ):最大好处是降低调用函数难度,类似注册时,多数用户只关心核心的信息,即其余信息设置为默认值。

    注意:定义默认参数时,必须指向不变对象。如 n = 2,不能 n = m ( m 为变量 )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def power(x, n = 2):
    s = 1
    while(n > 0):
    n = n - 1
    s = s * x
    return s

    print( power(5) ) # 输出 25
    print( power(5, 3) ) # 计算 5 的 3 次方,输出 125
  • 可变参数:顾名思义,可变参数就是传入的参数个数是可变的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # def calculator(numbers),即理解 numbers 为一个 tuple
    def 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 )。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def 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 中定义函数,可多种参数组合使用,但必须满足一下参数定义顺序:必选参数默认参数可变参数命名关键字关键字参数

    1
    2
    3
    4
    5
    def 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)
  • 结合 tupledict:即通过类似 func(*args, **kw) 形式调用函数。参数虽可自由组合使用,但不要组合太复杂,以造成可理解性较差的结果。

    1
    2
    3
    args = (1, 2, 3)
    kw = {'x' = 5, 'y' = 6}
    func(*args, **kw)

递归函数

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

    1
    2
    3
    4
    def 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 次,都只占用一个栈帧,不会出现栈溢出的情况。

      1
      2
      3
      4
      5
      6
      7
      def 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 )。

    1
    2
    3
    4
    list = [11, 22, 33, 44, 55]

    # 输出 [11, 22, 33],即从小标为 0 开始,步进为 1,取前 3 个元素
    print( list[0:3:1] )
  • 倒数切片:

    1
    2
    3
    4
    list = ['A', 'B', 'C', 'D', 'E']

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

    1
    2
    3
    4
    str = 'ABCDE'

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

迭代

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

    1
    2
    3
    4
    list = ['A', 'B', 'C', 'D', 'E']

    for str in list:
    print(str) # 输出 ABCDE
  • enumerate 函数可以把一个 list 变成 索引-元素树,这样就可以在 For 循环中同时迭代 索引元素本身

    1
    2
    3
    4
    list = ['A', 'B', 'C', 'D', 'E']

    for i, value in enumerate(list):
    print(i, value)

列表生成式

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    list1 = []

    list1 = [x**2 for num in range(1, 10)]
    # 输出 1x1,2x2,3x3, ..., 9x9
    print(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, 8x8
    print(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:把列表生成式的 [] 改成 () 即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    # 受到内存限制,运行过程中可能会崩掉
    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

    1
    2
    3
    4
    5
    6
    # 使用 isinstance() 判断一个对象是否为 Iterable 对象
    form collections import Iterable

    isinstance([], Iterable) # True
    isinstance((x for x in range(1, 10)), Iterable) # True
    isinstance(100, Iterable) # False
  • 生成器是 Iterator 对象;List、Dict、Str 虽然是 Iterable 对象,但却不是 Iterator
    我们可以通过 iter() 函数,把 List、Dict、Str 等 Iterable 转换达成 Iterator

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

    1
    2
    isinstance(iter([]), Iterator) # True
    isinstance(iter('abc'), Iterator) # True

函数式编程

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

高阶函数

  • 变量可指向函数:

    1
    2
    3
    4
    5
    6
    7
    8
    # 直接调用函数
    x = abs(-10)

    # 变量可指向函数
    f = abs
    x = f(-10)

    # x 的结果都为 10
  • 函数名也是变量:函数名其实就是指向函数的变量。

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

    1
    2
    3
    4
    5
    6
    7
    8
    abs = 10
    abs(-1)

    # 抛出异常
    # 即 abs 已指向一个整数 10,而不是指向求绝对值的函数。
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    TypeError: 'int' object is not callble
  • 传入函数:一个函数接收另一个函数作为参数,称为 高阶函数

    1
    2
    3
    4
    5
    6
    7
    8
    def 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 返回。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 例 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 )

    1
    2
    3
    4
    5
    from functools import reduce
    def add(x, y):
    return x + y

    print( reduce(add, [1, 2, 3, 4, 5]) )
  • 当然,上述的实例只是为了描述原理而设定,下面将结合 map() 与 reduce() 举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from 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() 函数获得所有结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 在一个 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()

    # 输出 ABC
    list( filter(rejectBlankStr, ['A', 'B', '', None, 'C']) )
Sorted
  • 排序算法:排序的核心是 比较两元素的大小。若是数字则直接比较;但比较的若是字符串或两个字典,则比较过程需通过函数抽象实现。

    1
    2
    # 输出 [-6, 2, 12, 24, 36]
    print( sorted( [36, 24, -6, 12, 2] ) )
  • sorted() 也是一高阶函数,可接收一个 key 函数来自定义排序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 输出 [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) )

返回函数

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # 通常情况实现一个可变参数的求和
    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() 才执行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    def count():
    fs = []
    for i in range(1, 4):
    def f():
    return i ** 2
    fs.append(f)
    return fs

    f1, f2, f3 = count()
    # 输出 9::9::9
    print( 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::9
    print( str(f1()) + '::' + str(f2()) + '::' + str(f3()) )

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

匿名函数

  • 当函数作为 传入参数 时,我们不需要显式地定义函数,直接传入匿名函数更便捷。
  • 关键字 lambda 表示匿名函数,冒号前面表示传入参数,后面为返回值 ( 一般为表达式运算后的结果 ),如 lambda x, y : x+y

    1
    2
    3
    4
    5
    6
    7
    # 以 map() 函数为例
    # 输出 [1, 4, 9, 16, 25]
    print( list(map(lambda x : x ** 2, [1, 2, 3, 4, 5])) )

    # 匿名函数实际为:
    def f(x):
    return x ** 2
  • 匿名函数有一好处,即不必担心 函数名冲突。此外,匿名函数也是一个函数对象,可把匿名函数赋值给一个变量,再利用变量来调用。

    1
    2
    f = lambda x : x ** 2
    print( f(5) ) # 输出 25
  • 匿名函数作为返回值返回:

    1
    2
    def build(x, y):
    return lambda: x * x + y * y

装饰器

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def log(func):
    def wrapper(*args, **kw):
    print('call %s():' % func.__name__)
    return func(*args, **kw)
    return wrapper

    @log
    def time():
    print('2018-11-11 23:11')
  • 那么装饰器是如何实现的?在实现装饰器之前,我们有必要回顾函数的特性:

    • 函数作为变量传递:函数作为变量来传递,代表的是一个函数对象。若函数不加括号,是不会执行的;
    • 函数作为参数传递:一个函数可以接受另一个函数对象作为自己的参数,并对函数对象进行处理;
    • 函数作为返回值:一个函数的返回值可以是另一个函数对象。
    • 函数嵌套及跨域访问:一个函数 (主函数) 内部是可以嵌套另一个函数 (子函数) 的;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      # 函数作为变量传递
      def add(x):
      return x + 1
      a = add # 作为变量

      # 函数作为参数传递
      def add(x):
      return x + 1
      def excute(f):
      return f(3)
      excute(add) # 作为参数

      # 函数作为返回值
      def add(x):
      return x + 1
      def 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()。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      def func():
      return '函数 func'

      def outer(x):
      def inner(): # 函数嵌套
      return '戴了 inner 帽子的' + x() # 跨域访问,引用了外部变量 x
      return inner # 函数作为返回值

      # 函数 func 作为 outer 的参数,函数作为变量赋给 closure
      closure = outer(func)

      print( func() ) # 执行原始函数
      print( closure() ) # 执行闭包

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def outer(x):
    def inner():
    return '戴了 inner 帽子的' + x()
    return inner

    @outer
    def func():
    return '函数 func'

    print( func() ) # 输出:戴了 inner 帽子的函数 func

偏函数

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    print( int('12345') ) # 输出 12345

    # int() 函数提供额外 base 参数,默认值为 10
    # 若传入 base 参数即可做 N 进制转换 ( N 进制转到 10 进制 )
    print( int('10', base = 8) ) # 输出 8
    print( int('A', base = 16) ) # 输出 10

    # 若我们要转换大量二进制字符串,则可通过定义函数
    def int2(x, base = 2):
    return int(x, base)

    # 这样转换二进制就非常便捷了
    print( int2('10000000') ) # 输出 128
    print( int2('10101010') ) # 输出 170
  • 其实 functools.partial 就是帮助我们创建一个偏函数,即其作用就是把一个函数的某些参数固定住 ( 设置默认值 ),返回一个新函数。

    1
    2
    import functools
    int2 = functools.partial(int, base = 2)
  • 创建偏函数时,实际可接收 函数对象*args**kw 这三个参数。

    1
    2
    3
    4
    5
    6
    int2 = 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

      1
      2
      3
      4
      mypython
      ├─ __init__.py
      ├─ sample.py
      └─ example.py

使用模块

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import sys

    def test():
    args = sys.argv
    if len(args) > 2:
    for str in args:
    print('%s' % str)
    else:
    print('Empty paramter')
  • 作用域:在一模块中,我们可能会定义很多函数和变量。或许我们有这样的需求:有的函数和变量仅希望是在模块内部使用。Python 中通过 _ 前缀实现的。

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

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

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      def __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,出现以下信息即安装成功,可正常导库使用:

    1
    2
    3
    python
    python 3.x.x | Anconda, Inc. | ... on darwin
    >>> import numpy # 直接倒入第三方模块即可

模块搜索路径

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

    1
    2
    3
    4
    >>> import mymodule
    Traceback (most recent call last)
    File "<stdin>", line 1, in <module>
    ModuleNotFoundError: No module named 'mymodule'
  • 默认情况,Python 解释器会搜索当前目录,所有已安装内置模块和第三方模块,搜索路径 存放在 sys 模块path 变量 中:

    1
    2
    3
    4
    # 若需要添加搜索目录
    import sys
    sys.path.append('/User/kofe/mypython')
    print(sys.path) # 查看是否已添加

面向对象编程

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

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

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class 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 类,实例即根据类创建出一个个具体的对象 stu1stu2
  • 通过 class 关键字定义类:

    1
    2
    3
    # 若没有合适的继承类,则默认使用 object 类,这是所有类最终都会继承的类
    class Student(object):
    pass
  • 创建类的实例,如 stu = Student()

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

    1
    2
    3
    4
    5
    6
    7
    class Student(object):
    def __init__(self, name, score):
    self.name = name
    self.score = score

    #创建实例时,不需要传入 self,即实例本身
    stu = Student('Lucy', 95)
  • 数据封装:访问实例本身的数据,不通过外部函数访问,而是通过类的内部定义访问数据的函数,这样实现数据封装。

    1
    2
    3
    class Student(object):
    def printInfo(self):
    print('name: %s, score: %s' % (self.name, self.score))

访问限制

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    class 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 <= score <= 100:
    self.__score = score
    else:
    raise ValueError('Bad Score')

    stu = Student('Bob', 90)

    # 直接访问会报错误:
    # AttributeError: 'Student' object has no attribute '__name'
    print( stu.__name )

    # 实现数据封装后
    # 使用 Getter 函数访问属性
    print( stu.getName() )
    # 使用 Setter 函数修改属性
    stu.setName('Lucy')
  • 在 Python 中,类似 __xxx__ 的变量名,是 特殊变量,可直接访问。

  • 在 Python 中,私有变量 _xxx__xxx,也是可以外部访问的。其实 Python 编译器是会把变量名修改为 _类名__变量名,致使直接访问报错。例如:_Student__name,通过 stu. _Student__name 可实现外部访问 “私有变量”。

    ⚠️ 当然,我们不建议这样做。因为不同版本的 Python 解释器可能会把 __xxx 改成不同变量名称。我们还是按照 约定俗成 的规定,视 __xxx 为私有变量。

继承和多态

  • 继承:在面向对象程序设计中,可从某个现有类继承,新的类称为 子类,被继承的类称为 基类、父类或超类,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Animal(object):
    def run():
    print('Animal is running...')

    # Animal 实现了 run() 方法,Dog 继承 Animal 类
    # Dog 作为子类自然也拥有了 run() 方法
    class Dog(Animal):
    def run():
    print('Dog is running...')

    # 当然,子类还可以重写方法和增加方法
    class Cat(Animal):
    def run():
    print('Cat is running...')
    def call():
    print('Miao, Miao, Miao...')
  • 多态:把不同子类对象都当作父类来看,可屏蔽不同子类对象之间的差异,写出通用代码。具体地,我们可从实例中理解多态:

    1
    2
    3
    4
    5
    6
    7
    def runTwice(animal):
    animal.run()
    animal.run()

    runTwice( Animal() ) # 输出:Animal is running...
    runTwice( Dog() ) # 输出:Dog is running...
    runTwice( Cat() ) # 输出:Cat is running...

获取对象信息

  • type() 函数:可判断对象类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 基本类型
    type(12345) # <class 'int'>
    type('hello') # <class 'str'>
    type(None) # <type(None) 'NoneType'>

    # 变量指向函数或者类
    type(abs) # <class 'builtin_function_or_method'>

    # type() 函数返回类型
    type(12345) == int # True
    type('HelloWorld') == str # True

    # 判断其他类型
    import types
    type(abs) == types.BuiltinFunctionType # True
    type(lambda x:x) == types.lambdaType # True
    type( (x for x in range(10)) ) == types.GeneratorType # True
  • isinstance() 函数

    • 判断基本类型:isinstance('abc', str)isinstance(b'a', bytes)
    • 判断 class 类型:如有继承关系,如 object -> Animal -> Dog,则有:
      a = Animal() => isinstance(a, Animal) =>True
      d = Dog() => isinstance(d, Animal) =>True
      d = Dog() => isinstance(d, Dog) =>True
  • dir() 函数:若要获得一个对象的 所有属性和方法,可使用该函数。它返回一个包含字符串的 list。例如,获得一个 str 对象的所有属性和方法。

    1
    2
    # 输出:['__add__', '__class__', ... 'zfill']
    dir('abc')
  • 类似 __xxx___ 的属性和方法在 Python 中都有特殊用途。如 __len__() 方法返回长度。调用 len() 函数,在函数内部实际是它自动地去调用该对象的 __len__() 方法,故下面代码是等价的。

    1
    len('abc') == 'abc'.__len__() # 输出 True
  • 仅仅把属性和方法列出来是不够的,配合 getattr()setattr()hasattr(),我们可直接操作一个 对象的状态

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Retangle(object):
    def __init__(self):
    self.x = x
    self.y = y
    def area(self):
    return self.x * self.y

    rectangle = Rectangle(5, 10)

    hasattr(rectangle, 'z') # 是否含有属性 z
    setattr(rectangle, 'z', 1) # 设置一个属性 z,令其等于 1
    getattr(rectangle, 'z') # 获取属性 z

    # 也可以获得对象方法
    if hasattr(rectangle, 'area'):
    fn = getattr(rectangle, 'area')

实例属性和类属性

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Student(object):
    # self 变量赋值
    def __init__(self, name, score):
    self.name = name
    self.score = score

    stu = Student('Bob', 80)
    # 实例变量赋值
    stu.gender = 'male'
  • 给类绑定属性,直接在 class 中定义属性即可。

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

    1
    2
    3
    4
    5
    class Student(object):
    grade = 'postgraduate'

    stu = Student('Lucy', 95)
    print( stu.grade ) # 与 print(Student.grade()) 效果相同

面向对象高级编程

使用 @property

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Student(object):
    # Getter 方法
    def getScore(self):
    return self.__score

    # Setter 方法
    # setXXX() 方法还可书写规则以约束输入数据或检查数据
    def setScore(self, score):
    if 0 <= score <= 100:
    self.__score = score
    else:
    raise ValueError('Bad Score')

    stu = Student()
    stu.set_score(90)
    print( stu.get_score() )
  • 改进:在操作逻辑层面,Python 还提供了更多特性,既直接 调用变量的方式操作属性,又不破坏数据的封装特性,@property 装饰器 的作用就在于此。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Student(object):

    @property
    def score(self):
    return self.__score

    @score.setter
    def score(self, value):
    if not isinstance(value, int):
    raise ValueError('Score must be an integer!')
    if value < 0 or value > 100:
    raise ValueError('score must between 0 ~ 100!')
    self.__score = value

    stu = Student()
    stu.score = 90 # 实际转化为 s.set_score(90)
    print( stu.score ) # 输出 90

多重继承

  • 继承是面向对象编程的一个重要的特性。通过继承,子类可以扩展父类的功能。
  • 在 Python 中,多实现多重继承,子类就可同时获得多个父类的所有功能。这种设计模式也叫 MixIn

    ⚠️ 同样是面向对象编程的语言,Java 只允许单继承,即一个类最多只能显示地继承于一个父类。当然,Java 要获得更多 “属性能力”,也可通过实现接口的方式实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 需求:我们赋予不同动物不同的能力

    class Walking(object):
    def walk(self):
    print('Walking...')

    class Swimming(object):
    def swim(self):
    print('Swimming...')

    class Flying(object):
    def fly(self):
    print('Flying...')

    # 定义一双栖动物:通过继承父类,从而获得对应能力
    class Amphibian(Walking, Swimming):
    pass

    # 定义一只天鹅:能走能飞能游泳
    class Swan(Walking, Flying, Swimming):
    pass

定制类

  • 形如 __xxx__ 的变量或者函数名在 Python 中是有特殊用途的。例如:__slots__ 用于限制能绑定的属性,__len__() 方法返回对象本身的长度。

    除此之外,Python 的 class 中还有许多这样有特殊用途的 属性函数,可帮助我们定制属性和定制类。

__slots__
  • 当我们创建一 class 实例后,可给该实例绑定 任何 属性和方法,这正体现了动态语言的灵活性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Student(object):
    pass

    # 创建实例
    stu = Student()

    # 绑定属性
    stu.name = 'Bob'
    stu.score = 80

    # 绑定方法
    from types import MethodType

    def setAge(self, age):
    self.age = age
    stu.setAge = MethodType(setAge, stu) # 给实例绑定方法
    stu.setAge(25)

    # 上述方式只对本实例对象有效,若要所有实例对象起效,则需给 class 绑定方法
    def setGrage(self, grade):
    self.grade = grade
    Student.setGrage = setGrage
  • 动态绑定:允许我们在程序运行的过程中动态给 class 添加功能 (方法)。

  • 限定实例的属性:定义特殊变量 __slots__,可限制 class 实例能添加的属性。

    • 当子类定义了 slots 时,子类会继承父类的 slots,那么子类实例能添加的属性是子类与父类 slots 的 并集
    • 当子类定义中没有 slots 时,父类的 slots 对子类不起作用。

      1
      2
      3
      4
      5
      class Student(object):
      __slots__ = ('name', 'score', 'gender', 'age')

      stu = Student()
      stu.email = 'admin@kofes.cn' # email 不在限定内,会报 AttributeError 错误
__getattr__
  • 正常情况下,当我们调用类的方法或属性,若不存在则会报错。例如定义 Student 类:

    1
    2
    3
    4
    5
    6
    7
    class Student(object):
    def __init__(self):
    self.name = 'Bob'

    stu = Student()
    print(stu.name) # 输出 Bob
    print(stu.score) # 没有对应属性故会报 AttributeError 错误
  • 要避免这个错误,除了补上 score 属性外,Python 还有另一个机制,即通过 __getattr__() 方法,动态返回一个属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Student(object):
    def __init__(self):
    self.name = 'Bob'
    def __getattr__(self, attr):
    if 'score' == attr:
    return 90

    # 注意:只有在没有找到属性的情况下,才调用 __getattr__
    # 已有的属性,不会在 __getattr__ 中查找
    print(stu.score) # 输出 90

    # 注意:若在 __getattr__ 也没有匹配属性,则返回 None
    # __getattr__ 默认返回 None
    print(stu.age) # 输出 None
  • 要让 class 只响应特定的几个属性,我们就要按照约定,抛出 AttributeError 错误即可:

    1
    2
    3
    4
    5
    6
    7
    8
    class Student(object):
    def __init__(self):
    self.name = 'Bob'
    def __getattr__(self, attr):
    if 'score' == attr:
    return 90
    raise AttributeError(
    '\'Student\' object has no attribute \'%s\'' % attr)
__iter__
  • 若想让一个类用于 for ... in 循环,类似 list 或 tuple 那样,就必须实现一个 __iter__() 方法,该方法返回一个 迭代对象,然后 Python 的 For 循环就会不断调用该迭代对象的 __next__() 方法拿到循环的下一个值,直到遇到 StopIteration 错误时退出循环。

    我们以斐波那契数列为例,写一个 Fib 类作用于 For 循环 :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Fib(object):
    def __init__(self):
    self.a, self.b = 0, 1 # 初始化两个计数器 a,b

    # 方法重写
    def __iter__(self):
    return self # 实例本身就是迭代对象,故返回自己

    # 方法重写
    def __next__(self):
    self.a, self.b = self.b, self.a + self.b # 计算下一个值
    if self.a > 100000: # 退出循环的条件
    raise StopIteration()
    return self.a # 返回下一个值

    # Fib 实例作用于 For 循环:
    for n in Fib():
    print(n)
  • 对于定制类,我们让其实现了 __iter__()__next__() 方法,那么它就是一个 Iterator 类型的,这正是动态语言的特性。

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class 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 类型的枚举类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    from enum import Enum
    Month = 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.Jan
    print( Month(1) ) # 输出 Month.Jan
    print( Month['Jan'] ) # 输出 Month.Jan
    print( Month.Jan.value ) # 输出 1
  • 若有需求,我们可精确地控制枚举类型,即从 Enum 派生出自定义类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from 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。同理,我们设计函数时,也可相仿地设置返回代码。

    1
    2
    3
    4
    5
    6
    7
    RESULT_OK = 0
    RESULT_FALSE = -1

    def test():
    if false:
    return RESULT_FALSE
    return RESULT_OK
异常错误
  • 高级语言通常都内置了一套 try...except...finally... 的错误处理机制,Python 也不例外,使用方法见实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    try:
    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 时需要注意的是:它不但捕获该类型的错误,还把其子类也 “一网打尽”,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try:
    foo()
    except ValueError as e:
    print('ValueError')
    except UnicodeError as e:
    print('UnicodeError')

    # 假设 foo() 函数运行错误,则输出 "ValueError"
    # 第二个 except 永远也捕获不到 UnicodeError
    # 因为 UnicodeError 是 ValueError 的子类,即异常被第一个 except 给捕获了
调用栈
  • 在函数嵌套调用中,若错误没有被捕获,它就会一直往上抛,直至被 Python 解释器捕获,并打印一个错误信息然后程序退出。因此当发生错误时,一定要分析错误的 调用栈 信息,定位错误的位置,找出 错误根源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # err.py
    # 定义函数
    def foo(src):
    return 10 / int(src)
    def bar(src):
    return foo(src) * 2
    def main():
    bar('0')

    main() # 调用函数

    # 抛出异常错误,错误的跟踪信息如下:
    Traceback (most recent call last):
    File "err.py", line 11, in <module>
    main()
    File "err.py", line 9, in main
    bar('0')
    File "err.py", line 6, in bar
    return foo(s) * 2
    File "err.py", line 3, in foo
    return 10 / int(s)
    ZeroDivisionError: division by zero
抛出异常
  • 异常类型属于 class,捕获一个异常就是捕获到该 class 的一个实例。因此,异常并不是凭空产生而是 有意 创建并抛出的。Python 的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出异常。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 只有在必要的时候才定义我们自己的错误类型
    # 尽量使用 Python 内置的错误类型,例如 ValueError,TypeError
    class FooError(ValueError):
    pass

    def foo(s):
    n = int(s)
    if 0 == n:
    raise FooError('invalid value: %s' % s)
    return 10 / n

    foo('0')

调试

  • 推荐 IDE 调试,即设置断点、单步执行,就需要一个支持调试功能的 IDE。目前比较好的 Python IDE 有: PyCharmEclipse vs pyDev

单元测试

  • 单元测试是用来对一个 模块函数 来进行正确性检验的 测试 工作。
  • 这种以 测试驱动 的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的 测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。
  • 为了编写单元测试,我们需要引入 Python 自带的 unittest 模块。以下为一个 单元测试 的示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    import unittest

    # 继承unittest.TestCase
    class 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()
  • 下面是一些常用的断言,也就是校验结果:

    1
    2
    3
    4
    5
    6
    7
    8
    assertEqual(a, b)		# a == b
    assertNotEqual(a, b) # a != b
    assertTrue(x) # bool(x) is True
    assertFalse(x) # bool(x) is False
    assertIsNone(x) # x is None
    assertIsNotNone(x) # x is not None
    assertIn(a, b) # a in b
    assertNotIn(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() 函数,传入文件名和标示符:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    try:
    # 以只读方式读入 test.txt 文件
    file = open('/Users/kofe/test.txt', 'r', encoding='utf-8')

    # 若文件不存在,则抛出 IOError 的错误
    # Traceback (most recent call last):
    # File "<stdin>", line 1, in <module>
    # 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 模式打开文件即可:

    1
    2
    3
    4
    5
    file = open('/Users/kofe/test.jpg', 'rb')
    print( file.read() )

    # 输出十六进制表示的字节:
    b'\xff\xd8\xff\x18Exif\x00...'
写文件
  • 写文件和读文件是一样的,唯一区别是调用 open() 函数时,传入标识符 w 或者 wb 表示写 文本文件写二进制文件

    1
    2
    3
    4
    # 写入文件后,务必要调用 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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    from io import StringIO
    file = StringIO()
    file.write('Welcome to\n Python World!')

    while True:
    s = file.readline()
    if '' == s:
    break
    print( s.strip() )

    # 输出结果:
    Welcome to
    Python World!
BytesIO
  • StringIO 操作的只能是 str,如果要操作二进制数据,就需要使用 BytesIO。
  • BytesIO 实现了在内存中读写 bytes。

    1
    2
    3
    4
    5
    6
    7
    from io import BytesIO
    file = 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 模块,可以直接调用操作系统提供的 接口函数

    1
    2
    3
    4
    5
    6
    import os

    # 现实操作系统类型
    # posix:Linux、Unix 或 Mac OS X
    # nt:Windows
    print( os.name )
环境变量
  • 在操作系统中定义的环境变量,全部保存在 os.environ 这个变量中,可直接查看:

    1
    2
    3
    4
    5
    6
    7
    import os

    # 操作系统中定义的环境变量,全部保存在os.environ 变量中
    os.environ

    # 获取某个环境变量的值:os.environ.get('key')
    os.environ.get('PATH')
操作文件和目录
  • 操作文件和目录的函数一部分放在 os 模块中,一部分放在 os.path 模块中。
  • 查看、创建和删除目录可以这么调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # 查看当前目录的绝对路径
    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
  • 文件操作:

    1
    2
    3
    4
    5
    6
    7
    8
    # 对文件重命名
    os.rename('test.txt', 'test.py')

    # 删掉文件
    os.remove('test.py')

    # 然而在 os 模块中没有关于复制的函数
    # 借助 shutil 模块提供了copyfile() 的函数实现复制 (os 模块的补充)
  • 利用 Python 的特性操作文件或目录:

    1
    2
    # 列出当前目录下的所有目录
    [x for x in os.listdir('.') if os.path.isdir(x)]

序列化

  • 在程序运行期间,变量都是在内存中存放的。当 程序结束,变量所占用的内存将被操作系统 全部回收。若在程序运行期间,有需求保存 变量 或者 对象数据状态信息,以待下次启动程序时可直接加载该变量或对象。
  • 序列化:将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。
  • 反序列化:可通过从存储区中读取或反序列化对象的状态,重新创建该对象。
  • 在 Python 中,序列化称为 pickling,在其他语言中也被称为 serializationmarshallingflattening 等。Python 提供了 pickle 模块来实现序列化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import 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 格式的转换:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import json
    dict = {'name': 'Bob', 'age': 25, 'score': 90}

    # dump() 方法可以直接把 JSON 写入一个 File-like Object
    # dumps() 方法返回一个字符串,内容就是标准的 JSON
    json_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 类 ),再序列化该对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import json

    class 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 序列化:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 默认情况下 dumps() 不知道如何将 Student 实例变为 JSON 的 {"key": value}
    # 我们只需为 Student 实例专门写一个转换 (组装) 函数
    def student2dict(std):
    return {
    'name': std.name,
    'age': std.age,
    'score': std.score
    }

    # 这样,Student 实例首先被 student2dict() 函数转换成 dict,再被序列化为 JSON
    print( json.dumps(stu, default = student2dict) )
  • 当然,若我们遇到一个 Teacher 类的实例,照样无法序列化为 JSON。其实可以通过一种 通用方法 将任意 class 的实例变为 dict。

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

    1
    print( json.dumps(s, default = lambda obj: obj.__dict__) )
  • 同理,我们需要把 JSON 反序列化为一个 Student 实例对象,loads() 方法首先转换出一个 dict 对象,然后我们传入的 object_hook 函数,其负责把 dict 转换为 Student 实例对象。

    1
    2
    3
    4
    5
    def 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 模型的区别,同步模型即按普通顺序写执行代码:

    1
    2
    3
    4
    5
    6
    7
    8
    do_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 模型 需要一个 消息循环。在消息循环中,主线程不断地重复 读取消息 / 处理消息 这一过程:

    1
    2
    3
    4
    loop = get_event_loop()
    while True:
    event = loop.get_event()
    process_event(event)
协程

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

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

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

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      def A():
      print('1')
      print('2')

      def B():
      print('x')
      print('y')

      # 若由协程执行,在执行 A 的过程中可随时中断去执行 B
      # B 也可能在执行过程中中断再去执行 A,则执行结果有:
      1
      x
      y
      2
  • 从上述例子结果可看出,A、B 的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

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

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

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

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

    • 传统 生产者 - 消费者 模型是:一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但有很大机率出现 死锁
    • 若改用协程,生产者生产消息后,直接通过 yield 跳转到消费者开始执行,待消费者执行完毕后切换回生产者继续生产。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      def 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
  • asyncioPython 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,因此实现了 并发执行

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      import asyncio

      @asyncio.coroutine
      def hello():
      print("Hello world!")
      # 异步调用 asyncio.sleep(1):
      r = yield from asyncio.sleep(1)
      print("Hello again!")

      # 获取 EventLoop
      loop = asyncio.get_event_loop()
      # 执行 coroutine
      loop.run_until_complete(hello())
      loop.close()
  • 举一反三:我们尝试用 Task 封装两个 coroutine:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import threading
    import asyncio

    @asyncio.coroutine
    def 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! (<_MainThread(MainThread, started 140735195337472)>)
    Hello world! (<_MainThread(MainThread, started 140735195337472)>)
    (暂停约 1 秒)
    Hello again! (<_MainThread(MainThread, started 140735195337472)>)
    Hello again! (<_MainThread(MainThread, started 140735195337472)>)
  • 具体应用场景:我们 asyncio 的异步网络连接来获取 sina、sohu 的首页。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import asyncio

    @asyncio.coroutine
    def 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 OK
    www.sohu.com header > Content-Type: text/html
    ...
    (打印出 sina 的 header)
    www.sina.com.cn header > HTTP/1.1 200 OK
    www.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 开始引入了新语法 asyncawait,可以让 coroutine 的代码更简洁易读。请注意,async 和 await 是针对 coroutine 的新语法,即只需要做两步简单的替换:

    • @asyncio.coroutine 替换为 async
    • yield from 替换为 await.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      # asyncio 原语法书写
      @asyncio.coroutine
      def 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 框架。

参考资料