函数
函数是一段执行一定功能的代码块,在编写大型程序时通常需要将程序分解成不同的功能模块,这些功能模块往往就是函数,通过编写函数可以降低程序编写的难度,并实现代码的重用。下面将介绍自定义函数的基本概念,然后介绍python中的库函数,最后讨论一些关于作用域的话题。
更新历史:
- 2021年06月13日完成初稿
1. 函数基本概念
就其本质,函数与之前编写的程序没有什么区别,但如果在编写程序时在多处需要相同或相似的程序,那么通过定义一个函数并在相应需要的地方进行调用可以实现相同的功能而且可以简化代码,具有很好的封装性。
1.1 函数的定义和调用
函数的定义如下:
def <函数名> (<参数列表>) :
函数体
return <返回值>
函数都需要一个函数名,它是函数的一个标识符,这些函数名不能与python中的关键字相同,但能和库函数、导入模块的函数名相同,不过自定义函数名会覆盖这些函数名。在定义函数时,需要列出函数的参数,并返回任意多个返回值,函数参数和返回值可以理解为函数的输入和输出,也是外部程序和函数的接口,而函数体则是执行函数功能的一段代码,看看下面的例子:
# 计算 1+2+3+...+n 的累加值
def acc(n):
result = 0
for i in range(1, n+1):
result += i
return result
在上面的函数中,就通过输入一个函数参数n,计算1+2+3+…+n的值,并返回该值,不过定义函数不是最终的目的,还是需要通过函数实现一定的功能,然后在适当的位置调用函数。
函数调用是指通过函数名来调用函数,调用的实质可以这样理解:在程序中,将函数看成一个小黑盒,通过函数名指定这个黑盒子并传入参数,然后这个黑盒子工作并将返回值返回给原代码块,这个返回值可以被赋值给其他变量或作其他用途。对于上面的例子,增加调用函数的代码之后为:
# 增加函数调用
def acc(n):
...
n = eval(input("Please enter a number: "))
val = acc(n) # 调用函数并将返回值赋值给val
print("1+2+3+...+n = {}".format(val))
形参和实参
形参是指函数定义时的形式上的参数,它们只起到了占位符的作用,形参只有在函数被调用时才会分配内存空间;实参是指函数调用时具有意义的实际参数,它们需要将其值传给形参。
尽管上面的函数十分的简单,但已经包含了函数的许多要素,在此还需要明确下面的几个问题:
- 函数可以有几个形参?实参之间有没有顺序?应该如何设置实参?
- 函数的返回值没有的话,那函数还有什么作用?而且如何返回多个值呢?
1.2 函数的参数和返回值
首先,函数可以有0个或多个形参,当含有多个形参时,每个形参之间用逗号分隔。一般来说,高效的函数通常含有几个必要的形参,当函数形参过多时,调用函数时必须传入相同数量和相同定义顺序的实参,这会导致少传或者多传实参,更糟糕的是实参顺序错误,这种情况不会被编译器识别。对于少传或者多传实参这种情况,可以利用默认参数来解决。
默认参数是指在定义函数的形参时,同时设置形参的默认值,不过当且仅当形参没有得到相应值时才会在函数体重利用形参的默认值,例如:
# 默认参数
def acc(n = 1):
...
在上面,如果函数没有得到形参n的值,那么就会利用n的默认值,由于python中允许普通参数和默认参数同时存在,而对应的默认参数可以不传入实际的实参,因此规定默认参数必须放在普通参数的最后:
# 普通参数和默认参数
def function(x1, x2, x3 = 0):
...
在函数中,在调用函数时是按照函数定义时的形参顺序传入对应的实参,因此当传入参数顺序不当时会引起传参错误,但编译器并不会发现这个逻辑上的错误,为了避免这个情况,python允许按照形参的名字传入实参,这类参数也称为关键字参数:
# 关键字参数
def function(x1, x2, x3, y1, y2, y3):
...
function(y1 = 1, y2 = 2, y3 = 3, x1 = 1, x2 = 2, x3 = 3)
另外,定义函数时有可能不知道函数形参的个数,而希望参数个数是可变的。例如利用函数max()计算n个值的最大值时,就需要n个形参,按照上面的定义是无法实现的,因此python贴心地设计了可变数量的参数,通过在参数前增加"*"号即可,不过这些参数必须放在参数列表的最后:
# 可选参数
def function(a, *b):
...
相比于函数参数的复杂,函数的返回值倒是很简洁,对于函数而言,通常需要返回1个或多个返回值,这些返回值可以被赋值给变量。当函数不需要返回值时,可以直接不写return语句,或者return None。
可变数量参数和多个返回值
实际上,可变数量参数和多个返回值是利用元组(一种组合数据类型)来处理的,可变数量参数组成一个元组,从而以元组的形式来传入函数中。而多个返回值实际也是一个含有多个元素的元组,函数实际返回的是整个元组。元组数据类型将在之后的文章中介绍,现在可以将元组理解为一种类似于数组的数据类型。
另外还有一类特殊的函数lambda函数,它们与常见的函数定义格式不同,但也是一类小巧的函数。
1.3 lambda函数
lambda函数也被称为匿名函数,匿名的意思并不是没有函数名,而是将函数名直接作为函数结果返回,其语法格式为:
<函数名> = lambda <参数列表>: <函数表达式>
对于lambda函数而言,一般用于定义简单的函数,即完成功能简单以至于一行以内就可以写完函数体的函数:
# lambda函数
add = lambda a, b: a+b #输入参数a, b返回a+b的值
尽管如此,但还是不建议使用lambda函数声明一个函数(特别是复杂函数),不过lambda会在之后的文章中发挥很大的作用,在这里明白有一类这样的函数即可。
2. 函数的常见用法
上面介绍的都是自定义函数,在python中还有系统内置函数、标准库函数和第三方库函数。自定义函数需要自己编写,而内置函数、标准库函数和第三方库函数可以直接使用。下面首先介绍一些经典的内置函数、标准库函数和第三方库函数,之后再介绍一下递归函数。
2.1 内置函数
Python系统定义了一些常用的函数,可直接使用,在python官方网站上列出来所有的Built-in Functions。在前面已经介绍了一些内置函数,如eval()、input()、print()、range()等,下面列出一些常见的函数:
内置函数 | 意义 |
---|---|
abs(x) | 求x的绝对值 |
chr(i)、ord© | 返回Unicode编码为i的字符、求字符c的Unicode编码 |
complex(real, imag) | 返回 real + imag * j 的复数或将字符串或数字转换为复数 |
divmod(a, b) | 返回a除以b的商和余数 |
float(x)、int(x) | 将x转换为浮点数、将x转换为整数 |
hex(x)、oct(x) | 将整数x转换为以“0x”为前缀的小写十六进制字符串、转换为以“0o”为前缀的八进制字符串 |
len(x) | 返回对象x的长度(项目数),len([1, 2, 3]) = 3 |
max()、min() | 求若干个值或对象的最大值、最小值 |
pow(base, exp[,mod]) | 求base**exp,若有参数mod,表示求pow(base, exp) % mod,例如pow(2, 1000, 11) = 1 |
round(number) | 向偶数舍入 |
sorted(iterable) | 从iterable对象(可迭代对象)返回一个新的排序列表,sorted([1, 3, 2]) = [1, 2, 3] |
str() | 将对象转换为字符串类型 |
sum(iterable) | 对iterable对象求和,sum([1, 2, 3]) = 6 |
type(object) | 返回对象的类型,type([1, 2, 3]) 返回<class ‘list’>,表明[1, 2, 3]为列表类型(一种组合数据类型) |
2.2 标准库函数和第三方库函数
Python中有许多的标准库函数,这些库函数需要先导入模块再使用,每一个模块都有相应的一些函数,如与数学相关的math、random模块、与日期和时间相关的datetime模块等等。对于每个模块,都不是python内置的,必须通过import关键字导入:
import modulename # 直接导入模块
from modulename import * # 导入模块中所有对象,与第一行代码等价
导入模块后,可以通过help(modulename)或者dir(modulename)的方式查看模块中的内容,其中的内容可以通过modname.funcname的方式进行调用。下面就介绍常用的math模块、random模块和datetime模块的相应函数或常数。
math库中有很多与数学运算相关的函数或者常数:
math模块 | 意义 |
---|---|
pi | 圆周率3.141592653589793 |
e | 自然常数2.718281828459045 |
ceil() | 向上取整 |
floor() | 向下取整 |
pow(x, y) | 计算xy |
log(x) | 计算logx |
sqrt(x) | 计算 x \sqrt{x} x |
sin(x) | 计算sinx |
利用random库可以产生随机数,不过这些随机数都是伪随机数,不是真正的随机数:
random模块 | 意义 |
---|---|
random() | 生成[0.0, 1.0)之间的随机浮点数 |
randint(a, b) | 生成一个[a, b]的随机整数,a小于等于b |
uniform(a, b) | 生成a ~ b之间的随机浮点数,a不一定小于等于b |
randrange(start, stop=None, step=1) | 从range(start, stop[, step])生成的数中随机输出一个数 |
datetime库中包括了date库和time库,是有关于日期和时间的库:
datetime模块 | 意义 |
---|---|
from datetime import date | 导入date模块,这是有关日期的模块 |
date.today() | 查询当前的年月日 |
date(year, month, day) | 产生一个日期对象 |
from datetime import time | 导入time模块,这是有关时间的模块 |
time(hour, minute, second) | 产生一个时间对象 |
from datetime import datetime | 导入datetime模块,这是有关日期时间的模块 |
datetime.now() | 显示当前的时间和日期 |
除了上面几个常用的标准库函数之外,python标准库函数还有很多,这里不一一介绍。
Python的强大之处在于Python中有许多第三方库,比如著名的科学计算包SciPy、图像处理库Pillow和自然语言处理包NLTK等,这一部分内容会在之后的python课程里面逐一介绍。
2.3 递归函数
在函数中,有一类非常特殊的函数,它直接或者间接地调用了本身,被称为递归函数,这种调用方式称为递归调用,在算法设计与分析中递归函数编写的递归算法可以解决许多的问题。下面以计算n! = n×(n-1)×…×1(0! = 1)为例,介绍递归函数:
# 阶乘递归函数,不考虑n为负数
def fact(n):
if n==0 or n==1: # 当n为0或者1时停止递归
return 1
else:
return n*fact(n-1) # 递归地调用函数本身
下面以计算3!为例,图解递归函数的调用过程:
在上图中,首先n=3时不满足n == 0 or n == 1的「递归终止条件」,因此递归调用函数fact(2),也不满足,之后调用fact(1)时满足递归终止条件并直接返回1,之后返回到上一层递归调用并返回2*1,之后不断返回,直至返回3!。在上述递归调用中有两个过程:
- 递进过程:函数不断调用本身,但函数的调用规模减小,直至满足递归终止条件
- 回归过程:从递归终止条件返回上一层递归,之后不断将返回值返回,并完成计算
其中最重要的有两个条件,这两个条件缺一不可:
- 递进过程中,存在递归终止条件
- 递进过程中,递归规模减小以能够满足递归终止条件
对于初学者而言,编写递归函数的一些技巧是编写递归函数的几个主要部分:递归终止条件、递归函数体,并确保递归规模不断变小。不过递归函数的缺点在于其效率不高,需要多次调用函数(甚至重复调用函数),对于内存资源是一个很大的浪费。一般而言递归函数都可以转化为非递归函数,比如上面求阶乘的例子就可以转换为:
# 阶乘普通函数,不考虑n为负数
def fact(n):
result = 1
if n == 0 or n == 1:
return result
else:
for i in range(n, 1, -1):
result *= i
return result
3. 变量的作用域
上面介绍了很多关于函数的知识,在上面的函数中函数体在前,而函数调用在后,根据先定义后使用的规则这自然是正确的,但其中还有一些值得考虑的问题:在函数体中出现的变量是否在调用函数时也可以利用?回到上面的程序中:
def acc(n):
result = 0
for i in range(1, n+1):
result += i
return result
n = eval(input("Please enter a number: "))
val = acc(n)
print("1+2+3+...+n = {}".format(val))
在上面的程序中,调用acc(n)返回result并赋值给val,那么能不能在打印语句中直接利用result而省去使用变量val的机会呢?答案是不可以,因为变量都有一定的作用域,当函数acc(n)返回result之后,result已经不存在了。
在python中,每个变量都有自己的作用域,即在某个代码段内使用该变量是合法的,在此代码段之外使用该变量则是非法的。在这里可能需要深刻地讨论这样的话题,尽可能用通俗简单的语言回答,另外在python官网上面有关于python作用域和命名空间的详细介绍,不过在这里只需要了解以下几点也可以达到相同的效果:
首先给出namespaces(命名空间)的定义:命名空间是从名字(标识符)到对象的映射,在不同的命名空间中,两个相同或者不同的名字是没有任何关系的,因此在两个函数中可以分别定义相同名字的变量而不会引起冲突。而在python中一般有三种命名空间:
- 内置命名空间:在 Python 解释器启动时创建,并且永远不会被消亡
- 全局命名空间:对于模块而言,其命名空间在读入模块定义时创建的,通常会持续到解释器退出
- 局部命名空间:对于函数而言,其命名空间在调用函数时创建,并在函数返回时消亡
而作用域则是命名空间可直接访问的代码区域,尽管作用域是静态确定的,但它们通常被动态使用,在执行过程中主要有以下3个或4个作用域,当在函数中有未确定的变量名时搜索变量名的顺序是:
- 局部作用域:只包含中函数中
- 嵌套作用域:通常对于函数而言,指在上一层函数中
- 全局作用域:通常是整个模块中
- 内置作用域:预定义在系统内置模块中
当搜索完上面的作用域都找不到名称后,则会产生NameError异常。因此在上面的程序中result在局部作用域中,而打印语句中位于全局作用域,其不会在局部作用域中寻找有没有一个变量叫做result,因此无法使用该变量。