像计算机科学家一样思考python-第3章 函数

在程序设计中,函数是指用于进行某种计算的一系列语句的有名称的组合。定义一个函数时,需要指定函数的名称并写下一系列程序语句。之后,就可以使用名称来“调用”这个函数


3.1函数调用

一个函数调用的例子

>>> type(42)

<class 'int'>


这个函数的名称是type,括号中的表达式我们称之为函数的参数。这个函数调用的结果是求得参数的类型。


我们通常说函数“接收”参数,并“返回”结果。这个结果也称为返回值


3.2数学函数

Python有一个数学计算模块,提供了大多数常用的数学函数。模块是指包含一组相关的函数的文件。


要想使用模块中的函数,需要先使用import语句将它导入运行环境

>>> import math


这个语句将会创建一个名为math的模块对象。如果显示这个对象,可以看到它的一些信息:

>>> math

<module 'math' (built-in)>

 

模块对象包含了该模块中定义的函数和变量。若要访问其中一个函数,需要同时指定模块名称和函数名称,用一个句点(.)分隔。这个格式称为句点表示法(dot notation

>>> radians=0.7

>>> height=math.sin(radians)

>>> height

0.644217687237691

 

 

 

3.3组合

到现在为止,我们已经分别了解了程序的基本元素——变量、表达式和语句,但还没有接触如何将它们有机地组合起来。


程序设计语言最有用的特性之一就是可以将各种小的构建块(building block)组合起来。例如,函数的参数可以是任何类型的表达式,包括算术操作符:

>>> x = math.sin(degrees / 360.0 * 2 * math.pi)


甚至还包括函数调用:

>>> math.exp(math.log(x+1))


基本上在任何可以使用值的地方,都可以使用任意表达式,只有一个例外:赋值表达式的左边必须是变量名称,在左边放置任何其它的表达式都语法错误(后面还会看到这条规则的例外情况)


3.4添加新函数

我们可以自己添加新的函数。函数定义指定新函数的名称,并提供一系列程序语句,当函数被调用时,这些语句会顺序运行。


下面是一个例子:

>>> def print_lyrics():

... print("I'm a lumberjack,and I'm okay")

... print("I sleep all night and I Work all day.")


def是一个关键字,表示接下来是一个函数定义。这个函数的名称是print_lyrics。函数名称的书写规则与变量名称一:字母、数字、下划线是合法的,但第一个字符不能是数字。关键字不能作为函数名,而且我们应用尽量避免函数和变量同名。函数名后的空括号表示它不接收任何参数。


有所思:如果函数名与变量名一样会发生什么情况呢?

假如有一个变量a=1,后面又定一个函数也叫a,执行1print语句,这时候变量名a指向的是函数对象呢,还是int类型的1呢?我猜肯定指向的是函数a,因为python是按顺序执行的语句,如果同一个变量名被重复定义,则以最后一次定义的内容为准。

>>> a =1

>>> type(a)

<class 'int'>


>>> def a():

... print("函数a")

...

>>> type(a)

<class 'function'>


函数定义的第一行称为函数头(header),其它部分称为函数体(body)。函数头应该以冒号结束,函数体则应当整体缩进一级。依照惯例,缩进总是使用4个空格,函数体的代码语句行数不限。


本例中的print语句里的字符串使用双引号括起来。单引号和双引号的作用相同。大部分情况下,人们都使用单引号,只在本例中这样的特殊情况下才使用双引号。本例中的字符串里本身就存在单引号(这里的单引号作为缩略符号用)


代码中所有的引号(包括双引号和单引号)都必须是”直引号”,通常在键盘上Enter键附近。而”斜引号”,在Python中是非法的。


如果在交互模式里输入函数定义,则解释器会输出省略号(...)提示用户当前的定义还没有结束:

>>> def print_lyrics():

... print("I'm a lumberjack,and I'm okay")

... print("I sleep all night and I Work all day.")

... 

 

想要结束这个函数的定义,需要输入一个空行。


定义一个函数会创建一个函数对象,其类型是”function”

>>> print(print_lyrics)

<function print_lyrics at 0x7f25f96a8e18>

>>> type(print_lyrics)

<class 'function'>

 

调用新创建的函数的方式,与调用内置函数是一样的:

>>> print_lyrics()

I'm a lumberjack,and I'm okay

I sleep all night and I Work all day.


定义好一个函数之后,就可以在其它函数中调用它。例如,若想重复上面的歌词,我们可以写一个repeat_lyrics函数:

>>> def repeat_lyrics():

... print_lyrics()

... print_lyrics()

 

然后调用 repeat_lyrics:

>>> repeat_lyrics()

I'm a lumberjack,and I'm okay

I sleep all night and I Work all day.

I'm a lumberjack,and I'm okay

I sleep all night and I Work all day.

 

3.5 定义和使用

将前面一节的代码片段整合起来,整个流程就像下面这个样子:


def print_lyrics():
    print("I'm a lumberjack,and I'm okay")
    print("I sleep all night and I Work all day.")

def repeat_lyrics():
    print_lyrics()
    print_lyrics()

repeat_lyrics()  


这个程序包含两个函数定义: print_lyricsrepeat_lyrics。函数定义的执行方式和其他语句一样,不同的是执行后会创建函数对象。函数体里面的语句并不会立即运行,而是等到函数被调用时才执行。函数定义不会产生任何输出。


你可能已经猜到,必须先创建一个函数,才能运行它。换言之,函数定义必须在函数被调用之前先运行。


练习1

作为练习,将程序的最后一行移动到行首,于是函数调用会先于函数定义执行。运行程序并查看会有什么样的错误信息。



repeat_lyrics()

def print_lyrics():
    print("I'm a lumberjack,and I'm okay")
    print("I sleep all night and I Work all day.")

def repeat_lyrics():
    print_lyrics()
    print_lyrics()

运行程序报错如下:

NameError: name 'repeat_lyrics' is not defined


练习2

现在将函数调用那一行放回到末尾,并将函数print_lyrics 的定义移到函数 repeat_lyrics 定义之后。这时候运行程序会发生什么?



def repeat_lyrics():
    print_lyrics()
    print_lyrics()
def print_lyrics():
    print("I'm a lumberjack,and I'm okay")
    print("I sleep all night and I Work all day.")
repeat_lyrics()

我以为会报错,提示 print_lyrics 函数没有定义,为什么没有报错呢?


打开程序的debug模式调试程序,发现程序的执行顺序是这样的


def repeat_lyrics(): #1
print_lyrics()#4
print_lyrics()#7
def print_lyrics():#2
print("I'm a lumberjack,and I'm okay")#5  #8
print("I sleep all night and I Work all day.")#6  #9
repeat_lyrics()#3

为什么练习1会报错,而练习2却能正常运行呢?


原因就是:函数定义必须在函数被调用之前先运行函数体里面的语句并不会立即运行,而是等到函数被调用时才执行

在练习2中,在repeat_lyrics函数的函数体中,调用 print_lyrics函数,而在执行repeat_lyrics函数前(#3), print_lyrics已经被定义过了(#2),所以程序不会出错。


而在练习1中,程序自上而下运行,首先执行调用repeat_lyrics函数的语句,而在此条语句之前又没有执行定义repeat_lyrics函数的语句所以会出错。


举一反三,下面这样执行程序也会报错,因为 print_lyrics函数还没有被定义,就已经被调用。

def repeat_lyrics():
    print_lyrics()
    print_lyrics()

repeat_lyrics()

def print_lyrics():
    print("I'm a lumberjack,and I'm okay")
    print("I sleep all night and I Work all day.")

NameError: name 'print_lyrics' is not defined

3.6 执行流程

为了保证函数的定义先于其首次调用执行,需要知道程序中语句运行的顺序,即执行流程。


执行总是从程序的第一行开始。语句按照从下到下的顺序逐一运行。


函数定义并不会改变程序的执行流程,但应注意函数体中的语句并不立即运行,而是等到函数被调用时运行。


函数调用可以看作程序运行流程中的一个迂回路径。遇到函数调用时,并不会直接继续运行下一条语句,而是跳到被调用函数的函数体的第一行,继续运行完函数体的所有语句,再跳回到原来离开的地方。


这样看似简单,但马上你就会发现,函数体中可以调用其他函数。当程序流程运行到一个函数之中时,可能需要运行其他函数中的语句。而后,当运行那个函数中的语句时,又可能再需要调用运行另外一个函数语句!


幸好Python对于它运行到哪里有很好的记录,所以每个函数执行结束后,程序都能跳回到它离开的地方。直到执行到整个程序的结尾,才会结束程序。


总之,在阅读代码时,并不总应该按照代码书写顺序一行行阅读;有时候,按照程序执行的流程来阅读代码,理解的效果可能会更好


3.7 形参和实参

在函数内部,实参会赋值给称为形参(parameter)的变量。下面的例子是一个函数的定义,接收一个实参


def print_twice(bruce):
    print(bruce)
    print(bruce)

这个函数在调用时会把实参的值赋到形参bruce上,并将其打印两次。这个函数对任何可以打印的值都可用。


print_twice('span'*4)
span span span span 
span span span span 

print_twice(42)
42
42

print_twice(math.pi)

3.141592653589793

3.141592653589793

 

内置函数的组合规则,在用户自定义函数上同样可用,所以我们可以对print_twice 使用任何表达式作为实参:

>>> print_twice('span '*4)

span span span span

span span span span

>>> print_twice(math.cos(math.pi))

-1.0

-1.0


作为实参的表达式会在函数调用之前先执行。所以在这个例子中,表达式'span '*4math.cos(math.pi)都只执行一次。


也可以使用变量作为实参:

>>> michael = 'Eric,the half a bee'

>>> print_twice(michael)

Eric,the half a bee

Eric,the half a bee

 

作为实参传入到函数的变量的名称(michael)和函数定义里形参的名称(bruce)没有关系。函数内部只关心形参的值,而不用关心它在调用前叫什么名字;在 print_twice函数内部,大家都叫bruce


3.8 变量和形参是局部的

在函数体内新建一个变量时,这个变量是局部的(local),即它只存在于这个函数之内。例如:

def print_twice(bruce):
    print(bruce)
    print(bruce)

def cat_twice(part1,part2):
    cat = part1+part2
    print_twice(cat)
这个函数接收两个实参,将它们拼接起来,并将结果打印两遍。下面是一个使用这一函数的例子:
line1 = 'Bing tiddle '
line2 = 'tiddle bang. '

>>> cat_twice(line1,line2)

Bing tiddle tiddle bang.

Bing tiddle tiddle bang.

 

cat_twice 结束时,变量cat会被销毁。这时再尝试打印它的话,会得到一个异常:

>>> print(cat)

NameError: name 'cat' is not defined

 

形参也是局部的。例如,在 print_twice函数之外,不存在 bruce这个变量。


3.9 栈图

要跟踪哪些变量在哪些地方使用,有时候画一个栈图(stack diagram)会很方便。和状态图一样,栈图可以展示每个变量的值,不同的是它会展示每个变量所属的函数。


每个函数使用一个帧包含,帧在栈图中就是一个带着函数名称的盒子,里面有函数的参数和变量。前面的函数示例的栈图如图31所示。

图中各个帧从上到下安排成一个栈,能够展示出哪个函数被哪个函数调用了。在这个例子里,print_twicecat_twice调用,而cat_twice__main__调用。__main__是用于表示整个栈图的图框的特别名称。在所有函数之外新建变量时,它就是属于__main__的。


每个形参都指向与其对应的实参相同的值,所以part1line1的值相同,part2line2的值相同,而brucecat的值相同。


如果调用函数的过程中发生了错误,Python会打出函数名、调用它的函数的名称,以及调用这个调用者的函数名,依此类推,一直到__main__


例如,如果在print_twice中访问cat变量,则会得到一个NameError


Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in cat_twice
  File "<stdin>", line 4, in print_twice
NameError: name 'cat' is not defined

上面这个函数列表被称为回溯traceback)。它告诉你错误出现在哪个程序文件,哪一行,以及哪些函数正在运行。它也会显示导致错误的那一行代码。


回溯中函数的顺序和栈图中图框的顺序一致。当前正在执行的函数在最底部。


这里插个题外话,说到形参与实参的值相同,突然想到一个点,如果ab的值相同,a b可以视作是同个对象吗?神奇的是如果ab都指同一个整数(指向同一个对象地址),python会认为它们是同一个对象,而如果ab是浮点型,则python不认为它们是同一个对象(指向不同的对象地址)

>>> a=2

>>> b=2

>>> a is b

True

>>> id(a)

10943040

>>> id(b)

10943040

>>> a=2.3

>>> b=2.3

>>> a is b

False

>>> id(a)

139801075219456

>>> id(b)

139801075219648


3.10 有返回值函数和无返回值函数

在我们使用过的函数中,有一部分函数,如数学函数,会返回结果。因为没有想到更好的名字,我称这类函数为有返回值函数(fruitful function)。 另一些函数,如print_twice,会执行一个动作,但不返回任何值。我们称这类函数为无返回值函数(void function)


当调用一个有返回值的函数时,大部分情况下你都想要对结果做某种操作。例如,你可能会想把它赋值给一个变量,或者在一个表达式中:


x = math.cos(radians)

golden = (math.sqrt(5)+1)/2


在交互模式中调用函数时,Python会直接显示结果:

>>> math.sqrt(5)

2.23606797749979


但是在脚本中,如果只是直接调用这类函数,那么它的返回值就会永远选择掉!

 

math.sqrt(5)

 


这个脚本计算5的平方根,但由于并没有把计算结果存储到某个变量中,或显示出来,所以其实没什么实际作用。


无返回值函数可能在屏幕上显示某些东西,或者有其他的效果,但是它们没有返回值。如果把该结果赋值给某个变量,则会得到一个特殊的值None

>>> result = print_twice('Bing')

Bing

Bing

>>> print(result)

None


None和字符串’None’并不一样。它是一个特殊的值,有自己独特的类型:

>>> type(None)

<class 'NoneType'>


到目前为止,我们自定义的函数都是无返回值函数。再过几章我们就会开始写有返回值的函数了。

 

3.11 为什么要有函数

为什么要花功夫将程序拆分成函数呢?也许刚开始编程的时候这其中的原因并不明晰。下面这些解释都可作为参考。


新建一个函数,可以让你有机会给一组语句命名,这样可以让代码更易读和更易调试。


函数可以通过减少重复代码使程序更短小。后面如果需要修改代码,也只要修改一个地方即可。


将一长段程序拆分成几个函数后,可以对每一个函数单独进行调试,再将它们组装起来成为完整的产品。


一个设计良好的函数,可以在很多程序中使用。书写一次,调试一次,复用无穷。



3.12调试

你将会掌握的一个最重要的技能就是调试。虽然调试可能时有烦恼,但它的确是编程活动中最耗脑力、最有挑战、最有趣的部分。


在某种程度上,调试和刑侦工作很像。你会面对一些线索,而且必须推导出事情发生的过程,以及导致现场结果的事件。


调试也像是一种实验科学。一旦猜出错误的可能原因,就可以修改程序,再运行一次。如果猜对了,那么程序的运行结果会符合预测,这样就离正确的程序更近了一步。如果猜错了,则需要重新思考。正如夏洛克.福尔摩丝所说的:“当你排除掉所有的可能性,那么剩下的,不管多么不可能,必定是真相。”


对某些人来说,编程和调试是同一件事。也就是说,编程正是不断调试修改直到程序达到设计目的的过程。这种想法的要旨是,应该从一个能做某些事的程序开始,然后做一点点修改,并调试改正,如此迭代,以确保总是有一个可以运行的程序。


例如,Linux是包含了数百万行代码的操作系统,但最开始只是Linus Torvalds编写的用来研究Intel 80386芯片的简单程序。据Larry Greenfield所说:“Linus最早的一个程序是交替打印AAAA BBBB。后来这些程序演化成了Linux”


3.13术语表

函数(function):一个有名称的语句序列,可以进行某种有用的操作。函数可以接收或者不接收参数,可以返回或不返回结果。

 

函数定义(function definition):一个用来创建新函数的语句,指定函数的名称、参数以及它包含的语句序列。

 

函数对象(function object):函数定义所创建的值。函数名可以用作变量来引用一个函数对象。

 

函数头(header):函数定义的第一行。

 

函数体(body):函数定义内的语句序列。

 

形参(parameter):函数内使用的用来引用作为实参传入的值的名称。

 

函数调用(function call ):运行一个函数的语句。它由函数名称和括号中的参数列表组成。

 

实参(argument):当函数调用时,提供给它的值。这个值会被赋值给对应的形参。

 

局部变量(local variable):函数内定义的变量。局部变量只能在函数体内使用。

 

返回值(return value):函数的结果。如果函数被当作表达式调用,返回值就是表达式的值。

 

有返回值函数(fruitful function):返回一个值的函数

 

无返回值函数(void function):总是返回None的函数

 

None:由无返回值函数返回的一个特殊值。

 

模块(module):一个包含相关函数以及其他定义的集合的文件

 

import语句(import statement):读入一个模块文件,并创建一个模块对象的语句。

 

模块对象(module object):使用import语句时创建的对象,提供对模块中定义的值的访问。

 

句点表示法(dot notation):调用另一个模块中的函数的语法,使用模块名加上一个句点符号,再加上函数名。

 

组合(composition):使用一个表达式作为一个更大的表达式的一部分,或者使用语句作为更大的语句的一部分。

 

栈图(stack diagram):函数栈的图形表达形式,也展示它们的变量,以及这些变量引用的值。

 

图框(frame):栈图中的一个图框,表达一个函数调用。它包含了局部变量以及函数的参数。

 

回溯(traceback):当异常发生时,打印出正在执行的函数栈。

 

posted @ 2019-08-18 16:56  wangju003  阅读(459)  评论(0编辑  收藏  举报