行事为人要端正,好像行在白昼。不可荒宴醉酒,不可好色邪荡,不可争竞嫉妒。总要披戴主耶稣基督,不要为肉体安排,去放纵私欲。

Let us live decently as in the daytime, not in carousing and drunkenness, not in sexual immorality and sensuality, not in discord and jealousy. Instead, put on the Lord Jesus Christ, and make no provision for the flesh to arouse its desires.(ROMANS 13:13-14)

函数(4)

再理解函数

如果把对函数的理解停留在此前的层面,还没有深入到函数的内涵,或者说只能做一些简单的事情,也可能是面临负责问题的时候不得不用冗长的代码解决。

所以,还要对函数进行深入探究。

递归

什么是递归?

递归,见递归.

这是对“递归”最精简的定义。还有故事类型的定义.

从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事。故事是什么呢?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事。故事是什么呢?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事。故事是什么呢?……””

如果用上面的做递归的定义,总感觉有点调侃,来个严肃的(选自维基百科):

递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。

最典型的递归例子之一是斐波那契数列,虽然前面用迭代的方式实现了它,但是那种方法在理解上不很直接。如果忘记了这个数列的定义,可以回到《练习》中查看。

根据斐波那契数列的定义,可以直接写成这样的斐波那契数列递归函数。

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

def fib(n):
    """
    This is Fibonacci by Recursion.
    """
    if n==0:
        return 0
    elif n==1:
        return 1
    else:
        return fib(n-1) + fib(n-2)

if __name__ == "__main__":
    f = fib(10)
    print f    #Python 3: print(f)

把上述代码保存。这个代码的意图是要得到n=10的值。运行之:

$ python 20401.py
55

fib(n-1) + fib(n-2)就是又调用了这个函数自己,实现递归。

为了明确递归的过程,下面走一个计算过程(考虑到次数不能太多,就让n=3)

  1. n=3,fib(3),自然要走return fib(3-1) + fib(3-2)分支
  2. 先看fib(3-1),即fib(2),也要走else分支,于是计算fib(2-1) + fib(2-2)
  3. fib(2-1)即fib(1),在函数中就要走elif分支,返回1,即fib(2-1)=1。同理,容易得到fib(2-2)=0。将这两个值返回到上面一步。得到fib(3-1)=1+0=1
  4. 再计算fib(3-2),就简单了一些,返回的值是1,即fib(3-2)=1
  5. 最后计算第一步中的结果:fib(3-1) + fib(3-2) = 1 + 1 = 2,将计算结果2作为返回值

从而得到fib(3)的结果是2。

从上面的过程中可以看出,每个递归的过程,都是向着最初的已知条件a0=0,a1=1方向挺近一步,直到通过这个最底层的条件得到结果,然后再一层一层向上回馈计算结果。

其实,上面的代码有一个问题。因为a0=0,a1=1是已知的了,不需要每次都判断一边。所以,还可以优化一下。优化的基本方案就是初始化最初的两个值。

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

"""
the better Fibonacci
"""
meno = {0:0, 1:1}    

def fib(n):
    if not n in meno:    
        meno[n] = fib(n-1) + fib(n-2)
    return meno[n]

if __name__ == "__main__":
    f = fib(10)
    print f        #Python: print(f)

#运行结果
$ python 20402.py 
55

以上实现了递归,但是,至少在Python中,递归要慎重使用。在一般情况下,递归是能够被迭代或者循环替代的,而且后者的效率常常比递归要高。所以,我个人的建议是,对使用递归要考虑周密,不小心就永远运行下去了。

传递函数

前面已经多次提到函数也是对象。

对于函数的参数,我们也做了一些探究。通过参数,可以将数字、字符串、列表等等那些已经学习过的Python中默认类型的对象以引用的方式传入函数——也可以传入以后要学习过的自定义类型的对象引用。

阅读了上面两句话,你是否有一个疑惑?都是对象,函数对象的引用能不能作为参数传给函数呢?

看这样一个举例:

>>> def bar():
    print "I am in bar()"

>>> def foo(func):
    func()

这里定义了两个函数,bar()就是我们熟悉的函数;而foo() 则有些许变化,其参数要求是一个函数,否则函数体内的代码块无法执行func(),因为这就是调用一个函数。

所以,要这样来调用foo()函数。

>>> foo(bar)
I am in bar()

下面的例子,是不是可以算一个小的应用呢?

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

def convert(func, seq):
    return [func(i) for i in seq]

if __name__ == "__main__":
    myseq = (111, 3.14, -9.21)
    r = convert(str, myseq)
    print r    #Python 3: print(r)

这个例子或者类似的,常常被作为“传递函数”的例子。在r = convert(str, myseq)里面,str是实现字符串转化的函数str()的名字。

你当然也可以自己编写一个函数,替换str

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

def convert(func, seq):
    return [func(i) for i in seq]

def num(n):
    if n%2 == 0:
        return n**n
    else:
        return n*n

if __name__ == "__main__":
    myseq = (3, 4, 5)
    r = convert(num, myseq)
    print r    #Python 3: print(r)

