《Learning to Program: Crafting Quality Code》 笔记

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:自动化测试

依靠 doctestunittest 等模块可以实现自动化测试。

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 IDLErun 一下,输入这两行代码,可得到测试输出:

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

unittestdoctest 的区别在于,前者需要额外编写测试代码。
举例说明,上面用过的整除检查代码,从 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 后的值为真则不打印字符串,否则打印该信息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值