- 1.开启python之旅
- 解释器
- 脚本文件
- 代码风格建议
- 注释
- 模块
- 总结
- 2.变量和数据类型
- 关键字和标识符
- 从键盘读取输入
- 代码示例
- 单行定义多个变量或复制
- 总结
- 3.运算符和表达式
- 运算符
- 表达式
- 总结
- 程序挑战:圆的面积
- 挑战:圆的面积
- 4.控制流if-else
- if语句
- else语句
- 真值检验
- 总结
- 5.循环
- while循环
- 列表
- for循环
- comtinue语句
- 循环的else语句
- 棍子游戏
- 总结
- 6.数据结构
- 列表
- 元组
- 集合
- 字典
- 程序示例
- 总结
- 7.字符串
- 字符串的表示
- 字符串的方法
- 字符串剥离
- 文本搜索
- 回文检查
- 单词计数
- 总结
- 8.函数
- 定义一个函数
- 局域或全部变量
- 默认参数值
- 关键字参数
- 强制关键字参数
- 文档字符串
- 高阶函数
- 总结
- 9.文件处理
- 文件操作
- 文件操作示例程序
- 使用with语句
- 实现lscpu命令
- 总结
- 挑战:字符串操作
- 10. 异常
- 异常
- 处理异常
- 抛出异常
- 定义清理行为
- 总结
- 挑战:玩转函数
- 11.类
- 实验步骤
- 定义类
- \init_方法
- 继承
- 多继承
- 删除对象
- 属性(attributes)读取方法
- 装饰器
- 总结
- 12.模块
- 模块
- 包
- 默认模块
- argparse 命令行参数处理模块
- TAB补全
- 总结
- 13.Collections 模块
- Counter
- defaultdict
- namedtuple
- 总结
- 挑战:类和Collection
- 14.PEP8 代码风格指南
- 实验步骤
- 介绍
- 愚蠢的一致性就像没脑子的妖怪
- 代码排版
- 字符串引号
- 表达式和语句中的空格
- 注释
- 版本注记
- 命名约定
- 公共和内部接口
- 编写程序的建议
- 总结
- 15.迭代器、生成器、装饰器
- 迭代器
- 生成器
- 生成器表达式
- 闭包
- 装饰器
- 总结
- 16.Virtualenv
- 安装 virtualenv
- 用法
- 总结
- 17.测试
- 测试范围
- 单元测试
- 第一个测试用例
- 2.7异常测试
- mounttab.py
- 测试覆盖率
- ">

- 总结
- 18.项目结构
- 创建Python项目
- 总结
- 19.Flask介绍
- 基本概念
- “Hello World”应用
- Flask中使用参数
- 额外工作
- 总结
https://www.shiyanlou.com/courses/596/learning/
1.开启python之旅
实验介绍
本课程基于Python for you and me教程翻译制作,其中参考了Python tutorial和The Python Standard Library,并对原教程的内容进行了改进与补充。
相关链接地址如下:
- Python tutorial:http://www.pythondoc.com/pythontutorial3/index.html
- Python for you and me:http://pymbook.readthedocs.io/en/latest/
- The Python Standard Library:https://docs.python.org/3/library/index.html
Python for you and me的法律声明有如下内容:
Permission is granted to copy, distribute, and/or modify this document under the terms of the Open Publication Licence, Version 1.0, or any later version.
知识点
- 使用交互模式的 Python3 解释器
- 简单使用 vim 编写 Python3 脚本
- 执行 Python3 脚本
- Python3 代码风格建议
- Python3 中使用注释
- 认识 Python3 模块
实验环境
- Python 3.5
- Xfce 终端
- Vim
适合人群
本课程属于初级级别课程,不仅适用于那些有其它语言基础的同学,对没有编程经验的同学也非常友好。
- 如果你是第一次接触实验楼,花几分钟过一下这个课程可以帮助你适应实验楼用户的界面:新手指南之玩转实验楼
- 如果你是以前没有在使用过 Linux,这个课程会让你在实验中更得心应手:Linux 基础入门(新版)
解释器
Python 是一个脚本语言,你可以在 Python 解释器中直接写代码或者将代码写到一个文件里,然后执行这个文件(即脚本文件)。
首先,我们从使用解释器开始,打开 Xfce 终端,键入python3并回车。这个时候解释器便工作于交互模式。


作为我们第一个 Python 代码,我们将打印经典的 “Hello World!”,输入python3,如下所示:
>>> print("Hello World!")Hello World!

欣赏完自己的杰作后,按Ctrl + D输入一个 EOF 字符来退出解释器,你也可以键入exit()来退出解释器。
脚本文件
作为正式的程序员,你可能想要将上述的代码写到一个源文件。那么我们来创建一个 helloworld.py 文件,用你喜欢的任何编辑器来创建这个文件,我使用 Vim ,你可以使用基于 GUI 的工具,如实验楼环境桌面上的 gedit。
我们建议使用 Vim,在这里我们简单的使用它,你可以在实验楼的课程Vim编辑器里深入学习或者通过这张vi(vim)键盘图帮助你记忆:

(原图来自:www.viemu.com)
首先,创建的文件会放在/home/shiyanlou目录下,打开 Xfce 终端,键入vim helloworld.py来启动 Vim 并编辑 helloworld.py,启动后不要乱按键。

然后按i键进入插入模式,此时你可以看到左下角有 “插入” 两个字,现在你可以键入下面的代码了。
#!/usr/bin/env python3print("Hello World!")
其中第一行的前两个字符#!称为Shebang,目的是告诉 shell 使用 Python 解释器执行其下面的代码。

然后按Esc键退出插入模式,再键入:wq回车,Vim 就会保存文件并退出。
要运行脚本文件helloworld.py,还要为文件添加可执行权限(否则会出现权限不足的错误):
$ chmod +x helloworld.py
在这里使用到了linux下的chmod命令来变更文件或目录的权限,
+x代表赋予了其可执行的权限。
若想要了解更多,推荐你学习我们的Linux基础入门(新版)。
你也可以点击此处快速掌握常用 Unix/Linux 常用命令查看。
然后执行脚本文件:
$ ./helloworld.py

这里需要注意如果程序中没有#!/usr/bin/env python3的话,应该使用python3 helloworld.py来执行,否则使用./helloworld.py程序会被当作 bash 脚本来执行,最终报错。
代码风格建议
在 Python 中,空格很重要,我们使用空格将标识符区分开,行开始处的空格我们称之为缩进,如果你的缩进是错误的,Python 解释器会抛出错误。例如下面情形:
>>> a = 12>>> a = 12File "<stdin>", line 1a = 12^IndentationError: unexpected indent
这是因为第二行开始处出现了多余的空格,所以需要经常检查代码的缩进。
如果你在代码中混用制表符和空格,这种缩进错误会很常见。
所以如果是用空格,就一直用空格缩进,不要使用制表符。
建议遵守以下约定:
- 使用 4 个空格来缩进
- 永远不要混用空格和制表符
- 在函数之间空一行
- 在类之间空两行
- 字典,列表,元组以及参数列表中,在
,后添加一个空格。对于字典,:后面也添加一个空格 - 在赋值运算符和比较运算符周围要有空格(参数列表中除外),但是括号里则不加空格:
a = f(1, 2) + g(3, 4)
注释
注释是一些文本用来解释此处代码是做什么的,写注释是为了使他人更容易理解代码。
Python 的注释以#字符开始的,在#字符到行尾之间的所有东西都被程序忽略为注释,也就说对程序没有任何影响。
>>> # 这是一个注释>>> # 下面这一行是求两数之和>>> a = 12 + 34>>> print(a) # 这里也是注释 :)
注释主要面向的人群是开发人员和维护代码库的人员,所以如果你写了一些复杂代码,你应该写足够的注释在里面,以便让其他任何人能够通过你的注释读懂代码。你应该总是在#后跟一个空格,然后再写注释。你也可以做一些标准的注释,像下面这样。
# FIXME -- fix these code later# TODO -- in future you have to do this
模块
模块是包含了我们能复用的代码的文件,包含了不同的函数定义,变量。模块文件通常以 .py 为扩展名。
Python 本身在默认安装时就带有大量的模块。我们之后将会用到其中的一部分。在使用模块前先导入它。
>>> import math # 导入math模块>>> print(math.e)2.71828182846
我们会在模块这个实验里学习更多有关模块的知识。
总结
完成本实验后,应当明白如何使用交互模式的 Python3 解释器,如何简单使用 vim 编写脚本文件并执行这个脚本文件。
要特别注意的是,Python 使用缩进来定义语句块,缩进是 Python 语法的一部分,这与其它大多数语言不同。
代码风格在本课程的实验PEP8 代码风格指南有更为详细的讲述。注释的话,能使用英文建议使用英文。模块同样会在之后的实验里深入学习。
2.变量和数据类型
实验介绍
本节实验内容主要包括 Python 基本的数据类型和变量。
知识点
- python 关键字
- 变量的定义与赋值
- input() 函数
- 字符串的格式化
关键字和标识符
每一种编程语言都有它们自己的语法规则,就像我们所说的外语。
下列的标识符是 Python3 的关键字,并且不能用于通常的标识符。关键字必须完全按照下面拼写:
False def if raiseNone del import returnTrue elif in tryand else is whileas except lambda withassert finally nonlocal yieldbreak for notclass from orcontinue global pass
这些内容可以在 Python3 解释器中得到:

在 Python 中 我们不需要为变量指定数据类型。所以你可以直接写出abc = 1,这样变量abc就是整数类型。如果你写出abc = 1.0,那么变量abc就是浮点类型。
>>> a = 13>>> b = 23>>> a + b36
通过上面的例子你应该理解了如何在 Python 中定义变量,也就是只需要输入变量名和值就行了。Python 也能操作字符串,它们用单引号或双引号括起来,就像下面这样。
>>> 'ShiYanLou''ShiYanLou'>>> 'ShiYanLou\'s best'"ShiYanLou's best">>> "Hello World!"'Hello World!'
从键盘读取输入
通常情况下,Python 的代码中是不需要从键盘读取输入的。不过我们还是可以在 Python 中使用函数input()来做到这一点,input()有一个用于打印在屏幕上的可选字符串参数,返回用户输入的字符串。
我们来写一个程序,它将会从键盘读取一个数字并且检查这个数字是否小于 100。这个程序名称是/home/shiyanlou/testhundred.py。还记得如何使用 Vim 吗?忘了的话可以看看下面的动图,打开 Xfce 终端,使用 Vim 开始编写代码:

在终端中输入:
$ vim testhundred.py
输入i后,编写以下代码:
#!/usr/bin/env python3number = int(input("Enter an integer: "))if number <= 100:print("Your number is less than or equal to 100")else:print("Your number is greater than 100")
接着按ESC键并输入:wq退出Vim。
该段代码的含义如下:
如果number小于等于 100,输出 “Your number is less than or equal to 100”,如果大于 100,输出 “Your number is greater than 100”。
在执行程序前,别忘了为文件添加可执行权限:
$ chmod +x testhundred.py
程序运行起来就像这样:
$ ./testhundred.pyEnter an integer: 13Your number is less than or equal to 100$ ./testhundred.pyEnter an integer: 123Your number is greater than 100
后续程序中将会用到之后实验将学到的 while 循环语句,这里先简单介绍下。
while 是使用一个表达式作为判断的条件,如果条件不能够达成则停止循环:
w = 20while w > 1:print(w)w -= 1
这个循环中判断条件为w > 1,当条件不满足的时候就停止循环。当 w 的值小于等于 1 的时候,循环退出。这里要注意w -= 1,等同于w = w - 1。
下一个程序我们写入/home/shiyanlou/investment.py,来计算投资,使用 Vim 输入以下代码:
#!/usr/bin/env python3amount = float(input("Enter amount: ")) # 输入数额inrate = float(input("Enter Interest rate: ")) # 输入利率period = int(input("Enter period: ")) # 输入期限value = 0year = 1while year <= period:value = amount + (inrate * amount)print("Year {} Rs. {:.2f}".format(year, value))amount = valueyear = year + 1
运行程序:
$ cd /home/shiyanlou$ chmod +x investment.py$ ./investment.pyEnter amount: 10000Enter Interest rate: 0.14Enter period: 5Year 1 Rs. 11400.00Year 2 Rs. 12996.00Year 3 Rs. 14815.44Year 4 Rs. 16889.60Year 5 Rs. 19254.15
while year <= period:的意思是,当year的值小于等于period的值时,下面的语句将会一直循环执行下去,直到year大于period时停止循环。
Year {} Rs. {:.2f}".format(year, value)称为字符串格式化,大括号和其中的字符会被替换成传入str.format()的参数,也即year和value。其中{:.2f}的意思是替换为 2 位精度的浮点数。
代码示例
一些关于变量和数据类型的代码实例。下面的例子程序请都写在/home/shiyanlou目录下。
本部分包括下面的几个实例:
- 求 N 个数字的平均值
- 华氏温度到摄氏温度转换程序
请按照指定目录完成每个程序,并理解每个程序中每行代码的含义。
求N个数字的平均值
下面的程序用来求 N 个数字的平均值。请将程序代码写入到文件/home/shiyanlou/averagen.py中,程序中将需要输入 10 个数字,最后计算 10 个 数字的平均值。
代码内容,请理解每一行代码含义:
#!/usr/bin/env python3N = 10sum = 0count = 0print("please input 10 numbers:")while count < N:number = float(input())sum = sum + numbercount = count + 1average = sum / Nprint("N = {}, Sum = {}".format(N, sum))print("Average = {:.2f}".format(average))
运行程序过程,需要输入 10 个数字:
$ cd /home/shiyanlou$ chmod +x averagen.py$ ./averagen.py1.23.43.533.22462.445.5N = 10, Sum = 65.2Average = 6.52
温度转换
在下面的程序里,我们使用公式 C = (F - 32) / 1.8 将华氏温度转为摄氏温度。
#!/usr/bin/env python3fahrenheit = 0print("Fahrenheit Celsius")while fahrenheit <= 250:celsius = (fahrenheit - 32) / 1.8 # 转换为摄氏度print("{:5d} {:7.2f}".format(fahrenheit , celsius))fahrenheit = fahrenheit + 25
{:5d}的意思是替换为 5 个字符宽度的整数,宽度不足则使用空格填充。{:7.2f}的意思是替换为为7个字符宽度的保留两位的小数,小数点也算一个宽度,宽度不足则使用空格填充。其中7指宽度为7,.2f指保留两位小数。
运行程序:
$ cd /home/shiyanlou$ chmod +x temperature.py$ ./temperature.pyFahrenheit Celsius0 -17.7825 -3.8950 10.0075 23.89100 37.78125 51.67150 65.56175 79.44200 93.33225 107.22250 121.11
单行定义多个变量或复制
你甚至可以在一行内将多个值赋值给多个变量。
在终端中输入:
$ python3
进入到 python3 交互式界面:
>>> a , b = 45, 54>>> a45>>> b54
这个技巧用来交换两个数的值非常方便。
>>> a, b = b , a>>> a54>>> b45
要明白这是怎么工作的,你需要学习元组(tuple)这个数据类型。我们是用逗号创建元组。在赋值语句的右边我们创建了一个元组,我们称这为元组封装(tuple packing),赋值语句的左边我们则做的是元组拆封 (tuple unpacking)。
下面是另一个元组拆封的例子:
>>> data = ("shiyanlou", "China", "Python")>>> name, country, language = data>>> name'shiyanlou'>>> country'China'>>> language'Python'
总结
完成这个实验我们应该了解到以下的内容:
- Python3 关键字有哪些(在这里不要求全部记住)
- 变量如何赋值变量
- Python3 怎样从键盘读取输入
- Python3 字符串的格式化
- 元组封装和拆封
在这里可以了解更多有关字符串格式化的信息:https://docs.python.org/3/library/string.html#formatstrings。
3.运算符和表达式
实验介绍
在 Python 中你会写大量的表达式。
表达式由运算符和操作数组成,像2+3就是一个表达式,其中 2 和 3 是操作数,加号是运算符。
本节实验中我们将学习运算符和表达式的基本用法。
知识点
- 关系/逻辑运算
- 表达式
- 类型转换
运算符
运算符是一些符号,它告诉 Python 解释器去做一些数学或逻辑操作。一些基本的数学操作符如下所示:
>>> 2 + 35>>> 23.0 - 320.0>>> 22 / 121.8333333333333333
只要有任意一个操作数是浮点数,结果就会是浮点数。
进行除法运算时若是除不尽,结果将会是小数,这很自然,如果要进行整除,使用//运算符,它将返回商的整数部分。
%是求余运算符:
>>> 14 % 32
整数运算示例
整数运算符比较容易理解,代码如下:
#!/usr/bin/env python3days = int(input("Enter days: "))months = days // 30days = days % 30print("Months = {} Days = {}".format(months, days))
在终端里使用Vim输入上述代码后,用chmod命令赋予可执行权限,最后运行程序:

在Enter days:后获得用户输入的天数,然后获得月份数和天数,最后把这些数打印出来。你可以使用更容易的办法:
#!/usr/bin/env python3days = int(input("Enter days: "))print("Months = {} Days = {}".format(*divmod(days, 30)))
divmod(num1, num2)返回一个元组,这个元组包含两个值,第一个是 num1 和 num2 相整除得到的值,第二个是 num1 和 num2 求余得到的值,然后我们用*运算符拆封这个元组,得到这两个值。
关系运算符
你可以使用下面的运算符实现关系运算。
关系运算符
| Operator | Meaning |
|---|---|
| < | Is less than |
| <= | Is less than or equal to |
| > | Is greater than |
| >= | Is greater than or equal to |
| == | Is equal to |
| != | Is not equal to |
在终端里输入python3进入交互页面,试一试以下一些例子:
>>> 1 < 2True>>> 3 > 34False>>> 23 == 45False>>> 34 != 323True
逻辑运算符
对于逻辑 与,或,非,我们使用and,or,not这几个关键字。
逻辑运算符and和or也称作短路运算符:它们的参数从左向右解析,一旦结果可以确定就停止。例如,如果A和C为真而B为假,A and B and C不会解析C。作用于一个普通的非逻辑值时,短路运算符的返回值通常是能够最先确定结果的那个操作数。
关系运算可以通过逻辑运算符and和or组合,比较的结果可以用not来取反意。逻辑运算符的优先级又低于关系运算符,在它们之中,not具有最高的优先级,or优先级最低,所以A and not B or C等于(A and (notB)) or C。当然,括号也可以用于比较表达式。
下面是一些例子:
>>> 5 and 4 # 首先判断5,肯定为true,那么最终的结果就取决于 and 后面那个的布尔值,4 的布尔值为 true,这样就可以确定整个表达式的值为 true 了,所以返回 44>>> 0 and 4 # 首先判断0,因为 0 的布尔值为 false,那么不管 and 后面那个的布尔值是什么,整个表达式的布尔值都应该为 false 了,这个时候就不需要判断 4 了,直接返回最先确定结果的那个数也就是00>>> False or 3 or 03>>> 2 > 1 and not 3 > 5 or 4True
简写运算符
x op= expression为简写运算的语法形式。其等价于x = x op expression,举例如下:
>>> a = 12>>> a += 13>>> a25>>> a /= 3>>> a8.333333333333334>>> a += (26 * 32)>>> a840.3333333333334
/home/shiyanlou/shorthand.py示例,输入quit()退出python交互页面,用Vim写入以下代码:
#!/usr/bin/env python3N = 100a = 2while a < N:print(str(a))a *= a
运行程序:
$ cd /home/shiyanlou$ chmod +x shorthand.py$ ./shorthand.py2416
表达式
通常我们书写表达式的时候,会在每一个运算符左右都放一个空格,这样使代码更可读,如:
a = 234 * (45 - 56 / 34)
一个用于展示表达式的例子,注意其中运算符的优先级。
#!/usr/bin/env python3a = 9b = 12c = 3x = a - b / 3 + c * 2 - 1y = a - b / (3 + c) * (2 - 1)z = a - (b / (3 + c) * 2) - 1print("X = ", x)print("Y = ", y)print("Z = ", z)
运行程序:
$ cd /home/shiyanlou$ chmod +x evaluationexp.py$ ./evaluationexp.pyX = 10.0Y = 7.0Z = 4.0
第一个计算的是x,步骤如下:
9 - 12 / 3 + 3 * 2 -19 - 4 + 3 * 2 - 19 - 4 + 6 - 15 + 6 - 111 - 110
由于括号的存在,y和z的计算方式不同,你可以自己去验证它们。
类型转换
我们可以手动的执行类型转换。
| 类型转换函数 | 转换路径 |
|---|---|
| float(string) | 字符串 -> 浮点值 |
| int(string) | 字符串 -> 整数值 |
| str(integer) | 整数值 -> 字符串 |
| str(float) | 浮点值 -> 字符串 |
>>> a = 8.126768>>> str(a)'8.126768'
可以分别尝试下前面的四个类型转换函数。
程序示例
2.7.1. evaluateequ.py
这个程序计算数列1/x+1/(x+1)+1/(x+2)+ … +1/n,我们设 x = 1,n = 10。
使用Vim写入以下代码:
#!/usr/bin/env python3sum = 0for i in range(1, 11):sum += 1.0 / iprint("{:2d} {:6.4f}".format(i , sum))
运行程序:

