一、Python函数
函数是Python为了代码最大程度地重用和最小化代码冗余而提供的基本程序结构。函数是一种设计工具,它能让程序员将复杂的系统分解为可管理的部件。函数用于将相关功能打包并参数化。需要注意的是函数只能返回一个值,如果return语句后面有多个逗号分隔的值,会自动的封包成一个元祖。另外一个函数可以有任意多个return语句,但是始终只会执行一个return语句,执行return语句,会返回到调用方的作用域。
Python提供了很多内置函数,比如print()。但你也可以自己创建函数,这被叫做用户自定义函数。
再谈作用域,在Python中作用域分为全局作用域和函数作用域。函数定义了函数作用域,应用范围仅存在与一个函数体内;而模块定义了全局作用域,每个模块都是一个全局作用域,因此全局作用域的范围仅限于单个程序文件。可以使用locals()函数查看当前作用域内的相关变量。
二、函数设计规范
1. 耦合性(降低)
- 通过参数接受输入,以及通过return产生输出以保证函数的独立性。
- 尽量减少使用全局变量进行函数通信。
- 不要在函数中修改可变类型的参数。
- 避免直接修改定义在另外一个模块中的变量。
2. 聚合性(提高)
- 每个函数都应该有一个单一的、统一的目标。
- 每个函数的功能都应该相对简单。
三、调用函数
Python内置了很多有用的函数,我们可以直接调用。要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数abs,只有一个参数。
1 2 3 4 5 6 |
>>> abs(100) 100 >>> abs(-20) 20 >>> abs(12.34) 12.34 |
调用函数的时候,如果传入的参数数量不对,会报TypeError的错误,并且Python会明确地告诉你有几个参数
如果传入的参数数量是对的,但参数类型不能被函数所接受,也会报TypeError的错误,并且给出错误信息:str是错误的参数类型:
1 2 3 4 |
>>> abs('a') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: bad operand type for abs(): 'str' |
而max函数max()可以接收任意多个参数,并返回最大的那个:
1 2 3 4 |
>>> max(1, 2) 2 >>> max(2, 3, 1, -5) 3 |
四、定义函数
在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:。然后,在缩进块中编写函数体,函数的返回值用return语句返回(rreturn可以简单理解为结束一个函数,return跟print可不一样的)。
1 2 |
def function_name([args,args,...]) return |
我们自定义一个求绝对值的my_abs函数为例:
1 2 3 4 5 |
def my_abs(x): if x >= 0: return x else: return -x |
调用my_abs看看返回结果是否正确。
1 2 3 4 5 6 |
>>> my_abs(12.2) 12.2 >>> my_abs(-2) 2 >>> my_abs(2) 2 |
请注意,函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回,后续任何语句都不会再执行,简单理解为函数结束指令。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。return None可以简写为return。
如果你已经把my_abs()的函数定义保存为abstest.py文件了,那么,可以在该文件的当前目录下启动Python解释器,用from abstest import my_abs来导入my_abs()函数,注意abstest是文件名(不含.py扩展名)。
1 2 3 |
>>> from abstest import my_abs >>> my_abs(-12) 12 |
如果想定义一个什么事也不做的空函数,可以用pass语句:
1 2 |
def nop(): pass |
pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。
pass还可以用在其他语句里,比如:
1 2 |
if age >= 18: pass |
缺少了pass,代码运行就会有语法错误。
上面说了,函数只能返回一个值,如果return语句后面有多个逗号分隔的值,会自动的封包成一个元祖。
1 2 3 4 5 |
>>> def r(x,y): ... return x, y+1 ... >>> r(1,2) (1, 3) |
但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。
1 2 3 4 5 6 |
>>> def r(x,y): ... print x, y+1 ... >>> >>> r(1,2) 1 3 |
另外一个函数可以有任意多个return语句,但是始终只会执行一个return语句,执行return语句,会返回到调用方的作用域。如下示例:
1 2 3 4 5 6 |
>>> def r(x): ... return x ... return x+1 ... >>> r(3) 3 |
说完函数定义的基本语法后,简单说一下如何定义函数帮助,也称为docstring。其实在函数内部可以使用三引号进行编写函数帮助信息,如下:
1 2 3 4 5 6 7 |
def my_abs(x, y): ''' @param x int @param y int @return int ''' return x + y |
查看帮助
1 2 3 4 5 6 7 |
>>> help(my_abs) Help on function my_abs in module __main__: my_abs(x, y) @param x int @param y int @return int |
从Python 3.4开始提供了类型示意的特性,就是可以把帮助信息简单化。语法如下:
1 2 |
def my_abs(x:int, y:int) -> int: return x + y |
意思就是x类型为int,y的类型为int,输出结果也为int。上面都说了是类型示意,也就是没有类型强制,随便输入浮点型、整形、字符型都可以,类型示意目前仅仅作用就是提供帮助信息,帮助IDE做检查。当然,通过这种机制,利用装饰器也可以做类型检查的。
五、参数传递
定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。
Python的函数定义非常简单,但灵活度却非常大。定义函数时可以用位置参数、关键字参数、默认参数、可变位置参数和可变关键字参数,这几种参数可以一起使用,或者只用其中某些。但是请注意,参数定义的顺序必须是:位置参数、关键字参数、默认参数、可变位置参数和可变关键字参数。对于可变的参数需要后置,另外最好可变的参数不和默认参数一起出现。
“位置参数”的传递规则是自左而右,比如执行一个带参数的函数。
1 2 3 4 5 6 7 |
>>> def test(x,y): ... return x + y ... >>> test(3,4) 7 >>> test('hello','world') 'helloworld' |
对于参数值的传递必须一一对应好函数的参数位置,不能错乱,所以也称之为位置参数。这种方式,当函数的参数过多时,写起来就非常累了,写着写着可能自己都混淆了。所以,Python提供了关键字参数来解决这种烦恼。
“关键字参数”允许你可以按参数名称匹配传递,这样就不需要自左而右传递参数值了,如下:
1 2 |
>>> test(y='world',x='hello') 'helloworld' |
有了位置参数,我们写和修改起来就好多了。上面说了参数的传递顺序,位置参数必须在关键字参数之前,不然是会抛异常的,如下:
1 2 3 4 5 6 7 8 |
# 位置参数在关键字参数之前,正常; >>> test('hello',y='world') 'helloworld' # 关键字参数在位置参数之前,异常; >>> test(x='hello',world) File "<stdin>", line 1 SyntaxError: non-keyword arg after keyword arg |
对于位置参数和关键字参数都是“必选参数”,如果有缺少则会抛异常。
定义函数时可以使用“默认参数”,有默认值的参数且默认参数一定要用不可变对象,如str/none,如果是可变对象运行会有逻辑错误(好处注册信息时符合默认值的就不需要从新填写,而不符合默认值的也可以写)。默认参数最大的好处是能降低调用函数的难度。
1 2 3 4 5 |
def student(name,gender,age=20,city='shanghai'): print "name: ", name print "gender: ", gender print "age: ", age print "city: ", city |
调用函数:
1 2 3 4 5 |
>>> student('eric','c',10) name: eric gender: c age: 10 city: shanghai |
从上面的例子可以看出,默认参数可以简化函数的调用。设置默认参数时,必选参数在前,默认参数在后,否则Python的解释器会报错(思考一下为什么默认参数不能放在必选参数前面)。
默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:
先定义一个函数,传入一个list,添加一个END再返回:
1 2 3 |
def add_end(L=[]): L.append('END') return L |
当你正常调用时,结果似乎不错:
1 2 3 4 |
>>> add_end([1, 2, 3]) [1, 2, 3, 'END'] >>> add_end(['x', 'y', 'z']) ['x', 'y', 'z', 'END'] |
当你使用默认参数调用时,一开始结果也是对的:
1 2 |
>>> add_end() ['END'] |
但是,再次调用add_end()时,结果就不对了:
1 2 3 4 |
>>> add_end() ['END', 'END'] >>> add_end() ['END', 'END', 'END'] |
可能会很疑惑,默认参数是[],但是函数似乎每次都“记住了”上次添加了’END’后的list。
原因解释如下:
Python函数在定义的时候,默认参数L的值就被计算出来了,即[],因为默认参数L也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。
所以,定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None这个不变对象来实现:
1 2 3 4 5 |
def add_end(L=None): if L is None: L = [] L.append('END') return L |
现在,无论调用多少次,都不会有问题:
1 2 3 4 |
>>> add_end() ['END'] >>> add_end() ['END'] |
为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
定义函数时使用“*”号加参数名可以定义“可变位置参数”(如果已经有一个list或者tuple,要调用一个可变参数,Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去)
1 2 3 4 5 |
def cal(*numbers): sum = 0 for x in numbers: sum = sum + x * x return sum |
可变位置参数既可以直接传入func(1, 2, 3),也可以先组装list或tuple,再通过*args传入:func(*(1, 2, 3))
1 2 |
>>> cal(1,2,3) 14 |
如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:
1 2 3 |
>>> nums = [1, 2, 3] >>> cal(nums[0], nums[1], nums[2]) 14 |
这种写法当然是可行的,也称为参数解构,问题是太繁琐,且元素必须一一对应,不能多也不能少。所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
1 2 3 |
>>> nums = [1, 2, 3] >>> cal(*nums) 14 |
*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
定义函数时使用“**”号加关键字名就可以定义“可变关键字参数”,可变关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict(好处可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用可变关键字参数来定义这个函数就能满足注册的需求)
1 2 3 4 |
def person(name,age,**kw): print 'name: ', name print 'age: ', age print 'other: ',kw |
可变关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过**kw传入:func(**{‘a’: 1, ‘b’: 2})。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
>>> person('eric',30) name: eric age: 30 other: {} >>> person('eric',30,city='shanghai') name: eric age: 30 other: {'city': 'shanghai'} >>> kw = {'city':'shanghai'} >>> person('eric',30,**kw) name: eric age: 30 other: {'city': 'shanghai'} |
在Python中定义函数,可以用位置参数、关键字参数、默认参数、可变位置参数和可变关键字参数,这几种参数可以一起使用,或者只用其中某些。但是请注意,参数定义的顺序必须是:位置参数、关键字参数、默认参数、可变位置参数和可变关键字参数。对于可变的参数需要后置处理,另外最好可变的参数不和默认参数一起出现。
定义一个函数,包含上述几种参数(注意参数顺序):
1 2 3 4 5 6 |
def func(a,b,c=0,*args,**kw): print ('a: '), a print ('b: '),b print ('c: '),c print ('args: '), args print ('kw: '), kw |
在函数调用时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# 位置参数; >>> func(1,2) a: 1 b: 2 c: 0 args: () kw: {} # 位置参数 + 关键字参数; >>> func(1,b=2) a: 1 b: 2 c: 0 args: () kw: {} # 关键字参数 + 默认参数; 默认参数最好不要跟可变参数一块使用; >>> func(1,b=2,c=3) a: 1 b: 2 c: 3 args: () kw: {} # 可变位置参数,不能跟关键字参数同时使用,且需要使用引号引起来; >>> func(1,2,3,'a','b=2') a: 1 b: 2 c: 3 args: ('a', 'b=2') kw: {} # 可变关键字参数,可以跟关键参数同时使用,且不需要使用引号引起来; >>> func(a=1,b=2,c='c',x='y',k='v') a: 1 b: 2 c: c args: () kw: {'x': 'y', 'k': 'v'} |
最后通过一个tuple和dict,你也可以调用该函数。
1 2 3 4 5 6 7 8 |
>>> args = (1,2,3,4,5) >>> kw = {'x':'99'} >>> func(*args,**kw) a: 1 b: 2 c: 3 args: (4, 5) kw: {'x': '99'} |
另外,对于“可变位置参数”,在函数接收到之后,会封包成tuple类型。而“可变关键字参数”在函数接收到之后会封包成dict类型。如下代码示例:
1 2 3 4 5 6 7 8 9 10 11 |
def func(*args, **kwargs): print(type(args), len(args)) print(type(kwargs), len(kwargs)) >>> func('dkey', 23) <class 'tuple'> 2 <class 'dict'> 0 >>> func(name='dkey', age=23) <class 'tuple'> 0 <class 'dict'> 2 |
对于函数传参使用起来非常灵活,知识点也多,很多时候可能并不知道有些功能点具体应用场景。目前只需要记住这些功能点即可,慢慢代码写多了,自然也就理解这些功能在实际场景中的应用了。
六、参数检查
调用自定义函数时,如果参数个数不对,Python解释器会自动检查出来,并抛出TypeError。
1 2 3 4 |
>>> my_abs(1,2) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: my_abs() takes exactly 1 argument (2 given) |
但是如果参数类型不对,Python解释器就无法帮我们检查。试试my_abs和内置函数abs的差别;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# python 2.7; >>> my_abs('A') 'A' # python 3.4; >>> my_abs('A') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in my_abs TypeError: unorderable types: str() >= int() >>> abs('A') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: bad operand type for abs(): 'str' |
当传入了不恰当的参数时,内置函数abs会检查出参数错误,而我们定义的my_abs没有参数检查,所以,这个函数定义不够完善。让我们修改一下my_abs的定义,对参数类型做检查,只允许整数和浮点数类型的参数。数据类型检查可以用内置函数isinstance实现
添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:
1 2 3 4 5 6 7 |
def my_abs(x): if not isinstance(x, (int, float)): raise TypeError('bad operand type') if x >= 0: return x else: return -x |
添加了参数检查后,如果传入错误的参数类型,函数就可以抛出一个错误:
1 2 3 4 5 |
>>> my_abs('A') Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 3, in my_abs TypeError: bad operand type |
七、递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。
举个例子,我们来计算阶乘n! = 1 x 2 x 3 x … x n,用笨方法循环写,如下:
1 2 3 4 5 6 7 8 9 10 |
>>> n = 1 >>> for i in range(1,6): ... n = i * n ... print n ... 1 2 6 24 120 |
如果用函数fact(n)表示,可以看出:fact(n) = n! = 1 x 2 x 3 x … x (n-1) x n = (n-1)! x n = fact(n-1) x n
所以,fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理。
于是,fact(n)用递归的方式写出来就是:
1 2 3 4 |
def fact(n): if n==1: return 1 return n * fact(n - 1) |
上面就是一个递归函数。可以试试:
1 2 3 4 5 6 |
>>> fact(1) 1 >>> fact(5) 120 >>> fact(10) 3628800 |
如果我们计算fact(5),可以根据函数定义看到计算过程如下:
1 2 3 4 5 6 7 8 9 10 |
===> fact(5) ===> 5 * fact(4) ===> 5 * (4 * fact(3)) ===> 5 * (4 * (3 * fact(2))) ===> 5 * (4 * (3 * (2 * fact(1)))) ===> 5 * (4 * (3 * (2 * 1))) ===> 5 * (4 * (3 * 2)) ===> 5 * (4 * 6) ===> 5 * 24 ===> 120 |
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。但是在Python中函数递归还是少用,尽量不用;函数递归执行起来特别慢。
另外在使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。可以试试fact(1000):
1 2 3 4 5 6 7 |
>>> fact(1000) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in fact ... File "<stdin>", line 4, in fact RuntimeError: maximum recursion depth exceeded in comparison |
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
上面的fact(n)函数由于return n * fact(n – 1)引入了乘法表达式,所以就不是尾递归了。要改成尾递归方式,需要多一点代码,主要是要把每一步的乘积传入到递归函数中:
1 2 3 4 5 6 7 |
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在函数调用前就会被计算,不影响函数调用。
fact(5)对应的fact_iter(5, 1)的调用如下:
1 2 3 4 5 6 |
===> fact_iter(5, 1) ===> fact_iter(4, 5) ===> fact_iter(3, 20) ===> fact_iter(2, 60) ===> fact_iter(1, 120) ===> 120 |
尾递归调用时,如果做了优化,栈不会增长,因此,无论多少次调用也不会导致栈溢出。
遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。
八、生成器函数
通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:
1 2 3 4 5 6 |
>>> L = [x * x for x in range(10)] >>> L [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> g = (x * x for x in range(10)) >>> g <generator object <genexpr> at 0x1022ef630> |
创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator。
下面我们用类似函数的方式定义一个生成器函数,生成器的定义和函数类似,但是有yield语句,如下:
1 2 3 |
def gen(x): for i in range(x): yield i |
我们看一下类型:
1 2 3 |
>>> g = gent(10) >>> type(g) <type 'generator'> |
生成器函数返回的是一个生成器,生成器是可迭代的,所以我们可以使用next()方法调用,如下:
1 2 3 4 |
>>> g.next() 0 >>> g.next() 1 |
generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。
前面说过,函数内return是结束一个函数,在生成器函数里面也是一样,当遇到return时,这个函数就被结束,也就意味着生成器被结束。如下在Python 3中执行:
1 2 3 4 5 |
def gen(x): for i in range(x): if i == 2: return i yield i |
九、nonlocal
前面说了,Python里只有2种作用域:全局作用域和局部作用域。全局作用域是指当前代码所在模块的作用域,局部作用域是指当前函数或方法所在的作用域。其实准确来说,Python 3.x引入了nonlocal关键字,可以用于标识外部作用域的变量。
在Python 2.x中,局部作用域里的代码可以读外部作用域(包括全局作用域)里的变量,但不能更改它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# 可以读取; >>> def a(): ... x = 0 ... def b(): ... print(x) ... return b ... >>> a()() 0 # 不能更改; >>> def a(): ... x = 0 ... def b(): ... x += 1 ... print(x) ... return b ... >>> a()() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in b UnboundLocalError: local variable 'x' referenced before assignment |
要在Python 2.x中解决这个问题,目前只能使用全局变量global关键字,但全局变量在任何语言中都不被提倡,因为它很难控制。
为了解决这个问题,Python 3.x引入了nonlocal关键字(详见The nonlocal statement)。只要在闭包内用nonlocal声明变量,就可以让解释器在外层函数中查找变量名了,例如:
1 2 3 4 5 6 7 8 9 10 |
>>> def a(): ... x = 0 ... def b(): ... nonlocal x ... x += 1 ... print(x) ... return b ... >>> a()() 1 |
完结。。。