函数是可重用的代码块,它不仅可以实现代码的复用,而且可以实现代码的一致性。一致性是指,只要修改函数内部的代码,则所有调用函数的地方都能得到体现。
程序由一个个任务组成,函数就代表一个任务或者一个功能;函数是代码复用的通用机制。
4.1 Python 函数的分类
- 内置函数
内置函数可以直接使用,比如 str( )、len( )、ord( )、chr( )、sorted( )等。 - 标准库函数
可以通过 import 语句导入库,然后使用其中定义的函数。 - 第三方库函数
Python 社区也提供了很多高质量的库,下载安装这些库后,通过 import 语句导入后,就可以使用这些第三方库的函数。 - 用户自定义函数
开发中适应用户自身需求定义的函数。
4.2 函数的定义和调用
Python 中,函数定义的语法如下:
def 函数名([参数列表]):
'''注释'''
函数体
return 返回值
Python 执行 def 时,会创建一个函数对象,绑定到函数名变量上;
参数不需要指定数据类型,有多个参数时,用逗号分隔;无参数,括号也必须保留;
返回值不需要指定数据类型;通过 return 可以结束函数并返回值;无 return 时,返回 None 值;返回多个值使用列表、元组、字典、集合即可。
调用函数前,必须先定义函数。内置函数对象会自动创建,标准库和第三方库函数,通过 import 导入模块时,会执行模块中的 def 语句。
函数调用的语法如下:
函数名([参数列表])
# 定义函数,就是定义了一个变量 test,指向了函数对象
def test():
print('My first function')
# 调用函数
test()
# 函数对象,将 test 引用的函数对象赋值给 fun 变量
fun = test
# 通过 fun 调用函数
fun()
4.3 函数的参数
形参属于局部变量,只在函数范围内可见。
4.3.1 参数传递
参数传递的本质:将形参赋值给实参。Python 中,一切皆对象,所有的赋值操作都是“引用的传递”,所以参数的传递都是“引用传递”,不存在“值传递”。具体操作时分为两类:
- 对可变对象的修改,直接作用于原对象本身,如列表、字典、集合、自定义对象;
- 对不可变对象的修改,会创建对象的浅拷贝,再对拷贝对象进行操作(达到“值传递”的效果,但是不是“值传递”),如元组、字符串、整数、浮点数、function。
# 传递可变对象
my_list = [10, 20]
print(id(my_list))
print(my_list)
def mutable_para(li):
print(id(li))
li.append(30)
mutable_para(my_list)
print(my_list)
-------------执行结果--------------
1515465441216
[10, 20]
1515465441216
[10, 20, 30]
# 传递不可变对象
num = 100
print(f'{num} : {id(num)}')
def immutable_para(n):
print(f'{n} : {id(n)}') # 传进来的是 num 对象的引用
n += 200 # 由于 num 是不可变对象,创建了拷贝对象
print(f'{n} : {id(n)}') # n 已经变成新对象了
immutable_para(num)
print(f'{num} : {id(num)}')
-------------执行结果--------------
100 : 140708722840336
100 : 140708722840336
300 : 2197745400848
100 : 140708722840336
浅拷贝和深拷贝
浅拷贝,使用内置函数 copy()
,不拷贝子对象的内容,只拷贝子对象的引用,对子对象的修改会影响源对象。
深拷贝,使用内置函数 deepcopy()
,会将子对象的内容也全部拷贝一份,对子对象的修改不会影响源对象。
import copy
def test_copy():
"""测试浅拷贝"""
a = [10, 20, [1, 2]]
b = copy.copy(a)
print(f'a : {a}')
print(f'b : {b}')
b.append(30)
b[2].append(3)
print(f'a : {a}')
print(f'b : {b}')
test_copy()
-------------执行结果--------------
a : [10, 20, [1, 2]]
b : [10, 20, [1, 2]]
a : [10, 20, [1, 2, 3]]
b : [10, 20, [1, 2, 3], 30]
import copy
def test_deep_copy():
"""测试深拷贝"""
a = [10, 20, [1, 2]]
b = copy.deepcopy(a)
print(f'a : {a}')
print(f'b : {b}')
b.append(30)
b[2].append(3)
print(f'a : {a}')
print(f'b : {b}')
test_deep_copy()
-------------执行结果--------------
a : [10, 20, [1, 2]]
b : [10, 20, [1, 2]]
a : [10, 20, [1, 2]]
b : [10, 20, [1, 2, 3], 30]
4.3.2 位置参数
位置参数,是按照从左到右的顺序定义的参数,调用时实参与形参按照顺序一一对应;位置参数调用时必须被传值。
def name_para(a, b, c):
print(a if c > 0 else b)
name_para(10, 20, 0) # 位置参数
4.3.3 关键字参数
函数调用时,按照 key=value
的形式传入实参,可以不按照顺序进行传参。
注意:
- 关键字参数必须在位置参数右边;
- 对同一个参数不能重复传值。
def name_para(a, b, c):
print(a if c > 0 else b)
name_para(c=1, a=100, b=200) # 关键字参数
4.3.4 默认值参数
默认值参数,就是定义时设置了默认值的参数。
注意:
- 默认值参数在定义时必须放在位置参数的右边;
- 调用时是可选的。
def default_para(a, b, c=1): # c 为默认值参数
return a if c == 1 else b
print(default_para(10, 20, 0)) # 传入默认值参数
print(default_para(10, 20)) # 不传入默认值参数
4.3.5 可变长度参数
可变长度参数,是指调用时实参个数不固定的参数。
其实调用时,实参只有位置参数和关键字参数,所以要提供两种解决方案分别处理:
- 可变长度的位置参数:使用
*
(一个星号)接收,将多个溢出的位置参数放到一个“元组”对象中; - 可变长度的关键字参数:使用
**
(两个星号)接收,将多个溢出的关键字参数放到一个“字典”对象中。
可变参数 *args 与 **kwargs 通常是组合在一起使用的,如果一个函数的形参为 (*args, **kwargs),代表该函数可以接收任何形式、任意长度的参数。
实参中 * 和 ** 的作用:打回原形。如 fun(*(1, 3)) 就代表 fun(1, 3),fun(**{‘a’:1, ‘b’:2}) 就代表 fun(a=1, b=2)。
def mutable_para_num1(a, *b): # 可变长位置参数
print(a, b)
mutable_para_num1(1, 2, 3, 4)
---------------执行结果-----------------
1 (2, 3, 4)
def mutable_para_num2(a, **b): # 可变长关键字参数
print(a, b)
mutable_para_num2(1, name='lw', age=30)
---------------执行结果-----------------
1 {'name': 'lw', 'age': 30}
def mutable_para_num3(a, *args, **kwargs):
print(a, b, c)
mutable_para_num3(1, 2, 3, 4, name='lw', age=30)
---------------执行结果-----------------
1 (2, 3, 4) {'name': 'lw', 'age': 30}
4.3.6 命名关键字参数
在可变长参数后面还有其他参数时,必须是“命名关键字参数”。
def force_named_para(*args, b, c):
print(args, b, c)
force_named_para(1, 2, c=4, b=3)
---------------执行结果-----------------
(1, 2) 3 4
4.4 函数的注释
函数应该尽可能见名知意,也可以在函数内部的第一行写文档字符串,叫函数的注释,用 '''
或者 """
来表示。
def print_max(a, b):
"""打印 a 和 b 中的较大值"""
print('较大值', a if a > b else b)
可以使用 help(函数名.__doc__)
打印输出函数的注释,执行结果如下:
No Python documentation found for '打印 a 和 b 中的较大值'.
4.5 命名空间与作用域
4.5.1 命名空间
命名空间,即存放名字与对象映射关系的地方。定义 x=“abc”,Python 会申请内存空间存放对象 abc,然后将名字 x 与 “abc” 的绑定关系存放于名称空间中,del x 表示清除该绑定关系。
三种命名空间
在程序执行期间最多会存在三种名称空间:
- 内置命名空间
随着 Python 解释器的启动而产生,随着 Python 解释器的关闭而销毁,是第一个被加载的命名空间,用来存放一些内置名称。 - 全局命名空间
随着 Python 文件的执行而产生,文件执行完毕而销毁,是第二个被加载的命名空间,用来存放文件执行过程中产生的名称。 - 局部命名空间
随着函数的调用而产生,函数的结束而销毁,用来存放函数的形参以及函数内定义的变量。
命名空间的加载顺序
内置命名空间 > 全局命名空间 > 局部命名空间
名字的查找顺序
局部命名空间 > 全局命名空间 > 内置命名空间
4.5.2 作用域
变量起作用的范围称为变量的作用域,不同作用域内的同名变量互不影响。变量按照作用域分为:全局变量(内置名称空间与全局名称空间)和局部变量(局部名称空间)。
作用域关系是在函数定义阶段就已经固定的,与函数的调用位置无关。
全局变量
在函数和类之外定义的变量,位于内置命名空间和全局命名空间,从定义位置开始全局有效。
全局变量降低了函数的通用性和可读性,应尽量避免使用,一般用作常量的定义。
函数内要改变全局变量,需要使用 global 关键字。
使用 globals()
可以获取所有全局变量,结果为字典。
局部变量
在函数和类中定义的变量,位于局部命名空间,作用域为定义的函数或者类。
局部变量的引用比全局变量快,优先考虑使用。
如果局部变量和全局变量同名,则在函数内部隐藏全局变量,只使用同名的局部变量。
使用 locals()
可以获取所有局部变量,结果为字典。在全局作用域 locals() 与 globals() 的结果相同。
num = 100 # 声明全局变量
def test_variable():
num = 200 # 声明局部变量,隐藏全局变量
print(num)
test_variable()
----------执行结果----------
200
num = 100 # 声明全局变量
def test_variable():
global num # 在函数内部改变全局变量,使用 global 关键字
print(num)
num = 300
test_variable()
----------执行结果----------
100
LEGB 规则
Python 在查找“名称”时,按照 LEGB 规则查找,即 Local > Enclosed > Global > Built in。
Local – 函数或者类的方法内部;
Enclosed – 嵌套函数,闭包;
Global – 模块中的全局变量;
Built in – Python 中的内置名称。
如果某个名称按照 LEGB 规则没有找到,就会产生 NameError 错误。
4.6 匿名函数
匿名函数,就是没有名字的函数,一次性使用。通常使用 lambda 表达式来声明。
lambda 表达式只允许包含一个表达式,不能包含复杂语句,该表达式的计算结果就是函数的返回值。
lambda 表达式语法:lambda arg1,arg2,arg3... : 表达式
fun = lambda a, b: a * b
print(fun)
print(fun(2, 3))
------------------执行结果-----------------
<function <lambda> at 0x0000019CADF17280>
6
li = [lambda a: a * 2, lambda b: b * 3, lambda c: c * 4]
print(li[0](2), li[1](3), li[2](4))
------------------执行结果-----------------
4 9 16
salaries = {
'siry': 3000,
'tom': 7000,
'lili': 10000,
'jack': 5000
}
max(salaries, key=lambda k: salaries[k]) # 获取薪资最高的人名
sorted(salaries, key=lambda k: salaries[k], reverse=True) # 按降序排列薪资
4.7 递归函数
递归函数是指在函数内部调用自己,必须包含2个部分:
- 终止条件:表示递归在什么时候结束,不再调用自己;
- 递归步骤:第 n 次调用与第 n-1 次调用需要有关联。
递归函数在调用时会创建大量的函数对象,极其消耗内存和运算能力。在处理大数据量时,谨慎使用。
Python中,递归层级限制默认为 1000
,可通过 sys.getrecursionlimit()
获取,也可通过 sys.setrecursionlimit(num)
设置递归层级。
def factorial(n):
"""计算阶乘"""
if n == 1:
return 1
else:
return n * factorial(n - 1)
print(factorial(5))
def accumulation(n):
"""计算累加和"""
if n == 1:
return 1
else:
return n + accumulation(n - 1)
print(accumulation(100))
4.8 嵌套函数
在函数内部定义的函数,只能在定义的函数中调用,外面调用不了。用于专门给外部函数提供服务的场景。
def f1():
print('f1 running')
def f2():
print('f2 running')
f2() # 只能在 f1 内部调用
f1()
在内部函数中,修改外部函数的变量需要用 nonlocal
关键字声明。
def f1():
b = 10
def f2():
nonlocal b # 声明外部函数的局部变量
print(f'inner b: {b}')
b = 20
f2()
print(f'outer b: {b}')
f1()
------------------执行结果-----------------
inner b: 10
outer b: 20
4.9 闭包函数
闭包函数是一个记录了定义它的作用域信息的函数。
闭包函数必须满足以下3个条件:
- 定义在函数内部的函数;
- 内部函数包含对外部函数作用域中变量的引用;
- 通常将闭包函数通过 return 语句返回,可以在任意位置调用产生同样的结果。
def outer():
x = 10
def inner(): # 闭包函数
print(x)
return inner
闭包函数的用途
给函数传值有两种方式:一是通过参数传值,二是通过闭包将值包给函数。
import requests
# 方式一
def get(url):
return requests.get(url).text
# 方式二
def page(url):
def get():
return requests.get(url).text
return get
对比两种传值方式,如果爬取同一页面时,方式一需要重复传入 url,而方式二只需要传入一次,就会得到一个包含指定 url 的闭包,以后调用该闭包就无需再传值。
# 方式一爬取同一网页
get('https://www.baidu.com')
get('https://www.baidu.com')
get('https://www.baidu.com')
# 方式二爬取同一网页
baidu = page('https://www.baidu.com')
baidu()
baidu()
baidu()