零基础学Python:函数(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.

参数和变量

参数

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

其实,根本不用区分这个,因为没有什么意义,只不过类似孔乙己先生知道茴香豆的茴字有多少种写法罢了。

在本教程中,把那个所谓实参,就称之为值(或者数据、或者对象),形参就笼统称之为参数(似乎不很合理,但是接近数学概念)。随着你敲代码的实践越多,或许会对各种参数概念有深入理解。

比较参数和变量

在不同的参数名称面前,糊涂也罢、明白也罢,对写程序的干扰不大。不过,对于变量和参数,这两个就不能算糊涂账了。不过它们的确容易让把人搞糊涂了。

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

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

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

在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

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

命名空间

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

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

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

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

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