python 函数和函数式编程 1 什么是函数 2 调用函数 3 创建函数 4 变长参数 5 函数式变成 6 变量作用域 7 生成器

函数是对程序逻辑进行结构化或过程化的一种编程方法。能将整块代码巧妙地隔离成易于管理 的小块,把重复代码放到函数中而不是进行大量的拷贝--这样既能节省空间,也有助于保持一致性,因为你只需改变单个的拷贝而无须去寻找再修改大量复制代码的拷贝。

1.1 过程 vs 函数

在C++里我不记得有过程这种东西,但是在一些其它的语言比如PL/SQL里面会有过程。过程和函数一样是可以调用的代码块,但是过程没有返回值。在pyhon里,面其实过程和函数是一个东西,因为如果你在函数里面不指定返回值,python的interpreter会返回None.

1.2 返回值

python的函数会返回一个返回值。 如果你显示的指定了这个值,那么返回的就是你想要的类型,否则python的interpreter默认返回none。比如下面的例子就是:

>>> def foo():
...     pass
...
>>> print a
None
View Code

但是,有人声称python的函数可以返回多个值。其实事实是,python把这些返回值打包到了一个元组中。比如下面的例子:

>>> def foo():
...     return 1,2,'a','b'
...
>>> foo()
(1, 2, 'a', 'b')
View Code

python 可以通过返回一个容器,比如 list ,dict,tuple等来间接的实现返回多个值的目的,上面的例子中返回的是一个元组,因为元组的语法不强制要求写括号,所以 return语句就把后面逗号分隔的内容当做一个元组一起返回。

2 调用函数

2.1 关键字参数

关键字参数指的是如下调用函数的形式,可以看到在函数调用的时候采用的是(x= , y= )。这样的形式。这种理念是让调用者通过函数调用中的参数名字来区分参数。这样规范允许参不按顺序,因为解释器能通过给出的关键字来匹配参数的值。

>>> def foo(x,y):
...     print x,y
...
>>> foo(y=2,x=1)
1 2
View Code

但要注意的是关键字参数的概念仅仅是针对函数调用来说的,也就是说在定义函数的时候不用做任何的特别设定。我们再看一个例子,假设现在有一个函数conn()需要两个参数 host 和 port。 定义如下:

>>> def conn(host,port):
...     hostname=host
...     portnumber=port
...     print hostname,portnumber
View Code

你在调用的时候可以采用正常的调用方式,按顺序输入恰当的参数,如:

>>> conn('server_A',22)
server_A 22
View Code

你也可以用关键字调用的方式,这时候就不用管输入参数的顺序了,如:

>>> conn(port=22,host='server_B')
server_B 22
View Code

2.2 默认参数

这个很简单,在c/c++里都有,就是在函数定义的时候提供一个默认的参数,这样未来调用的时候如果不显示提供参数,函数就会使用默认的参数。

>>> def foo(x=4,y=4):
...     return x+y
...
>>> foo()
8
View Code

2.3 参数组

python的函数在调用时,可以把参数放入一个元组或者是字典,然后把这个元组或者字典传递给函数进行调用。当然元组中存放的是非关键字参数而字典中存放的是关键字参数。

>>> def foo(x,y):
...     return x+y
...
>>> a=4,4
>>> foo(*a)
8
>>> d={'y':5,'x':3}
>>> foo(**d)
8
View Code

注意在元组前加一个* 而在字典前要加**。他们表示把字典或者元组解开的意思,也就是把a还原成 4,4 而 d还原成 y=3,x=5

3 创建函数

3.1 用def语句创建函数

创建函数的语法非常简单,如下:

def function_name(arguments):
    "function documentation string"
    function body
View Code

文档字符串是可选的。

3.2 声明和定义的比较

有些语言比如C/C++中,函数的声明和定义是分开的。声明只包括函数的名字和参数列表,而函数的定义则要包括函数的内部逻辑。函数声明和定义有区别的语言往往是因为他们要把函数声明和函数定义放到不同的文件里,而在python里面两者是一体的。

3.3 向前引用

