
昨天Tyree写完第15课的作业,得意得很。他把猜数字游戏用函数改得整整齐齐,还跑来给我演示:“妈妈你看,我现在想加一个新功能,只需要改一个函数就行,不用到处翻代码了。”
我说:“那你知道函数还能更灵活吗?比如你写一个打招呼的函数,有时候想叫‘朋友’,有时候想叫‘同学’,有时候想叫全名,怎么办?”
他说:“那就多写几个函数呗,
greet_friend()、greet_classmate()……”
我打断他:“其实不用那么麻烦。你可以给参数设默认值,不传的时候它自动用默认的,传了就用你传的。”
他试了试,觉得挺好。然后他又问:“那如果我写一个求和函数,有时候传2个数,有时候传100个数,参数个数不确定怎么办?”
我说:“那就用*args,它能把所有传进来的数打包成一个元组,你想加几个加几个。”
他很高兴:“这个好!那我就可以写一个万能计算器了。”
今天我们就来学这些让函数“更聪明”的写法:默认参数、关键字参数、可变参数*args和**kwargs。
学会了你的函数就能适应各种奇怪的需求,想怎么调就怎么调。
01.
默认参数:给参数一个“保底值”
我们先写一个最简单的打招呼函数:

这个函数要求必须传一个name进去。如果你忘了传,比如写greet(),Python就会翻脸,报错说缺少一个参数。
但有时候我们想让这个函数“聪明”一点:如果传了名字,就用传的名字;如果没传,就默认叫“朋友”。
这就用到了默认参数。写法很简单,在定义参数的时候用等号给它一个默认值:

greet() # 不传参数,输出:你好,朋友
greet("Tyree") # 传参数,输出:你好,Tyree
注意,name="朋友"这里的等号不是赋值,而是在告诉Python:“如果你没收到这个参数,就用‘朋友’顶上。”这是一个保底值。
有一个很容易踩的坑:带默认值的参数必须放在后面。比如def func(a, b=10):是正确的,但def func(a=10, b):就会报错。
为什么呢?因为Python匹配参数是按顺序来的,如果把默认参数放在前面,后面的普通参数就永远收不到值了。
Tyree刚开始就写反了,报错后他记住了:“默认参数往后站。”
02.
关键字参数:调用时指名道姓
正常情况下,我们调用函数是按位置传参的:

info("Tyree", 12, "茂名") # 按顺序:name, age, city
这种方式叫“位置参数”。顺序必须跟函数定义一致,一旦写错了,比如写成
info(12, "Tyree", "茂名"),程序不会报错,但会把12当成名字,“Tyree”当成年龄,逻辑全乱。
Python提供了一种更安全的方式:关键字参数。调用的时候直接写上参数名,顺序可以随便调换:
info(city="茂名", name="Tyree", age=12) # 顺序乱写也没事

这样写,每个值都明确告诉Python要传给哪个参数,既不用记顺序,代码读起来也清楚。
Tyree后来写了一个注册函数,有七八个参数,他全部用关键字参数调用,说:“这样我自己不会搞混,别人看我的代码也一眼就懂。”
有一个小规则:关键字参数必须放在位置参数的后面。
比如info("Tyree", city="茂名", age=12)是可以的,但info(name="Tyree", 12, "茂名")就会报错。
03.
可变参数 *args:接受任意多个位置参数
假如你想写一个求和函数,但用户可能传2个数,也可能传10个数——参数个数不确定,怎么办?难道要写10个版本吗?
不用。Python有一个写法叫*args,它会把所有多余的位置参数打包成一个元组(你可以把它当成一个列表来用):

这里的星号是关键。
它告诉Python:“把调用时传进来的所有位置参数,不管有多少个,统统收集起来,放进一个叫args的元组里。”
名字args可以随便取,但星号必须写。
Tyree第一次看到这个写法,问我:“为什么前面要加个星号?”
我让他试一下,如果写成
def sum_all(args):,调用sum_all(1,2,3)只会把(1,2,3)这个元组整体当成一个参数,函数体里循环一次就把整个元组当作一个数来处理,不会分别加。
他试了之后说:“哦,星号的意思是‘拆开装’。”没错,星号相当于一个收集袋,把散装的东西装到一起。
需要注意的是,*args只能接收不带名字的参数。
如果你写sum_all(a=1, b=2),它会报错,因为带名字的(关键字参数)不会被*args收走。
04.
可变参数 **kwargs:接受任意多个关键字参数
*args处理的是没有名字的参数。那如果用户传进来的是name="Tyree"这种带名字的呢?用**kwargs。

