返回首页 《从零开始学 Python》(第二版)

预备

基本数据类型

语句和文件

函数

错误和异常

模块

保存数据

实战

用Tornado做网站

科学计算

结尾

函数(2)

在上一节中,已经明确了函数的基本结构和初步的调用方法。但是,上一节中写的函数,还有点缺憾,不知道读者是否觉察到了。我把结果是用 print 语句打印出来的。这是实际编程中广泛使用的吗?肯定不是。因为函数在编程中,起到一段具有抽象价值的代码作用,一般情况下,用它得到一个结果,这个结果要用在其它的运算中。所以,不能仅仅局限在把某个结果打印出来。所以,函数必须返回一个结果。

结论:函数要有返回值,也必须有返回值。

返回值

为了能够说明清楚,先编写一个函数。还记得斐波那契数列吗?我打算定义一个能够得到斐波那契数列的函数,从而实现可以实现任意的数列。你先想想,要怎么写?

参考代码:

#!/usr/bin/env Python
# coding=utf-8

def fibs(n):
    result = [0,1]
    for i in range(n-2):
        result.append(result[-2] + result[-1])
    return result

if __name__ == "__main__":
    lst = fibs(10)
    print lst

把含有这些代码的文件保存为名为 20202.py 的文件。在这个文件中,首先定义了一个函数,名字叫做 fibs,其参数是输入一个整数。在后面,通过 lst = fibs(10) 调用这个函数。这里参数给的是 10,就意味着要得到 n=10 的斐波那契数列。

运行后打印数列:

$ python 20202.py
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

当然,如果要换 n 的值,只需要在调用函数的时候,修改一下参数即可。这才体现出函数的优势呢。

观察 fibs 函数,最后有一个语句 return result,意思是将变量 result 的值返回。返回给谁呢?一般这类函数调用的时候,要通过类似 lst = fibs(10) 的语句,那么返回的那个值,就被变量 lst 贴上了,通过 lst 就能得到该值。如果没有这个赋值语句,虽然函数照样返回值,但是它飘忽在内存中,我们无法得到,并且最终还被当做垃圾被 Python 回收了。

注意:上面的函数之返回了一个返回值(是一个列表),有时候需要返回多个,是以元组形式返回。

>>> def my_fun():
...     return 1,2,3
... 
>>> a = my_fun()
>>> a
(1, 2, 3)

有的函数,没有 renturn,一样执行完毕,就算也干了某些活儿吧。事实上,不是没有返回值,也有,只不过是 None。比如这样一个函数:

>>> def my_fun():
...     print "I am doing somthin."
... 

我在交互模式下构造一个很简单的函数,注意,我这是构造了一个简单函数,如果是复杂的,千万不要在交互模式下做。如果你非要做,是能尝到苦头的。

这个函数的作用就是打印出一段话。也就是执行这个函数,就能打印出那段话,但是没有 return。

>>> a = my_fun()
I am doing somthin.

我们再看看那个变量 a,到底是什么

>>> print a
None

这就是这类只干活儿,没有 return 的函数,返回给变量的是一个 None。这种模样的函数,通常不用上述方式调用,而采用下面的方式,因为他们返回的是 None,似乎这个返回值利用价值不高,于是就不用找一个变量来接受返回值了。

>>> my_fun()
I am doing somthin.

特别注意那个 return,它还有一个作用。观察下面的函数和执行结果,看看能不能发现它的另外一个作用。

>>> def my_fun():
...     print "I am coding."
...     return
...     print "I finished."
... 
>>> my_fun()
I am coding.

看出玄机了吗?在函数中,本来有两个 print 语句,但是中间插入了一个 return,仅仅是一个 return。当执行函数的时候,只执行了第一个 print 语句,第二个并没有执行。这是因为第一个之后,遇到了 return,它告诉函数要返回,即中断函数体内的流程,离开这个函数。结果第二个 print 就没有被执行。所以,return 在这里就有了一个作用,结束正在执行的函数。有点类似循环中的 break 的作用。

函数中的文档

“程序在大多数情况下是给人看的,只是偶尔被机器执行以下。”所以,写程序必须要写注释。前面已经有过说明,如果用 # 开始,Python 就不执行那句(Python 看不到它,但是人能看到),它就作为注释存在。

除了这样的一句之外,一般在每个函数名字的下面,还要写一写文档,以此来说明这个函数的用途。

#!/usr/bin/env python
# coding=utf-8

def fibs(n):
    """
    This is a Fibonacci sequence.
    """
    result = [0,1]
    for i in range(n-2):
        result.append(result[-2] + result[-1])
    return result

if __name__ == "__main__":
    lst = fibs(10)
    print lst

在这个函数的名称下面,用三个引号的方式,包裹着对这个函数的说明,那个就是函数文档。

