学习目标
- 定义带形参的函数。
- 用实参来调用函数。
- 区分带返回值和不带返回值的函数。
- 使用位置参数和关键字参数调用函数。
- 通过传参数的引用值来传递参数。
- 开发可重用代码来模块化程序,使程序易读、易调试和易维护。
- 为可重用函数创建模块。
- 决定变量的作用域。
- 定义带默认参数的函数。
- 定义一个返回多个值的函数。
- 在软件开发中使用函数抽象的概念。
- 用逐步求精的方法设计和实现函数。
- 使用可重用代码简化程序。
一、引言
函数是为实现一个操作而集合在一起的语句集。在前面的章节中,我们已经学习了像eval( "numricString")和random.randint(a,b)这样的函数。例如:当调用random.randint(a,b)函数时,系统会执行函数里的这些语句,并返回结果。在本章里,我们将学习如何定义和使用函数以及如何应用函数抽象去解决复杂的问题。
二、定义一个函数
关键点:函数定义包括函数名称、形参以及函数体。
定义函数的语法如下所示:
def functionName(list of parameters)
# Function body
我们来看一个用来找出两个数中哪个比较大的函数。这个函数被命名为max,它有两个参数:num1和num2,函数返回这两个数中较大的那个。图6-1解释了这个函数的组件。
函数包括函数头和函数体。函数头以一个def关键字开始,后面紧接着函数名以及形参并以冒号结束。
函数头中的参数被称为形式参数或简称为形参。参数就像一个占位符:当调用函数时,就将一个值传递给参数。这个值被称为实际参数或实参。参数是可选的;也就是说,函数可以不包含参数。例如:函数random.random()就不包含参数。
某些函数有返回值,而一些其他的函数会完成要求的操作而不返回值。如果函数有返回值,则被称为带返回值的函数。
函数体包含一个定义函数做什么的语句集。例如:函数max的函数体使用if语句来判断哪个数更大然后返回这个数的值。一个带返回值的函数需要一个使用关键字return的返回语句来返回一个值。执行return语句意味着函数的终止。
三、调用一个函数
关键点:调用一个函数来执行函数中的代码。
在函数的定义中,定义函数要做什么。为了使用函数,必须调用它。调用函数的程序被称为调用者。根据函数是否有返回值,调用函数有两种方式。
如果函数带有返回值,对这种函数的调用通常当作一个值处理。例如:
Targer = max(3,4)
调用max(3,4)并将函数的结果赋值给变量larger。
另外一个把它当作值处理的调用例子是
print(max(3,4))
这条语句输出调用函数max(3,4)后的返回值。
如果函数没有返回值,那么对函数的调用必须是一条语句。例如:函数print没有返回值。下面的调用就是一条语句:
print("Programming is fun! ")
注意:带返回值的函数也可以当作语句被调用。在这种情况下,函数返回值就会被忽略掉。这是很少见的,但如果函数调用者对返回值不感兴趣,这样也是允许的。
当程序调用一个函数时,程序控制权就会转移到被调用的函数上。当执行完函数的返回语句或执行到函数结束时,被调用函数就会将程序控制权交还给调用者。
3.1、调用栈
每次调用一个函数时,系统就会创建一个为函数存储它的参数和变量的激活记录,然后将这个激活记录放在一个被称为堆栈的内存区域。调用栈又被称为执行堆栈、运行堆栈或机器堆栈,经常被简称为堆栈。当一个函数调用另一个函数时,调用者的激活记录保持不变,然后为新函数的调用创建一个新的激活记录。当一个函数结束了它的工作之后就将程序控制权转移给它的调用者,同时从堆栈中删除它的激活记录。
堆栈采用后进先出的方式存储激活记录。最后被调用的函数的激活记录将最先从栈里删除。假设函数m1调用m2,然后调用m3。运行时系统将m1的激活记录压人栈中,然后是将m2的激活记录压入栈中,最后是将m3的激活记录压入栈中。在完成m3的工作之后,它的激活记录被弹出栈。在完成m2的工作之后,它的激活记录被弹出栈,在完成m1的工作之后,它的激活记录被弹出栈。
四、带返回值或不带返回值的函数
关键点:函数不是一定有返回值
注意:实际上,不管你是否使用return,所有Python的函数都将返回一个值。如果某个函数没有返回值,默认情况下,它返回一个特殊值None。因此,无返回值函数不会返回值,它也被称作None函数。None 函数可以赋值给一个变量来表明这个变量不指向任何对象。例如,如果你运行下面程序:
def sum(number1, number2) :
total = number1 + number2
print(sum(1,2))
你会发现输出为None,因为sum函数没有return语句。默认情况下,它返回None。
注意:None函数不是一定需要return语句,但它能用于终止函数并将控制权返回函数
调用者。它的语法是
return
或
return None
这种用法很少使用,但对于改变函数的正常流程是很有用的。
五、位置参数和关键字参数
关键点:函数实参是作为位置参数和关键字参数被传递。
函数的作用就在于它处理参数的能力。当调用函数时,需要将实参传递给形参。实参有两种类型:位置参数和关键字参数。使用位置参数要求参数按它们在函数头的顺序进行传递。例如,下面的函数输出消息n次:
def nPrintln(message, n):
for i in range(n) :
print (message)
你可以使用nPrintln('a',3)输出a三次。nPrintln('a',3) 语句将a传递给message,将3传递给n,然后输出a三次。但是,语句nPrintln(3,'a') 则有不同的含义。它将3传递给message,将a传递给n。当我们调用这样的函数时,就被称为使用位置参数。实参必须和函数头中定义的形参在顺序、个数和类型上匹配。
你也可以使用关键字参数调用函数,通过name=value的形式传递每个参数。例如:nPlintln(n=5,message="good")将5传递给n,将"good"传递给message。使用关键字参数
时,参数可以以任何顺序出现。
位置参数和关键字参数很可能被混在一起,但位置参数不能出现在任何关键字参数之后。假设函数头是:
def f(p1, p2,p3):
你可以通过使用
f(30,p2=4,p3=10)
调用它。
但是,你通过使用
f(30,p2 = 4,10)
调用它则会出错。因为位置参数10出现在关键字参数p2=4之后。
六、通过传引用来传递参数
关键点:当你调用带参数的函数时,每个实参的引用值被传递给函数的形参。
因为Python中的所有数据都是对象,所以对象的变量通常都是指向对象的引用。当你调用一个带参数的函数时,每个实参的引用值就被传递给形参。这在程序设计术语中被称为通过值传递。简单地说,调用函数时,实参的值就被传递给形参。这个值通常就是对象的引用值。
如果实参是一个数字或者是一个字符串,那么不管函数中的形参有没有变化,这个实参是不受影响的。
def main():
x=1
print("Before the cal1,X is",x)
increment(x)
print("'After the cal1, x is",x)
def increment(n):
n+=1
print("\tn inside the function is", n)
main() # Call the main function
如程序清单的输出,x(1)的值被传递给形参n来调用increment函数(第4行)。函数中参数n递增1,但是不论函数做什么x都不会改变。
这样的原因是因为数字和字符串被称为不可变对象。不可变对象的内容是不能被改变的。当你将一个新数字赋值给变量时,Python 就会为这个新数字创建新对象,然后将这个新对象的引用赋值给这个变量。
七、模块化代码
关键点:模块化可以使代码易于维护和调试,并且提高代码的重用性。
函数可以用来减少冗余的代码并提高代码的可重用性。函数也可以用来模块化代码并提高程序的质量。在Python中,你可以将函数的定义放在一个被称为模块的文件中,这种文件的后缀名是.py。之后这些模块可以被导入到程序中以便重复使用。这些模块文件应该和其他程序一起放在同一个地方。一个模块可以包含不止一个函数。一个模块的每个函数都有不同的名字。注意: turtle、random 和math是定义在Python库里的模块,这样,它们可以被导入到任何一个Python程序中。
如果你在一个模块里定义了两个同名函数,那会出现什么情况呢?在这种情况下不会出现语法错误,但后者的优先级高。
八、变量的作用域
关键点:变量的作用域是指该变量可以在程序中被引用的范围。
这节讨论在函数范围中变量的作用域。在函数内部定义的变量被称为局部变量。局部变量只能在函数内部被访问。局部变量的作用域从创建变量的地方开始,直到包含该变量的函数结束为止。
在Python中,你也可以使用全局变量。它们在所有的函数之外创建,可以被所有的函数访问。
注意:尽管允许使用全局变量,可以看到全局变量在其他程序中使用。但在一个函数中允许修改全局变量并不是一个好习惯。因为这样做可能使程序更易出错。但是,可以定义全局变量以便模块中的所有函数都能使用它。
九、默认参数
关键点: Python允许定义带默认参数值的函数。当函数被调用时无参数,那么这些默认值就会被传递给实参。
下面代码示例演示了如何定义带默认参数值的函数以及如何调用这样的函数。
def printArea(width = 1,height = 2):
area = width * height
print("width:",width, "\theight:", height, "\tarea:", area)
printArea() # Default arguments width = 1 and height = 2
printArea(4,2.5) # Positional arguments width = 4 and height = 2.5
printArea(height = 5,width = 3) # Keyword arguments width
printArea(width = 1.2) # Default height = 2
printArea(height = 6.2) # Default width = 1
第1行用参数width和height定义函数printArea。width的默认值是1,而height的默认值是2。第5行是在没有传递实参的情况下调用这个函数。所以,程序就将默认值1赋给width,将默认值2赋给height。第6行通过将4和6分别赋给width和height来调用这个函数。第7行通过将3和5分别赋值给width和height来调用这个函数。注意:我们也可以通过指定参数名来传递实参的值,如第8行和第9行所示。
注意:函数可以混用默认值参数和非默认值参数。这种情况下,非默认值参数必须定义在默认值参数之前。
注意:尽管许多语言支持在同一个模块里定 义两个同名的函数,但是Python 并不支持这个特点。通过默认参数你只可以定义函数一次,但可以通过许多不同的方式调用函数。这和在其他语言中定义同名的多个函数的效果一样。如果你在Python中定义了多个同名函数,那么后面的函数就会取代先前的函数。
十、返回多个值
关键点: Python的return语句可以返回多个值。
Python允许函数返回多个值。下面程序清单定义了一个输人两个数并以升序返回这两个数的函数。
def sort (number1,number2) :
if number1 < number2:
return number1, number2
else:
return number2, number1
n1,n2 = sort(3,2)
print("n1 is",n1)
print("n2 is",n2)
sort函数返回两个值。当它被调用时,你需要同时赋值传递这些返回值。
十一、函数抽象和逐步求精
关键点:函数抽象就是将函数的使用和函数的实现分开来实现的。
软件开发的关键就是应用抽象的概念。函数抽象将函数的使用从函数的实现分离出来。一个用户程序或简称为用户,可以在不知道函数是如何实现的情况下使用函数。函数的实现细节被封装在函数内,并对调用该函数的用户隐藏。这被称为信息隐藏或封装。如果决定改变函数的实现,只要不改变函数名,用户程序就不会受影响。函数的实现对用户而言是隐藏在一个黑匣子中,如图6-5所示。
我们已经在用户程序中使用过许多Python内置函数,并且知道如何在程序中编写代码来调用这些函数,但是,作为这些函数的使用者,我们并不需要知道它们是如何实现的。
函数抽象的概念可以被应用到开发程序的过程中。当编写一个大程序时,你可以使用分治策略,也称之为逐步求精,将大问题分解为子问题。这些子问题又被分为更小更容易管理的问题。
十二、总结
- 1.程序模块化和可重用性是软件工程的中心目标之一。函数可以实现这个目标。
- 2.函数头由关键字def开始,接下来是函数名和形式参数,最后以冒号结束。
- 3.形式参数是可选的;也就是说,函数可以不包含任何形式参数。
- 4.无返回值的函数被称为void或None函数。
- 5.一个return语句可以在void函数中用来终止函数并将程序控制权返回给函数的调用者。有时,这对保证函数控制流正常是非常有用的。
- 6.传给函数的参数必须和定义在函数头里的形参在数目、类型和顺序上保持一-致。
- 7.当程序调用一个函数时,程序的控制权就转移到被调用的函数。当执行到函数的return语句或执行到函数的最后一条语句时,被调用的函数就将控制权转给调用者。
- 8.带返回值函数也可以当作Python语句被调用。在这种情况下,函数的返回值被忽略。
- 9.函数参数可以当作位置参数或关键字参数传递。
- 10.当调用一个带形式参数的函数时,实参的值就被传给形参。这用程序设计术语讲就是值传递。
- 11.函数中创建的变量被称为局部变量。局部变量的作用域从它被创建的位置开始,直到函数返回为止都存在。变量必须在使用前被创建。
- 12.全局变量被定义在所有函数之外,而且它们可以被所有函数访问。
- 13. Python允许用默认参数值定义函数。当无参数调用函数时,默认值就被传给形参。
- 14. Python的return语句可以返回多个值。
- 15.函数抽象是通过将函数的使用和实现分开实现。一个用户可以在不知道函数是如何实现的情况下使用函数。函数的实现细节被封装在函数内,并对调用该函数的用户来说是隐藏的。这被称为信息隐藏或封装。
- 16.函数抽象将程序模块化为整齐、分层的形式。程序被写成简洁函数的集合,这样使程序更易于编写、调试、维护和修改。这种编写风格会提高函数的可重用性。
- 17.当实现一个大程序时,使用自顶向下或自底向上的编码方法。不要一次性编写整个程序。这个方法似乎要占用更多的编码时间(因为要反复地运行这个程序),但它实际上更省时间和更易于调试。