针对前一篇文档《Python 函数中使用默认值参数 — 谈谈可变对象的坑?!》再进行补充说明
在 python 中进行函数调用,其参数可以使用 “默认值参数” 类型,那么当为 “默认值” 参数传入一个 “对象” 的时候,有什么限制和要求么?
参数的默认值可以是:
- 可变对象,如列表、字典、集合
- 不可变对象,如整数、浮点数、字符串和元组
一. 参数默认值使用可变对象
1. 调用时不传实参
函数定义中,形参被赋予了一个默认值,且该默认值为一个可变对象(空白的列表)
- id(): 该方法可以输出对象的唯一标识,用于区分是否为同一个对象
- 连续调用该函数两次
- 观察:函数内部对局部变量 lt 的处理
# 定义一个函数,其形参 lt 是一个 “可变对象(列表)”
def fun(lt=[]):
print(f'函数内:lt = {lt}, 指向对象:id(lt)={id(lt)}')
# 连续两次调用函数,且均不传参
fun()
fun()
# 输出为:对象id相同,证明二者为同一个对象
函数内:lt = [], 指向对象:id(lt)=2333811036544
函数内:lt = [], 指向对象:id(lt)=2333811036544
结合图形分析:
- 第一次调用,在函数内部为局部变量(即形参) lt 创建了一个对象,编号为2333811036544
- 第二次调用,由于不需要额外的处理活动,因此在函数内部复用了前面创建的 lt 对象,不再创建一个新的对象
- 原因:二者id编号相同
2. 调用时传入实参
- 函数定义中,形参被赋予了一个默认值,且该默认值为一个可变对象(列表)
- 在函数外部定义了两个列表对象 lst01 和 lst02,调用函数时分别将这两个变量传入
- 观察:函数内部对局部变量 lt 的处理,是新建还是复用?
# 定义一个函数,形参lt使用了 “可变对象(列表)”
def fun(lt=[]):
print(f'函数内对象:lt = {lt}, 指向对象:id(lt)={id(lt)}')
lst01 = [6, 6, 6]
print(f'函数外对象:lst01 ,指向对象:id(lst01)={id(lst01)}')
lst02 = [8, 8, 8]
print(f'函数外对象:lst02 ,指向对象:id(lst02)={id(lst02)}')
# 连续两次调用函数,且传入不同的实参
fun(lst01)
fun(lst02)
# 输出为:
函数外对象:lst01 ,指向对象:id(lst01)=2379271796992
函数外对象:lst02 ,指向对象:id(lst02)=2379271892928
函数内对象:lt = [6, 6, 6], 指向对象:id(lt)=2379271796992
函数内对象:lt = [8, 8, 8], 指向对象:id(lt)=2379271892928
结合图形分析:
- 在函数外定义了两个列表对象 lst01 和 lst02,从输出可见二者的对象编号不相等,即不是相同的对象
- 第一次调用,传入了 lst01 对象(的引用),其编号为2379271796992
- 在函数中创建局部变量(即形参 lt),使其指向 lst01 变量所指向的对象空间,即这两个变量(lt 和 lst01)都指向了同一个变量空间
- 此时通过 lt 进行修改的时候,实际和通过 lst01 进行修改的效果是相同的
- 第二次调用与第一次调用同理,但是由于形参要接收新的传值,因此就不会复用了,而是创建一个新的局部变量(也叫 lt),使其指向 lst02 变量所指向的对象空间
- 从程序输出结果看,两个对象的 id 不同,确实分别指向了两个不同的对象
3. 先传参调用再不传参调用
- 先进行传参的调用,再进行不传参的调用
- 观察:函数内部对局部变量 lt 的处理,是新建还是复用?
# 定义一个函数,形参lt使用了 “可变对象(列表)”
def fun(lt=[]):
print(f'函数内对象:lt = {lt}, 指向对象:id(lt)={id(lt)}')
lst01 = [6, 6, 6]
print(f'函数外对象:lst01 ,指向对象:id(lst01)={id(lst01)}')
lst02 = [8, 8, 8]
print(f'函数外对象:lst02 ,指向对象:id(lst02)={id(lst02)}')
# 调用:先传参
fun(lst01)
fun(lst02)
# 调用:再不传参,相对前面案例仅增加了这一行代码
fun()
# 输出为:
函数外对象:lst01 ,指向对象:id(lst01)=2844358899968
函数外对象:lst02 ,指向对象:id(lst02)=2844358995904
函数内对象:lt = [6, 6, 6], 指向对象:id(lt)=2844358899968
函数内对象:lt = [8, 8, 8], 指向对象:id(lt)=2844358995904
函数内对象:lt = [], 指向对象:id(lt)=2844358103424 # 输出了一个新的对象地址
结合图形分析:
- 前两次调用,都传参了,在函数内分别创建了两个局部变量(同名的 lt),且分别指向新传入的对象(引用)(2844358899968、2844358995904)
- 第三次调用,没有传参,在函数内创建的局部变量 lt 指向了一个新的列表对象 (2844358103424)(是一个空的列表对象)
4. 案例分析
需求描述:定义一个函数,为传入的列表(list)尾部添加一个 “end” 元素。
如传入列表: [ 1, 2, 3, 4 ], 期望输出列表为:[ 1, 2, 3, 4, 'end' ]
4.1. 实现代码
- 创建函数,参数使用可变对象(列表),而且设置了默认值(空列表)
- 调用函数,传入准备好的实参(lst1)
- 执行结果:在原列表基础上增加了 “end” 列表项,符合需求
def add_end(lt=[]):
# 完成需求逻辑,为传入的列表增加 “end” 元素
lt.append('end')
return lt
lst1 = [1, 2, 3, 4]
print(add_end(lst1))
lst2 = [5, 6, 7, 8]
print(add_end(lst2))
# 结果输出:符合需求
[1, 2, 3, 4, 'end']
[5, 6, 7, 8, 'end']
4.2. 调整代码后的疑问
- 在后面增加两次不传参的调用(由于使用了默认参数,因此调用的时候可以不传参)
- 观察结果:不传参调用后,好像原有列表内容被 “清空”,而且最后一次向尾部添加了两个 “end” 元素。为什么???
def add_end(lt=[]):
lt.append('end')
return lt
lst1 = [1, 2, 3, 4]
print(add_end(lst1))
lst2 = [5, 6, 7, 8]
print(add_end(lst2))
# 增加两次不传参调用
print(add_end())
print(add_end())
# 结果输出:
[1, 2, 3, 4, 'end']
[5, 6, 7, 8, 'end']
['end']
['end', 'end']
结合图形分析:
- 前两次传参调用,每次都创建一个新的局部变量(lt),并分别指向被传入的对象,
- 局部变量(lt)和函数外部变量(lst01)都指向了同一个对象空间,所以通过 lt 修改后,该空间的内容变化增加了一个 “end” 元素
- 第三次无参调用,又创建了一个新的局部变量(lt),指向了一个空白的对象空间,修改后,该空间的内容变化增加了一个 “end” 元素
- 第四次无参调用,复用局部变量(lt),因此修改后,最终出现了两个 “end” 元素