6.1 懒惰即美德
假设我们编写了一小段代码来计算斐波那契数列:
fibs = [0,1]
for i in range(8):
fibs.append(fibs[-2] + fibs[-1])
fibs = [0,1]
num = input(‘How many Fibonacci numbers do you want?‘)
for i in range(num-2)
fibs.append(fibs[-2] + fibs[-1])
print fibs
抽象后
num = input(‘How many numbers do you want?‘)
print fibs(num)
6.2 抽象和结构
page = download_page()
freqs = computer_frequencies(page)
for word,freq in freqs:
print word,freq
6.3 创建函数
函数是可以调用,它执行某种行为并且返回一个值。一般来说,内建的callable函数可以用来判断函数是否可调用:
>>> import math
>>> x = 1
>>> y = math.sqrt
>>> callable(x)
False
>>> callable(y)
True
>>>
def hello(name):
return ‘hello,‘ + name + ‘!‘
创建一个名为hello的函数,他可以返回一个将输入的参数作为名字的问候语。可以像使用内建函数一样使用它:
>>>print hello(‘world’)
hello,world
斐波那契函数
def fibs(num)
resule = [0,1]
for i in range(num-2):
result.append(result[-2] + result[-1])
return result
6.3.1 记录函数
如果想要给函数写文档,让后面使用该函数人能理解的话,可以加入注释。另外一个方式就是直接写上字符串。如果在函数的开头写下字符串,他就会作为函数的一部分进行存储,这称为文档字符串。
def square(x):
‘Calculates the square of the number x.‘
return x*x
文档字符串可以按如下方式访问:
>>>square._doc_
‘Calculates the square of the number x.‘
_doc_是函数属性
内建的help函数是非常有用的。在交互解释器中使用它,就可以得到关于函数,包括它的文档字符串的信息
>>>help(square)
6.3.2 并非真正函数的函数
数学意义上的函数,总在计算其参数后返回点什么。python的有些函数却并不返回任何东西。在其他语言中,这类函数可能有其他名字。但是python的函数就是函数,即便它从学术上并不是函数。没有return语句,或者虽有return语句但return后边没有跟任何值得函数不返回值:
def test():
print ‘this is printed‘
return 这里的return函数只起到结束函数的作用
print ‘this is not‘
>>>x = test()
This is printed
可以看到,第2个print语句被跳过了,但是如果test不返回任何值,那么x又引用什么呢?
>>>x
>>>
没东西,在仔细看看
>>>print x
None
所以所有的函数都返回了东西,:当不需要他们返回值得时候,它们就返回None。
千万不要被默认行为所迷惑。如果在if语句内返回值,那么要确保其他分支也有返回值,这样一来当调用者期待一个序列的时候,就不会意外的返回None。
6.4 参数
6.4.1 值从哪里来
写在def语句中函数名后面的变量通常叫做函数的形式参数,而调用函数的时提供的值是实际参数,或者成为参数。
6.4.2 我能改变参数么
在函数内内为参数赋予新值不会改变外部任何变量的值:
>>> def try_to_change(n): n = ‘Mr,Gumby‘
...
>>> name = ‘Mrs,Entity‘
>>> try_to_change(name)
>>> name
‘Mrs,Entity‘
>>>
在try_to_change内,参数n获得了新值,但是它没有影响到name变量。n实际上是个完全不同的变量,具体的工作方式类似于下面这样:
>>> name = ‘Mrs,Entity‘
>>> n = name #这句的作用基本上等于传参数
>>> n = ‘Mr.Gumby‘ #在函数内部完成的
>>> name
‘Mrs,Entity‘
>>>
结果是显而易见的,当变量n改变的时候,变量name不变。同样,当在函数内部把参数重绑定的时候,函数外的变量不会受到影响。
参数存储在局部作用域。
字符串是不可变的,即无法被修改。
>>> def change(n): n[0] = ‘Mr.Gumby‘
...
>>> names = [‘Mrs.Entity‘,‘Mrs.Thing‘]
>>> change(names)
>>> names
[‘Mr.Gumby‘, ‘Mrs.Thing‘]
>>>
本例中,参数被改变了。这就是本例和前面例子中至关重要的区别。前面的例子中,局部变量被赋予了新值,但是这个例子中变量names所绑定的列表的确改变了。
>>> names = [‘Mrs.Entity‘,‘Mrs.Thing‘]
>>> n = names
>>> n[0]=‘Mr.Gumby‘
>>> names
[‘Mr.Gumby‘, ‘Mrs.Thing‘]
>>>
这类情况在前面已经出现了多次。当两个变量同时引用一个列表的时候,他们的确是同时引用一个列表。如果想避免出现这种情况,可以复制一个列表的副本。当在序列中做切片的时候,返回的切片总是一个副本。因此,如果你复制了整个列表的切片,将会得到一个副本:
>>> names = [‘Mrs.Entity‘,‘Mrs.Thing‘]
>>> n = names[:]
现在n和name包含两个独立的列表,其值相等:
>>> n is names
False
>>> n == names
True
>>>
如果现在改变n,则不会影响到names:
>>> n[0]=‘Mr.Gumby‘
>>> n
[‘Mr.Gumby‘, ‘Mrs.Thing‘]
>>> names
[‘Mrs.Entity‘, ‘Mrs.Thing‘]
>>>
再用change试一下:
>>> change(names[:])
>>> names
[‘Mrs.Entity‘, ‘Mrs.Thing‘]
>>>
现在参数n包含一个副本,而原始的列表是安全的。
函数的局部名称-----包括参数在内-----并不和外面的函数名称冲突。
1.为什么我想要修改参数
使用函数改变数据结构是将程序抽象化的好方法。假设需要编号一个存储名字并且能用名字、中间名或姓查找联系人的程序,可以使用下面的数据结构:
storage = {}
storage[‘first‘] = {}
storage[‘middle‘] = {}
storage[‘last‘] = {}
storage这个数据结构的存储方式是带有3个键“first”“middle”和“last”的字典。每个键下面都又存储一个字典。子字典中,可以使用名字作为键,插入联系人列表作为值。比如要把我自己的名字加入这个数据结构,可以像下面这么做:
>>> me = ‘Magnus Lie Hetland‘
>>> storage[‘first‘][‘Magnus‘] = [me]
>>> storage[‘middle‘][‘Lie‘] = [me]
>>> storage[‘last‘][‘Hetland‘] = [me]
每个键下面都存储了一个以人名组成的列表。本例中,列表中只有我。
现在如果想得到所有注册的中间名为Lie的人,可以像下面这么做:
>>> storage[‘middle‘][‘Lie‘]
[‘Magnus Lie Hetland‘]
>>>
将人名加到列表中的步骤有点枯燥乏味,尤其是要加入很多姓名相同的人时,因为需要扩展已经存储了那些名字的列表。例如,下面加入我姐姐的名字,而且假设不知道数据库中已经存储了什么:
>>> storage[‘first‘].setdefault(‘Anne‘,[]).append(my_sister)
>>> storage[‘middle‘].setdefault(‘Lie‘,[]).append(my_sister)
>>> storage[‘last‘].setdefault(‘Hetland‘,[]).append(my_sister)
>>> storage[‘first‘][‘Anne‘]
[‘Anne Lie Hetland‘]
>>> storage[‘middle‘][‘Lie‘]
[‘Magnus Lie Hetland‘, ‘Anne Lie Hetland‘]
>>>
如果要写个大程序来这样更新列表,那么很显然程序很快就会变得臃肿且笨拙不堪了。
抽象的要点就是隐藏更新时的繁琐的细节,这个过程可以用函数实现。下面的例子就是初始化数据结构的函数:
def init(data):
data[‘first‘] = {}
data[‘middle‘] = {}
data[‘last‘] = {}
上面的代码只是把初始化语句放到了函数中:
>>> storage = {}
>>> init(storage)
>>> storage
{‘middle‘: {}, ‘last‘: {}, ‘first‘: {}}
可以看到,函数包办了初始化的工作。
字典的键并没有具体的顺序。
在编写存储名字的函数前,先写个获得名字的函数:
def lookup(data,label,name):
return data[label].get(name)
标签(比如middle)以及名字(比如Lie)可以作为参数提供给lookup函数使用,这样会获得包含全名的列表。换句话说,如果我的名字已经存储了,可以像下面这样做:
>>> lookup(storage,‘middle‘,‘Lie‘)
注意,返回的列表和存储在数据结构中的列表是相同的,所以如果列表被修改了,那么也会影响数据结构(没有查询到人的时候就问题不大了,因为函数返回的是None)
def store(data,full_name):
names = full_name.split()
if len(names) == 2: names.insert(1,‘‘)
labels = ‘first‘,‘middle‘,‘last‘
for label,name in zip(labels,names):
people = lookup(data,label,name)
if people:
people.append(full_name)
else:
data[label][name] = [full_name]
store函数执行以下步骤:
1.使用参数data和full_name进入函数,这两个函数被设置为函数在外部获得的一些值。
2.通过拆分full_name,得到一个叫names 的列表
3.如果names的长度为2(只有首名和末名),那么插入一个空字符串作为中间名。
4.将字符串“first”、“middle”和“last”作为元组存储在labels中(也可以使用列表:这里只为了方便而去掉括号)
5.使用zip函数联合标签和名字,对于每一个(label,name)对,进行以下处理:
获得属于给定标签和名字的列表。
将full_name添加到列表中,或者插入一个需要的新列表。
来试用一下刚刚实现的程序:
>>> Mynames={}
>>> init(Mynames)
>>> store(Mynames,‘Magnus Lie Hetland‘)
>>> lookup(Mynames,‘middle‘,‘Lie‘)
[‘Magnus Lie Hetland’]
好像可以工作:
>>> store(Mynames,‘Robin Hood‘)
>>> store(Mynames,‘Robin Locksley‘)
>>> lookup(Mynames,‘first‘,‘Robin‘)
[‘Robin Hood’,‘Robin Locksley’]
>>> store(Mynames,‘Mr.Gumby‘)
>>> lookup(Mynames,‘middle‘,‘‘)
[‘Robin Hood’,‘Robin Locksley’,‘Mr。Gumby’]
可以看到,如果某些人的名字,中间名或姓相同,那么结果中会包含所有这些人的信息。
2.如果我的参数不可变
函数只能修改参数对象本身。但是如果你得参数不可变----比如数字---又该怎么办? 这是没有办法的,这时候你应该从函数中返回你需要的值(如果值多于一个话就以元组形式返回)。例如,将变量的数值增1的函数可以这样写:
>>> def inc(x):return x+1
...
>>> foo=10
>>> foo = inc(foo)
>>> foo
11
>>>
如果真的想改变参数,那么可以使用一点小技巧,即将值放置在列表中:
>>> def inc(x):x[0] = x[0] + 1
...
>>> foo = [10]
>>> inc(foo)
>>> foo
[11]
>>>
这样就只返回新值,代码看起来也比较清晰。
6.4.3关键字参数和默认值
目前为止我们所使用的参数都叫做位置参数,因为它们的位置很重要----事实上比它们的名字更加重要。本节中引入的这个功能可以回避为止问题,当你慢慢习惯使用这个功能以后,就会发现程序规模越大,它们的作用也就越大。
考虑下面的两个函数:
def hello_1(greeting,name):
print ‘%s,%s!‘ % (greeting,name)
def hello_2(greeting,name):
print ‘%s,%s!‘ % (greeting,name)
两个代码所实现的是完全一样的功能,只是参数名字反过来了:
>>>hello_1(‘hello‘,‘world‘)
hello,world
>>>hello_2(‘hello‘,‘world‘)
hello,world
有些时候,参数的顺序是很难记住的,为了让事情简单些,可以提供参数的名字:
>>>hello_1(greeting=‘hello’,name=‘world’)
hello,world!
这样一来,顺序就完全没影响了:
>>>hello_1(name=‘world’,greeting=‘hello’)
hello,world!
但参数名和值一定要对应:
>>>hello_2(greeting=‘hello‘,name=‘world‘)
world,hello
这类使用参数提供的参数叫做关键字参数。它的作用在于可以明确每个参数的作用,也就避免了下面这样的奇怪的函数调用
>>>storre(‘Mr. Brainsample‘,10,20,13,5)
可以使用
>>>store(patient=‘Mr. Brainsample‘,hour=10,minute=20,day=13,mouth=5)
尽管这么做打得字多了些,但是很显然,每个参数的含义变得更加清晰,而且就算弄乱了参数的顺序,对程序的功能也没有任何影响。
关键字参数最厉害的地方在于可以在函数中给函数提供默认值:
def hello_3(greeting=‘hello‘,name=‘world‘):
print ‘%s,%s!‘ % (greeting,name)
当参数具有默认值的时候,调用的时候就不用提供参数了!可以不提供,提供一些或提供所有的参数:
>>>hello_3()
hello,world
>>>hello_3(‘Greetings‘)
Greetings,world
>>>hello_3(‘Greeting‘,‘universe‘)
Greeting,universe
可以看到,位置参数这个方法不错-----除了在提供名字的时候就要提供问候语。但是如果只想提供name参数,而让greeting使用默认值该怎么办呢?
>>>hello_3(name=‘Gumby‘)
hello,Gumby
位置和关键字参数是可以联合使用的。把位置参数放置在前面就可以了,如果不这样做,解释器会不知道它们到底是谁(也就是它们应该处的位置)。
除非完全清楚程序的功能和参数的意义,否则应该避免混合使用位置参数和关键字参数。一般来说,只有在强制要求的参数个数比可修改的具有默认值的参数个数少的时候,才使用上面提到的参数书写方法。
例如,hello函数可能需要名字作为参数,但是也允许用户自定义名字,问候语和标点:
def hello_4(name,greeting=‘hello’,punctuation=‘!’):
print ‘%s,%s%s’ % (greeting,name,punctuation)
函数的调用方式很多:
>>>hello_4(‘Mars‘)
hello,Mars!
>>>hello_4(‘Mars‘,‘Howdy‘)
Howdy,Mars!
>>>hello_4(‘Mars‘,‘Howdy‘,‘...‘)
Howdy,Mars...
>>>hello_4(‘Mars‘,punctuation=‘.‘)
hello,Mars.
>>>hello_4()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
如果为name也赋予默认值,那么最后一个语句就不会产生异常
6.4.4 收集参数
有些时候让用户提供任意数量的参数是很有用的,比如在名字存储过程中,用户每次只能存一个名字。如果能像下面这样存储多个名字就更好了:
>>>store(data,name1,name2,name3)
用户可以给函数提供任意多的参数,实现起来也不难。
试着像下面这样定义函数:
def print_params(*params):
print params
这里我只指定了一个参数,但是前面加上了个星号。
>>>print_params(‘Testing‘)
(‘Testing‘,)
可以看到,结果作为元组打印出来,因为里面有个逗号。所以在参数前使用星号就能打印出元组?那么Params中使用多个参数看看会发生什么:
>>>print_params(1,2,3)
(1,2,3)
参数前的星号将所有值放置在同一个元组中,可以说是将这些值收集起来,然后使用。
def print_params_2(title,*params):
print title
print params
>>>print_params_2(‘Params:‘,1,2,3)
Params:
(1,2,3)
没问题,所以星号的意思是收集其余的位置参数。如果不提供任何供收集的元素,params就是个空元组:
>>>print_params_2(‘Nothing:‘)
Nothing:
()
>>>print_params_2(‘Hmm...‘,something=42)
会报错
def print_params_3(**params):
print params
至少解释器没有发牢骚,调用下
>>>print_params_3(x=1,y=2,z=3)
{‘z‘:3,‘x‘:1,‘y‘:2}
返回的是字典而不是元组。
def print_params_4(x,y,z=3,*pospar,**keypar):
print x,y,z
print pospar
print keypar
和我们期望的结果别无二致
>>>print_params_4(1,2,3,5,6,7,foo=1,bar=2)
123
(5,6,7)
{‘foo‘:1,‘bar‘:2}
>>>print_params_4(1,2)
1 2 3
()
{}
怎么实现多个名字同时存储。
def store(data,*full_names):
for full_name in full_names:
names = full_name.split()
if len(names) == 2:names.insert(1,‘‘)
labels = ‘first‘,‘middle‘,‘last‘
for label,name in zip(labels,names):
people = lookup(data,label,name)
if people:
people.append(full_name)
else:
data[label][name] = [full_name]
>>> d = {}
>>> init(d)
>>> store(d,‘Han Solo‘)
但是现在可以这样用
>>>store(d,‘Luke Skywalker‘,‘Anakin Skywalker‘)
>>>lookup(d,‘last‘,‘Skywalker‘)
[‘Luke Skywalker‘,‘Anakin Skywalker‘]
6.4.5 反转过程
如何将参数收集为元组和字典,但是事实上,如果使用*和**的话,也可以执行相反的操作。那么函数收集的逆过程是什么样?
def add(x,y): return x+y
比如说有包含由两个要想加的数字组成的元组:
params = (1,2)
这个过程或多或少有点像我们上一节中介绍的逆过程。不是要收集参数,而是分配它们在“另一端”。使用*运算符就简单了------不过是在调用而不是在定义时使用:
>>>add(*params)
3
对于参数列表来说工作正常,只要扩展的部分是最新的就可以。可以使用同样的技术来处理字典-----使用双星号运算符。假设之前定义了hello_3,那么可以这样使用:
>>>params = {‘name‘:‘Sir Robin‘,‘greeting‘:‘well met‘}
>>>hello_3(**params)
Well met,Sir Robin!
在定义或者调用函数时使用星号(或者双星号)仅传递元组或字典,所以可能没遇到什么麻烦:
>>>def with_stars(**kwds):
print kwds[‘name‘],‘is‘,kwds[‘age‘],‘years old‘
>>>def without_stars(kwds):
print kwds[‘name‘],‘is‘,kwds[‘age‘],‘years old‘
>>>args = {‘name‘:‘Mr. Gumby‘,‘age‘:42}
>>>with_stars(**args)
Mr. Gumby is 42 years old
>>>without_stars(args)
Mr. Gumby is 42 years old
可以看到,在with_stars中,我在定义和调用函数时都使用了星号。而在without_stars中两处都没用,但是得到了同样的效果。所以星号只在定义函数(允许使用不定数目的参数)或者调用(“分割”字典或者序列)时才有用。
使用拼接操作符“传递”参数很有用,因为这样一来就不用关心参数的个数之类的问题,例如:
def foo(x,y,z,m=0,n=0):
print x,y,z,m,n
def call_foo(*args,**kwds):
print "Calling foo!"
foo(*args,**kwds)
在调用超类的构造函数时这个方法尤其有用。
6.4.6 练习使用参数
有了这么多种提供和接受参数的方法,很容易犯晕吧!所以让我们把这些方法放在一起举个 例子。首先,我定义了一些函数:
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 redundant 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‘)
Once upon a time,there was a king called Gumby.
>>>print story(name=‘Sir Robin‘,job=‘brave knight‘)
Once upon a time,there was a brave knight called Sir Robin.
>>>params = {‘job‘: ‘language‘,‘name‘:‘Python‘}
>>>print story(**params)
Once upon a time,there was a language called Python.
>>>del params[‘job‘]
>>>print story(job=‘stroke of genius‘,**params)
Once upon a time,there was a stroke of genius called Python.
>>>power(2,3)
8
>>>power(3,2)
9
>>>power(y=3,x=2)
8
>>>params = (5,) * 2
>>>power(*params)
3125
>>>power(3,3,‘hello,world‘)
Received redundant parameters:(‘hello,world‘,)
27
>>>interval(10)
[0,1,2,3,4,5,6,7,8,9]
>>>interval(1,5)
[1,2,3,4]
>>>interval(3,12,4)
[3,7,11]
>>>power(*interval(3,7))
Received redundant parameters:(5,6)
81
6.5 作用域
内建的vars函数可以返回这个字典:
>>>x = 1
>>>scope = vars()
>>>scope[‘x‘]
1
>>>scope[‘x‘] += 1
>>>x
2
一般来说,vars所返回的字典是不能修改的,因为根据官方的Python文档的说法,结果是未定义的,换句话说,可能得不到想要的结果。
这类“不可见字典”叫做命名空间或者作用域。那么到底有多少个命名空间?除了全局作用域外,每个函数调用都会创建一个新的作用域:
>>>def foo():x = 42
>>>x = 1
>>>foo()
>>>x
1
这里的foo函数改变了变量x,但是在最后的时候,x并没有变。这是因为当调用foo的时候,新的命名空间就被创建了,它作用于foo内的代码块。赋值语句x=42只在内部作用域(局部命名空间)起作用,所以它并不影响外部作用域的x。函数内的变量被称为局部变量。参数的工作元素类似于局部变量,所以用全局变量的名字作为参数名并没有问题。
>>>def output(x):print x
>>> x = 1
>>>y = 2
>>>output(y)
2
但是如果需要在函数内部访问全局变量怎么办?而且只想读取变量的值(也就是说不想重绑定变量),一般来说是没有问题的:
>>>def combine(parameter):print parameter + external
>>>external = ‘berry‘
>>>combine(‘Shrub‘)
Shrubberry
像这样使用全局变量是诱发错误的引发原因。慎重使用全局变量。
屏蔽的问题
读取全局变量一般来说并不是问题,但是还是有个会出问题的事情。如果局部变量或者参数的名字和想要访问的全局变量名相同的话,就不能直接访问了。全局变量会被局部变量屏蔽。
如果的确需要的话,可以使用globals函数获取全局变量值,该函数的近亲是vars,take返回全局变量的字典(locals返回局部变量的字典)。例如,如果前例中有个叫做parameter的全局变量,那么就不能再combine函数内部访问该变量,因为你有一个与之同名的参数。必要时,能使用globals()[‘parameter‘]获取:
>>>def combine(parameter):
print parameter + globals()[‘parameter‘]
>>>parameter = ‘berry‘
>>>combine(Shrub)
Shrubberry
接下来讨论重绑定全局变量(使变量引用其他新值)。如果在函数内部将值赋予一个变量,它自动成为局部变量----除非告知Python将其声明为全局变量。那么怎么才能告诉Python这是一个全局变量呢?
>>>x=1
>>>def change_global()
global x
x = x+1
>>>change_global()
>>>x
2
嵌套作用域
Python的函数是可以嵌套的,也就是说可以将一个函数放在另一个里面。下面是一个例子 :
def foo()
def bar():
print "hello,world"
bar()
嵌套一般来说并不是那么有用,但它又一个很突出的应用,例如需要用一个函数“创建”另一个,也就意味着可以像下面这样书写函数:
def multiplier(factor):
def multiplyByFactor(number):
return number*factor
return multiplyByFactor
一个函数位于另外一个里面,外层函数返回里层函数。也就是说函数本身被返回了----但并没有被调用。重要的是返回的函数还可以访问它的定义所在的作用域,换句话说,它带着它的环境和相关的局部变量。
每次调用外层函数,它内部的函数都被重新绑定,factor变量每次都有一个新的值。由于Python的嵌套作用域,来自外部作用域的这个变量,稍后会被内层函数访问。例如:
>>>double = multiplier(2)
>>>double(5)
10
>>>triple = multiplier(3)
>>>triple(3)
9
>>>multiplier(5)(4)
20
类似multiplyByFactor函数存储于封闭作用域的行为叫做闭包
外部作用域的变量一般来说是不能进行重新绑定的。但是Python3.0中,nonlocal关键字被引入。它和global关键字的使用方式类似,可以让用户对外部作用域的变量进行赋值。
6.6 递归
递归的定义包括它们自身定义内容的引用。由于每个人对递归的掌握程度不同,它可能会让人大伤脑筋。
def recursion():
return recursion()
上述递归叫做无穷递归。有用的递归函数包含以下几部分:
当函数直接返回值时有基本实例
递归实例,包括一个或者多个问题最小部分的递归调用
这里的关键就是将问题分解为小部分,递归不能永远继续下去,因为它总是以最小可能性问题结束,而这些问题又存储在基本实例中。
所以让函数调用自身。但是怎么实现呢?
6.6.1 两个经典:阶乘和幂
首先,假设想要计算数n的阶乘。n的阶乘定义为n*(n-1)*(n-2)*。。。*1.很多数学应用中都会用到它。
def factorial(n):
result = n
for i in range(1,n):
result *= i
return result
这个方法可行而且很容易实现。它的过程主要是;首先将result赋到n上,然后result依次与1到n-1的数相乘,最后返回结果。 阶乘数学定义:
1的阶乘是1
大于1的数n的阶乘是n乘n-1的阶乘
可以看到,这个定义完全符合刚才所介绍的递归的两个条件。
现在考虑如何定义实现为函数。理解定义本身后,实现其实很简单:
def factorial(n):
if n==1:
return 1
else:
return n * factorial(n-1)
这就是定义的直接实现。只要记住函数调用factorial(n)是和调用factorial(n-1)不同的实体就行。
假设需要计算幂,就像内建的pow函数或者**运算符一样。可以用很多种方法定义一个数的幂。先看一个简单的例子:power(x,n) (x为n的幂次) 是x自乘n-1次的结果。所以power(2,3) 是2乘以自身3次:2*2*2=8
实现很简单:
def power(x,n):
result = 1
for i in range(n):
result *= x
return result
接下来该变成递归版本:
对于任意数字x来说,power(x,0)是1
对于任何大于0的数来说,power(x,n)是x乘以(x,n-1)的结果。
def pwoer(x,n):
if n == 0:
return 1
else:
return x * power(x,n-1)
6.6.2 另外一个经典:二元查找
def search(sequence,number,lower,upper):
if lower == upper:
assert number == sequence[upper]
return upper
else:
middle = (lower + upper) // 2
if number > sequence[middle]:
return search(sequence,number,middle+1,upper)
else:
return search(sequence,number,lower,middle)
完全符合定义。如果lower == upper,那么返回upper,也就是上限。注意,程序设计(断言)所查找的数字一定会被找到(number == sequence[upper])。如果没有到达基本实例的话,先找到middle,检查数字是在左边还是在右边,然后使用新的上下限继续调用递归过程。也可以将限制设为可选以方便用。只要在函数定义的开始部分加入下面的条件语句即可:
def search(sequence,number,lower=0,upper=None):
if upper is None:upper = len(sequence)-1
现在如果不提供限制,程序会自动设定查找范围为整个序列,看看行不行:
>>>seq = [34,67,8,123,4,100,95]
>>>seq.sort()
>>>seq
[4,8,34,67,95,100,123]
>>>search(seq,34)
2
>>search(seq,100)
5
但不必这么麻烦,一则可以直接使用列表方法index,如果想要自己实现的话,只要从程序的开始处循环迭代直到找到数字就行了。
当然可以,使用index没问题了,但是只使用循环可能效率有点低,刚才说过查找100内的一个数,只需要7个问题即可。用循环的话,在最糟糕的情况下要问100个问题。
标准库中的bisect模块可以非常有效的实现二元查找。
可以使用map函数将序列中的元素全部传递给一个函数:
>>>map(str,range(10))
[‘0‘,‘1‘,‘2‘,‘3‘,‘4‘,‘5‘,‘6‘,‘7‘,‘8‘,‘9‘]
filter函数可以基于一个返回布尔值的函数对元素进行过滤。
>>>def func(x):
return x.isalnum()
>>>seq = ["foo","x41","?!","***"]
>>>filter(func,seq)
[‘foo‘,‘x41‘]
本例中,使用列表推导式可以不用专门定义一个函数:
>>>[x for x in seq if x.isalnum()]
[‘foo‘,‘x41‘]
事实上,还有个叫lambda表达式的特性,可以创建短小的函数。
>>>filter(lambda x: x.isalnum(),seq)
[‘foo‘,‘x41‘]
reduce函数一般来说不能轻松被列表推导式替代,但是通常用不到这个功能。它会将序列的前两个元素与给定的函数联合使用,并且将它们的返回值和第三个元素继续联合使用,直到整个序列都处理完毕,并且得到一个最终结果。例如,需要计算一个序列的数字的和,可以使用reduce函数加上lambda x,y:x+y (继续使用相同的数字):
>>>numbers = [72,101,108,108,111,44,32,119,111,114,108,100,33]
>>>reduce(lambda x,y: x+y,numbers)
1161