情况下面一段代码:

def foo():
        print "in foo()"
        bar()

def bar():
        print "in bar()"
foo()
View Code

如果放在python解释器中运行这段代码,也许你会认为会出错,因为在调用bar()函数的时候,bar函数还没有定义。 但实际上不会出错,因为虽然foo中bar调用在bar的定义之前,但是foo()本身的调用并不是在bar的定义之前

3.4 函数属性

python中函数其实也是一个实例或者说对象。而python中的对象可以当做一个名称空间。换个说法就是python中的对象,比如这里的函数,可以用来存储名字。看一下下面的实例那就明白了:

>>> def foo():
...     'this is the doc string of foo'
...
>>> foo.attr1=1
>>> foo.attr2=2
>>> foo.attr1
1
>>> foo.attr2
2
View Code

我们创建了一个函数,这个函数什么都没做,然后我们用句点符号来为函数创建了两个属性attr1,attr2并且赋值,这样我们就可以通过句点符号来访问这两个属性了。 关于名称空间的概念后面会有详细介绍,这里我们只要理解为一个存储名字的空间就可以了。要注意的另外一点是,上面例子中在定义foo的时候创建了一个文档字符串。所谓文档字符串就是函数里面第一个没有赋给变量的字符串,这个字符串可以通过 函数名.__doc__来访问,也可以通过help来访问

>>> help(foo)
Help on function foo in module __main__:

foo()
    this is the doc string of foo

>>> foo.__doc__
'this is the doc string of foo'
View Code

3.5 内嵌函数

内嵌函数就是在函数体的内部创建的一个函数,比如下面的代码:

>>> def foo():
...     print 'foo is called'
...     def bar():
...             print 'bar is called'
...     bar()
...
>>> foo()
foo is called
bar is called
View Code

但要注意的是,内嵌函数的作用域只能是在外部函数内,所以下面的代码就会出错

>>> bar()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'bar' is not defined
>>>
View Code

因为你调用bar的地方是在foo的函数体外部,这里识别不到bar。

3.6 装饰器

装饰器是python中的一个比较独特的东西,至少在c/c++中是没有这个东西的。 我们首先看一下python的装饰器大概是什么样的,如果感觉困惑,我们后面的链接中提供了一篇文章,可以很清晰的阐述装饰器的意义已经它到底是什么。

首先,python中使用装饰器的代码大概是下面这样的

>>> @decorator
... def foo():
...     pass
...
View Code

上面代码中的decorator就是一个装饰器,用@放在它的前面就表示它是装饰器,而上面代码的意思就是,我们将要用 decorator 这个装饰器,来修饰一下我们的foo函数,修饰完成之后,你再调用foo的时候,foo其实就已经变成了另外一个函数,上面代码等同于 foo=decorator(foo). 

如果你还是感觉困惑,可以看一下这个链接 http://www.cnblogs.com/kramer/p/3640040.html

4 变长参数

有时候我们需要处理可变数量参数的情况。 也就是说直到调用的时候 才会知道函数需要多少个参数。 这在python中是能够做到的。 不过,我们知道python中调用函数的时候分为两种情况,非关键字参数和关键字参数,相对应的,如果要实现可变长参数,也要分两种情况考虑--可变长的非关键字参数和可变长的关键字参数。首先看可变长的非关键字参数。 比如下面的代码,

>>> def foo(arg1,arg2=2,*restArgs):
...     print arg1
...     print arg2
...     print restArgs
...
View Code

上面的函数定义中,除了形参 arg1,arg2=2之外,还有一共*restArgs。 最后这个形参的意思就是,如果 foo在调用的时候有大于等于3个非关键字参数,那么除了第一个要赋给arg1,第二个要赋给 arg2之外,剩下的要组成一个元组赋给 restArgs。 我们可以看一下,运行时的情况:

>>> foo(1,2,3,4,5,6,7,8)
1
2
(3, 4, 5, 6, 7, 8)
>>> foo(1)
1
2
()
View Code

