爱人不可虚假,恶要厌恶,善要亲近。爱弟兄,要彼此亲热;恭敬人,要彼此推让。殷勤不可懒惰。要心里火热,常常服侍主。在指望中要喜乐,在患难中要忍耐,祷告要恒切。(ROMANS 12:9-12)

函数(2)

在上一节中,已经明确了函数的基本结构和初步的调用方法。但是,上一节中写的函数,还有点缺憾,不知道读者是否觉察到了。我把结果是用print语句打印出来的。这是实际编程中广泛使用的吗?肯定不是。在程序中,函数是一段具有抽象作用的代码。一般情况下,通过它可以得到某个结果,这个结果有一个专有名字,叫做“返回值”。返回值会继续被用到程序中的某个地方。

返回值

为了能够说明清楚,先编写一个函数。还记得斐波那契数列吗?忘了没关系,回头看看或者google。不过,在你要实施google或者顺延着向下阅读之前,最好先自己尝试一下,能不能写一个斐波那契数列的函数。

我这里提供一段参考代码(既然是参考,显然不是唯一正确答案):

#!/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。如果没有这个赋值语句,函数照样返回值,但是它飘忽在内存中,我们无法得到,并且最终还被当做垃圾被python回收了。

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

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

有的函数,没有return,一样执行完毕,就算也干了某些活儿吧。事实上,不是没有返回值,也有,只不过是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

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

其实没有那么复杂。关键要理解函数名括号后面的东东(管它什么参呢)的作用是传递值。所以,那个参数的作用本质上就是一个“占位符”,当调用一个函数的时候,并不是赋值了一份参数的值来替换占位符,比如add(x),并没有用3来替换原来的占位符,而是把占位符指向了变量,进而指向了对象。换个角度说,就是通过一连串的接力动作,把对象传给了函数。这样说来,你就可以在函数内部改变那个对象了。

>>> def foo(lst):
...     lst.append(99)
...     return lst
... 
>>> x = [1, 3, 5]
>>> y = foo(x)
>>> y
[1, 3, 5, 99]
>>> x
[1, 3, 5, 99]
>>> id(x)
3075464588L
>>> id(y)
3075464588L

结合前面学习过的列表能够被原地修改知识,加上刚才说的参数特点,你是不是能理解上面的操作呢?

全局变量和局部变量

下面是一段代码,注意这段代码中有一个函数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)

如有疑问,请到Python论坛提问