人工智能技术之函数之一
第7章 函数(上)
本章学习目标
• 理解函数的概念
• 掌握函数的定义
• 掌握函数的参数与返回值
• 理解变量的作用域
• 理解函数的嵌套调用与递归调用
Python程序都是由一系列语句组成的,这些语句都是为了实现某个具体的功能。如果这个功能在整个应用中会经常使用,则每一处需要该功能的位置都写上同样的代码,必将会造成大量的冗余代码,不便于开发及后期维护。为此,Python中引入函数的概念,它就是为了解决一些常见问题而提前制作的模型。
7.%2 函数的概念
函数可以理解为实现某种功能的代码块,这样当程序中需要这个功能时就可以直接调用,而不必每次都编写一次。这就好比生活中使用计算器来计算,当需要计算时,直接使用计算器输入要计算的数,计算完成后显示计算结果,而不必每次计算都通过手写演算出结果。
在程序中,如果需要多次输出“拼搏到无能为力,坚持到感动自己!”,则可以将这个功能写成函数,具体示例如下:
def output():
print('拼搏到无能为力,坚持到感动自己!')
当需要使用该函数时,则可以使用以下语句:
output()
该条语句可以多次使用。函数使减少代码冗余成为现实,并为代码维护节省了不少力气。
Python中的函数分为内建函数和自定义函数。内建函数是Python自带的,即可以直接使用,如print()函数、input()函数等。常见的内建函数如表7.1所示,本章主要介绍自定义函数。
表7.1 内建函数
abs() |
dict() |
help() |
min() |
setattr() |
all() |
dir() |
hex() |
next() |
slice() |
any() |
divmod() |
id() |
object() |
sorted() |
ascii() |
enumerate() |
input() |
oct() |
staticmethod() |
bin() |
eval() |
int() |
open() |
str() |
bool() |
exec() |
isinstance() |
ord() |
sum() |
bytearray() |
filter() |
issubclass() |
pow() |
super() |
bytes() |
float() |
iter() |
print() |
tuple() |
callable() |
format() |
len() |
property() |
type() |
chr() |
frozenset() |
list() |
range() |
vars() |
classmethod() |
getattr() |
locals() |
repr() |
zip() |
compile() |
globals() |
map() |
reversed() |
|
complex() |
hasattr() |
max() |
round() |
|
delattr() |
hash() |
memoryview() |
set() |
|
7.%2 函数的定义
内建函数的数量是有限的,如果编程者想自己设计符合使用需求的函数,则可以定义一个函数,其语法格式如下:
def 函数名(参数列表):
函数体
在上述语法格式中,需注意以下几点:
• def(即define,定义)为关键字,表示定义一个函数。
• 函数名是一个标识符,注意不能与关键字重名。
• 圆括号之间可以用于定义参数,参数是可选的,但圆括号不可少。
• 函数体以冒号起始,并且缩进。
• 函数体的第一行语句可以选择性地使用文档字符串用来存放函数说明。
• return [表达式]结束函数,将表达式的值返回给调用者,也可以省略。
接下来演示一个简单的自定义函数,如例7-1所示。
例7-1
1 def sum2num(a, b):
2 '''
3 求两个数的和
4 param a: 左操作数
5 param b: 右操作数
6 return: 左操作数与右操作数之和
7 '''
8 return a + b
9 x = sum2num(3, 4)
10 print(x)
11 print(sum2num.__doc__)
运行结果如图7.1所示。
图7.1 运行结果
在例7-1中,第2行到第7行为文档字符串,初学者在初学阶段只需了解即可。若想查看一个函数的文档字符串,则可以通过__doc__属性,如第11行所示。关于自定义函数sum2num()的解释,如图7.2所示。
图7.2 自定义函数
定义函数后,就相当于有了一个具有某些功能的代码。如果想让程序执行这些代码,则需要调用之前定义的函数,其语法格式如下:
函数名(参数)
在例7-1中,求3与4的和时,则可以通过以下语句实现:
sum2num(3, 4)
7.%2 函数的参数
参数列表由一系列参数组成,并用逗号隔开。在调用函数时,如果需要向函数传递参数,则被传入的参数称为实参,而函数定义时的参数称为形参,实参与形参之间可以传递数据。
7.3.1 位置参数
位置参数是指函数调用时传递实参的顺序与定义函数的形参顺序一致,如例7-2所示。
例7-2
1 def printInfo(name, score):
2 print('姓名:%s\n成绩:%.2f'%(name, score))
3 printInfo('小千', 98)
4 # printInfo(98, '小千')
运行结果如图7.3所示。
图7.3 运行结果
在例7-2中,第1行到第2行定义printInfo()函数。第3行调用该函数,其数据传递如图7.4所示。第4行将两个实参的位置调换,则发生错误,因此将此行注释。
图7.4 函数参数传递
在图7.4中,当函数调用时,实参的传递顺序与定义函数形参的顺序需保持一致。由于实参的顺序与函数定义时形参的位置有关,因此称为位置参数。
7.3.2 关键参数
关键参数是指函数调用时允许传递实参的顺序与定义函数的形参顺序不一致,因为 Python解释器能够用形参名匹配实参值,避免了用户需要牢记位置参数顺序的麻烦,如例7-3所示。
例7-3
1 def printInfo(name, score):
2 print('姓名:%s\n成绩:%.2f'%(name, score))
3 printInfo('小千', 98)
4 printInfo(score = 98, name = '小千')
运行结果如图7.5所示。
图7.5 运行结果
在例7-3中,第1行到第2行定义printInfo()函数,第4行调用函数,其参数是根据函数定义时形参的名称进行数据传递,因此称为关键参数。
7.3.3 默认参数
如果在函数定义时参数列表中的某个形参有值,就称这个参数为默认参数。注意默认参数必须放在非默认参数的右侧,否则函数将出错,如例7-4所示。
例7-4
1 def printInfo(name, school = '千锋教育'):
2 print('姓名:%s\t学校:%s'%(name, school))
3 printInfo('小千')
4 printInfo('小锋', '扣丁学堂')
5 printInfo(school = '好程序员特训营', name = '小明')
运行结果如图7.6所示。
图7.6 运行结果
在例7-4中,第3行调用函数时,由于定义函数时形参school有默认值'千锋教育',因此调用时可以省略不写该参数,如果想修改默认值,则在调用时传入该参数即可,如本例中的第4行。
默认参数可以让函数的调用尽可能简化,就如同安装PC端软件时,程序会提示用户默认安装路径,当然用户也可以自定义安装路径。
此外,如果将例题中的name与school调换位置,具体示例如下:
def printInfo(school = '千锋教育', name): # 错误写法
print('姓名:%s\t学校:%s'%(name, school))
运行程序将会报错,如图7.7所示。
图7.7 运行结果
7.3.4 不定长参数
在前面的函数介绍中,一个形参只能接收一个实参,除此之外,函数形参可以接收不定个数的实参,即用户可以给函数提供可变长度的参数,这可以通过在参数前面使用*来实现,如例7-5所示。
例7-5
1 def mySum(a = 0, b = 0, *args):
2 print(a, b, args)
3 sum = a + b
4 for n in args:
5 sum += n
6 return sum
7 print(mySum(1, 2))
8 print(mySum(1, 2, 3))
9 print(mySum(1, 2, 3, 4))
运行结果如图7.8所示。
图7.8 运行结果
在例7-5中,第1行中加了星号的变量args会存放所有未命名的变量参数,其数据类型为元组。第7行调用函数时传入2个实参,分别对应形参a与b,此时args是一个空元组。第8行调用函数时传入3个参数,此时将第3个参数添加到元组中。第9行调用函数时传入4个参数,此时将后两个参数添加到元组中。
此外,不定长参数还可以接受关键参数并将其存放到字典中,这时需要使用**来实现,如例7-6所示。
例7-6
1 def mySum(a = 0, b = 0, *args1, **args2):
2 print(a, b, args1, args2)
3 sum = a + b
4 for n in args1: # 遍历元组
5 sum += n
6 for key in args2: # 遍历字典
7 sum += args2[key]
8 return sum
9 print(mySum(1, 2, 3, 4))
10 print(mySum(1, 2, c = 3, d = 4))
11 print(mySum(1, 2, 3, 4, c = 5, d = 6))
运行结果如图7.9所示。
图7.9 运行结果
在例7-6中,第1行中加了两个星号的变量args2会存放关键参数,其数据类型为字典。第9行调用函数时传入4个实参,第3个参数与第4个参数添加到元组args1中,此时args2是一个空字典。第10行调用函数时传入4个参数,第3个参数与第4个参数添加到字典args2中,此时args1是一个空元组。第11行调用函数时传入6个参数,第3个参数与第4个参数添加到元组args1中,第5个参数与第6个参数添加到字典args2中。
此外,通过*还可以进行相反的操作,如例7-7所示。
例7-7
1 def mySum(a, b, c):
2 return a + b + c
3 tuple1 = (1, 2, 3)
4 print(mySum(*tuple1))
5 list1 = [1, 2, 3]
6 print(mySum(*list1))
运行结果如图7.10所示。
图7.10 运行结果
在例7-7中,第4行在调用函数时在元组tuple1前加上星号,此时将tuple1中的3个元素分别传递给形参a、b、c,此外,还可以在列表前加星号,如第6行。
另外,通过**可以将字典转换为关键参数,如例7-8所示。
例7-8
1 def mySum(a, b, c):
2 return a + b + c
3 dict1 = {'a':1, 'b':2, 'c':3}
4 print(mySum(**dict1))
运行结果如图7.11所示。
图7.11 运行结果
在例7-8中,第4行在调用函数时在字典dict1前加上两个星号,此时将dict1中的3个键值对分别转换为关键参数。
此外,需注意上述两种方式的传递顺序,如例7-9所示。
例7-9
1 def mySum(a, b, c):
2 return a + b + c
3 print(mySum(*(1,),**{'b':2, 'c':3}))
4 # print(mySum(**{'b':2, 'c':3}, *(1,))) 错误写法
运行结果如图7.12所示。
图7.12 运行结果
在例7-9中,第3行调用mySum()函数时,第一个参数在元组前加*,第二个参数在字典前加**,此时形参中a、b、c的值分别为1、2、3。第4行交换参数的位置,则会发生错误。
7.3.5 传递不可变与可变对象
在Python中,数字、字符串与元组是不可变类型,而列表、字典是可变类型,两者区别如下:
• 不可变类型:该类型的对象所代表的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
• 可变类型:该类型的对象所代表的值可以被改变。变量改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。
接下来演示调用函数时传递不可变与不可变对象,如例7-10所示。
例7-10
1 def test1(alist):
2 alist.append(5)
3 print(alist)
4 def test2(astr):
5 astr += '.com'
6 print(astr)
7 list1 = [1, 2, 3, 4]
8 str1 = 'codingke'
9 test1(list1) # 可变对象
10 test2(str1) # 不可变对象
11 print(list1, str1)
运行结果如图7.13所示。
图7.13 运行结果
在例7-10中,第9行调用test1()函数,实参为可变对象,第10行调用test2()函数,实参为不可变对象。从程序运行结果可发现,可变对象可以被修改,但不可变对象不能被修改。
7.%2 函数的返回值
函数可以通过return语句返回值,函数调用时的参数传递实现了从函数外部向函数内部输入数据,而函数的返回实现了函数向外部输出数据。
此处需注意,如果函数定义时省略return语句或者只有return而没有返回值,则Python将认为该函数以return None结束,None代表没有值,如例7-11所示。
例7-11
1 def output():
2 print('拼搏到无能为力,坚持到感动自己!')
3 print(output())
运行结果如图7.14所示。
图7.14 运行结果
在例7-11中,第3行通过print()函数打印output()函数的返回值,此时输出None。
return语句可以放置在函数中任何位置,当执行到第一个return语句时,程序返回到调用程序处接着执行,此时不会执行该函数中return语句后的代码,如例7-12所示。
例7-12
1 def myMax(a, b):
2 if a > b:
3 return a
4 else:
5 return b
6 print(a, b)
7 print(max(2, 5))
运行结果如图7.15所示。
图7.15 运行结果
在例7-12中,第7行调用函数时,将实参2、5分别传递给形参a、b,程序跳转到第1行处执行,由于a小于b,因此执行else后的return语句,此时函数调用结束,不会执行第6行语句,最终输出函数的返回值5。
当函数具有多个返回值的时候,如果只用一个变量来接收返回值,函数返回的多个值实际上构成了一个元组,如例7-13所示。
例7-13
1 def calculate(a, b):
2 return a + b, a - b, a * b, a / b
3 x = calculate(6, 3)
4 print(x)
5 a1, b1, c1, d1 = calculate(6, 3)
6 print(a1, b1, c1, d1)
运行结果如图7.16所示。
图7.16 运行结果
在例7-13中,第2行函数通过return语句返回4个值,第3行通过一个变量接受函数calculate()的返回值,第4行打印该变量,输出一个元组。第5行利用多变量同时赋值语句来接收多个返回值。
7.%2 变量的作用域
变量起作用的代码范围称为变量的作用域,一个变量在函数外部定义和在函数内部定义,其作用域是不同的。不同作用域内变量名可以相同,互不影响。
7.5.1 局部变量
在函数内部定义的普通变量只在函数内部起作用,称为局部变量。当函数执行结束后,局部变量自动删除,不可以再使用,如例7-14所示。
例7-14
1 def fun1():
2 x = 1 # 局部变量
3 print('fun1()函数中的x为%d'%x)
4 def fun2():
5 x = 2 # 局部变量
6 print('fun2()函数中的x为%d'%x)
7 fun1()
8 fun2()
9 # print(x)
运行结果如图7.17所示。
图7.17 运行结果
在例7-14中,第2行与第5行定义的x虽然同名,但属于不同的作用域,两者互不影响,第9行在函数外访问局部变量,程序运行时会报x未定义的错误。
7.5.2 全局变量
如果需要在函数内部给一个定义在函数外的变量赋值,那么这个变量就不能是局部的,其作用域必须为全局的,能够同时作用于函数内外,称为全局变量,此时可以在函数内通过global关键字来声明,如例7-15所示。
例7-15
1 x = 2 # 全局变量
2 def fun():
3 global x # 使用global关键字声明
4 x += 1
5 print('x = %d'%x)
6 fun()
7 print(x)
运行结果如图7.18所示。
图7.18 运行结果
在例7-15中,变量x已在函数外定义,在函数fun()内修改外部变量x,则必须在函数内用global声明这个变量,将其声明为全局变量。
此处需注意,如果不使用global声明,则在函数中访问的是局部变量,如例7-16所示。
例7-16
1 x = 2 # 全局变量
2 def fun():
3 x = 3 # 局部变量
4 print('x = %d'%x)
5 fun()
6 print(x)
运行结果如图7.19所示。
图7.19 运行结果
在例7-16中,第1行x为全局变量,第3行x为局部变量,只在fun()函数内有效,第4行打印局部变量x的值3,第6行打印全局变量x的值2。
此外,使用内置函数globals()和locals()可以查看局部变量和全局变量,如例7-17所示。
例7-17
1 x = 1 # 全局变量
2 def fun():
3 x, y = 2, 3 # 局部变量
4 print(x, y)
5 global z # 全局变量
6 z = 1
7 print(locals())
8 fun()
9 print(x)
10 print(globals())
运行结果如图7.20所示。
图7.20 运行结果
在例7-16中,函数globals()和locals()返回一个字典,通过打印字典中的元素,可以查看全局变量与局部变量。另外,在函数内部可以通过global关键字直接将一个变量声明为全局变量,即使在函数外没有定义,则该函数执行后,这个变量将成为全局变量,如本例中的变量z。
7.%2 函数的嵌套调用
Python语言允许在函数定义中出现函数调用,从而形成函数的嵌套调用,如例7-17所示。
例7-18
1 def fun1():
2 print('fun1()函数开始')
3 print('fun1()函数结束')
4 def fun2():
5 print('fun2()函数开始')
6 fun1()
7 print('fun2()函数结束')
8 fun2()
运行结果如图7.21所示。
图7.21 运行结果
在例7-16中,第5行在fun2()函数中调用fun1()函数,程序执行时会跳转到fun1()函数处去执行,执行完fun1()后,接着执行fun2()函数中剩余的代码,如图7.21所示。
图7.22 函数的嵌套调用执行过程
7.%2 函数的递归调用
在函数的嵌套调用中,一个函数除了可以调用其他函数外,还可以调用自身,这就是函数的递归调用,递归必须要有结束条件,否则会无限地递归。
接下来演示函数的递归调用,如例7-19所示。
例7-19
1 def f(n):
2 '''
3 计算阶乘公式:
4 0! = 1
5 n! = n * (n -1)!, n > 0
6 转化为递归函数:
7 f(0) = 1
8 f(n) = n * f(n - 1), n > 0
9 '''
10 if n == 0:
11 return 1
12 return n * f(n - 1)
13 print('4! = %d'%f(4))
运行结果如图7.23所示。
图7.23 运行结果
在例7-19中,第10行到第12行定义f()函数用于计算阶乘,当n == 0时,程序立即返回结果,这种简单情况称为结束条件,如果没有结束条件,就会出现无限递归。当n > 0时,就将这个原始问题分解成计算(n – 1)阶乘的子问题,持续分解,直到问题达到结束条件为止,就将结果返回给调用者,然后调用者进行计算并将结果返回给它自己的调用者,过程持续进行,直到结果返回原始调用者为止。原始问题就可以通过将f(n-1)的结果乘以n得到,这种调用过程就称为递归调用,如图7.24所示。
图7.24 函数的递归调用
7.%2 小案例
7.8.1 案例一
编写两个函数,一个函数接收一个整数num为参数,生成杨辉三角形前num行数据,另一个函数接受生成的杨辉三角形并按以下形式输出,如图7.25所示。
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1
1 8 28 56 70 56 28 8 1
图7.25 杨辉三角
在图7.25中,列出了杨辉三角形的前9行。每一层左右两端的数都是1并且左右对称;从第1层开始,每个不位于左右两端的数等于上一层左右两个数相加之和,具体实现如例7-20所示。
例7-20
1 # 生成杨辉三角形
2 def triangle(num):
3 triangle=[[1]]
4 for i in range(2, num + 1):
5 triangle.append([1]*i)
6 for j in range(1, i - 1):
7 triangle[i-1][j] = triangle[i-2][j] + triangle[i-2][j-1]
8 return triangle
9 # 打印杨辉三角形
10 def printtriangle(triangle):
11 width = len(str(triangle[-1][len(triangle[-1]) // 2])) + 3
12 column = len(triangle[-1]) * width
13 for sublist in triangle:
14 result = []
15 for contents in sublist:
16 # 控制间距
17 result.append('{0:^{1}}'.format(str(contents), width))
18 # 控制缩进
19 print('{0:^{1}}'.format(''.join(result), column))
20 num = int(input('请输入行数:'))
21 triangle = triangle(num)
22 printtriangle(triangle)
运行结果如图7.26所示。
图7.26 运行结果
在例7-20中,triangle()函数中使用列表来存储杨辉三角形中的数据,列表中的每个元素又是一个列表,其中存储杨辉三角形的某一行数据。printtriangle()函数中的format()函数用于格式化字符串,读者在此处只需简单了解即可。
7.8.2 案例二
汉诺塔问题是源于印度一个古老传说,大梵天创造世界时,在世界中心贝拿勒斯的圣庙中做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘(称为汉诺塔)。大梵天命令婆罗门把圆盘从一根柱子上按大小顺序重新摆放在另一根柱子上,并规定在三根柱子之间一次只能移动一个圆盘且小圆盘上不能放置大圆盘,如图7.27所示。
图7.27 汉诺塔
假设使用1、2、3、…、n标记n个大小互不相同的圆盘,A、B、C标记3个柱子,初始状态时所有圆盘都放在A柱子上,最终状态时所有的圆盘都放在C柱子上,柱子C作为中间缓冲。
当n = 1时,即只有1个圆盘,此时可以直接把这个圆盘从柱子A移动到柱子C,这也是递归的终止条件。
当n > 1时,依次解决以下三个子问题:
• 借助柱子C将前n-1个盘子从柱子A移到柱子B
• 将圆盘n从柱子A移到柱子C
• 借助柱子A将前n-1个圆盘从柱子B移到柱子C
汉诺塔问题的具体实现如例7-21所示。
例7-21
1 def hanoi(n, a, b, c):
2 if n == 1:
3 # 当仅有1个圆盘,直接将圆盘从柱子a移动到柱子c上
4 print(n, a, '->',c)
5 else:
6 # 将n-1个圆盘从柱子a移动到柱子b上
7 hanoi(n-1, a, c, b)
8 # 将最大的圆盘从柱子a移动到柱子c上
9 print(n, a, '->',c)
10 # 将n-1个圆盘从柱子b移动到柱子c上
11 hanoi(n-1, b, a, c)
12 n = int(input('请输入圆盘数:'))
13 hanoi(n, 'A', 'B', 'C')
运行结果如图7.28所示。
图7.28 运行结果
在例7-21中,使用函数的递归调用解决汉诺塔问题,使用递归需抓住两个关键点,一是递归的结束条件,二是递归的规律。
7.%2 本章小结
本章主要介绍了Python中函数的基本知识,包括函数的定义、函数的参数、函数的返回值、函数的嵌套调用与递归调用。通过本章的学习,读者应熟练掌握如何自定义函数、如何设置函数的参数,另外,在实际编写程序时,应尽量使用函数来简化一些代码,提高代码的可读性、可重用性及可维护性。
7.%2 习题
1.填空题
(1) 关键字表示定义一个函数。
(2) 通过 语句可以返回函数值并退出函数。
(3) 在函数内部定义的变量为 变量。
(4) 省略return语句的函数将返回 。
(5) 在函数内部修改全局变量,需要使用 关键字声明。
2.选择题
(1) 若想查看一个函数的文档字符串,则可以通过( )属性。
A.__doc__ B.__name__
C.__func__ D.__str__
(2) 若只用一个变量来接收函数返回的多个值,则这个变量类型为( )。
A.列表 B.元组
C.字典 D.集合
(3) 下列属于可变类型的是( )。
A.数字 B.字符串
C.列表 D.元组
(4) 在函数定义时某个形参有值,则称这个参数为( )。
A.位置参数 B.位置参数
C.关键参数 D.默认参数
(5) ( )函数可以得到程序中所有的全局变量。
A.globals() B.locals()
C.global() D.local()
3.思考题
(1) 简述局部变量与全局变量的区别。
(2) 简述位置参数与关键参数的区别。
4.编程题
编写函数,计算形式如x + xx + xxx + xxxx + ... + xxx...xxx的表达式的值(其中x为小于10的自然数)。
- 点赞
- 收藏
- 关注作者
评论(0)