在这个例子中,我写了一个num(n)函数,然后在r = convert(num, myseq)中使用这个函数的名字num。其实跟前面的举例类似,只是为了让读者更深刻理解所谓“传函数”,使用的是函数名字,不是调用函数——调用函数使用num()的式样。

嵌套函数

函数不仅可以作为对象传递,还能在函数里面嵌套一个函数。例如:

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

def foo():
    def bar():
        print "bar() is running"        #Python 3用户请修改为print()函数,下同,从略
    print "foo() is running"

foo()        #调用函数

上面的代码中,在函数foo()里面定义了函数bar(),这就是嵌套函数,而bar()则称为foo()的内嵌函数,因为它在foo()的里面定义的。

如果调用foo()函数,会得到如下结果:

foo() is running

这说明,在上面的调用方式和内嵌函数写法中,bar()根本就没有被调用,或者说函数foo()并没有按照从上到下的顺序依次执行其里面的代码。

要想让bar()这个内嵌函数得到执行,就要在foo()函数里面显示地调用它,比如:

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

def foo():
    def bar():
        print "bar() is running"
    bar()                    #显示调用内嵌函数
    print "foo() is running"

foo()

这样的运行结果就是:

bar() is running
foo() is running

如果我单独调用定义的内嵌函数,是不是可行呢?调用方式就是把上面代码中调用foo(),修改为调用bar(),然后运行,显示结果报错信息NameError: name 'bar' is not defined

显然这样调用是不行的。因为bar()函数是定义在foo()里面的函数,它生效的范围仅局限在foo()函数体之内,也就是它的作用域是foo()范围。既然如此,bar()在使用变量的时候也会受到foo()的拘束了。

def foo():
    a = 1
    def bar():
        b = a + 1
        print "b=",b    #Python 3的用户请使用print()
    bar()
    print "a=",a

foo()
#output:
#b= 2
#a= 1

在函数bar()之外但在foo()之内定义了a = 1,在bar()中能够被顺利调用。这个关系不难理解,可是如果遇到下面的,就迷茫了。

def foo():
    a = 1
    def bar():
        a = a + 1        #修改之处
        print "bar()a=",a
    bar()
    print "foo()a=",a

foo()

如果运行这段程序,是会报错的。重要的报错信息是UnboundLocalError: local variable 'a' referenced before assignment。观察bar()里面,使用了变量a,按照该表达式,Python解析器认定该变量应是在bar()内部建立的,而不是引用的外部对象。所以就报错了。

在Python 3中,你可以使用nonlocal关键词,如下演示。

def foo():
    a = 1
    def bar():
        nonlocal a
        a = a + 1
        print("bar()a=",a)
    bar()
    print("foo()a=",a)

foo()
#output
#bar()a= 2
#foo()a= 2

以上说明了嵌套函数的原理,在编程实践中,怎么用呢?

def maker(n):
    def action(x):
        return x ** n
    return action

maker()函数中,return action返回的是action()函数对象。

f = maker(2)
print f
m = f(3)
print m

f所引用的对象是一个函数对象——action()函数对象,print f就是打印这个函数对象的信息。观察执行结果,对比上述代码,会有所感悟的。

<function action at 0x02A39970>
9

从这个角度看,嵌套函数,其实能够制作一个动态的函数对象——action。这个话题延伸下去,就是所谓的“闭包”,关于“闭包”的问题,我会在后面跟读者聊一聊。

初识装饰器

至此,我们已经明确,函数——是对象——能够被传递,也能够嵌套。重复一个简单的举例,目的是抛砖引玉。

def foo(fun):
    def wrap():
        print "start"        #Python 3用户请自行更换为print(),下同,从略
        fun()
        print "end"
        print fun.__name__
    return wrap    

def bar():
    print "I am in bar()"

foo()的参数是一个函数,如果我们这样调用此函数:

f = foo(bar)
f()
#output:
#start
#I am in bar()
#end
#bar

这就是向foo()传递了函数对象bar——你已经熟悉的传递函数。对于这个问题,我们可以换一个写法——仅仅是换一个写法。

def foo(fun):
    def wrap():
        print "start"
        fun()
        print "end"
        print fun.__name__
    return wrap

@foo                  #增加的内容
def bar():
    print "I am in bar()"

@foo是一个看起来很奇怪的东西,人们常常把类似这种东西叫做语法糖。

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。(源自《维基百科》)

如果用上面的方式,我们可以这样执行程序:

bar()

结果是:

start
I am in bar()
end
bar

以上,就是所谓的装饰器及其应用,foo()是装饰器函数,使用@foo来装饰bar()函数。

装饰器本身是一个函数,将被装饰的类(后面会介绍这种东西)或者函数当作参数传递给装饰器函数,如上面所演示的那样。

关于装饰器,后面我们还会遇到。这里是刚刚认识,就如同跟人交往一样,初次见面,姑且简单了解,以后日久天长。


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

如果你认为有必要打赏我,请通过支付宝:[email protected],不胜感激。

results matching ""

    No results matching ""