8.1 函数的定义和调用
8.1.1 定义函数
函数是组织好的,可重复使用的,用来实现单一或相关联功能的代码段。函数能提高应用的模块性和代码的重复利用率。在Python中有很多内置函数(例如print()函数),编程人员也可以创建自己的自定义函数来提高自己的工作效率。
定义一个自定义函数需要遵守以下规则:
① 函数代码块以def关键词开头,后接函数标识符名称和圆括号();
② 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数;
③ 函数的第一行语句可以选择性地使用文档字符串-用于存放函数说明;
④ 函数内容以冒号起始,并且缩进;
⑤ return[表达式]结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回None。
在Python中定义函数需要使用def关键字,默认情况下,参数值和参数名称是按函数声明中定义的顺序匹配起来的。它的语法格式如下:
l def 函数名(参数列表):
函数体
和定义变量名称一样,定义一个函数时,函数名称必须以字母开头,可以包括下划线”_”。在定义函数名称时,要注意不能把Python中的关键字定义成函数的名称。函数内的语句数量是随意的,每个语句至少有一个空格的缩进来表示该语句属于这个函数。函数体必须保持缩进一致,因为在函数中,缩进结束就表示函数结束。
建议一个自定义函数的方法示例如下:
#!/usr/bin/python3
# 计算面积函数
def area(width, height): #建立一个函数area()
return width * height
def print_welcome(name): #建立一个函数print_welcome()
print("Welcome", name)
print_welcome("Python")
w = 4
h = 5
print("width =", w, " height =", h, " area =", area(w, h))
执行后输出结果如下:
Welcome Python
width = 4 height = 5 area = 20
还可以用lambda表达式来声明匿名函数(即没有函数名字的临时使用的小函数)。lambda表达式仅可以包含一个表达式,不允许包含其他复杂的语句,但在表达式中可以调用其他函数,并支持默认值参数和关键参数。该表达式的计算结果相当于函数的返回值。下面简单演示一下如何使用lambda函数实现自定义排序:
class People :
age = 0
gender = 'male'
def __init__( self , age , gender ):
self.age = age
self.gender = gender
def toString (self):
return 'Age:' + str ( self . age ) + ' /t Gender:' + self . gender
List=[People(21,'male'),People(20,'famale'),People(34,'male'),People(19,'famale')]
print('Befor sort:')
for p in List :
print(p.toString ())
List.sort ( lambda p1,p2 : cmp ( p1.age,p2.age))
print('/n After ascending sort:')
for p in List :
print(p.toString ())
List.sort (lambda p1,p2:-cmp(p1.age,p2.age))
print('/n After descending sort:')
for p in List :
print(p.toString ())
上面的代码定义了一个People类,并通过lambda函数,实现了对包含People类对象的列表按照People的年龄,进行升序和降序排列。运行结果如下:
Befor sort:
Age:21 Gender:male
Age:20 Gender:famale
Age:34 Gender:male
Age:19 Gender:famale
After ascending sort:
Age:19 Gender:famale
Age:20 Gender:famale
Age:21 Gender:male
Age:34 Gender:male
After descending sort:
Age:34 Gender:male
Age:21 Gender:male
Age:20 Gender:famale
Age:19 Gender:famale
def与lambda的主要不同点是def是语句而lambda是表达式,首先在Python里面语句是可以嵌套的,如果需要根据某个条件来定义方法话,只能用def,如果使用了lambda表达式,系统会进行报错。而有的时候需要在Python表达式里操作的时候,那需要用到表达式嵌套,这个时候def语句不能得到想要的结果,就只能用lambda表达式了。在创建匿名函数时,lambda会创建一个函数对象,但不会把这个函数对象赋给一个标识符,而def则会把函数对象赋值给一个变量。
8.1.2 调用函数
给函数一个名称,并指定函数里包含的参数和代码块结构,就可以定义一个函数。当函数的基本结构完成以后,就可以通过另一个函数调用执行,也可以直接从Python命令提示符执行。调用函数的方式示例如下:
#!/usr/bin/python3
#定义函数
def printme( str ):
"打印任何传入的字符串"
print (str)
return
#调用函数
printme("我要调用用户自定义函数!")
printme("再次调用同一函数")
函数执行后所得输出结果如下:
我要调用用户自定义函数!
再次调用同一函数
8.2.3 递归函数
递归,在科学与计算机中,是指在函数的定义中使用函数自身的方法。在使用递归函数时,需要注意以下几点:
① 递归就是在过程或函数里调用自身;
② 必须有一个明确的递归结束条件,称为递归出口;
③ 当函数直接返回值时有基本实例(最小可能性问题);
④ 递归实例,包括一个或多个问题最小部分的递归调用;
函数每次被调用时都会创建一个新命名空间,当程序调用自身时,实际上运行的时两个不同函数(一个函数具有两个不同的命名空间)。使用递归关键在于将问题分解为小部分,递归不能永远继续下去,因为它总是以最小可能性问题结束,而这些问题又存储在基本实例中。以下是一个递归实例,计算阶乘n! = 1*2*3*```*n,用函数fact(n)表示,代码示例如下:
>>> def fact(n)
if n == 1:
return 1
else:
return n*fact(n - 1)
>>> fact(1)
1
>>> fact(10)
3628800
如上方代码所示,当使用正整数对fact()函数进行调用时,就会通过递减数字来递归的调用自己,利用if进行判定,当数字n减少至1时,就终止程序进行结果输出,递归结束。
递归函数有利有弊,它的优点有:
① 递归使代码看起来更为整洁,优雅;
② 可以用递归将复杂任务分解成更简单的子问题;
③ 使用递归比使用一些嵌套迭代更容易一些。
它的缺点有:
① 递归的逻辑很难对其进行调试、跟进;
② 因为占用了大量的内存和时间,所递归调用的代价高昂(效率低)。
8.2 函数参数
8.2.1 形参与实参概念
在Python中,有两种类型参数,分别是函数定义里的形参,和调用函数时传入的实参。
经常在使用一些内置函数时需要传入参数,如调用math.sin时,需要传入一个整型数字作为实参。有的函数需要多个参数,如math.pow需要两个参数,一个是基数,另一个是指数。在函数内部,会将实参的值赋给形参,例如:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
def personinfo(age,name)
print(“年龄是:”,age)
print(“名称是:”,name)
return
在该函数中,函数名personinfo后面的参数列表age和name就是实参,在函数体中分别将age和name的值传递给age和name,函数体中的age和name就是形参。需要注意的是,在函数体中都是对形参进行操作的,不能操作实参,也就是说不能对实参作出更改。
内置函数的组合规则在自定义函数上同样适用。例如,对自定义的personinfo函数可以使用任何表达式作为实参,示例代码如下:
personinfo(21,’kim king’*2)
执行结果如下:
年龄:21
名称:kim kingkim king
由执行结果可以看到,可以用字符串的乘法表达式作为实参。
在Python中,作为实参的表达式会在函数调用前执行。例如,在上面的示例中,实际上先执行’kim king’*2的操作,将执行结果作为一个实参传递到函数体中。
8.2.2 位置参数
位置参数,又称为必须参数,在调用函数时需要函数定义的参数位置和顺序来传递参数。调用时数量必须和声明时一致。示例代码如下:
#!/usr/bin/env python
# coding=utf-8
def print_hello(name, sex):
sex_dict = {1: u'先生', 2: u'女士'}
print('hello %s %s,welcome to python world!'%(name,sex_dict.get(sex,u'先生')))
#两个参数的顺序必须一一对应,且必须传递对应正确个数的参数。
#print_hello('tanggu', 1)
8.2.3 关键字参数
关键字参数和函数调用关系紧密,函数调用使用关键字参数确定传入的参数值。
使用关键字参数允许调用函数时参数的顺序与声明时的顺序不同,因为Python解释器能够用参数名匹配参数值。用关键字参数进行函数调用时,通过”键-值”形式加以指定,可以让函数更加清晰、容易使用,同时也清除了参数的顺序需求。运用关键字参数调用函数的示例代码如下:
# 以下是用关键字参数正确调用函数的实例
# print_hello('tanggu', sex=1)
# print_hello(name='tanggu', sex=1)
# print_hello(sex=1, name='tanggu')
# 以下是错误的调用方式
# print_hello(1, name='tanggu')
# print_hello(name='tanggu', 1)
# print_hello(sex=1, 'tanggu')
通过上面的代码可以看出,有位置参数时,位置参数必须在关键字参数的前面,但关键字参数之间不存在先后顺序。
8.2.4 默认值参数
默认参数用于定义函数,为参数提供默认值,调用函数时可传可不传该默认参数的值,需要注意的是,所有位置参数都必须出现在默认参数前,包括函数定义和调用。在设置默认参数时,如果不传入默认参数值,无论有多少个默认参数,Python解释器都会使用默认值。若要更改某一个默认参数值,又不想传入其它默认参数,且该默认参数的位置不是第一个,则可以通过参数名更改想要更改的默认参数值。如果有一个默认参数通过传入参数名更改参数值,则其它想要更改的默认参数都需要传入参数来更改参数值,否则会报错。当更改默认参数值时,传入默认参数的顺序不需要根据定义的函数中默认参数的顺序传入,不过最好同时传入参数名,否则容易出现执行结果和预期不一致的情况。使用默认参数对程序进行调用的示例代码如下:
#正确的默认参数定义方式--> 位置参数在前,默认参数在后
def print_hello(name, sex=1):
····
#错误的定义方式
def print_hello(sex=1, name):
····
#调用时不传sex的值,则使用默认值1
#print_hello('tanggu')
#调用时传入sex的值,并指定为2
#print_hello('tanggu', 2)
8.2.5 长度可变参数
当需要一个函数能够处理的参数声明更多时,这些参数就被称为长度可变参数。与前面个所描述的参数不同,长度可变参数声明时不会命名,它的基本语法如下:
def functionname([formal_args,]*var_args_tuple):
“函数_文档字符串”
function_suite
return [experssion]
加了星号(*)的变量名会存放所有未命名的变量参数,如果变量参数在函数调用时没有指定参数,就是一个空元组,在使用长度可变参数时也可以不想可变函数传递未命名的变量。
长度可变参数有两种,分别是包裹位置参数和包裹关键字参数,用这两种可变参数来进行参数的传递会显得非常方便。
(1) 包裹位置参数
使用包裹位置参数传递时,传进的所有参数都会被args变量收集,它会根据传进参数的位置合并成一个元组,args是元组类型,这就是包裹位置传递。它的使用方法示例如下:
def func(*args):
....
# func()
# func(a)
# func(a, b, c)
(2) 包裹关键字参数
kargs是一个字典,用于收集所有关键字参数。此方法的使用方式示例如下:
def func(**kargs):
....
# func(a=1)
# func(a=1, b=2, c=3)
*和**,也可以在函数调用时使用,此种使用方式称之为解包裹。此处以下代码段为例对解包裹的使用方式进行展示。
在传递元组时,让元组的每个元素对应一个位置参数,示例如下:
def print_hello(name, sex):
print(name, sex)
#args = ('kim king', '女')
#print_hello(*args)
#kim king 女
在传递字典时,让字典的每个键/值对作为一个关键字参数传递给函数,示例代码如下:
def print_hello(kargs):
print(kargs)
#kargs = {'name': 'tanggu', 'sex', u'男'}
#print_hello(**kargs)
#{'name': 'tanggu', 'sex', u'男'}
在对位置参数,默认参数,可变参数进行混合使用时,要注意他们的定义和调用的顺序都应遵从先位置参数,再默认参数,再包裹位置参数,最后包裹关键字参数的顺序,这是基本原则切记不要用错。
8.3 变量作用域
变量起作用的代码范围称为变量的作用域,不同作用域内同名变量之间互不影响。一个变量在函数外部定义和在函数内部定义,其作用域是不同的,函数内部定义的变量一般为局部变量,在函数外部定义的变量为全局变量。
局部变量的空间是在栈上分配的,而栈空间是由操作系统进行维护的,每当调用一个函数时,操作系统会为其分配一个栈帧,函数调用结束后立即释放这个栈帧。因此函数调用结束后,该函数内部所有的局部变量都不再存在。因此在函数内定义的普通变量只在该函数内起作用,当函数运行结束后,在其内部定义的局部变量将被自动删除而不可访问。在函数内部定义的全局变量当函数结束以后仍然存在并且可以继续访问。
如果想要在函数内部修改一个定义在函数外的变量值,那么这个变量就不能是局部变量了,其作用域必须是全局。可以砸函数内用global关键字来声明或定义全局变量,要这分为两种情况。
第一种情况为一个变量已经在函数外定义,如果在函数内需要修改这个变量的值,并将这个赋值结果反映到函数之外,可以在函数内用global明确声明要使用已定义的同名全局变量。
第二种情况为在函数内部直接使用global关键字将一个变量声明为全局变量,如果在函数中没有定义该全局变量,在调用这个函数之后,会自动增加新的全局变量。
或者可以这样来进行理解:在函数内如果只引用某个变量的值而没有为其赋新值,该变量为(隐式的)全局变量;如果在函数内任意位置尤为变量赋值的操作,该变量即被认为是(隐式的)局部变量,除非在函数内显式地用关键字global进行声明。下面的代码演示了局部变量和全局变量的用法:
>>> def demo():
global x #声明或创建全局变量
x = 3 #修改全局变量的值
y = 4 #局部变量
print(x,y)
>>> x = 5 #在函数外部定义了全局变量x
>>> demo() #本次调用修改了全局变量x的值
3 4
>>> x
3
>>> y #局部变量在函数运行结束后自动删除
Traceback(most recent call last):
File”<pyshell#11>”,line 1,in <module>
y
NameError:name’y’ is not defined
>>> del x #删除了全局变量x
>>> x
Traceback(most recent call last):
File”<pyshell#13>”,line 1,in <module>
x
NameError:name’x’ is not defined
>>> demo() #本次调用创建了全局变量
3 4
>>> x
3
>>> y #局部变量在函数调用结束后自动删除
Traceback(most recent call last):
File”<pyshell#11>”,line 1,in <module>
y
NameError:name’y’ is not defined
如果局部变量与全局变量具有相同的名字,那么该局部变量会在自己的作用域内隐藏同名的全局变量。如果需要在同一个程序的不同模块之间共享全局变量,可以编写一个专门的模块来实现这一目的。
一般而言,局部变量的引用回避全局变量速度快,应优先考虑使用。同时应尽量避免过多的使用全局变量,因为全局变量会增加不同函数之间的隐式耦合度,降低代码的可读性,并使得代码测试和纠错变的很困难。
8.4 函数返回值
函数的返回值是函数重要的组成部分。函数的根本在于实现程序的部分功能,所以很多时候需要将函数执行后的结果返回给程序再由程序作出进一步的操作。可以说是函数的返回值令函数与函数之间,函数与主程序之间更加紧密的联系起来。
在Python的函数中都有一个返回值,默认为None(不使用return语句进行结束返回时)。若果要返回一个None,可以只写一个return,但要返回具体的数值,就需要在return后面加上需要返回的内容。对于函数定义来说,使用return语句可以向外提供该函数执行的一些操作结果;对于函数的调用者来说,是否可以使用函数中执行的一些操作结果,就在于函数是否使用return语句返回了对应的执行结果。
在Python中,有的函数会产生结果(例如数学函数),就称这种函数为有返回值函数;有的函数执行一些动作后不反悔任何值,就称这种函数为无返回值函数。当调用有返回值函数时,可以使用返回的结果做相关操作;当使用无返回值函或返回None的函数时,只能得到一个None值。函数返回值的使用方式示例如下:
#!/usr/bin/env python
def testReturn(input1,input2):
sum = input1 + input2
return [sum,input1,input2]
calculation = testReturn(1,2)
x,y,z = testReturn(1,2)
print calculation
print(x)
print(y)
print(z)
执行完毕后输出结果如下:
[3, 1, 2]
3
1
2
一个函数除了可以返回值以外,还可以返回一个函数。返回一个函数的操作模式称之为闭包,在下节会做详细介绍。
8.5 函数嵌套定义、闭包、装饰器
(1) 函数嵌套定义
Python允许在定义函数时,其函数体内又包含另一个函数的完整定义,这就是函数嵌套。
函数是用def语句定义的,凡是其他语句出现的地方,def语句同样可以出现。像这样定义在其他函数内的函数叫做内部函数,内部函数所在的函数叫做外部函数。也可以进行多层嵌套,这样的话,除了最外层和最内层的函数外,其他函数既是外部函数也是内部函数。使用嵌套函数进行编码的示例如下:
#!/usr/bin/python
# -*- coding utf8 -*-
#函数的嵌套分为函数的嵌套调用,函数的嵌套定义
def max2(x,y):
return x if x > y else y
def max4(a,b,c,d):
res1 = max2(a,b)
res2 = max2(res1,c)
res3 = max2(res2,d)
return res3
print(max4(10,3,443,345))
#h嵌套定义
def f1():
def f2():
print('from f2')
def f3():
print('from f3')
f3()
f2()
f1()
执行上方代码后所得输出结果如下:
443
from f2
from f3
(2) 闭包
闭包是函数式编程中重要的语法结构,它提高了代码的可重用性。不同语言实现闭包的方式不同,Python是以函数对象为基础,为闭包这一语法结构提供支持的。函数在Python中是第一类对象,即函数可以作为一个变量的值,也可以作为另一个函数的参数或返回值。除此之外,函数还可以嵌套定义。需要注意的是,函数对象也有其作用域。
把函数当作数据处理时,它将显式地携带与定义该函数的周围环境的相关信息。这将影响到函数中自由变量的绑定方式。
对于嵌套函数来说,如果一个内部函数对外部作用域中的变量进行了引用,那么内部函数就被认为是闭包(closure)。内部函数由一个名字(变量)来指代,而这个名字(变量)对于“外层”包含它的函数而言,是本地变量。使用嵌套函数时,闭包将捕获内部函数执行所需的整个环境,在外部函数返回后,闭包中的局部变量也仍然继续存在,并能被被内部函数继续引用。
所有函数都拥有两个与闭包相关的重要属性:
l 指向了定义该函数的全局命名空间的__globals__属性,这始终对应于定义函数的闭包模块。
l __closure__属性,包含函数引用的除全局变量之外的自由变量信息。
在Python中创建一个闭包可以归结为以下三点:
l 闭包函数必须有内嵌函数
l 内嵌函数需要引用该嵌套函数上一级namespace中的变量
l 闭包函数必须返回内嵌函数
用以下的例子来解释如何创建闭包:
def fun_closule(y):
#闭包的作用: 当外部函数返回了, 外部函数的局部变量还可以被内部函数引用
print('id(num): %X', id(y))
def tmp(x):
return x * y
print('id(tmp): %X', repr(tmp))
return tmp
if __name__ == '__main__':
closule = fun_closule(4)
print('id(closule): ', repr(closule))
#当删除了fun_closule对象后, 外部参数还可以被内部函数引用.
del fun_closule
print(closule(2))
上方代码执行后输出结果如下:
<span style="font-size:18px;">id(num): %X 140186159726672
id(tmp): %X <function tmp at 0x101c5f398>
id(closule): <function tmp at 0x101c5f398>
8</span>
从结果可以看出,当fun_closule(4)执行后,创建和返回了closule这个函数对象,内存地址是0x101c5f398,并且发现tmp内部函数和它的内存地址相同,即closule只是这个函数对象的一个引用。
(3) 装饰器
装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。有了装饰器,就可以抽离大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。装饰器是用闭包来实现的,它的基本语法如下:
l def foo():
print(foo)
装饰器分为无参装饰器,带参数装饰器,类装饰器,内置装饰器四类。
① 无参装饰器
foo()函数被用作装饰器,其本身接收一个函数对象作为参数,然后做一些工作后,返回接收的参数,供外界调用。需要时刻注意的是@foo只是一个语法糖,其本质是foo = bar(foo)。此种方式的示例代码如下:
def foo(func):
print('decorator foo')
return func
@foo
def bar():
print('bar')
bar()
② 带参数装饰器
用一段代码来解释带参数装饰器,示例代码如下:
import time
def function_performance_statistics(trace_this=True):
if trace_this:
def performace_statistics_delegate(func):
def counter(*args, **kwargs):
start = time.clock()
func(*args, **kwargs)
end =time.clock()
print('used time: %d' % (end - start, ))
return counter
else:
def performace_statistics_delegate(func):
return func
return performace_statistics_delegate
@function_performance_statistics(True)
def add(x, y):
time.sleep(3)
print('add result: %d' % (x + y,))
@function_performance_statistics(False)
def mul(x, y=1):
print('mul result: %d' % (x * y,))
add(1, 1)
mul(10)
上述代码想要实现一个性能分析器,并接收一个参数,来控制性能分析器是否生效,其运行效果如下所示:
add result: 2
used time: 0
mul result: 10
③ 类装饰器
类装饰器相比函数装饰器,具有灵活度大,高内聚、封装性等优点。其实现起来主要是靠类内部的 __call__ 方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法,下面是一个实例:
class Foo(object):
def __init__(self, func):
super(Foo, self).__init__()
self._func = func
def __call__(self):
print('class decorator')
self._func()
@Foo
def bar():
print('bar')
bar()
运行结果如下:
class decorator
bar
④ 内置装饰器
Python中内置的装饰器有三个: staticmethod、classmethod 和property。
staticmethod 是类静态方法,其跟成员方法的区别是没有 self 指针,并且可以在类不进行实例化的情况下调用。property 是属性的意思,即可以通过通过类实例直接访问的信息。如果使用老式的Python类定义,所声明的属性不是 read only的,下面代码说明了这种情况:
class Foo:
def __init__(self, var):
self._var = var
@property
def var(self):
return self._var
foo = Foo('var 1')
print(foo.var)
foo.var = 'var 2'
print(foo.var)
其运行结果如下:
var 1
var2