2.5.6 Python函数
函数是组织好的,可重复使用的,用来实现单一功能或功能相互关联的代码段。
函数能提高应用的模块性,和代码的重复利用率。大家应该知道Python提供了许多内建函数,比如print()。但我们也可以自己创建函数,即用户自定义函数。
在Python中我们是如何定义一个函数的呢?
定义一个有自己想要功能的函数时,需要遵循以下规则:
·函数代码块以def关键词开头,后接函数标识符名称和圆括号“( )”。
·任何传入参数和自变量必须放在圆括号中间,圆括号之间可以定义参数。
·函数的第一行语句可以选择性地使用文档字符串——用于存放函数说明。
·函数内容以冒号起始,并且要有缩进。
·用return [表达式]结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回None。
例如,我们可以定义一个函数,名字叫MyFirstFun(),内容如下:
# -*- coding: UTF-8 -*- def MyFirstFun(name): '''函数定义过程中的name是叫形参 ''' print 'My name is:' + name MyFirstFun('余洪春')
运行结果如下:
My name is:余洪春
在MyFirstFun后面的name叫形参,它只是一个形式,表示占据了一个参数值,在print后面传递进来的name叫实参。
1.Python函数的参数传递
Python函数的参数传递分不可变类型和可变类型两种。
·不可变类型:类似C++的值传递,如整数、字符串、元组。如fun(a),传递的只是a的值,没有影响a对象本身。比如在fun(a)内部修改a的值,只是修改了另一个复制的对象,不会影响a本身。
·可变类型:类似C++的引用传递,如列表、字典等。如fun(list1),表示将list1真正地传过去,修改后fun外部的list1也会受影响。
不可变类型的举例说明如下:
# -*- coding: UTF-8 -*- def ChangeInt(a): a = 10 print "a的值为:",a b = 2 ChangeInt(b) print "b的值为:",b
结果如下:
a的值为: 10 b的值为: 2
可变类型的举例说明:
# -*- coding: UTF-8 -*- #可用''' '''写函数说明 def changeme(mylist): "修改传入的列表" mylist.append([1,2,3,4]); print "函数内取值:", mylist return #调用changeme函数 mylist = [10,20,30]; changeme(mylist); print "函数外取值:", mylist
输出结果如下:
函数内取值: [10, 20, 30, [1, 2, 3, 4]] 函数外取值: [10, 20, 30, [1, 2, 3, 4]]
大家在实际工作中要注意可变参数和不可变参数的区别。
对于Python的参数,以下是调用函数时可使用的正式参数类型:
·必备参数
·关键字参数
·默认参数
·不定长参数
(1)必备参数
必备参数须以正确的顺序传入函数。调用时的数量必须和声明时的一样。例如在上面的函数中,如果不传入一个实参的话,则会有如下报错:
TypeError: MyFirstFun() takes exactly 1 argument (0 given)
报错信息的意思其实很明显,告诉我们需要传入一个参数进来。
(2)关健字参数
关键字参数和函数调用关系紧密,函数调用使用关键字参数来确定传入的参数值。使用关键字参数时,允许函数调用时参数的顺序与声明时的不一致,因为Python解释器能够用参数名匹配参数值。
这里举个简单的函数例子,如下:
# -*- coding: UTF-8 -*- def SaySomething(name,word): print name + '→' + word SaySomething('余洪春','一枚码农')
执行这段代码的结果如下:
余洪春 → 一枚码农
如果这里把SaySomething函数中的内容调换了呢?例如:
SaySomething('一枚码农','余洪春')
则输出结果如下:
一枚码农 → 余洪春
很明显,这个结果不是我们想要的,这时就可以利用关键字参数来确定传入的参数值,如下:
SaySomething(word='一枚码农',name='余洪春')
大家可以发现,即使顺序改变了,也可达到我们想要的结果:
余洪春一枚码农
(3)默认参数
默认参数也叫缺省参数。调用函数时,默认参数的值如果没有传入,则被认为是默认值。
这里举个简单的函数例子:
# -*- coding: UTF-8 -*- #可写函数说明 def printinfo(name, age = 35): print "Name: ", name; print "Age:", age; return; #调用printinfo函数 printinfo(age=50, name="cc") printinfo(name="cc")
执行这段代码的结果如下:
Name: cc Age 50 Name: cc Age: 35
我们通过观察可以得知,在printinfo(name="cc")中是没有输入age参数值的,但在执行代码的时候,name=cc一样输出了默认的age值,也就是之前设定的age=35。
(4)不定长参数
大家可能需要用一个函数来处理比当初声明时更多的参数。这些参数叫作不定长参数,和上述参数不同,它进行声明时不会命名。基本语法如下:
def functionname(*var): 函数体 return [expression]
加了星号(*)的变量名会存放所有未命名的变量参数,这里举一个简单的函数例子:
# -*- coding: UTF-8 -*- def testparams(*params): print "参数的长度是:",len(params) print "第一个参数是:",params[0] print "第二个参数是:",params[1] print "打印所有的输入实参:",params testparams('cc',1,2,3)
此段代码的输出结果为:
参数的长度是: 4 第一个参数是: cc 第二个参数是: 1 打印所有的输入实参: ('cc', 1, 2, 3)
大家可以清楚地看到,我们输入的实参'cc',1,2,3已经全部被赋值给params变量了,并且被正确打印出来了。
2.函数返回值(return语句)
return语句表示从Python函数返回一个值,在介绍自定义函数时有讲过,每个函数都要有一个返回值。Python中的return语句有什么作用,下面仔细地讲解一下。
Python函数中一定要有return返回值才是完整的函数。如果没有该返回值,那么得到的结果是None对象,而None表示没有任何值。
return是返回数值的意思,比如定义两个函数,一个是有返回值,另一个用print语句,看看结果有什么不同。
# -*- coding: UTF-8 -*- def func1(x,y): print x+y result = func1(2,3) result is None
当函数没有显式return时,默认返回None值,大家可以观察一下此段代码的返回结果:
True
另一个有返回值return的函数如下:
# -*- coding: UTF-8 -*- def func2(x,y): return x + y #python函数返回值 result = func2(2,3) result is None
传入参数后得到的结果不是None值,会得到如下输出结果:
False
另外,Python的return是支持多返回值的,这里举个简单的例子:
def func(a,b): c = a + b return a,b,c x,y,z = func(1,2) print x,y,z
观察输出结果可知,x、y、z的值都正确输出了:
1,2,3
3.函数变量作用域
一个程序的所有变量并不是在哪个位置都可以访问的。访问权限取决于这个变量是在哪里赋值的。
变量的作用域决定了在程序的哪一部分你可以访问哪个特定的变量名称。两种最基本的变量作用域如下:
·全局变量
·局部变量
定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。
局部变量只能在其被声明的函数内部访问,而全局变量则可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入作用域中。
如果我们要在函数内部使用全局变量,可以使用global实现这一功能,详细代码如下:
# -*- coding: UTF-8 -*- def func(): global x print 'x:', x x = 2 y = 1 print 'Changed local x to:', x print 'global',globals() print 'local',locals() x = 50 func() print 'Value of x is:', x
程序输出结果如下:
x: 50 Changed local x to: 2 global {'__builtins__': <module '__builtin__' (built-in)>, '__file__': 'test8.py', '__package__': None, 'func': <function func at 0x7fb2b6196668>, 'x': 2, '__name__': '__main__', '__doc__': None} local {} Value of x is: 2
另外,这里有个概念需要理解,即Python的Namespace(命名空间)。
Namespace只是从名字到对象的一个映射。大部分Namespace都是按Python中的字典来实现的。从某种意义上来说,一个对象(Object)的所有属性(attribute)也构成了一个Namespace。在程序执行期间,会有多个名空间同时存在。不同Namespace的创建/销毁时间也不同。
注意
两个不同的Namespace中,名字相同的两个变量之间没有任何联系。
接下来看一下Python中Namespace的查找顺序。
Python中通过提供Namespace来实现重名函数/方法、变量等信息的识别,一共有如下三种Namespace。
·local Namespace:作用范围为当前函数或者类方法。
·Global Namespace:作用范围为当前模块。
·Build-In Namespace:作用范围为所有模块。
当函数/方法、变量等信息发生重名时,Python会按照“local Namespace→Global Namespace→Build-In Namespace”的顺序搜索用户所需元素,并且以第一个找到此元素的Namespace为准。
4.Python内部函数和闭包
Python内部函数、闭包的共同之处在于都是以函数作为参数传递到函数的,不同之处在于返回与调用有所区别。
(1)Python内部函数
当需要在函数内部多次执行复杂任务时,内部函数非常有用,它可避免循环和代码的堆叠重复。示例如下:
在Python中创建一个闭包可以归结为以下三点:
·闭包函数必须有内嵌函数。
·内嵌函数需要引用该嵌套函数上一级Namespace中的变量。
·闭包函数必须返回内嵌函数。
通过这三点,就可以创建一个Python闭包了。
5.匿名函数
Python使用lambda来创建匿名函数,其语法为:
lambda 变量1,变量2:表达式
这里可以举个简单的例子说明其用法:
sum = lambda x,y:x+y print sum(1,11) print sum(7,18)
输出结果为:
12 25
匿名函数的特征为:
·lambda的主体是一个表达式,仅在lambda中封装有限的逻辑进去;
·lambda只是一个表达式,函数体比def简单很多;
·lambda的目的是调用小函数时不占用栈内存,从而增加运算效率;
·lambda并不会使程序运行效率提高,只会使代码更简洁。
事实上,既然这里提到了lambda,就不得不提一下Python的函数式编程,因为lambda函数在函数式编程中经常用到。对于函数式编程,简单来说,其特点就是允许把函数本身作为参数传入另一个函数,还允许返回一个函数。
Python中用于函数式编程的主要是4个基础函数(map、reduce、filter和sorted)和1个算子(即lambda)。
函数式编程的好处:
·代码更为简洁。
·代码中没有了循环体,少了很多临时变量(纯粹的函数式编程语言编写的函数是没有变量的),逻辑更为简单明了。
·数据集、操作和返回值都放在一起了。
下面通过示例来说明它的用法。
(1)map()函数
map()函数的语法如下:
map(函数,序列)
我们用求平方的例子来说明其用法,代码如下:
#encoding:utf-8 #求数字1~9的平方数 squares = map(lambda x:x*x,[1,2,3,4,5,7,8,9]) print squares
代码执行后输出结果如下:
[1, 4, 9, 16, 25, 49, 64, 81]
(2)reduce()函数
reduce()函数的语法如下:
reduce(函数,序列)
用reduce实现阶乘是非常容易的事,示例如下:
#encoding:utf-8 #数字9阶乘 #9!=9*8*7*6*5*4*3*2*1 print reduce(lambda x,y: x*y, range(1,9))
输出结果如下:
40320
6.生成器
通过Python列表生成式可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用的存储空间很大,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。实际工作中会经常遇到这种需求。
此外,提到生成器(Generator),总会不可避免地要把迭代器拉出来对比着讲,生成器在行为上和迭代器非常类似,二者功能上差不多,但是生成器更优雅。
顾名思义,迭代器就是用于迭代操作(for循环)的对象,它像列表一样可以迭代获取其中的每一个元素,任何实现了__next__方法的对象都可以称为迭代器。它与列表的区别在于,构建迭代器的时候,不像列表把所有元素一次性加载到内存,而是以一种延迟计算(lazy evaluation)的方式返回元素,这正是它的优点。比如列表含有1000万个整数,需要占超过400MB的内存,而迭代器只需要几十字节的空间。因为它并没有把所有元素装载到内存中,而是等到调用next方法时才返回该元素(按需调用即call by need的方式,本质上for循环就是不断地调用迭代器的next方法)。
了解了迭代器,那什么是生成器呢?
如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list了,可节省大量的空间。在Python中,这种一边循环一边计算的机制,就称为生成器。
创建生成器的方法很多。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个Generator,示例如下:
g = (x*2 for x in range(10)) l = [x*2 for x in range(10)] type(g) type(l)
如果要把元素一个个地打印出来,可以通过Generator的next()方法,即每次调用g.next(),就计算出下一个元素的值,直到计算到最后一个元素时,抛出StopIteration错误。
当然,如果要打印Generator中的每一个元素,用for循环就够了。
In [5]: for num in g: ...: print num ...: 0 2 4 6 8 10 12 14 16 18
那么在什么场景下需要使用序列,什么场景下要使用Generator呢?
当程序需要较高的性能或一次只需要一个值进行处理时,使用Generator函数;当需要一次获取一组元素的值时,使用序列。
事实上,在创建了一个Generator后,基本上不会调用next()方法,而是会通过for循环来迭代它。Generator非常强大,如果推算的算法比较复杂,用类似列表生成式的for循环无法实现时,还可以用函数来实现。比如,著名的斐波拉契数列,除第一个和第二个数外,任意一个数都可由前两个数相加得到。可以用下面的函数来实现:
def fib(max): n, a, b = 0, 0, 1 while n < max: yield b a, b = b, a + b n = n + 1
这就是定义Generator的另一种方法。如果一个函数的定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个Generator。试着执行一下:
In [7]: fib(5)
输出结果为:
Out[7]: <generator object fib at 0x02CEC490>
事实上,很多时候我们可以利用Generator来打开大文件,比如说超过10个GB的日志文件,可以使用yield生成自定义可迭代对象,即generator,每一个带有yield的函数就是一个Generator。它会将文件切分成小段,每次处理完一小段内容后,释放内存。可以参考下面的代码:
#-*- coding:utf-8 -*- def read_in_block(file_path): BLOCK_SIZE = 1024 with open(file_path, "r") as f: while True: block = f.read(BLOCK_SIZE) #每次读取固定长度到内存缓冲区 if block: yield block else: return #如果读取到文件末尾,则退出 def test(): file_path = "/tmp/test.log" for block in read_in_block(file_path): print block
当然,Python下面有更优雅和简洁的处理方法,那就是使用系统自带方法with open()生成迭代对象,使用方式如下:
with open(filename, 'rb') as f: for line in f: <do something with the line>
对可迭代对象f进行迭代遍历(即for line in f语句)时,会自动地使用缓冲I/O(buffered I/O)及内存管理,因此不用担心任何大文件的问题。让系统来处理,其实是最简单的方式。
另外,这里要注意yield与return的区别,来看一段程序:
#-*- coding:utf-8 -*- def func(n): for i in range(n): return i def func2(n): for i in range(n): yield i print func(3) f = func2(3) print f print f.next() print f.next() print f.next()
第4行代码直接返回i的值,循环语句将被中止,整个程序到此结束。
第7行代码循环生成n个数字,循环语句不会被中止。
最后提一个知识点,工作中我们经常会遇到一个需求——Python中如何优雅地处理命令行参数?
Python中有专门针对这种需求的getopt模块,该模块是专门用来处理命令行参数的。
函数getopt的具体格式如下:
getopt(args, shortopts, longopts = [])
参数args即sys.argv[1:],shortopts表示短格式(-),longopts表示长格式(--)。
我们需要一个conv.py脚本,它的作用是接收IP和port端口号,要求该脚本满足以下条件:
·通过-i或-p选项来区别脚本后面接的是IP还是port;
·当不知道convert.py需要哪些参数时,用-h打印出帮助信息。
这里可以用个简单的脚本来说明:
#!/usr/bin/python import getopt import sys def usage(): print ' -h help \n' \ ' -i ip address\n' \ ' -p port number\n' \ '' if __name__ == '__main__': try: options, args = getopt.getopt(sys.argv[1:], "hp:i:", ['help', "ip=", "port="]) for name, value in options: if name in ('-h', '--help'): usage() elif name in ('-i', '--ip'): print value elif name in ('-p', '--port'): print value except getopt.GetoptError: usage()
上述脚本的说明如下:
1)真正处理逻辑所使用的函数叫getopt(),因为是直接使用import导入的getopt模块,所以要加上限定getopt才可以,同理,这里也要导入sys模块。
2)使用sys.argv[1:]过滤掉第一个参数(它是执行脚本的名字,不应算作参数的一部分)。
3)使用短格式分析串"hp:i:"。当一个选项只是表示开关状态时,即后面不带附加参数时,在分析串中写入选项字符。当选项后面带一个附加参数时,在分析串中写入选项字符的同时在后面再加一个":"号。所以"hp:i:"就表示"h"是一个开关选项;"p:"和"i"则表示后面应该带一个参数。
4)使用长格式分析串列表['help',"ip=","port="]。长格式串也可以有开关状态,即后面不跟等号。如果跟一个等号则表示后面还应有一个参数。这个长格式表示help是一个开关选项,ip=和output=则表示后面应该带一个参数。
5)调用getopt函数。函数返回两个列表:opts和args。opts为分析出的格式信息,args为不属于格式信息的剩余命令行参数。opts是一个两元组的列表。每个元素均为选项串,附加参数。如果没有附加参数则为空串''或'' ''。
6)整个过程使用异常来处理,当分析出错时,就可以打印出信息来通知用户如何使用这个程序。