简单的说,函数就是个黑匣子,它接收输入(参数),然后执行特定任务以完成特定功能,最后生成输出(返回值)。其中,输入(参数)和输出(返回值)都是可选的,也就是说,可以有也可以没有。
函数就是执行特定任务以完成特定功能的一段代码。可以在程序中将某段代码定义成函数,并指定一个函数名及接收的输入(参数),这样,就可以在程序的其它地方通过函数名多次调用并执行该段代码了,每次调用并执行后,都会根据接收的输入(参数)执行特定任务以完成特定功能从而生成相应的输出(返回值)。
Python语言已经定义了很多内置函数,我们可以在程序中通过函数名直接调用这些内置函数。
函数的作用
- 复用代码:如果在程序中需要多次完成某个特定的功能,我们无需把这个特定功能的相关代码在多个地方编写多次,完全可以把这个特定功能的相关代码定义成函数,然后在多个地方调用该函数,每次调用都会把函数对应的相关代码执行一遍。
- 隐藏实现细节:函数就是个黑匣子,将实现细节隐藏起来了,很多时候我们无需关注函数的实现细节,只需关注其接收的输入(参数)及生成的输出(返回值)就可以了。
- 提高可维护性:把完成某段特定功能的代码定义为函数后,如果需要对这段代码进行修改,只需要在一个地方进行修改,提高了程序的可维护性。否则,你需要找到这段代码的多个不同地方,每个地方都需要进行同样的修改。
- 提高可读性、便于调试:每个函数都对应一段完成特定功能的代码,提高了程序的可读性,也便于程序调试。
函数的定义及调用
定义函数
定义函数的语法格式为:
def 函数名([形式参数1, 形式参数2, ...., 形式参数n]):
函数体
关于函数名的说明:
- 每个函数都有一个函数名,用于函数的调用
- 函数名属于标识符,因此,必须遵守标识符的命名规则。此外,函数名最好是动宾格式,以表明函数完成的特定功能。
关于形式参数的说明:
- 形式参数简称形参
- 形参用于在调用函数时接收输入,也就是接收传递的实际参数(简称实参)。
- 形参用中括号括起来,表示形参是可选的,也就是说,可以定义也可以不定义。
- 形参的本质是变量,其作用域(起作用的范围)仅限于函数体。
关于函数体的说明:
- 函数体适用于执行特定任务以完成特定功能的主体代码。
- 函数体对应的代码必须缩进。
- 如果函数需要有输出(返回值),可以在函数体内通过语句return xxx进行返回,同时结束函数体的执行。如果函数不需要有输出(返回值),可以在函数体内通过语句 return直接结束函数体的执行,或者让函数体正常执行结束,其实,函数在这两种情况下都是有返回值的,其返回值都是None。
- 函数体在调用函数时才会被执行,因此,定义函数不会改变程序的执行流程。
def decide_args(arg1, arg2):
if arg1 and arg2:
return arg1, arg2
elif (not arg1) and (not arg2):
return
else:
result = arg1 or arg2
# 定义函数后,就创建了一个函数对象,他的内心是function
print(decide_args) # <function decide_args at 0x000002471FB63E20>
print(type(decide_args)) # <class 'function'>
调用函数
调用函数时,每个实参都被用于初始化相应的形参。所有形参都被初始化之后,函数体对应的代码块被执行。程序的执行流会跳转到定义函数的函数体内,执行函数体对应的代码块,执行网函数体后再跳回到调用函数的地方,继续执行下一条语句。
def decide_args(arg1, arg2):
if arg1 and arg2:
return arg1, arg2
elif (not arg1) and (not arg2):
return
else:
result = arg1 or arg2
# 定义函数后,就创建了一个函数对象,他的内心是function
print(decide_args) # <function decide_args at 0x000002471FB63E20>
print(type(decide_args)) # <class 'function'>
print(decide_args(18, 'Hello')) # (18, 'Hello')
print(decide_args([], {})) # None
print(decide_args(18, [])) # None
调用-位置实参
调用函数时,可以根据每个形参在所有形参中的位置传递对应位置的市场,从而用每个实参初始化对应位置的形参,这样的实参称为位置实参。
上方代码就是使用的位置实参进行传递。
调用-关键字实参
调用函数时,传递的实参的形式可以为:形参名 = 实参值,从而用指定的实参值初始化指定名称的形参,这样的实参称为关键字实参。
def f(a, b, c):
print('a =', a,'b =', b,'c =', c)
f(a = 2,b = 3,c = 4)
由于关键字实参中指定了形参名,所以实参和形参的匹配关系更加清晰,而且每个关键字实参在所有关键字实参中的位置是可以任意的
f(b = 1,c = 2,a = 5) # a = 5 b = 1 c = 2
调用函数时,可以组合使用位置实参和关键字实参,但是,位置实参必须位于关键字实参之前,否则,无法根据位置来匹配位置实参和对应的形参。
f(1,2,c= 5) # a = 1 b = 2 c = 5
调用-实参的传递
变量相当于标签,对于赋值语句:变量 = 对线给,相当于给对象贴了一个标签,标签名就是变量名。
调用函数时把实参传递给形参从而用实参初始化形参,本质上执行了赋值语句:形参 = 实参对象,相当于给实参对象贴了一个标签,标签名就是形参名。
如果实参对象是可变类型,在函数体内对形参对象的任何修改其实就是对实参对象的修改。
def f(arg1, arg2):
print('初始化形参后:arg1 =', arg1, 'arg2 =', arg2)
arg1 *= 2
arg2.append(4)
print('修改形参后: arg1 =', arg1, 'arg2 =', arg2)
i = 10
L = [1, 2, 3]
print('调用函数前:i =', i, 'L =', L)
f(i, L)
print('调用函数后:i =', i, 'L =', L)
返回结果如下:
调用函数前:i = 10 L = [1, 2, 3]
初始化形参后:arg1 = 10 arg2 = [1, 2, 3]
修改形参后: arg1 = 20 arg2 = [1, 2, 3, 4]
调用函数后:i = 10 L = [1, 2, 3, 4]
定义-多个返回值
如果需要在调用函数后有多个返回值,可以在定义函数时在函数体内使用return语句返回由多个返回值组成的元组。
# 把列表中的所有数分成奇数和偶数两类
def classify_numbers(numbers):
odds = []
evens = []
for number in numbers:
if number % 2:
odds.append(number)
else:
evens.append(number)
return odds, evens
print(classify_numbers([15, 86, 39, 26, 53, 68])) # ([15, 39, 53], [86, 26, 68])
# 查找列表中的最小值和最大值
def lookup_min_max(numbers):
if len(numbers) == 0:
return
min_num = numbers[0]
max_num = numbers[0]
for number in numbers[1:len(numbers)]:
if number < min_num:
min_num = number
elif number > max_num:
max_num = number
return min_num, max_num
print(lookup_min_max([35, 26, 19, 86, 93, 68])) # (19, 93)
定义-带默认值的形参
定义函数时,可以给形参设置默认值,这样,调用函数时如果不传递对应的实参,就会使用设置的默认值初始化形参。
给形参设置默认值的格式为:形参 = 默认值。
给形参设置默认值后,可以简化函数的调用,只有与默认值不同的形参才需要传递额外的实参。
def f1(a, b = 5):
print('a =', a, 'b =', b)
f1(2, 6) # a = 2 b = 6
f1(2) # a = 2 b = 5
def f2(a, b = 5, c = 8):
print('a =', a, 'b =', b, 'c =', c)
f2(2, 6, 9) # a = 2 b = 6 c = 9
f2(2) # a = 2 b = 5 c = 8
f2(2, 6) # a = 2 b = 6 c = 8
f2(2, c = 9) # a = 2 b = 5 c = 9
定义函数时,没有设置默认值的形参必须位于设置了默认值的形参之前,否则,无法根据位置来匹配位置实参和对应的形参。
当函数有多个形参时,把变化大的形参放在前面,把变化小的形参放在后面,变化小的形参就可以设置默认值。
给形参设置默认值后,调用函数就存在多种调用方式。
定义函数时,给形参设置的默认值就被计算出来了,因此,如果给形参设置的默认是是可变类型的对象,并且前一次调用函数时在函数体内修改了形参的默认值,那么修改后的值将作为下一次调用函数时形参的默认值。
定义-使用*定义关键字形参
定义函数时,可以在所有形参的某个位置添加一个*,这样*后面的所有形参都被定义为只能接受关键字实参的关键字形参。
def f(a, b, *, c, d):
print('a =', a, 'b =', b, 'c =', c, 'd =', d)
f(1, 2, c =3, d =4) # a = 1 b = 2 c = 3 d = 4
定义-使用*定义个数可变的位置形参
定义函数时,可能无法事先确定穿的位置实参的个数,在这种情况下,可以在形参前面添加一个*,将形参定义为个数可变的位置形参,从而可以接受0个或任意多个位置实参。这些位置实参会将个数可变的位置形参初始化为一个元组。
def f(*args):
print(args)
f() # ()
f(1) # (1,)
f(1, 2, 3) # (1, 2, 3)
定义函数时,最多只能定义一个个数可变的位置形参
很多内置函数都定义了个数可变的位置形参。列入内置函数print()。
通常,把个数可变的位置形参定义为最后一个形参,一遍接收所有剩余的位置实参。
def fun1(a, b, *c):
print('a =', a, 'b =', b, 'c ', c)
fun1(1, 2, 3, 4, 5) # a = 1 b = 2 c (3, 4, 5)
如果个数可变的位置形参不是最后一个形参,那么其后面的所有形参都被定义为只能接受关键字实参的关键字形参。如果向这些关键字形参传递位置实参,所有的位置实参都会被算作个数可变的形参,从而导致关键字实参的缺失。
调用-使用*将序列中的每个元素都转换为位置实参
def f(a, b, c):
print('a =', a, 'b =', b, 'c =', c)
f(1, 2, 3) # a = 1 b = 2 c = 3
L = [1, 2, 3]
# 列表L整体作为一个位置实参
# f(L) # TypeError: f() missing 2 required positional arguments: 'b' and 'c'
f(L[0], L[1], L[2]) # a = 1 b = 2 c = 3
# 调用函数时,可以在序列前面添加一个*,从而将序列中的每个元素都转换为一个单独的位置实参。
f(*L) # a = 1 b = 2 c = 3
# 注意和个数可变的位置形参进行区分。个数可变的位置形参是在定义函数时使用,使用*将序列中的每个元素都转换为位置实参是在调用函数时使用。
def fun(*args):
print(args)
fun(L) # ([1, 2, 3],)
fun(*L) #(1, 2, 3)
定义-使用**定义个数可变的关键字形参
定义函数时,可能无法事先确定传递的关键字实参的个数,在这种情况下可以在形参前添加两个*,将形参定义为个数可变的关键字形参,从而可以接受0个或任意个关键字实参。这些关键字实参会将个数可变的关键字形参初始化为一个字典。
def f(**kwargs):
print(kwargs)
f() # {}
f(a = 1) # {'a': 1}
f(a = 1, b = 2, c =3) # {'a': 1, 'b': 2, 'c': 3}
定义函数时,最多只能定义一个个数可变的关键字形参。
很多内置函数都定义了个数可变的关键字形参。例如,内置函数sorted()。
因为调用函数时为止实参必须位于关键字实参之前,所以个数可变的位置形参必须位于个数可变的关键字形参之前。
调用-使用**将字典中的每个键值对都转换为关键字实参
def f(a, b, c):
print('a =', a, 'b =', b, 'c =', c)
f(a= 1, b = 2, c = 3) # a = 1 b = 2 c = 3
d = {'a': 1, 'b': 2, 'c': 3}
# 将字典d整体作为一个位置实参,报错如下:
# f(d) # TypeError: f() missing 2 required positional arguments: 'b' and 'c'
f(**d) # a = 1 b = 2 c = 3
定义-文档字符串
什么是文档字符串
对于函数、模块、类或方法,位于其第一行的字符串被称为文档字符串,通常用三个引号表示。
文档字符串用于对函数、模块、类或方法进行解释说明。
之所以称为“文档”字符串,是因为可以使用工具根据文档字符串自动的生成文档。
应该养成编写文档字符串的习惯,以提高程序的可读性。
通过属性__doc__
可以访问文档字符串。
调用内置函数help()得到的帮助信息中会包含文档字符串。
print(len.__doc__)
print(help(len))
常见内容和格式约定
- 第一行公司简明扼要的总结
- 第一行的首字母大写,第一行以句号结尾。
- 如果文档字符串包含多行,第二行是空行,从第三行开始时详细的描述。
定义-函数注解
定义函数时,为了让形参或返回值的类型或作用更加清晰,可以给形参或返回值添加函数注解,从而对形参或返回值做解释说明,以帮助函数文档化。
函数注解是可选的,可以添加也可以不添加。
函数注解可以是任意的表达式
解释器会忽略函数注解,因此解释器并不会使用函数注解来检查实参的类型和返回值的类型。
添加函数注解
给形参添加函数注解的方式为:在形参后面添加:和任意的表达式。
给返回值添加函数注解的方式为:在)的后面添加->和任意的表达式
def f(a: 'string type', b: int) -> 'join a with b':
return a + str(b)
print(f('hello', 12.3)) # hello12.3
访问函数注解
通过属性__annotations__
可以访问函数注解。
调用内置函数help()得到的帮助信息中会包含函数注解。
def f(a: 'string type', b: int) -> 'join a with b':
return a + str(b)
print(f.__annotations__)
print(help(f))
返回结果如下:
{'a': 'string type', 'b': <class 'int'>, 'return': 'join a with b'}
Help on function f in module __main__:
f(a: 'string type', b: int) -> 'join a with b'
None
pass语句
pass语句什么都不做,他只是一个张伟胡,用在语法上需要语句的地方,例如:
- if语句的条件执行体
- for-in语句的循环体
- 定义函数时的函数体
有时候可能还没有想好上述相关语句该怎么写,就可以先试用pass语句作为占位符,以确保程序可以运行,等想好了之后再把pass语句替换掉。
age = 23
if age > 18:
pass
for i in range(8):
pass
def test():
pass
递归函数
在一个函数的函数体内,可以调用其他函数。
如果在一个函数的函数体内调用了该函数本身,该函数就是递归函数。
递归函数包含了一种隐式的循环,因此,递归函数必须有一个明确的递归结束条件,也成为递归出口。
能用递归来解决的问题必须满足两个条件:
- 可以通过递归调用来缩小问题的规模,且新问题与原问题有着相同的形式。
- 存在一个简单情景,可以使递归在简单情景下退出
使用递归计算阶乘
n! = 1 * 2 * 3 * ... * n = (n -1)! * n,且1! = 1
如果用函数fac(n)表示n!,那么fac(n) = fac(n - 1) * n = n * fac(n - 1),且fac(1) = 1
def fac(n):
if n == 1:
return 1
return n * fac(n - 1)
print('fac(10) =', fac(10)) # fac(10) = 3628800
lambda函数
lambda 函数是一种小的匿名函数。
lambda 函数可接受任意数量的参数,但只能有一个表达式。
语法:
lambda arguments : expression
x = lambda a : a + 10
print(x(5)) # 15
def myfunc(n):
return lambda a : a * n
mydoubler = myfunc(2)
print(mydoubler(11)) # 22