前言Python 的函数参数比很多语言灵活既支持按位置传也支持按关键字传既能接固定数量的参数也能接任意数量的参数。灵活带来的代价是规则细细节记错就会写出「本地能跑、换个调用方式就炸」的代码。最常见的三个误解以为默认参数每次调用都会重新创建。实际上默认值在def执行的那一刻就求值并绑定好了所以可变的默认值会被所有调用共享。以为*args拿到的就是「列表」。它拿到的是元组tuple**kwargs拿到的是字典dict。以为 Python 是「值传递」或「引用传递」之一。准确的说法是对象引用传递call by object reference也叫「按共享传递」函数拿到的是对象引用的副本改不改得到调用方取决于你操作的是不可变对象还是可变对象。本文把四类参数、参数顺序规则、打包与解包、以及默认参数和可变参数上的坑一次讲清楚。一、四类参数与书写顺序一个完整的函数签名可以同时包含下面几部分顺序是固定的def 函数名(位置参数..., 带默认值的位置参数..., *可变位置参数, 仅关键字参数..., **可变关键字参数)# 适用于 Python 3.8def func(a, b2, *args, c, d4, **kwargs):print(a , a)print(b , b)print(args , args, type(args).__name__)print(c , c)print(d , d)print(kwargs , kwargs)func(1, 3, 5, 6, c7, x8, y9)a 1b 3args (5, 6) tuplec 7d 4kwargs {x: 8, y: 9}几个要点*args把多出来的位置实参收成一个元组**kwargs把多出来的关键字实参收成字典。args/kwargs只是习惯名字起作用的是*和**。*args之后的参数如上面的c变成仅关键字参数调用时必须写成c7。单独一个*可以不收集任何东西只用来把后面的参数强制变成仅关键字def f(a, *, b)。这个语法从Python 3.0就有。仅位置参数用/表示需要 Python 3.8 及以上def f(a, b, /, c)里a、b只能按位置传。不能在有默认值的参数后面再放没有默认值的参数def f(a1, b)是SyntaxError。二、默认参数只求值一次这是默认参数上最具「迷惑性」的行为。默认值表达式在函数定义时求值一次之后每次调用复用同一个对象。# 适用于 Python 3.8def append_item(item, box[]):box.append(item)return boxprint(append_item(1)) # [1]print(append_item(2)) # [1, 2] —— 不是 [2]正确写法是把默认值设成None在函数体内按需创建# 适用于 Python 3.8def append_item(item, boxNone):if box is None:box []box.append(item)return boxprint(append_item(1)) # [1]print(append_item(2)) # [2]注意判断条件是is None而不是if not box——后者在调用方显式传入一个空列表时也会走「新建」分支把调用方的意图覆盖掉。三、参数传递改不改得动调用方函数拿到的是对象引用的副本。所以结论很简单在函数内重新赋值x ...只影响函数内的名字不影响调用方。在函数内原地修改可变对象x.append(...)、x[0] ...调用方会看到变化。# 适用于 Python 3.8def rebind(x):x [9, 9, 9] # 只是让局部名字指向新对象def mutate(x):x.append(9) # 原地改调用方看得见data [1, 2, 3]rebind(data)print(data) # [1, 2, 3]mutate(data)print(data) # [1, 2, 3, 9]不可变对象int、str、tuple、frozenset根本没法原地改所以「传进去改一改」这类写法对它们一定无效。四、调用时的解包*和**在调用时也有用作用是把一个可迭代对象/字典拆成多个实参# 适用于 Python 3.8def show(a, b, c):print(a, b, c)nums [1, 2, 3]show(*nums) # 1 2 3opts {a: 1, b: 2, c: 3}show(**opts) # 1 2 3Python 3.5 及以上还允许在一次调用里写多个解包# 适用于 Python 3.5def total(*nums):return sum(nums)first, second [1, 2], [3, 4]print(total(*first, *second)) # 10五、完整示例一个灵活的日志函数# 适用于 Python 3.8def log(message, levelINFO, *tags, sep-, upperFalse, **extra):level 有默认值tags 收集多余位置实参sep、upper 是仅关键字参数extra 收集多余关键字实参。if upper:message message.upper()tag_part sep.join(tags) if tags else noneparts [f[{level}], message, ftags{tag_part}]for key in sorted(extra):parts.append(f{key}{extra[key]})return .join(parts)if __name__ __main__:print(log(started))print(log(started, WARN, db, cache, upperTrue))print(log(done, ERROR, userann, code500, sep))[INFO] started tagsnone[WARN] STARTED tagsdb-cache[ERROR] done tagsnone code500 userann注意log(done, ERROR, userann, code500, sep)这一句user、code、sep都是关键字所以正确落到了各自的参数上不会误进*tags。六、用 inspect 读参数签名不确定一个函数到底接受哪些参数时不要靠猜用标准库inspect直接读出来。inspect.signature()返回一个Signature对象每个参数还有kind用来说明它属于哪一类——POSITIONAL_ONLY、POSITIONAL_OR_KEYWORD、VAR_POSITIONAL、KEYWORD_ONLY、VAR_KEYWORD# 适用于 Python 3.8import inspectdef demo(a, b2, *args, c, **kwargs):passsig inspect.signature(demo)for name, param in sig.parameters.items():print(name, param.kind.name, param.default)a POSITIONAL_OR_KEYWORD class inspect._emptyb POSITIONAL_OR_KEYWORD 2args VAR_POSITIONAL class inspect._emptyc KEYWORD_ONLY class inspect._emptykwargs VAR_KEYWORD class inspect._empty没有默认值的参数param.default会是哨兵对象inspect.Parameter.empty上面打印出来就是那串class inspect._empty。这对读第三方库的签名、写自己的参数校验很有用比看文档猜靠谱得多。常见坑点可变默认参数❌def f(item, box[])多次调用共享同一个列表 ✅def f(item, boxNone)函数体内if box is None: box []默认值是函数调用❌def f(ttime.time())只在定义时取一次不刷新 ✅def f(tNone)函数体内t t if t is not None else time.time()误以为*args是列表❌args.append(1)会报AttributeError元组没有append✅ 需要列表就args list(args)只读时直接按元组用在*args后面写普通参数❌def f(*args, b)却调用f(1, 2, 3)b缺少值 ✅*args之后的参数是仅关键字必须写f(1, 2, 3, b4)用not box判断是否传了参数❌if not box: box []调用方传空列表会被悄悄替换 ✅if box is None: box []只区分「没传」这一种情况以为改函数内参数名会影响调用方❌ 在函数里x x [1]就指望调用方的列表被改 ✅ 要影响调用方就原地改x [1]对列表是原地否则把结果return出去混淆mysort(l, key)与mysort(l, keykey)❌f(data, len)以为传给了key实际落到了下一个位置参数 ✅ 明确写关键字f(data, keylen)旧式元组参数解包❌def f((a, b)):这种 Python 2 的写法在 Python 3 里是语法错误 ✅def f(pair):函数体内a, b pair总结参数类型写法传参方式收到的类型位置或关键字a位置或a1原对象带默认值a1可省略原对象可变位置*args多余位置实参tuple仅关键字*之后如def f(a, *, b)必须b...原对象仅位置/之前3.8只能按位置原对象可变关键字**kwargs多余关键字实参dict记两条主线就够了默认值在定义时求值一次所以永远不要把可变对象直接当默认值参数传递传的是对象引用能不能改到调用方取决于操作的是可变对象还是不可变对象。把这两条记牢参数相关的诡异现象基本都能自己解释清楚。