参数详解:默认参数、可变参数、关键字参数

上一篇 函数定义与调用 我们讲了函数的基本形态,但参数才是函数的”接口”,也是 Python 最灵活的地方。Python 函数参数系统能玩出比 C/Java 多得多的花样:默认值、可变参数、关键字参数、位置参数、*/* 特殊分隔符……这一篇要把它们全串起来。学完之后你就能读懂 def open(file, mode='r', buffering=-1, encoding=None, ...) 这种复杂签名了。

一、位置参数

最基本的形式:按位置传参

1
2
3
4
def add(a, b):
return a + b

add(3, 5) # 8 按位置

二、关键字参数

调用时可以显式写出参数名,顺序可以变:

1
2
3
4
5
def greet(name, greeting):
print(f"{greeting}{name}")

greet(name="咖飞", greeting="你好")
greet(greeting="早上好", name="小王")

关键字参数一定要在位置参数之后

1
2
greet("咖飞", greeting="你好")     # ✅
greet(name="咖飞", "你好") # ❌ SyntaxError

三、默认参数

给参数一个默认值,调用时可以省略:

1
2
3
4
5
6
def greet(name, greeting="你好"):
print(f"{greeting}{name}")

greet("咖飞") # 你好,咖飞
greet("咖飞", "早上好") # 早上好,咖飞
greet("咖飞", greeting="hi") # hi,咖飞

默认参数一定要放在后面

1
2
def bad(x=1, y):    # ❌ SyntaxError: 非默认参数在默认参数后
...

⚠️ 可变默认参数陷阱(Python 最经典的坑)

1
2
3
4
5
6
def append(item, lst=[]):
lst.append(item)
return lst

append(1) # [1]
append(2) # [1, 2] !! 上一次的 lst 还在

原因:默认值 [] 只在 def 语句执行时求值一次,之后所有调用共享同一个对象。

修复

1
2
3
4
5
def append(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

规则:默认参数值只用不可变对象(数字、字符串、元组、None)。

四、可变位置参数 *args

*args 接受任意多个位置参数,打包成元组

1
2
3
4
5
6
7
def total(*args):
print(type(args)) # <class 'tuple'>
return sum(args)

total(1, 2, 3) # 6
total(1, 2, 3, 4, 5) # 15
total() # 0

名字 args 只是惯例,也可以叫 *nums*values关键是那个 *

五、可变关键字参数 **kwargs

**kwargs 接受任意多个关键字参数,打包成字典

1
2
3
4
5
6
7
8
def show(**kwargs):
print(type(kwargs)) # <class 'dict'>
for k, v in kwargs.items():
print(f"{k}={v}")

show(name="咖飞", age=3)
# name=咖飞
# age=3

六、混合使用

*args**kwargs 可以同时用,顺序固定:

1
def f(位置参数, *args, **kwargs):

例子:

1
2
3
4
5
def log(level, *args, **kwargs):
print(f"[{level}]", *args, kwargs)

log("INFO", "server started", port=8080, host="localhost")
# [INFO] server started {'port': 8080, 'host': 'localhost'}

七、参数解包(调用时的反向操作)

调用函数时,*** 用来”解包”:

1
2
3
4
5
6
7
8
def f(a, b, c):
print(a, b, c)

args = [1, 2, 3]
f(*args) # 等价 f(1, 2, 3)

kwargs = {"a": 1, "b": 2, "c": 3}
f(**kwargs) # 等价 f(a=1, b=2, c=3)

这在函数转发里非常有用:

1
2
3
def wrapper(*args, **kwargs):
# 做点前置逻辑
return original_func(*args, **kwargs)

八、仅位置参数与仅关键字参数(Python 3.8+)

现代 Python 提供两个特殊分隔符:

  • /:它之前的参数只能按位置传
  • *:它之后的参数只能按关键字传
1
2
3
4
5
6
7
def f(pos_only, /, both, *, kw_only):
print(pos_only, both, kw_only)

f(1, 2, kw_only=3) # ✅
f(1, both=2, kw_only=3) # ✅
f(pos_only=1, ...) # ❌ 不允许,pos_only 只能位置
f(1, 2, 3) # ❌ 不允许,kw_only 只能关键字

什么时候用 /

  • 参数名不稳定:不想让调用方依赖参数名字,将来改名不影响;
  • 性能极致:跳过关键字查找(微小优化);
  • API 语义:像内置 abs(x)len(s),这种”名字无关”的参数。

什么时候用 *

  • 参数很多,容易搞错顺序——强制关键字,代码可读性大幅提升;
  • 布尔开关参数sort(items, *, reverse=False),写起来 sort(items, reverse=True)sort(items, True) 清晰得多。

九、参数顺序的完整规则

一个函数的参数顺序可以有五类,按顺序:

1
2
3
4
5
6
7
8
def f(
仅位置, # 出现 / 之前
/,
位置或关键字, # 常规参数
*args, # 可变位置
仅关键字, # 出现在 *args 或 * 之后
**kwargs # 可变关键字
): ...

一个”极限例子”:

1
2
def spec(a, b, /, c, d, *args, e, f=10, **kwargs):
...
  • a, b:仅位置;
  • c, d:位置或关键字;
  • args:可变位置;
  • e:仅关键字,无默认;
  • f:仅关键字,有默认;
  • kwargs:可变关键字。

日常写函数不用同时用全部五种,但你应该看得懂别人写的这种签名。

十、类型注解与参数

参数一起写类型注解:

1
2
3
4
5
6
7
8
9
10
11
def register(
name: str,
age: int,
*emails: str,
active: bool = True,
**extras: str,
) -> dict:
return {"name": name, "age": age, "emails": emails,
"active": active, "extras": extras}

register("咖飞", 3, "a@x.com", "b@x.com", active=False, city="北京")

十一、一个”综合参数”例子

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
# params.py
"""演示各种参数形式的组合用法。"""


def create_user(name: str, /, age: int, *hobbies: str,
city: str = "北京", **extras) -> dict:
"""创建用户档案。

Args:
name: 姓名(仅位置)
age: 年龄
*hobbies: 任意多个爱好
city: 城市,仅关键字,默认"北京"
**extras: 额外自定义属性
"""
return {
"name": name,
"age": age,
"hobbies": hobbies,
"city": city,
**extras,
}


print(create_user("咖飞", 3, "读书", "跑步", city="上海", height=178))

十二、函数转发的经典模式

*args + **kwargs 让”包装函数”变得优雅:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
import functools

def timed(func):
"""一个简易装饰器:打印函数耗时。"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时 {time.time() - start:.3f}s")
return result
return wrapper

@timed
def compute(n, base=10):
return sum(i ** base for i in range(n))

compute(1000, base=3)

装饰器我们在 装饰器入门 详细讲。

十三、命令行参数解析预告

大多数命令行工具都用 argparse(标准库)解析参数:

1
2
3
4
5
6
7
8
9
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("input")
parser.add_argument("--output", default="out.txt")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()

print(args.input, args.output, args.verbose)

具体用法后面讲。

十四、参数传递的机制:值 or 引用?

Python 的参数传递是 “传对象引用”(pass by object reference):函数拿到的是”和外部同一个对象的引用”。

1
2
3
4
5
6
7
8
9
10
11
12
def f(x):
x = 10 # 让本地名字 x 指向新对象,不影响外部
def g(lst):
lst.append(1) # 就地修改可变对象,外部感知得到

a = 5
f(a)
print(a) # 5,没变

b = []
g(b)
print(b) # [1],变了

规则

  • 重新赋值 → 不影响外部;
  • 就地修改可变对象 → 影响外部。

想让函数”完全不影响外部”,传入之前拷贝:g(b[:]) 或者内部拷贝 lst = lst.copy()

十五、常见陷阱

  1. 可变默认参数:老生常谈,永远用 None + 内部判断。
  2. *args**kwargs 顺序args 一定在 kwargs 前。
  3. f(*args) 里 args 必须可迭代f(*None) 报错。
  4. **kwargs 里 kwargs 必须是字典且 key 是字符串
  5. 同一个参数名多次传值f(1, x=1)x 是第一个参数,会 TypeError。
  6. 默认值参数在 def 时求值,不是调用时:
1
2
3
4
import datetime
def log(msg, ts=datetime.datetime.now()):
print(ts, msg)
# 所有调用都用第一次 def 时的时间,除非 ts=None 再内部生成

正确:def log(msg, ts=None): if ts is None: ts = datetime.datetime.now()

  1. 对 dict 用 ** 解包时 key 冲突
1
f(**{"a": 1}, a=2)   # ❌ TypeError

十六、小结与延伸阅读

  • 位置参数、关键字参数、默认参数、*args**kwargs/* 七种参数形式;
  • 参数顺序:位置 → / → 位置或关键字 → *args → 仅关键字 → **kwargs
  • 可变默认参数是经典陷阱;
  • *args/**kwargs 是包装/转发函数的核心工具;
  • Python 参数传递是”传对象引用”;
  • 学会用 /* 让 API 更严谨。

延伸阅读:

下一篇 作用域与闭包 我们讲变量的”能见度”问题,以及闭包是怎么”记住外层变量”的。