第一次调用 除了第一个和第二个参数,剩下的都被赋给了 restArgs,而第二次调用,由于只有一个参数,所以它赋给了arg1,arg2采用了默认参数,而restArgs是一个空的元组。

上面是针对非关键字参数的情况,那么可变长的关键字参数,怎么处理呢?

>>> def foo(arg1,arg2,**restKeyArgs):
...     print arg1
...     print arg2
...     print restKeyArgs
...
View Code

上面这段代码的意思是,在调用foo的时候,除了第一个和第二个参数,剩下所有的关键字参数都要复制给 restKeyArgs作为一个字典对象。比如在调用的时候:

>>> foo(1,2,x=3,y=4)
1
2
{'y': 4, 'x': 3}
View Code

1,2赋值给了arg1,arg2. x=3和y=4复制给了restKeyArgs作为一个字典对象。

如果你想让你的函数在调用的时候,可以接受任意个非关键字参数和关键字参数,你可以这样定义函数:

>>> def foo(arg1,arg2,*restNonkey,**restKey):
...     print arg1
...     print arg2
...     print restNonkey
...     print restKey
...

>>> foo(1,2,3,4,5,x=1,y=2)
1
2
(3, 4, 5)
{'y': 2, 'x': 1}
View Code

总结一下,就是说如果你想让函数接受可变长的非关键字参数,就使用一个带有一个*的形参来接受可变长个实参 , 如果你想让函数接受可变长的 关键字参数 ,那么就用一个带有两个*的形参来接受可变长个关键字实参。

但是有一个地方要明白,我们之前提到了参数组。所谓参数组就是可以在调用函数的时候把参数打包进容器(非关键字打包进元组,而关键字参数打包进字典),然后在调用的时候通过*或者**来表名他们是关键字参数或者是非关键字参数。虽然都用到了*和**这个符号,但跟我们这一小节讲的可变长参数虽然很像是有区别的。因为这一小节的知识是在定义函数的时候让函数可以有可变长个参数,而参数组部分的知识是说我们可以把实参打包进容器传递给函数。 仔细看下面的例子:

>>> def foo(arg1,arg2,*restNK,**restK):
...     print arg1
...     print arg2
...     print restNK
...     print restK
...
View Code

这里是我们这一节讲的知识点,可变长参数。而下面的调用涉及的* 和 **是参数组部分的知识点。

>>> t1
(1, 2)
>>> d1
{'y': 2, 'x': 1}
>>> foo(*t1,**d1)
1
2
()
{'y': 2, 'x': 1}
View Code

5 函数式变成

函数是编程到底和面向对象变成有什么区别,这个我还不太清楚,不过关于python的函数是编程知识点可以总结为下面的内容。

5.1 匿名函数与lambda

匿名函数,顾名思义就是没有名字的函数,而lambda就是定义匿名函数的一个方法。比如下面的代码就是用lambda定义了一个匿名函数。

>>> lambda x:x*x
<function <lambda> at 0x1c8e70>
>>> type(lambda x:x*x)
<type 'function'>
View Code

第一行代码返回一个匿名函数,用type可以验证这一点。 而且可以看出lambda定义匿名函数的语法非常简单  lambda 后面紧跟形参列表,然后是冒号':'。 冒号后面是函数逻辑。简单的说,第一行代码就是定义了一个需要接受一个形参x,然后返回x的平方的一个函数。不过这个函数没有名称。整个代码除了没有一个函数名字外跟真正的函数没什么区别。

看到这里你一定想问,那么为什么我们还需要匿名函数呢?看一下下面的场景。

假设你在写程序的时候写了下面一段代码,

map(lambda x:x*x , [y for y in range(10)])

上面这段代码使用了 lambda。逻辑非常清晰,就是说把 [y for y in range(10)]这个列表里面的所有元素都平方一次,并返回一个新的列表。 但假设如果没有lambda呢? 你需要这么写, 首先定义一个函数

>>> def a(x):
...     return x*x
...

然后你要把这个函数应用到你的代码里面去,

map(a,[y for y in range(10)])

