Python基础——函数、参数传递、递归、引用模块、异常处理

第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: 代码规范

函数和模块,在命名时全部使用的是小写字母。单词之间用下划线连接。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值