当同一个功能、同一套算法等在多个地方重复使用,就要考虑把其抽成一个函数,在需要的地方调用即可。这样可以提高代码的复用性,也可便于维护代码。其实前面在学习循环等知识点时,已经接触到了函数。本文将详细学习Python3中的函数这一块内容,具体包括:函数的定义、调用、参数的分类、作用域等。
函数的定义规格及格式及调用
python3中定义一个函数需要遵循以下规则:
- 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()。
- 任何传入参数和自变量必须放在圆括号中间,圆括号之间可以用于定义参数。
- 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。
- 函数内容以冒号起始,并且缩进。
- return [表达式] 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回 None。
Python 定义函数使用 def 关键字,一般格式如下:
def 函数名(参数列表):
函数体
实例:输入一个一元二次方程的系数,如:ax^2 + bx + c中的a,b,c,画出一个一元二次的方程的图像
# 输入一个一元二次方程的系数,如:ax^2 + bx + c中的a,b,c,画出一个一元二次的方程的图像
import math
import matplotlib.pyplot as plt
import numpy as np
# 定义函数1,用来接收一个一元二次方程的系数
def the_power_function(a1=1, b=0.0, c=0.0):
# 调整系数符号
b0 = fix_symbol(b)
c0 = fix_symbol(c)
# 调整bx
bx = ''
if b0 != '':
bx = b0 + 'x'
# 组装表达式
funcExpress = str(a1) + 'x' + r"^2" + bx + c0
print("一元二次函数是:" + funcExpress)
startPoint = -( b/2/a1 + 5)
endPoint = 5 - b/2/a1
u = np.arange(startPoint, endPoint, 0.01)
y1 = [[math.pow((a + b/2/a1), 2) + (c - b*b/4/a1)] for a in u]
plt.plot(u, y1, linewidth = 1, color="#FF0000", label= funcExpress)
plt.legend(loc = "lower right")
plt.grid(True)
plt.show()
# 定义函数2:一个修正系数符号的函数
def fix_symbol(c):
if c < 0:
return str(c)
elif c > 0:
return "+" + str(c)
else:
return ""
# 调用函数
the_power_function(1, 4, -3.8)
运行结果:
一元二次函数是:1x^2+4x-3.8
绘制的函数图像是:
解析:
- 上面实例中定义了2个函数:
函数1.接收一个一元二次方程的系数并绘制函数图像的函数,形参列表是方程的系数,并提供了默认参数。当用户什么也不传,或者某一参数不传时,就会使用提供的默认参数。
函数2:用来修正方程系数的函数
2.函数的调用:在需要调用函数地方,直接写上已定义好的函数名,并按照函数签名(函数的名、形参列表)传入需要的参数即可。
3.对于函数1调用时,如果参数不传,就会使用默认参数了。关于函数参数,接下来会有详细的介绍。
4.关于画图,后面会有专门章节学习。
参数的分类
定义函数的时候,我们把参数的名字和位置确定下来,函数的接口定义就完成了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。
Python的函数定义非常简单,但灵活度却非常大。除了正常定义的必选参数外,还可以使用默认参数、可变参数和关键字参数,使得函数定义出来的接口,不但能处理复杂的参数,还可以简化调用者的代码。
1.普通参数
普通参数是在调用函数时,要严格按照参数的类型、个数传入的,否则会报TypeError异常。
2.默认参数
当某个参数没有传入实参时,会使用函数定义时的参数的值,即默认参数。上面一元二次函数例子中,如果二次项系数a1、一次项系数b、常数项c都不传入,则按照def the_power_function(a1=1, b=0.0, c=0.0):
函数定义时的a1=1, b = 0.0, c = 0.0。得到一元二次函数: 1x^2。
# 上接
# 不传入参数
the_power_function()
运行结果:一元二次函数是:1x^2
从上面的例子可以看出,默认参数可以简化函数的调用。设置默认参数时,有几点要注意:
一是普通参数在前,默认参数在后,否则Python的解释器会报错(思考一下为什么默认参数不能放在普通参数前面);
二是如何设置默认参数。
当函数有多个参数时,把调用函数时必须传入的参数放前面,其他参数可以使用默认值的写在后面。这种可以使用默认值的参数就可以作为默认参数。
使用默认参数有什么好处?最大的好处是能降低调用函数的难度。
也可以不按顺序提供部分默认参数。当不按顺序提供部分默认参数时,需要把参数名写上。比如调用the_power_function(b=4, a1=3, c=9.9)
,意思是,b、a1、c参数用传进去的值,其他默认参数继续使用默认值
the_power_function(b=4, a1=3, c=9.9)
运行结果:
一元二次函数是:3x^2+4x+9.9
注意:
默认参数很有用,但使用不当,也会掉坑里。默认参数有个最大的坑,演示如下:
先定义一个函数,传入一个list,添加一个END再返回:
# 默认参数的坑
# 自定义一个list的拼接函数,参数是一个可变的、默认长度为0的list
def the_end(my_list=[]):
my_list.append("The End")
return my_list
list1 = ["a", "b", "c"]
# 传入实参list1
the_end(list1)
print(list1)
运行结果:
['a', 'b', 'c', 'The End']
一切正常,正如所期望的结果一样。接着传入实参list2,并打印list2:
list2 = ["1", "2", "3"]
# 传入实参list2
the_end(list2)
print(list2)
运行结果:
['1', '2', '3', 'The End']
此时,运行结果仍与预期一样。再次尝试:不传参数,使用默认的参数[]:
# 不传参数,使用默认的参数[]
print(the_end())
运行结果:
['The End']
到目前为止,无论传入新的list,还是使用默认的参数值,运行结果都如预期。但是,倘若对同一个list调用两次,具体操作如下:
# 自定义一个list的拼接函数,参数是一个可变的、默认长度为0的list
def the_end(my_list=[]):
my_list.append("The End")
return my_list
# 测试连续调用两次结尾函数:
print(the_end())
print(the_end())
运行结果:
[ 'The End']
['The End', 'The End']
思考:这是为什么呢?
原因解释如下:
Python函数在定义的时候,默认参数my_list的值就被计算出来了,即[],因为默认参数my_list也是一个变量,它指向对象[],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[]了。
定义默认参数要牢记一点:默认参数必须指向不变对象!
优化如下:
# 默认参数的坑
# 自定义一个list的拼接函数,参数是一个可变的、默认长度为0的list
def the_end(my_list = None):
if my_list is None:
my_list = []
my_list.append("The End")
return my_list
# 测试连续调用两次结尾函数:
print(the_end())
print(the_end())
运行结果:
['The End']
['The End']
这样修改后,多次调用也没关系了。
为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
补充:可更改(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 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。
3.可变参数
在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的,可以是1个、2个到任意个,当然还可以是0个。在其它高级编程语言,如Java,C等都支持该语法,这在一定程度上大大提高了函数的灵活性。
我们以数学题为例子,给定一组数字a,b,c……,请计算a2 + b2 + c2 + ……。
要定义出这个函数,我们必须确定输入的参数。由于参数个数不确定,我们首先想到可以把a,b,c……作为一个list或tuple传进来,这样,函数可以定义如下:
# 定义一个求平方的和的函数
def get_sum(nums):
total = 0
for i in nums:
total = total + i * i
print(total)
return total
但是调用的时候,需要先组装出一个list或tuple:
# 但是调用的时候,需要先组装出一个list或tuple:
nums1 = [1, 2, 3, 4]
# 调用求平方的和的函数
get_sum(nums1)
运行结果:
30
如果利用可变参数,调用函数的方式可以简化成这样:
get_sum(1, 2, 3, 4)
为了支持这种方便快捷的调用函数,我们把函数的参数改为可变参数:
# 定义一个求平方和的函数(可变参数形式)
def get_sum_mutable_params(*nums):
total = 0
for i in nums:
total = total + i * i
print(total)
return total
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
# 调用函数
get_sum_mutable_params(1, 2, 3, 4)
get_sum_mutable_params(1, 2, 3, 4, 5, 6, 7)
运行结果:
30
140
如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:
nums1 = [1, 2, 3, 4]
# 调用函数
get_sum_mutable_params(nums1[0], nums1[1], nums1[2], nums1[3])
运行结果:30
这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
nums1 = [1, 2, 3, 4]
# 使用*将一个list或tuple中的元素,传入给可变参数
get_sum_mutable_params(*nums1)
运行结果:30
*nums1
表示把nums1这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
提示:可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。
4.关键字参数
上面提到: 可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。
比如,大家在某网站注册账号时,除了必填信息(如:注册账号、登录密码)外,还有一些是可选填项(如:爱好,地区等)。
# 关键字参数
def enrollment(account, pwd, **kws):
print('account:', account, ' pwd:', pwd, ' other:', kws)
enrollment("15639067890@163.com", r'******')
enrollment("15639067890@163.com", r'******', city="北京")
运行结果:
account: 15639067890@163.com pwd: ****** other: {}
account: 15639067890@163.com pwd: ****** other: {'city': '北京'}
说明:函数enrollment()除了必选参数account和pwd外,还接受关键字参数kws。在调用该函数时,可以只传入必选参数。也可以传入任意个数的关键字参数:
# 关键字参数
def enrollment(account, pwd, **kws):
print('account:', account, ' pwd:', pwd, ' other:', kws)
enrollment("15639067890@163.com", r'******')
enrollment("15639067890@163.com", r'******', city="北京")
enrollment("15639067891@163.com", r'***', city="广州", sex='male')
enrollment("15639067892@163.com", r'*****', city="上海", name='Wang DaChui')
运行结果:
# 只传入必传参数
account: 15639067890@163.com pwd: ****** other: {}
# 既有必传参数,又有关键词参数
account: 15639067890@163.com pwd: ****** other: {'city': '北京'}
account: 15639067891@163.com pwd: *** other: {'city': '广州', 'sex': 'male'}
account: 15639067892@163.com pwd: ***** other: {'city': '上海', 'name': 'Wang DaChui'}
关键字参数有什么用?它可以扩展函数的功能。比如,在enrollment()函数里,我们保证能接收到account和pwd这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了账号和密码是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
# 关键字参数
def enrollment(account, pwd, **kws):
print('account:', account, ' pwd:', pwd, ' other:', kws)
# 一个字段
other = {'city' : '深圳', 'job' : '程序员'}
enrollment("15639067890@163.com", r'******', city=other['city'], job=other['job'])
运行结果:
account: 15639067890@163.com pwd: ****** other: {'city': '深圳', 'job': '程序员'}
当然,上面复杂的调用可以用简化的写法:
# 关键字参数
def enrollment(account, pwd, **kws):
print('account:', account, ' pwd:', pwd, ' other:', kws)
other = {'city' : '深圳', 'job' : '程序员'}
# 常规调用形式:
enrollment("15639067890@163.com", r'******', city=other['city'], job=other['job'])
# 简化调用形式
enrollment("15639067893@163.com", r'******', **other)
运行结果:
account: 15639067890@163.com pwd: ****** other: {'job': '程序员', 'city': '深圳'}
account: 15639067893@163.com pwd: ****** other: {'job': '程序员', 'city': '深圳'}
说明:
**other
表示把other这个dict的所有key-value用关键字参数传入到函数的**kws参数,kws将获得一个dict,注意kws获得的dict是other的一份拷贝,对kws的改动不会影响到函数外的other。
命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。但是调用者仍可以传入不受限制的关键字参数。
# 关键字参数
def enrollment(account, pwd, **kws):
if 'sex' in kws:
# 有sex
pass
if 'city' in kws:
# 有city
pass
print('account:', account, ' pwd:', pwd, ' other:', kws)
但是调用者仍可以传入不受限制的关键字参数:
enrollment("15639067891@163.com", r'***', city="广州", sex='male')
enrollment("15639067892@163.com", r'*****', city="上海", name='Wang DaChui')
运行结果:
account: 15639067891@163.com pwd: *** other: {'city': '广州', 'sex': 'male'}
account: 15639067892@163.com pwd: ***** other: {'city': '上海', 'name': 'Wang DaChui'}
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和sex作为关键字参数。这种方式定义的函数如下。
# 命名关键字参数
def enrollment(account, pwd, *, city, sex):
print('account:', account, ' pwd:', pwd, ' city:', city, 'sex:', sex)
# 调用函数
enrollment("15639067891@163.com", r'***', city="广州", sex='male')
运行结果:
account: 15639067891@163.com pwd: *** city: 广州 sex: male
和关键字参数kw不同,命名关键字参数需要一个特殊分隔符,后面的参数会被视为命名关键字参数。在调用函数时,关键词参数不能多不能少,要严格按照定义时的参数名。
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。由于调用时缺少参数名city和job,Python解释器把这4个参数均视为位置参数。
# 命名关键字参数
def enrollment(account, pwd, *, city, sex):
print('account:', account, ' pwd:', pwd, ' city:', city, 'sex:', sex)
# 少传入命名关键词参数
enrollment("15639067891@163.com", r'***', "广州")
运行结果:
Traceback (most recent call last):
File "F:/python_projects/09argument.py", line 103, in <module>
enrollment("15639067891@163.com", r'***', "广州")
TypeError: enrollment() takes 2 positional arguments but 3 were given
意思是:enrollment()需要 2 个位置参数,但是有3个被传入了。
此时,如果少传入了命名关键词参数,则报错:TypeError: enrollment() missing 1 required keyword-only argument:xxx
。例如:
# 命名关键字参数
def enrollment(account, pwd, *, city, sex):
print('account:', account, ' pwd:', pwd, ' city:', city, 'sex:', sex)
# 少传入命名关键词参数
enrollment("15639067891@163.com", r'***', city="广州")
运行结果:
Traceback (most recent call last):
File "F:/python_projects/09argument.py", line 102, in <module>
enrollment("15639067891@163.com", r'***', city="广州")
TypeError: enrollment() missing 1 required keyword-only argument: 'sex'
意思是:缺少了必须的关键词参数:sex
如果多传入了命名关键词参数,则报错:TypeError: enrollment() got an unexpected keyword argument :xxx
。例如:
# 命名关键字参数
def enrollment(account, pwd, *, city, sex):
print('account:', account, ' pwd:', pwd, ' city:', city, 'sex:', sex)
# 多传入关键词参数
enrollment("15639067892@163.com", r'*****', city="上海", sex='male', name='Wang DaChui')
运行结果:
Traceback (most recent call last):
File "F:/python_projects/09argument.py", line 105, in <module>
enrollment("15639067892@163.com", r'*****', city="上海", sex='male', name='Wang DaChui')
TypeError: enrollment() got an unexpected keyword argument 'name'
意思是:调用enrollment()时,不期望有的(多余的)关键词参数:name
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
# 命名关键字参数
def enrollment(account, pwd, *other, city, sex):
print('account:', account, ' pwd:', pwd, 'other :', other, 'city:', city, 'sex:', sex)
other = [2, 4, 5]
# 调用形式
enrollment("15639067893@163.com", r'******', *other, city='北京', sex='male')
运行结果:
account: 15639067893@163.com pwd: ****** other : (2, 4, 5) city: 北京 sex: male
这里的*other
,已经是一个可变参数了,所以后面跟着的命名关键字参数就不再需要一个特殊分隔符了。使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:
命名关键字参数可以有缺省值,从而简化调用:
# 命名关键字参数
def enrollment(account, pwd, *other, city='北京', sex):
print('account:', account, ' pwd:', pwd, 'other :', other, 'city:', city, 'sex:', sex)
other = [2, 4, 5]
# 调用形式
enrollment("15639067893@163.com", r'******', *other, city='北京', sex='male')
# 使用默认关键词参数的值
enrollment("15639067894@163.com", r'****', *other)
运行结果:
account: 15639067893@163.com pwd: ****** other : (2, 4, 5) city: 北京 sex: male
account: 15639067894@163.com pwd: **** other : (2, 4, 5) city: 北京 sex: male
如果,命名关键词没有给初始值,则调用时必须传入。否则,会报错缺少必须的关键词参数。
# 命名关键字参数
def enrollment(account, pwd, *other, city='北京', sex):
print('account:', account, ' pwd:', pwd, 'other :', other, 'city:', city, 'sex:', sex)
other = [2, 4, 5]
# 调用形式
enrollment("15639067893@163.com", r'******', *other, city='北京', sex='male')
# 使用默认关键词参数的值
enrollment("15639067894@163.com", r'****', *other)
运行结果:
Traceback (most recent call last):
account: 15639067893@163.com pwd: ****** other : (2, 4, 5) city: 北京 sex: male
File "F:/python_projects/09argument.py", line 125, in <module>
enrollment("15639067894@163.com", r'****', *other)
TypeError: enrollment() missing 1 required keyword-only argument: 'sex'
参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
小结
Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。
1.默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!
2.要注意定义可变参数和关键字参数的语法:
*args
是可变参数,args接收的是一个tuple
;
**kw
是关键字参数,kw接收的是一个dict
。
3.以及调用函数时如何传入可变参数和关键字参数的语法:
可变参数既可以直接传入: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的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
4.命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
5.定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。