你觉的那种做法更好呢?是lambda还是后面这种? 要知道,在实际情况下, 你可能是在面对几万条甚至更多的代码。在阅读代码的时候,遇到上面的代码,你就需要去查询一下a这个函数是干嘛的。这无疑是很耗费精力的。 如果这种情况特别多,你就需要不断的去查询函数定义,然后返回来查看代码,这是很头疼的。 但是用第一种形式就相当的简单明了。

所以lambda尤其适用于这种逻辑简单的函数。尤其是,如果你这个函数仅仅定义且使用一次,那就更应该使用lambda了。 函数存在的意义是抽象而抽象的目的是提高代码复用率。但是对这种仅仅使用一次的代码,根本不涉及复用的问题,我们为什么不直观一点呢?

6 变量作用域

6.1 局部变量和全局变量

函数内部创建的变量,叫做局部变量,只在函数内部可以访问。而模块内部最高层变量叫做全局变量。全局变量在模块内任何一个地方都能够访问。如下面的例子:

>>> g1=123
>>>
>>> def foo():
...     l1=456
...     print 'g1 is ', g1
...     print 'l1 is ', l1
...
>>> foo()
g1 is  123
l1 is  456
>>> g1
123
>>> l1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'l1' is not defined
View Code

首先我们定义了一个全局变量g1,赋值为 123。然后定义一个函数foo并且在函数内部定义局部变量l1赋值为456. 我们在foo内部会访问以下g1和l1.  通过foo()函数调用可以发现访问g1和l1都没问题。 但是在foo外部访问g1可以,访问l1却出错。 

虽然说在模块内部任何一个地方都可以访问全局变量,但这么说也不完全正确。 python在访问一个变量的时候是先从局部作用域找起,然后向外层作用域查找。如果这个过程中找到了变量,python就停止查找。 所以如果你在局部作用域--比如说函数内部,定义了一个和全局变量同名的变量,那么在这个局部作用域你可能就访问不到全局变量了,因为python总是先找到你定义的局部变量,然后就停止查找了。比如:

>>> global_v=1234
>>>
>>> def foo():
...     global_v=45679
...
...
>>> foo()
>>> global_v
1234
View Code

这个例子中,我们在foo内部,也就是局部作用域定义了一个和全局变量同名的变量global_v。所以在foo内部你就访问不到全局变量global_v。所以你会发现虽然foo对global_v进行了重新赋值,但是真正的全局变量global_v并没有改变。

不过你可以用global 关键字来指定,你访问的就是全局变量,如下:

>>> global_v=1234
>>> def foo():
...     global global_v
...     global_v=45678
...
>>> foo()
>>> global_v
45678
>>>
View Code

6.2 python嵌套作用域

python中作用域是嵌套的,就是说内部作用域总是能访问外部作用域中的变量。比如下面的代码:

>>> def foo1():
...     l1='first'
...     def foo2():
...             l2='second'
...             def foo3():
...                     l3='third'
...                     print 'l1 ', l1
...                     print 'l2 ', l2
...                     print 'l3 ', l3
...             foo3()
...     foo2()
...
>>> foo1()
l1  first
l2  second
l3  third
View Code

foo3是第三层函数了,但是它可以访问所有外层变量

6.3 闭包