2.7.2. quadraticequation.py
这个程序的名称为quadratic equation组合,是二次方程的英文词组。
这个程序用来求解二次方程式:
#!/usr/bin/env python3import matha = int(input("Enter value of a: "))b = int(input("Enter value of b: "))c = int(input("Enter value of c: "))d = b * b - 4 * a * cif d < 0:print("ROOTS are imaginary")else:root1 = (-b + math.sqrt(d)) / (2 * a)root2 = (-b - math.sqrt(d)) / (2 * a)print("Root 1 = ", root1)print("Root 2 = ", root2)
运行程序:

2.7.3. salesmansalary.py
这个程序计算一位数码相机销售人员的工资。他的基本工资是 1500,每售出一台相机他可以得到 200 并且获得 2% 的抽成。程序要求输入相机数量及单价。
#!/usr/bin/env python3basic_salary = 1500bonus_rate = 200commission_rate = 0.02numberofcamera = int(input("Enter the number of inputs sold: "))price = float(input("Enter the price of camera: "))bonus = (bonus_rate * numberofcamera)commission = (commission_rate * price * numberofcamera)print("Bonus = {:6.2f}".format(bonus))print("Commission = {:6.2f}".format(commission))print("Gross salary = {:6.2f}".format(basic_salary + bonus + commission))
运行程序:
shiyanlou:~/ $ ./salesmansalary.pyEnter the number of inputs sold: 5Enter the price of camera: 2000Bonus = 1000.00Commission = 200.00Gross salary = 2700.00
总结
本节实验知识点回顾:
- 关系/逻辑运算
- 表达式
- 类型转换
除了数值运算,关系和逻辑运算也是程序的重要组成部分。另外 Python 是强类型语言,所以必要的时候需要手动进行类型转换。
程序挑战:圆的面积
挑战:圆的面积
介绍
我们通过实验已经学习了基本的 Python 3 语法。现在我们就来用一个挑战来巩固一下我们的实验效果。
本挑战中,我们将实现一个程序用来计算半径为 2 的圆的面积并打印输出。
目标
在/home/shiyanlou/Code创建一个 Python 脚本CircleArea.py:
$ cd /home/shiyanlou/Code$ touch CircleArea.py
CircleArea.py 能够计算出一个半径为2的圆的面积,并且把面积打印出来,保留小数点后10位。
不要使用 input 等方法获得输入,程序不需要输入任何参数,程序执行如下,直接输出半径为 2 的圆的面积:
$ python3 /home/shiyanlou/Code/CircleArea.py
提示语
import math- 使用
format可以控制输出的小数位数,可以使用'{:.10f}'.format()来进行格式化输出保留 10 位小数 - 注意代码文件的路径必须是
/home/shiyanlou/Code/CircleArea.py - 请不要使用 input 获取输入,程序执行不需要任何输入的参数
知识点
- 导入模块
- 打印输出
- 运算符和表达式
参考代码
注意:请务必先独立思考获得 PASS 之后再查看参考代码,直接拷贝代码收获不大
/home/shiyanlou/Code/CircleArea.py参考代码:
import math# 计算圆的面积area = 2 * 2 * math.pi# 格式化输出圆的面积,保留10位小数print("{:.10f}".format(area))
4.控制流if-else
实验介绍
我们处理现实生活中的问题时会做出决定,就像决定买哪种相机或者怎样更好的打篮球。
同样我们写计算机程序的时候也要做相同的事情。
我们通过if-else语句来做决定,我们使用它来改变程序运行的流程。
知识点
- if 语句
- else 语句
- 真值检测
if语句
语法如下:
if expression:do this
如果表达式expression的值为真(不为零的任何值都为真),程序将执行缩进后的内容。务必要使用正确的缩进,在表达式为真的情况将会执行缩进的所有行。
一个简单的例子,使用vim写入文件/home/shiyanlou/number100.py,程序接受用户输入的一个数并且检查这个数是否小于 100。
#!/usr/bin/env python3number = int(input("Enter a number: "))if number < 100:print("The number is less than 100")
然后我们运行它:
$ cd /home/shiyanlou$ chmod +x number100.py$ ./number100.py
操作截图:

else语句
在上面的例子中,我们想要这样做:如果输入数大于 100 则打印 “Greater than”。我们使用else语句来做到这一点,它将在if语句未满足的情况时工作。
#!/usr/bin/env python3number = int(input("Enter a number: "))if number < 100:print("The number is less than 100")else:print("The number is greater than 100")
运行它:

另一个非常基础的例子:
>>> x = int(input("Please enter an integer: "))>>> if x < 0:... x = 0... print('Negative changed to zero')... elif x == 0:... print('Zero')... elif x == 1:... print('Single')... else:... print('More')
在上面的例子中,elif是else if的缩写。
真值检验
检测真值的优雅方式是这样的:
if x:pass
不要像下面这样做:
if x == True:pass
总结
这个实验非常简单,这里只是提一句,Python 中的很多值是具有布尔意义的,所以有时候我们可以写的更优雅一点。
同时也要注意下if``elif``else的基本语法结构。
5.循环
在以前的例子里,有些时候我们需要多次执行相同的任务,我们使用一个计数器来检查代码需要执行的次数。这个技术被称为循环。
实验知识点
- while 循环
- print() 函数的 end 参数
列表
- 索引
- 切片
- for 循环
- range() 函数
- continue 关键字
- for 循环中的 else 关键字
while循环
while语句的语法如下:
while condition:statement1statement2
想要多次执行的代码必须以正确的缩进放在while语句下面。在表达式condition为真的时候它们才会执行。同if-else一样,非零值为真。启动python3交互页面,让我们写一个简单的代码,它按顺序打印 0 到 10 的数字:
>>> n = 0>>> while n < 11:... print(n)... n += 1...012345678910
在第一行我们使n = 0,然后在while语句中把条件设置为n < 11,这意味着在while语句下面缩进的所有行将会被执行,直到n的值大于等于11。在循环里我们只是打印 n 的值然后令它增一。
想想如果没有循环语句,你想要打印 0 到 10 的所有数字,那你得手动打印 11 次!
斐波那契(Fibonacci)数列
让我们来试试打印斐波那契数列。这个数列前两项为 1,之后的每一个项都是前两项之和。所以这个数列看起来就像这样:1,1,2,3,5,8,13 ……
#!/usr/bin/env python3a, b = 0, 1while b < 100:print(b)a, b = b, a + b
运行程序:

第一行代码中我们初始化a和b。当b的值小于 100 的时候,循环执行代码。循环里我们首先打印b的值,然后在下一行将a + b的值赋值给b,b的值赋值给a。
学习其他语言的同学在这里可能有些困惑,你可以这样理解,Python 中赋值语句执行时会先对赋值运算符右边的表达式求值,然后将这个值赋值给左边的变量。
默认情况下,print()除了打印你提供的字符串之外,还会打印一个换行符,所以每调用一次print()就会换一次行,如同上面一样。
你可以通过print()的另一个参数end来替换这个换行符,就像下面这样,下面的程序写入/home/shiyanlou/fibonacci2.py:
#!/usr/bin/env python3a, b = 0, 1while b < 100:print(b, end=' ')a, b = b, a + bprint()
运行程序:
$ cd /home/shiyanlou$ chmod +x fibonacci2.py$ ./fibonacci2.py

幂级数
我们来写一个程序计算幂级数:e^x = 1 + x + x^2 / 2! + x^3 / 3! + … + x^n / n!(0 < x < 1)。
该程序写入代码文件/home/shiyanlou/powerseries.py
#!/usr/bin/env python3x = float(input("Enter the value of x: "))n = term = num = 1result = 1.0while n <= 100:term *= x / nresult += termn += 1if term < 0.0001:breakprint("No of Times= {} and Sum= {}".format(n, result))
运行程序:

在这个程序里我们介绍一个新的关键字break,它可以终止最里面的循环。这个例子里我们在if语句里使用break:
if term < 0.0001:break
这意味着如果term的值小于0.0001,那么终止循环。
乘法表
这个例子里我们打印 10 以内的乘法表。写入代码文件/home/shiyanlou/multiplication.py
#!/usr/bin/env python3i = 1print("-" * 50)while i < 11:n = 1while n <= 10:print("{:5d}".format(i * n), end=' ')n += 1print()i += 1print("-" * 50)
运行如下:

这里我们在while循环里使用了另一个while循环,这被称为嵌套循环。你应该已经看到一条有趣的语句:
print("-" * 50)
字符串若是乘上整数 n,将返回由 n 个此字符串拼接起来的新字符串。
下面是一些例子:
>>> 's' * 10'ssssssssss'>>> print("*" * 10)**********>>> print("#" * 20)####################>>> print("--" * 20)---------------------------------------->>> print("-" * 40)----------------------------------------
一些打印星号的例子
这里是一些你可以在大学的实验报告里经常看到的例子。
2.4.1. 设计 1
#!/usr/bin/env python3row = int(input("Enter the number of rows: "))n = rowwhile n >= 0:x = "*" * nprint(x)n -= 1
运行这个程序:

2.4.2. 设计 2
#!/usr/bin/env python3n = int(input("Enter the number of rows: "))i = 1while i <= n:print("*" * i)i += 1
运行这个程序:

2.4.3 设计 3
#!/usr/bin/env python3row = int(input("Enter the number of rows: "))n = rowwhile n >= 0:x = "*" * ny = " " * (row - n)print(y + x)n -= 1
运行这个程序:

列表
在继续学习循环之前,我们先学习一个叫做列表的数据结构。它可以写作中括号之间的一列逗号分隔的值。列表的元素不必是同一类型:
>>> a = [ 1, 342, 223, 'India', 'Fedora']>>> a[1, 342, 223, 'India', 'Fedora']
你可以将上面的列表想象为一堆有序的盒子,盒子包含有上面提到的值,每个盒子都有自己的编号(红色的数字),编号从零开始,你可以通过编号访问每一个盒子里面的值。对于列表,这里的编号称为索引。

我们像下面这样通过索引来访问列表中的每一个值:
>>> a[0]1>>> a[4]'Fedora'
如果我们使用负数的索引,那将会从列表的末尾开始计数,像下面这样:
>>> a[-1]'Fedora'
你甚至可以把它切成不同的部分,这个操作称为切片,例子在下面给出:
>>> a[0:-1][1, 342, 223, 'India']>>> a[2:-2][223]
切片并不会改变正在操作的列表,切片操作返回其子列表,这意味着下面的切片操作返回列表一个新的(栈)拷贝副本:
>>> a[:][1, 342, 223, 'India', 'Fedora']
切片的索引有非常有用的默认值;省略的第一个索引默认为零,省略的第二个索引默认为切片的字符串的大小:
>>> a[:-2][1, 342, 223]>>> a[-2:]['India', 'Fedora']
有个办法可以很容易地记住切片的工作方式:切片时的索引是在两个元素之间 。左边第一个元素的索引为 0,而长度为 n 的列表其最后一个元素的右界索引为 n。例如:
+---+-----+-----+---------+----------+| 1 | 342 | 223 | 'India' | 'Fedora' |+---+-----+-----+---------+----------+0 1 2 3 4 5-5 -4 -3 -2 -1
上面的第一行数字给出列表中的索引点 0…5。第二行给出相应的负索引。切片是从 i 到 j 两个数值表示的边界之间的所有元素。
对于非负索引,如果上下都在边界内,切片长度就是两个索引之差。例如a[2:4]是 2。
Python 中有关下标的集合都满足左闭右开原则,切片中也是如此,也就是说集合左边界值能取到,右边界值不能取到。
对上面的列表,a[0:5]用数学表达式可以写为[0,5),其索引取值为0,1,2,3,4,所以能将a中所有值获取到。 你也可以用a[:5], 效果是一样的。
而a[-5:-1],因为左闭右开原则,其取值为-5,-4,-3,-2是不包含-1的。
为了取到最后一个值,你可以使用a[-5:],它代表了取该列表最后5个值。
试图使用太大的索引会导致错误:
>>> a[32]Traceback (most recent call last):File "<stdin>", line 1, in <module>IndexError: list index out of range>>> a[-10]Traceback (most recent call last):File "<stdin>", line 1, in <module>IndexError: list index out of range
Python 能够优雅地处理那些没有意义的切片索引:一个过大的索引值(即大于列表实际长度)将被列表实际长度所代替,当上边界比下边界大时(即切片左值大于右值)就返回空列表:
>>> a[2:32][223, 'India', 'Fedora']>>> a[32:][]
切片操作还可以设置步长,就像下面这样:
>>> a[1::2][342, 'India']
它的意思是,从切片索引 1 到列表末尾,每隔两个元素取值。
列表也支持连接这样的操作,它返回一个新的列表:
>>> a + [36, 49, 64, 81, 100][1, 342, 223, 'India', 'Fedora', 36, 49, 64, 81, 100]
列表允许修改元素:
>>> cubes = [1, 8, 27, 65, 125]>>> cubes[3] = 64>>> cubes[1, 8, 27, 64, 125]
也可以对切片赋值,此操作可以改变列表的尺寸,或清空它:
>>> letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g']>>> letters['a', 'b', 'c', 'd', 'e', 'f', 'g']>>> # 替换某些值>>> letters[2:5] = ['C', 'D', 'E']>>> letters['a', 'b', 'C', 'D', 'E', 'f', 'g']>>> # 现在移除他们>>> letters[2:5] = []>>> letters['a', 'b', 'f', 'g']>>> # 通过替换所有元素为空列表来清空这个列表>>> letters[:] = []>>> letters[]
细心的同学可能发问了,前面不是说过切片操作不改变列表么?严格来说,这里并不算真正的切片操作,只是上面代码中赋值运算符左边的这种操作与切片操作形式一样而已。
要检查某个值是否存在于列表中,你可以这样做:
>>> a = ['ShiYanLou', 'is', 'cool']>>> 'cool' in aTrue>>> 'Linux' in aFalse
这意味着我们可以将上面的语句使用在if子句中的表达式。通过内建函数len()我们可以获得列表的长度:
>>> len(a)3
如果你想要检查列表是否为空,请这样做:
if list_name: # 列表不为空passelse: # 列表为空pass
列表是允许嵌套的(创建一个包含其它列表的列表),例如:
>>> a = ['a', 'b', 'c']>>> n = [1, 2, 3]>>> x = [a, n]>>> x[['a', 'b', 'c'], [1, 2, 3]]>>> x[0]['a', 'b', 'c']>>> x[0][1]'b'
for循环
通过for语句我们可以使用 for 循环。Python 里的 for 循环与 C 语言中的不同。这里的 for 循环遍历任何序列(比如列表和字符串)中的每一个元素。下面给出示例:
>>> a = ['ShiYanLou', 'is', 'powerful']>>> for x in a:... print(x)...ShiYanLouispowerful
我们也能这样做:
>>> a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]>>> for x in a[::2]:... print(x)13579
range() 函数
如果你需要一个数值序列,内置函数range()会很方便,它生成一个等差数列(并不是列表):
>>> for i in range(5):... print(i)...01234>>> range(1, 5)range(1, 5)>>> list(range(1, 5))[1, 2, 3, 4]>>> list(range(1, 15, 3))[1, 4, 7, 10, 13]>>> list(range(4, 15, 2))[4, 6, 8, 10, 12, 14]
comtinue语句
如同break,我们可以在循环中使用另一个语句continue。它会跳过其后的代码回到循环开始处执行。这意味着它可以帮助你跳过部分循环。在下面的例子中,我们要求用户输入一个整数,如果输入的是负数,那么我们会再次要求输入,如果输入的是整数,我们计算这个数的平方。用户输入 0 来跳出这个无限循环。
#!/usr/bin/env python3while True:n = int(input("Please enter an Integer: "))if n < 0:continue # 这会返回到循环开始处执行elif n == 0:breakprint("Square is ", n ** 2)print("Goodbye")
运行程序:

循环的else语句
我们可以在循环后面使用可选的else语句。它将会在循环完毕后执行,除非有break语句终止了循环。
>>> for i in range(0, 5):... print(i)... else:... print("Bye bye")...01234Bye bye
在本课程的后续内容中,我们会看到更多有关break和continue的例子。
棍子游戏
这是一个非常简单的游戏。这里有 21 根棍子,首先用户选 1 到 4 根棍子,然后电脑选 1到 4 根棍子。谁选到最后一根棍子谁就输。判断一下用户有赢的机会吗?如果没有的话,如何修改游戏规则可以使用户有赢的机会呢?
特别说明:用户和电脑一次选的棍子总数只能是5。
#!/usr/bin/env python3sticks = 21print("There are 21 sticks, you can take 1-4 number of sticks at a time.")print("Whoever will take the last stick will loose")while True:print("Sticks left: " , sticks)if sticks == 1:print("You took the last stick, you loose")breaksticks_taken = int(input("Take sticks(1-4):"))if sticks_taken >= 5 or sticks_taken <= 0:print("Wrong choice")continueprint("Computer took: " , (5 - sticks_taken) , "\n")sticks -= 5
总结
这个实验中我们了解了两种循环:while和for循环,其中的for循环我们通常与range()函数配合使用,要特别注意的是,range()函数返回的并不是列表而是一种可迭代对象:

