前面编写的程序都很小,但如果要编写大型程序,你很快就会遇到麻烦。想想看,如果你在一个地方编写了一些代码,但需要在另一个地方再次使用,该如何办呢?例如,假设你编写了一段代码,它计算斐波那契数(一种数列,其中每个数都是前两个数的和)。
>>> 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 #
>>> max(1,2) #2
要给函数编写文档,以确保其他人能够理解,可添加注释(以#打头的内容)。还有另一种编写注释的方式,就是添加独立的字符串。在有些地方,如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)
# 求裴波那契数列
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
语句的位置是可选的,不是固定出现再函数的最后,可以自定义在函数中的任何地方。
对函数的好处概括如下:
增加代码的复用性,增加代码可读性,减少代码的编写量,降低维护成本
我们前面讲解了函数可以由返回值,除了返回值,函数中是否可以返回函数呢?
>>> 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())
# .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点:
函数使用起来简单,创建起来也不那么复杂,但要习惯参数的工作原理就不那么容易了。
在函数定义时,也就是是函数名后面的参数,我们一般叫做形参,使用函数时传进的数为实参。当你在定义函数时,通常不用担心值从何处来,你只需要考虑接受正确的值并处理。编写函数旨在为当前程序(甚至其他程序)提供服务,你的职责是确保它在提供的参数正确时完成任务,并在参数不对时以显而易见的方式失败。(为此,通常使用断言或异常。异常将在第8章介绍)在很重要的情况下,我会将实参称为值,以便将其与类似于变量的形参区分开来。
在函数内部给参数赋值对外部没有任何影响。
>>> 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包含的是副本,因此原始列表是安全的。
注意:函数内的局部名称(包括参数)不会与函数外的名称(即全局名称)冲突。
在提高程序的抽象程度方面,使用函数来修改数据结构(如列表或字典)是一种不错的方式。假设你要编写一个程序,让它存储姓名,并让用户能够根据名字、中间名或姓找人。为此,你可能使用一个类似于下面的数据结构:
>>> 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']
注意:这种程序非常适合使用面向对象编程,这将在下一章介绍。
在有些语言(如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]
但更清晰的解决方案是返回修改后的值。
前面使用的参数都是位置参数,因为它们的位置至关重要—事实上比名称还重要。本节介绍的技巧让你能够完全忽略位置。
>>> 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也指定了默认值,最后一个调用就不会引发异常。
有时候,允许用户提供任意数量的参数很有用。
如果有时候你不知道用户会输入多少参数,那么可以用*
收集起来
>>> 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 "" , 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']
既然有收集,就会有分配参数。同样是使用星号*
。
>>> 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章介绍)。
下面来看一个综合示例。
>>> 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
为了保证函数的定义先于首次调用执行,我们需要知道语句的执行顺序,即执行流程。
执行总是从程序的第一行代码开始,从上到下、从左到右,按顺序一次执行第一条语句。
函数的定义并不会改变程序的执行流程,不过函数代码块中的语句并不是立即执行,而是等函数被程序调用时才执行。
函数调用可以看作程序执行流程中的一个迂回路径,遇到函数调用时,并不会直接继续执行下一条语句,而是跳到函数体的第一行,继续执行函数代码块中的所有语句,再调回原来离开的地方。
Python函数的两种类型参数,一种是函数定义里的形参,一种是调用函数时传入的实参。
>>> def personinfo(age, name)
print(age)
print(ame)
return
在函数中,函数名personinfo
后面的参数列表age和name就是实参,在函数体中分别将age和name的值传递给age和name,函数体重的age和name就是形参。
提示:在函数体内都是对形参进行操作,不能操作实参,即对实参做出更改。
提示:作为实参传入函数的变量名称和函数定义里形参的名字没有关系。函数只关心形参的值,而不关心它在调用前叫什么名字。
参数混合时:
定义的时候,位置参数必须在默认参数之前。
关键字参数必须放到最后。
确保参数拿到值,也不能多拿
>>> 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中,程序的变量并不是在任何位置都可以访问的,访问权限决定于这个变量是在哪里赋值的,代码中变量被赋值的位置决定哪些范围的对象可以访问这个变量,这个范围就是命名空间。那么有多少个命名空间呢?除全局作用域外,每个函数调用都将创建一个,函数中定义的变量等可以认为都是存储在这个命名空间中的,这些变量的调用不会影响到全局变量。
变量的作用域决定哪一部分程序可以访问特定的变量名称。
>>> 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
另外,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()
这个函数中的递归称为无穷递归,因为它从理论上说永远不会结束。这类递归称作无穷递归,实际操作一会儿程序就崩溃了。因为每次调用函数都会用掉一点内存,当内存空间被占满,程序就会报异常。
如果一个函数在内部调用自身,这个函数就称作递归函数
有用的递归函数通常包含下面两部分:
阶乘:当然,你可以用循环的思想来写,像下面这样
>>> 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 "" , line 1, in <module>
fact(1000)
File "" , line 4, in fact
return n * fact(n-1)
File "" , line 4, in fact
return n * fact(n-1)
File "" , line 4, in fact
return n * fact(n-1)
[Previous line repeated 989 more times]
File "" , 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在函数调用前就会被计算,不影响函数调用。
由操作结果看到,调用尾递归时如果做了优化,栈就不会增长,因此无论多少次调用都不会导致栈溢出。
例如,对方心里想着一个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提供了标准的二分查找实现。
在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函数的取值方式。
一般情况多考虑使用匿名函数:
下面来看几个匿名函数的示例。
无参匿名函数:
>>> 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__))
>>> li = dir(__builtins__)
>>> li.index('abs') #80
>>> li[80:]
['abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
>>> len(li[80:]) #72
len 求长度
min 求最小值
max 求最大值
sorted 排序
reversed 反向
sum 求和
>>> help(sum) #求和一个可迭代对象,start从哪开始。
Help on built-in function sum in module builtins:
sum(iterable, start=0, /)
Return the sum of a 'start' value (default: 0) plus an iterable of numbers
When the iterable is empty, return the start value.
This function is intended specifically for use with numeric values and may
reject non-numeric types.
>>> sum([1,2,3,4]) 10
>>> sum([1,2,3,4],10) 20
函数名 | 描述 |
---|---|
bin() | 转换为二进制 |
oct() | 转换为八进制 |
hex() | 转换为十六进制 |
ord() | 将字符转换成对应的ASCII码值 |
chr() | 将ASCII码值转换成对应的字符 |
enumerate(iterable[, start]) -> iterator for index, value of iterable
返回一个可以枚举的对象(一个元组(index, value))
>>> help(enumerate)
Help on class enumerate in module builtins:
class enumerate(object)
| enumerate(iterable[, start]) -> iterator for index, value of iterable
>>> enumerate([1,2,3,4]) #
>>> list(enumerate([1,2,3,4])) #[(0, 1), (1, 2), (2, 3), (3, 4)]
>>> list(enumerate(['a','b','c'])) #[(0, 'a'), (1, 'b'), (2, 'c')]
>>> list(enumerate(['a','b','c'],5)) #[(5, 'a'), (6, 'b'), (7, 'c')]
>>> list(enumerate({'a','b','c'},5)) #[(5, 'a'), (6, 'c'), (7, 'b')]
>>> list(enumerate({'a':1,'b':1,'c':1},5)) #[(5, 'a'), (6, 'b'), (7, 'c')]
filter(function or None, iterable) --> filter object
过滤器 将可迭代对象经过函数的过滤再返回一个filter object
参数(筛选函数,筛选对象)
help(filter)
Help on class filter in module builtins:
class filter(object)
| filter(function or None, iterable) --> filter objectfilter(lambda x:x>2,[1,2,3,4,5])
#list(filter(lambda x:x>2,[1,2,3,4,5]) # [3, 4, 5]
list(filter(fun,[1,2,3,4,5]) #这里只是放一个函数体
list(filter(None,[1,2,3,4,5])) #[1, 2, 3, 4, 5]
map(func, *iterables) --> map object
#加工。
对于参数iterable中的每个元素都应用fuction函数,并返回一个map对象
help(map)
Help on class map in module builtins:
class map(object)
| map(func, *iterables) --> map objectmap(str,[1,2,3]) #
zip(iter1 [,iter2 […]]) --> zip object
#将对象逐一配对
>>> zip([1,2,3]) #
>>> list(zip([1,2,3])) #[(1,), (2,), (3,)]
>>> list(zip([1,2,3],[11,22,33])) #[(1, 11), (2, 22), (3, 33)]
>>> list(zip([1,2,3],[11,22,33],'a')) #[(1, 11, 'a')]后面两个没有配了
>>> list(zip([1,2,3],[11,22,33],'aaaaa'))
#[(1, 11, 'a'), (2, 22, 'a'), (3, 33, 'a')]
配对的参数可多不可少