还记得在《自省》那节中,提到的__doc__吗?对于函数,它的内容就来自这里。

>>> def my_fun():
...     """
...     This is my function.
...     """
...     print "I am a craft."
... 
>>> my_fun.__doc__
'\n    This is my function.\n    '

如果在交互模式中用 help(my_fun) 得到的也是三个引号所包裹的文档信息。

Help on function my_fun in module __main__:

my_fun()
    This is my function.

参数和变量

参数

虽然在上一节,已经知道如何通过函数的参数传值,如何调用函数等。但是,这里还有必要进一步讨论参数问题。在别的程序员嘴里,你或许听说过“形参”、“实参”、“参数”等名词,到底指什么呢?

在定义函数的时候(def 来定义函数,称为 def 语句),函数名后面的括号里如果有变量,它们通常被称为“形参”。调用函数的时候,给函数提供的值叫做“实参”,或者“参数”。

其实,根本不用区分这个,因为没有什么意义,只不过类似孔乙己先生知道茴香豆的茴字有多少种写法罢了。但是,我居然碰到过某公司面试官问这种问题。

在本教程中,把那个所谓实参,就称之为值(或者数据、或者对象),形参就笼统称之为参数(似乎不很合理,但是接近数学概念)。

比较参数和变量

参数问题就算说明白了,糊涂就糊涂吧,也没有什么关系。不过,对于变量和参数,这两个就不能算糊涂账了。因为它容易让人糊涂了。

在数学的函数中 y = 3x + 2,那个x叫做参数,也可以叫做变量。但是,在编程语言的函数中,与此有异。

先参考一段来自微软网站的比较高度抽象,而且意义涵盖深远的说明。我摘抄过来,看官读一读,是否理解,虽然是针对VB而言的,一样有启发。

参数和变量之间的差异 (Visual Basic)

多数情况下,过程必须包含有关调用环境的一些信息。执行重复或共享任务的过程对每次调用使用不同的信息。此信息包含每次调用过程时传递给它的变量、常量和表达式。

若要将此信息传递给过程,过程先要定义一个形参,然后调用代码将一个实参传递给所定义的形参。 您可以将形参当作一个停车位,而将实参当作一辆汽车。 就像一个停车位可以在不同时间停放不同的汽车一样,调用代码在每次调用过程时可以将不同的实参传递给同一个形参。

形参表示一个值,过程希望您在调用它时传递该值。

当您定义 Function 或 Sub 过程时,需要在紧跟过程名称的括号内指定形参列表。对于每个形参,您可以指定名称、数据类型和传入机制(ByVal (Visual Basic) 或 ByRef (Visual Basic))。您还可以指示某个形参是可选的。这意味着调用代码不必传递它的值。

每个形参的名称均可作为过程内的局部变量。形参名称的使用方法与其他任何变量的使用方法相同。

实参表示在您调用过程时传递给过程形参的值。调用代码在调用过程时提供参数。

调用 Function 或 Sub 过程时,需要在紧跟过程名称的括号内包括实参列表。每个实参均与此列表中位于相同位置的那个形参相对应。

与形参定义不同,实参没有名称。每个实参就是一个表达式,它包含零或多个变量、常数和文本。求值的表达式的数据类型通常应与为相应形参定义的数据类型相匹配,并且在任何情况下,该表达式值都必须可转换为此形参类型。

看官如果硬着头皮看完这段引文,发现里面有几个关键词:参数、变量、形参、实参。本来想弄清楚参数和变量,结果又冒出另外两个东东,更混乱了。请稍安勿躁,本来这段引文就是有点多余,但是,之所以引用,就是让列位开阔一下眼界,在编程业界,类似的东西有很多名词。下次听到有人说这些,不用害怕啦,反正自己听过了。

在 Python 中,没有这么复杂。

看完上面让人晕头转向的引文之后,再看下面的代码,就会豁然开朗了。

>>> def add(x):     #x 是参数,准确说是形参
...     a = 10      #a 是变量
...     return a+x  #x 就是那个形参作为变量,其本质是要传递赋给这个函数的值
... 
>>> x = 3           #x 是变量,只不过在函数之外
>>> add(x)          #这里的 x 是参数,但是它由前面的变量 x 传递对象 3
13
>>> add(3)          #把上面的过程合并了
13

至此,看官是否清楚了一点点。当然,我所表述不正确之处或者理解错误之处,也请看官不吝赐教,小可作揖感谢。

其实没有那么复杂。关键要理解函数名括号后面的东东(管它什么参呢)的作用是传递值。

全局变量和局部变量

下面是一段代码,注意这段代码中有一个函数 funcx(),这个函数里面有一个变量 x=9,在函数的前面也有一个变量 x=2

