1.1 引言
调试(debugging)的一些指导原则:
- 逐步测试:每个写好的程序都由小型的组件模块组成,这些组件可以独立测试。尽快测试你写好的任何东西来捕获错误。
- 隔离错误:复杂程序的输出、表达式,或语句中的错误,通常更可以归于特定的组件模块。在尝试诊断问题、修正错误前,一定要先将它跟踪到最小的代码片段。
- 询问他人
1.2 编程元素
1.2.3 导入库函数
例如:from operator import add, sub, mul
其中operator为模块名称(math也是模块名称),add,sub,mul是被导入模块的具名属性。
1.2.4 名称和环境
如果一个值被赋予了名称,我们就说这个名称绑定到了值上面。
环境:将名称绑定到值上,以及随后通过名称来检索这些值的可能,意味着解释器必须维护某种内存来跟踪这些名称和值的绑定。这些内存叫做环境。
名称也可以绑定到函数,比如:
>>> max
<built-in function max>
我们可以使用赋值运算符来给现有的函数取新的名字:
>>> f = max
>>> f
<built-in function max>
>>> f(3, 4)
4
如果给函数名赋值为其他数据类型(如:int),则再次调用该函数时会报错:
>>> max(3,4)
4
>>> max = 2
>>> max(3,4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
1.2.5 嵌套表达式的求解
为了求出调用表达式,Python 会执行下列事情:
- 求出运算符和操作数子表达式,之后
- 在值为操作数子表达式的参数上调用值为运算符子表达式的函数。
这个简单的过程大体上展示了一些过程上的重点。第一步表明为了完成调用表达式的求值过程,我们首先必须求出其它表达式。所以,求值过程本质上是递归的,也就是说,它会调用其自身作为步骤之一。
这是一个表达式数(expression tree)。在计算机科学中,树从顶端向下生长。每一点上的对象叫做节点(codes),比如该例中的mul、add(2, mul(4, 6))、add(3, 5)。
要求根节点(root node),也就是整个表达式,即mul(add(2, mul(4, 6)), add(3, 5))
,首先需要求出枝干节点,也就是子表达式。
叶子节点(leaf expressionns)(即:没有子节点的节点)的表达式表示函数或数值,如:mul,add,2,4,6等。
内部节点分为两部分:
- 想要应用求值规则的表达式
- 表达式的结果
求值:
- 数字求值为它标明的数值
- 名称求值为当前为当前环境中这个名称所关联的值
1.2.6 函数图解
- 纯函数:具有一些输入(参数)和一些输出(调用结果)的函数。比如
abs()
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TfTeMkna-1632023353237)(image/2021-08-20-14-57-10.png)]
纯函数的特性:调用时除了返回值之外没有其他效果。
-
非纯函数:除了返回值,还会产生一些改变解释器或计算机状态的副作用,一个普遍的副作用就是在返回值之外生成额外的输出,比如
print()
print()
的返回值永远是None
,Python交互式解释器并不会自动打印None
值。作为副作用,print()
自己打印了输出:>>> print(1, 2, 3) 1 2 3
我们可以通过调用print()
的嵌套表达式来理解它的非纯特性:
>>> print(print(1), print(2))
1
2
None None
>>> two = print(2)
2
>>> print(two)
None
1.3 定义新的函数
def <name>(<formal parameters>):
return <return expression>
注:return表达式并不是立即求值,它储存为新定义函数的一部分,只在函数最终调用时会被求出。
1.3.1 环境
环境中帧的顺序会影响由表达式中的名称检索返回的值。我们之前说名称求解为当前环境中与这个名称关联的值。我们现在可以更精确一些:
- 名称求解为当前环境中,最先发现该名称的帧中,绑定到这个名称的值。
-> 这个原则非常重要
1.3.4 局部名称
形式参数的选择不应影响函数行为。
>>> def square(x):
return mul(x, x)
>>> def square(y):
return mul(y, y)
这个原则导致的最简单的记过就是函数参数名称应保留在函数体的局部范围中。
1.3.5 名称的选择
- 函数名小写,单词之间用下划线分隔。尽量使用描述性名称
- 参数名称小写,单词之间用下划线分隔,首选单字名称(指只包含一个单词的名称)
- 参数名称应该体现参数在函数中的作用
- 当它们的作用很明显时,单字母参数名称是可以接受的,但避免使用“l”(小写字母 ell)、“O”(大写字母 oh)或“I”(大写字母 i)以避免与数字混淆。
1.3.6 作为抽象的函数
要正确使用一个函数,我们应当了解它的几个方面:
- The domain of a function is the set of arguments it can take.
函数的域是它可以采用的参数集(输入是什么) - The range of a function is the set of values it can return.
函数的范围是它可以返回的值的集合(输出是什么) - The intent of a function is the relationship it computes between inputs and output (as well as any side effects it might generate)
该函数输入与输出之间的关系(以及它可能产生的任何副作用)
以square()
函数为例:
domain:一个实数
range:一个非负实数
intent:输出是输入的平方
1.3.7 运算符
5 / 4
等价于truediv(5, 4)
5 // 4
等价于floordiv(5, 4)
1.4 函数的设计
- 定义函数时的几个注意事项:
- DRY原则——Don’t repeat yourself:代码尽量不要有重复的部分,有就写成函数
- 函数应该被一般性定义
如:python内没有square()函数的原因是已经有了pow()函数
1.4.1 文档字符串
- docstring
- 在python的def关键词下的一行用"""包裹的文字是叫做docstring,用来解释这个函数所做的事情。通常,第一行解释函数的作用,接下来的行分别解释参数的意义:
def pressure(v, t, n):
"""Compute the pressure in pascals of an ideal gas.
Applies the ideal gas law: http://en.wikipedia.org/wiki/Ideal_gas_law
v -- volume of gas, in cubic meters
t -- absolute temperature in degrees kelvin
n -- particles of gas
"""
k = 1.38e-23 # Boltzmann's constant
return n * k * t / v
- 使用
help(<name>)
可以获取该函数的docstring
help(pressure)
- 在编写 Python 程序时,除了最简单的函数之外,所有的函数都需要包含文档字符串。
Remember, code is written only once, but often read many times.
- #及其后的注释不会出现在help中
- doctests
在docstring中如果加入了>>>,这就是doctest,用来测试函数的正确性,比如:
from operator import floordiv, mod
def divide_exact(n, d):
"""Return the quotient and remainder of dividing N by D
>>> q, r = divide_exact(2013,10)
>>> q
201
>>> r
3
"""
return floordiv(n, d), mod(n, d)
1.4.2 参数默认值(Default Argument Values)
定义函数时,默认参数应该跟在可变参数的后面。
准则:用于函数体的大多数数据值应该表示为具名参数的默认值,这样便于查看,以及被函数调用者修改。一些值永远不会改变,就像基本常数k_b,应该定义在全局帧中。
1.5 控制
return
语句会重定向控制:无论什么时候执行return
语句,函数调用的流程都会中止,返回表达式的值会作为被调用函数的返回值。
if
语句示例:
>>> def absolute_value(x):
"""Compute abs(x)."""
if x > 0:
return x
elif x == 0:
return 0
else:
return -x
>>> absolute_value(-2) == abs(-2)
True
在Python中,false有0,None,False,true为all other numbers
Python也内建了三个基本的逻辑运算符:
>>> True and False
False
>>> True or False
True
>>> not False
True
其中and
和or
遵循短路原则。
执行比较并返回布尔值的函数通常以 is 开头,后面不跟下划线(例如,isfinite、isdigit、isinstance 等。
1.5.5 迭代
while
用法:
while <expression>:
<suite>
不终止的while语句叫做无限循环。按下-C可以强制让 Python 停止循环。
1.5.6 测试(Testing)
- assert statements
Python中的assert语法为后面跟一个condition,如果这个condition为false,则打印AssertionError:<…>,比如:
assert a > 0, 'a must greater than 0'
如果a <= 0,则打印AssertionError:<a must greater than 0>
如果a > 0,则无事发生
再举一个例子:
def fib_test():
assert fib(2) == 1, 'The 2nd Fibonacci number should be 1'
assert fib(3) == 1, 'The 3rd Fibonacci number should be 1'
assert fib(50) == 7778742049, 'Error at the 50th Fibonacci number'
-
测试通常写在同一个文件或者带有后缀_test.py 的邻近文件中
-
testmod()
- from doctest import testmod或import doctest
- 使用
doctest.testmod()
调用该函数 - 在执行 doctest.testmod()函数时,它会执行该模块中各成员说明性文档包含的测试代码,并将执行结果和指定的结果做比对,如果一致,则什么也不输出;反之,则输出以下提示信息:
显示在哪个源文件的哪一行。
Failed example,显示是哪个测试用例出错了。
Expected,显示程序期望的输出结果。也就是在“>>>命令”的下一行给出的运行结果,它就是期望结果。
Got,显示程序实际运行产生的输出结果。只有当实际运行产生的输出结果与期望结果一致时,才表明该测试用例通过。
- run_docstring_examples
- from doctest import run_docstring_examples或import doctest
-
Its first argument is the function to test. The second should always be the result of the expression globals(), a built-in function that returns the global environment. The third argument is True to indicate that we would like “verbose” output: a catalog of all tests run.
第一个参数:要测试的函数名
第二个参数:globals()
第三个参数:True的意义是想要详细输出所有测试运行的目录
- When the return value of a function does not match the expected result, the run_docstring_examples function will report this problem as a test failure.
- 在文件中编写 Python 时,可以通过使用 doctest 命令行选项启动 Python 来运行文件中的所有 doctest:
python3-m doctest < python_source _ file >
- unit test:只测试单个功能(函数)的test
- 建议:实现新功能(函数)后立即编写并运行测试
lab01
-
If Python reaches the end of the function body without executing a return statement, it will automatically return None.
-
布尔计算的优先顺序:not > and > or
-
Python values such as 0, None, ‘’ (the empty string), and [] (the empty list) are considered false values. All other values are considered true values.
1.6 高阶函数(Higher-Order Functions)
高阶函数:接受其他函数作为参数的函数,或者将函数作为返回值的函数。
1.6.3 函数的嵌套定义(Nested Definitions)
def square_root(x):
def update(guess):
return average(guess, x/guess)
def test(guess):
return approx_eq(square(guess), x)
return iter_improve(update, test)
词法作用域:局部定义的函数也可以访问它们定义所在作用域的名称绑定。这个例子中,update引用了名称x,它是外层函数square_root的一个形参。这种在嵌套函数中共享名称的规则叫做词法作用域。严格来说,内部函数能够访问定义所在环境(而不是调用所在位置)的名称。
词法作用域的两个关键优势:
- 局部函数的名称并不影响定义所在函数外部的名称,因为局部函数的名称绑定到了定义处的当前局部环境中,而不是全局环境。
- 局部函数可以访问外层函数的环境。这是因为局部函数的函数体的求值环境扩展于定义处的求值环境。
1.6.4 作为返回值的函数
>>> def compose1(f, g):
def h(x):
return f(g(x))
return h
>>> add_one_and_square = compose1(square, successor)
>>> add_one_and_square(12)
169
1.6.5 Lambda表达式
>>> def compose1(f,g):
return lambda x: f(g(x))
我们可以通过构造相应的英文语句来理解 Lambda 表达式:
lambda x : f(g(x))
"A function that takes x and returns f(g(x))"
1.6.8 函数装饰器(Function Decorators)
Python 提供了特殊的语法,将高阶函数用作执行def
语句的一部分,叫做装饰器。
>>> def trace1(fn):
def wrapped(x):
print('-> ', fn, '(', x, ')')
return fn(x)
return wrapped
>>> @trace1
def triple(x):
return 3 * x
>>> triple(12)
-> <function triple at 0x102a39848> ( 12 )
36
这个例子中,定义了高阶函数trace1
,它返回一个函数,这个函数在调用它的参数之前执行print
语句来输出参数。triple
的def
语句拥有一个注解,@trace1
,它会影响def
的执行规则。像通常一样,函数triple
被创建了,但是,triple
的名称并没有绑定到这个函数上,而是绑定到了在新定义的函数triple
上调用trace1
的返回函数值上。在代码中,这个装饰器等价于:
>>> def triple(x):
return 3 * x
>>> triple = trace1(triple)
注意triple这个名称绑定了wrapped
函数,因为triple = trace1(triple)
可以阅读装饰器的第一篇评论来更好地了解装饰器。
Project 1: The Game of Hog
练习:分析下列函数,给出流程图与最后的a值,并将代码放入Online Python Tutor中执行,将自己的流程与实际流程进行比较
def make_test_dice(*outcomes):
index = len(outcomes) - 1
def dice():
nonlocal index
index = (index + 1) % len(outcomes)
return outcomes[index]
return dice
def roll_dice(num_rolls, dice):
sum = 0
tag = 0
while num_rolls:
current_num = dice()
if current_num == 1:
tag = True
sum += current_num
num_rolls -= 1
if tag is True:
return 1
else:
return sum
def make_averaged(fn, num_samples=4):
def average(*args):
i, total = 0, 0
while (i < num_samples):
total += fn(*args)
i += 1
return total / num_samples
return average
dice = make_test_dice(3, 1, 5, 6)
averaged_roll_dice = make_averaged(roll_dice, 4)
a = averaged_roll_dice(2, dice)
注意:
-
名称绑定的是函数,不同的名称可以绑定相同的函数,使用名称的本质是调用这个名称绑定的函数
例子:def make_test_dice(*outcomes): index = len(outcomes) - 1 def dice(): nonlocal index index = (index + 1) % len(outcomes) return outcomes[index] return dice
这里的return值绑定的是
dice
函数,所以如果调用make_test_dice
,比如a = make_test_dice(3, 1, 5, 6)
,本质上就是把名称a绑定到dice
函数。 -
*args:传入不定长度的参数
-
make_test_dice(3, 1, 5, 6)
中的3,1,5,6以元组形式传入