第5章 函数和函数式编程
5.1 引言
函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段。函数能提高应用的模块性,和代码的重复利用率。在Python中有很多内建函数,比如print()。当然随着学习的深入,你也可以学会创建对自己有用的函数(用户自定义函数)。简单的理解下函数的概念,就是用户编写了一些语句,为了方便使用这些语句,把这些语句组合在一起,给它起一个名字。使用的时候只要调用这个名字,就可以实现语句组的功能了。
在没用过函数之前,我们要计算一个数的幂时会用到**,方法是这样的:
>>>2**3
8 #此处为[**python 函数返回值**](http://www.iplaypy.com/jinjie/return.html "python 返回值")
现在知道了函数,就可以用内建函数pow来计算乘方了:
>>>pow(2,3)
8
5.2 调用函数
python系统中自带的一些函数就叫做内建函数,比如:dir()、type()等等,不需要我们自己编写。还有一种是第三方函数,就是其它程序员编好的一些函数,共享给大家使用。这两种函数都是拿来就可以直接使用的。最后就是我们自己编些的方便自己工作学习用的函数,就叫做自定义函数了。
Python内置了很多有用的函数,我们可以直接调用。要调用一个函数,需要知道函数的名称和参数,比如求绝对值的函数abs
,只有一个参数。可以直接从Python的官方网站查看文档:
http://docs.python.org/3/library/functions.html#abs
也可以在交互式命令行通过help(abs)
查看abs
函数的帮助信息。
调用abs
函数:
>>> abs(100)
100
>>> abs(-10)
10
比如max函数可以接收任意多个参数,并返回最大的那个:
>>> max(1, 2)
2
>>> max(-2, 0, 1, 4)
4
同时在调用函数的时候,需要注意传入的参数数量是否符合要求。例如当调用abs函数时,若传入的参数数量不对,会报出TypeError的错误,并且Python会明确地告诉你:abs()有且仅有1个参数,但给出了两个:
>>> abs(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: abs() takes exactly one argument (2 given)
如果传入的参数数量是对的,但参数类型不能被函数所接受,也会报TypeError的错误,并且给出错误信息:str是错误的参数类型:
>>> abs('a')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'
数据类型转换
Python内置的常用函数还包括数据类型转换函数,比如int()函数可以把其他数据类型转换为整数:
>>> int('12')
12
>>> int(12.3)
12
>>> float('12.3')
12.3
>>> str(1.23)
'1.23'
>>> str(10)
'10'
>>> bool(1)
True
>>> bool('')
False
也可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”:
>>> a = abs # 变量a指向abs函数
>>> a(-1) # 通过a调用abs函数
1
定义一个函数:给了函数一个名称,指定了函数里包含的参数,和代码块结构。
这个函数的基本结构完成以后,用户可以通过另一个函数调用执行,也可以直接从 Python 命令提示符执行。
如下实例调用了 printme() 函数:
#!/usr/bin/python3
# 定义函数
def printme( str ):
"打印传入的字符串"
print (str);
return;
# 调用函数
printme("调用用户自定义函数!");
printme("再次调用同一函数");
调用用户自定义函数!
再次调用同一函数
小结
调用Python的函数,需要根据函数定义,传入正确的参数。
如果函数调用出错,一定要注意看错误信息!
5.3 定义函数
在Python中,定义一个具有特定功能的函数需要符合一定规则:
- 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()。
- 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
- 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
函数内容以冒号起始,并且缩进。 - return [表达式] 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回 None。
一般格式如下:
def 函数名(参数列表):
函数体
默认情况下,参数值和参数名称是按函数声明中定义的的顺序匹配起来的。
我们以自定义一个输出"Hello World!"的函数为例:
>>> def hello() :
print("Hello World!")
>>> hello()
Hello World!
在更复杂的应用中,函数中带上参数变量:
#!/usr/bin/python3
# 计算面积函数
def area(width, height):
return width * height
def print_welcome(name):
print("Welcome", name)
print_welcome("Runoob")
w = 4
h = 5
print("width =", w, " height =", h, " area =", area(w, h))
Welcome Runoob
width = 4 height = 5 area = 20
注意:函数体内部语句执行时,一旦执行到return时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。
注意:如果没有return语句,函数执行完毕后也会返回结果,只是结果为None。(return None可以简写为return)
如果用户已经把hello()
的函数定义保存为test.py
文件了,那么,可以在该文件的当前目录下启动Python解释器,用from test import hello
来导入hello()
函数,注意test
是文件名(不含.py
扩展名):
>>> from test import hello
>>> hello()
Hello World!
import
的用法在后续“模块”一节中会详细介绍。
空函数
如果想定义一个什么事也不做的空函数,可以用pass语句:
def nop():
pass
pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。
参数传递
在 python 中,类型属于对象,变量是没有类型的:
a=[1,2,3]
a="Runoob"
以上代码中,[1,2,3] 是 List 类型,"Runoob" 是 String 类型,而变量 a 是没有类型,她仅仅是一个对象的引用(一个指针),可以是指向 List 类型对象,也可以是指向 String 类型对象。
可更改(mutable)与不可更改(immutable)对象
在 python 中,strings, tuples, 和 numbers 是不可更改的对象,而 list,dict 等则是可以修改的对象。
- 不可变类型:变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变a的值,相当于新生成了a。
- 可变类型:变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身la没有动,只是其内部的一部分值被修改了。
python 函数的参数传递:
- 不可变类型:类似 c++ 的值传递,如 整数、字符串、元组。如fun(a),传递的只是a的值,没有影响a对象本身。比如在 fun(a)内部修改 a 的值,只是修改另一个复制的对象,不会影响 a 本身。
- 可变类型:类似 c++ 的引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后fun外部的la也会受影响。
python 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。
python 传不可变对象实例
#!/usr/bin/python3
def ChangeInt( a ):
a = 10
b = 2
ChangeInt(b)
print( b ) # 结果是 2
实例中有 int 对象 2,指向它的变量是 b,在传递给 ChangeInt 函数时,按传值的方式复制了变量 b,a 和 b 都指向了同一个 Int 对象,在 a=10 时,则新生成一个 int 值对象 10,并让 a 指向它。
传可变对象实例
可变对象在函数里修改了参数,那么在调用这个函数的函数里,原始的参数也被改变了。例如:
#!/usr/bin/python3
# 可写函数说明
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]]
小结
定义函数时,需要确定函数名和参数个数;
如果有必要,可以先对参数的数据类型做检查;
函数体内部可以用return随时返回函数结果;
函数执行完毕也没有return语句时,自动return None。
5.4 函数的参数
在定义函数时,我们把参数的名字和位置确定后,函数的接口定义就完成了。对于函数的调用者来说,只需知道如何传递正确的参数,以及函数的返回值即可,函数内部被封装起来,调用者无需了解。
Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
鉴于函数定义中可能包含多个形参,因此函数调用中也可能包含多个实参。想函数传递实参的方式很多,可使用位置实参,和要求实参的顺序相同;也可使用关键字实参,其中每个实参都由变量名和值组成,等等。以下是调用函数时可使用的正式参数类型:
位置参数
5.4.1位置实参
你调用函数时,Python必须将函数调用中的每个实参都关联到函数定义的一个形参。为此,最简单的关联方式是基于实参的顺序,这种关联方式被称为位置实参,可以多次调用。
为明白其中的工作原理,来看一个显示学生信息的函数。这个函数指出一名学生的名字以及年龄,如下所示:
❶ def describe_student(person_name, student_age):
"""显示学生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
❷ describe_student('Jack', '18')
这个函数的定义表明,它需要一名学生的姓名和一个年龄数字(见❶)。调用describe_student() 时,需要按顺序提供一名学生的姓名和一个年龄数字。例如,在前面的函数调用中,实参'Jack' 存储在形参person_name中,而实参'18' 存储在形参student_age 中(见❷)。在函数体内,使用了这两个形参来显示学生的信息。输出描述了一名18岁的学生Jack:
My name is Jack.
Jack is 18 years old.
- 调用函数多次
用户可以根据需要调用函数任意次。要再描述一名学生,只需再次调用describe_student() 即可:
def describe_student(person_name, student_age):
"""显示学生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
describe_student('Jack', '18')
describe_student('Bob', '17')
第二次调用describe_student() 函数时,我们向它传递了实参'Bob' 和'17' 。与第一次调用时一样,Python将实参'Bob' 关联到形参person_name,并将实参'17' 关联到形参student_age 。与前面一样,这个函数完成其任务,但打印的是一名17岁的学生Bob的信息。至此,我们描述了一名18岁的学生Jack和17岁的学生Bob.
My name is Jack.
Jack is 18 years old.
My name is Bob.
Jack is 17 years old.
调用函数多次是一种效率极高的工作方式。我们只需在函数中编写描述学生的代码一次,然后每当需要描述新学生时,都可调用这个函数,并向它提供新学生的信息。即便描述全校的学生,用户依然只需使用一行调用函数的代码,就可描述一名新学生。
在函数中,可根据需要使用任意数量的位置实参,Python将按顺序将函数调用中的实参关联到函数定义中相应的形参。
- 位置实参的顺序很重要
使用位置实参来调用函数时,如果实参的顺序不正确,结果可能出乎意料:
def describe_student(person_name, student_age):
"""显示学生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
describe_student('18', 'Jack')
在这个函数调用中,我们先指定名字,再指定学生年龄。由于实参'18' 在前,这个值将存储到形参person_name中;同理,'Jack' 将存储到形参student_age中。结果是我们得到了一名年龄为Jack的18:
My name is 18.
Jack is Jack years old.
如果结果像上面一样荒谬,请确认函数调用中实参的顺序与函数定义中形参的顺序一致。
5.4.2 默认参数
编写函数时,可给每个形参指定默认值 。在调用函数中给形参提供了实参时,Python将使用指定的实参值;否则,将使用形参的默认值。因此,给形参指定默认值后,可在函数调用中省略相应的实参。使用默认值可简化函数调用,还可清楚地指出函数的典型用法。
例如describe_student函数定义没有问题,但若如下调用会出现错误:
>>> describe_student('Jack')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: describe_student() missing 1 required positional argument: 'student_age'
Python的错误信息很明确:调用函数describe_student()缺少了一个位置参数student_age。这个时候,默认参数就排上用场了。若大部分学生的年龄为18岁,我们可以把第二个参数student_age的默认值设定为18:
def describe_student(person_name, student_age=18):
"""显示学生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
这样,当我们调用describe_student(Jack)时,相当于调用describe_student(Jack,18):
>>> describe_student('Jack')
My name is Jack.
Jack is 18 years old.
>>> describe_student('Jack','18')
My name is Jack.
Jack is 18 years old.
而对于student > 18的其他情况,就必须明确地传入student_age,比如describe_student(Herbie,19)。
从上面的例子可以看出,默认参数可以简化函数的调用。
注意:
设置默认参数时,必选参数在前,默认参数在后,否则Python的解释器会报错(思考一下为什么默认参数不能放在必选参数前面)
当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
使用默认参数有什么好处?最大的好处是能降低调用函数的难度。
举个例子,我们编写一个学生注册的函数,需要传入name和gender两个参数:
def enroll(name, gender):
"""注册学生的信息"""
print("name: ",name)
print("gender: ",gender)
这样,调用enroll()函数只需要传入两个参数:
>>> enroll('Jack', 'F')
name: Jack
gender: F
如果要继续传入年龄、城市等信息怎么办?这样会使得调用函数的复杂度大大增加。
我们可以把年龄和城市设为默认参数:
def enroll(name, gender,age=18, city='Beijing'):
print('name: ', name)
print('gender: ', gender)
print('age: ', age)
print('city:', city)
这样,大多数学生注册时不需要填写年龄和城市,只提供必须的两个参数:
>>> enroll('Sarah', 'F')
name: Sarah
gender: F
age: 18
city: Beijing
只有与默认参数不符的学生才需要提供额外的信息:
enroll('Bob', 'M', 17)
enroll('Adam', 'M', city='Tianjin')
可见,默认参数降低了函数调用的难度,而一旦需要更复杂的调用时,又可以传递更多的参数来实现。无论是简单调用还是复杂调用,函数只需要定义一个。
有多个默认参数时,调用的时候,既可以按顺序提供默认参数,比如调用enroll('Bob', 'M', 17),意思是,除了name,gender这两个参数外,最后一个参数应用在参数age上,city参数由于没有提供,仍然使用默认值。也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用enroll('Adam', 'M', city='Tianjin'),意思是,city参数用传进去的值,其他默认参数继续使用默认值。
默认参数很有用,但定义默认参数时要牢记一点:默认参数必须指向不变对象!否则会出现重大错误,例如:
先定义一个函数,传入一个list,添加一个END再返回:
def test_add(H=[]):
H.append('END')
return H
当正常调用时,结果似乎不错:
>>> test_add([1, 2, 3])
[1, 2, 3, 'END']
>>> add_end(['a', 'b', 'c'])
['a', 'b', 'c', 'END']
当初次使用默认参数调用时,结果也是对的:
>>> test_add()
['END']
但是当再次调用test_add()时,结果就出现错误了:
>>> test_add()
['END', 'END']
>>> test_add()
['END', 'END', 'END']
我们会发现,默认参数是[],但是函数test_add()似乎每次都“记住了”上次添加了'END'后的list。这是为什么呢?原因如下:
Python函数在定义的时候,默认参数H的值就被计算出来了,即[],因为默认参数H也是一个变量,它指向对象[],每次调用该函数,如果改变了H的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。
注意:定义默认参数时,默认参数必须指向不变对象!
我们可以用None这个不变对象来解决此问题:
def test_add(H=None):
if H is None:
H = []
H.append('END')
return H
现在,无论调用多少次,都不会有问题:
>>> test_add()
['END']
>>> test_add()
['END']
为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
5.4.3 不定长参数
在Python函数中,还可以定义不定长参数,也叫可变参数。顾名思义,不定长参数就是传入的参数个数是可变的。
我们以数学题为例子,给定一组数字a,b,c……,请计算a+b+c+ ……要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:
def calc(numbers):
sum = 0
for n in numbers:
sum = sum + n
return sum
但是调用的时候,需要先组装出一个list或tuple:
>>> calc([1, 2, 3])
6
>>> calc((1, 2, 3, 4))
10
但若把函数的参数改为不定长参数:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n
return sum
则调用函数的方式可以简化成这样:
>>> calc(1, 2, 3)
6
>>> calc(1, 2, 3, 4)
10
定义不定长参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
>>> calc()
0
如果已经有一个list或者tuple,要调用一个不定长参数可如下:
>>> nums = [1, 2, 3]
>>> calc(*nums)
6
*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
5.4.4 关键字参数
关键字实参是传递给函数的名称—值对。你直接在实参中将名称和值关联起来了,因此向函数传递实参时不会混淆(不会得到名为18的Jack这样的结果)。关键字实参让用户无需考虑函数调用中的实参顺序,还清楚地指出了函数调用中各个值的用途。
下面重新编写describe_student()函数,在其中使用关键字实参来调用describe_student():
def describe_student(person_name, student_age):
"""显示学生的信息"""
print("\nMy name is " + person_name + ".")
print(person_name + " is " + student_age+ " years old.")
describe_student(person_name='Jack', student_age='18')
函数describe_pet() 还是原来那样,但调用这个函数时,我们向Python明确地指出了各个实参对应的形参。看到这个函数调用时,Python知道应该将实参'Jack' 和'18' 分别存储在形参person_name和 student_age中。输出正确无误,它指出我们有一名名叫Jack,年龄为18岁的学生。关键字实参的顺序无关紧要,因为Python知道各个值该存储到哪个形参中。下面两个函数调用是等效的:
describe_pet(animal_type='hamster', pet_name='harry')
describe_pet(pet_name='harry', animal_type='hamster')
注意:使用关键字实参时,务必准确地指定函数定义中的形参名。
不定长参数允许传入0个或任意个参数,这些不定长参数在函数调用时自动组装为一个元组(tuple)。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个字典(dict)。如下,函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:
def enroll(name, age,**kw)):
print('name: ', name, 'age: ', age, 'other: ', kw)
>>> enroll('Michael', 18)
name: Michael age: 18 other: {}
也可以传入任意个数的关键字参数:
>>> enroll('Bob', 17, city='Beijing')
name: Bob age: 17 other: {'city': 'Beijing'}
>>> enroll('Adam', 19, gender='M', job='Engineer')
name: Adam age: 19 other: {'gender': 'M', 'job': 'Engineer'}
关键字参数有扩展函数的功能。比如,在enroll函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和不定长参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
>>> extra = {'gender':'M','city': 'Beijing'}
>>> enroll('Jack', 18, gender=extra['M'], city=extra['city'])
name: Jack age: 18 other: {'gender':'M','city': 'Beijing'}
当然,上面复杂的调用可以用简化的写法:
>>> extra = {'gender':'M','city': 'Beijing'}
>>> enroll('Jack', 18, **extra)
name: Jack age: 18 other: {'gender':'M','city': 'Beijing'}
注意:**extra
表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。
5.4.5 命名关键字参数
如果要限制关键字参数的名字,就可以用命名关键字参数。
和关键字参数**kw
不同,如果没有可变参数,命名关键字参数就必须加一个*
作为特殊分隔符。如果缺少*
,Python解释器将无法识别位置参数和命名关键字参数。例如,若只接收age和city作为关键字参数。这种方式定义的函数如下:
def enroll(name, gender, *, age, city):
print(name, gender, age, city)
>>> enroll('Jack', ' M', age='18', city='Beijing')
Jack M 18 Beijing
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
def enroll(name, gender, *grade, age, city):
print(name, gender, grader, age, city)
注意:和位置参数不同,命名关键字参数必须传入参数名。
如果没有传入参数名,调用将报错:
>>> enroll(' Jack',' M',' 18',' Beijing')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: enroll() takes 2 positional arguments but 4 were given
由报错信息可知,由于调用时缺少参数名age和city,Python解释器把这4个参数均视为位置参数,但enrroll()函数仅接受2个位置参数。
注意:命名关键字参数可以有缺省值。
由于命名关键字参数city具有默认值,调用时可不传入city参数:
def enroll(name, gender, *, age='18', city):
print(name, gender, age, city)
>>> enroll('Jack','M',city='Beijing')
Jack M 18 Beijing
5.4.6 参数组合
现在我们知道python定义函数的参数类型有:
必选参数、默认参数、可变参数、关键字参数和命名关键字参数
在Python中定义函数,我们是可以组合使用这些参数的。但要注意的是,参数定义是有顺序的,定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:
def func(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)
在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。
>>> func(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> func(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> func(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> func(1, 2, 3, 'a', 'b', x=4)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 4}
另外对于任意函数,都可以通过类似 func(*args, **kw) 的形式调用它,无论它的参数是如何定义的。以一个元组(tuple)和字典(dict)为例:
>>> args = (1, 2, 3, 4)
>>> kw = {'x': 5}
>>> func(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'x': 5}
小结
Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。
默认参数一定要用不可变对象,如果是可变对象,运行会有逻辑错误!
要注意定义可变参数和关键字参数的语法:
args 是可变参数,args接收的是一个tuple;
kw 是关键字参数,kw接收的是一个dict。
以及调用函数时如何传入可变参数和关键字参数的语法:
可变参数既可以直接传入: func(1, 2, 3) ,又可以先组装list或tuple,再通过*args
传入: func((1, 2, 3)) ;
关键字参数既可以直接传入: func(a=1, b=2) ,又可以先组装dict,再通过 **kw
传入: func({'a': 1, 'b': 2}) 。
使用 *args
和**kw
是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法
5.6 返回值
函数并非总是直接显示输出,相反,它可以处理一些数据,并返回一个或一组值。函数返回的值被称为返回值 。在函数中,可使用return 语句将值返回到调用函数的代码行。返回值让你能够将程序的大部分繁重工作移到函数中去完成,从而简化主程序。
return [表达式] 语句用于退出函数,选择性地向调用方返回一个表达式。不带参数值的return语句返回None。以下实例演示了 return 语句的用法:
#!/usr/bin/python3
def sum( arg1, arg2 ):
total = arg1 + arg2 # 返回2个参数的和
print ("函数内 : ", total)
return total;
total = sum( 10, 20 );
print ("函数外 : ", total)
函数内 : 30
函数外 : 30
5.7 函数式编程
函数式编程是一种编程范式,我们常见的编程范式有命令式编程、函数式编程、逻辑式编程,常见的面向对象编程也是一种命令式编程。命令式编程是面向计算机硬件的抽象,在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令。而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,越是抽象的计算,离计算机硬件越远。
值得注意的是,函数式编程中的“函数”不是指计算机中的函数,而是指数学中的函数,即自变量的映射,一个函数的值仅决定于函数参数的值,不依赖其它状态。在编程式语言中,“函数”可以在任何地方定义,在函数内或函数外,可以作为函数的参数或返回值,可以对函数进行组合。纯函数式编程中的变量也不是命令式编程语言中的变量,而是代数中的变量,即一个值的名称,变量的值是不可变的,比如命令式编程中的"x = x+1"这种依赖可变状态的事实此时被认为为假。
例:通过别的名称使用函数,再把函数作为参数传递
>>> fact = factorial
>>> fact
<function factorial at >
>>> func(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'x': 5}
函数式编程最主要的好处主要是不可变性带来的,函数是“引用透明”的,并可避免“副作用”,它不会依赖也不会改变当前函数以外的数据。
高阶函数
接受函数为参数,或者把函数作为结果返回的函数是高阶函数(higher-
order function)。map 函数就是一例,此外,内置函数 sorted 也是:可选的 key 参数用于提供一个函数,它会应用到各个元素上进行排序.
例,若想根据单词的长度排序,只需把 len 函数传给 key 参数,如下:
根据单词长度给一个列表排序
>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=len)
['fig', 'apple', 'cherry', 'banana', 'raspberry', 'strawberry']
任何单参数函数都能作为 key 参数的值。例如,为了创建押韵词典,可以把各个单词反过来拼写,然后排序。注意,示例 5-4 中列表里的单词没有变,我们只是把反向拼写当作排序条件,因此各种浆果(berry)都排在一起。
示例:根据反向拼写给一个单词列表排序
>>> def reverse(word):
... return word[::-1]
>>> reverse('testing')
'gnitset'
>>> sorted(fruits, key=reverse)
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']
在函数式编程范式中,最为人熟知的高阶函数有map、filter、reduce 和 apply。apply 函数在 Python 2.3 中标记为过时,在 Python 3 中移除了,因为不再需要它了。如果想使用不定量的参数调用函数,可以编写 fn(*args, **keywords),不用再编写 apply(fn, args, kwargs)。
5.5 匿名函数
所谓匿名,意即不再使用 def 语句这样标准的形式定义一个函数。python 使用 lambda 来创建匿名函数。
lambda 只是一个表达式,函数体比 def 简单很多。lambda 函数的语法如下:
lambda [arg1 [,arg2,.....argn]]:expression
lambda的主体是一个表达式,而不是一个代码块,仅能在lambda表达式中封装有限的逻辑进去。
注意:lambda 函数拥有自己的命名空间,且不能访问自己参数列表之外或全局命名空间里的参数。
注意:虽然lambda函数看起来只能写一行,却不等同于C或C++的内联函数,后者的目的是调用小函数时不占用栈内存从而增加运行效率。
如下实例:
#!/usr/bin/python3
sum = lambda arg1, arg2: arg1 + arg2;
# 调用sum函数
print ("相加后的值为 : ", sum( 1, 2 ))
print ("相加后的值为 : ", sum( 2, 2 ))
相加后的值为 : 3
相加后的值为 : 4
变量作用域
Python 中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。
变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。Python的作用域一共有4种,分别是:
L (Local) 局部作用域
E (Enclosing) 闭包函数外的函数中
G (Global) 全局作用域
B (Built-in) 内建作用域
以 L –> E –> G –>B 的规则查找,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内建中找。
x = int(2.9) # 内建作用域
g_count = 0 # 全局作用域
def outer():
o_count = 1 # 闭包函数外的函数中
def inner():
i_count = 2 # 局部作用域
Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这这些语句内定义的变量,外部也可以访问,如下代码:
>>> if True:
... msg = 'I am from Runoob'
...
>>> msg
'I am from Runoob'
实例中 msg 变量定义在 if 语句块中,但外部还是可以访问的。
如果将 msg 定义在函数中,则它就是局部变量,外部不能访问:
>>> def test():
... msg_inner = 'I am from Runoob'
...
>>> msg_inner
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'msg_inner' is not defined
从报错的信息上看,说明了 msg_inner 未定义,无法使用,因为它是局部变量,只有在函数内可以使用。
全局变量和局部变量
定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。
局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入到作用域中。如下实例:
#!/usr/bin/python3
total = 0; # 这是一个全局变量
# 可写函数说明
def sum( arg1, arg2 ):
#返回2个参数的和."
total = arg1 + arg2; # total在这里是局部变量.
print ("函数内是局部变量 : ", total)
return total;
#调用sum函数
sum( 10, 20 );
print ("函数外是全局变量 : ", total)
函数内是局部变量 : 30
函数外是全局变量 : 0
global 和 nonlocal关键字
当内部作用域想修改外部作用域的变量时,就要用到global和nonlocal关键字了。
以下实例修改全局变量 num:
#!/usr/bin/python3
num = 1
def fun1():
global num # 需要使用 global 关键字声明
print(num)
num = 123
print(num)
fun1()
1
123
如果要修改嵌套作用域(enclosing 作用域,外层非全局作用域)中的变量则需要 nonlocal 关键字了,如下实例:
#!/usr/bin/python3
def outer():
num = 10
def inner():
nonlocal num # nonlocal关键字声明
num = 100
print(num)
inner()
print(num)
outer()
100
100
另外有一种特殊情况,假设下面这段代码被运行:
#!/usr/bin/python3
a = 10
def test():
a = a + 1
print(a)
test()
以上程序执行,报错信息如下:
Traceback (most recent call last):
File "test.py", line 7, in <module>
test()
File "test.py", line 5, in test
a = a + 1
UnboundLocalError: local variable 'a' referenced before assignment
错误信息为局部作用域引用错误,因为 test 函数中的 a 使用的是局部,未定义,无法修改。
全局变量和局部变量
定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。
局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入到作用域中。如下实例:
#!/usr/bin/python3
total = 0 # 这是一个全局变量
# 可写函数说明
def sum( arg1, arg2 ):
#返回2个参数的和."
total = arg1 + arg2 # total在这里是局部变量.
print ("函数内是局部变量 : ", total)
return total
#调用sum函数
sum( 10, 20 )
print ("函数外是全局变量 : ", total)
以上实例输出结果:
函数内是局部变量 : 30
函数外是全局变量 : 0