x = 2

def funcx():
    x = 9
    print "this x is in the funcx:-->",x

funcx()
print "--------------------------"
print "this x is out of funcx:-->",x

那么,这段代码输出的结果是什么呢?看:

this x is in the funcx:--> 9
--------------------------
this x is out of funcx:--> 2

从输出看出,运行 funcx(),输出了 funcx() 里面的变量 x=9;然后执行代码中的最后一行,print "this x is out of funcx:-->",x

特别要关注的是,前一个 x 输出的是函数内部的变量 x;后一个 x 输出的是函数外面的变量 x。两个变量彼此没有互相影响,虽然都是 x。从这里看出,两个 x 各自在各自的领域内起到作用。

把那个只在函数体内(某个范围内)起作用的变量称之为局部变量

有局部,就有对应的全部,在汉语中,全部变量,似乎有歧义,幸亏汉语丰富,于是又取了一个名词:全局变量

x = 2
def funcx():
    global x    #跟上面函数的不同之处
    x = 9
    print "this x is in the funcx:-->",x

funcx()
print "--------------------------"
print "this x is out of funcx:-->",x

以上两段代码的不同之处在于,后者在函数内多了一个 global x,这句话的意思是在声明 x 是全局变量,也就是说这个 x 跟函数外面的那个 x 同一个,接下来通过 x=9 将 x 的引用对象变成了 9。所以,就出现了下面的结果。

this x is in the funcx:--> 9
--------------------------
this x is out of funcx:--> 9

好似全局变量能力很强悍,能够统帅函数内外。但是,要注意,这个东西要慎重使用,因为往往容易带来变量的换乱。内外有别,在程序中一定要注意的。

命名空间

这是一个比较不容易理解的概念,特别是对于初学者而言,似乎它很飘渺。我在维基百科中看到对它的定义,不仅定义比较好,连里面的例子都不错。所以,抄录下来,帮助读者理解这个名词。

命名空间(英语:Namespace)表示标识符(identifier)的可见范围。一个标识符可在多个命名空间中定义,它在不同命名空间中的含义是互不相干的。这样,在一个新的命名空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其它命名空间中。

例如,设 Bill 是 X 公司的员工,工号为 123,而 John 是 Y 公司的员工,工号也是 123。由于两人在不同的公司工作,可以使用相同的工号来标识而不会造成混乱,这里每个公司就表示一个独立的命名空间。如果两人在同一家公司工作,其工号就不能相同了,否则在支付工资时便会发生混乱。

这一特点是使用命名空间的主要理由。在大型的计算机程序或文档中,往往会出现数百或数千个标识符。命名空间(或类似的方法,见“命名空间的模拟”一节)提供一隐藏区域标识符的机制。通过将逻辑上相关的标识符组织成相应的命名空间,可使整个系统更加模块化。

在编程语言中,命名空间是对作用域的一种特殊的抽象,它包含了处于该作用域内的标识符,且本身也用一个标识符来表示,这样便将一系列在逻辑上相关的标识符用一个标识符组织了起来。许多现代编程语言都支持命名空间。在一些编程语言(例如 C++ 和 Python)中,命名空间本身的标识符也属于一个外层的命名空间,也即命名空间可以嵌套,构成一个命名空间树,树根则是无名的全局命名空间。

函数和类的作用域可被视作隐式命名空间,它们和可见性、可访问性和对象生命周期不可分割的联系在一起。

显然,用“命名空间”或者“作用域”这样的名词,就是因为有了函数(后面还会有类)之后,在函数内外都可能有外形一样的符号(标识符),在 Python 中(乃至于其它高级语言),通常就是变量,为了区分此变量非彼变量(虽然外形一样),需要用这样的东西来框定每个变量所对应的值(发生作用的范围)。

前面已经讲过,变量和对象(就是所变量所对应的值)之间的关系是:变量类似标签,贴在了对象上。也就是,通过赋值语句实现了一个变量标签对应一个数据对象(值),这种对应关系让你想起了什么?映射!Python 中唯一的映射就是 dict,里面有“键值对”。变量和值得关系就有点像“键”和“值”的关系。有一个内建函数 vars,可以帮助我们研究一下这种对应关系。

>>> x = 7
>>> scope = vars()
>>> scope['x']
7
>>> scope['x'] += 1
>>> x
8
>>> scope['x']
8

既然如此,诚如前面的全局变量和局部变量,即使是同样一个变量名称。但是它在不同范围(还是用“命名空间”这个词是不是更专业呢?)对应不同的值。


总目录   |   上节:函数(1)   |   下节:函数(3)

如果你认为有必要打赏我,请通过支付宝:qiwsir@126.com,不胜感激。

上一篇: 函数(1) 下一篇: 函数(3)