从前面的章节中,你已经熟悉了print、input和len函数。Python提供了这样一些内建函数,但你也可以编写自己的函数。“函数”就像一个程序内的小程序。
为了更好地理解函数的工作原理,让我们来创建一个函数。在文件编辑器中输入下面的程序,保存为helloFunc.py:
❶ def hello:❷ print('Howdy!') print('Howdy!!!') print('Hello there.')❸ hello hello hello
第一行是def语句❶,它定义了一个名为hello的函数。def语句之后的代码块是函数体❷。这段代码在函数调用时执行,而不是在函数第一次定义时执行。
函数之后的hello语句行是函数调用❸。在代码中,函数调用就是函数名后跟上括号,也许在括号之间有一些参数。如果程序执行遇到这些调用,就会跳到函数的第一行,开始执行那里的代码。如果执行到达函数的末尾,就回到调用函数的那行,继续像以前一样向下执行代码。
因为这个程序调用了3次hello函数,所以函数中的代码就执行了3次。在运行这个程序时,输出看起来像这样:
Howdy!Howdy!!!Hello there.Howdy!Howdy!!!Hello there.Howdy!Howdy!!!Hello there.
函数的一个主要目的就是将需要多次执行的代码放在一起。如果没有函数定义,你可能每次都需要复制粘贴这些代码,程序看起来可能会像这样:
print('Howdy!')print('Howdy!!!')print('Hello there.')print('Howdy!')print('Howdy!!!')print('Hello there.')print('Howdy!')print('Howdy!!!')print('Hello there.')
一般来说,我们总是希望避免复制代码,因为如果一旦决定要更新代码(比如说,发现了一个缺陷要修复),就必须记住要修改所有复制的代码。
随着你获得更多的编程经验,常常会发现自己在为代码“消除重复”,即去除一些重复或复制的代码。消除重复能够使程序更短、更易读、更容易更新。
3.1 def语句和参数
如果调用print或len函数,你会传入一些值,放在括号之间,在这里称为“参数”。也可以自己定义接收参数的函数。在文件编辑器中输入这个例子,将它保存为helloFunc2.py:
❶ def hello(name):❷ print('Hello ' + name)❸ hello('Alice') hello('Bob')
如果运行这个程序,输出看起来像这样:
Hello AliceHello Bob
在这个程序的hello函数定义中,有一个名为name的变元❶。“变元”是一个变量,当函数被调用时,参数就存放在其中。hello函数第一次被调用时,使用的参数是'Alice'❸。程序执行进入该函数,变量name自动设为'Alice',就是被print语句打印出的内容❷。
关于变元有一件特殊的事情值得注意:保存在变元中的值,在函数返回后就丢失了。例如前面的程序,如果你在hello('Bob')之后添加print(name),程序会报NameError,因为没有名为name的变量。在函数调用hello('Bob')返回后,这个变量被销毁了,所以print(name)会引用一个不存在的变量name。
这类似于程序结束时,程序中的变量会丢弃。在本章稍后,当我们探讨函数的局部作用域时,我会进一步分析为什么会这样。
3.2 返回值和return语句
如果调用len函数,并向它传入像'Hello'这样的参数,函数调用就求值为整数5。这是传入的字符串的长度。一般来说,函数调用求值的结果,称为函数的“返回值”。
用def语句创建函数时,可以用return语句指定应该返回什么值。return语句包含以下部分:
- return关键字;
- 函数应该返回的值或表达式。
如果在return语句中使用了表达式,返回值就是该表达式求值的结果。例如,下面的程序定义了一个函数,它根据传入的数字参数,返回一个不同的字符串。在文件编辑器中输入以下代码,并保存为magic8Ball.py:
❶ import random❷ def getAnswer(answerNumber):❸ if answerNumber == 1: return 'It is certain' elif answerNumber == 2: return 'It is decidedly so' elif answerNumber == 3: return 'Yes' elif answerNumber == 4: return 'Reply hazy try again' elif answerNumber == 5: return 'Ask again later' elif answerNumber == 6: return 'Concentrate and ask again' elif answerNumber == 7: return 'My reply is no' elif answerNumber == 8: return 'Outlook not so good' elif answerNumber == 9: return 'Very doubtful'❹ r = random.randint(1, 9)❺ fortune = getAnswer(r)❻ print(fortune)
在这个程序开始时,Python首先导入random模块❶。然后getAnswer函数被定义❷。因为函数是被定义(而不是被调用),所以执行会跳过其中的代码。接下来,random.randint函数被调用,带两个参数,1和9❹。它求值为1和9之间的一个随机整数(包括1和9),这个值被存在一个名为r的变量中。
getAnswer函数被调用,以r作为参数❺。程序执行转移到getAnswer函数的顶部❸,r的值被保存到名为answerNumber的变元中。然后,根据answerNumber中的值,函数返回许多可能字符串中的一个。程序执行返回到程序底部的代码行,即原来调用getAnswer的地方❺。返回的字符串被赋给一个名为fortune变量,然后它又被传递给print调用❻,并被打印在屏幕上。
请注意,因为可以将返回值作为参数传递给另一个函数调用,所以你可以将下面3行代码
r = random.randint(1, 9)fortune = getAnswer(r)print(fortune)
缩写成一行等价的代码:
print(getAnswer(random.randint(1, 9)))
记住,表达式是值和操作符的组合。函数调用可以用在表达式中,因为它求值为它的返回值。
3.3 None值
在Python中有一个值称为None,它表示没有值。None是NoneType数据类型的唯一值(其他编程语言可能称这个值为null、nil或undefined)。就像布尔值True和False一样,None必须大写首字母N。
如果你希望变量中存储的东西不会与一个真正的值混淆,这个没有值的值就可能有用。有一个使用None的地方就是print的返回值。print函数在屏幕上显示文本,但它不需要返回任何值,这和len或input不同。但既然所有函数调用都需要求值为一个返回值,那么print就返回None。要看到这个效果,请在交互式环境中输入以下代码。
>>> spam = print('Hello!')Hello!>>> None == spamTrue
在幕后,对于所有没有return语句的函数定义,Python都会在末尾加上return None。这类似于while或for循环隐式地以continue语句结尾。而且,如果使用不带值的return语句(也就是只有return关键字本身),那么就返回None。
3.4 关键字参数和print
大多数参数是由它们在函数调用中的位置来识别的。例如,random.randint(1, 10)与random.randint(10, 1)不同。函数调用random.randint(1, 10)将返回1到10之间的一个随机整数,因为第一个参数是范围的下界,第二个参数是范围的上界(而random.randint(10, 1)会导致错误)。
但是,“关键字参数”是由函数调用时加在它们前面的关键字来识别的。关键字参数通常用于可选变元。例如,print函数有可选的变元end和sep,分别指定在参数末尾打印什么,以及在参数之间打印什么来隔开它们。
如果运行以下程序:
print('Hello')print('World')
输出将会是:
HelloWorld
这两个字符串出现在独立的两行中,因为print函数自动在传入的字符串末尾添加了换行符。但是,可以设置end关键字参数,将它变成另一个字符串。例如,如果程序像这样:
print('Hello', end='')print('World')
输出就会像这样:
HelloWorld
输出被打印在一行中,因为在'Hello'后面不再打印换行,而是打印了一个空字符串。如果需要禁用加到每一个print函数调用末尾的换行,这就很有用。
类似地,如果向print传入多个字符串值,该函数就会自动用一个空格分隔它们。在交互式环境中输入以下代码:
>>> print('cats', 'dogs', 'mice')cats dogs mice
但是你可以传入sep关键字参数,替换掉默认的分隔字符串。在交互式环境中输入以下代码:
>>> print('cats', 'dogs', 'mice', sep=',')cats,dogs,mice
也可以在你编写的函数中添加关键字参数,但必须先在接下来的两章中学习列表和字典数据类型。现在只要知道,某些函数有可选的关键字参数,在函数调用时可以指定。
3.5 局部和全局作用域
在被调用函数内赋值的变元和变量,处于该函数的“局部作用域”。在所有函数之外赋值的变量,属于“全局作用域”。处于局部作用域的变量,被称为“局部变量”。处于全局作用域的变量,被称为“全局变量”。一个变量必是其中一种,不能既是局部的又是全局的。
可以将“作用域”看成是变量的容器。当作用域被销毁时,所有保存在该作用域内的变量的值就被丢弃了。只有一个全局作用域,它是在程序开始时创建的。如果程序终止,全局作用域就被销毁,它的所有变量就被丢弃了。否则,下次你运行程序的时候,这些变量就会记住它们上次运行时的值。
一个函数被调用时,就创建了一个局部作用域。在这个函数内赋值的所有变量,存在于该局部作用域内。该函数返回时,这个局部作用域就被销毁了,这些变量就丢失了。下次调用这个函数,局部变量不会记得该函数上次被调用时它们保存的值。
作用域很重要,理由如下:
- 全局作用域中的代码不能使用任何局部变量;
- 但是,局部作用域可以访问全局变量;
- 一个函数的局部作用域中的代码,不能使用其他局部作用域中的变量。
- 如果在不同的作用域中,你可以用相同的名字命名不同的变量。也就是说,可以有一个名为spam的局部变量,和一个名为spam的全局变量。
Python有不同的作用域,而不是让所有东西都成全局变量,这是有理由的。这样一来,当特定函数调用中的代码修改变量时,该函数与程序其他部分的交互,只能通过它的参数和返回值。这缩小了可能导致缺陷的代码作用域。如果程序只包含全局变量,又有一个变量赋值错误的缺陷,那就很难追踪这个赋值错误发生的位置。它可能在程序的任何地方赋值,而你的程序可能有几百到几千行!但如果缺陷是因为局部变量错误赋值,你就会知道,只有那一个函数中的代码可能产生赋值错误。
虽然在小程序中使用全局变量没有太大问题,但当程序变得越来越大时,依赖全局变量就是一个坏习惯。
3.5.1 局部变量不能在全局作用域内使用
考虑下面的程序,它在运行时会产生错误:
def spam: eggs = 31337spamprint(eggs)
如果运行这个程序,输出将是:
Traceback (most recent call last): File "C:/test3784.py", line 4, in <module> print(eggs)NameError: name 'eggs' is not defined
发生错误是因为,eggs变量只属于spam调用所创建的局部作用域。在程序执行从spam返回后,该局部作用域就被销毁了,不再有名为eggs的变量。所以当程序试图执行print(eggs),Python就报错,说eggs没有定义。你想想看,这是有意义的。当程序执行在全局作用域中时,不存在局部作用域,所以不会有任何局部变量。这就是为什么只有全局变量能用于全局作用域。
3.5.2 局部作用域不能使用其他局部作用域内的变量
一个函数被调用时,就创建了一个新的局部作用域,这包括一个函数被另一个函数调用时的情况。请看以下代码:
def spam:❶ eggs = 99❷ bacon❸ print(eggs) def bacon: ham = 101❹ eggs = 0❺ spam
在程序开始运行时,spam函数被调用❺,创建了一个局部作用域。局部变量eggs❶被赋值为99。然后bacon函数被调用❷,创建了第二个局部作用域。多个局部作用域能同时存在。在这个新的局部作用域中,局部变量ham被赋值为101。局部变量eggs(与spam的局部作用域中的那个变量不同)也被创建❹,并赋值为0。
当bacon返回时,这次调用的局部作用域被销毁。程序执行在spam函数中继续,打印出eggs的值❸。因为spam调用的局部作用域仍然存在,eggs变量被赋值为99。这就是程序的打印输出。
要点在于,一个函数中的局部变量完全与其他函数中的局部变量分隔开来。
3.5.3 全局变量可以在局部作用域中读取
请看以下程序:
def spam: print(eggs)eggs = 42spamprint(eggs)
因为在spam函数中,没有变元名为eggs,也没有代码为eggs赋值,所以当spam中使用eggs时,Python认为它是对全局变量eggs的引用。这就是前面的程序运行时打印出42的原因。
3.5.4 名称相同的局部变量和全局变量
要想生活简单,就要避免局部变量与全局变量或其他局部变量同名。但在技术上,在Python中让局部变量和全局变量同名是完全合法的。为了看看实际发生的情况,请在文件编辑器中输入以下代码,并保存为sameName.py:
def spam:❶ eggs = 'spam local' print(eggs) # prints 'spam local' def bacon:❷ eggs = 'bacon local' print(eggs) # prints 'bacon local' spam print(eggs) # prints 'bacon local'❸ eggs = 'global' bacon print(eggs) # prints 'global'
运行该程序,输出如下:
bacon localspam localbacon localglobal
在这个程序中,实际上有3个不同的变量,但令人迷惑的是,它们都名为eggs。这些变量是:
❶名为eggs的变量,存在于spam被调用时的局部作用域;
❷名为eggs的变量,存在于bacon被调用时的局部作用域;
❸名为eggs的变量,存在于全局作用域。
因为这3个独立的变量都有相同的名字,追踪某一个时刻使用的是哪个变量,可能比较麻烦。这就是应该避免在不同作用域内使用相同变量名的原因。
3.6 global语句
如果需要在一个函数内修改全局变量,就使用global语句。如果在函数的顶部有global eggs这样的代码,它就告诉Python,“在这个函数中,eggs指的是全局变量,所以不要用这个名字创建一个局部变量。”例如,在文件编辑器中输入以下代码,并保存为sameName2.py:
def spam:❶ global eggs❷ eggs = 'spam' eggs = 'global' spam print(eggs)
运行该程序,最后的print调用将输出:
spam
因为eggs在spam的顶部被声明为global❶,所以当eggs被赋值为'spam'时❷,赋值发生在全局作用域的spam上。没有创建局部spam变量。
有4条法则,来区分一个变量是处于局部作用域还是全局作用域:
1.如果变量在全局作用域中使用(即在所有函数之外),它就总是全局变量。
2.如果在一个函数中,有针对该变量的global语句,它就是全局变量。
3.否则,如果该变量用于函数中的赋值语句,它就是局部变量。
4.但是,如果该变量没有用在赋值语句中,它就是全局变量。
为了更好地理解这些法则,这里有一个例子程序。在文件编辑器中输入以下代码,并保存为sameName3.py:
def spam:❶ global eggs eggs = 'spam' # this is the global def bacon:❷ eggs = 'bacon' # this is a local def ham:❸ print(eggs) # this is the global eggs = 42 # this is the global spam print(eggs)
在spam函数中,eggs是全局eggs变量,因为在函数的开始处,有针对eggs变量的global语句❶。在bacon中,eggs是局部变量,因为在该函数中有针对它的赋值语句❷。在ham中❸,eggs是全局变量,因为在这个函数中,既没有赋值语句,也没有针对它的global语句。如果运行sameName3.py,输出将是:
spam
在一个函数中,一个变量要么总是全局变量,要么总是局部变量。函数中的代码没有办法先使用名为eggs的局部变量,稍后又在同一个函数中使用全局eggs变量。
如果想在一个函数中修改全局变量中存储的值,就必须对该变量使用global语句。
在一个函数中,如果试图在局部变量赋值之前就使用它,像下面的程序这样,Python就会报错。为了看到效果,请在文件编辑器中输入以下代码,并保存为sameName4.py:
def spam: print(eggs) # ERROR!❶ eggs = 'spam local'❷ eggs = 'global' spam
运行前面的程序,会产生出错信息。
Traceback (most recent call last): File "C:/test3784.py", line 6, in <module> spam File "C:/test3784.py", line 2, in spam print(eggs) # ERROR!UnboundLocalError: local variable 'eggs' referenced before assignment
发生这个错误是因为,Python看到spam函数中有针对eggs的赋值语句❶,因此认为eggs变量是局部变量。但是因为print(eggs)的执行在eggs赋值之前,局部变量eggs并不存在。Python不会退回到使用全局eggs变量❷。
3.7 异常处理
到目前为止,在Python程序中遇到错误,或“异常”,意味着整个程序崩溃。你不希望这发生在真实世界的程序中。相反,你希望程序能检测错误,处理它们,然后继续运行。
例如,考虑下面的程序,它有一个“除数为零”的错误。打开一个新的文件编辑器窗口,输入以下代码,并保存为zeroDivide.py:
def spam(pideBy): return 42 / pideByprint(spam(2))print(spam(12))print(spam(0))print(spam(1))
我们已经定义了名为spam的函数,给了它一个变元,然后打印出该函数带各种参数的值,看看会发生什么情况。下面是运行前面代码的输出:
21.03.5Traceback (most recent call last): File "C:/zeroDivide.py", line 6, in <module> print(spam(0)) File "C:/zeroDivide.py", line 2, in spam return 42 / pideByZeroDivisionError: pision by zero
当试图用一个数除以零时,就会发生ZeroDivisionError。根据错误信息中给出的行号,我们知道spam中的return语句导致了一个错误。
函数作为“黑盒”
通常,对于一个函数,你要知道的就是它的输入值(变元)和输出值。你并非总是需要加重自己的负担,弄清楚函数的代码实际是怎样工作的。如果以这种高层的方式来思考函数,通常大家会说,你将该函数看成是一个黑盒。
这个思想是现代编程的基础。本书后面的章节将向你展示一些模块,其中的函数是由其他人编写的。尽管你在好奇的时候也可以看一看源代码,但为了能使用它们,你并不需要知道它们是如何工作的。而且,因为鼓励在编写函数时不使用全局变量,你通常也不必担心函数的代码会与程序的其他部分发生交叉影响。
错误可以由try和except语句来处理。那些可能出错的语句被放在try子句中。如果错误发生,程序执行就转到接下来的except子句开始处。
可以将前面除数为零的代码放在一个try子句中,让except子句包含代码,来处理该错误发生时应该做的事。
def spam(pideBy): try:return 42 / pideBy except ZeroDivisionError:print('Error: Invalid argument.')print(spam(2))print(spam(12))print(spam(0))print(spam(1))
如果在try子句中的代码导致一个错误,程序执行就立即转到except子句的代码。在运行那些代码之后,执行照常继续。前面程序的输出如下:
21.03.5Error: Invalid argument.None42.0
请注意,在函数调用中的try语句块中,发生的所有错误都会被捕捉。请考虑以下程序,它的做法不一样,将spam调用放在语句块中:
def spam(pideBy): return 42 / pideBytry: print(spam(2)) print(spam(12)) print(spam(0)) print(spam(1))except ZeroDivisionError: print('Error: Invalid argument.')
该程序运行时,输出如下:
21.03.5Error: Invalid argument.
print(spam(1))从未被执行是因为,一旦执行跳到except子句的代码,就不会回到try子句。它会继续照常向下执行。
3.8 一个小程序:猜数字
到目前为止,前面展示的小例子适合于介绍基本概念。现在让我们看一看,如何将所学的知识综合起来,编写一个更完整的程序。在本节中,我将展示一个简单的猜数字游戏。在运行这个程序时,输出看起来像这样:
I am thinking of a number between 1 and 20.Take a guess.10Your guess is too low.Take a guess.15Your guess is too low.Take a guess.17Your guess is too high.Take a guess.16Good job! You guessed my number in 4 guesses!
在文件编辑器中输入以下代码,并保存为guessTheNumber.py:
# This is a guess the number game.import randomsecretNumber = random.randint(1, 20)print('I am thinking of a number between 1 and 20.')# Ask the player to guess 6 times.for guessesTaken in range(1, 7): print('Take a guess.') guess = int(input) if guess < secretNumber:print('Your guess is too low.') elif guess > secretNumber:print('Your guess is too high.') else:break # This condition is the correct guess!if guess == secretNumber: print('Good job! You guessed my number in ' + str(guessesTaken) + ' guesses!')else: print('Nope. The number I was thinking of was ' + str(secretNumber))
让我们逐行来看看代码,从头开始。
# This is a guess the number game.import randomsecretNumber = random.randint(1, 20)
首先,代码顶部的一行注释解释了这个程序做什么。然后,程序导入了模块random,以便能用random.randint函数生成一个数字,让用户来猜。返回值是一个1到20之间的随机整数,保存在变量secretNumber中。
print('I am thinking of a number between 1 and 20.')# Ask the player to guess 6 times.for guessesTaken in range(1, 7): print('Take a guess.') guess = int(input)
程序告诉玩家,它有了一个秘密数字,并且给玩家6次猜测机会。在for循环中,代码让玩家输入一次猜测,并检查该猜测。该循环最多迭代6次。循环中发生的第一件事情,是让玩家输入一个猜测数字。因为input返回一个字符串,所以它的返回值被直接传递给int,它将字符串转变成整数。这保存在名为guess的变量中。
if guess < secretNumber:print('Your guess is too low.') elif guess > secretNumber:print('Your guess is too high.')
这几行代码检查该猜测是小于还是大于那个秘密数字。不论哪种情况,都在屏幕上打印提示。
else:break # This condition is the correct guess!
如果该猜测既不大于也不小于秘密数字,那么它就一定等于秘密数字,这时你希望程序执行跳出for循环。
if guess == secretNumber: print('Good job! You guessed my number in ' + str(guessesTaken) + ' guesses!')else: print('Nope. The number I was thinking of was ' + str(secretNumber))
在for循环后,前面的if...else语句检查玩家是否正确地猜到了该数字,并将相应的信息打印在屏幕上。不论哪种情况,程序都会打印一个包含整数值的变量(guessesTaken和secretNumber)。因为必须将这些整数值连接成字符串,所以它将这些变量传递给str函数,该函数返回这些整数值的字符串形式。现在这些字符串可以用+操作符连接起来,最后传递给print函数调用。
3.9 小结
函数是将代码逻辑分组的主要方式。因为函数中的变量存在于它们自己的局部作用域内,所以一个函数中的代码不能直接影响其他函数中变量的值。这限制了哪些代码才能改变变量的值,对于调试代码是很有帮助的。
函数是很好的工具,帮助你组织代码。你可以认为他们是黑盒。它们以参数的形式接收输入,以返回值的形式产生输出。它们内部的代码不会影响其他函数中的变量。
在前面几章中,一个错误就可能导致程序崩溃。在本章中,你学习了try和except语句,它们在检测到错误时会运行代码。这让程序在面对常见错误时更有灵活性。
3.10 习题
1.为什么在程序中加入函数会有好处?
2.函数中的代码何时执行:在函数被定义时,还是在函数被调用时?
3.什么语句创建一个函数?
4.一个函数和一次函数调用有什么区别?
5.Python程序中有多少全局作用域?有多少局部作用域?
6.当函数调用返回时,局部作用域中的变量发生了什么?
7.什么是返回值?返回值可以作为表达式的一部分吗?
8.如果函数没有返回语句,对它调用的返回值是什么?
9.如何强制函数中的一个变量指的是全局变量?
10.None的数据类型是什么?
11.import areallyourpetsnamederic语句做了什么?
12.如果在名为spam的模块中,有一个名为bacon的函数,在引入spam后,如何调用它?
13.如何防止程序在遇到错误时崩溃?
14.try子句中发生了什么?except子句中发生了什么?
3.11 实践项目
作为实践,请编写程序完成下列任务。
3.11.1 Collatz序列
编写一个名为collatz的函数,它有一个名为number的参数。如果参数是偶数,那么collatz就打印出number // 2,并返回该值。如果number是奇数,collatz就打印并返回3 * number + 1。
然后编写一个程序,让用户输入一个整数,并不断对这个数调用collatz,直到函数返回值1(令人惊奇的是,这个序列对于任何整数都有效,利用这个序列,你迟早会得到1!既使数学家也不能确定为什么。你的程序在研究所谓的“Collatz序列”,它有时候被称为“最简单的、不可能的数学问题”)。
记得将input的返回值用int函数转成一个整数,否则它会是一个字符串。
提示
如果number % 2 == 0,整数number就是偶数,如果number % 2 == 1,它就是奇数。
这个程序的输出看起来应该像这样:
Enter number:3105168421
3.11.2 输入验证
在前面的项目中添加try和except语句,检测用户是否输入了一个非整数的字符串。正常情况下,int函数在传入一个非整数字符串时,会产生ValueError错误,比如int('puppy')。在except子句中,向用户输出一条信息,告诉他们必须输入一个整数。