第3章 过程大于结果
2. 定义函数
def square_sum(a, b):
a = a**2
b = b**2
c = a + b
return c
即使没有输入数据,函数后面的括号也要保留。
由于函数定义中的参数是一个形式化代表,并非真正数据,所以又称为形参(Parameter)。
关键字return用于说明函数的返回值,即函数的输出数据。
作为函数的最后一句,函数执行到return时就会结束,不管它后面是否还有其他函数定义语句。如果把square_sum()改为如下的形式:
def square_sum(a,b):
a = a**2
b = b**2
c = a + b
return c
print("am I alive?")
则函数执行时,只会执行到return c。后面一句print()虽然也归属于函数,却不会被执行。所以,return还起到了中止函数和制定返回值的功能。在Python语法中,return并不是必须的。如果没有return,或者return后面没有返回值,则函数将返回None。None是Python中的空数据,用来表示什么都没有。关键字return也返回多个值。多个值跟在return后面,以逗号分隔。从效果上看,其等价于返回一个有多个数据的元祖。
return a,b,c # 相当于return (a,b,c)
3. 调用函数
在函数调用时出现的参数称为实参(argument)。
函数print()返回值为None,所以我们不关心这个返回值。但如果一个函数有其他返回值,那么我们可以获得这个返回值。一个常见的做法就是把返回值赋予给变量,方便以后使用。下面程序中调用了square_sum()函数:
x = square_sum(3,4)
print(x)
在调用函数时,我们把真实的数据填入到括号中,作为参数传递给函数。除具体的数据表达式外,参数还可以是程序中已经存在的变量,比如:
a = 5
b = 6
x = square_sum(a, b)
print(x)
可以用help()来找到某个函数的说明文档。
x = max(1, 4, 15, 8)
print(x)
>>> help(max) # 以下为help()运行的结果,也就是max()的说明文档
>>> help('sys') # 查看sys模块的帮助
>>> help('str') # 查看str数据类型的帮助
>>> a = [1, 2, 3]
>>> help(a) # 查看列表list帮助信息
>>> help(a.append) # 显示list的append方法的帮助
函数max()属于Python自身定义好的内置函数,所以已经提前准备好了说明文档。对于我们自定义的函数,还需要自己动手。下面给函数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()将返回我们定义函数时写下的内容:
>>>help(square_sum)
不过根据实验观察,我们没法将自己写的模块导入交互式界面。不过,可以在自己写的.py脚本中调用help()函数,来显示多行注释中的说明文档
def square_sum(a, b):
""" return the square sum of two arguments"""
a = a**2
b = b**2
c = a + b
return c
a = 5
b = 6
help(square_sum)
x = square_sum(a, b)
print(x)
也可以在交互式界面中,python 绝对路径\main.py
3.2 参数传递
1. 基本传参
如果有多个参数,那么在调用函数时,Python会根据位置来确定数据对应哪个参数,例如:
def print_arguments(a, b, c):
"""print arguments according to their sequence"""
print(a, b, c)
print_argument(1, 3, 5)
print_argument(5, 3, 1)
print_argument(3, 5, 1)
在程序的三次调用中,Python都是通过位置来确定实参与形参的对应关系的。
可以用关键字(Keyword)方式来传递参数。在定义函数时,我们给了形参一个标记符号,即参数名。关键字传递是根据参数名来让数据与符号对应上。
print_arguments(c=5, b=3, a=1) # 打印1、3、5
位置传递与关键字可以混用,即一部分的参数传递根据位置,另一部分根据参数名。在调用函数时,所有的位置参数都要出现在关键字参数之前。因此,可以用如下方式调用:
print_arguments(1, c=5, b=3) # 打印1,3,5
但如果把位置参数1放在关键字参数c=5的后面,则Python将报错:
print_arguments(c=5, 1, b=3)
SyntaxError: positional argument follows keyword argument
在函数定义时,我们可以设置某些形参的默认值。如果我们在调用时不提供这些形参的具体数据,那么它们将采用定义时的默认值。比如:
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)传参的方式来进行参数传递会非常有用。
和之前一样,包裹传参也有位置和关键字两种形式。下面是包裹位置传参的例子:
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)
两次调用,尽管参数个数不同,但都基于同一个package_position()定义。在调用package_position()时,所有的数据都根据先后顺序,收集到一个元组。在函数内部,我们可以通过元组来读取传入的数据。这就是包裹位置传参。为了提醒Python参数all_arguments是包裹位置传递所用的元组名,我们在定义package_position()时要在元组名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=11, c=11)
与上面一个例子类似,当函数调用时,所有参数会收集到一个数据容器里。只不过,在包裹关键字传递的时候,数据容器不再是一个元组,而是一个字典。每个关键字形式的参数调用,都会成为字典的一个元素。参数名成为元素的键,而数据成为元素的值。字典all_arguments收集了所有的参数,把数据传递给函数使用。为了提醒,参数all_arguments是包裹关键字传递所用的字典,因此在all_arguments前加。
包裹位置传参和包裹关键字传参还可以混合使用,比如:
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
在这个例子中,unpackage使用了基本的传参方法。函数有三个参数,按照位置传递。但在调用该函数时,我们用了解包裹的方式。可以看到,我们调用函数时传递的是一个元祖。按照基本传参的方式,一个元组是无法和三个参数对应上的。但我们通过在args前加上*符号,来提醒Python,我们想把元组拆成三个元素,每一个元素对应函数的一个位置参数。于是,元组的三个元素分别赋予了三个参数。
相应的,词典也可用于解包裹,使用相同的unpackage()定义:
args = {"a":1, "b":2, "c":3}
unpackage(**args) # 打印1、2、3
然后再传递词典args时,让词典的每个键值对作为一个关键字传递给函数unpackage()。
解包裹用于函数调用。在调用函数时,几种参数的传递方式也可以混合。依然是相同的基本原则:位置->关键字->位置解包裹->关键字解包裹。
3.3 递归
sum = 0
for i in range(1, 101): # range()这样的写法表示从1开始,直到10
sum = sum + i
print(sum) # 结果为5050
def gaussian_sum(n):
if n == 1:
return 1
else:
return n + gaussian_sum(n-1)
print(gaussian_sum(100)) # 结果为5050
递归(Recursion),即在一个函数定义中,调用了这个函数自身。递归要求程序有一个能够达到的终止条件(Base Case)。递归的关键是说明紧邻的两个步骤之间的衔接条件。比如,我们已经知道了1到51的累加和,即gaussian_sum(51),那么1到52的累加和就可以很容易地求得:gaussian_sum(52) = gaussian_sum(51) + 52。
使用递归设计程序时,我们从最终结果入手,即要想求得gaussian_sum(100),计算机会把这个计算拆解为求得gaussian_sum(99)的运算,以及gaussian_sum(99)加上100的运算。以此类推,直到拆解为gaussian_sum(1)的运算,就触发终止条件,也就是if结构中n=1时,返回一个具体的数1。在编写程序时,我们只需关注初始条件、终止条件及衔接,而无需关注具体的每一步。
3. 变量的作用域
函数内部可以创建新变量,如下面一个函数:
def interval_val(a, b):
c = a + b
return c
print(interval_var(2, 3)) # 结果为5
事实上,Python寻找变量的范围不止是当前帧。它还会寻找函数外部,也就是Python的主程序中定义了的变量。因此在一个函数内部,我们能“看到”函数外部已经存在的变量。比如下面程序:
(注释:所谓的主程序,其实就是一个.py程序构成的模块。)
def inner_var():
print(m)
m = 5
inner_var() # 结构将打印5
当主程序中已经有了一个变量,函数调用内部可以通过赋值的方式再创建一个同名变量。函数会优先使用自己函数帧中的那个变量。在下面的程序中,主程序和函数external_var()都有一个info变量。在函数external_var()内部,会优先使用函数内部的那个info:
def external_var():
info = "Vamei's Python"
print(info) # 结果为"Vamei's Python"
info = "Hello World!"
external_var()
print(info) # 结果为“Hello World!”
且函数内部使用的是自己内部的那一份,所以函数内部对info的操作不会影响到外部变量info。
函数的参数与函数内部变量类似。我们可以把参数理解为函数内部的变量。在函数调用时,会把数据赋值给这些变量。等函数返回时,这些参数相关的变量会被清空。但也有特例,如下面的例子:
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]
我们将一个表传递给函数,函数进行操作后,函数外部的表b发生变化。**当参数是一个数据容器时,函数内外部只存在一个数据容器,所以函数内部对该容器的操作,会影响到函数外部。**现在需要记住的是,对于数据容器来说,函数内部的更改会影响到外部。
3.4 引入那把宝剑
1. 引入模块
在Python中,一个.py文件就构成了一个模块。通过模块,你可以调用其他文件中的函数。而引入(import)模块,就是为了在新的程序中重复利用已有的Python程序
除了函数,我们还可以引入其他文件中包含的数据。
对于面向过程语言来说,模块是比函数更高一层的封装模式。程序可以以文件为单位实现复用。把常见的功能编到模块中,就成为库(library)。
2. 搜索路径
我们刚才在引入模块时,把库文件和应用文件放在了同一文件夹下。当在该文件夹下运行程序时,Python会自动在当前文件夹搜索它想要引入的模块。
但Python还会到其他的地方去寻找库:
(1) 标准库的安装路径
(2) 操作系统环境变量PYTHONPATH所包含的路径
标准库是Python官方提供的库。Python会自动搜索标准库所在的路径。因此,Python总能正确地引入标准库中的模块。例如:
import time
如果你是自定义的模块,则可以放在自认为合适的地方,然后修改PYTHONPATH这个环境变量。当PYTHONPATH包含模块所在的路径时,Python便可以找到那个模块。
3.5 异常处理
对于运行时可能产生的错误,我们可以提前在程序中处理。这样做有两个可能的目的:一个时让程序中止前进行更多的操作,比如提供更多的关于错误的信息。另一个则是让程序在犯错后依然能运行下去。
异常处理还可以提高程序的容错性。下面一段程序就用到了异常处理。
while True:
inputStr = input("Please input a number: ") # 等待输入
try:
num = float(inputStr)
print("Input number: ", num)
print("result: ", 10/num)
except ValueError:
print("Illegal input. Try again.")
except ZeroDivisionError:
print("Illegal devision by zero. Try again.")
需要异常处理的程序包裹在try结构中。而except说明了当特定错误发生时,程序应该如何应对。程序中,input()是一个内置函数,用来接收命令行的输入。而float函数则用于把其他类型的数据转换为浮点数。如果输入的是一个字符串,如"p",则将无法转换成浮点数,并触发ValueError,而相应的except就会运行隶属于它的程序。如果输入的是0,那么除法的分母为0,将触发ZeroDivisionError。这两种错误都由预设的程序处理,所以程序运行不会中止。
如果没有发生异常,比如输入5.0。那么try部分正常运行,except部分被跳过。异常处理完整的语法形式为:
try:
...
except exception1:
...
except exception2:
...
else:
...
finally:
...
如果try中有异常发生,则执行异常的归属,执行except。异常层层比较,看看是否是exception1、exception2…直到找到其归属,执行相应的except中的语句。如果try中没有异常,那么except部分将跳过,执行else中的语句。
finally是无论是否有异常,最后都要做的一些事情。
如果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.")
如果无法将异常交给合适的对象,那么异常将继续向上层抛出,直到被捕捉或者造成主程序错误,比如下面的程序:
def test_func():
try:
m = 1/0
except ValueError:
print("Catch ValueError in the sub-function")
try:
test_func()
except ZeroDivisionError:
print("Catch error in the main program")
子程序的try…except…结构无法处理相应的除以0的错误,所以错误被抛给上层的主程序。
使用raise关键字,我们也可以在程序中主动抛出异常。比如:
raise ZeroDivisionError()
附录A 搜索路径的设置
在Python内部,可以用下面的方法来查询搜索路径
import sys
print(sys.path)
可以看到,sys.path是一个列表。列表中的每个元素都是一个会被搜索的路径。我们可以通过增加或删除这个列表中的元素,来控制Python中的搜索路径。
上面的更改方法是动态的,所以每次写程序时都要添加相关的改变。
P86
附录B:安装第三方模块
使用pip安装第三方模块
pip install numpy
找到安装的所有模块,以及模块的版本:
pip freeze
附录C: 代码规范
函数和模块,在命名时全部使用的是小写字母。单词之间用下划线连接。