python 中for循环的else子句给我们提供了检测循环是否顺利执行完毕的一种优雅方法。
6.数据结构
实验介绍
Python 有许多内建的数据结构。如果你困惑于什么是数据结构,那么可以参考一下Wikipedia 。
简单的来说,数据结构(data structure)是计算机中存储、组织数据的方式。比如我们之前的课程中使用过的列表就是一种数据结构,在这里我们还会深入学习它。
知识点
- 列表的方法与列表元素的删除
- 将列表用作栈和队列
- 列表推导式
- 元组、集合、字典的创建与操作
enumerate()和zip()函数
列表
>>> a = [23, 45, 1, -3434, 43624356, 234]>>> a.append(45)>>> a[23, 45, 1, -3434, 43624356, 234, 45]
首先我们建立了一个列表a。然后调用列表的方法a.append(45)添加元素45到列表末尾。你可以看到元素 45 已经添加到列表的末端了。有些时候我们需要将数据插入到列表的任何位置,这时我们可以使用列表的insert()方法。
>>> a.insert(0, 1) # 在列表索引 0 位置添加元素 1>>> a[1, 23, 45, 1, -3434, 43624356, 234, 45]>>> a.insert(0, 111) # 在列表索引 0 位置添加元素 111>>> a[111, 1, 23, 45, 1, -3434, 43624356, 234, 45]
列表方法count(s)会返回列表元素中s的数量。我们来检查一下45这个元素在列表中出现了多少次。
>>> a.count(45)2
如果你想要在列表中移除任意指定值,你需要使用remove()方法。
>>> a.remove(234)>>> a[111, 1, 23, 45, 1, -3434, 43624356, 45]
现在我们反转整个列表。
>>> a.reverse()>>> a[45, 43624356, -3434, 1, 45, 23, 1, 111]
怎样将一个列表的所有元素添加到另一个列表的末尾呢,可以使用列表的extend()方法。
>>> b = [45, 56, 90]>>> a.extend(b) # 添加 b 的元素而不是 b 本身>>> a[45, 43624356, -3434, 1, 45, 23, 1, 111, 45, 56, 90]
给列表排序,我们使用列表的sort()方法,排序的前提是列表的元素是可比较的。
>>> a.sort()>>> a[-3434, 1, 1, 23, 45, 45, 45, 56, 90, 111, 43624356]
你也能使用del关键字删除指定位置的列表元素。
>>> del a[-1]>>> a[-3434, 1, 1, 23, 45, 45, 45, 56, 90, 111]
将列表用作栈和队列
栈是我们通常所说的一种LIFO(Last In First Out 后进先出)数据结构。它的意思是最后进入的数据第一个出来。一个最简单的例子往一端封闭的管道放入一些弹珠然后取出来,如果你想把弹珠取出来,你必须从你最后放入弹珠的位置挨个拿出来。用代码实现此原理:
>>> a = [1, 2, 3, 4, 5, 6]>>> a[1, 2, 3, 4, 5, 6]>>> a.pop()6>>> a.pop()5>>> a.pop()4>>> a.pop()3>>> a[1, 2]>>> a.append(34)>>> a[1, 2, 34]
上面的代码中我们使用了一个新方法pop()。传入一个参数 i 即pop(i)会将第 i 个元素弹出。
在我们的日常生活中会经常遇到队列,比如售票窗口、图书馆、超市的结账出口。队列是一种在末尾追加数据以及在开始弹出数据的数据结构。与栈不同,它是FIFO(First In First Out 先进先出)的数据结构。
>>> a = [1, 2, 3, 4, 5]>>> a.append(1)>>> a[1, 2, 3, 4, 5, 1]>>> a.pop(0)1>>> a.pop(0)2>>> a[3, 4, 5, 1]
我们使用a.pop(0)弹出列表中第一个元素。
列表推导式
列表推导式为从序列中创建列表提供了一个简单的方法。如果没有列表推导式,一般都是这样创建列表的:通过将一些操作应用于序列的每个成员并通过返回的元素创建列表,或者通过满足特定条件的元素创建子序列。
假设我们创建一个 squares 列表,可以像下面这样创建。
>>> squares = []>>> for x in range(10):... squares.append(x**2)...>>> squares[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
注意这个 for 循环中的被创建(或被重写)的名为x的变量在循环完毕后依然存在。使用如下方法,我们可以计算 squares 的值而不会产生任何的副作用:。
squares = list(map(lambda x: x**2, range(10)))
等价于下面的列表推导式。
squares = [x**2 for x in range(10)]
上面这个方法更加简明且易读。
列表推导式由包含一个表达式的中括号组成,表达式后面跟随一个 for 子句,之后可以有零或多个 for 或 if 子句。结果是一个列表,由表达式依据其后面的 for 和 if 子句上下文计算而来的结果构成。
例如,如下的列表推导式结合两个列表的元素,如果元素之间不相等的话:
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y][(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
等同于:
>>> combs = []>>> for x in [1,2,3]:... for y in [3,1,4]:... if x != y:... combs.append((x, y))...>>> combs[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]
值得注意的是在上面两个方法中的 for 和 if 语句的顺序。
列表推导式也可以嵌套。
>>> a=[1,2,3]>>> z = [x + 1 for x in [x ** 2 for x in a]]>>> z[2, 5, 10]
元组
元组是由数个逗号分割的值组成。
>>> a = 'Fedora', 'ShiYanLou', 'Kubuntu', 'Pardus'>>> a('Fedora', 'ShiYanLou', 'Kubuntu', 'Pardus')>>> a[1]'ShiYanLou'>>> for x in a:... print(x, end=' ')...Fedora ShiYanLou Kubuntu Pardus
你可以对任何一个元组执行拆封操作并赋值给多个变量,就像下面这样:
>>> divmod(15,2)(7, 1)>>> x, y = divmod(15,2)>>> x7>>> y1
元组是不可变类型,这意味着你不能在元组内删除或添加或编辑任何值。如果你尝试这些操作,将会出错:
>>> a = (1, 2, 3, 4)>>> del a[0]Traceback (most recent call last):File "<stdin>", line 1, in <module>TypeError: 'tuple' object doesn't support item deletion
要创建只含有一个元素的元组,在值后面跟一个逗号。
>>> a = (123)>>> a123>>> type(a)<class 'int'>>>> a = (123, )>>> b = 321,>>> a(123,)>>> b(321,)>>> type(a)<class 'tuple'>>>> type(b)<class 'tuple'>
通过内建函数type()你可以知道任意变量的数据类型。还记得我们使用len()函数来查询任意序列类型数据的长度吗?
>>> type(len)<class 'builtin_function_or_method'>
集合
集合是一个无序不重复元素的集。基本功能包括关系测试和消除重复元素。集合对象还支持 union(联合),intersection(交),difference(差)和 symmetric difference(对称差集)等数学运算。
大括号或 set() 函数可以用来创建集合。注意:想要创建空集合,你必须使用 set() 而不是 {}。后者用于创建空字典,我们在下一节中介绍的一种数据结构。
下面是集合的常见操作:
>>> basket = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}>>> print(basket) # 你可以看到重复的元素被去除{'orange', 'banana', 'pear', 'apple'}>>> 'orange' in basketTrue>>> 'crabgrass' in basketFalse>>> # 演示对两个单词中的字母进行集合操作...>>> a = set('abracadabra')>>> b = set('alacazam')>>> a # a 去重后的字母{'a', 'r', 'b', 'c', 'd'}>>> a - b # a 有而 b 没有的字母{'r', 'd', 'b'}>>> a | b # 存在于 a 或 b 的字母{'a', 'c', 'r', 'd', 'b', 'm', 'z', 'l'}>>> a & b # a 和 b 都有的字母{'a', 'c'}>>> a ^ b # 存在于 a 或 b 但不同时存在的字母{'r', 'd', 'b', 'm', 'z', 'l'}
从集合中添加或弹出元素:
>>> a = {'a','e','h','g'}>>> a.pop() # pop 方法随机删除一个元素并打印'h'>>> a.add('c')>>> a{'c', 'e', 'g', 'a'}
字典
字典是是无序的键值对(key:value)集合,同一个字典内的键必须是互不相同的。一对大括号{}创建一个空字典。初始化字典时,在大括号内放置一组逗号分隔的键:值对,这也是字典输出的方式。我们使用键来检索存储在字典中的数据。
>>> data = {'kushal':'Fedora', 'kart_':'Debian', 'Jace':'Mac'}>>> data{'kushal': 'Fedora', 'Jace': 'Mac', 'kart_': 'Debian'}>>> data['kart_']'Debian'
创建新的键值对很简单:
>>> data['parthan'] = 'Ubuntu'>>> data{'kushal': 'Fedora', 'Jace': 'Mac', 'kart_': 'Debian', 'parthan': 'Ubuntu'}
使用del关键字删除任意指定的键值对:
>>> del data['kushal']>>> data{'Jace': 'Mac', 'kart_': 'Debian', 'parthan': 'Ubuntu'
使用in关键字查询指定的键是否存在于字典中。
>>> 'ShiYanLou' in dataFalse
必须知道的是,字典中的键必须是不可变类型,比如你不能使用列表作为键。
dict()可以从包含键值对的元组中创建字典。
>>> dict((('Indian','Delhi'),('Bangladesh','Dhaka'))){'Indian': 'Delhi', 'Bangladesh': 'Dhaka'}
如果你想要遍历一个字典,使用字典的items()方法。
>>> data{'Kushal': 'Fedora', 'Jace': 'Mac', 'kart_': 'Debian', 'parthan': 'Ubuntu'}>>> for x, y in data.items():... print("{} uses {}".format(x, y))...Kushal uses FedoraJace uses Mackart_ uses Debianparthan uses Ubuntu
许多时候我们需要往字典中的元素添加数据,我们首先要判断这个元素是否存在,不存在则创建一个默认值。如果在循环里执行这个操作,每次迭代都需要判断一次,降低程序性能。
我们可以使用dict.setdefault(key, default)更有效率的完成这个事情。
>>> data = {}>>> data.setdefault('names', []).append('Ruby')>>> data{'names': ['Ruby']}>>> data.setdefault('names', []).append('Python')>>> data{'names': ['Ruby', 'Python']}>>> data.setdefault('names', []).append('C')>>> data{'names': ['Ruby', 'Python', 'C']}
试图索引一个不存在的键将会抛出一个keyError错误。我们可以使用dict.get(key, default)来索引键,如果键不存在,那么返回指定的 default 值。
>>> data['foo']Traceback (most recent call last):File "<stdin>", line 1, in <module>KeyError: 'foo'>>> data.get('foo', 0)0
如果你想要在遍历列表(或任何序列类型)的同时获得元素索引值,你可以使用enumerate()。
>>> for i, j in enumerate(['a', 'b', 'c']):... print(i, j)...0 a1 b2 c
你也许需要同时遍历两个序列类型,你可以使用zip()函数。
>>> a = ['Pradeepto', 'Kushal']>>> b = ['OpenSUSE', 'Fedora']>>> for x, y in zip(a, b):... print("{} uses {}".format(x, y))...Pradeepto uses OpenSUSEKushal uses Fedora
程序示例
本节实验将会通过两个实例程序来熟悉 Python3 的基本数据结构:
- 判断学生成绩是否达标的程序
- 计算两个矩阵的 Hadamard 乘积
students.py
这是一个判断学生成绩是否达标的程序,要求输入学生数量,以及各个学生物理、数学、历史三科的成绩,如果总成绩小于 120,程序打印 “failed”,否则打印 “passed”。
代码写入/home/shiyanlou/students.py文件:
#!/usr/bin/env python3n = int(input("Enter the number of students: "))data = {} # 用来存储数据的字典变量Subjects = ('Physics', 'Maths', 'History') # 所有科目for i in range(0, n):name = input('Enter the name of the student {}: '.format(i + 1)) # 获得学生名称marks = []for x in Subjects:marks.append(int(input('Enter marks of {}: '.format(x)))) # 获得每一科的分数data[name] = marksfor x, y in data.items():total = sum(y)print("{}'s total marks {}".format(x, total))if total < 120:print(x, "failed :(")else:print(x, "passed :)")
name 和 marks 是变量,name 用来存储学生的名称,marks 是个列表,用来存储输入的学生的成绩数据。
data 是个字典,字典的键值对中,键指的是 name 的值,值指的是 marks 的值。因此会使用data[name] = marks将学生数据存入到 data 字典。
最后通过 for 循环遍历字典,x 为学生的 name,y 为学生成绩列表 marks,sum()函数会将传入的列表进行加和。
运行如下:

matrixmul.py
这个例子里我们计算两个矩阵的 Hadamard 乘积。要求输入矩阵的行/列数(在这里假设我们使用的是 n × n 的矩阵)。
代码写入/home/shiyanlou/matrixmul.py文件:
#!/usr/bin/env python3n = int(input("Enter the value of n: "))print("Enter values for the Matrix A")a = []for i in range(n):a.append([int(x) for x in input().split()])print("Enter values for the Matrix B")b = []for i in range(n):b.append([int(x) for x in input().split()])c = []for i in range(n):c.append([a[i][j] * b[i][j] for j in range(n)])print("After matrix multiplication")print("-" * 7 * n)for x in c:for y in x:print(str(y).rjust(5), end=' ')print()print("-" * 7 * n)
运行如下:

这里我们使用了几次列表推导式。[int(x) for x in input().split()]首先通过input()获得用户输入的字符串,再使用split()分割字符串得到一系列的数字字符串,然后用int()从每个数字字符串创建对应的整数值。我们也使用了[a[i][j] * b[i][j] for j in range(n)]来得到矩阵乘积的每一行数据。
总结
本实验了解了 Python 内置的几种常用数据结构,在写程序的过程中,不同的场景应当选取合适的数据结构。
一般来说,目前我们见到的数据结构已经够用了,不过 Python 中还有一些其它有用的数据结构,可以在这里了解:https://docs.python.org/3/library/datatypes.html。
7.字符串
实验介绍
字符串是 Python 中最常用的数据类型。本节实验将会学习如何对 Python3 的字符串进行处理操作。
知识点
- 字符串的3种表示
- 字符串的分割、连接、大小写转换、搜索等常用操作
字符串的表示
可以通过几种不同的方式表示字符串。如单引号('...')或双引号("...")。下面的例子能帮助你更好的理解字符串。
>>> s = "I am Chinese">>> s'I am Chinese'>>> s = 'I am Chinese'>>> s = "Here is a line \... split in two lines">>> s'Here is a line split in two lines'>>> s = "Here is a line \n split in two lines">>> s'Here is a line \n split in two lines'>>> print(s)Here is a linesplit in two lines
如果你想要分几行输入字符串,并且希望行尾的换行符自动包含到字符串当中,可以使用三对引号:"""..."""或'''...'''。
>>> print("""\... Usage: thingy [OPTIONS]... -h Display this usage message... -H hostname Hostname to connect to... """)Usage: thingy [OPTIONS]-h Display this usage message-H hostname Hostname to connect to
字符串的方法
每一个字符串对象都有几个可用的内建方法,我们已经使用过一些了,比如s.split()。
>>> s = "shi yan lou">>> s.title()'Shi Yan Lou'
方法title()返回字符串的标题版本,即单词首字母大写其余字母小写。
>>> z = s.upper()>>> z'SHI YAN LOU'>>> z.lower()'shi yan lou'
方法upper()返回字符串全部大写的版本,反之lower()返回字符串的全部小写版本。
>>> s = "I am A pRoGraMMer">> s.swapcase()'i AM a PrOgRAmmER'
方法swapcase()返回字符串大小写交换后的版本 :)
>>> s = "jdwb 2323bjb">>> s.isalnum()False>>> s = "jdwb2323bjb">>> s.isalnum()True
方法isalnum()检查所有字符是否只有字母和数字,上面的代码中第一行的字符串s中包含空格字符,所以返回False。
>>> s = "SankarshanSir">>> s.isalpha()True>>> s = "Sankarshan Sir">>> s.isalpha()False
方法isalpha()检查字符串之中是否只有字母。
>>> s = "1234">>> s.isdigit() # 检查字符串是否所有字符为数字True>>> s = "ShiYanLou is coming">>> s.islower() # 检查字符串是否所有字符为小写False>>> s = "Shiyanlou Is Coming">>> s.istitle() # To 检查字符串是否为标题样式True>>> s = "CHINA">>> s.isupper() # 检查字符串是否所有字符为大写True
我们可以使用split()分割任意字符串,split()允许有一个参数,用来指定字符串以什么字符分隔(默认为" "),它返回一个包含所有分割后的字符串的列表。
>>> s = "We all love Python">>> s.split()['We', 'all', 'love', 'Python']>>> x = "shiyanlou:is:waiting">>> x.split(':')['shiyanlou', 'is', 'waiting']
相反的,方法join()使用指定字符连接多个字符串,它需要一个包含字符串元素的列表作为输入然后连接列表内的字符串元素。
>>> "-".join("GNU/Linux is great".split())'GNU/Linux-is-great'
在上面的例子中,我们基于空格" "分割字符串"GNU/Linux is great",然后用"-"连接它们。
字符串剥离
字符串有几个进行剥离操作的方法。最简单的一个是strip(chars),用来剥离字符串首尾中指定的字符,它允许有一个字符串参数,这个参数为剥离哪些字符提供依据。不指定参数则默认剥离掉首尾的空格和换行符,代码如下:
>>> s = " a bc\n ">>> s.strip()'a bc'
你可以使用lstrip(chars)或rstrip(chars)只对字符串左或右剥离。
>>> s = "www.foss.in">>> s.lstrip("cwsd.") #删除在字符串左边出现的'c','w','s','d','.'字符'foss.in'>>> s.rstrip("cnwdi.") #删除在字符串右边出现的'c','n','w','d','i','.'字符'www.foss'
文本搜索
字符串有一些方法能够帮助你搜索字符串里的文本或子字符串。下面给出示例:
>>> s = "faulty for a reason">>> s.find("for")7>>> s.find("fora")-1>>> s.startswith("fa") # 检查字符串是否以 fa 开头True>>> s.endswith("reason") # 检查字符串是否以 reason 结尾True
find()能帮助你找到第一个匹配的子字符串,没有找到则返回 -1。
回文检查
回文是一种无论从左还是从右读都一样的字符序列。比如 “madam”。在这个例子中,我们检查用户输入的字符串是否是回文,并输出结果。
代码写入文件/home/shiyanlou/palindrome.py:
#!/usr/bin/env python3s = input("Please enter a string: ")z = s[::-1] #把输入的字符串s 进行倒序处理形成新的字符串zif s == z:print("The string is a palindrome")else:print("The string is not a palindrome")
运行程序:

单词计数
在讲解单词计数之前我们先了解一个概念:格式化操作符(%)。
我们先看下面的这个例子:
print("my name is %s.I am %d years old" % ('Shixiaolou',4))

在这个例子中,%s为第一个格式符,表示一个字符串;%d为第二个格式符,表示一个整数。格式符为真实值预留位置,并控制显示的格式。常用的有:
%s 字符串(用 str() 函数进行字符串转换)
%r 字符串(用 repr() 函数进行字符串转换)
%d 十进制整数
%f 浮点数
%% 字符“%”
那么接下来我们对用户输入的一行文本进行单词计数。
代码写入文件/home/shiyanlou/countwords.py:
#!/usr/bin/env python3s = input("Enter a line: ")print("The number of words in the line are %d" % (len(s.split(" "))))
运行程序:

总结
回顾本节实验的知识点:
- 字符串的3种表示
- 字符串的分割、连接、大小写转换、搜索等常用操作
本实验学习了字符串的几种表示方法(3种引号)以及对字符串的各种常用操作(分割、连接、大小写转换、搜索…),应当熟练掌握它们。
单词计数的实例需要仔细理解,字符串的更多用法在实验楼的项目实验中也都会涉及到。
8.函数
实验介绍
我们经常需要在同一个程序里多次复用代码。函数可以很好的帮助我们完成这一点。我们在函数里写我们要重复做的事,然后我们在任何需要的时候调用它。我们已经看到一些内建的函数,比如len(),divmod()。
知识点
- 函数的定义
- 局部/全局变量的概念
- 默认参数,关键字参数及强制关键字参数
- 文档字符串的使用
- 高阶函数,map() 函数
定义一个函数
我们使用关键字def来定义一个函数,语法描述如下所示:
def 函数名(参数):语句1语句2
让我们编写一个函数,它将接受两个整数作为输入,然后返回总和。
>>> def sum(a, b):... return a + b
第二行有个return关键字,我们把a + b的值返回给调用者。
你必须像下面这样调用这个函数。
>>> res = sum(234234, 34453546464)>>> res34453780698L
还记得我们上一个实验讲过的回文检查程序么,让我们编写一个函数来检查给出的字符串是否为回文,然后返回True或者False。
#!/usr/bin/env python3def palindrome(s):return s == s[::-1]if __name__ == '__main__':s = input("Enter a string: ")if palindrome(s):print("Yay a palindrome")else:print("Oh no, not a palindrome")
将上述代码写入到 testpalindrome.py 文件,执行如下:
$ python3 testpalindrome.pyEnter a string: test111Oh no, not a palindrome$ python3 testpalindrome.pyEnter a string: 1234321Yay a palindrome
局域或全部变量
我们通过几个例子来弄明白局域或全局变量,首先我们在函数内部和函数调用的代码中都使用同一个变量 a,将下方代码写入/home/shiyanlou/local.py:
#!/usr/bin/env python3def change():a = 90print(a)a = 9print("Before the function call ", a)print("inside change function", end=' ')change()print("After the function call ", a)
运行程序:

首先我们对a赋值 9,然后调用更改函数,这个函数里我们对a赋值 90,然后打印a的值。调用函数后我们再次打印a的值。
当我们在函数里写a = 90时,它实际上创建了一个新的名为a的局部变量,这个变量只在函数里可用,并且会在函数完成时销毁。所以即使这两个变量的名字都相同,但事实上他们并不是同一个变量。
那么如果我们先定义a,在函数中是否可以直接使用呢?
例如下面这段代码:
#!/usr/bin/env python3a = 9def change():print(a)change()
这段代码是没有问题的,可以直接打印输出 9。稍微改动一下:
#!/usr/bin/env python3a = 9def change():print(a)a = 100change()
现在就会报错了:“UnboundLocalError: local variable ‘a’ referenced before assignment”,原因是当函数中只要用到了变量 a,并且 a 出现在表达式等于号的前面,就会被当作局部变量。当执行到print(a)的时候会报错,因为 a 作为函数局部变量是在print(a)之后才定义的。
现在我们使用global关键字,对函数中的a标志为全局变量,让函数内部使用全局变量的 a,那么整个程序中出现的a都将是这个:
#!/usr/bin/env python3a = 9def change():global aprint(a)a = 100print("Before the function call ", a)print("inside change function", end=' ')change()print("After the function call ", a)
程序中的end=' '参数表示,print 打印后的结尾不用换行,而用空格。默认情况下 print 打印后会在结尾换行。
程序执行的结果,不会报错了,因为函数体内可以访问全局的变量a:
Before the function call 9inside change function 9After the function call 100
在函数内使用global会有什么作用呢?尝试下面的代码:
#!/usr/bin/env python3def change():global aa = 90print(a)a = 9print("Before the function call ", a)print("inside change function", end=' ')change()print("After the function call ", a)
程序执行的结果:
Before the function call 9inside change function 90After the function call 90
这里通过关键字global来告诉 a 的定义是全局的,因此在函数内部更改了a的值,函数外a的值也实际上更改了。
运行程序:

默认参数值
函数的参数变量可以有默认值,也就是说如果我们对指定的参数变量没有给出任何值则会赋其默认值。
>>> def test(a , b=-99):... if a > b:... return True... else:... return False
在上面的例子里,我们在函数的参数列表写出b = -99。这表示如果调用者未给出b的值,那么b的值默认为-99。这是一个关于默认参数的非常简单的例子。
你可以通过调用函数测试代码。
>>> test(12, 23)False>>> test(12)True
有两个非常重要的地方,第一个是具有默认值的参数后面不能再有普通参数,比如f(a,b=90,c)就是错误的。
第二个是默认值只被赋值一次,因此如果默认值是任何可变对象时会有所不同,比如列表、字典或大多数类的实例。例如,下面的函数在后续调用过程中会累积(前面)传给它的参数:
>>> def f(a, data=[]):... data.append(a)... return data...>>> print(f(1))[1]>>> print(f(2))[1, 2]>>> print(f(3))[1, 2, 3]
要避免这个问题,你可以像下面这样:
>>> def f(a, data=None):... if data is None:... data = []... data.append(a)... return data...>>> print(f(1))[1]>>> print(f(2))[2]
关键字参数
函数可以通过关键字参数的形式来调用,形如keyword = value。如下:
>>> def func(a, b=5, c=10):... print('a is', a, 'and b is', b, 'and c is', c)...>>> func(12, 24)a is 12 and b is 24 and c is 10>>> func(12, c = 24)a is 12 and b is 5 and c is 24>>> func(b=12, c = 24, a = -1)a is -1 and b is 12 and c is 24
在上面的例子中你能看见调用函数时使用了变量名,比如func(12,c = 24),这样我们将24赋给c且b具有默认值。
强制关键字参数
我们也能将函数的参数标记为只允许使用关键字参数。用户调用函数时将只能对每一个参数使用相应的关键字参数。
>>> def hello(*, name='User'):... print("Hello", name)...>>> hello('shiyanlou')Traceback (most recent call last):File "<stdin>", line 1, in <module>TypeError: hello() takes 0 positional arguments but 1 was given>>> hello(name='shiyanlou')Hello shiyanlou
了解更多,请阅读PEP-3102。
文档字符串
在 Python 里我们使用文档字符串(docstrings)来说明如何使用代码,这在交互模式非常有用,也能用于自动创建文档。下面我们来看看使用文档字符串的例子。
#!/usr/bin/env python3import mathdef longest_side(a, b):"""Function to find the length of the longest side of a right triangle.:arg a: Side a of the triangle:arg b: Side b of the triangle:return: Length of the longest side c as float"""return math.sqrt(a*a + b*b)if __name__ == '__main__':print(longest_side.__doc__)print(longest_side(4,5))
运行程序:

高阶函数
高阶函数(Higher-order function)或仿函数(functor)是可以接受函数作为参数的函数:
- 使用一个或多个函数作为参数
- 返回另一个函数作为输出
Python 里的任何函数都可以作为高阶函数,下面举一个简单的例子:
# 创建一个函数,将参数列表中每个元素都变成全大写>>> def high(l):... return [i.upper() for i in l]...# 创建高阶函数,接受一个函数和一个列表作为参数>>> def test(h, l):... return h(l)...>>> l = ['python', 'Linux', 'Git']# 运行高阶函数,返回预期的结果>>> test(high, l)['PYTHON', 'LINUX', 'GIT']
阅读官方文档了解更多。
map 函数
map是一个在 Python 里非常有用的高阶函数。它接受一个函数和一个序列(迭代器)作为输入,然后对序列(迭代器)的每一个值应用这个函数,返回一个序列(迭代器),其包含应用函数后的结果。
举例:
>>> lst = [1, 2, 3, 4, 5]>>> def square(num):... "返回所给数字的平方."... return num * num...>>> print(list(map(square, lst)))[1, 4, 9, 16, 25]
总结
经过本实验应当知道如何定义函数,局域变量和全局变量一定要弄清楚,参数默认值、关键字参数也需要掌握。
另外,其它高级语言常见的函数重载,Python 是没有的,这是因为 Python 有默认参数这个功能,函数重载的功能大都可以使用默认参数达到。
在后面我们还介绍了高阶函数的概念并使用了map()函数。在 Python 中还有其它的高阶函数,如sorted()、filter()以及functools模块中的函数,大家可以了解一下。
9.文件处理
实验介绍
文件是保存在计算机存储设备上的一些信息或数据。你已经知道了一些不同的文件类型,比如你的音乐文件,视频文件,文本文件。Python 给了你一些简单的方式操纵文件。通常我们把文件分为两类,文本文件和二进制文件。文本文件是简单的文本,二进制文件包含了只有计算机可读的二进制数据。
知识点
- 文件打开模式
- 文件读取与写入
with语句
文件操作
本部分实验内容会实践学习以下的知识:
- 文件打开与关闭
- 文件读取与写入
所有的实例程序都需要你在实验环境中完整的输入并执行。
文件打开
我们使用open()函数打开文件。它需要两个参数,第一个参数是文件路径或文件名,第二个是文件的打开模式。模式通常是下面这样的:
"r",以只读模式打开,你只能读取文件但不能编辑/删除文件的任何内容"w",以写入模式打开,如果文件存在将会删除里面的所有内容,然后打开这个文件进行写入"a",以追加模式打开,写入到文件中的任何数据将自动添加到末尾
默认的模式为只读模式,也就是说如果你不提供任何模式,open()函数将会以只读模式打开文件。我们将实验打开一个文件,不过要准备实验材料:
$ wget http://labfile.oss.aliyuncs.com/courses/596/sample.txt

然后进入 Python3 打开这个文件。
>>> fobj = open("sample.txt")>>> fobj<_io.TextIOWrapper name='sample.txt' mode='r' encoding='UTF-8'>
文件关闭
打开文件后我们应该总是关闭文件。我们使用方法close()完成这个操作。
>>> fobj.close()
始终确保你显式关闭每个打开的文件,一旦它的工作完成你没有任何理由保持打开文件。因为程序能打开的文件数量是有上限的。如果你超出了这个限制,没有任何可靠的方法恢复,因此程序可能会崩溃。每个打开的文件关联的数据结构(文件描述符/句柄/文件锁…)都要消耗一些主存资源。因此如果许多打开的文件没用了你可以结束大量的内存浪费,并且文件打开时始终存在数据损坏或丢失的可能性。
文件读取
使用read()方法一次性读取整个文件。
>>> fobj = open("sample.txt")>>> fobj.read()'I love Python\nI love shiyanlou\n'>>> fobj.close()
如果你再一次调用read(),它会返回空字符串因为它已经读取完整个文件。
read(size)有一个可选的参数size,用于指定字符串长度。如果没有指定size或者指定为负数,就会读取并返回整个文件。当文件大小为当前机器内存两倍时,就会产生问题。反之,会尽可能按比较大的size读取和返回数据。
readline()能帮助你每次读取文件的一行。
>>> fobj = open("sample.txt")>>> fobj.readline()'I love Python\n'>>> fobj.readline()'I love shiyanlou\n'>>> fobj.close()
使用readlines()方法读取所有行到一个列表中。
>>> fobj = open('sample.txt')>>> fobj.readlines()['I love Python\n', 'I love shiyanlou\n']>>> fobj.close()
你可以循环遍历文件对象来读取文件中的每一行。
>>> fobj = open('sample.txt')>>> for x in fobj:... print(x, end = '')...I love PythonI love shiyanlou>>> fobj.close()
让我们写一个程序,这个程序接受用户输入的字符串作为将要读取的文件的文件名,并且在屏幕上打印文件内容。
#!/usr/bin/env python3name = input("Enter the file name: ")fobj = open(name)print(fobj.read())fobj.close()
运行程序:
$ cd /home/shiyanlou$ chmod +x test.py$ ./test.pyEnter the file name: sample.txtI love PythonI love shiyanlou
文件写入
让我们通过write()方法打开一个文件然后我们随便写入一些文本。
>>> fobj = open("ircnicks.txt", 'w')>>> fobj.write('powerpork\n')>>> fobj.write('indrag\n')>>> fobj.write('mishti\n')>>> fobj.write('sankarshan')>>> fobj.close()
现在读取我们刚刚创建的文件。
>>> fobj = open('ircnicks.txt')>>> s = fobj.read()>>> fobj.close()>>> print(s)powerporkindragmishtisankarshan
文件操作示例程序
本部分实验将通过两个实例程序来实践之前学习的文件操作方法:
- 拷贝文件程序
- 文本文件信息统计程序
所有的实例程序都需要你在实验环境中完整的输入并执行。
拷贝文件
在这个例子里我们拷贝给定的文本文件到另一个给定的文本文件。
#!/usr/bin/env python3import sysif len(sys.argv) < 3:print("Wrong parameter")print("./copyfile.py file1 file2")sys.exit(1)f1 = open(sys.argv[1])s = f1.read()f1.close()f2 = open(sys.argv[2], 'w')f2.write(s)f2.close()
运行程序:

你可以看到我们在这里使用了一个新模块sys。sys.argv包含所有命令行参数。这个程序的功能完全可以使用 shell 的cp命令替代:在cp后首先输入被拷贝的文件的文件名,然后输入新文件名。
sys.argv的第一个值是命令自身的名字,下面这个程序打印命令行参数。
#!/usr/bin/env python3import sysprint("First value", sys.argv[0])print("All values")for i, x in enumerate(sys.argv):print(i, x)
运行程序:
$ ./argvtest.py Hi thereFirst value ./argvtest.pyAll values0 ./argvtest.py1 Hi2 there
这里我们用到了一个新函数enumerate(iterableobject),在序列中循环时,索引位置和对应值可以使用它同时得到。
文本文件现关信息统计
让我们试着编写一个程序,对任意给定文本文件中的制表符、行、空格进行计数。
代码写入文件/home/shiyanlou/parsefile.py:
#!/usr/bin/env python3import osimport sysdef parse_file(path):"""分析给定文本文件,返回其空格、制表符、行的相关信息:arg path: 要分析的文本文件的路径:return: 包含空格数、制表符数、行数的元组"""fd = open(path)i = 0spaces = 0tabs = 0for i,line in enumerate(fd):spaces += line.count(' ')tabs += line.count('\t')# 现在关闭打开的文件fd.close()# 以元组形式返回结果return spaces, tabs, i + 1def main(path):"""函数用于打印文件分析结果:arg path: 要分析的文本文件的路径:return: 若文件存在则为 True,否则 False"""if os.path.exists(path):spaces, tabs, lines = parse_file(path)print("Spaces {}. tabs {}. lines {}".format(spaces, tabs, lines))return Trueelse:return Falseif __name__ == '__main__':if len(sys.argv) > 1:main(sys.argv[1])else:sys.exit(-1)sys.exit(0)
运行程序:

你可以看到程序有两个函数,main()和parse_file(),parse_file函数真正的分析文件并返回结果,然后在main()函数里打印结果。通过分割代码到一些更小的单元(函数)里,能帮助我们组织代码库并且也更容易为函数编写测试用例。
使用with语句
在实际情况中,我们应该尝试使用with语句处理文件对象,它会在文件用完后会自动关闭,就算发生异常也没关系。它是 try-finally 块的简写:
>>> with open('sample.txt') as fobj:... for line in fobj:... print(line, end = '')...I love PythonI love shiyanlou
实现lscpu命令
在 Linux 下你可以使用lscpu命令来查看当前电脑的 CPU 相关信息,如下图:

实际上lscpu命令是读取/proc/cpuinfo这个文件的信息并美化输出,现在你可以自己写一个 Python 程序以只读模式读取/proc/cpuinfo这个文件,然后打印出来,这样你就有自己的一个 Python 版本的lscpu命令了 :)
记得一行一行的读取文本文件,不要一次性读取整个文件,因为有时候你读取的文件可能比可用内存还大。
总结
回顾本节实验知识点:
- 文件打开模式
- 文件读取与写入
with语句
本实验我们学习了文件的打开与读写,在读写完毕后一定要记得关闭文件,或者使用 with 语句也是极好的。在 Linux 中你还需要注意你操作的文件的权限。Linux 有一个思想是“一切皆文件”,这在实验最后的lscpu的实现中得到了体现。
挑战:字符串操作
介绍
我们需要实现一个程序用来提取文件中的字符串中的数字,然后打印输出。
目标
首先,打开 Xfce 终端,下载所需的测试文件:
$ cd /tmp$ wget http://labfile.oss.aliyuncs.com/courses/790/String.txt
这个文件/tmp/String.txt中存储了一个很长的字符串,需要读取并进行处理。
然后,我们在/home/shiyanlou/Code创建FindDigits.pyPython 脚本:
$ cd /home/shiyanlou/Code$ touch FindDigits.py
FindDigits.py中,我们需要完成以下任务:
- 使用 open 打开文件
/tmp/String.txt并读取其中的字符串 - 提取字符串中的所有数字,并组合成一个新的字符串,然后打印输出
程序执行过程如下:
$ python3 /home/shiyanlou/Code/FindDigits.py
提示语
- 可以使用循环来访问字符串中的单个字符
- 可以使用
isdigit()来判断字符是否为数字 - 使用
print()把新的数字组成的字符串输出 - 代码文件必须保存在
/home/shiyanlou/Code/FindDigits.py
知识点
- 文件读取
- for 循环
- 字符串操作
参考代码
注意:请务必先独立思考获得 PASS 之后再查看参考代码,直接拷贝代码收获不大
/home/shiyanlou/Code/FindDigits.py参考代码:
# 打开并读取文件里的字符串with open('/tmp/String.txt') as f:s = f.read()res = ""# 循环字符串里的每个字符,判断是否为数字for char in s:if char.isdigit():res += charprint(res)
10. 异常
实验介绍
在这个实验我们学习 Python 的异常以及如何在你的代码中处理它们。
知识点
- NameError
- TypeError
- 异常处理(try..except)
- 异常抛出(raise)
- finally 子句
异常
在程序执行过程中发生的任何错误都是异常。每个异常显示一些相关的错误信息,比如你在 Python3 中使用 Python2 独有的语法就会发生SyntaxError:
不小心在行首多打了一个空格就会产生IndentationError:

