1.前言
这章的内容可能有点多,也有可能有些难以理解。这一章,我们会介绍到:抽象、函数定义、参数、作用域、递归、函数式编程。其中有的是新的概念,有的是原来的旧的概念。不论是新的还是旧的,都需要我们去及时关注和整理,也需要我们去掌握。难理解的概念我会说的很详细,请大家不要嫌麻烦,认真理解。为后面重要内容的学习奠定一定的基础。下面就开始我们的学习!
2.抽象与结构
我们首先来看一个例子,是我们熟悉的斐波那契数列的程序实现:
#Fibonacci number #author youzi #coding:utf-8 fibs=[0,1] num=int(raw_input("How many Fibonacci number do you want: ")) for i in range(num-2): fibs.append(fibs[-2]+fibs[-1]) print fibs
在这里,我稍微地设计的人性化了一点。用户可以输入自己的数字作为动态范围的长度使用,实现方式当然是采用我们熟悉的for循环,一目了然。再来看下面这样的设计:
num=raw_input('How many numbers do you want? ') print fibs(num)
这种程序的设计更加抽象一些,但是很清楚。读入数值,然后打印出结果。事实上计算斐波那契数列是由一种抽象的方式来完成的:只需要告诉计算机去做就好,不同特别说明应该怎么做,名为fibs的函数将会被创建,然后在需要的地方调用它就可以咯。如果这个函数要被调用很多次,这么做的话会很省力!
抽象的这种形式可以让人们很轻松的读懂计算机程序,有时候你只需要关注关键的几步就能得到你想要的结果,而不是里面实现的每一步我都需要去额外的特别关注,那岂不是很麻烦,失去了抽象设计的意义。
3.创建函数
在前面的入门基础第三课里面,我们简单的介绍了一下函数的内容,函数可以被创建,也可以被调用。我们也讲了函数调用的方法,下面再来看一些用法。
还是前面的斐波那契数列创建例子,我们将它封装到一个具体的函数里面:
def fibs(num): result=[0,1] for i in range(num-2): result.append(result[-2]+result[-1]) return result print fibs(10)
有了这样的函数以后,我们再想生成斐波那契数列的时候就可以直接调用fibs函数,像fibs(10)、fibs(15)这样的调用就可以叨叨我们的目的,这也是将具体的实现过程封装到了函数里面,也是抽象形式的一种体现。需要注意的是这里的函数名字和里面的变量都是随自己取的,但是函数结尾的return语句是不可或缺的,用来从函数中返回值的。
3.1文档化函数
def square(x): 'Calculates the square of the number x.' return x*x
然后你就可以去调用它了,文档字符串可以按如下方式访问:
>>>square.__doc__ 'Calculates the square of the number x.'
如果你觉得这样方式有些麻烦,还有个很好用的内建函数help,是很有用的。在shell里面去使用它,就可以得到关于函数,包括它的文档字符串的信息。
3.2参数
3.2.1参数从何而来
函数被创建好以后,传入的值是从哪里来的呢?一般来讲不用太关注这些问题,能保证函数在被提供给可接受参数的时候正常工作就行,参数的传入要是不正确的话会导致程序失败。
函数通过它的参数获取到值,那么这些值我们可以改变吗?参数只是变量而已,你在函数内为参数赋予新的值不会改变外部任何变量的值。来看下面这些例子:
>>> def try_to_change(n):n='Mr. Gumby' ... >>> name='Ms .Entity' >>> try_to_change(name) >>> name 'Ms .Entity'
再来看看具体的工作方式:
>>> name='Mrs. Entity' >>> n=name >>> n='Mr. Gumby' >>> name 'Mrs. Entity'
结果是显而易见的,我们只是将name参数的值传给了n,并没有改变name参数的值,当在函数内部把参数重新绑定的时候,函数外的变量不会受到影响。
前面说过,字符串、数字和元组是不能被修改的,我们只能用新的值去覆盖它们。但是除了上面说过的,不要忘了还有列表,当我们利用列表作为参数的时候回发生什么,我们来看看:
>>> def change(n): n[0]='Mr.Gumby' ... >>> names=['youzi','Alice'] >>> change(names) >>> names ['Mr.Gumby', 'Alice']
哎呦,怎么和前面说的不一样,参数被改变了。这就是这个例子和前面说的例子重要的区别。我们来看看它的具体实现方式:
>>> names=['youzi','Alice'] >>> n=names >>> n[0]='Mr.Gumby' >>> names ['Mr.Gumby', 'Alice']
当两个变量同时指向一个列表的时候,它们确实是同时引用一个列表。如果要避免这种情况,可以复制一个列表的副本。来看看:
>>> names ['Mr.Gumby', 'Alice'] >>> n=names[:] #制作一个names的副本 >>> n is names #同一性 False >>> n==names #值相等 True
在这样的程序执行完以后,n和names是完全独立的两个列表,但是两个的值是相等的。参数n相当于是names的一个副本,如果现在去和上面一样修改n就不会影响到names。具体的实现大家可以仿照上面自己实现。
使用函数改变数据结构是这一种将程序抽象化的好方法,抽象的要点就是隐藏更新时候繁琐的细节,这个过程可以用函数来实现,来看看下面的例子,我们利用这样函数来初始化数据结构。
def init(data): data['first']={} data['middle']={} data['last']={}
然后利用这样方式来使用它:
>>>storage={} >>>init(storage) >>>storage {'middle':{}, 'last':{}, 'first':{}}
我们可以很清楚的看到,函数承包了初始化数据结构的工作,让代码更易读。
假如我们碰到不可变的参数该怎么办?不好意思咩,没有办法,这个时候你应该从函数中返回所有你需要的值。但是如果你真的想改变一下参数,可以用一下下面的小技巧。
>>> def inc(x): x[0]=x[0]+1 ... >>> foo=[10] >>> inc(foo) >>> foo [11]
看咯,这样就能返回新的值,就是改变了参数。
3.2.2关键字参数和位置参数
目前为止我们使用过的参数都是位置参数,因为它们的位置很重要,事实上比它们的名字更加重要。还有一类是关键字参数,我们可以使用关键字参数为函数提供默认值,很方便。位置参数和关键字参数是可以联合使用的,但是使用的时候把位置参数放在最前面就可以了,如果不这样做,解释器就不会知道它们到底谁是谁,也就是不知道它们应该处的位置在哪里。我们直接来看一些例子,会更加明了。
>>> def hello_1(greeting,name): print '%s, %s!' %(greeting,name) #一般一号问候函数 ... >>> def hello_2(greeting,name): print '%s, %s!' %(name,greeting) #一般二号问候函数 ... >>> hello_1('Hello','world') #函数测试输出 Hello, world! >>> hello_2('Hello','world') #函数测试输出 world, Hello! >>> hello_1(name='world',greeting='Hello') #关键字参数输出 Hello, world! >>> hello_2(greeting='Hello',name='world') #不同的关键字参数输出 world, Hello! >>> def hello_3(greeting='Hello',name='world'): print '%s, %s' %(greeting,name) #关键字参数提供默认值的三号问候函数 ... >>> hello_3() #不给任何参数的默认参数输出 Hello, world >>> hello_3('Greetings') #只含位置参数的输出 Greetings, world >>> hello_3('Greetings','universe') #位置参数输出 Greetings, universe >>> hello_3(name='Gumby') #只提供name参数,默认greeting参数的输出 Hello, Gumby
3.2.3参数收集
有时候让用户一次性提供任意数量的参数是很有用的,我们来看一看例子就会明了。
>>> def print_params(*params): print params #利用*来进行参数收集 ... >>> print_params('Testing') ('Testing',) >>> print_params(1,2,3) (1, 2, 3) >>> def print_params3(**params): print params #利用**来进行参数收集 ... >>> print_params3(x=1,y=2,z=3) {'y': 2, 'x': 1, 'z': 3}对比上面两种参数收集的方式和最后测试打印结果的区别,你应该不难看出两者的差别。所以,没有问题。星号的意思就将所有的位置参数放置在同一个元组里面,可以说是将这些值收集起来,然后在需要的地方使用。而两个星号是用来处理关键字的收集操作。
3.2.4参数收集的逆过程
我们说*和**的方法可以用来收集参数,那么能不能用来进行逆过程的操作呢?逆过程我们说是这样的--不是收集参数,而是分配它们在“另一端”。不是在定义的时候使用,而是在调用的时候使用。我们来看看:
>>> def add(x,y): return x+y ... >>> params=(1,2) >>> add(*params) 3 >>> add(params) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: add() takes exactly 2 arguments (1 given)
上面这个例子还不明显的话,再来看:
>>> params={'name':'Youzi','greeting':'Hello'} >>> hello_3(**params) Hello, Youzi
4.参数使用练习
#coding:utf-8 #author:youzi def story(**kwds): return 'Once upon a time, there was a %(job)s called %(name)s. ' %kwds def power(x,y,*others): if others: print 'Received reduntant parameters: ', others return pow(x,y) def interval(start,stop=None,step=1): 'Imitates range() for step>0' if stop is None: start, stop=0,start result=[] i=start while i<stop: result.append(i) i+=step return result print story(job='king',name='Gumby') print story(name='youzi',job='brave king') print pow(2,3) print power(2,3,'Hello,World') print interval(10) print interval(1,5) print interval(3,12,4) print power(*interval(3,7))上面的程序都是很好理解的,自己动手实践,得出结果详细分析。你会有所收获。