@[toc]
本章介绍如何将语句组合成函数,这让你能够告诉计算机如何完成任务,且只需说一次,无需反复向计算机传达详细指令。本章详细介绍参数和作用域,还将讨论递归是什么及其在程序中的用途。
一、懒惰是一种美德
前面编写的程序都很小,但如果要编写大型程序,你很快就会遇到麻烦。想想看,如果你在一个地方编写了一些代码,但需要在另一个地方再次使用,该如何办呢?例如,假设你编写了一段代码,它计算斐波那契数(一种数列,其中每个数都是前两个数的和)。
>>> fibs = [0,1]
>>> for i in range(8):
fibs.append(fibs[-2] + fibs[-1])
>>> fibs #[0,1,1,2,3,5,8,13,21,34]
真正的程序员不会这样做的。真正的程序员很懒,不做无谓的工作。
那么真正的程序员会怎么做呢?让程序更抽象。要让前面的程序更抽象,可以像下面这样做。
>>> num = input('How many numbers do you want?')
>>> print(fibs(num))
在这里,只具体地编写了这个程序独特的部分(读取数字并打印结果)。实际上,斐波那契数的计算是以抽象的方式完成的:你只是让计算机这样做,而没有具体的告诉他如何做。你创建了一个名为fibs
的函数,并在需要计算斐波那契数的时候调用它。
二、抽象和结构
抽象可节省人力,但实际上还有个更重要的优点:抽象是程序能够被人理解的关键所在。
程序应非常抽象,如下载网页、计算使用频率、打印每个单词的使用频率。这很容易理解。下面就将前述简单描述转换为一个Python程序。
>>> page = download_page()
>>> freqs = compute+frequencies(page)
>>> for word, freq in freqs:
print(word, freq)
看到这些代码,任何人都知道这个程序是做什么的。然而,至于具体该如何做,你未置一词。你只是让计算机去下载网页并计算使用频率,至于这些操作的具体细节,将在其他地方(独立的函数定义)中给出。
三、自定义函数
函数是组织好的、可城府使用的,用来实现单一或相关联功能的代码段。在程序设计中,函数是指用于进行某种计算的一系列语句的有名称的组合。
函数执行特定的操作并返回一个值或者不返回,你可以调用它(调用时可能需要提供一些参数—放在圆括号中的内容)。一般而言,要判断某个对象是否可调用,可使用内置函数callable
。
>>> import math
>>> x = 1
>>> y = math.sqrt
>>> callable(x) #False
>>> callable(y) #True
函数是结构化编程的核心。函数就是对代码进行一个封装。把实现,某一功能的相同代码,进行封装到一起。下次需要使用时,就不需要再进行代码编写,直接调用即可。
好处:增加代码的复用性,增加代码可读性,减少代码的编写量,降低维护成本
函数可以看成,解决某类问题的'工具'。我们使用def
(表示定义函数)语句来定义函数。
下面是自定义函数的简单规则:
- 函数代码块以
def
关键字开头,后接函数标识符名称和圆括号() - 所有传入的参数和自变量都必须放在圆括号内,可以在圆括号中定义参数。
- 函数的第一行语句可以选择性使用文档字符串,用于存放函数说明。
- 函数内容以冒号
:
开始,并且要缩进。 -
return [表达式]
结束函数,选择性返回一个值给调用方。不带表达式的return
相当于返回None
。
>>> def fibs(num):
result = [0, 1]
for i in range(num - 2):
result.append(result[-2]+result[-1])
return result
>>> print(fibs(10)) #[0,1,1,2,3,5,8,13,21,34]
return
语句用于从函数返回值。函数括号中的表达式称为函数的参数。函数接收参数,并返回结果,这个结果称为返回值。
函数名的命名规则:
必须以字母开头,可以包含下划线 借名之义 驼峰原则
函数名其实是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个别名。
函数内的语句数量是任意的,每个语句至少有一个空格的缩进,以表示该语句属于这个函数。函数体必须保持缩进一致,因为在函数中,缩进结束就表示函数结束。
函数()的()表示调用它。
>>> max #<built-in function max>
>>> max(1,2) #2
1. 给函数编写文档
要给函数编写文档,以确保其他人能够理解,可添加注释(以#打头的内容)。还有另一种编写注释的方式,就是添加独立的字符串。在有些地方,如def语句后面(以及模块和类的开头,这将在第7章和第10章详细介绍),添加这样的字符串很有用。放在函数开头的字符串成为文档字符串,将作为函数的一部分存储起来。
>>> def fibs(num):
'求裴波那契数列'
result = [0, 1]
for i in range(num - 2):
result.append(result[-2]+result[-1])
return result
使用_doc_
来查看函数注释
>>> print(fibs.__doc__)
注意:__doc__
是函数的一个属性。属性将在第7章详细介绍。属性名中的双下划线表示这是一个特殊的属性。特殊("魔法")属性将在第9章讨论。
也可以使用help
函数来查看函数相关信息,其中包含函数的文档字符串。
>>> print(help(fibs))
#Help on function fibs in module __main__:
#fibs(num)
# 求裴波那契数列
2. return
return有两个作用:
- 用来返回函数的运行结果,或者调用另外一个函数。比如
max()
函数 - 函数结束的标志。只要运行了return,就强制结束了函数。return后面的程序都不会被执行。
其实,函数不一定有返回值。什么都不返回的函数不包含return语句,或者包含return语句,但没有在return后面指定值。
>>> def test():
print('This is printed')
return
print('This is not')
这里使用return语句只是为了结束函数。
>>> x = test() #This is printed
如你所见,跳过了第二条print语句。既然test什么都不返回,那么x指向的是什么呢?
>>> x #
>>> print(x) #None
所有的函数都返回值。如果你没有告诉他们该返回什么,就将返回None。
警告:不要让这种默认行为带来麻烦。如果你在if之类的语句中返回值,务必确保其他分支也返回值,以免在调用者期望函数返回一个序列时,不小心返回了None。
在Python中,有的函数会产生结果,我们称这种函数为有返回值函数;有的函数执行一些动作后不返回任何值,我们称这类函数为无返回值函数。
return
语句的位置是可选的,不是固定出现再函数的最后,可以自定义在函数中的任何地方。
3. 为什么要有函数
对函数的好处概括如下:
增加代码的复用性,增加代码可读性,减少代码的编写量,降低维护成本
- 新建一个函数,让我们有机会为一组语句命名,成为一个代码块,这样更有利于阅读代码,并且组织后的代码更容易调试。
- 函数方法可以减少重复代码的使用,让程序代码总行数更少,之后修改代码时只需要少量修改就可以了。
- 将一个很长的代码片段拆分成几个函数后,可以对每一个函数进行单独调试,单个函数调试通过后,再将它们组合起来形成一个完整的产品。
- 一个设计良好的函数可以在很多程序中复用,不需要重复编写。
4. 返回函数
我们前面讲解了函数可以由返回值,除了返回值,函数中是否可以返回函数呢?
>>> def sum_late(*args):
def calc_sum():
ax = 0
for n in args:
ax = ax + n
return ax
return calc_sum
>>> print('调用sum_late的结果:', sum_late(1,2,3,4))
>>> calc_sum = sum_late(1,2,3,4)
>>> print('调用calc_late的结果:', calc_late())
# <function sum_late.<locals>.calc_sum at 0x000001EF019F2E18>
#调用calc_late的结果:10
由执行结果看到,调用定义的函数时没有直接返回求和结果,而是返回了一串字符串(这个字符串其实就是函数)。
在这个例子中,在函数sum_late
中又定义了函数calc_sum
,并且内部函数calc_sum
可以引用外部函数sum_late
的参数和局部变量。当sum_late
返回函数calc_sum
时,相关参数和变量都保存在返回的函数中,称为闭包。这种程序结构威力极大。
有一点需要注意,当调用sum_late()
函数时,每次调用都会返回一个新的函数,即使传入相同的参数也是如此。
闭包的定义:如果在一个内部函数里对外部函数(不是在全局作用域)的变量进行引用,内部函数就被认为是闭包。
在上面的示例中,返回的函数在定义内部引用了局部变量args,当函数返回一个函数后,内部的局部变量会被新函数引用。
我们定义一个函数:
>>> def count():
fs=[]
for i in range(1,4):
def f():
return i*i
fs.append(f)
return fs
>>> f1,f2,f3=count()
该示例中,每次循环都会创建一个新函数,最后把创建的3个函数都返回了。调用f1(),f2(),f3()的结果是1,4,9吗?
>>> print('f1的结果是',f1()) #f1的结果是9
>>> print('f2的结果是',f2()) #f2的结果是9
>>> print('f3的结果是',f3()) #f3的结果是9
都是9的原因在于返回的函数引用了变量i,但它并非立即执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为9.
注意:返回闭包时,返回函数不要引用任何循环变量或后续会发生变化的变量,否则很容易出现你意想不到的问题。
如果一定要引用循环变量怎么办?
>>> def count():
def f(j):
def g():
return j*j
return g
fs = []
for i in range(1,4):
fs.append(f(i))
return fs
>>> f1,f2,f3=count()
>>> f1(),f2(),f3() #(1, 4, 9)
如果一个函数不能正常工作,可以先考虑以下3点:
- 函数获得的实参有问题,某个前置条件没有达到。
- 函数本身有问题,某个后置条件没有达到。
- 函数的返回值有问题或使用方式不对。
四、参数魔法
函数使用起来简单,创建起来也不那么复杂,但要习惯参数的工作原理就不那么容易了。
1. 值从哪里来
在函数定义时,也就是是函数名后面的参数,我们一般叫做形参,使用函数时传进的数为实参。当你在定义函数时,通常不用担心值从何处来,你只需要考虑接受正确的值并处理。编写函数旨在为当前程序(甚至其他程序)提供服务,你的职责是确保它在提供的参数正确时完成任务,并在参数不对时以显而易见的方式失败。(为此,通常使用断言或异常。异常将在第8章介绍)在很重要的情况下,我会将实参称为值,以便将其与类似于变量的形参区分开来。
2. 我能修改参数吗
在函数内部给参数赋值对外部没有任何影响。
>>> def fun(n):
n = "ABC"
>>> name = "DEF"
>>> fun(name)
>>> name #'DEF'
在fun
内将新值赋给了参数n,但这对变量name并没有影响。参数存储在局部作用域内。
字符串,元组等时不可变的,当然你在函数中修改他们也没有用(即只能替换为新值)。
但是可变的数据结构,如列表等,在函数中的改变会影响函数外的值
>>> def f(n):
n[0] = 'A'
>>> names = ['a','b','c']
>>> f(names)
>>> names #['A', 'b', 'c']
当然,这种规则下,有些操作时不够方便的,比如我想要在函数中修改普通变量的值,也改变函数外该变量的值,我们通常会用返回值将修改后的数据提供给函数外变量。
要避免这样的结果,必须创建列表的副本。对序列执行切片操作时,返回的切片都是副本。因此,如果你创建覆盖整个列表的切片,得到的将是列表的副本。
>>> names = ['a','b','c']
>>> n = names[:]
现在n和names包含两个相等但不同的列表。
现在如果修改n,将不会影响names。下面尝试结合使用这种技巧和函数f。
>>> def f(n[:]):
n[0] = 'A'
>>> names #['a','b','c']
注意到参数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']
如你所见,将人员添加到这个数据结构中有点繁琐,在多个人的名字、中间名或姓相同时尤其如此,因为在这种情况下需要对存储在名字、中间名或姓下的列表进行扩展。下面来添加我的妹妹,并假设我们不知道数据库中存储了什么内容。
>>> my_sister = 'Anne Lie Hetland'
>>> storage['first'].setdefaault('Anne', []).append(my_sister)
>>> storage['middle'].setdefaault('Lie', []).append(my_sister)
>>> storage['last'].setdefaault('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 #{'first':{}, 'middle':{}, 'last':{}}
如你所见,这个函数承担了初始化的职责,让代码的可读性高了很多。
注意:在字典中,键的排列顺序是不固定的,因此打印字典时,每次的顺序都可能不同。如果你在解释器中打印出来的顺序不用,请不用担心。
下面先来编写获取人员姓名的函数,再接着编写存储人员姓名的函数。
>>> def lookup(data, label, name):
return data[label].get(name)
函数lookup
接受参数labe
l和name
,并返回一个由全名组成的列表。换而言之,如果已经存储了作者的姓名,就可以像下面这样做:
>>> lookup(storage, 'middle', 'Lie') #['Magnus Lie Hetland']
请注意,返回的是存储在数据结构中的列表。因此如果对返回的列表进行修改,将影响数据结构。(未找到任何人时除外,因为在这种情况下返回的是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
执行如下操作:
- 将参数
data
和full_name
提供给这个函数。这些参数被设置为从外部获得的值。 - 通过拆分
full_name
创建一个名为names
的列表 - 如果
names
的长度为2(只有名字和姓),就将中间名设置为空字符串。 - 将
'first'
、'middle'
和'last'
存储在元组labels
中(也可使用列表,这里使用元组只是为了省略方括号)。 - 使用
函数zip
将标签和对应的名字合并,以便队每个标签-名字对执行如下操作:- 获取属于该标签和名字的列表
- 将
full_name
附加到该列表末尾或插入一个新列表。
下面来尝试运行该程序:
>>> MyNames = {}
>>> init(MyNames)
>>> stroe(MyNames, 'Magnus Lie Hetland')
>>> lookup(MyNames, 'middle', 'Lie') #['Magnus Lie Hetland']
>>> stroe(MyNames, 'Robin Hood')
>>> stroe(MyNames, 'Mr. Gumby')
>>> lookup(MyNames, 'middle', '')
['Robin Hood', 'Mr. Gumby']
注意:这种程序非常适合使用面向对象编程,这将在下一章介绍。
(2) 如果参数是不可变的
在有些语言(如C++、Pascal和Ada)中,经常需要给参数赋值并让这正修改影响函数外部的变量。在Python中,没法直接这样做,只能修改参数对象本身。但如果参数是不可变的(如数)呢?
不好意思,没办法。在这种情况下,应从函数返回所有需要的值(如果需要返回多少个值,就以元组返回它们)。例如,可以向下面这样编写将变量的值加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]
但更清晰的解决方案是返回修改后的值。
3. 关键字参数和默认参数
前面使用的参数都是位置参数,因为它们的位置至关重要—事实上比名称还重要。本节介绍的技巧让你能够完全忽略位置。
>>> def hello_1(greeting, name):
print('{},{}!'.format(greeting, name)
>>> def hello_2(name, greeting):
print('{},{}!'.format(greeting, name)
>>> hello_1('Hello', 'world') #Hello,world!
>>> hello_2('Hello', 'world') #Hello,world!
有时候,参数的排列顺序可能难以记住,尤其是参数很多时。为了简化调用工作,可指定参数的名称。
>>> hello_1(greeting='Hello', name='world') #Hello,world!
>>> hello_2(name='Hello', greeting ='world') #Hello,world!
在这里,参数的顺序无关紧要。不过名称很重要。
像这样使用名称指定的参数称为关键字参数,主要优点是有助于澄清各个参数的作用。
>>> store(patient='Mr. Brainsample',hour=10,minute=20)
虽然这样做的输入量多些,但每个参数的作用很清晰。另外,参数的顺序错了也没有关系。
关键字参数的最大优点在于,可以指定默认值。
>>> def hello_3(greeting='Hello', name='world'):
print('{},{}!'.format(greeting, name))
像这样给参数指定默认值后,调用函数时可不提供它!当对默认参数传值时,函数执行时调用的是我们传入的值。默认参数一定要放在非默认参数的后面。
可以根据需要,一个参数值也不提供、提供部分参数值或提供全部参数值。
>>> hello_3() #Hello,world!
>>> hello_3('Greetings') #Greetings,world!
>>> hello_3('Greetings', 'university') #Greetings,university!
如你所见,仅使用位置参数就很好,只不过如果要提供参数name,必须同时提供参数greeting。如果只想提供参数name,并让参数greeting使用默认值呢?
>>> hello_3(name='Gumby') #Hello,Gumby!
以下是默认参数的使用规则:
- 无论有多少默认参数,默认参数都不能在位置参数之前。
- 无论有多少默认参数,若不传入默认参数值,则使用默认值。
- 若要更改某一个默认参数值,又不想传入其他默认参数,且该默认参数的位置不是第一个,则可以通过参数名更改想要更改的默认参数值。
- 若有一个默认参数通过传入参数名更改参数值,则其他想要更改的默认参数都需要传入参数名更改参数值,否则报错。
- 更改默认参数值时,传入默认参数的顺序不需要根据定义中的函数中的默认参数的顺序传入,最好同时传入参数名,否则容易出现出现执行结果与预期不一致的情况。
你可以结合使用位置参数和关键字参数,但必须先指定所有的位置参数,否则解释器将不知道它们是哪个参数(即不知道参数对应的位置)。.
注意:通常不应结合使用位置参数和关键字参数,除非你知道这样做的后果。一般而言,除非必不可少的参数很少,而带默认值的可选参数很多,否则不应结合使用关键字参数和位置参数。
例如,函数hello
可能要求必须指定姓名,而问候语和标点是可选的。
>>> def hello_4(name, greeting='Hello', punctution='!'):
print('{},{}!'.format(greeting, name, punctuation))
注意:如果给参数name也指定了默认值,最后一个调用就不会引发异常。
4. 收集参数(不定长参数)
有时候,允许用户提供任意数量的参数很有用。
如果有时候你不知道用户会输入多少参数,那么可以用*
收集起来
>>> def print_str(title, *str):
print(str)
print(title)
>>> print_str('aaa',1, 5, 7,24 ,56)
#(1, 5, 7,24 ,56)
#aaa
参数前面的星号将提供的所有值放在一个元组中,而不是列表[]。如果没有可供收集的参数str将是一个空元组()。
>>> def fun4(*args):
print(args)
>>> fun4([1,2,3]) #([1,2,3],)
返回的是有一个元素的元组
>>> def fun4(*args):
print(args)
>>> fun4(*[1,2,3]) #(1,2,3)
当调用的时候 加一个*
就会解包,返回的是列表里的元素组成的元组。
同样的,变量可以放在任何位置收集参数,但不同的是,在变量后面有参数时,需要使用名称来指定后续参数,防止出现歧义。
>>> def in_the_middle(x, *y, z):
print(x, y, z)
>>> in_the_middle(1,2,3,4,5,z=7) #1 (2,3,4,5) 7
>>> in_the_middle(1,2,3,4,5,7)
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
in_the_middle(1,2,3,4,5,7)
TypeError: in_the_middle() missing 1 required keyword-only argument: 'z'
星号不会收集关键字参数,如果你非常想收集,你可以使用两个星号**
。
>>> 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)
#1 2 3
#(5,6,7)
#{'foo':1,'bar':2}
>>> print_params_4(1,2)
#123
#()
#{}
通过结合使用这些技术,可做的事情很多。在下一节你将看到,不管在函数定义中是否使用了*
和**
,都可以在函数调用中使用它们。
现在回到最初的问题:如何在姓名存储示例中使用这种技术?解决方案如下:
>>> 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]
现在可以这样做。
>>> store(d, 'Luke Skywalker', 'Anakin Skywalker')
>>> lookup(d, 'last', 'Skywalker') #['Luke Skywalker', 'Anakin Skywalker']
5. 分配参数
既然有收集,就会有分配参数。同样是使用星号。
>>> def add(x, y):
return x+y
>>> n = (1, 2)
>>> print(add(*n))
这种做法也可用于参数列表的一部分,条件是这部分位于参数列表末尾。通过使用运算符**
,可将字典中的值分配给关键字参数。
>>> 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
两者效果相同。因此,只有在定义函数(允许可变数量的参数)或调用函数时(拆分字典或序列)使用,星号才能发挥作用。
提示:使用这些拆分运算符来传递参数很有用,因为这样无需操心参数个数之类的问题,如下所示:
>>> 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)
这在调用超类的构造函数时特别有用(将在第9章介绍)。
6. 练习使用参数
下面来看一个综合示例。
>>> def story(**kwds):
return 'One upon a time, there was a '\
'{job} called {name}.'.format_map(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: #如果没有给参数stop指定值,
start, stop = 0, start #就调整参数start和stop的值
result = []
i = start #从start开始从上数
while i < stop: #数到stop位置
result.append(i) #将当前数的数附加到result末尾
i += step #增加到当前数和step(>0)之和
return result
7. 执行流程
为了保证函数的定义先于首次调用执行,我们需要知道语句的执行顺序,即执行流程。
执行总是从程序的第一行代码开始,从上到下、从左到右,按顺序一次执行第一条语句。
函数的定义并不会改变程序的执行流程,不过函数代码块中的语句并不是立即执行,而是等函数被程序调用时才执行。
函数调用可以看作程序执行流程中的一个迂回路径,遇到函数调用时,并不会直接继续执行下一条语句,而是跳到函数体的第一行,继续执行函数代码块中的所有语句,再调回原来离开的地方。
8. 形参和实参
Python函数的两种类型参数,一种是函数定义里的形参,一种是调用函数时传入的实参。
>>> def personinfo(age, name)
print(age)
print(ame)
return
在函数中,函数名personinfo
后面的参数列表age和name就是实参,在函数体中分别将age和name的值传递给age和name,函数体重的age和name就是形参。
提示:在函数体内都是对形参进行操作,不能操作实参,即对实参做出更改。
提示:作为实参传入函数的变量名称和函数定义里形参的名字没有关系。函数只关心形参的值,而不关心它在调用前叫什么名字。
9. 参数顺序
参数混合时:
定义的时候,位置参数必须在默认参数之前。
关键字参数必须放到最后。
确保参数拿到值,也不能多拿
>>> fun8(b,m=1,*args):
pass
>>> fun8(1,m=2,3) #关键字参数必须放到最后
>>> fun8(1,3,m=2) #默认参数重复了
>>> def fun8(b,m=1,*arg):
print(arg)
如果不懂的话就按这个顺序,位置参数,默认参数,不定长参数
五、作用域
变量到底是什么呢?可将其视为指向值的名称。因此,执行赋值语句x=1后,名称x指向值1。这几乎与使用字典一样(字典中的键指向值),只是你使用的是"看不见"的字典。实际上,这种解释已经离真相不远。有一个名为vars的内置函数,它返回这个不可见的字典:
>>> x = 1
>>> scope = vars()
>>> scope['x'] #1
警告:一般而言,不应修改vars返回的字典,因为根据Python官方文档的说法,这样做的结果是不确定的。
这种"看不见的字典"称为命名空间或作用域。在Python中,程序的变量并不是在任何位置都可以访问的,访问权限决定于这个变量是在哪里赋值的,代码中变量被赋值的位置决定哪些范围的对象可以访问这个变量,这个范围就是命名空间。那么有多少个命名空间呢?除全局作用域外,每个函数调用都将创建一个,函数中定义的变量等可以认为都是存储在这个命名空间中的,这些变量的调用不会影响到全局变量。
1. 局部变量与全局变量
变量的作用域决定哪一部分程序可以访问特定的变量名称。
>>> def foo(): x = 42
>>> x = 1
>>> foo()
>>> x #1
在这里,函数foo修改了变量x,但当你最终查看时,他根本没变。这是因为调用foo时创建了一个新的命名空间,供foo中的代码块使用。赋值语句x=42是在这个内部作用域(局部命名空间)中执行的,不影响外部(全局)作用域内的x。在函数内使用的变量只能被函数内部引用,不能再函数外引用,这个变量的作用域是局部的,也称为局部变量。在函数外,一段代码最开始赋值的变量可以被多个函数引用,这就是全局变量。全局变量可以在整个程序范围内访问。参数类似于局部变量,因此参数与全局变量同名不会有任何问题。
函数中使用某个变量时,如果参数中的局部变量与全局变量同名,默认使用局部变量。
如果只是想读取这种变量的值(不去重新关联它),通常不会有任何问题。
>>> def combine(parameter): print(parameter + external)
>>> external = 'berry'
>>> combine('Shurb') #Shrubbery
警告:像这样访问全局变量是众多bug的根源。务必慎用全局变量。
如果有一个局部变量或参数与你要访问的全局变量同名,就无法直接访问全局变量,因为它被局部变量遮住了。
如果你想指明使用全局变量,可以使用globals()['全局变量名'],或者global 变量名。这个函数返回一个包含全局变量的字典。(locals返回一个包含局部变量的字典。)
例如,在前面的示例中,如果有一个名为parameter的全局变量,就无法在函数combine中访问它,因为有一个与之同名的参数。然而,必要时可使用globals()['parameter']来访问它。
>>> def combine(parameter): print(parameter + globals()['parameter'])
>>> external = 'berry'
>>> combine('Shurb') #Shrubbery
重新关联全局变量(使其指向新值)是另一码事。在函数内部给变量赋值时,该变量默认为局部变量,除非你明确告诉Python它是全局变量。那么如何告知呢?
>>> x = 1
>>> def change_global():
global x
x = x + 1
>>> change_global()
>>> x #2
2. 函数嵌套
另外,Python是支持函数嵌套使用的,即可将一个函数放在另一个函数内,如下所示:
>>> def foo():
def bar():
print('hello')
bar()
嵌套的用处不大,但有一个很突出的用途,使用一个函数来创建另一个函数。这意味着可像下面这样编写函数:
>>> def multiplier(factor):
def multiplyByFactor(number):
return number * factor
return multiplyByFactor
在这里,一个函数位于另一个函数中,且外面的函数返回里面的函数。也就是返回一个函数,而不是调用它。重要的是,返回的函数能够访问其定义所在的作用域。换而言之,它携带着自己所在的环境(和相关的局部变量)。
每当外部函数被调用时,都将重新定义内部的函数,而变量factor的值也可能不同。由于Python的嵌套作用域,可在内部函数中访问这个来自外部局部作用域(multiplier)的变量,如下所示;
>>> double = multiplier(2)
>>> double(5) #10
>>> trible = multiplier(3)
>>> trible(3) #9
>>> multiplier(5)(4) #20
像multiplyByFactor这样存储其所在作用域的函数成为闭包。
通常,不能给外部作用域内的变量赋值,但如果一定要这样做,可使用关键字nonlocal。这个关键字的用法和global很像,让你能够给外部作用域(非全局作用域)内的变量赋值。
六、递归
递归意味着引用自身,即自己调用自己。例如:
>>> def recursion():
return recursion()
这个函数中的递归称为无穷递归,因为它从理论上说永远不会结束。这类递归称作无穷递归,实际操作一会儿程序就崩溃了。因为每次调用函数都会用掉一点内存,当内存空间被占满,程序就会报异常。
如果一个函数在内部调用自身,这个函数就称作递归函数
有用的递归函数通常包含下面两部分:
- 基线条件(针对最小的问题):满足这种条件时函数将直接返回一个值。(这样就避免了无限调用的可能)
- 递归条件:包含一个或多个调用,这些调用旨在解决问题的一部分。
这里的关键是,通过将问题分解为较小的部分,可避免递归没完没了,因为问题终将被分解成基线条件可以解决的最小问题。
其实函数每次被调用时都会创建一个新命名空间,也就是当函数调用自身时,实际上运行的是两个不同的函数(也可以说一个函数具有两个不同的命名空间)。
1. 两个经典案例:阶乘和幂
阶乘:当然,你可以用循环的思想来写,像下面这样
>>> def factorial(n):
result = n
for i in range(1,n):
result *= i
return result
它是这样做的:首先将result设置为n,再将其依次乘以1到n-1的每个数字,最后返回result。关于阶乘的数学定义为:1的阶乘为1。对于大于1的数字n其阶乘为n-1的阶乘再乘以n。
这里我们换一种思路,用递归来实现:
>>> def factorial(n):
if n == 1: #基线条件,满足即退出函数
return 1
else:
return n * factorial(n – 1)
我们再来定义幂的运算(就是和内置函数pow一样的效果)。幂运算的定义是power(x,n)(x的n次幂)是将数字x自乘n-1次的结果,即将n个x相乘的结果。
>>> def power(x, n):
result = 1
for i in range(n):
result *= x
return result
递归式定义为对于任何数字x,power(x,0)都为1。n>0时,power(x,n)为power(x,n-1)与x的乘积。
>>> def power(x, n):
if n == 0:
return 1
else:
return x * power(x, n-1)
当然,你可以明显的看到,递归大部分情况是可以用循环代替的,而且循环在时间复杂度可能更好一点,但是当你掌握了递归,你就会爱上这种简洁的表达方式。
提示:如果函数或算法复杂难懂,在实现前用自己的话进行明确的定义将大有裨益。以这种"准编程语言"编写的程序通常称为伪代码。
在大多数情况下,使用循环的效率可能更高。然而,在很多情况下,使用递归的可读性更高,且有时要高得多。递归函数的优点是定义简单,逻辑清晰。
>>> def fact(n):
if n == 1:
return 1
return n * fact(n – 1)
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的。每当进入一个函数调用,栈就会加一层栈帧;每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,因此递归调用的次数过多会导致栈溢出。
>>> fact(1000)
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
fact(1000)
File "<pyshell#13>", line 4, in fact
return n * fact(n-1)
File "<pyshell#13>", line 4, in fact
return n * fact(n-1)
File "<pyshell#13>", line 4, in fact
return n * fact(n-1)
[Previous line repeated 989 more times]
File "<pyshell#13>", line 2, in fact
if n == 1:
RecursionError: maximum recursion depth exceeded in comparison
异常提示超过最大递归深度。
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果一样,把循环看成是一种特殊尾递归函数也可以。
尾递归是指在函数返回时调用函数本身,并且return语句不能包含表达式。这样,编译器或解释器就可以对尾递归进行优化,使递归本身无论调用多少次都只占用一个栈帧,从而避免栈溢出的情况。
>>> def fact(n):
return fact_iter(n,1)
>>> def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
可以看到,return fact_iter(num - 1, num * product)仅返回递归函数本身, num - 1和num * product在函数调用前就会被计算,不影响函数调用。
由操作结果看到,调用尾递归时如果做了优化,栈就不会增长,因此无论多少次调用都不会导致栈溢出。
2. 另一个经典案例:二分查找
例如,对方心里想着一个1-100的数字,你必须猜出是哪个。实际上只需要猜7次。首先问:这个数字大于50吗?如果答案是肯定的,再问:这个数字大于75吗?不断将可能的区间减半,知道猜对为止。你无需过多地思考就能成功。
这里的关键是,这种算法自然而然地引出了递归式定义和实现。先来回顾一下定义,确保知道该如何做。
- 如果上限和下限相同,就说明它们都指向数字所在的位置,因此将这个数字返回。
- 否则,找出区间的中间位置(上限和下限的平均值),再确定数字在左半部分还是有半部分。然后继续在数字所在的那部分中查找。
在这个递归案例中,关键在于元素是经过排序的。找出中间的元素后,只需将其与要查找的数字进行比较即可。如果要查找的数字更大,肯定在右边;如果更小,它必然在左边。递归部分为"继续在数字所在的那部分中查找",因为查找方式与定义所指定的完全相同。(请注意,这种查找算法返回数字应该在的位置。如果这个数字不在序列中,那么这个位置上的自然是另一个数字。)
现在可以实现二分查找了。
>>> def search(sequence, number, lower=0, upper=None):
if upper is None: upper = len(sequence) - 1
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)
提示:实际上,模块bisect提供了标准的二分查找实现。
3. 函数式编程
在Python中,通常不会如此倚重函数(而是创建自定义对象,这将在下一章详细介绍),但完全可以这样做。
Python提供了一些有助于这种函数式编程的函数:map、filter和reduce。在较新的Python版本中,函数map和filter的用途并不大,应该使用列表推导来替代它们。你可使用map将序列的所有元素传递给函数。
函数 | 描述 |
---|---|
map(func, seq[,seq,…]) | 对序列中的所有元素执行函数 |
filter(func,seq) | 返回一个列表,其中包含对其执行函数时结果为真的所有函数 |
reduce(func,seq[,initial]) | 等价于func(func(func(seq[0],seq[1]),seq[2]),…) |
sum(seq) | 返回seq中所有元素的和 |
apply(func[,args[,kwargs]]) | 调用函数(还提供要传递给函数的参数) |
>>> list(map(str, range(10))) #与[str(i)for i in range(10)]等价
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
你可使用filter根据布尔函数的返回值对元素进行过滤。
>>> def func(x):
return x.isalnum()
>>> seq = ["foo", "x41", "?!", "***"]
>>> list(filter(func, seq)) #['foo', 'x41']
就这个示例而言,如果转而使用列表推导,就无需创建前述自定义函数。
>>> [x for x in seq if x.isalnum()] #['foo', 'x41']
实际上,Python提供了一种名为lambda表达式的功能,让你能够创建内嵌的简单函数(主要供map、filter和reduce使用)
>>> filter(lambda x: x.isalnum(), seq) #['foo', 'x41']
然而,使用列表推导的可读性不是更高吗?
要使用列表推导来替换函数reduce不那么容易,而这个函数提供的功能即便能用到,也用的不多。它使用指定的函数将序列的前两个元素合二为一,再将结果与第3个元素合二为一,以此类推,直到处理完整个序列并得到一个结果。例如,如果你要将序列中的所有数相加,可结合使用reduce和lambda x,y:x+y。
>>> numbers = [1,2,3,4]
>>> from functools import reduce
>>> reduce(lambda x,y: x+y,numbers) #10
就这个示例而言,还不如使用内置函数sum。
七、匿名函数
匿名函数就是不再使用def语句这样的标准形式定义一个函数。
Python使用lambda创建匿名函数。lambda只是表达式,函数体比def简单很多。
lambda的主体是一个表达式,而不是一个代码块,仅能在lambda表达式中封装优先的逻辑。 lambda函数拥有自己的命名空间,不能访问自有参数列表之外或全局命名空间的参数。
lambda函数的语法只包含一个语句:lambda [args1[,args2,…argn]]:expression
看一个求两个数和的示例。
>>> def func(x,y):
return x+y
>>> lambda x,y:x+y
可以看出,使用lambda表达编写的代码比使用def语句少。
比如求一个列表中大于3的元素,通过函数式编程实现,运用filter。
>>> def func(x):
return x>3
>>> f_list = filter(func,[1,2,3,4,5])
>>> print([item for item in f_list])
如果使用匿名函数,
>>> print([item for item in filter(lambda x:x>3,[1,2,3,4,5])
从上面的操作可以看出,lambda一般应用于函数式编程,代码简介,常和filter等函数结合使用。
我们对lambda进行解析。在表达式中
x
为lambda函数的一个参数,:
为分割符,x>3
为返回值,item for item in filter
是filter函数的取值方式。
一般情况多考虑使用匿名函数:
- 程序一次性使用、不需要定义函数名时,用匿名函数可以节省内存中变量定义空间。
- 如果想让程序更加简洁,使用匿名函数就可以做到。
匿名函数有3个规则要记住: - 一般有一行表达式,必须有返回值
- 不能有return
- 可以没有参数,也可以有一个或多个参数
下面来看几个匿名函数的示例。
无参匿名函数:
>>> t = lambda :True
>>> t() #True
带参数匿名函数
>>> lambda x : x**3
>>> lambda x,y,z : x+y+z
>>> lambda x,y=3 : x*y
匿名函数调用:
>>> c = lambda x,y,z : x*y*z
>>> c(2,3,4) #24
八、偏函数
偏函数通过模块functools被用户调用。
偏函数是将所要承载的函数作为partial()函数的第一个参数,原函数的各个参数一次作为partial()函数的后续参数,除非使用关键字参数。
在这个例子里,将实现一个取余函数,取得整数100对不同数m的100%m的余数。
>>> from functools import partial
>>> def mod(n,m):
return n%m
>>> mod_by_100 = partial(mod,100)
>>> print(mod(100,7) 2
>>> print(mod_by_100(7)) 2
由执行结果看到,使用偏函数所需代码量比自定义函数更少、更简洁。
九、快速排序
快速排序是一种分治排序算法。该算法首先选取一个划分元素(pivot);然后重排列表,将其划分为3个部分,即left(小于划分元素pivot的部分),pivot、right(大于划分元素pivot的部分),此时划分元素 pivot已经在列表的最终位置上;最后分别对left和right两部分进行递归排序。
其中,划分元素的选取直接影响快速排序算法的效率,通常选择列表的第一个元素、中间元素或最后一个元素作为划分元素,当然也有更复杂的选择方式。划分过程根据划分元素重排列表,是快速排序算法的关键所在。
快速排序算法的优点是原位排序(只使用很小的辅助栈),平均时间复杂度为O(n log n)。快速排序算法的缺点是不稳定,最坏情况下时间复杂度为O(n2)
>>> def quicksort(L):
qsort(L, 0, len(L) - 1)
>>> def qsort(L, first, last):
if first < last:
split = partition(L, first, last)
qsort(L, first, split - 1)
qsort(L, split + 1, last)
>>> def partition(L, first, last):
# 选取列表中的第一个元素作为划分元素
pivot = L[first]
leftmark = first + 1
rightmark = last
while True:
while L[leftmark] <= pivot:
# 如果列表中存在与划分元素相等的元素,让它位于left部分
# 以下检测用于划分元素pivot是列表中的最大元素时
# 放置leftmark越界
if leftmark == rightmark:
break
leftmark += 1
while L[rightmark] > pivot:
# 这里不需要检测,划分元素pivot是列表中的最小元素时
# rightmark自动停在first处
rightmark -= 1
if leftmark < rightmark:
# 此时,leftmark处的元素大于pivot
# rightmark处的元素小于等于pivot,交换两者
L[leftmark], L[rightmark] = L[rightmark], L[leftmark]
else:
break
# 交换first处的划分元素与rightmark处的元素
L[first], L[rightmark] = L[rightmark], L[first]
# 返回划分元素pivot的最终位置
return rightmark
>>> num_list = [5,-4,6,3,7,11,1,2]
>>> quicksort(num_list)
>>> print(num_list) #[-4, 1, 2, 3, 5, 7, 6, 11]
十、常见的内置函数
常见的内置函数:
查看内置函数:
print(dir(builtins))
常见函数
len 求长度
min 求最小值
max 求最大值
sorted 排序
reversed 反向
sum 求和
进制转换函数:
bin() 转换为二进制
oct() 转换为八进制
hex() 转换为十六进制
ord() 将字符转换成对应的ASCII码值
chr() 将ASCII码值转换成对应的字符
补充:
- enumerate() #返回一个可以枚举的对象
- filter() #过滤器
- map() #加工。对于参数iterable中的每个元素都应用fuction函数,并返回一个map对象
- zip() #将对象逐一配对