动态参数
- *args
1 | def func(*args): |
- **kwargs
1 | def func(**kwargs): |
- *args,**kwargs
1 | def func(*args,**kwargs): |
提示:是否还记得字符串格式化时的format功能
1 | v1 = "我叫{},今年{}".format("蒙珣",18) |
参数内存地址相关
如果想查看某个值在内存中的地址
1 | "蒙珣" v1 = |
1 | 11,22,33] v1 = [ |
1 | 11,22,33] v1 = [ |
记住一句话:函数执行传参时,传递的是内存地址
即:python传参默认传递的是内存地址
Python参数的这一特性有两个好处:
- 节省内存
- 对于可变类型且函数中修改元素的内容,所有的地方都会修改。可变类型:列表、字典、集合。
1 | # 可变类型 & 修改内部 |
1 | # 特殊情况:可变类型 & 重新赋值 |
1 | # 特殊情况:不可变类型,无法修改内部元素,只能重新赋值 |
函数的返回值是内存地址
其他很多编程语言执行函数时,默认传参时会将数据重新拷贝一份,会浪费内存
当然,如果你不想让外部的变量和函数内部参数的变量一致,也可以选择将外部值拷贝一份,再传给函数。
1 | import copy |
函数内存地址引用的变化
1 | def func(): |
上述代码的执行过程:
- 执行func函数
data = [11,22,33]
创建一块内存区域,内部存储 [11, 22, 33],data变量指向这块内存地址 10000001110。return data
返回data指向的内存地址- v1接收返回值,所以v1和data都指向
[11,22,33]
的内存地址(两个变量质量此内存,引用计数器为2) - 由函数执行完毕后,函数内部的变量都会被释放。(即:删除data变量,内存地址的引用计数器-1)
所以,最终v1只想的函数内部创建的那块内存地址。(v1指向的1000001110内存地址)
- 执行func函数
data = [11,22,33]
创建一块内存区域,内部存储 [11, 22, 33],data变量指向这块内存地址 11111001110。return data
返回data指向的内存地址- v2接收返回值,所以v1和data都指向
[11,22,33]
的内存地址(两个变量质量此内存,引用计数器为2) - 由函数执行完毕后,函数内部的变量都会被释放。(即:删除data变量,内存地址的引用计数器-1)
所以,最终v1指向的函数内部创建的那块内存地址。(v1指向的 11111001110)
参数的默认值【面试题】
这个知识点在面试题中出现的概率比较高,但真正实际开发中用的比较少
1 | def func(a1,a2=18): |
原理:Python在创建函数(未执行)时,如果发现函数的参数中有默认值,则在函数内部会创建一块区域并维护这个值
执行函数未传值时,则让a2指向函数维护的那个值的地址
1 func("root")执行函数传值时,则让a2指向新传入的值的地址
1 func("admin",20)
在特定情况【默认参数的值是可变类型】&【函数内部会修改这个值】
坑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# 在函数内存中会维护一块区域存储 [1,2,666] 100010001
def func(a1,a2=[1,2]):
a2.append(666)
print(a1,a2)
# a1 = 100
# a2 -> 100010001
func(100) # 100 [1,2,666]
# a1 = 200
# a2 -> 100010001
func(200) # 200 [1,2,666,666]
# a1 = 99
# a2 -> 111111101
func(99,[77,99]) # 99 [77,99,666]
# a1 = 300
# a2 -> 100010001
func(300) # 300 [1,2,666,666,666]大坑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16# 在内部会维护一块区域存储[1,2],内存地址 1010101010
def func(a1,a2=[1,2]):
a2.append(a1)
return a2
v1 = func(10)
print(v1) # [1,2,10]
v2 = func(20)
print(v2) # [1,2,10,20]
v3 = func(30,[11,22])
print(v3) # [11,22,30]
v4 = func(40)
print(v4) # [1,2,10,20,40]深坑
1
2
3
4
5
6
7
8
9
10
11
12
13def func(a1,a2=[1,2]):
a2.append(al)
return a2
v1 = func(10)
v2 = func(20)
v3 = func(30,[11,22])
v4 = func(40)
print(v1) # [1,2,10,20,40]
print(v2) # [1,2,10,20,40]
print(v3) # [11,22,30]
print(v4) # [1,2,10,20,40]V1,V2,V4用的都是同一块内存
动态参数补充
实参使用*,即在执行函数时也可以用*
形参固定,实参用
*和**
1
2
3
4
5
6
7
8def func(a1,a2):
print(a1,a2)
func(11,22) # 11 22
func(a1=1,a2=2) # 1 2
func(*[11,22]) # 11 22
func(**{"a1":11,"a2":22}) # 11 22形参用
*和**
,实参也用*和**
1
2
3
4
5
6
7
8
9
10def func(*args,**kwargs):
print(args,kwargs)
func(11,22)
func(11,22,name="蒙珣",age=18)
func([11,22,33],{"k1":1,"k2":2}) # ([11, 22, 33], {'k1': 1, 'k2': 2}) {}
func(*[11,22,33],**{"k1":1,"k2":2}) # (11, 22, 33) {'k1': 1, 'k2': 2}
# 值得注意:按照这个方式将数据传给args和kwargs时,数据是会重新拷贝的(可以理解为内部循环每个元素并设置到args和kwargs中)
所以,在使用format字符串格式化时,可以这样使用:
1 | v1 = "我是{},年龄:{}".format("蒙珣",18) |
函数做元素
1 | def func(): |
注意:函数同时也可以被哈希,所以函数名通常也可以当做集合的元素、字典的键
掌握这个知识后,对后续的项目开发有很大的帮助,例如,在项目中遇到根据选择不同操作时:
情景1,例如:开发一个类似微信的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31def send_message():
"""发送信息"""
pass
def send_image():
"""发送图片"""
pass
def send_emoji():
"""发送表情"""
pass
def send_file():
"""发送文件"""
pass
print("欢迎使用xx系统")
print("请选择:1.发送消息;2.发送图片;3.发送表情;4.发送文件")
choice = input("输入选择的序号")
if choice == "1":
send_message()
elif choice == "2":
send_image()
elif choice == "3":
send_emoji()
elif choice == "4":
send_file()
else:
print("输入错误")
其实这种大量使用 if 判断不是一个很好的方式,我们可以将其放入字典中再使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33def send_message():
"""发送信息"""
pass
def send_image():
"""发送图片"""
pass
def send_emoji():
"""发送表情"""
pass
def send_file():
"""发送文件"""
pass
function_dict = {
'1': send_message,
'2': send_image,
'3': send_emoji,
'4': send_file
}
print("欢迎使用xx系统")
print("请选择:1.发送消息;2.发送图片;3.发送表情;4.发送文件")
choice = input("输入选择的序号")
func = function_dict.get(choice)
if not func:
print("输入错误")
else:
# 执行函数
func()
上述情况,在参数相同时才可用,如果参数不一致,会出错。
情景2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36def send_message(phone,content):
"""发送信息"""
pass
def send_image(img_path,content):
"""发送图片"""
pass
def send_emoji(emoji):
"""发送表情"""
pass
def send_file(path):
"""发送文件"""
pass
# 函数名和参数
function_dict = {
"1":[send_message,['1513121255089','你好']],
"2":[send_image,['xxx/xxx/xx.png','消息内容']],
"3":[send_emoji,["😊"]],
"4":[send_file,['xx.zip']]
}
print("欢迎使用xx系统")
print("请选择:1.发送消息;2.发送图片;3.发送表情;4.发送文件")
choice = input("输入选择的序号")
item = function_dict.get(choice)
if not item:
print("输入错误")
else:
# 执行函数
func = item[0]
param_list = item[1]
func(*param_list)情景2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22def send_msg(mobile,content):
"""发送信息"""
pass
def send_email(to_email,subject,content):
"""发送图片"""
pass
def send_wechat(user_id,content):
"""发送微信"""
pass
func_list = [
{"name": send_msg, "params": {'mobile': "17610837191", "content":"您有新短息"}},
{"name": send_email, "params": {'to_emial': "mengxu@117.com", "subject": "报警消息", "content": "硬盘容量不够用了"}},
{"name": send_wechat, "params": {'user_id': 1, 'content': "出去玩吗"}}
]
for item in func_list:
func = item['name']
param_dict = item['params']
func(**param_dict)
函数嵌套
上述示例中的函数均定义在全局作用域,其实函数也可以定义在局部作用域,这样函数呗局部作用域和其子作用域中调用(函数的嵌套)
1 | def func(): |
到现在你会发现,只要理解数据定义时所存在的作用域,并根据从上到下代码执行过程进行分析,再怎么嵌套都可以搞定
现在的你可能有疑问:为什么要这么嵌套定义?把函数都定义在全局不好吗?
其实,大多数情况下我们都会将函数定义在全局,不会嵌套着定义函数。不过,当我们定义一个函数去实现某功能,想要将内部功能拆分成N个函数,又担心这个N个函数放在全局会与其他函数名冲突时(尤其多人协同开发)可以选择使用函数的嵌套
1 | def f1(): |
1 | def func(): |
![image-20230227222215296](/Users/william/Library/Application Support/typora-user-images/image-20230227222215296.png)
![image-20230227222321268](/Users/william/Library/Application Support/typora-user-images/image-20230227222321268.png)
![image-20230227222345169](/Users/william/Library/Application Support/typora-user-images/image-20230227222345169.png)
嵌套引发作用域问题
函数作用域分析1
1 | name = "蒙珣" |
函数在执行过程中,先创建了一个全局作用域
name = 蒙珣
,然后创建了一个全局作用域run函数
。之后在我们调用
函数run()
时,该函数在内部声明了一个局部作用域name = mengxun
,而后又声明了一个局部作用域的函数为inner函数
。在函数内部中,他自己调用
inner()
,先从函数内部寻找,是否有inner
函数。如果有则调用内部函数,如果没有则调用外部函数。最后执行 inner函数,结果为
mengxun
函数作用域分析2
1 | name = "蒙珣" |
- 函数在执行过程中,先创建了一个全局作用域
name = 蒙珣
,然后创建了一个全局作用域run函数
- 之后我们调用
函数run()
,并将其声明成全局变量 v1 - 此时,
函数run()
内部,开始声明局部变量name = mengxun
,并返回局部函数inner()
的内存地址 - 接着,我们调用
全局变量v1
,于是开始执行run函数
内部的inner函数
inner函数
先从内部找name
变量,如果内部没有name
变量,就开始在上一级函数中寻找name
变量,并使用。- 最后打印 mengxun
- 声明全局变量v2,同上。不同的是,v2是创建的不同于v1的另一个调用栈。
函数作用域分析3
1 | name = "蒙珣" |
三句话搞定作用域:
- 优先在自己的作用域找,自己没有就去上级作用域
- 在作用域中寻找值时,要确保此次此刻值是什么
- 分析函数的执行,并确定函数作用域链(函数嵌套)
![image-20230227224253973](/Users/william/Library/Application Support/typora-user-images/image-20230227224253973.png)