day11 函数进阶
目标:掌握函数相关易错点 & 项目开发必备技能。
今日概要:
- 参数的补充
- 函数名到底是什么?
- 返回值和print,傻傻分不清楚。
- 函数的作用域
1.参数的补充
在函数基础部分,我们掌握函数和参数基础知识,掌握这些其实完全就可以进行项目的开发。
今天的补充的内容属于进阶知识,包含:内存地址相关、面试题相关等,在特定情况下也可以让代码更加简洁,提升开发效率。
1.1 参数内存地址相关
查询某个参数的内存地址,可以使用Python内置函数 id():
v1 = "轩小陌"
addr = id(v1)
print(addr)
>>输出结果:
2902636304592
v1 = [11,22,33]
v2 = [11,22,33]
print( id(v1) )
print( id(v2) )
>>输出结果:
2498192294080
2498192285568
v1 = [11,22,33]
v2 = v1
print( id(v1) )
print( id(v2) )
>>输出结果:
1571382902976
1571382902976
记住一句话:函数执行传参时,传递的是内存地址。
v1 = "轩小陌"
print(id(v1))
>>输出结果:140247057684592
def func(data):
print(data, id(data))
func(v1)
>>输出结果:140247057684592
Python参数的这一特性有两个作用:
-
节省内存
-
对于参数为可变类型(列表、字典、集合),可通过函数对参数的内部元素进行修改。
def func(data): data.append(999) v1 = [11,22,33] func(v1) print(v1) >>输出结果:[11,22,33,666]
# 如果在函数中对传入的参数进行了重新赋值,则会生成新的内存地址储存: def func(data): data = ["轩小陌","alex"] v1 = [11,22,33] func(v1) print(v1) >>输出结果:[11,22,33]
# 如果参数为不可变类型(字符串、元组),则无法通过函数修改内部元素,只能重新赋值: def func(data): data = "alex" v1 = "轩小陌" func(v1) print(v1) >>输出结果:轩小陌
-
其他很多编程语言执行函数时,传参时默认会将数据重新拷贝一份,这样会浪费内存。不过,其他语言也可以通过 ref 等关键字来实现传递内存地址。
-
当然,如果你不想让外部的变量和函数内部参数的变量一致,也可以选择将外部值拷贝一份,再传给函数。
import copy
def func(data):
data.append(999)
v1 = [11, 22, 33]
new_v1 = copy.deepcopy(v1)
func(new_v1)
print(v1)
>>输出结果:[11,22,33]
1.2 函数的返回值是内存地址
def func():
data = [11, 22, 33]
return data
v1 = func()
print(v1)
>>输出结果:[11,22,33]
上述代码的执行过程:
- 执行func函数
data = [11, 22, 33]
创建一块内存区域,内部存储[11,22,33],data
变量指向这块内存地址。- return data 返回
data
指向的内存地址 v1
接收返回值,所以v1
和data
都指向[11,22,33]
的内存地址(两个变量指向此内存,引用计数器为2)- 函数执行完毕之后,函数内部的变量都会被释放。因为
data
被释放后,只剩v1
指向该内存地址,所以内存地址的引用计数器变为1。
def func():
data = [11, 22, 33]
return data
v1 = func()
print(v1) # [11,22,33]
v2 = func()
print(v2) # [11,22,33]
上述代码的执行过程:
-
执行func函数(第一次)
-
data = [11, 22, 33]
创建一块内存区域,内部存储[11,22,33],data
变量指向这块内存地址。 -
return data 返回
data
指向的内存地址(假设为1000001110)。 -
v1
接收返回值,所以v1
和data
都指向[11,22,33]
的内存地址(两个变量指向此内存,引用计数器为2)。 -
函数执行完毕之后,函数内部的变量都会被释放,内存地址的引用计数器变为1。因为
data
被释放后,只剩v1
指向该内存地址,所以v1指向地址1000001110。 -
执行func函数(第二次)
-
data = [11, 22, 33]
创建一块内存区域,内部存储[11,22,33],data
变量指向这块内存地址。 -
return data 返回
data
指向的内存地址(假设为11111001110)。 -
v2
接收返回值,所以v2
和data
都指向[11,22,33]
的内存地址(两个变量指向此内存,引用计数器为2)。 -
函数执行完毕之后,函数内部的变量都会被释放,内存地址的引用计数器变为1。因为
data
被释放后,只剩v2
指向该内存地址,所以v1指向地址11111001110。
由此可见,v1
和v2
为两个不同的内存地址。
1.3 参数的默认值【面试题】
def func(a1,a2=18):
print(a1,a2)
func("root")
func("admin",20)
Python在创建函数(未执行)时,如果发现函数的参数中有默认值,则在函数内部会创建一块内存区域并维护这个默认值。
执行函数未传值时,则让a2指向函数一开始维护的默认值的地址。
执行函数传值时,则让a2指向新传入的值的地址。
在特定情况下:
- 默认参数的值是可变类型 list/dict/set
- 函数内部对默认参数进行了修改
后续在调用该函数并传入参数时,会存在特殊情况,需要特别注意:
-
案例1:
# 在函数内存中会维护一块区域存储 [1,2],假设内存地址为: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 -> 新地址:1111111101 func(99,[77,88]) >>输出结果:99 [77,88,666] # a1 = 300 # a2 -> 100010001 func(300) >>输出结果:300 [1,2,666,666,666]
-
案例2:
# 在函数内部会维护一块区域存储[1, 2],假设内存地址为:1010101010 def func(a1, a2=[1, 2]): a2.append(a1) return a2 # a1 = 10 # a2 -> 1010101010 # v1 -> 1010101010 v1 = func(10) print(v1) >>输出结果:[1, 2, 10] # a1 = 20 # a2 -> 1010101010 # v2 -> 1010101010 v2 = func(20) print(v2) >>输出结果:[1, 2, 10, 20] # a1 = 30 # a2 -> 新地址:11111111111 # v3 -> 新地址:11111111111 v3 = func(30, [11, 22]) print(v3) >>输出结果:[11, 22,30] # a1 = 40 # a2 -> 1010101010 # v4 -> 1010101010 v4 = func(40) print(v4) >>输出结果:[1, 2, 10, 20, 40]
-
案例3:
# 在函数内部会维护一块区域存储[1, 2],假设内存地址为:1010101010 def func(a1, a2=[1, 2]): a2.append(a1) return a2 # a1=10 # a2 -> 1010101010 # v1 -> 1010101010 v1 = func(10) # a1=20 # a2 -> 1010101010 # v2 -> 1010101010 v2 = func(20) # a1=30 # a2 -> 新地址:11111111111 # v3 -> 新地址:11111111111 v3 = func(30, [11, 22]) # a1=40 # a2 -> 1010101010 # v4 -> 1010101010 v4 = func(40) print(v1) print(v2) print(v3) print(v4) >>输出结果: [1, 2, 10, 20, 40] [1, 2, 10, 20, 40] [11,22,30] [1, 2, 10, 20, 40]
小结::
-
对于函数中的默认参数为可变类型(列表、字典、集合),且函数中对默认参数进行了修改操作的情况,后续调入函数时,如果没有传入默认参数的值,则每调用一次函数,默认参数就会修改一次。如果重新赋值了默认参数,则会按传入的新值进行操作。
-
总之,不管参数怎么变,只要记住函数执行传参时,传递的是内存地址,根据这个原则去逐步分析,就不会出错。
1.4 动态参数
动态参数,定义函数时在形参位置用 *args或**kwargs
可以接收任意个参数。
def func(*args,**kwargs):
print(args,kwargs)
func("宝强","杰伦",n1="alex",n2="eric")
除了在定义函数时可以用 *args和**kwargs
,其实在执行函数时,也可以用:
-
形参固定,实参用
*args和**kwargs
def func(a1,a2): print(a1,a2) func( 11, 22 ) func(a1=11, a2=22) func(*[11,22]) func(**{ "a1":11,"a2":22}) >>以上输出结果均为: 11,22
-
形参用
*args和**kwargs
,实参也用*args和**kwargs
def func(*args,**kwargs): print(args,kwargs) func(11, 22) # 只通过位置传参,会默认传入*args的空元组()中 >>输出结果:(11,22) { } func(11, 22, name="轩小陌", age=18) # 通过位置和关键字传参,前半部分传入*args的空元组()中,后半部分传入**kwargs的空字典{}中 >>输出结果:(11,22) { name:'轩小陌',age:18} func(*[11,22,33], **{ "k1":1,"k2":2}) # 通过*和**传参,*后面的参数传入*args的空元组()中,**后面的参数传入**kwargs的空字典{}中 >>输出结果:(11,22,33) { "k1":1,"k2":2} # 注意:按照这个方式将数据传递给args和kwargs时,数据是会重新拷贝一份的(可理解为内部循环每个元素并设置到args和kwargs中)。
因此,在使用format字符串格式化时,可以有更多的方式:
之前的写法: v1 = "我是{},年龄:{}。".format("轩小陌",18) v2 = "我是{name},年龄:{age}。".format(name="轩小陌",age=18) 新增的写法: v3 = "我是{},年龄:{}。".format(*["轩小陌",18]) v4 = "我是{name},年龄:{age}。".format(**{ "name":"轩小陌","age":18})
练习题:
-
看代码写结果
def func(*args,**kwargs): print(args,kwargs) params = { "k1":"v2","k2