NameError
当访问一个未定义的变量则会发生 NameError。
>>> print(kushal)Traceback (most recent call last):File "<stdin>", line 1, in <module>NameError: name 'kushal' is not defined
最后一行包含了错误的详细信息,其余行显示它是如何发生(或什么引起该异常)的详细信息。
TypeError
TypeError 也是一种经常出现的异常。当操作或函数应用于不适当类型的对象时引发,一个常见的例子是对整数和字符串做加法。
>>> print(1 + "kushal")Traceback (most recent call last):File "<stdin>", line 1, in <module>TypeError: unsupported operand type(s) for +: 'int' and 'str'
处理异常
我们使用try...except块来处理任意异常。基本的语法像这样:
try:statements to be inside try clausestatement2statement3...except ExceptionName:statements to evaluated in case of ExceptionName happens
它以如下方式工作:
- 首先,执行
try子句 (在try和except关键字之间的部分)。 - 如果没有异常发生,
except子句 在try语句执行完毕后就被忽略了。
如果在try子句执行过程中发生了异常,那么该子句其余的部分就会被忽略。
- 如果异常匹配于
except关键字后面指定的异常类型,就执行对应的except子句。然后继续执行try语句之后的代码。
如果发生了一个异常,在except子句中没有与之匹配的分支,它就会传递到上一级try语句中。
- 如果最终仍找不到对应的处理语句,它就成为一个未处理异常,终止程序运行,显示提示信息。
下面的例子展示了这些情况:
>>> def get_number():... "Returns a float number"... number = float(input("Enter a float number: "))... return number...>>>>>> while True:... try:... print(get_number())... except ValueError:... print("You entered a wrong value.")...Enter a float number: 45.045.0Enter a float number: 24,0You entered a wrong value.Enter a float number: Traceback (most recent call last):File "<stdin>", line 3, in <module>File "<stdin>", line 3, in get_numberKeyboardInterrupt
首先我输入了一个合适的浮点值,解释器返回输出这个值。
然后我输入以逗号分隔的值,抛出ValueError异常,except子句捕获之,并且打印出错误信息。
第三次我按下Ctrl + C,导致了KeyboardInterrupt异常发生,这个异常并未在except块中捕获,因此程序执行被中止。
一个空的except语句能捕获任何异常。阅读下面的代码:
>>> try:... input() # 输入的时候按下 Ctrl + C 产生 KeyboardInterrupt... except:... print("Unknown Exception")...Unknown Exception
抛出异常
使用raise语句抛出一个异常。
>>> raise ValueError("A value error happened.")Traceback (most recent call last):File "<stdin>", line 1, in <module>ValueError: A value error happened.
我们可以捕获任何其它普通异常一样,来捕获这些异常。
>>> try:... raise ValueError("A value error happened.")... except ValueError:... print("ValueError in our code.")...ValueError in our code.
定义清理行为
try语句还有另一个可选的finally子句,目的在于定义在任何情况下都一定要执行的功能。例如:
>>> try:... raise KeyboardInterrupt... finally:... print('Goodbye, world!')...Goodbye, world!KeyboardInterruptTraceback (most recent call last):File "<stdin>", line 2, in ?
不管有没有发生异常,finally子句 在程序离开try后都一定会被执行。当try语句中发生了未被except捕获的异常(或者它发生在except或else子句中),在finally子句执行完后它会被重新抛出。
在真实场景的应用程序中,finally子句用于释放外部资源(文件或网络连接之类的),无论它们的使用过程中是否出错。
总结
本实验我们知道了异常是什么,然后怎样处理异常以及抛出异常。记得在前面说过的with语句吧,它是try-finally块的简写,使用with语句能保证文件始终被关闭。
异常是什么?其实异常是一种类,而类将会在下一个实验介绍。
挑战:玩转函数
介绍
本次挑战中我们将实现一个程序,将分钟转为小时和分钟。
注意:代码中不要使用input()函数,否则挑战测试会卡住,出现Timeout的报错。
目标
请在/home/shiyanlou/Code创建代码文件MinutesToHours.py:
$ cd /home/shiyanlou/Code$ touch MinutesToHours.py
在 MinutesToHours.py 文件中实现一个函数Hours(),将用户输入的分钟数转化为小时数和分钟数,并要求小时数尽量大。将结果以XX H, XX M的形式打印出来。
要求:
- 用户能够通过命令行参数输入分钟数,不要使用
input,命令行参数可以使用sys.argv来提取。例如程序执行为python3 MinutesToHours.py 80,传入的参数 80 就是分钟数,程序需要打印出相应的小时数和分钟数,输出为1 H, 20 M。 - 如果用户输入的是一个负值,程序需要 raise 来抛出
ValueError异常。 Hours()函数调用的时候,需要使用try...except处理异常。获取异常后,在屏幕上打印出Parameter Error提示用户输入的值有误。
操作实例:
$ python3 /home/shiyanlou/Code/MinutesToHours.py 801 H, 20 M$ python3 /home/shiyanlou/Code/MinutesToHours.py 951 H, 35 M$ python3 /home/shiyanlou/Code/MinutesToHours.py -10Parameter Error$ python3 /home/shiyanlou/Code/MinutesToHours.py abcdParameter Error
提示语
sys.argv获取命令行参数,注意获取的参数为字符串,可以使用int()将字符串转为整数,此处也可能会出现异常情况,例如输入为 “abcd” 是无法转为整数的raise语句try...except语句
知识点
- 异常
- 文件处理
- if-else
参考代码
注意:请务必先独立思考获得 PASS 之后再查看参考代码,直接拷贝代码收获不大
/home/shiyanlou/Code/MinutesToHours.py参考代码:
import sys# 转换函数def Hours(minute):# 如果为负数则 raise 异常if minute < 0:raise ValueError("Input number cannot be negative")else:print("{} H, {} M".format(int(minute / 60), minute % 60))# 函数调用及异常处理逻辑try:Hours(int(sys.argv[1]))except:print("Parameter Error")
11.类
实验介绍
在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。
知识点
- 类的定义
- 对象初始化
实验步骤
本部分实验中将通过定义一些简单的 Python 类,来学习 Python 面向对象编程的基本概念。
实验安排如下:
- 定义简单的类
- init 方法
- Python 中的继承
- 多继承
- 删除对象
- 属性读取方法
@property装饰器
定义类
在写你的第一个类之前,你应该知道它的语法。我们以下面这种方式定义类:
class nameoftheclass(parent_class):statement1statement2statement3
在类的声明中你可以写任何 Python 语句,包括定义函数(在类中我们称为方法)。
>>> class MyClass(object):... """A simple example class"""... i = 12345... def f(self):... return 'hello world'
\init_方法
类的实例化使用函数符号。只要将类对象看作是一个返回新的类实例的无参数函数即可。例如(假设沿用前面的类):
x = MyClass()
以上创建了一个新的类实例并将该对象赋给局部变量x。
这个实例化操作创建一个空的对象。很多类都倾向于将对象创建为有初始状态的。因此类可能会定义一个名为__init__()的特殊方法,像下面这样:
def __init__(self):self.data = []
类定义了__init__()方法的话,类的实例化操作会自动为新创建的类实例调用__init__()方法。所以在下例中,可以这样创建一个新的实例:
x = MyClass()
当然,出于弹性的需要,__init__()方法可以有参数。事实上,参数通过__init__()传递到类的实例化操作上。例如:
>>> class Complex:... def __init__(self, realpart, imagpart):... self.r = realpart... self.i = imagpart...>>> x = Complex(3.0, -4.5)>>> x.r, x.i(3.0, -4.5)
继承
当一个类继承另一个类时,它将继承父类的所有功能(如变量和方法)。这有助于重用代码。
在下一个例子中我们首先创建一个叫做Person的类,然后创建两个派生类Student和Teacher。当两个类都从Person类继承时,它们的类除了会有Person类的所有方法还会有自身用途的新方法和新变量。
2.3.1 student_teacher.py
代码写入文件/home/shiyanlou/student_teacher.py:
#!/usr/bin/env python3class Person(object):"""返回具有给定名称的 Person 对象"""def __init__(self, name):self.name = namedef get_details(self):"""返回包含人名的字符串"""return self.nameclass Student(Person):"""返回 Student 对象,采用 name, branch, year 3 个参数"""def __init__(self, name, branch, year):Person.__init__(self, name)self.branch = branchself.year = yeardef get_details(self):"""返回包含学生具体信息的字符串"""return "{} studies {} and is in {} year.".format(self.name, self.branch, self.year)class Teacher(Person):"""返回 Teacher 对象,采用字符串列表作为参数"""def __init__(self, name, papers):Person.__init__(self, name)self.papers = papersdef get_details(self):return "{} teaches {}".format(self.name, ','.join(self.papers))person1 = Person('Sachin')student1 = Student('Kushal', 'CSE', 2005)teacher1 = Teacher('Prashad', ['C', 'C++'])print(person1.get_details())print(student1.get_details())print(teacher1.get_details())
运行程序