两个星号的作用是:把所有传入的关键字参数打包成一个字典。字典的key是参数名,value是参数值。你可以用.items()遍历它们。
items() 是字典自带的一个方法,它的作用是:把字典里的每一对“键-值”打包成一个元组,然后返回一个可以循环的视图。
Tyere问我:“为什么一个星号和两个星号不一样?”
我打了个比方:一个星号是把东西“排成一排”放进元组(像排队),两个星号是“贴上标签”放进字典(像快递柜,有取件码)。他听完后说:“我记住了,星号越多,结构越复杂。”
**kwargs必须放在所有参数的最后面。
05.
综合使用:普通参数 + 默认参数 + *args + **kwargs
这些参数可以一起用,但顺序是固定的:
普通参数 → 默认参数 → *args → **kwargs。这个顺序不能乱,否则Python不知道哪个参数该归谁。
看下图代码

输出:
a= 1
b= 2
args= (3, 4, 5)
kwargs= {'name': 'Tyree', 'age': 12}
我们一步步看Python是怎么分配这些参数的:
第一个位置参数1,给了a。
第二个位置参数2,给了b(覆盖了默认值10)。
剩下的位置参数3,4,5,没有对应的普通参数了,所以被*args收走,变成元组(3,4,5)。
剩下的关键字参数
name="Tyree", age=12,没有对应的普通参数,被**kwargs收走,变成字典。
Tyree看了这个输出后说:“原来参数是这样一层层‘分配’的,先满足普通参数,再喂给*args,最后喂给**kwargs。”
他试着把b的默认值去掉,然后只传一个参数,结果报错了——因为b没有收到值。他总结:“顺序和数量都不能乱。”
06.
小项目:用可变参数写一个“成绩计算器”
下面我们来写一个函数,接收任意多个成绩,返回总分、平均分、最高分、最低分。
代码看下图示:

这里有一个容易被忽略的地方:如果调用时一个成绩都没传
(比如analyze_scores()),scores就是一个空元组。这时候直接计算total / len(scores)就会触发ZeroDivisionError(除以零错误)。
所以函数开头要先判断一下:
if not scores:,意思是“如果scores是空的”,就直接返回四个0。
Tyree一开始没加这个判断,程序崩溃了,他说:“原来函数也要考虑‘没数据’的情况。”这就是所谓的“边界条件”——程序在正常情况和极端情况下都要能跑。
另外,return后面跟了四个值,Python会自动把它们打包成一个元组。
调用方可以像第一段代码那样用一个变量接住整个元组,然后通过索引([0]、[1])取出来;也可以像第二段代码那样用多个变量直接解包。Tyree更喜欢解包的方式,觉得更直观。
08.
今天学到了什么?
关键字参数func(参数名=值),传参时指名道姓,顺序随便。
可变位置参数def func(*args):,收集所有多余的位置参数,打包成元组
可变关键字参数def func(**kwargs):,收集所有多余的关键字参数,打包成字典。
参数顺序:普通 → 默认 → *args → **kwargs 顺序乱了Python就不认得路了。
kwargs全称是 Keyword Arguments(关键字参数)。
kw = Keyword(关键字,就是参数名)
args = Arguments(参数)
合起来 kwargs 就是“关键字参数们”。这个命名习惯来自Python社区,就像 *args 代表“位置参数们”一样。
好了今天课程就到这里,明天第17课:变量的作用域(局部与全局)。我们会学为什么函数内部的变量改不了外面的,以及怎么用global声明。
Tyree已经碰到过这个问题了——他试过在函数里直接修改外面的变量,结果没成功,明天正好给他讲清楚。
————热门推荐————
自学编程第15课:函数——把代码打包成“小工具”(def、参数、返回值)可反复调用
自学编程第7课:turtle画图入门(画一个正方形,五角形,螺旋形,三角形)
自学编程第2课:用input让电脑问你名字(做一个打招呼程序)
自学编程第一步:安装Python和Thonny(零基础图文教程)
(本系列教程每天更新,欢迎关注收藏)