函数用法和底层分析
1.函数简介和分类
-
函数是可重用的程序代码块。
-
函数的作用,不仅可以实现代码的复用,更能实现代码的一致性。一致性指的是,只要修改函数的代码,则所有调用该函数的地方都能得到体现。
-
一个程序由一个一个的任务组成;函数就是代表一个任务或者一个功能(
function
)。 -
函数是代码复用的通用机制
-
分类思维图
- 内置函数:我们前面使用的
str()
、list()
、len()
等这些都是内置函数,我们可以拿来直接使用。 - 标准库函数:我们可以通过
import
语句导入库,然后使用其中定义的函数 - 第三方库函数:Python社区也提供了很多高质量的库。下载安装这些库后,也是通过 import 语句导入,然后可以使用这些第三方库的函数
- 用户自定义函数:用户自己定义的函数,显然也是开发中适应用户自身需求定义的函数。今天我们学习的就是如何自定义函数。
- 内置函数:我们前面使用的
2.函数的定义
- 语法
def 函数名 ([参数列表]) : '''文档字符串''' 函数体/若干语句
3.形参和实参
- 定义函数圆括号里面是形式参数列表,有多个参数时要用逗号隔开。
- 定义时的形式参数不需要声明类型,也不需要指定函数返回值类型。
- 调用时的实际参数必须与形参列表一一对应
- 函数定义时的参数为形式参数,函数传递时的参数为实际参数。
4.文档字符串
- 函数的注释
- 通过
help(函数名)
来打印输出函数的文档字符串 - 通过
函数名.__doc__
来直接获取函数的文档字符串
- 通过
5.返回值
- 如果函数体中包含
return
语句,则结束函数执行并返回值 - 如果函数体中不包含
return
语句,则返回None
值 - 要返回多个值,使用列表、元组、字典、集合将多个值“存起来”即可
6.函数对象的底层内存分析
- 示例代码:
def print_star(n):
print("*"*n)
print(print_star)
print(id(print_star))
c = print_star
c(3)
# 执行结果
<function print_star at 0x0000000002BB8620>45844000
上面代码执行 def
时,系统中会创建函数对象,并通过 print_star
这个变量进行引用:
我们执行 c=print_star
后,显然将 print_star
变量的值赋给了变量 c
,内存图变成了:
- 显然,我们可以看出变量
c
和print_star
都是指向了同一个函数对象。因此,执行c(3)
和执行print_star(3)
的效果是完全一致的。 - Python中,圆括号意味着调用函数。在没有圆括号的情况下,Python会把函数当做普通对象。
7.函数中变量的作用域
- 全局变量
- 在函数和类定义之外声明的变量。作用域为定义的模块,从定义位置开始直到模块结束。
- 全局变量降低了函数的通用性和可读性。应尽量避免全局变量的使用。
- 要在函数内改变全局变量的值,使用
global
声明一下
- 局部变量
- 在函数体中(包含形式参数)声明的变量。
- 局部变量的引用比全局变量快,优先考虑使用
- 如果局部变量和全局变量同名,则在函数内隐藏全局变量,只使用同名的局部变量
- 全局变量和局部变量的代码测试
def f1():
global a # 如果要在函数内改变全局变量的值,增加global关键字声明
print(a) # 打印全局变量a的值
a = 300
f1() # 100
f1() # 300
print(a) # 300
- 全局变量和局部变量同名测试
a=100
def f1():
a = 3 #同名的局部变量
print(a)
f1() # 3
print(a) #a仍然是100,没有变化
局部变量的查询和访问速度比全局变量快,优先考虑使用,尤其是
在循环的时候。
8.参数的传递
函数的参数传递本质上就是:从实参到形参的赋值操作。Python中
“一切皆对象”,所有的赋值操作都是“引用的赋值”。所以,Python
中参数的传递都是“引用传递”,不是“值传递”。
- 具体参数传递分为两种方式:
- 对“可变对象”(字典、列表、集合、自定义的对象等)进行“写操作”,直接作用于原对象本身。
- 对“不可变对象”(数字、字符串、元组、function等)进行“写操作”,会产生一个新的“对象空间”,并用新的值填充这块空间。
- 传递可变对象的引用
传递参数是可变对象(例如:列表、字典、自定义的其他可变对象
等),实际传递的还是对象的引用。在函数体中不创建新的对象拷
贝,而是可以直接修改所传递的对象。
测试代码如下:b = [10,20] def f2(m): print("m:",id(m)) #b和m是同一个对象 m.append(30) #由于m是可变对象,不创建对象拷 贝,直接修改这个对象 f2(b) # m: 45765960 print("b:",id(b)) # b: 45765960 print(b) # [10, 20, 30]
- 传递不可变对象的引用
传递参数是不可变对象(例如:int
、float
、字符串、元组、布尔
值),实际传递的还是对象的引用。在”赋值操作”时,由于不可变
对象无法修改,系统会新创建一个对象。
测试代码如下:
显然,通过 id 值我们可以看到 n 和 a 一开始是同一个对象。给n赋值后,n是新的对象。a = 100 def f1(n): print("n:",id(n)) #传递进来的是a对象的地址 n = n+200 #由于a是不可变对象,因此创建新的对象n print("n:",id(n)) #n已经变成了新的对象 print(n) f1(a) print("a:",id(a)) # 运行结果 n: 1663816464 n: 46608592 300 a: 1663816464
- 传递不可变对象包含的子对象是可变的引用
不创建对象,修改原对象。#传递不可变对象时。不可变对象里面包含的子对象是可变 的。则方法内修改了这个可变对象,源对象也发生了变化。 a = (10,20,[5,6]) print("a:",id(a)) def test01(m): print("m:",id(m)) m[2][0] = 888 print(m) print("m:",id(m)) test01(a) print(a) # 运行结果 a: 41611632 m: 41611632 (10, 20, [888, 6]) m: 41611632 (10, 20, [888, 6])
9.浅拷贝和深拷贝
为了更深入的了解参数传递的底层原理,我们需要测试一下“浅拷贝和深拷贝”。我们可以使用内置函数: copy
(浅拷贝)、 deepcopy
(深拷贝)。
- 浅拷贝:拷贝对象,但不拷贝子对象的内容,只是拷贝子对象的引用。
- 深拷贝:拷贝对象,并且会连子对象的内存也全部(递归)拷贝一份,对子对象的修改不会影响源对象
#测试浅拷贝和深拷贝
import copy
def testCopy():
'''测试浅拷贝'''
a = [10, 20, [5, 6]]
b = copy.copy(a)
print("a", a)
print("b", b)
b.append(30)
b[2].append(7)
print("浅拷贝......")
print("a", a)
print("b", b)
def testDeepCopy():
'''测试深拷贝'''
a = [10, 20, [5, 6]]
b = copy.deepcopy(a)
print("a", a)
print("b", b)
b.append(30)
b[2].append(7)
print("深拷贝......")
print("a", a)
print("b", b)
testCopy()
print("*************")
testDeepCopy()
#运行结果
a [10, 20, [5, 6]]
b [10, 20, [5, 6]]
浅拷贝......
a [10, 20, [5, 6, 7]]
b [10, 20, [5, 6, 7], 30]
a [10, 20, [5, 6]]
b [10, 20, [5, 6]]
深拷贝......
a [10, 20, [5, 6]]
b [10, 20, [5, 6, 7], 30]
10.参数的类型
-
位置参数
函数调用时,实参默认按位置顺序传递,需要个数和形参匹配。按位置传递的参数,称为:“位置参数”。
测试代码:def f1(a,b,c): print(a,b,c) f1(2,3,4) # 2 3 4 f1(2,3) #报错,位置参数不匹配
-
默认值参数
我们可以为某些参数设置默认值,这样这些参数在传递时就是可选的。称为“默认值参数”。默认值参数放到位置参数后面。
测试代码:def f1(a,b,c=10,d=20): #默认值参数必须位于普通位置参数后面 print(a,b,c,d) f1(8,9) f1(8,9,19) f1(8,9,19,29) # 运行结果 8 9 10 20 8 9 19 20 8 9 19 29
-
命名参数
我们也可以按照形参的名称传递参数,称为“命名参数”,也称“关键字参数”。
测试代码def f1(a,b,c): print(a,b,c) f1(8,9,19) #位置参数 f1(c=10,a=20,b=30) #命名参数 # 运行结果 8 9 19 20 30 10
-
可变参数
可变参数指的是“可变数量的参数”。分两种情况:*param
(一个星号),将多个参数收集到一个“元组”对象中。**param
(两个星号),将多个参数收集到一个“字典”对象中。
测试代码:
def f1(a,b,*c): print(a,b,c) f1(8,9,19,20) # 8 9 (19, 20) def f2(a,b,**c): print(a,b,c) f2(8,9,name='gaoqi',age=18) # 8 9 {'name': 'gaoqi', 'age': 18} def f3(a,b,*c,**d): print(a,b,c,d) f3(8,9,20,30,name='gaoqi',age=18) # 8 9 (20, 30) {'name':'gaoqi', 'age': 18}
-
强制命名参数
在带星号的“可变参数”后面增加新的参数,必须在调用的时候“强制
命名参数”。def f1(*a,b,c): print(a,b,c) #f1(2,3,4) #会报错。由于a是可变参数,将2,3,4全部 收集。造成b和c没有赋值。 f1(2,b=3,c=4) # (2,) 3 4
11.lambda表达式和匿名参数
- 常见希腊字母表:
lambda
表达式可以用来声明匿名函数。lambda
函数是一种简单的、在同一行中定义函数的方法。lambda
函数实际生成了一个函数对象。
lambda
表达式只允许包含一个表达式,不能包含复杂语句,该表达式的计算结果就是函数的返回值。 - 语法:
# arg1 arg2 arg3 为函数的参数。<表达式>相当于函数体。运算结果是:表达式的运算结果。 lambda arg1,arg2,arg3... : <表达式>
- lambda表达式应用
f = lambda a,b,c:a+b+c print(f) print(f(2,3,4)) g = [lambda a:a*2,lambda b:b*3,lambda c:c*4] print(g[0](6),g[1](7),g[2](8)) # 运行解果 <function <lambda> at 0x0000000002BB8620> 9 12 21 32
12.eval()
函数
- 描述:将字符串
str
当成有效的表达式来求值并返回计算结果。 - 语法:
eval(source[, globals[, locals]]) -> value
source
:一个Python表达式或函数compile()
返回的代码对象globals
:可选。必须是dictionary
locals
:可选。任意映射对象
- 应用代码示例:
#测试eval()函数 s = "print('abcde')" eval(s) a = 10 b = 20 c = eval("a+b") print(c) dict1 = dict(a=100,b=200) d = eval("a+b",dict1) print(d)
13.递归函数
- 递归(recursion)是一种常见的算法思路,在很多算法中都会用
到。比如:深度优先搜索(DFS:Depth First Search)等。 - 递归的基本思想就是“自己调用自己”。
- 每个递归函数必须包含的两部分:
- 终止条件:表示递归什么时候结束。一般用于返回值,不再调用自己。
- 递归步骤:把第n步的值和第n-1步相关联。
- 注意:⚠️递归函数由于会创建大量的函数对象、过量的消耗内存和运算能力。在处理大量数据时,谨慎使用。
- 递归程序示例:
上述递归程序结构图:def my_recursion(n): print("start:" + str(n)) if n == 1: print("recursion over!") else: my_recursion(n - 1) print("end:" + str(n)) my_recursion(3) # 运行结果 start:3 start:2 start:1 recursion over! end:1 end:2 end:3
14.嵌套函数(函数内部定义的函数)
- 代码示例:
上面程序中, inner() 就是定义在 outer() 函数内部的函数。 inner() 的定义和调用都在 outer() 函数内部。def outer(): print('outer running...') def inner(): print('inner running...') inner() # outer running... outer() # inner running...
- 嵌套函数的使用场景:
- 封装 - 数据隐藏,外部无法访问“嵌套函数”。
- 贯彻
DRY
(Don’t Repeat Yourself) 原则。 - 嵌套函数,可以让我们在函数内部避免重复代码。
- 闭包。
15.nonlocal
关键字
- 关系图
nonlocal
: 用来在内部函数中,声明外层的局部变量。
global
: 函数内声明全局变量,然后才使用全局变量。- 代码示例:
#测试nonlocal、global关键字的用法 a = 100 def outer(): b = 10 def inner(): nonlocal b #声明外部函数的局部 变量 print("inner b:",b) b = 20 global a #声明全局变量 a = 1000 inner() print("outer b:",b) outer() print("a:",a) # 运行结果 inner b: 10 outer b: 20 a: 1000
16.LEGB规则
Python在查找“名称”时,是按照LEGB规则查找的:
Local
指的就是函数或者类的方法内部Enclosed
指的是嵌套函数(一个函数包裹另一个函数,闭包)Global
指的是模块中的全局变量Built in
指的是Python为自己保留的特殊名称
总结来说:由内而外依次查找。
如果某个 name
映射在局部 local
命名空间中没有找到,接下来就会
在闭包作用域 enclosed
进行搜索,如果闭包作用域也没有找到,
Python就会到全局 global
命名空间中进行查找,最后会在内建
built-in
命名空间搜索 (如果一个名称在所有命名空间中都没有找
到,就会产生一个 NameError
)
END