在这个例子中你能看到我们是怎样在Student类和Teacher类中调用Person类的__init__方法。
我们也在Student类和Teacher类中重写了Person类的get_details()方法。
因此,当我们调用student1和teacher1的get_details()方法时,使用的是各自类(Student和Teacher)中定义的方法。
多继承
一个类可以继承自多个类,具有父类的所有变量和方法,语法如下:
class MyClass(Parentclass1, Parentclass2,...):def __init__(self):Parentclass1.__init__(self)Parentclass2.__init__(self)......
这里没有提供额外的实例,将会在其他实验楼的项目实战中用到。
删除对象
现在我们已经知道怎样创建对象,现在我们来看看怎样删除一个对象。我们使用关键字del来做到这个。
>>> s = "I love you">>> del s>>> sTraceback (most recent call last):File "<stdin>", line 1, in <module>NameError: name 's' is not defined
del实际上使对象的引用计数减少一,当对象的引用计数变成零的时候,垃圾回收器会删除这个对象。
属性(attributes)读取方法
在 Python 里请不要使用属性(attributes)读取方法(getters和setters)。如果你之前学过其它语言(比如 Java),你可能会想要在你的类里面定义属性读取方法。请不要这样做,直接使用属性就可以了,就像下面这样:
>>> class Student(object):... def __init__(self, name):... self.name = name...>>> std = Student("Kushal Das")>>> print(std.name)Kushal Das>>> std.name = "Python">>> print(std.name)Python
装饰器
你可能想要更精确的调整控制属性访问权限,你可以使用@property装饰器,@property装饰器就是负责把一个方法变成属性调用的。
下面有个银行账号的例子,我们要确保没人能设置金额为负,并且有个只读属性 cny 返回换算人民币后的金额。
代码写入文件/home/shiyanlou/property.py
#!/usr/bin/env python3class Account(object):"""账号类,amount 是美元金额."""def __init__(self, rate):self.__amt = 0self.rate = rate@propertydef amount(self):"""账号余额(美元)"""return self.__amt@propertydef cny(self):"""账号余额(人民币)"""return self.__amt * self.rate@amount.setterdef amount(self, value):if value < 0:print("Sorry, no negative amount in the account.")returnself.__amt = valueif __name__ == '__main__':acc = Account(rate=6.6) # 基于课程编写时的汇率acc.amount = 20print("Dollar amount:", acc.amount)print("In CNY:", acc.cny)acc.amount = -100print("Dollar amount:", acc.amount)
运行程序:

总结
实验知识点回顾:
- 类的定义
- 对象初始化
本实验我们了解了类的定义,类的继承以及多继承,并且最后我们还接触了装饰器这个概念,本质上,装饰器也是一种高阶函数。
12.模块
实验介绍
在这个实验我们将要学习 Python 模块相关知识。包括模块的概念和导入方法,包的概念和使用,第三方模块的介绍,命令行参数的使用等。
知识点
- 模块的导入
- 包
- 默认/第三方模块介绍
- 命令行参数
模块
到目前为止,我们在 Python 解释器中写的所有代码都在我们退出解释器的时候丢失了。但是当人们编写大型程序的时候他们会倾向于将代码分为多个不同的文件以便使用,调试以及拥有更好的可读性。在 Python 中我们使用模块来到达这些目的。模块是包括 Python 定义和声明的文件。文件名就是模块名加上.py后缀。
你可以由全局变量__name__得到模块的模块名(一个字符串)。
现在我们来看看模块是怎样工作的。创建一个bars.py文件。文件内容如下:
"""Bars Module============这是一个打印不同分割线的示例模块"""def starbar(num):"""打印 * 分割线:arg num: 线长"""print('*' * num)def hashbar(num):"""打印 # 分割线:arg num: 线长"""print('#' * num)def simplebar(num):"""打印 - 分割线:arg num: 线长"""print('-' * num)
现在我们启动解释器然后导入我们的模块。
>>> import bars>>>
我们必须使用模块名来访问模块内的函数。
>>> bars.hashbar(10)##########>>> bars.simplebar(10)---------->>> bars.starbar(10)**********
导入模块
有不同的方式导入模块。我们已经看到过一种了。你甚至可以从模块中导入指定的函数。这样做:
>>> from bars import simplebar, starbar>>> simplebar(20)--------------------
你也可以使用from module import *导入模块中的所有定义,然而这并不是推荐的做法。
包
含有__init__.py文件的目录可以用来作为一个包,目录里的所有.py文件都是这个包的子模块。
本节实验将创建下面的 mymodule 目录,目录结构如下:

在这个例子中,mymodule是一个包名并且bars和utils是里面的两个子模块。
首先创建 mymodule 目录:
$ cd /home/shiyanlou$ mkdir mymodule
然后将上一节编写的bars.py拷贝到 mymodule 目录下,然后可以使用touch创建一个utils.py文件。
使用touch命令创建一个空的__init__.py文件。
$ touch mymodule/__init__.py
如果__init__.py文件内有一个名为__all__的列表,那么只有在列表内列出的名字将会被公开。
因此如果mymodule内的__init__.py文件含有以下内容:
from mymodule.bars import simplebar__all__ = [simplebar, ]
那么导入时将只有simplebar可用。如果你在 python3 解释器中进行测试,需要确定是在 mymodule 目录同级的目录下执行的python3,类似下面的操作,否则会出现ImportError: No module named 'mymodule'的报错。
$ cd /home/shiyanlou$ python3>>>
from mymodule import *只能工作在模块级别的对象上,试图导入函数或类将导致 syntax error。
参考资料
默认模块
现在你安装 Python 的时候会附带安装不同的模块,你可以按需使用它们,也可以为其它特殊用途安装新模块。在下面的几个例子中,我们将要看到同样例子很多。

上面的例子展示了怎样获得你系统中安装的所有模块的列表。在这里就不粘贴它们了,因为这是一个很大的列表。
你也能在解释器里使用help()函数查找任何模块/类的文档。如果你想要知道字符串所有可用的方法,你可以像下面这样做:
>>> help(str)
os模块
os模块提供了与操作系统相关的功能。你可以使用如下语句导入它:
>>> import os
getuid()函数返回当前进程的有效用户 id。
>>> os.getuid()500
getpid()函数返回当前进程的 id。getppid()返回父进程的 id。
>>> os.getpid()16150>>> os.getppid()14847
uname()函数返回识别操作系统的不同信息,在 Linux 中它返回的详细信息可以从uname -a命令得到。uname()返回的对象是一个元组,(sysname, nodename, release, version, machine)。
>>> os.uname()('Linux', 'd80', '2.6.34.7-56.fc13.i686.PAE', '#1 SMP Wed Sep 15 03:27:15 UTC 2010', 'i686')
getcwd() 函数返回当前工作目录。chdir(path)则是更改当前目录到 path。在例子中我们首先看到当前工作目录是/home/shiyanlou,然后我们更改当前工作目录到/Code并再一次查看当前工作目录。
>>> os.getcwd()'/home/shiyanlou'>>> os.chdir('Code')>>> os.getcwd()'/home/shiyanlou/Code'
所以现在让我们使用 os 模块提供的另一个函数来创建一个自己的函数,它将列出给定目录下的所有文件和目录。
def view_dir(path='.'):"""这个函数打印给定目录中的所有文件和目录:args path: 指定目录,默认为当前目录"""names = os.listdir(path)names.sort()for name in names:print(name, end =' ')print()
使用例子中的view_dir()函数。
>>> view_dir('/').bashrc .dockerenv .profile bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
os 模块还有许多非常有用的函数,你可以在这里阅读相关内容。
Requests模块
Requests是一个第三方 Python 模块,其官网的介绍如下:
Requests 唯一的一个非转基因的 Python HTTP 库,人类可以安全享用。
警告:非专业使用其他 HTTP 库会导致危险的副作用,包括:安全缺陷症、冗余代码症、重新发明轮子症、啃文档症、抑郁、头疼、甚至死亡。
第三方模块并不是默认的模块,意味着你需要安装它,我们使用pip3安装它。
首先要安装pip3:
$ sudo apt-get update$ sudo apt-get install python3-pip
然后用pip3安装requests
$ sudo pip3 install requests
上面的命令会在你的系统中安装 Python3 版本的 Requests 模块。
4.2.1 获得一个简单的网页
你可以使用get()方法获取任意一个网页。
>>> import requests>>> req = requests.get('https://github.com')>>> req.status_code200
req的text属性存有服务器返回的 HTML 网页,由于 HTML 文本太长就不在这里贴出来了。
使用这个知识,让我们写一个能够从指定的 URL 中下载文件的程序。
代码写入文件/home/shiyanlou/download.py:
#!/usr/bin/env python3import requestsdef download(url):'''从指定的 URL 中下载文件并存储到当前目录url: 要下载页面内容的网址'''# 检查 URL 是否存在try:req = requests.get(url)except requests.exceptions.MissingSchema:print('Invalid URL "{}"'.format(url))return# 检查是否成功访问了该网站if req.status_code == 403:print('You do not have the authority to access this page.')returnfilename = url.split('/')[-1]with open(filename, 'w') as fobj:fobj.write(req.content.decode('utf-8'))print("Download over.")if __name__ == '__main__':url = input('Enter a URL: ')download(url)
测试一下程序:

可以看到目录下已经多了一个 sample.txt 文件。
你可能已经注意到了if __name__ == '__main__':这条语句,它的作用是,只有在当前模块名为__main__的时候(即作为脚本执行的时候)才会执行此if块内的语句。换句话说,当此文件以模块的形式导入到其它文件中时,if块内的语句并不会执行。
你可以将上面的程序修改的更友好些。举个例子,你可以检查当前目录是否已存在相同的文件名。os.path模块可以帮助你完成这个。
argparse 命令行参数处理模块
你还记得ls命令吗,你可以传递不同的选项作为命令行参数。
这里是用到的模块是sys,命令行传入的所有参数都可以使用sys.argv获取。如果希望对参数进行处理可以使用argparse模块,阅读这篇文档学习。
TAB补全
首先创建一个文件:~/.pythonrc,文件内写入如下内容:
import rlcompleter, readlinereadline.parse_and_bind('tab: complete')history_file = os.path.expanduser('~/.python_history')readline.read_history_file(history_file)import atexitatexit.register(readline.write_history_file, history_file)
下一步在~/.bashrc文件中设置 PYTHONSTARTUP 环境变量指向这个文件:
$ export PYTHONSTARTUP=~/.pythonrc
现在,从今以后每当你打开 bash shell,你将会有 TAB 补全和 Python 解释器中代码输入的历史记录。
要在当前 shell 中使用,source 这个 bashrc 文件。
$ source ~/.bashrc
总结
本实验了解了什么是模块,模块怎样导入,举例了 os 和 Requests 模块的使用。Python 吸引人的一点是其有众多的模块可以使用,对于自带模块,可以看看 Python3 的官方文档,对于第三方模块,可以在PyPI上找找。很多时候你都能找到合适的包帮你优雅的完成部分工作。比如argparse模块帮你非常容易的编写用户友好的命令行接口。
13.Collections 模块
实验介绍
collections 是 Python 内建的一个集合模块,提供了许多有用的集合类。
知识点
- Counter 类
- defaultdict 类
- namedtuple 类
Counter
在这个实验我们会学习Collections模块。这个模块实现了一些很好的数据结构,它们能帮助你解决各种实际问题。
>>> import collections
这是如何导入这个模块,现在我们来看看其中的一些类。
Counter是一个有助于hashable对象计数的 dict 子类。它是一个无序的集合,其中hashable对象的元素存储为字典的键,它们的计数存储为字典的值,计数可以为任意整数,包括零和负数。
我们可以这样查看Counter的帮助信息,事实上这些信息来源于 Counter 的文档字符串(collections.Counter.__doc__)。


