3.1 懒人炒菜机
1.函数是什么
数学上函数f(x)定义了两组数字之间的对应关系:
x -> y
1 1
2 4
3 9
4 16
…
对于程序员来说,函数是这样一种语法结构:它把一些指令封装在一起,形成一个组合拳。函数是对封装理念的实践。 输入数据被称为参数,参数能影响函数的行为。
2.定义函数
制作函数的过程又称为定义函数(definefunction)。这个函数的功能是计算两个数的平方和:
def square_sum(a,b):
a = a**2
b = b**2
c = a + b
return c
在定义函数square_sum()时,我们用参数a和b完成了符号化的平方求和。 最后一句return用于说明函数的返回值,即函数的输出数据。函数执行到return时就会结束,不管它后面是否还有其他函数定义语句。
在Python的语法中,return并不是必需的。 如果没有return, 或者return后面没有返回值时,则函数将返回None。 None是Python中的空数据,用来表示什么都没有。 关键字return也返回多个值。 多个值跟在return后面,以逗号分隔。 从效果上看,其等价于返回一个有多个数据的元组。
return a,b,c # 相当于 return (a,b,c)
3.调用函数
a = 5
b = 6
x = square_sum(a, b)
print(x) # 结果为61
Python通过参数出现的先后位置,知道定义中的第一个形参a,和第二个形参b,然后把参数传递给函数square_sum()。 函数square_sum()执行内部的语句,直到得出返回值61。
4.函数文档
一个问题常见就是,我们经常会忘记一个函数是用来做什么的。我们可以用内置函数help()来找到某个函数的说明文档。 以函数max()为例,用这个函数用来返回最大值。 比如:
x = max(1, 4, 15, 8)
print(x) # 结果为15
>>> help(max) # 以下为help()运行的结果,也就是max()的说明文档。
Help on built-in function max in module __builtin__:
max(...)
max(iterable[, key=func]) -> value
max(a, b, c, ...[, key=func]) -> value
With a single iterable argument, return its largest item.
With two or more arguments, return the largest argument.
(END)
可以看到,函数max()有两种调用方式。 我们之前的调用是按照第二种方式。 对于我们自定义的函数,还需要自己动手添加函数说明文档。 下面给函数square_sum()加上简单的注释:
def square_sum(a,b):
"""return the square sum of two arguments"""
a = a**2
b = b**2
c = a + b
return c
>>>help(square_sum)
Help on function square_sum in module __main__:
square_sum(a, b)
return the square sum of two arguments
3.2 参数传递
1.基本传参
把数据用参数的形式输入到函数,被称为参数传递。 如果有多个参数,那么在调用函数时,Python会根据位置来确认数据对应哪个参数,比如:
def print_arguments(a, b, c):
"""print arguments according to their sequence"""
print(a, b, c)
print_arguments(1, 3, 5) # 打印1、 3、 5
print_arguments(5, 3, 1) # 打印5、 3、 1
print_arguments(3, 5, 1) # 打印3、 5、 1
在定义函数时,我们给了形参一个符号标记,即参数名。 关键字传递是根据参数名来让数据与符号对应上。 因此,如果在调用时使用关键字传递,那么不用遵守位置的对应关系。
print_arguments(c=5,b=3,a=1) # 打印1、 3、 5
位置传递与关键字传递可以混合使用,但如果把位置参数1放在关键字参数c=5的后面,则Python将报错。
print_arguments(1, c=5,b=3) # 打印1、 3、 5
print_arguemnts(c=5, 1, b=3) # 程序报错
但在函数定义时,我们可以设置某些形参的默认值。 如果我们在调用时
不提供这些形参的具体数据,那么它们将采用定义时的默认值。
def f(a,b,c=10):
return a+b+c
print(f(3,2,1)) # 参数c取传入的1。 结果打印6
print(f(3,2)) # 参数c取默认值10。 结果打印15
2.包裹传参
有时在定义函数时,我们并不知道参数的个数。这时候,用包裹(packing)传参的方式来进行参数传递会非常有用。为了提醒Python参数all_arguments是包裹位置传递所用的元组名,我们在定义package_position()时要在元组名all_arguments前加*号。下面是包裹位置传参的例子:
def package_position(*all_arguments):
print(type(all_arguments))
print(all_arguments)
package_position(1,4,6)
package_position(5,6,7,1,2,3)
参数all_arguments是包裹关键字传递所用的字典,因此在all_arguments前加**
下面是包裹关键字传递的例子。
def package_keyword(**all_arguments):
print(type(all_arguments))
print(all_arguments)
package_keyword(a=1,b=9)
package_keyword(m=2,n=1,c=11)
包裹位置传参和包裹关键字传参还可以混合使用:
def package_mix(*positions, **keywords):
print(positions)
print(keywords)
package_mix(1, 2, 3, a=7, b=8, c=9)
包裹传参和基本传参可以混合使用。 它们出现的先后顺序是:位置→关键字→包裹位置→包裹关键字。
3.解包裹
*和**可用于函数调用实现一种叫作解包裹(unpacking)的语法。 解包裹允许我们把一个数据容器传递给函数,再自动地分解为各个参数。包裹传参和解包裹并不是相反操作,而是两个相对独立的功能。 例如:
def unpackage(a,b,c):
print(a,b,c)
args = (1,3,4)
unpackage(*args) # 结果为1 3 4
args = {"a":1,"b":2,"c":3}
unpackage(**args) # 打印1、 2、 3
我们通过在args前加上*符号,来提醒Python,我想把元组拆成三个元素,每一个元素对应函数的一个位置参数。于是,元组的三个元素分别赋予了三个参数。 通过在args前面加上**符号提醒Python传递词典args,让词典的每个键值对作为一个关键字传递给函数unpackage()。解包裹用于函数调用时依然是相同的基本原则:位置→关键字→位置解包裹→关键字解包裹。
3.3 递归
1.高斯求和与数学归纳法
老师说必须算出1到100的和才能回家。 7岁的高斯想出了一个聪明的解决办法,后来这个方法被称为高斯求和公式。
sum = 0
for i in range(1, 101): # range()这样的写法表示从1开始,直到100
sum = sum + i
print(sum) # 结果为5050
我们还可以用了递归法。递归(Recursion),即在一个函数定义中,调用了这个函数自身。递归的关键是说明紧邻的两个步骤之间的衔接条件。 尽管整个递归过程很复杂,但在编写程序时,我们只需关注初始条件、 终止条件及衔接,而无须关注具体的每一步。 计算机会负责具体的执行。
def gaussian_sum(n):
if n == 1:
return 1
else:
return n + gaussian_sum(n-1)
print(gaussian_sum(100)) # 结果为5050
递归源自数学归纳法:
第一步 证明命题对于n = 1成立。
第二步 假设命题对于n成立,n为任意自然数,则证明在此假设下,命题对于n+1成立。
命题得证
2.函数栈
程序中的递归需要用到栈(Stack)这一数据结构。 栈最显著的特征是“后进先出” (LIFO,Last In,First Out)。就像我们往箱子里存放一叠书时,每一本书,也就是栈的每个元素,称为一个帧(frame)。 栈只支持两个操作:pop和 push。 栈用弹出(pop)操作来取出栈顶元素,用推入(push)操作将一个新的元素存入栈顶。
为了计算gaussian_sum(100),我们需要先暂停gaussian_sum(100),开始gaussian_sum(99)的计算。每次函数调用时,我们在栈中推入一个新的帧,用来保存这次函数调用的相关信息。 栈不断增长,直到计算出gaussian_sum(1)后,我们又会恢复计算gaussian_sum(2)、gaussian_sum(3),……。
所以,程序运行的过程,可以看作是一个先增长栈后消灭栈的过程。 每次函数调用,都伴随着一个帧入栈。 如果函数内部还有函数调用,那么又会多一个帧入栈。 当函数返回时,相应的帧会出栈。 等到程序的最后,栈清空,程序就完成了。
3.变量的作用域
在下面的程序中,主程序和函数external_var()都有一个info变量。 在函数external_var()内部,会优先使用函数内部的那个info,且函数内部使用的是自己内部的那一份,所以函数内部对info的操作不会影响到外部变量info。
def external_var():
info = "Vamei's Python"
print(info) # 结果为"Vamei's Python"
info= "Hello World!"
external_var()
print(info) # 结果为"Hello World!"
在函数调用时,会把数据赋值给这些变量。 等到函数返回时,这些参数相关的变量会被清空。 但也有特例,如:
b = [1,2,3]
def change_list(b):
b[0] = b[0] + 1
return b
print(change_list(b)) # 打印[2, 2, 3]
print(b) # 打印[2, 2, 3]
3.4 引入那把宝剑
1.引入模块
在Python中,一个.py文件就构成一个模块。 通过模块,你可以调用其他文件中的函数。 而引入(import)模块,就是为了在新的程序中重复利用已有的Python程序。我们先写一个first.py文件,再在同一目录下写一个second.py文件。 在这段程序中引入first模块:
def laugh():
print("HaHaHaHa")
from first import laugh
for i in range(10):
laugh()
借着import语句,我们可以在second.py中使用first.py中定义的laugh()函数。 除了函数,我们还可以引入其他文件中包含的数据。把常见的功能编到模块中,方便未来使用,就成为所谓的库(library)。
2.搜索路径
Python还会到其他的地方寻找库:
(1)标准库的安装路径
(2)操作系统环境变量PYTHONPATH所包含的路径
标准库是Python官方提供的库。 Python会自动搜索标准库所在的路径。 因此,
Python总能正确地引入标准库中的模块
3.5 异常处理
1.恼人的bug
工程师很早就开始用bug这个词来指代机械缺陷。 bug也被用于指代程序缺陷。如语法错误:
for i in range(10)
print(i)
SyntaxError: invalid syntax
编译器才会发现的错误被称为运行时错误(RuntimeError):
a = [1, 2, 3]
print(a[3])
IndexError: list index out of range
还有一种错误,称为语义错误(Semantic Error)。比如想打印第一个元素,却打印了第二个元素。
bundle = ["a", "b", "c"]
print(bundle[1])
2.Debug
修改程序缺陷的过程称为debug。 对于初学者来说,不需要花太多的时间在这些工具上。 在程序内部插入简单的print()函数,就可以查看变量的状态以及运行进度。
从另一个方面来看,debug也是写程序的一个自然部分。 有一种开发程序的方式,就是测试驱动开发(Test-Driven Development,TDD)。debug其实是你写出完美程序的一个必要步骤。
3.异常处理
对于运行时可能产生的错误,我们可以提前在程序中处理。 这样做有两个可能的目的:一个是让程序中止前进行更多的操作,比如提供更多的关于错误的信息。另一个则是让程序在犯错后依然能运行下去。
异常处理还可以提高程序的容错性。 下面的一段程序就用到了异常处理:
while True:
inputStr = input("Please input a number:") # 等待输入
try:
num = float(inputStr)
print("Input number:", num)
print("result:", 10/num)
exceptValueError:
print("Illegal input.Try Again.")
exceptZeroDivisionError:
print("Illegal devision by zero.Try Again.")
若except后面没有任何参数那么表示所有的exception都交给这段程序处理:
while True:
inputStr = input("Please input a number:")
try:
num = float(inputStr)
print("Input number:", num)
print("result:", 10/num)
except:
print("Something Wrong.Try Again.")
使用raise关键字可以在程序中主动抛出异常:
raiseZeroDivisionError()