首页 » Python编程快速上手 » Python编程快速上手全文在线阅读

《Python编程快速上手》第3章 函数

关灯直达底部

从前面的章节中,你已经熟悉了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子句中,向用户输出一条信息,告诉他们必须输入一个整数。