下面我们来看一个例子,例子中我们查看 Python 的 LICENSE 文件中某些单词出现的次数。
Counter 示例
>>> from collections import Counter>>> import re>>> path = '/usr/lib/python3.5/LICENSE.txt'>>> words = re.findall('\w+', open(path).read().lower())>>> Counter(words).most_common(10)[('the', 80), ('or', 78), ('1', 66), ('of', 61), ('to', 50), ('and', 48), ('python', 46), ('in', 38), ('license', 37), ('any', 37)]
Counter 对象有一个叫做elements()的方法,其返回的序列中,依照计数重复元素相同次数,元素顺序是无序的。
>>> c = Counter(a=4, b=2, c=0, d=-2)>>> list(c.elements())['b','b','a', 'a', 'a', 'a']
most_common()方法返回最常见的元素及其计数,顺序为最常见到最少。
>>> Counter('abracadabra').most_common(3)[('a', 5), ('r', 2), ('b', 2)]
defaultdict
defaultdict是内建dict类的子类,它覆写了一个方法并添加了一个可写的实例变量。其余功能与字典相同。
defaultdict()第一个参数提供了default_factory属性的初始值,默认值为None,default_factory属性值将作为字典的默认数据类型。所有剩余的参数与字典的构造方法相同,包括关键字参数。
同样的功能使用defaultdict比使用dict.setdefault方法快。
defaultdict 用例
>>> from collections import defaultdict>>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]>>> d = defaultdict(list)>>> for k, v in s:... d[k].append(v)...>>> d.items()dict_items([('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])])
在例子中你可以看到,即使defaultdict对象不存在某个键,它会自动创建一个空列表。
namedtuple
命名元组有助于对元组每个位置赋予意义,并且让我们的代码有更好的可读性和自文档性。你可以在任何使用元组地方使用命名元组。在例子中我们会创建一个命名元组以展示为元组每个位置保存信息。
>>> from collections import namedtuple>>> Point = namedtuple('Point', ['x', 'y']) # 定义命名元组>>> p = Point(10, y=20) # 创建一个对象>>> pPoint(x=10, y=20)>>> p.x + p.y30>>> p[0] + p[1] # 像普通元组那样访问元素30>>> x, y = p # 元组拆封>>> x10>>> y20
总结
本节知识点回顾:
- Counter 类
- defaultdict 类
- namedtuple 类
这个实验我们使用了 Collections 中的一些数据结构,可能你目前并用不上他,但希望你以后需要的时候会想起它们。
挑战:类和Collection
介绍
本次挑战中我们将通过改写之前实验中的student_teacher.py程序实现更加丰富的功能。
目标
改写 我们在类这个实验中继承部分的student_teacher.py脚本,实现以下功能:
- 在
Person()类中增添函数get_grade() - 对于教师类,
get_grade()函数可以自动统计出老师班上学生的得分情况并按照频率的高低以A: X, B: X, C: X, D: X的形式打印出来 - 对于学生类,
get_grade()函数则可以以Pass: X, Fail: X来统计自己的成绩情况(A,B,C 为 Pass, 如果得了 D 就认为是 Fail)。
student_teacher.py文件可以通过在 Xfce 终端中输入如下代码来获取
$ cd /home/shiyanlou/Code$ wget http://labfile.oss.aliyuncs.com/courses/790/student_teacher.py
要求:
- 请把最终的
student_teacher.py代码文件放在/home/shiyanlou/Code/路径下 - 根据命令行中的第一个参数
teacher或者student来判断最终输出的格式。 - 命令行中第二个输入的参数是需要统计的字符串
执行实例:

提示语
- Teacher 及 Student 类的
__init__()也要增加 grade 参数 import syscollections中的Counter子类format()以及join
知识点
- 类
- Collection 模块
- 注意最终的打印形式
参考代码
注意:请务必先独立思考获得 PASS 之后再查看参考代码,直接拷贝代码收获不大
/home/shiyanlou/Code/student_teacher.py参考代码:
#!/usr/bin/env python3import sysfrom collections import Counterclass Person(object):"""返回具有给定名称的 Person 对象"""def __init__(self, name):self.name = namedef get_details(self):"""返回包含人名的字符串"""return self.namedef get_grade(self):return 0class Student(Person):"""返回 Student 对象,采用 name, branch, year 3 个参数"""def __init__(self, name, branch, year,grade):Person.__init__(self, name)self.branch = branchself.year = yearself.grade = gradedef get_details(self):"""返回包含学生具体信息的字符串"""return "{} studies {} and is in {} year.".format(self.name, self.branch, self.year)def get_grade(self):common = Counter(self.grade).most_common(4)n1 = 0n2 = 0for item in common:if item[0] != 'D':n1 += item[1]else:n2 += item[1]print("Pass: {}, Fail: {}".format(n1,n2))class Teacher(Person):"""返回 Teacher 对象,采用字符串列表作为参数"""def __init__(self, name, papers, grade):Person.__init__(self, name)self.papers = papersself.grade = gradedef get_details(self):return "{} teaches {}".format(self.name, ','.join(self.papers))def get_grade(self):s = []common = Counter(self.grade).most_common(4)for i,j in common:s.append("{}: {}".format(i,j))print(', '.join(s))person1 = Person('Sachin')if sys.argv[1] == "student":student1 = Student('Kushal', 'CSE', 2005, sys.argv[2])student1.get_grade()else:teacher1 = Teacher('Prashad', ['C', 'C++'], sys.argv[2])teacher1.get_grade()
14.PEP8 代码风格指南
实验介绍
编程语言不是艺术,而是工作或者说是工具,所以整理并遵循一套编码规范是十分必要的。 这篇文章原文实际上来自于这里:https://www.python.org/dev/peps/pep-0008/
知识点
- 代码排版
- 字符串引号
- 表达式和语句中的空格
- 注释
- 版本注记
- 命名约定
- 公共和内部接口
- 程序编写建议
实验步骤
建议在实验楼中打开 Python 解释器或者 vim 自己照着做一下,或者看看以前自己写的代码


介绍
这份文档给出的代码约定适用于主要的 Python 发行版所有标准库中的 Python 代码。请参阅相似的 PEP 信息,其用于描述实现 Python 的 C 代码规范[1]。
这份文档和PEP 257(文档字符串约定) 改编自 Guido 的 Python 风格指南原文,从 Barry 的风格指南里添加了一些东西[2]。
随着时间的推移,这份额外约定的风格指南已经被认可了,过去的约定由于语言自身的发展被淘汰了。
许多项目有它们自己的编码风格指南。如果有冲突,优先考虑项目规定的编码指南。
愚蠢的一致性就像没脑子的妖怪
Guido 的一个主要见解是读代码多过写代码。这里提供指南的意图是强调代码可读性的重要性,并且使大多数 Python 代码保持一致性。如PEP 20所述,“Readability counts”。
风格指南是关于一致性的。风格一致对于本指南来说是重要的,对一个项目来说是更重要的,对于一个模块或者方法来说是最重要的。
但是最最重要的是:知道什么时候应该破例–有时候这份风格指南就是不适用。有疑问时,用你最好的判断力,对比其它的例子来确定这是不是最好的情况,并且不耻下问。
特别说明:不要为了遵守这份风格指南而破坏代码的向后兼容性。
这里有一些好的理由去忽略某个风格指南:
- 当应用风格指南的时候使代码更难读了,对于严格依循风格指南的约定去读代码的人也是不应该的。
- 为了保持和风格指南的一致性同时也打破了现有代码的一致性(可能是历史原因)–虽然这也是一个整理混乱代码的机会(现实中的 XP 风格)。
- 因为问题代码的历史比较久远,修改代码就没有必要性了。
- 当代码需要与旧版本的 Python 保持兼容,而旧版 Python 又不支持风格指南中提到的特性的时候。
代码排版
2.3.1 缩进
每层缩进使用4个空格。
续行要么与圆括号、中括号、花括号这样的被包裹元素保持垂直对齐,要么放在 Python 的隐线(注:应该是相对于def的内部块)内部,或者使用悬挂缩进。使用悬挂缩进的注意事项:第一行不能有参数,用进一步的缩进来把其他行区分开。
好的:
# Aligned with opening delimiter.foo = long_function_name(var_one, var_two,var_three, var_four)# More indentation included to distinguish this from the rest.def long_function_name(var_one, var_two, var_three,var_four):print(var_one)# Hanging indents should add a level.foo = long_function_name(var_one, var_two,var_three, var_four)
不好的:
# Arguments on first line forbidden when not using vertical alignment.foo = long_function_name(var_one, var_two,var_three, var_four)# Further indentation required as indentation is not distinguishable.def long_function_name(var_one, var_two, var_three,var_four):print(var_one)
4空格规则是可选的:
# Hanging indents *may* be indented to other than 4 spaces.foo = long_function_name(var_one, var_two,var_three, var_four)
当 if 语句的条件部分足够长,需要将它写入到多个行,值得注意的是两个连在一起的关键字(i.e. if),添加一个空格,给后续的多行条件添加一个左括号形成自然地4空格缩进。如果和嵌套在 if 语句内的缩进代码块产生了视觉冲突,也应该被自然缩进4个空格。这份增强建议书对于怎样(或是否)把条件行和 if 语句的缩进块在视觉上区分开来是没有明确规定的。可接受的情况包括,但不限于:
# No extra indentation.if (this_is_one_thing andthat_is_another_thing):do_something()# Add a comment, which will provide some distinction in editors# supporting syntax highlighting.if (this_is_one_thing andthat_is_another_thing):# Since both conditions are true, we can frobnicate.do_something()# Add some extra indentation on the conditional continuation line.if (this_is_one_thingand that_is_another_thing):do_something()
在多行结构中的右圆括号、右中括号、右大括号应该放在最后一行的第一个非空白字符的正下方,如下所示:
my_list = [1, 2, 3,4, 5, 6,]result = some_function_that_takes_arguments('a', 'b', 'c','d', 'e', 'f',)
或者放在多行结构的起始行的第一个字符正下方,如下:
my_list = [1, 2, 3,4, 5, 6,]result = some_function_that_takes_arguments('a', 'b', 'c','d', 'e', 'f',)
2.3.2 制表符还是空格?
空格是首选的缩进方法。
制表符(Tab)应该被用在那些以前就使用了制表符缩进的地方。
Python 3 不允许混合使用制表符和空格来缩进代码。
混合使用制表符和空格缩进的 Python 2 代码应该改为只使用空格。
当使用-t选项来调用 Python 2 的命令行解释器的时候,会对混合使用制表符和空格的代码发出警告。当使用-tt选项的时候,这些警告会变成错误。这些选项是强烈推荐的!
2.3.3 每行最大长度
限制每行的最大长度为79个字符。
对于那些约束很少的文本结构(文档字符串或注释)的长块,应该限制每行长度为72个字符。
限制编辑窗口的宽度使并排打开两个窗口成为可能,使用通过代码审查工具时,也能很好的通过相邻列展现不同代码版本。
一些工具的默认换行设置打乱了代码的可视结构,使其更难理解。限制编辑器窗口宽为80来避免自动换行,即使有些编辑工具在换行的时候会在最后一列放一个标识符。一些基于 Web 的工具可能根本就不提供动态换行。
一些团队更倾向于长的代码行。对于达成了一致意见来统一代码的团队而言,把行提升到80~100的长度是可接受的(实际最大长度为99个字符),注释和文档字符串的长度还是建议在72个字符内。
Python 标准库是非常专业的,限制最大代码长度为79个字符(注释和文档字符串最大长度为72个字符)。
首选的换行方式是在括号(小中大)内隐式换行(非续行符\)。长行应该在括号表达式的包裹下换行。这比反斜杠作为续行符更好。
反斜杠有时仍然适用。例如,多个很长的with语句不能使用隐式续行,因此反斜杠是可接受的。
with open('/path/to/some/file/you/want/to/read') as file_1, \open('/path/to/some/file/being/written', 'w') as file_2:file_2.write(file_1.read())
(见前面关于多行if语句的讨论来进一步思考这种多行with语句该如何缩进)
另一种使用反斜杠续行的案例是assert语句。
确保续行的缩进是恰到好处的。遇到二元操作符,首选的断行位置是操作符的后面而不是前面。这有一些例子:
class Rectangle(Blob):def __init__(self, width, height,color='black', emphasis=None, highlight=0):if (width == 0 and height == 0 andcolor == 'red' and emphasis == 'strong' orhighlight > 100):raise ValueError("sorry, you lose")if width == 0 and height == 0 and (color == 'red' oremphasis is None):raise ValueError("I don't think so -- values are %s, %s" %(width, height))Blob.__init__(self, width, height,color, emphasis, highlight)
2.3.4 空行
顶级函数和类定义上下使用两个空行分隔。
类内的方法定义使用一个空行分隔。
可以使用额外的空行(有节制的)来分隔相关联的函数组。在一系列相关联的单行代码中空行可以省略(e.g. 一组虚拟的实现)。
在函数中使用空白行(有节制的)来表明逻辑部分。
Python 接受使用换页符(i.e.Ctrl+L)作为空格;许多工具都把Ctrl+L作为分页符,因此你可以用它们把你的文件中相似的章节分页。注意,一些编辑器和基于 Web 的代码查看工具可能不把Ctrl+L看做分页符,而是在这个位置放一个其它的符号。
2.3.5 源文件编码
在核心 Python 发布版中的代码应该总是使用UTF-8编码(或者在 Python 2 中使用ASCII)。
使用ASCII(Python 2)或UTF-8(Python 3)的文件不需要有编码声明(注:它们是默认的)。
在标准库中,非缺省的编码应该仅仅用于测试目的,或者注释或文档字符串中的作者名包含非ASCII码字符;否则,优先使用\x、\u、\U或者\N来转义字符串中的非ASCII数据。
对于 Python 3.0 和之后的版本,以下是有关标准库的政策(见PEP 3131):所有 Python 标准库中的标识符必须使用只含ASCII的标识,并且应该使用英语单词只要可行(在多数情况下,缩略语和技术术语哪个不是英语)。此外,字符串和注释也必须是ASCII。仅有的例外是:(a)测试用例测试非ASCII特性时,(b)作者名。作者的名字不是基于拉丁字母的必须提供他们名字的拉丁字母音译。
面向全球用户的开源项目,鼓励采取相似的政策。
2.3.6. 导入包
import不同的模块应该独立一行,如:
- 好的:
import osimport sys
不好的:
import sys, os
这样也是可行的:
from subprocess import Popen, PIPE
import语句应该总是放在文件的顶部,在模块注释和文档字符串之下,在模块全局变量和常量之前。
import语句分组顺序如下:
* 导入标准库模块* 导入相关第三方库模块* 导入当前应用程序/库模块
每组之间应该用空行分开。
- 然后用
__all__声明本文件内的模块。 - 绝对导入是推荐的,它们通常是更可读的,并且在错误的包系统配置(如一个目录包含一个以
os.path结尾的包)下有良好的行为倾向(至少有更清晰的错误消息):
import mypkg.siblingfrom mypkg import siblingfrom mypkg.sibling import example
当然,相对于绝对导入,相对导入是个可选替代,特别是处理复杂的包结构时,绝对导入会有不必要的冗余:
from . import siblingfrom .sibling import example
标准库代码应该避免复杂的包结构,并且永远使用绝对导入。
应该从不使用隐式的相对导入,而且在 Python 3 中已经被移除。
- 从一个包含类的模块导入类时,这样写通常是可行的:
from myclass import MyClassfrom foo.bar.yourclass import YourClass
如果上面的方式会本地导致命名冲突,则这样写:
import myclassimport foo.bar.yourclass
以myclass.MyClass和foo.bar.yourclass.YourClass这样的方式使用。
应该避免通配符导入(from import *),这会使名称空间里存在的名称变得不清晰,迷惑读者和自动化工具。这里有一个可辩护的通配符导入用例,,重新发布一个内部接口作为公共 API 的一部分(例如,使用纯 Python 实现一个可选的加速器模块的接口,但并不能预知这些定义会被覆盖)。
- 当以这种方式重新发布名称时,下面关于公共和内部接口的指南仍然适用。
字符串引号
在 Python 里面,单引号字符串和双引号字符串是相同的。这份指南对这个不会有所建议。选择一种方式并坚持使用。一个字符串同时包含单引号和双引号字符时,用另外一种来包裹字符串,而不是使用反斜杠来转义,以提高可读性。
对于三引号字符串,总是使用双引号字符来保持与文档字符串约定的一致性(PEP 257)。
表达式和语句中的空格
2.5.1 不能忍受的情况
避免在下列情况中使用多余的空格:
- 与括号保持紧凑(小括号、中括号、大括号):
Yes: spam(ham[1], {eggs: 2})No: spam( ham[ 1 ], { eggs: 2 } )
- 与后面的逗号、分号或冒号保持紧凑:
Yes: if x == 4: print x, y; x, y = y, xNo: if x == 4 : print x , y ; x , y = y , x
切片内的冒号就像二元操作符一样,任意一侧应该被等同对待(把它当做一个极低优先级的操作)。在一个可扩展的切片中,冒号两侧必须有相同的空格数量。例外:切片参数省略时,空格也省略。
- 好的:
ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]ham[lower:upper], ham[lower:upper:], ham[lower::step]ham[lower+offset : upper+offset]ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]ham[lower + offset : upper + offset]
不好的:
ham[lower + offset:upper + offset]ham[1: 9], ham[1 :9], ham[1:9 :3]ham[lower : : upper]ham[ : upper]
- 函数名与其后参数列表的左括号应该保持紧凑:
Yes: spam(1)No: spam (1)
- 与切片或索引的左括号保持紧凑:
Yes: dct['key'] = lst[index]No: dct ['key'] = lst [index]
在赋值操作符(或其它)的两侧保持多余一个的空格:
- 好的:
x = 1y = 2long_variable = 3
不好的:
x = 1y = 2long_variable = 3
2.5.2 其他建议
- 总是在这些二元操作符的两侧加入一个空格:赋值(=),增量赋值(+=, -= etc.),比较(==, <, >, !=, <>, <=, >=, in, not in, is, is not),布尔运算(and, or, not)。
在不同优先级之间,考虑在更低优先级的操作符两侧插入空格。用你自己的判断力;但不要使用超过一个空格,并且在二元操作符的两侧有相同的空格数。
- 好的:
i = i + 1submitted += 1x = x*2 - 1hypot2 = x*x + y*yc = (a+b) * (a-b)
不好的:
i=i+1submitted +=1x = x * 2 - 1hypot2 = x * x + y * yc = (a + b) * (a - b)
不要在关键值参数或默认值参数的等号两边加入空格。
- 好的:
def complex(real, imag=0.0):return magic(r=real, i=imag)
不好的:
def complex(real, imag = 0.0):return magic(r = real, i = imag)
【注:Python 3】带注释的函数定义中的等号两侧要各插入空格。此外,在冒号后用一个单独的空格,也要在表明函数返回值类型的->左右各插入一个空格。
- 好的:
def munge(input: AnyStr):def munge(sep: AnyStr = None):def munge() -> AnyStr:def munge(input: AnyStr, sep: AnyStr = None, limit=1000):
不好的:
def munge(input: AnyStr=None):def munge(input:AnyStr):def munge(input: AnyStr)->PosInt:
打消使用复合语句(多条语句在同一行)的念头。
- 好的:
if foo == 'blah':do_blah_thing()do_one()do_two()do_three()
宁可不:
if foo == 'blah': do_blah_thing()do_one(); do_two(); do_three()
有时候把if/for/while和一个小的主体放在同一行也是可行的,千万不要在有多条语句的情况下这样做。此外,还要避免折叠,例如长行。
- 宁可不:
if foo == 'blah': do_blah_thing()for x in lst: total += xwhile t < 10: t = delay()
绝对不:
if foo == 'blah': do_blah_thing()else: do_non_blah_thing()try: something()finally: cleanup()do_one(); do_two(); do_three(long, argument,list, like, this)if foo == 'blah': one(); two(); three()
注释
与代码相矛盾的注释不如没有。注释总是随着代码的变更而更新。
注释应该是完整的句子。如果注释是一个短语或语句,第一个单词应该大写,除非是一个开头是小写的标识符(从不改变标识符的大小写)。
如果注释很短,末尾的句点可以省略。块注释通常由一个或多个有完整句子的段落组成,并且每个句子应该由句点结束。
你应该在一个句子的句点后面用两个空格。
写英语时,遵循《Strunk and White》(注:《英文写作指南》,参考维基百科)。
来自非英语国家的程序员:请用英语写注释,除非你120%确定你的代码永远不会被那些不说你的语言的人阅读。
2.6.1 块注释
块注释通常用来说明跟随在其后的代码,应该与那些代码有相同的缩进层次。块注释每一行以#起头,并且#后要跟一个空格(除非是注释内的缩进文本)。
2.6.2 行内注释
有节制的使用行内注释。
一个行内注释与语句在同一行。行内注释应该至少与语句相隔两个空格。以#打头,#后接一个空格。
无谓的行内注释如果状态明显,会转移注意力。不要这样做:
x = x + 1 # Increment x
但有的时候,这样是有用的:
x = x + 1 # Compensate for border
2.6.3 文档字符串
编写良好的文档字符串(a.k.a “docstring”)的约定常驻在PEP 257
- 为所有的公共模块、函数、类和方法编写文档字符串。对于非公共的方法,文档字符串是不必要的,但是也应该有注释来说明代码是干什么的。这个注释应该放在方法声明的下面。
- PEP 257描述了良好的文档字符串的约定。注意,文档字符串的结尾
"""应该放在单独的一行,例如:
"""Return a foobangOptional plotz says to frobnicate the bizbaz first."""
- 对于单行的文档字符串,把结尾
"""放在同一行。
版本注记
如果必须要 Subversion,CVS 或 RCS 标记在你的源文件里,像这样做:
__version__ = "$Revision$"# $Source$
这几行应该在模块的文档字符串后面,其它代码的前面,上下由一个空行分隔。
命名约定
Python 库的命名规则有点混乱,因此我们永远也不会使其完全一致的 – 不过,这里有一些当前推荐的命名标准。新的模块和包(包括第三方框架)应该按照这些标准来命名,但是已存在库有不同的风格,内部一致性是首选。
2.8.1 覆盖原则
API 里对用户可见的公共部分应该遵循约定,反映的是使用而不是实现。
2.8.2 规定:命名约定
有许多不同的命名风格。这有助于识别正在使用的命名风格,独立于它们的用途。
下面的命名风格通常是有区别的:
- b (一个小写字母)
- B (一个大写字母)
- lowercase
- lower_case_with_underscores
- UPPERCASE
- UPPER_CASE_WITH_UNDERSCORES
- CapitalizedWords (又叫 CapWords,或者 CamelCase(骆驼命名法) – 如此命名因为字母看起来崎岖不平[3]。有时候也叫 StudlyCaps。
注意:在 CapWords 使用缩略语时,所有缩略语的首字母都要大写。因此HTTPServerError比HttpServerError要好。
- mixedCase (和上面不同的是首字母小写)
- Capitalized_Words_With_Underscores (丑陋无比!)
也有种风格用独一无二的短前缀来将相似的命名分组。在 Python 里用的不是很多,但是为了完整性被提及。例如,os.stat()函数返回一个元组,通常有像st_mode,st_size,st_mtime等名字。(强调与 POSIX 系统调用的字段结构一致,有助于程序员对此更熟悉)
X11 库的所有公共函数都用 X 打头。在 Python 中这种风格被认为是不重要的,因为属性和方法名的前缀是一个对象,函数名的前缀为一个模块名。
此外,下面的特许形式用一个前导或尾随的下划线进行识别(这些通常可以和任何形式的命名约定组合):
- _single_leading_underscore :仅内部使用的标识,如
from M import *不会导入像这样一下划线开头的对象。 - singletrailing_underscore : 通常是为了避免与 Python 规定的关键字冲突,如
Tkinter.Toplevel(master, class_='ClassName')。 - double_leading_underscore : 命名一个类属性,调用的时候名字会改变(在类
FooBar中,boo变成了_FooBar__boo;见下)。 - doubleleadingandtrailingunderscore :”魔术”对象或属性,活在用户控制的命名空间里。例如,
__init__,`__import和__file`。永远不要像这种方式命名;只把它们作为记录。
2.8.3 规定:命名约定
2.8.3.1 应该避免的名字
永远不要使用单个字符l(小写字母 el),O(大写字母 oh),或I(大写字母 eye)作为变量名。
在一些字体中,这些字符是无法和数字1和0区分开的。试图使用l时用L代替。
2.8.3.2 包和模块名
模块名应该短,且全小写。如果能改善可读性,可以使用下划线。Python 的包名也应该短,全部小写,但是不推荐使用下划线。
因为模块名就是文件名,而一些文件系统是大小写不敏感的,并且自动截断长文件名,所以给模块名取一个短小的名字是非常重要的 – 在 Unix 上这不是问题,但是把代码放到老版本的 Mac, Windows,或者 DOS 上就可能变成一个问题了。
用 C/C++ 给 Python 写一个高性能的扩展(e.g. more object oriented)接口的时候,C/C++ 模块名应该有一个前导下划线。
2.8.3.3 类名
类名通常使用 CapWords 约定。
The naming convention for functions may be used instead in cases where the interface is documented and used primarily as a callable.
注意和内建名称的区分开:大多数内建名称是一个单独的单词(或两个单词一起),CapWords 约定只被用在异常名和内建常量上。
2.8.3.4 异常名
因为异常应该是类,所以类名约定在这里适用。但是,你应该用Error作为你的异常名的后缀(异常实际上是一个错误)。
2.8.3.5 全局变量名
(我们希望这些变量仅仅在一个模块内部使用)这个约定有关诸如此类的变量。
若被设计的模块可以通过from M import *来使用,它应该使用__all__机制来表明那些可以可导出的全局变量,或者使用下划线前缀的全局变量表明其是模块私有的。
2.8.3.6 函数名
函数名应该是小写的,有必要的话用下划线来分隔单词提高可读性。
混合大小写仅仅在上下文都是这种风格的情况下允许存在(如thread.py),这是为了维持向后兼容性。
2.8.3.7 函数和方法参数
总是使用self作为实例方法的第一个参数。
总是使用cls作为类方法的第一个参数。
如果函数参数与保留关键字冲突,通常最好在参数后面添加一个尾随的下划线,而不是使用缩写或胡乱拆减。因此class_比clss要好。(或许避免冲突更好的方式是使用近义词)
2.8.3.8 方法名和实例变量
用函数名的命名规则:全部小写,用下划线分隔单词提高可读性。
用一个且有一个前导的下划线来表明非公有的方法和实例变量。
为了避免与子类变量或方法的命名冲突,用两个前导下划线来调用 Python 的命名改编规则。
Python 命名改编通过添加一个类名:如果类Foo有一个属性叫__a,它不能被这样Foo.__a访问(执着的人可以通过这样Foo._Foo__a来访问)通常,双前导的下划线应该仅仅用来避免与其子类属性的命名冲突。
注意:这里有一些争议有关__names的使用(见下文)。
2.8.3.9 常量
常量通常是模块级的定义,全部大写,单词之间以下划线分隔。例如MAX_OVERFLOW和TOTAL。
2.8.3.10 继承的设计
总是决定一个类的方法和变量(属性)是应该公有还是非公有。如果有疑问,选择非公有;相比把共有属性变非公有,非公有属性变公有会容易得多。
公有属性是你期望给那些与你的类无关的客户端使用的,你应该保证不会出现不向后兼容的改变。非公有的属性是你不打算给其它第三方使用的;你不需要保证非公有的属性不会改变甚至被移除也是可以的。
我们这里不适用“私有”这个术语,因为在 Python 里没有真正的私有属性(一般没有不必要的工作量)。
另一种属性的分类是“子类 API”的一部分(通常在其它语言里叫做“Protected”)。一些类被设计成被继承的,要么扩展要么修改类的某方面行为。设计这样一个类的时候,务必做出明确的决定,哪些是公有的,其将会成为子类 API 的一部分,哪些仅仅是用于你的基类的。
处于这种考虑,给出 Pythonic 的指南:
- 共有属性不应该有前导下划线。
- 如果你的公有属性与保留关键字发生冲突,在你的属性名后面添加一个尾随的下划线。这比使用缩写或胡乱拆减要好。(尽管这条规则,已知某个变量或参数可能是一个类情况下,
cls是首选的命名,特别是作为类方法的第一个参数)
注意一:见上面推荐的类方法参数命名方式。
- 对于简单的公有数据属性,最好的方式是暴露属性名,不要使用复杂的访问属性/修改属性的方法。记住,Python 提供了捷径去提升特性,如果你发现简单的数据属性需要增加功能行为。在这种情况下,使用
properties把功能实现隐藏在简单的数据属性访问语法下面。
注意一:properties仅仅在新式类下工作。 注意二:尽量保持功能行为无边际效应,然而如缓存有边际效应也是好的。 注意三:避免为计算开销大的操作使用properties;属性标记使调用者相信这样来访问(相对来说)是开销很低的。
- 如果你的类是为了被继承,你有不想让子类使用的属性,给属性命名时考虑给它们加上双前导下划线,不要加尾随下划线。这会调用 Python 的名称重整算法,把类名加在属性名前面。避免了命名冲突,当子类不小心命名了和父类属性相同名称的时候。
注意一:注意只是用了简单的类名来重整名字,因此如果子类和父类同名的时候,你仍然有能力避免冲突。
注意二:命名重整有确定的用途,例如调试和__getattr__(),就不太方便。命名重整算法是有据可查的,易于手动执行。
注意三:不是每个人都喜欢命名重整。尽量平衡名称的命名冲突与面向高级调用者的潜在用途。
公共和内部接口
保证所有公有接口的向后兼容性。用户能清晰的区分公有和内部接口是重要的。
文档化的接口考虑公有,除非文档明确的说明它们是暂时的,或者内部接口不保证其的向后兼容性。所有的非文档化的应该被假设为非公开的。
为了更好的支持内省,模块应该用__all__属性来明确规定公有 API 的名字。设置__all__为空list表明模块没有公有 API。
甚至与__all__设置相当,内部接口(包、模块、类、函数、属性或者其它的名字)应该有一个前导的下划线前缀。
被认为是内部的接口,其包含的任何名称空间(包、模块或类)也被认为是内部的。
导入的名称应始终视作一个实现细节。其它模块不能依赖间接访问这些导入的名字,除非它们是包含模块的 API 明确记载的一部分,例如os.path或一个包的__init__模块暴露了来自子模块的功能。
编写程序的建议
代码的编写方式不能对其它 Python 的实现(PyPy、Jython、IronPython、Cython、Psyco,诸如此类的)不利。
- 例如,不要依赖于 CPython 在字符串拼接时的优化实现,像这种语句形式
a += b和a = a + b。即使是 CPython(仅对某些类型起作用) 这种优化也是脆弱的,不是在所有的实现中都不使用引用计数。在库中性能敏感的部分,用''.join形式来代替。这会确保在所有不同的实现中字符串拼接是线性时间的。
比较单例,像None应该用is或is not,从不使用==操作符。
- 当你的真正用意是
if x is not None的时候,当心if x这样的写法 – 例如,测试一个默认值为None的变量或参数是否设置成了其它值,其它值可能是那些布尔值为 false 的类型(如空容器)。
用is not操作符而不是not ... is。虽然这两个表达式是功能相同的,前一个是更可读的,是首选。
- 好的:
if foo is not None:
不好的:
if not foo is None:
用富比较实现排序操作的时候,实现所有六个比较操作符(__eq__、__ne__、__lt__,__le__,__gt__,__ge__)是更好的,而不是依赖其它仅仅运用一个特定比较的代码
为了最大限度的减少工作量,functools.total_ordering()装饰器提供了一个工具去生成缺少的比较方法。
- PEP 207说明了 Python 假定的所有反射规则。因此,解释器可能交换
y > x与x < y,y >= x与x <= y,也可能交换x == y和x != y。sort()和min()操作肯定会使用<操作符,max()函数肯定会使用>操作符。当然,最好是六个操作符都实现,以便不会在其它上下文中有疑惑。
始终使用def语句来代替直接绑定了一个lambda表达式的赋值语句。
- 好的:
def f(x): return 2*x
不好的:
f = lambda x: 2*x
第一种形式意味着函数对象的__name__属性值是'f'而不是'<lambda>'。通常这对异常追踪和字符串表述是更有用的。使用赋值语句消除的唯一好处,lambda表达式可以提供一个显示的def语句不能提供的,如,lambda能镶嵌在一个很长的表达式里。
异常类应派生自Exception而不是BaseException。直接继承自BaseException是为Exception保留的,如果从BaseException继承,捕获到的错误总是错的。
设计异常结构层次,应基于那些可能出现异常的代码,而不是在出现异常后的。编码的时候,以回答“出了什么问题?”为目标,而不是仅仅指出“这里出现了问题”(见PEP 3151一个内建异常结构层次的例子)。
- 类的命名约定适用于异常,如果异常类是一个错误,你应该给异常类加一个后缀
Error。用于非本地流程控制或者其他形式的信号的非错误异常不需要一个特殊的后缀。
适当的使用异常链。在 Python 3 里,raise X from Y用于表明明确的替代者,不丢失原有的回溯信息。
- 有意替换一个内部的异常时(在 Python 2 用
raise X,Python 3.3+ 用raise X from None),确保相关的细节全部转移给了新异常(例如,把KeyError变成AttributeError时保留属性名,或者把原始异常的错误信息嵌在新异常里)。
在 Python 2 里抛出异常时,用raise ValueError('message')代替旧式的raise ValueError, 'message'。
在 Python 3 之后的语法里,旧式的异常抛出方式是非法的。
- 使用括号形式的异常意味着,当你传给异常的参数过长或者包含字符串格式化时,你就不需要使用续行符了,这要感谢括号!
捕获异常时,尽可能使用明确的异常,而不是用一个空的except:语句。
- 例如,用:
try:import platform_specific_moduleexcept ImportError:platform_specific_module = None
一个空的except:语句将会捕获到SystemExit和KeyboardInterrupt异常,很难区分程序的中断到底是Ctrl+C还是其他问题引起的。如果你想捕获程序的所有错误,使用except Exception:(空except:等同于except BaseException)。
一个好的经验是限制使用空except语句,除了这两种情况:
- 如果异常处理程序会打印出或者记录回溯信息;至少用户意识到错误的存在。
- 如果代码需要做一些清理工作,但后面用
raise向上抛出异常。try .. finally是处理这种情况更好的方式。
- 绑定异常给一个名字时,最好使用 Python 2.6 里添加的明确的名字绑定语法:
try:process_data()except Exception as exc:raise DataProcessingFailedError(str(exc))
Python 3 只支持这种语法,避免与基于逗号的旧式语法产生二义性。
- 捕获操作系统错误时,最好使用 Python 3.3 里引进的明确的异常结构层次,而不是自省的
errno值。
此外,对于所有的try/except语句来说,限制try里面有且仅有绝对必要的代码。在强调一次,这能避免屏蔽错误。
- 好的:
try:value = collection[key]except KeyError:return key_not_found(key)else:return handle_value(value)
不好的:
try:# Too broad!return handle_value(collection[key])except KeyError:# Will also catch KeyError raised by handle_value()return key_not_found(key)
- 当资源是本地的特定代码段,用
with语句确保其在使用后被立即干净的清除了,try/finally也是也接受的。
当它们做一些除了获取和释放资源之外的事的时候,上下文管理器应该通过单独的函数或方法调用。例如:
- 好的:
with conn.begin_transaction():do_stuff_in_transaction(conn)
不好的:
with conn:do_stuff_in_transaction(conn)
第二个例子没有提供任何信息来表明__enter__和__exit__方法在完成一个事务后做了一些除了关闭连接以外的其它事。在这种情况下明确是很重要的。
坚持使用return语句。函数内的return语句都应该返回一个表达式,或者None。如果一个return语句返回一个表达式,另一个没有返回值的应该用return None清晰的说明,并且在一个函数的结尾应该明确使用一个return语句(如果有返回值的话)。
- 好的:
def foo(x):if x >= 0:return math.sqrt(x)else:return Nonedef bar(x):if x < 0:return Nonereturn math.sqrt(x)
不好的:
def foo(x):if x >= 0:return math.sqrt(x)def bar(x):if x < 0:returnreturn math.sqrt(x)
用字符串方法代替字符串模块。
- 字符串方法总是更快,与 unicode 字符串共享 API。如果需要向后兼容性覆盖这个规则,需要 Python 2.0 以上的版本。
用''.startswith()和''.endswith()代替字符串切片来检查前缀和后缀。
startswith()和endswith()是更简洁的,不容易出错的。例如:
Yes: if foo.startswith('bar'):No: if foo[:3] == 'bar':
- 对象类型的比较应该始终使用
isinstance()而不是直接比较。
Yes: if isinstance(obj, int):No: if type(obj) is type(1):
当比较一个对象是不是字符串时,记住它有可能也是一个 unicode 字符串!在 Python 2 里面,str和unicode有一个公共的基类叫basestring,因此你可以这样做:
if isinstance(obj, basestring):
注意,在 Python 3 里面,unicode和basestring已经不存在了(只有str),byte对象不再是字符串的一种(被一个整数序列替代)。
对于序列(字符串、列表、元组)来说,空的序列为False:
- 好的:
if not seq:if seq:
不好的:
if len(seq):if not len(seq):
- 不要让字符串对尾随的空格有依赖。这样的尾随空格是视觉上无法区分的,一些编辑器(or more recently, reindent.py)会将其裁剪掉。
- 不要用
==比较True和False。
Yes: if greeting:No: if greeting == True:Worse: if greeting is True:
Python 标准库将不再使用函数标注,以至于给特殊的标注风格给一个过早的承若。代替的,这些标注是留给用户去发现和体验的有用的标注风格。
建议第三方实验的标注用相关的修饰符指示标注应该如何被解释。
早期的核心开发者尝试用函数标注显示不一致、特别的标注风格。例如:
[str]是很含糊的,它可能代表一个包含字符串的列表,也可能代表一个为字符串或为空的值。open(file:(str,bytes))可能用来表示file的值可以是一个str或者bytes,也可能用来表示file的值是一个包含str和bytes的二元组。- 标注
seek(whence:int)体现了一个过于明确又不够明确的混合体:int太严格了(有__index__的应该被允许),又不够严格(只有0,1,2是被允许的)。同样的,标注write(b: byte)太严格了(任何支持缓存协议的都应该被允许)。 - 像
read1(n: int=None)这样的标注自我矛盾,因为None不是int。像source_path(self, fullname:str) -> object标注是迷惑人的,返回值到底是应该什么类型? - 除了上面之外,在具体类型和抽象类型的使用上是不一致的:
int对integral(整数),set/fronzenset对MutableSet/Set。 - 不正确的抽象基类标注规格。例如,集合之间的操作需要另一个对象是集合的实例,而不只是一个可迭代序列。
- 另一个问题是,标注成为了规范的一部分,但却没有经受过考验。
- 在大多数情况下,文档字符串已经包括了类型规范,比函数标注更清晰。在其余的情况下,一旦标注被移除,文档字符串应该被完善。
- 观察到的函数标注太标新立异了,相关的系统不能一致的处理自动类型检查和参数验证。离开这些标注的代码以后很难做出更改,使自动化工具可以支持。
总结
即使内容有点多,但每一个 Python 开发者都应该尽量遵守 PEP8 规范。
参考文献
[1]:PEP 7 , Style Guide for C Code, van Rossum
[2]:Barry’s GNU Mailman style guidehttp://barry.warsaw.us/software/STYLEGUIDE.txt
[3]:http://www.wikipedia.com/wiki/CamelCase
版权说明
This document has been placed in the public domain.
Source:https://hg.python.org/peps/file/tip/pep-0008.txt
15.迭代器、生成器、装饰器
实验介绍
在这个实验里我们学习迭代器、生成器、装饰器有关知识。
这几个概念是 Python 中不容易理解透彻的概念,务必把所有的实验代码都完整的输入并理解清楚其中每一行的意思。
知识点
- 迭代器
- 生成器
- 生成器表达式
- 闭包
- 装饰器
迭代器
Python 迭代器(Iterators)对象在遵守迭代器协议时需要支持如下两种方法。
__iter__(),返回迭代器对象自身。这用在for和in语句中。
__next__(),返回迭代器的下一个值。如果没有下一个值可以返回,那么应该抛出StopIteration异常。
class Counter(object):def __init__(self, low, high):self.current = lowself.high = highdef __iter__(self):return selfdef __next__(self):#返回下一个值直到当前值大于 highif self.current > self.high:raise StopIterationelse:self.current += 1return self.current - 1
现在我们能把这个迭代器用在我们的代码里。
>>> c = Counter(5,10)>>> for i in c:... print(i, end=' ')...5 6 7 8 9 10
请记住迭代器只能被使用一次。这意味着迭代器一旦抛出StopIteration,它会持续抛出相同的异常。
>>> c = Counter(5,6)>>> next(c)5>>> next(c)6>>> next(c)Traceback (most recent call last):File "<stdin>", line 1, in <module>File "<stdin>", line 11, in nextStopIteration>>> next(c)Traceback (most recent call last):File "<stdin>", line 1, in <module>File "<stdin>", line 11, in nextStopIteration
我们已经看过在for循环中使用迭代器的例子了,下面的例子试图展示迭代器被隐藏的细节:
>>> iterator = iter(c)>>> while True:... try:... x = iterator.__next__()... print(x, end=' ')... except StopIteration as e:... break...5 6 7 8 9 10
生成器
在这一节我们学习有关 Python 生成器(Generators)的知识。生成器是更简单的创建迭代器的方法,这通过在函数中使用yield关键字完成:
>>> def my_generator():... print("Inside my generator")... yield 'a'... yield 'b'... yield 'c'...>>> my_generator()<generator object my_generator at 0x7fbcfa0a6aa0>
在上面的例子中我们使用yield语句创建了一个简单的生成器。我们能在for循环中使用它,就像我们使用任何其它迭代器一样。
>>> for char in my_generator():... print(char)...Inside my generatorabc
在下一个例子里,我们会使用一个生成器函数完成与 Counter 类相同的功能,并且把它用在 for 循环中。
>>> def counter_generator(low, high):... while low <= high:... yield low... low += 1...>>> for i in counter_generator(5,10):... print(i, end=' ')...5 6 7 8 9 10
在 While 循环中,每当执行到yield语句时,返回变量low的值并且生成器状态转为挂起。在下一次调用生成器时,生成器从之前冻结的地方恢复执行然后变量low的值增一。生成器继续while循环并且再次来到yield语句…
当你调用生成器函数时它返回一个生成器对象。如果你把这个对象传入dir()函数,你会在返回的结果中找到__iter__和__next__两个方法名。

我们通常使用生成器进行惰性求值。这样使用生成器是处理大数据的好方法。如果你不想在内存中加载所有数据,你可以使用生成器,一次只传递给你一部分数据。
os.path.walk()函数是最典型的这样的例子,它使用一个回调函数和当前的os.walk生成器。使用生成器实现节约内存。
我们可以使用生成器产生无限多的值。以下是一个这样的例子。
>>> def infinite_generator(start=0):... while True:... yield start... start += 1...>>> for num in infinite_generator(4):... print(num, end=' ')... if num > 20:... break...4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
如果我们回到my_generator()这个例子,我们会发现生成器的一个特点:它们是不可重复使用的。
>>> g = my_generator()>>> for c in g:... print(c)...Inside my generatorabc>>> for c in g:... print(c)...
我们无法创建一个可重复使用的生成器,但可以创建一个对象,将它的__iter__方法调用得到一个生成器,举例如下:
>>> class Counter(object):... def __init__(self, low, high):... self.low = low... self.high = high... def __iter__(self):... counter = self.low... while self.high >= counter:... yield counter... counter += 1...>>> gobj = Counter(5, 10)>>> for num in gobj:... print(num, end=' ')...5 6 7 8 9 10>>> for num in gobj:... print(num, end=' ')...5 6 7 8 9 10
上面的gobj并不是生成器或迭代器,因为它不具有__next__方法,只是一个可迭代对象,生成器是一定不能重复循环的。而gobj.__iter__()是一个生成器,因为它是一个带有 yield 关键字的函数。
如果想要使类的实例变成迭代器,可以用__iter__+__next__方法实现:
>>> from collections import Iterator>>> class Test():...: def __init__(self, a, b):...: self.a = a...: self.b = b...: def __iter__(self):...: return self...: def __next__(self):...: self.a += 1...: if self.a > self.b:...: raise StopIteration()...: return self.a...:>>> test = Test(5, 10)>>> isinstance(test, Iterator)True
生成器表达式
在这一节我们学习生成器表达式(Generator expressions),生成器表达式是列表推导式和生成器的一个高性能,内存使用效率高的推广。
举个例子,我们尝试对 1 到 9 的所有数字进行平方求和。
>>> sum([x*x for x in range(1,10)])
这个例子实际上首先在内存中创建了一个平方数值的列表,然后遍历这个列表,最终求和后释放内存。你能理解一个大列表的内存占用情况是怎样的。
我们可以通过使用生成器表达式来节省内存使用。
>>> sum(x*x for x in range(1,10))
生成器表达式的语法要求其总是直接在在一对括号内,并且不能在两边有逗号。这基本上意味着下面这些例子都是有效的生成器表达式用法示例:
>>> sum(x*x for x in range(1,10))285>>> g = (x*x for x in range(1,10))>>> g<generator object <genexpr> at 0x7fc559516b90>
我们可以把生成器和生成器表达式联系起来,在下面的例子中我们会读取文件'/var/log/cron'并且查看任意指定任务(例中我们搜索'anacron')是否成功运行。
我们可以用 shell 命令tail -f /etc/crontab |grep anacron完成同样的事(按 Ctrl + C 终止命令执行)。

>>> jobtext = 'anacron'>>> all = (line for line in open('/etc/crontab', 'r') )>>> job = ( line for line in all if line.find(jobtext) != -1)>>> text = next(job)>>> text'25 6\t* * *\troot\ttest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )\n'>>> text = next(job)>>> text'47 6\t* * 7\troot\ttest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )\n'>>> text = next(job)>>> text'52 6\t1 * *\troot\ttest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )\n'
你可以写一个for循环遍历所有行。
闭包
闭包(Closures)是由另外一个函数返回的函数。我们使用闭包去除重复代码。在下面的例子中我们创建了一个简单的闭包来对数字求和。
>>> def add_number(num):... def adder(number):... #adder 是一个闭包... return num + number... return adder...>>> a_10 = add_number(10)>>> a_10(21)31>>> a_10(34)44>>> a_5 = add_number(5)>>> a_5(3)8
装饰器
装饰器(Decorators)用来给一些对象动态的添加一些新的行为,我们使用过的闭包也是这样的。
我们会创建一个简单的示例,将在函数执行前后打印一些语句。
>>> def my_decorator(func):... def wrapper(*args, **kwargs):... print("Before call")... result = func(*args, **kwargs)... print("After call")... return result... return wrapper...>>> @my_decorator... def add(a, b):... #我们的求和函数... return a + b...>>> add(1, 3)Before callAfter call4
总结
知识点回顾:
- 迭代器
- 生成器
- 生成器表达式
- 闭包
- 装饰器
本实验我们学习了迭代器和生成器以及装饰器这几个高级特性的定义方法和用法,也了解了怎样使用生成器表达式和怎样定义闭包。
16.Virtualenv
实验介绍
虚拟的 Python 环境(简称 venv) 是一个能帮助你在本地目录安装不同版本的 Python 模块的 Python 环境,你可以不再需要在你系统中安装所有东西就能开发并测试你的代码。
实验知识点
- virtualenv 的安装
- 创建虚拟环境
- 激活虚拟环境
- 使用多个虚拟环境
- 关闭虚拟环境
安装 virtualenv
首先安装 pip3,打开 xfce 终端输入下面的命令:
$ sudo apt-get update$ sudo apt-get install python3-pip
用如下命令安装 virtualenv:
$ sudo pip3 install virtualenv

用法
我们会创建一个叫做virtual的目录,在里面我们会有两个不同的虚拟环境。
$ cd /home/shiyanlou$ mkdir virtual
下面的命令创建一个叫做 virt1 的环境。
$ cd virtual$ virtualenv virt1

现在我们激活这个 virt1 环境。
$ source virt1/bin/activate(virt1)shiyanlou:~/$
提示符的第一部分是当前虚拟环境的名字,当你有多个环境的时候它会帮助你识别你在哪个环境里面。
现在我们将安装redis这个 Python 模块。
(virt1)$ sudo pip3 install redis

使用deactivate命令关闭虚拟环境。
(virt1)$ deactivate$
现在我们将创建另一个虚拟环境 virt2,我们会在里面同样安装redis模块,但版本是 2.8 的旧版本。
$ virtualenv virt2$ source virt2/bin/activate(virt2)$ sudo pip3 install redis==2.8

这样可以为你的所有开发需求拥有许多不同的环境。
总结
本节知识点回顾:
- virtualenv 的安装
- 创建虚拟环境
- 激活虚拟环境
- 使用多个虚拟环境
- 关闭虚拟环境
永远记住当开发新应用时创建虚拟环境,这会帮助你的系统模块保持干净。
17.测试
实验介绍
编写测试检验应用程序所有不同的功能。每一个测试集中在一个关注点上验证结果是不是期望的。定期执行测试确保应用程序按预期的工作。当测试覆盖很大的时候,通过运行测试你就有自信确保修改点和新增点不会影响应用程序。
知识点
- 单元测试概念
- 使用 unittest 模块
- 测试用例的编写
- 异常测试
- 测试覆盖率概念
- 使用 coverage 模块
测试范围
如果可能的话,代码库中的所有代码都要测试。但这取决于开发者,如果写一个健壮性测试是不切实际的,你可以跳过它。就像Nick Coghlan(Python 核心开发成员) 在访谈里面说的:有一个坚实可靠的测试套件,你可以做出大的改动,并确信外部可见行为保持不变。
单元测试
这里引用维基百科的介绍:
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
单元测试模块
在 Python 里我们有 unittest 这个模块来帮助我们进行单元测试。
阶乘计算程序
在这个例子中我们将写一个计算阶乘的程序/home/shiyanlou/factorial.py:
import sysdef fact(n):"""阶乘函数:arg n: 数字:returns: n 的阶乘"""if n == 0:return 1return n * fact(n -1)def div(n):"""只是做除法"""res = 10 / nreturn resdef main(n):res = fact(n)print(res)if __name__ == '__main__':if len(sys.argv) > 1:main(int(sys.argv[1]))
运行程序:
$ python3 factorial.py 5
第一个测试用例
测试哪个函数?
正如你所看到的,fact(n)这个函数执行所有的计算,所以我们至少应该测试这个函数。
编辑/home/shiyanlou/factorial_test.py文件,代码如下:
import unittestfrom factorial import factclass TestFactorial(unittest.TestCase):"""我们的基本测试类"""def test_fact(self):"""实际测试任何以 `test_` 开头的方法都被视作测试用例"""res = fact(5)self.assertEqual(res, 120)if __name__ == '__main__':unittest.main()
运行测试:
$ python3 factorial_test.py.----------------------------------------------------------------------Ran 1 test in 0.000sOK
说明
我们首先导入了 unittest 模块,然后测试我们需要测试的函数。
测试用例是通过子类化unittest.TestCase创建的。
现在我们打开测试文件并且把 120 更改为 121,然后看看会发生什么?
各类 assert 语句
| Method | Checks that | New in |
|---|---|---|
| assertEqual(a, b) | a == b | |
| assertNotEqual(a, b) | a != b | |
| assertTrue(x) | bool(x) is True | |
| assertFalse(x) | bool(x) is False | |
| assertIs(a, b) | a is b | 2.7 |
| assertIsNot(a, b) | a is not b | 2.7 |
| assertIsNone(x) | x is None | 2.7 |
| assertIsNotNone(x) | x is not None | 2.7 |
| assertIn(a, b) | a in b | 2.7 |
| assertNotIn(a, b) | a not in b | 2.7 |
| assertIsInstance(a, b) | isinstance(a, b) | 2.7 |
| assertNotIsInstance(a, b) | not isinstance(a, b) |
2.7异常测试
如果我们在factorial.py中调用div(0),我们能看到异常被抛出。
我们也能测试这些异常,就像这样:
self.assertRaises(ZeroDivisionError, div, 0)
完整代码:
import unittestfrom factorial import fact, divclass TestFactorial(unittest.TestCase):"""我们的基本测试类"""def test_fact(self):"""实际测试任何以 `test_` 开头的方法都被视作测试用例"""res = fact(5)self.assertEqual(res, 120)def test_error(self):"""测试由运行时错误引发的异常"""self.assertRaises(ZeroDivisionError, div, 0)if __name__ == '__main__':unittest.main()
mounttab.py
mounttab.py 中只有一个mount_details()函数,函数分析并打印挂载详细信息。
import osdef mount_details():"""打印挂载详细信息"""if os.path.exists('/proc/mounts'):fd = open('/proc/mounts')for line in fd:line = line.strip()words = line.split()print('{} on {} type {}'.format(words[0],words[1],words[2]), end=' ')if len(words) > 5:print('({})'.format(' '.join(words[3:-2])))else:print()fd.close()if __name__ == '__main__':mount_details()
重构 mounttab.py
现在我们在 mounttab2.py 中重构了上面的代码并且有一个我们能容易的测试的新函数parse_mounts()。
import osdef parse_mounts():"""分析 /proc/mounts 并 返回元祖的列表"""result = []if os.path.exists('/proc/mounts'):fd = open('/proc/mounts')for line in fd:line = line.strip()words = line.split()if len(words) > 5:res = (words[0],words[1],words[2],'({})'.format(' '.join(words[3:-2])))else:res = (words[0],words[1],words[2])result.append(res)fd.close()return resultdef mount_details():"""打印挂载详细信息"""result = parse_mounts()for line in result:if len(line) == 4:print('{} on {} type {} {}'.format(*line))else:print('{} on {} type {}'.format(*line))if __name__ == '__main__':mount_details()
同样我们测试代码,编写 mounttest.py 文件:
#!/usr/bin/env pythonimport unittestfrom mounttab2 import parse_mountsclass TestMount(unittest.TestCase):"""我们的基本测试类"""def test_parsemount(self):"""实际测试任何以 `test_` 开头的方法都被视作测试用例"""result = parse_mounts()self.assertIsInstance(result, list)self.assertIsInstance(result[0], tuple)def test_rootext4(self):"""测试找出根文件系统"""result = parse_mounts()for line in result:if line[1] == '/' and line[2] != 'rootfs':self.assertEqual(line[2], 'ext4')if __name__ == '__main__':unittest.main()
运行程序
$ python3 mounttest.py..----------------------------------------------------------------------Ran 2 tests in 0.001sOK
测试覆盖率
测试覆盖率是找到代码库未经测试的部分的简单方法。它并不会告诉你的测试好不好。
在 Python 中我们已经有了一个不错的覆盖率工具来帮助我们。你可以在实验楼环境中安装它:
$ sudo pip3 install coverage
覆盖率示例
$ coverage3 run mounttest.py..----------------------------------------------------------------------Ran 2 tests in 0.013sOK$ coverage3 report -mName Stmts Miss Cover Missing--------------------------------------------mounttab2.py 22 7 68% 16, 25-30, 34mounttest.py 14 0 100%--------------------------------------------TOTAL 36 7 81%
我们还可以使用下面的命令以 HTML 文件的形式输出覆盖率结果,然后在浏览器中查看它。
$ coverage3 html

总结
知识点回顾:
- 单元测试概念
- 使用 unittest 模块
- 测试用例的编写
- 异常测试
- 测试覆盖率概念
- 使用 coverage 模块
本实验了解了什么是单元测试,unittest 模块怎么用,测试用例怎么写。以及最后我们使用第三方模块 coverage 进行了覆盖率测试。
在实际生产环境中,测试环节是非常重要的的一环,即便志不在测试工程师,但以后的趋势就是 DevOps,所以掌握良好的测试技能也是很有用的。
18.项目结构
实验介绍
本实验阐述了一个完整的 Python 项目结构,你可以使用什么样的目录布局以及怎样发布软件到网络上。
知识点
- 创建项目,编写
__init__文件 - 使用 setuptools 模块,编写 setup.py 和 MANIFEST.in 文件
- 创建源文件的发布版本
- 项目注册&上传到 PyPI
创建Python项目
我们的实验项目名为factorial,放到/home/shiyanlou/factorial目录:
$ cd /home/shiyanlou$ mkdir factorial$ cd factorial/
我们给将要创建的 Python 模块取名为myfact,因此我们下一步创建myfact目录。
$ mkdir myfact$ cd myfact/
主代码将在fact.py文件里面。
"myfact module"def factorial(num):"""返回给定数字的阶乘值:arg num: 我们将计算其阶乘的整数值:return: 阶乘值,若传递的参数为负数,则为 -1"""if num >= 0:if num == 0:return 1return num * factorial(num -1)else:return -1
我们还有模块的__init__.py文件,内容如下:
from fact import factorial__all__ = [factorial, ]
我们还在factorial目录下添加了一个README.rst文件。因此,目录结构看起来像下面这样:

MANIFEST.in
现在我们要写一个/home/shiyanlou/factorial/MANIFEST.in文件,它用来在使用sdist命令的时候找出将成为项目源代码压缩包一部分的所有文件。
include *.pyinclude README.rst
如果你想要排除某些文件,你可以在这个文件中使用exclude语句。
安装 python-setuptools 包
我们使用virtualenv(这里不示范步骤)。
$ sudo pip3 install setuptools
setup.py
最终我们需要写一个/home/shiyanlou/factorial/setup.py,用来创建源代码压缩包或安装软件。
#!/usr/bin/env python3"""Factorial project"""from setuptools import find_packages, setupsetup(name = 'factorial', # 注意这里的name不要使用factorial相关的名字,因为会重复,需要另外取一个不会与其他人重复的名字version = '0.1',description = "Factorial module.",long_description = "A test module for our book.",platforms = ["Linux"],author="ShiYanLou",author_email="support@shiyanlou.com",url="https://www.shiyanlou.com/courses/596",license = "MIT",packages=find_packages())
name是项目名称,version是发布版本,description和long_description分别是项目介绍,项目长描述。platforms是此模块的支持平台列表。find_packages()是一个能在你源目录下找到所有模块的特殊函数,packaging docs。
2.3.1. setup.py 用例
要创建一个源文件发布版本,执行以下命令。
$ python3 setup.py sdist
执行完毕会返回类似下面的信息:
running sdistrunning egg_infocreating factorial.egg-infowriting factorial.egg-info/PKG-INFOwriting top-level names to factorial.egg-info/top_level.txtwriting dependency_links to factorial.egg-info/dependency_links.txtwriting manifest file 'factorial.egg-info/SOURCES.txt'reading manifest file 'factorial.egg-info/SOURCES.txt'reading manifest template 'MANIFEST.in'writing manifest file 'factorial.egg-info/SOURCES.txt'running checkcreating factorial-0.1creating factorial-0.1/factorial.egg-infocreating factorial-0.1/myfactmaking hard links in factorial-0.1...hard linking MANIFEST.in -> factorial-0.1hard linking README.rst -> factorial-0.1hard linking setup.py -> factorial-0.1hard linking factorial.egg-info/PKG-INFO -> factorial-0.1/factorial.egg-infohard linking factorial.egg-info/SOURCES.txt -> factorial-0.1/factorial.egg-infohard linking factorial.egg-info/dependency_links.txt -> factorial-0.1/factorial.egg-infohard linking factorial.egg-info/top_level.txt -> factorial-0.1/factorial.egg-infohard linking myfact/__init__.py -> factorial-0.1/myfacthard linking myfact/fact.py -> factorial-0.1/myfactWriting factorial-0.1/setup.cfgcreating distCreating tar archiveremoving 'factorial-0.1' (and everything under it)
我们能在dist目录下看到一个 tar 压缩包。
$ ls dist/factorial-0.1.tar.gz
记住尝试安装代码时使用 virtualenv。
执行下面的命令从源代码安装。
$ sudo python3 setup.py install
学习更多可前往packaging.python.org。
Python Package Index(PyPI)
你还记得我们经常使用的pip命令吗?有没有想过这些包是从哪里来的?答案是PyPI。这是 Python 的软件包管理系统。
为了实验,我们会使用PyPI的测试服务器https://testpypi.python.org/pypi。
2.4.1 创建账号
首先在这个链接注册账号。你会收到带有链接的邮件,点击这个链接确认你的注册。
创建 ~/.pypirc 文件,存放你的账号详细信息,其内容格式如下:
[distutils]index-servers = pypitestpypi[pypi]repository: https://upload.pypi.org/legacy/username: <username>password: <password>[testpypi]repository:https://test.pypi.org/legacy/username: <username>password: <password>
替换<username>和<password>为您新创建的帐户的详细信息。在这里,由于我们是到testpypi的网页上去注册账号,即将相应的服务上传到testpypi,所以在这里,你只需修改[testpypi]的用户名和密码
记得在setup.py中更改项目的名称为其它的名字来测试下面的指令,在接下来的命令中我将项目名称修改为factorial2,为了不重复,大家需要自行修改至其它名称(不要使用 factorial 和 factorial2,因为已经被使用了)。
2.4.2 上传到 TestPyPI 服务
下一步我们会将我们的项目到 TestPyPI 服务。这通过twine命令完成。
我们也会使用-r把它指向测试服务器。
$ sudo pip3 install twine$ twine upload dist/* -r testpypi
执行完毕会返回类似下面的信息:
Uploading distributions to https://test.pypi.org/legacy/Uploading factorial2-0.1.tar.gz
现在如果你浏览这个页面,你会发现你的项目已经准备好被别人使用了。

在这里你也可以使用下面的命令上传到 PyPI 服务上,但这里需要注意,在~/.pypirc里面,你需要到https://pypi.python.org页面,按照上面的步骤去注册一个账号,然后到~/.pypirc的[pypi]下填写相应的用户名和密码。testpypi和pypi的账号密码并不通用。
$ twine upload dist/* -r pypi
总结
实验知识点回顾:
- 创建项目,编写
__init__文件 - 使用 setuptools 模块,编写 setup.py 和 MANIFEST.in 文件
- 创建源文件的发布版本
- 项目注册&上传到 PyPI
本实验使用了 setuptools 包,并完成了较为完整的项目创建&发布流程,最后还将项目发布到了网络 (PyPI)上。
19.Flask介绍
实验介绍
本节实验通过一些简单的示例,学习 Flask 框架的基本使用。
知识点
- 微框架、WSGI、模板引擎概念
- 使用 Flask 做 web 应用
- 模板的使用
- 根据 URL 返回特定网页
基本概念
什么是 Flask?
Flask 是一个 web 框架。也就是说 Flask 为你提供工具,库和技术来允许你构建一个 web 应用程序。这个 web 应用程序可以是一些 web 页面、博客、wiki、基于 web 的日历应用或商业网站。
Flask 属于微框架(micro-framework)这一类别,微架构通常是很小的不依赖于外部库的框架。这既有优点也有缺点,优点是框架很轻量,更新时依赖少,并且专注安全方面的 bug,缺点是,你不得不自己做更多的工作,或通过添加插件增加自己的依赖列表。Flask 的依赖如下:
维基百科 WSGI 的介绍:
Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口)。自从WSGI被开发出来以后,许多其它语言中也出现了类似接口。
什么是模板引擎?
你搭建过一个网站吗?你面对过保持网站风格一致的问题吗,你不得不写多次相同的文本吗?你有没有试图改变这种网站的风格?
如果你的网站只包含几个网页,改变网站风格会花费你一些时间,这确实可行。尽管如此,如果你有许多页面(比如在你商店里的售卖物品列表),这个任务便很艰巨。
使用模板你可以设置你的页面的基本布局,并提及哪个元素将发生变化。这种方式可以定义您的网页头部并在您的网站的所有页面使它保持一致,如果你需要改变网页头部,你只需要更新一个地方。
使用模板引擎创建/更新/维护你的应用会节约你很多时间。
“Hello World”应用
我们将使用 flask 完成一个非常基础的应用。
- 安装 flask
$ sudo pip3 install flask
- 创建项目结构
$ cd /home/shiyanlou$ mkdir -p hello_flask/{templates,static}
这是你的 web 应用的基本结构:
$ tree hello_flask/hello_flask|-- static`-- templates2 directories, 0 files
templates文件夹是存放模板的地方,static文件夹存放 web 应用所需的静态文件(images, css, javascript)。
- 创建应用文件
$ cd hello_flask$ vim hello_flask.py
hello_flask.py 文件里编写如下代码:
#!/usr/bin/env python3import flask# Create the application.APP = flask.Flask(__name__)@APP.route('/')def index():""" 显示可在 '/' 访问的 index 页面"""return flask.render_template('index.html')if __name__ == '__main__':APP.debug=TrueAPP.run()
- 创建模板文件
index.html
$ vim templates/index.html
index.html 文件内容如下:
<!DOCTYPE html><html lang='en'><head><meta charset="utf-8" /><title>Hello world!</title><link type="text/css" rel="stylesheet"href="{{ url_for('static',filename='hello.css')}}" /></head><body>It works!</body></html>
- 运行 flask 应用程序
$ python3 hello_flask.py
访问http://127.0.0.1:5000/,这应该只是显示黑字白底的 “It works!” 文本,如下图:

Flask中使用参数
在本节中我们将要看到如何根据用户使用的 URL 返回网页。
为此我们更新 hello_flask.py 文件。
- 在 hello_flask.py 文件中添加以下条目
@APP.route('/hello/<name>/')def hello(name):""" Displays the page greats who ever comes to visit it."""return flask.render_template('hello.html', name=name)
- 创建下面这个模板 hello.html
<!DOCTYPE html><html lang='en'><head><meta charset="utf-8" /><title>Hello</title><link type="text/css" rel="stylesheet"href="{{ url_for('static',filename='hello.css')}}" /></head><body>Hello {{name}}</body></html>
- 运行 flask 应用
$ python3 hello_flask.py
访问http://127.0.0.1:5000/,这应该只是显示黑字白底的 “It works!” 文本。
访问http://127.0.0.1:5000/hello/you,这应该返回文本 “Hello you”,见下图:

无论你在 URL 中/hello/后填写的什么,都会出现在返回的网页中。
这是你第一次使用模板,我们在 hello_flask.py 中建立了name变量(参见 hello 函数的 return 行)。通过语法{{name}},name 变量之后在页面中显示其自身。
额外工作
目前,对于每一个页面我们都创建了一个模板,其实这是不好的做法,我们应该做的是创建一个主模板并且在每个页面使用它。
- 创建模板文件 master.html。
<!DOCTYPE html><html lang='en'><head><meta charset="utf-8" /><title>{% block title %}{% endblock %} - Hello Flask!</title><link type="text/css" rel="stylesheet"href="{{ url_for('static',filename='hello.css')}}" /></head><body>{% block body %}{% endblock %}</body></html>
- 调整模板 index.html。
{% extends "master.html" %}{% block title %}Home{% endblock %}{% block body %}It works!{% endblock %}
正如你所看到的,在 master.html 模板中我们定义了两部分,名为title和body的blocks。
在模板 index.html 中,我们声明这个模板扩展自 master.html 模板,然后我们定义了内容来放在这两个部分中(blocks)。在第一个 blocktitle中,我们放置了Home单词,在第二个 blockbody中我们定义了我们想要在页面的 body 中有的东西。
- 作为练习,更改其他模板 hello.html,同样要使用 master.html。
- 在 hello 页面添加首页链接。
调整模板 hello.html,添加到首页的链接。
<a href="{{ url_for('index') }}"><button>Home</button></a>
- 作为你的任务,在首页添加到 hello 页面的链接。
总结
实验知识点回顾:
- 微框架、WSGI、模板引擎概念
- 使用 Flask 做 web 应用
- 模板的使用
- 根据 URL 返回特定网页
本实验中我们了解了微框架、WSGI、模板引擎等概念,学习使用 Flask 做一个 web 应用,在这个 web 应用中,我们使用了模板。而用户以正确的不同 URL访问服务器时,服务器返回不同的网页。最后还给大家留了一个小任务,希望大家能完成。
想要深入学习Flask,你还可以学习实验楼的Python Flask Web框架课程。
当然,在学习过程中有任何不懂的地方或者对Flsak非常感兴趣,推荐学习Flask官方文档。
