此文章参考廖雪峰大神的官方:调用函数 - 廖雪峰的官方网站 (liaoxuefeng.com)
一、什么是函数
- 函数通俗的来讲,就是一段固定的代码,函数还可以传入参数,最终得到的结果其实就固定代码的执行结果。
- python中内置了许多函数,我们可以直接进行调用,并且我们自己也可以自定义函数
二、调用函数
- python内置了许多函数,我们可以直接进行调用
- 如果要调用函数的话,需要先知道函数的函数名称以及可以传入参数的数量和数据类型等,可以去pyton的官网进行查询:查询python内置函数
- 也可以在python交互模式中使用
help()
函数来查看指定函数的相关信息
>>> help(abs)
Help on built-in function abs in module builtins:
abs(x, /)
Return the absolute value of the argument.
- 调用函数时,如果传入的参数数量、数据类型不对的话,会报错,以求绝对值函数
abs()
为例:
>>> abs(1,2,3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: abs() takes exactly one argument (3 given)
>>> abs(1)
1
>>> abs("1")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'str'
#例如求最大值函数max这种函数,可传入的参数数量就和abs不同
>>> max(1,23,4,5)
23
- 除了上述的max和abs,python内置的常用函数还包括数据类型转换的函数,下面来看几个例子:
>>> int('123') #转换字符串为数字
123
>>> int(12.33) #转换浮点数为整数
12
>>> str(123) #转换数字为字符串
'123'
>>> bool(1) #转换数字为布尔值
True
>>> list('aaa') #转换字符串为列表
['a', 'a', 'a']
- 函数还可以指向其他变量:
>>> a = abs
>>> a(-1)
1
>>> abs(-1)
1
三、定义函数
- 定义函数可以使用
def
语句,依次写出def 函数名(参数):
,输入回车,在缩进块中编写代码,函数的返回值使用return
语句返回 - 以一个计算数字平方的函数示例:
- 交互环境中:
>>> def test(x): #输入:后会有...提示符,输入代码后连续两次回车可以返回>>>提示符下
... return x * x
...
>>> test(2)
4
>>> test(22)
484
- 写入py文件中:
# -*- coding: utf-8 -*-
def test(x):
return x * x
print(test(2))
print(test(22))
#输出
4
484
-
要注意的是,在函数中的语句执行时,只要遇到了
return
语句时,就代表函数执行完毕,并且返回执行结果,如果一个函数中没有return
语句,函数也会返回值,但是这个值是None
,return
没有指定返回值也会返回None
,也就是说return None
可以写成return
-
如果想要定义的函数什么都不做,可以使用
pass
语句:
>>> def test(x):
... if x > 10:
... return x
... else:
... pass
...
>>> test(20)
20
>>> test(9)
>>>
#pass语句表示什么都不做,可以作为占位符,例如函数的代码还没想好怎么写,就可以
- 如果还想让函数返回多个值的话,可以这样做:
# -*- coding: utf-8 -*-
def test(i):
x = i + 1
y = i - 1
return x,y
print(test(20))
#输出:
(21, 19)
四、函数的参数
- 在定义函数的时候,我们需要把函数参数的名字、数量、位置确定下来,在确定下来之后,函数的接口其实也就定义完成
- 对于函数调用者来说,想要使用函数只需要知道如何传递正确的参数、参数的数量、参数的类型等,以及调用函数后会返回什么值,这样就已经足够了,而函数中间的逻辑算法等,调用者是不用去了解的
- Python函数的定义简单、灵活度大,除了正常定义的必选参数外,还可以使用
默认参数
、可变参数
和关键字参数
,使得函数定义出来的接口,不但能够处理复杂的参数,还可以简化调用者的代码
1、位置参数
1、我们先来定义一个计算x二次方的函数:
>>> def test(x):
... return x * x
...
在test函数中,变量x就是位置参数,当我们要调用test函数时,必须传入x参数,并且只能传输一个,下面来看调用test参数时传入不同参数时的输出的结果
>>> print(test(4)) #传入整数
16
>>> print(test(4.5)) #传入浮点数
20.25
>>> print(test('4')) #传入字符串,发现报错了,因为字符串无法使用 “ * ” 进行运算
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in test
TypeError: can't multiply sequence by non-int of type 'str'
>>> print(test(4,5)) #传入两个参数,同样报错,因为test函数中只有一个位置参数
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: test() takes 1 positional argument but 2 were given
>>> print(test()) #不传入任何参数,因为test函数中必须传入一个参数x,所以报错了
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: test() missing 1 required positional argument: 'x'
2、现在来优化test参数,使之可以计算x的n次方:
>>> def test(x,n):
... i = 1
... while n > 0:
... n = n -1
... i = i * x
... return i
...
>>> print(test(7,3))
343
在优化后的test函数中,多了一个参数n,那么在每次调用test函数时,就需要两个数字类型的参数,传入的参数按照x和n的前后顺序依次传入,例如test(7,3),则x=7,n=3
2、默认参数、
- 从上面说的位置参数来看,我们可以知道在调用函数时传入参数的
数量
、数据类型
都必须正确,否则会报错。 - 以优化后可以计算x的n次方函数test为例,如果我们需要多次调用test函数,而每次参数n的值都是2时,我们需要每次调用函数都传入两个参数,这样调用的话,我们会传入很多重复的参数,而默认参数就可以帮助我们减少重复操作
1、定义test函数,可以计算x的n次方,默认计算是x的2次方
>>> def test(x,n=2):
... i = 1
... while n > 0:
... n = n - 1
... i = i * x
... return i
...
>>> print(test(3,2))
9
>>> print(test(3))
9
从调用test函数可以看出,默认n的参数没有传入时,n=2
2、在使用默认参数时,默认参数必须要指向不变对象,即不可变类型,下面来看一个案例:
>>> def aaa(L=[]):
... L.append('end')
... return L
...
>>> print(aaa([1,2,3]))
[1, 2, 3, 'end']
>>> print(aaa(['x','y','a']))
['x', 'y', 'a', 'end']
>>> print(aaa())
['end']
>>> print(aaa())
['end', 'end']
>>> print(aaa([1,2,3]))
[1, 2, 3, 'end']
从上面的案例可以看出,在定义aaa参数时使用了L=[ ]作为默认参数,在我们正常调用aaa函数,传入列表参数时,输出的结果都是正常的,而在没有传入参数,使用aaa函数的默认参数时,我们发现在调用两次后,输出的结果并不是我们想的一个end,而是又添加了一个end,这是因为在定义函数时,默认参数L的值就已经被计算出来了,因为L也算一个变量,它指向了对象[ ],在每次调用时,如果修改了L的内容(即修改了空列表[ ]),则在下次调用时,L会变成修改后的值(即修改后的列表['end']),所以会输出这种结果
而解决的方法其实可以加一个if判断例如:
>>> def aaa(L=None):
... if L is None:
... L = []
... L.append('end')
... return L
...
>>> print(aaa())
['end']
>>> print(aaa())
['end']
>>> print(aaa([1,2,3]))
[1, 2, 3, 'end']
定义L的默认值为None,当L等于None时,才会定义一个空列表,这样就可以避免上面的情况
在定义默认参数时需要注意:
- 函数的参数传入是通过前后顺序,所以在定义默认参数时,要把必选参数放在前面,默认参数放在后面,否则会造成
必须参数的值错误的传入了默认参数,而必须参数没有传入值,从而报错
- 默认参数必须指向不可变类型,避免不必要的错误
3、可变参数
- 在python中,还可以定义可变参数
1、定义一个函数test,当传入的函数为a,b,c时,test函数可以计算a*a+b*b+c*c
>>> def test(number):
... sum = 0
... for i in number:
... sum = sum + i * i
... return sum
...
>>> print(test([1,2,3,4]))
30
>>> print(test((7,3,15,4)))
299
上面定义的test函数虽然可以达到效果,但是在每次调用函数时,传入的参数需要先组装成一个列表或者元组
2、使用可变参数优化后:
>>> def test(*number):
... sum = 0
... for i in number:
... sum = sum + i * i
... return sum
...
>>> print(test(1,2,3,4))
30
>>> print(test([1,2,3,4]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in test
TypeError: can't multiply sequence by non-int of type 'list'
>>> print(test())
0
>>> l = [1,2,3,4]
>>> print(test(l[0],l[1],l[2],l[3]))
30
>>> print(test(*l))
30
可以看到test函数的参数number前面加了一个 “ * ” ,在调用函数时无需组装成列表或元组,直接使用逗号隔开即可,可以传入任意数量的参数,包括0个参数,但是这样我们想要传入列表时就会报错,我们可以先定义列表,然后同样使用” * “来把列表传入函数
4、关键字参数
- 上面的可变参数允许传入
0个或者任意数量
的参数,传入的参数在函数调用时会自动组装成一个元组
,而关键字参数允许传入0个或者任意数量的包含参数名
的参数,这样参数会自动组装成一个字典
1、利用关键字参数定义函数test
>>> def test(name,age,**id):
... print('name:',name,'age:',age,'id:',id)
...
>>> print(test('zhangsan',22)) #在只传入前两个参数时,可以看到最后一个参数id输出了一个空字典{}
name: zhangsan age: 22 id: {}
None
>>> print(test('zhangsan',22,id=2223))
name: zhangsan age: 22 id: {'id': 2223}
None
>>> print(test('zhangsan',22,id=2223,other=5553)) #传入两个key=value
name: zhangsan age: 22 id: {'id': 2223, 'other': 5553}
None
>>> aaa = {'id':2223,'other':5553} #定义一个aaa字典
>>> print(test('zhangsan',22,id=aaa['id'],other=aaa['other'])) #传入字典时,使用下标调用
name: zhangsan age: 22 id: {'id': 2223, 'other': 5553}
None
>>> print(test('zhangsan',22,**aaa)) #和关键字参数一样,可以使用**一次性传入字典,需要注意的是,利用关键字参数把aaa字典传入到test函数中的**id参数,id参数会得到一个字典,而id的值只是aaa值的一份拷贝而已,修改id并不会影响aaa的值
name: zhangsan age: 22 id: {'id': 2223, 'other': 5553}
None
- 对于关键字参数来说,我们可以传入任意不受限制的关键字参数,而具体的值,需要在函数内部通过带
**
的参数(即**id
)来检查
5、命名关键字参数
1、以上面的test函数为例,现在我们需要检查传入的参数是否有id和other参数,可以这样写:
>>> def test(name,age,**id):
... if 'id' in id:
... pass
... if 'other' in id:
... pass
... print('name:',name,'age:',age,'id:',id)
...
>>> print(test('zhangsan',22,id=2222,other=3333,io=4444))
name: zhangsan age: 22 id: {'id': 2222, 'other': 3333, 'io': 4444}
None
#但是发现我们仍然可以传入其他的参数,如果想要达到上面的效果,对传入的参数进行限制,我们可以使用命名关键字参数,例如:
>>> def test(name,age,*,id,other):
... print('name:',name,'age:',age,'id:',id,'other:',other)
...
>>> print(test('zhangsan',22,id=3333,other=4444))
name: zhangsan age: 22 id: 3333 other: 4444
None
>>> print(test('zhangsan',22,id=3333,other=4444,io=5555)) #可以看到使用命名关键字参数后,除了id和other,继续传入io参数时会报错
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: test() got an unexpected keyword argument 'io'
>>> print(test('zhangsan',22,3333,4444)) #除此之外,也不能和位置参数一样直接传入值,必须传入带函数名的值
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: test() takes 2 positional arguments but 4 were given
2、命名关键字参数还可以利用默认参数
>>> def test(name,age,*,id=2222,other): #id默认为2222
... print('name:',name,'age:',age,'id:',id,'other:',other)
...
>>> print(test('zhangsan',22,other=3333))
name: zhangsan age: 22 id: 2222 other: 3333
None
在定义命名关键字参数时需要注意:
- 在使用命名关键字参数时,需要注意加
” * “
,否则后面的命名关键字参数将会被python视为位置参数
6、参数组合
- 参数组合其实就是使用上面的必选(位置)参数、默认参数、可变参数、关键字参数和命名关键字参数来定义函数
- 需要注意的是在使用这些参数时有着定义顺序,顺序必须为
必选(位置)——默认——可变——命名关键字——关键字
1、下面来看几个例子
>>> def test_1(a,b,c=0,*args,**kw):
... print('a=',a,'b=',b,'c=',c,'agrs=',args,'kw=',kw)
...
>>> print(test_1(1,2))
a= 1 b= 2 c= 0 agrs= () kw= {}
None
>>> print(test_1(1,2,c=4))
a= 1 b= 2 c= 4 agrs= () kw= {}
None
>>> print(test_1(1,2,4,'aaa','bbb'))
a= 1 b= 2 c= 4 agrs= ('aaa', 'bbb') kw= {}
None
>>> print(test_1(1,2,4,'aaa','bbb',id=444,other=555))
a= 1 b= 2 c= 4 agrs= ('aaa', 'bbb') kw= {'id': 444, 'other': 555}
None
>>> def test_2(a,b,c=0,*,d,**kw):
... print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)
...
>>> print(test_2(1,2,d=4,id=1222,other=3333))
a = 1 b = 2 c = 0 d = 4 kw = {'id': 1222, 'other': 3333}
None
定义列表和字典
>>> l_1=['aaa','bbb']
>>> d_1={'id':111,'other':222}
>>> print(test_1(1,2,3,*l_1))
a= 1 b= 2 c= 3 agrs= ('aaa', 'bbb') kw= {}
None
>>> print(test_1(1,2,3,*l_1,**d_1))
a= 1 b= 2 c= 3 agrs= ('aaa', 'bbb') kw= {'id': 111, 'other': 222}
None
>>> l_2=[1,2,3,4]
>>> d_2={'d':1111,'id':2222,'other':3333}
>>> print(test_1(*l_2,**d_2))
a= 1 b= 2 c= 3 agrs= (4,) kw= {'d': 1111, 'id': 2222, 'other': 3333}
None
>>> l_3 = [1,2,3]
>>> print(test_2(*l_3,**d_2))
a = 1 b = 2 c = 3 d = 1111 kw = {'id': 2222, 'other': 3333}
None
2、可以看到使用了*和**传入了列表和字典,以print(test_1(*l_2,**d_2)):
test_1的位置参数有2个,默认参数有1个,可变参数有一个,关键字参数有一个,按顺序来看l_2的值为1、2、3、4,使用*一次性传入其实就变成了,1、2给位置参数赋值,3给默认参数赋值,4给可变参数赋值,如果l_2列表为1、2、3、4、5的话,那么4和5其实都是给可变参数赋值,而d_2字典给关键字参数赋值了
3、同理来看print(test_2(*l_3,**d_2)) ,其实都是一样的,只不过是d_2字典中的d值,先给命名关键字参数赋值了,然后后面的id和other才给最后的关键字参数赋值,需要注意的值使用的l_3列表的元素数量是与test_2函数的参数相匹配的,否则会报错
- 可以看到,对于仍和参数都可以通过类似于
test(*args,**kw)
的形式去调用它,无论他的参数是如何定义的
7、总结
- 默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误
- 要注意定义可变参数和关键字参数的语法:
*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的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
五、递归函数
-
在函数内部是可以调用其他函数的,而如果一个函数在自己的
内部调用自己本身
,那么这个函数就是递归函数 -
下面来看几个案例:
1、定义一个计算阶乘如1*2*3*4的函数
>>> def test(n):
... if n == 1:
... return 1
... return n * test(n-1)
...
>>> print(test(1))
1
>>> print(test(5))
120
>>> print(test(100))
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
来解析计算过程的话,其实就是:
test(5)
5 * test(4)
5 * (4 * test(3))
5 * (4 * (3 * test(2)))
5 * (4 * (3 * (2 * test(1))))
5 * (4 * (3 * (2 * 1))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120
- 递归函数的优点就是定义简单,逻辑清晰,理论上所有递归函数都可以写成循环的方式,但是循环的算法逻辑没有递归函数清晰
拓展-尾递归
- 使用递归函数需要防止栈
(stack)
溢出,在计算机中,函数调用是通过调用栈来实现的,每进入一个函数调用,栈就会多加一层栈帧,同样的每当函数返回时,栈就会减少一层栈帧 - 而由于栈的大小并不是无线的,所以当递归函数的调用次数过多,会导致
栈溢出
,例如:
>>> print(test(1000))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in test
File "<stdin>", line 4, in test
File "<stdin>", line 4, in test
[Previous line repeated 995 more times]
File "<stdin>", line 2, in test
RecursionError: maximum recursion depth exceeded in comparison
- 那么如何解决递归函数调用时引起的栈溢出呢,我们可以通过
尾递归优化
,而其实尾递归和循环的效果是相同的,所以可以把循环看成一种特殊的尾递归函数 - 尾递归就是指在函数返回的时候,调用自己本身,并且
使用return语句时不能包含表达式
,这样编译器或者解释器就可以把尾递归做优化,使递归无论调用多少次,都只占用一个栈
,从而解决栈溢出的问题 - 下面来看一下尾递归的案例:
以上面的test函数为例,test函数由于在末尾的return语句引用了乘法表达式,所以test函数不是尾递归,下面来优化一下test函数
>>> def test(n):
... return test_2(n,1)
...
>>> def test_2(num,pro):
... if num == 1:
... return pro
... return test_2(num - 1,num * pro)
...
可以看到test_2函数在最后只返回了test_2函数本身,num-1和num*pro在函数调用前就会被计算,不会影响函数调用
>>> print(test_2(5,1)) #test_2(5,1)就对应上面的test(5)
120
- 尾递归调用时,如果做了优化,栈就不会增长,从而避免栈溢出
- 但是大多数编程语言都没有针对尾递归做优化,python也没有,所以及时把上面的test函数改成尾递归方式,栈也会溢出
一下尾递归的案例:
以上面的test函数为例,test函数由于在末尾的return语句引用了乘法表达式,所以test函数不是尾递归,下面来优化一下test函数
>>> def test(n):
... return test_2(n,1)
...
>>> def test_2(num,pro):
... if num == 1:
... return pro
... return test_2(num - 1,num * pro)
...
可以看到test_2函数在最后只返回了test_2函数本身,num-1和num*pro在函数调用前就会被计算,不会影响函数调用
>>> print(test_2(5,1)) #test_2(5,1)就对应上面的test(5)
120
- 尾递归调用时,如果做了优化,栈就不会增长,从而避免栈溢出
- 但是大多数编程语言都没有针对尾递归做优化,python也没有,所以及时把上面的test函数改成尾递归方式,栈也会溢出