(这一小节参考博文 http://blog.csdn.net/marty_fu/article/details/7679297)python中的闭包简单的说就是,如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure). 比如下面的例子:

>>> def foo(x):
...     def inner(y):
...             return x+y
...     return inner
...
>>>
>>> inner=foo(2)
>>> type(inner)
<type 'function'>
>>> inner(6)
8
View Code

inner是foo的内部函数,但是引用了foo的局部变量,所以就形成了闭包。  其实我最开始接触闭包的时候还有一个疑问,拿这个例子来说,我认为foo调用返回后,x这个局部变量就应该被销毁了,所以inner调用应该会出错才对。 但实际上并不是,因为python中的变量是每被引用一次 引用reference数会加1,每被销毁或者删除引用一次,reference数会减一。在这个例子中,foo引用了一次 inner又引用了一次,虽然foo退出了,但是inner没有,所以x不会被消除。

6.3.1 python闭包的几个注意事项

a. python闭包修改外部作用域的时候,不要覆盖外部变量

比如下面的例子,就是python的一个经典错误。

>>> def foo():
...     a=1
...     def bar():
...             a=a+1
...             return a
...     return bar
...
>>>
>>> b=foo()
>>> print b()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in bar
UnboundLocalError: local variable 'a' referenced before assignment
View Code

这段代码本意是在每次print b()的时候,都能递加a一次。 但是仔细分析内部函数bar() 你就会发现问题所在。bar中a=a+1这一段,在python从左到右解读的时候会认为a=说明a是python的一个局部变量。 所以这里a就覆盖了foo中的a。但是紧接着你又使用a+1,这时候python就会认为你在使用一个没有赋值的局部变量,所以就出错了。 解决办法很简单,就是把a变成一个容器。

>>> def foo():
...     a=[1]
...     def bar():
...             a[0]=a[0]+1
...             return a[0]
...     return bar
...
>>> b=foo()
>>> print b()
2
View Code

为什么python 看到a[0]的时候就不会认为它是变量了呢? 我是这么理解的,如果看到一个a=认为它是变量,然后在本地先查找就找到了,所以就认为是local变量。但是看到a[0]的时候,会首先认为a是一个容器,然后就会在local查找,但是本地又没有a的定义,所以就会到外层查找,而这样就查到了外层的a。 

ok,现在你应该会觉得上面的写法有些别扭吧?我们的本意只是想让闭包函数能够引用外层函数的变量foo而已,有必要弄出容器什么的这么奇怪吗? python 3对这个就有了改进。在python 3中,你只要在 bar里面 a的前面加一个nonlocal 声明它是不是一个local变量就可以了。这样python就会跳过local 从外层开始查找,这样就找到了 foo的a。

b. another error

我们再看一个例子,这个例子可能和闭包没太大关系,但也值得一看。 先看下面一段代码:

>>> for i in range(3):
...     print i
...
0
1
2
>>> print i
2
>>>
View Code

这段代码本身只是很简单的一段for循环,但是在python里面for循环有个问题就是for循环结束之后不会销毁它的内部变量 i.

python 还有一个问题就是python的函数体只有在执行的时候才会确定其函数体里面变量的值。如下:

>>> funList=[]
>>> for i in range(3):
...     def foo(x): print x+i
...     funList.append(foo)
...
>>> for f in funList:
...     f(2)
...
View Code

这一段代码你也许会认为执行的结果是2,3,4 但实际上是4,4,4。这是因为python把这些函数放入funList的时候并没有确定i的值。

6.3.2 闭包在编程中的意义

说了这么多,闭包在函数编程中到底有什么意义呢?主要有如下两种作用:

a. 闭包执行完成后,保留住当前的运行环境。

以一个棋盘游戏为例,假设我们有一个50*50的棋盘,左下角坐标为0,0 我需要一个函数,假设函数名为 play,这个函数接受两个参数,方向和步长,然后返回按照方向步长移动棋子后,棋子的新位置。当然这个函数还要记住我现在的位置,下一次在执行的时候还要从这个位置开始执行,这里,就可以用到闭包了,因为闭包可以记住上一次执行结束的环境。

>>> orignal=[0,0] #orignal locaiton 
>>> def create(pos=orignal):
...     def player(direction,step):
...             pos[0]=pos[0]+direction[0]*step #x direction
...             pos[1]=pos[1]+direction[1]*step #y direction
...             return pos
...     return player
...
>>> p=create()
>>> print p([1,0],2) #x direction 2 steps
[2, 0]
>>> print p([1,0],3) #x direction 3 steps
[5, 0]
>>> print p([0,1],3) #y direction 3 steps
[5, 3]
>>> print p([1,0],-2)#x direction -2 steps
[3, 3]
>>> print p([0,1],2) #y direction 2 steps
[3, 5]
View Code

b. 闭包函数可以根据外部作用域的局部变量来得到不同的结果

这就类似于配置的意思。用外部函数的局部变量来配置内部闭包,看完下面的例子你就明白了。

假设我们需要对一些文件进行过滤处理, 过滤出含有某些词的行, 看下面的代码:

>>> def makeFilter(word):
...     def filter(file):
...             f=open(file)
...             lines=f.readlines()
...             f.close
...             results=[i for i in lines if word in i]
...             return results
...     return filter
...
>>>
>>> myfilter=makeFilter('host')
>>> myfilter('/tmp/a.txt')
View Code

这个闭包中,外部函数起到了一个配置的作用,通过外部函数可以创建不同的filter。

7 生成器

7.1 迭代器

理解生成器之前,需要了解另一个概念迭代器。迭代器是python中一种线性的数据集合,有一个next()方法。调用next() 会依次序返回迭代器中的数据,当访问完迭代器中最后一个数据之后,会抛出预定义的异常 StopIteration。 python中线性的数据集合有很多,比如list tuple string,但他们没有next()方法,所以都不是迭代器,不过我们可以用迭代器的工厂函数iter()来把这些类型变成迭代器。如下:

>>> a=(1,2,3,4,5,6,7,8)
>>> a1=iter(a)
>>> a1.next()
1
>>> a1.next()
2
View Code

具体可以参考这篇文章。http://www.cnblogs.com/kramer/p/3678879.html

7.2 生成器

python中使用了yield关键字的函数叫做生成器函数。普通的函数返回一个返回值,但是生成器函数却返回一个生成器。 比如下面的代码:

>>> def foo():
...     print 'begin run'
...     yield 1
...     yield 2
...     yield 3
...
>>> a=foo()
>>> type(a)
<type 'generator'>
View Code

使用了yield的函数foo 在 a=foo()的时候 并没有执行print语句,而我们用type(a)可以看到,foo()返回的是一个叫做生成器的东西。 接下来,我们要运行a.next(),函数foo的内部逻辑才会真的开始执行,但每次调用只会调用到一个yield关键字处,然后再次调用会调用到接下来的yield关键字处。最后一个yield返回后,我们再调用a.next()会像迭代器一样抛出StopIteration异常。

>>> a.next()
begin run
1
>>> a.next()
2
>>> a.next()
3
>>> a.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
View Code

所以,你可以发现,生成器和迭代器很像,唯一不同的就是,生成器里面可能会有一些函数逻辑,比如这里的print语句。

7.3 yield

python 2.5之后对yield做了一些调整。原来的yield是关键字或者说是语句,但现在是表达式了。 也就是说原来的 yield 5只是代表某次迭代的时候会返回5.但现在 yield 5会有返回值了,因为yield是一个表达式。 比如下面的代码:

>>> def foo():
...     print 'step 1'
...     a1=yield 1
...     print 'a1 is ', a1
...     a2=yield 2
...     print 'a2 is ', a2
...     a3=yield 3
...     print 'a3 is ', a3
...
View Code

在你调用 b=foo()会生成一个生成器,接下来你调用b.next()会执行到第一个yield 处并暂停

>>> b=foo()
>>> b.next()
step 1
1
View Code

接下来就是yield 跟以前不同的地方了。 我们知道yield现在是表达式了。a1=yield 1不但会打印出一个1,还应该返回一个值给a1。 那么这个值是什么呢? 这个值是一个叫send的函数传递进去的。 新版本的python有了一个send函数,这个函数的作用是接受一个参数,并把这个参数当做是yield 表达式的当前返回值。 所以我们接下来的调用是这样的。

>>> b.send('two')
a1 is  two
2
View Code

我们调用b.send('two')。 send这时候会把two当做当时yield的返回值 传递给a1.然后生成器继续往下走走到第二个yield 表达式处。 现在你明白send的用法了吧。 ok 其实next()和send(None)是等价的,所以我们可以理解为next()就是发送一个None给当时的yield当做返回值然后继续往下执行,那么我们看一下是不是这样的

>>> b.next()
a2 is  None
3
View Code

正是我们所期望的