Coursera 上的编程入门课,由多伦多大学制作,使用 Python 语言。这门课介绍了一些实践中非常有用,但在常规入门教程中容易被忽视的内容,对养成良好的编码习惯有一定的启发意义。
很久以前上过的课程,把知识点拿出来整理一下。
0 Week1:引子
以回文和餐厅推荐为例,简单介绍了算法和数据结构,讲解了如何用 Python 自顶向下地解决问题。细节内容包括函数和注释该怎么写等。
函数和注释写法举例:
def is_palindrome_v1(s):
""" (str) -> bool
# (输入类型) -> 输出类型
Return True if and only if s is a palindrome.
# 解释函数的作用
>>> is_palindrome_v1('noon')
True
>>> is_palindrome_v1('racecar')
True
>>> is_palindrome_v1('dented')
False
# 自动化测试样例
"""
return reverse(s) == s
def read_restaurants(file):
""" (file) -> (dict, dict, dict)
Return a tuple of three dictionaries based on the information in the file:
- a dict of {restaurant name: rating%}
- a dict of {price: list of restaurant names}
- a dict of {cusine: list of restaurant names}
"""
# 下略
写注释的时间可能比写代码还要多。但在大型软件开发中,规范、易读、充分的注释非常重要。
1 Week2:自动化测试
依靠 doctest
、unittest
等模块可以实现自动化测试。
1.1 doctest
doctest
分两步:在 docstring
中按要求写测试样例,在 Python Shell
中做 doctest
测试。
举例说明,函数 collect_vowels
接收一个字符串,返回其中的元音字母:
def collect_vowels(s):
""" (str) -> str
Return the vowels (a, e, i, o, and u) from s.
>>> collect_vowels('Happy Anniversary!')
'aAiea'
>>> collect_vowels('xyz')
''
"""
vowels = ''
for char in s:
if char in 'aeiouAEIOU':
vowels = vowels + char
return vowels
可以看到,docstring
中存在一段内容:
>>> collect_vowels('Happy Anniversary!')
'aAiea'
>>> collect_vowels('xyz')
''
执行doctest
的格式是:
>>> import doctest
>>> doctest.testmod()
在 Python IDLE
中 run
一下,输入这两行代码,可得到测试输出:
TestResults(failed=0, attempted=2)
另一个例子,检验可整除数:
def get_divisors(num, possible_divisors):
""" (int, list of int) -> list of int
Return a list of the values from possible_divisors
that are divisors of num.
>>> get_divisors(8, [1, 2, 3])
[1, 2]
>>> get_divisors(4, [-2, 0, 2])
[2]
"""
divisors = []
for item in possible_divisors:
if item != 0 and num % item == 0:
divisors.append(item)
return divisors
>>> import doctest
>>> doctest.testmod()
**********************************************************************
File "__main__", line 7, in __main__.get_divisors
Failed example:
get_divisors(4, [-2, 0, 2])
Exception raised:
Traceback (most recent call last):
File "/local/packages/python-2.7/lib/python2.7/doctest.py", line 1254, in __run
compileflags, 1) in test.globs
File "", line 1, in
get_divisors(4, [-2, 0, 2])
File "", line 12, in get_divisors
ZeroDivisionError: integer division or modulo by zero
**********************************************************************
1 items had failures:
1 of 2 in __main__.get_divisors
***Test Failed*** 1 failures.
TestResults(failed=1, attempted=2)
不难看到,出错原因是函数没有对除零错误做处理。
在外部 Shell
中进行 doctest
也简单。在待测试脚本中添加
import doctest
doctest.testmod()
然后执行 python to_be_doctested.py
。无返回表明没有错误。
想查看详细测试内容需要加一个 -v
参数,即 python to_be_doctested.py -v
:
Trying:
collect_vowels('Happy Anniversary!')
Expecting:
'aAiea'
ok
Trying:
collect_vowels('xyz')
Expecting:
''
ok
1 items passed all tests:
2 tests in __main__.collect_vowels
2 tests in 1 items.
2 passed and 0 failed.
Test passed.
这样做的问题是每次加载该脚本时都会执行一次 doctest
。解决方法是指定 __main__
if __name__ == '__main__':
import doctest
doctest.testmod()
即只有在该脚本为 __main__
时测试才会执行。
1.2 自定义类型
Python 中一切类型都是对象。自定义类非常简单,比如定义一个 str
的子类 WordplayStr
:
class WordplayStr(str):
"""A string that can report whether it has interesting properties."""
1.3 unittest
unittest
与 doctest
的区别在于,前者需要额外编写测试代码。
举例说明,上面用过的整除检查代码,从 doctest
转换为 unittest
:
左边是 divisors.py
文件中的内容,右边是 ‘test_divisors.py文件中的内容。
doctest` 的套路是:
可以看到,
import doctest
- 写
shell
格式的测试样例 - 写期望的输出
而 unittest
的模式是:
import unittest
- 为每一个测试样例写单独的
方法
,在每个方法
中:
- 调用待测试函数获得返回值,然后
- 调用 `call.assertEqual(…) 比较实际输出与期望输出
运行测试时,doctest
需要
- 调用
doctest.testmod()
检查当前模块下docstring
中有无自动化项目 - 执行找到的测试样例
- 报告实际输出与期望输出的区别
unittest
需要
- 调用
unittest.main()
检查当前模块中所有TestCase
的子类 - 调用每一个以
test
打头的方法
- 报告与期望结果不符的输出
运行 test_divisors
模块,得到无错输出:
..
----------------------------------------------------------------------
Ran 2 tests in 0.025s
OK
如果发现错误,比如把左边第十三行的 divisors = []
改为 divisors = [num]
,输出变为
FF
======================================================================
FAIL: test_divisors_example_1 (__main__.TestDivisors)
Test get_divisors with 8 and [1, 2, 3].
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_divisors.py", line 13, in test_divisors_example_1
self.assertEqual(actual, expected)
AssertionError: Lists differ: [8, 1, 2] != [1, 2]
First differing element 0:
8
1
First list contains 1 additional elements.
First extra element 2:
2
- [8, 1, 2]
? ---
+ [1, 2]
======================================================================
FAIL: test_divisors_example_2 (__main__.TestDivisors)
Test get_divisors with 4 and [-2, 0, 2].
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_divisors.py", line 20, in test_divisors_example_2
self.assertEqual(actual, expected)
AssertionError: Lists differ: [4, -2, 2] != [-2, 2]
First differing element 0:
4
-2
First list contains 1 additional elements.
First extra element 2:
2
- [4, -2, 2]
? ---
+ [-2, 2]
----------------------------------------------------------------------
Ran 2 tests in 0.018s
FAILED (failures=2)
生成了非常详细的错误报告。另一种情况是程序执行中报错:对 divisors.py
做一点修改:
输出变为
.E
======================================================================
ERROR: test_divisors_example_2 (__main__.TestDivisors)
Test get_divisors with 4 and [-2, 0, 2].
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_divisors.py", line 18, in test_divisors_example_2
actual = divisors.get_divisors(4, [-2, 0, 2])
File "divisors.py", line 16, in get_divisors
if num % item == 0:
ZeroDivisionError: integer division or modulo by zero
----------------------------------------------------------------------
Ran 2 tests in 0.048s
FAILED (errors=1)
关于自动化测试更详细的介绍,你应当去阅读 Python 的文档。
1.4 测试样例的设计/选择
代码需要保证能够尽可能地覆盖所有情况。但测试代码仍需要人来编写,所以需要选择针对性的测试样例,比如测试函数的边界、对特殊情况的处理等。这个话题比较复杂,课程介绍了几种常规的选择思路:
- 输入集合(如字符串,列表,元组,字典等类型)的大小(Size):
- 空集合
- 单元素集合
- 极少的特异情形
- 多元素情形
- 二分法。如可以考虑:
- 元音/非元音输入(对于前面元音检测的代码)
- 奇数/偶数情况
- 正例/反例
- 空输入/全输入
- etc.
- 边界情形。如代码中有判断、循环语句,检查其边界。
- 顺序。如果函数对不同顺序的输入表现不同,检查每一种顺序的影响。
2 Week3:算法分析
这一节举了几个简单的例子,对比算法之间的效率。
包括查找——线性查找和二分查找——和排序——冒泡、选择、插入——的算法实现,这里不再赘述。需要说明的是此处做性能分析的工具 cProfile
。
以查找为例:
>>> import cProfile
>>> L = list(range(10000000))
>>> len(L)
10000000
>>> cProfile.run('binary_search(L, 10000000)')
5 function calls in 0.000 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 :1(binary_search)
1 0.000 0.000 0.000 0.000 :1()
2 0.000 0.000 0.000 0.000 {len}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
>>> cProfile.run('linear_search(L, 10000000)')
10000005 function calls in 9.146 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 6.198 6.198 9.146 9.146 :1(linear_search)
1 0.000 0.000 9.146 9.146 :1()
10000002 2.948 0.000 2.948 0.000 {len}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
cProfile.run('function_name(para)')
命令返回了函数的运行情况:总的函数调用次数、运行时间,函数内部的调用情况。显然,对数级的二分查找比线性查找快得多。列出的调用和时间消耗情况可以用来分析代码的瓶颈。另外注意到线性查找调用 len()
的次数较多,修改后也有一定帮助:
>>> cProfile.run('linear_search(L, 10000000)')
4 function calls in 1.872 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 1.872 1.872 1.872 1.872 :1(linear_search)
1 0.000 0.000 1.872 1.872 :1()
1 0.000 0.000 0.000 0.000 {len}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
3 Week4:类
可以看作 Python 类的入门教学。包括实例化操作 __init__
和实例方法的编写。
每一个实例方法的第一个参数都是 self
,强调这一句是因为在仿+改一篇 AAAI 论文源码的时候感觉哪里不对,有日子没怎么写 Python 语法都混乱了。定了定神才发现他所有的实例方法都没加 self
。
接下来是特殊方法的使用和编写。前面说过,Python 的一切类型都是对象。object
是 Python 的元类,所有自定义类,如果不加指定,都是 object
的子类。object
中有许多“下划线方法”/特殊方法/“魔力”方法,可以看到:
>>> dir(object)
['__class__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__',
'__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__']
这些类有特殊用途。比如 __eq__
就是对运算符 ==
的重载,在未指定情况下, __eq__
只比较两个实例的地址而非内部的特定值;__str__
返回该对象的字符串表达——如 str()
或 print()
时可发挥作用。
下划线方法的写法与一般方法相同,比如:
def __eq__(self, other):
类似加减运算等也可通过下划线方法实现。
类似地,在默认情况下,__str__
返回实例的地址而非某种表示。其写法与 __eq__
类似:
def __str__(self):
return some_string_representation_of_self()
4 Week5:参数传递和意外处理
4.1 参数传递
函数也是对象。函数可以接收函数作为参数。比如:
def function_caller(f):
f()
def function1():
print('function_1 was called.')
def function2():
print('function_2 was called.')
调用 function_caller
:
>>> function_caller(function1)
function_1 was called.
函数可以指定输入的默认值。需要注意的是,有默认值的参数在函数调用的同时被分配地址。举例说明,函数 add_greeting
向列表 L
中添加 ‘hello’ 元素:
def add_greeting(L=[]):
""" (list) -> NoneType
Append 'hello' to L and print L.
>>> L = ['hi', 'bonjour']
>>> f(L)
>>> L
['hi', 'bonjour', 'hello']
"""
L.append('hello')
print(L)
在这种情况下, L
是可变的。继续说明:
>>> add_greeting()
['hello']
>>> add_greeting()
['hello', 'hello']
>>> add_greeting()
['hello', 'hello', 'hello']
可见,如果始终未提供输入参数而使用了默认参数,该参数会一直存在,即其地址对每一次函数调用来说都相同。当然指定参数后 L
会被“销毁”:
>>> add_greeting()
['hello']
>>> add_greeting()
['hello', 'hello']
>>> add_greeting(['bonjour'])
['bonjour', 'hello']
>>>
4.2 意外处理
发生错误时,程序会报错跳出。但许多错误是可以预料的。比如“除零错误”:
>>> 0/1
Traceback (most recent call last):
File "<pyshell#0>", line 1, in
1 / 0
ZeroDivisionError: division by zero
对此,Python 也有 try...except
机制:
try:
statements
except:
statements
try:
1 / 0
except ZeroDivisionError:
print("Divided by zero.")
这里指定了错误类型。当存在多种错误时:
try:
statements
except ExceptionType:
statements
except ExceptionType:
statements
还可以自定义错误:
def raise_an_exception(v):
raise ValueError("{} is not a valid value.".format(v))
def main():
raise_an_exception(3)
if __name__ == '__main__':
try:
main()
except ValueError as ve:
print(ve)
4.3 断言 assert
举例说明:
def every_nth(L, n=1):
""" (list, int) -> list
Precondition: 0 <= n < len(L)
Return a list containing every nth item of L,
starting at index 0.
>>> every_nth([1, 2, 3, 4, 5, 6], n=2)
[1, 3, 5]
>>> every_nth([1, 2, 3, 4, 5, 6], 3)
[1, 4]
>>> every_nth([1, 2, 3, 4, 5, 6])
[1, 2, 3, 4, 5, 6]
"""
assert 0 <= n < len(L), '{} is out of range.'.format(n)
result = []
for i in range(0, len(L), n):
result.append(L[i])
return result
if __name__ == '__main__':
import doctest
doctest.testmod()
断言一般用来检查 precondition
是否满足。assert
后的值为真则不打印字符串,否则打印该信息。