参数类型:位置参数、默认参数、可变参数、关键字参数
一句话概述
Python 函数的参数系统极其灵活——你可以按位置一个个传参,也可以给参数设默认值省去重复输入,还可以用 *args 接收任意数量的位置参数、用 **kwargs 接收任意数量的关键字参数。这套参数体系就像点菜系统:位置参数是"必须点的菜"(不点就饿着),默认参数是"套餐里的标配"(你可以换、也可以不换),*args 是"加菜"(任意加多少个都行),**kwargs 是"备注"(少辣、多加葱、不要香菜)。掌握这四种参数类型,你就能写出既简洁又强大的函数接口——这在 AI 框架的 API 设计中随处可见。
💡 核心要点:①位置参数按声明顺序对应传值,调用时必须提供且顺序要正确 ②默认参数在定义时用=赋默认值,调用时可省略,必须放在位置参数后面 ③*args接收任意多个位置参数,打包成元组,用星号语法解包 ④**kwargs接收任意多个关键字参数,打包成字典,用双星号语法解包 ⑤参数顺序固定:位置参数 →*args→ 关键字参数 →**kwargs
教学与演示
一、位置参数——按座次入座
是什么:位置参数(Positional Argument)是最基本、最常用的参数类型。定义时在括号里写变量名,调用时按顺序传入对应的值——第一个实参给第一个形参,第二个实参给第二个形参,一一对应,分毫不差。位置参数就像电影院的对号入座——1 号票坐 1 号位,2 号票坐 2 号位,不能乱坐。
大白话 位置参数就像排队买票——你站第一个,你就是第一个买;你站第二个,你就是第二个买。调用greet("张三", 25)——"张三"自动给第一个参数name,25 自动给第二个参数age。Python 不会因为你把"张三"给了age就报类型错——它只看位置,不看内容。
为什么:位置参数是最直观的传参方式——定义和调用的逻辑完全一致,读代码时从上到下、从左到右就能理解。大多数简单函数只有 1-2 个参数时,位置参数足够了。它的缺点是参数多了容易混淆顺序——draw_rect(x1, y1, x2, y2, color, width) 调用到第 5 个参数时已经记不清谁是谁了,这就是为什么需要关键字参数(见第四节)。
大白话 两三个参数用位置参数很舒服。五个以上?你大概率会把username和password的位置搞反——然后 debug 一小时。这就是为什么复杂函数要用关键字参数或默认参数来"防呆"。
怎么做:
import numpy as np
# ====== 1. 位置参数的基本用法 ======
def describe_person(name, age, city):
"""三个位置参数:name、age、city"""
print(f"{name} 今年 {age} 岁,来自 {city}。")
# 按位置一一对应传参
describe_person("张三", 25, "北京") # 张三→name, 25→age, "北京"→city
describe_person("李四", 30, "上海") # 顺序绝对不能乱!
# ====== 2. 位置参数必须完整提供 ======
# 少传参数会报错
try:
describe_person("王五") # 只传了 1 个,缺少 age 和 city
except TypeError as e:
print(f"错误: {e}") # missing 2 required positional arguments
# ====== 3. AI 场景:计算两个向量的欧几里得距离 ======
def euclidean_distance(v1, v2):
"""计算两个等长向量的欧几里得距离(L2 范数)"""
# v1 和 v2 是位置参数,调用时必须按顺序传入
if len(v1) != len(v2):
return None # 长度不同无法计算
sum_sq = sum((a - b) ** 2 for a, b in zip(v1, v2))
return sum_sq ** 0.5 # 开平方根
# 模拟:两个词向量(word embeddings 简化版)
word_vec_猫 = [0.5, 0.8, 0.1, 0.3] # "猫" 的 4 维向量
word_vec_狗 = [0.6, 0.7, 0.2, 0.4] # "狗" 的 4 维向量
word_vec_鸟 = [0.1, 0.2, 0.9, 0.7] # "鸟" 的 4 维向量
dist_cat_dog = euclidean_distance(word_vec_猫, word_vec_狗)
dist_cat_bird = euclidean_distance(word_vec_猫, word_vec_鸟)
dist_dog_bird = euclidean_distance(word_vec_狗, word_vec_鸟)
print(f"\n猫与狗的距离: {dist_cat_dog:.3f}") # 语义相近,距离应该小
print(f"猫与鸟的距离: {dist_cat_bird:.3f}") # 语义相差大,距离应该大
print(f"狗与鸟的距离: {dist_dog_bird:.3f}") # 语义相差大,距离应该大")
# 在 AI 中,向量距离用于语义相似度计算、KNN 分类、聚类等
# ====== 4. 位置参数调换顺序的陷阱 ======
def divide(a, b):
"""a ÷ b"""
return a / b
print(f"\n10 ÷ 2 = {divide(10, 2)}") # 5.0 — 正确
print(f"2 ÷ 10 = {divide(2, 10)}") # 0.2 — 顺序错了,结果完全不同!
# 这就是为什么复杂函数需要关键字参数什么用:在 AI 中,位置参数广泛用于简单函数。激活函数 relu(x) 只有一个参数,用位置参数最自然。大多数数学函数(sqrt(x)、log(x)、exp(x))也是单参数。当函数有 2-3 个语义明确的参数时,位置参数依然清晰——loss_fn(predictions, targets) 一看就知道含义。不过 AI 框架的设计者非常清楚:参数多了位置参数就是地雷,所以 PyTorch 的 nn.Conv2d(in_channels, out_channels, kernel_size, ...) 用到了大量默认参数和关键字参数。
二、默认参数——自带备选方案
是什么:默认参数(Default Parameter)在定义时用 = 给参数赋一个默认值。调用函数时,如果给这个参数传了值就用传的值,没传就用默认值。默认参数必须放在所有位置参数的后面——Python 规定"有默认值的参数必须在没有默认值的参数之后",这是铁律。
大白话 默认参数就像饭店的套餐——套餐里的米饭是默认配置(rice=True)。你啥也不说,端上来的就是米饭。但你可以说"米饭换成馒头"(rice=False),饭店就给你换。默认参数让你既能享受"不说的便利"(大多数场景),又能保留"说的自由"(特殊情况)。
为什么:默认参数解决了两大痛点。①简化常见调用:一个函数 10 个参数,但 90% 的情况下只有 2 个需要用户指定,其他用默认值就行了。②向后兼容:给函数加新参数时,如果给它设默认值,旧的调用代码不会报错。AI 框架中几乎每个复杂函数都有默认参数——Adam(lr=0.001, betas=(0.9, 0.999)) 大部分人只改 lr,其他用默认。
大白话 想象你要配置一台新电脑。如果没有默认参数,你得指定所有部件:CPU 型号、内存大小、硬盘容量、显卡型号、电源功率……累死你。有了默认参数,你只需要说"我要 16G 内存",其他都用默认配置——电脑店自动选最主流的 CPU、512G 硬盘、集显。这就是默认参数在 AI 框架中的角色。
怎么做:
import numpy as np
# ====== 1. 默认参数的基本用法 ======
def greet(name, greeting="你好"):
"""greeting 有默认值 '你好'"""
print(f"{greeting},{name}!")
# 不传 greeting——用默认值
greet("张三") # 你好,张三!
# 传了 greeting——覆盖默认值
greet("李四", "早上好") # 早上好,李四!
greet("外国人", "Hello") # Hello,外国人!
# ====== 2. 默认参数必须放在末尾——铁律! ======
# ✅ 正确:默认参数在位置参数之后
def power(base, exponent=2):
"""计算 base 的 exponent 次方,默认平方"""
return base ** exponent
# ❌ 错误:默认参数不能放在位置参数之前
# def wrong(option=True, name): ← SyntaxError! 默认参数必须在后面
print(f"\n5² = {power(5)}") # 25 — 默认平方
print(f"5³ = {power(5, 3)}") # 125 — 覆盖默认值
print(f"5¹ = {power(5, 1)}") # 5
# ====== 3. 多个默认参数的组合 ======
def configure_training(
epochs=100, # 默认训练 100 轮
batch_size=32, # 默认批大小 32
learning_rate=0.001, # 默认学习率 0.001
optimizer="adam", # 默认优化器 Adam
early_stop=True, # 默认开启早停
verbose=True # 默认打印日志
):
"""配置模型训练参数——大量默认值省去重复输入"""
config = {
"epochs": epochs,
"batch_size": batch_size,
"lr": learning_rate,
"optimizer": optimizer,
"early_stop": early_stop,
"verbose": verbose
}
return config
# 使用默认配置(最常见)
config1 = configure_training()
print(f"\n默认配置: {config1}")
# 只改学习率,其他用默认
config2 = configure_training(learning_rate=0.0001, epochs=200)
print(f"自定义配置: {config2}")
# ====== 4. 默认参数的"陷阱":可变默认值 ======
# ⚠️ 陷阱!千万不要用可变对象(列表、字典)做默认参数!
def bad_append(item, target_list=[]): # ❌ 默认值是空列表!
"""这是有 bug 的写法!默认值列表只会创建一次"""
target_list.append(item)
return target_list
print("\n=== 默认参数陷阱 ===")
print(bad_append(1)) # [1] — 第一次正常
print(bad_append(2)) # [1, 2] — 咦?怎么还有 1?!
print(bad_append(3)) # [1, 2, 3] — 默认列表在"累积"!
# 为什么?因为默认值 [] 只在函数定义时创建一次,后续调用共享同一个列表对象
# ✅ 正确做法:默认值用 None,函数内检查
def good_append(item, target_list=None):
"""正确的写法:None + 函数内部初始化"""
if target_list is None: # 如果没传列表
target_list = [] # 创建新列表
target_list.append(item)
return target_list
print("\n=== 正确的写法 ===")
print(good_append(1)) # [1]
print(good_append(2)) # [2] — 正常!每次都是新列表
print(good_append(3)) # [3]
# ====== 5. AI 场景:自定义数据增强函数 ======
def augment_image(
img, # 必须的参数:输入图像(numpy 数组)
rotation=0.0, # 默认不旋转
flip_horizontal=False, # 默认不水平翻转
brightness=1.0, # 默认不调亮度
noise_std=0.0 # 默认不加噪
):
"""数据增强函数——只为需要的增强方式传参"""
# 用 numpy 模拟图像增强效果
result = np.array(img, dtype=float)
if rotation != 0:
# 模拟旋转:对于简化的情况,乘以旋转因子
result = result * (1 + rotation * 0.01)
if flip_horizontal:
result = result[::-1] # 水平翻转 = 反转数组
if brightness != 1.0:
result = result * brightness # 调整亮度 = 乘因子
if noise_std > 0:
noise = np.random.normal(0, noise_std, result.shape)
result = result + noise # 添加高斯噪声
return result
# 模拟一个 5 像素的"图像"
image = np.array([10, 20, 30, 40, 50])
original = augment_image(image) # 不增强——原样输出
flipped = augment_image(image, flip_horizontal=True) # 只水平翻转
bright = augment_image(image, brightness=1.5) # 只调亮
noisy = augment_image(image, noise_std=2.0, flip_horizontal=True) # 加噪+翻转
print(f"\n原始: {original}")
print(f"翻转: {flipped}")
print(f"调亮: {bright}")
print(f"加噪+翻转: {[round(x, 1) for x in noisy]}")什么用:默认参数在 AI 框架设计中是标配。nn.Conv2d 有十几个参数,但 in_channels、out_channels、kernel_size 这三个是位置参数(必须提供),其他如 stride=1、padding=0、dilation=1 全是默认参数——初学者不需要理解每个参数就能跑起来。这种"渐进式学习曲线"完全依赖默认参数机制。你自己写训练脚本时,用默认参数封装配置,既简洁又专业。
三、可变参数 *args——来多少接多少
是什么:*args(Arbitrary Positional Arguments)让函数可以接收任意数量的位置参数。在函数内部,args 是一个元组(tuple),包含了所有被传进来的额外位置参数。星号 * 是"打包"操作符——把零散的多个参数打包成一个元组。
大白话*args就像食堂的"随到随吃"窗口——不管你来 3 个人、5 个人还是 20 个人,窗口都能接待。sum(1, 2, 3, 4, 5)为什么能传任意多个参数?因为sum的定义就是sum(*args)——它不管你传几个数,全打包成一个元组,然后在函数里遍历求和。*args让函数从"固定模式"变成"弹性模式"。
为什么:有些场景下你无法提前知道会传多少个参数——比如 print() 可以接任意多个值,max() 可以接任意多个数。*args 让函数接口极其灵活。在 AI 中,当你需要处理不定长的数据(如变长序列、不同数量特征),*args 让代码保持简洁。
大白话 没有*args的世界:你想写一个求平均值的函数,但不知道用户会传 2 个数还是 100 个数。你只能让用户传一个列表——avg([1,2,3])。有了*args,你可以直接avg(1,2,3)——干净利落。注意*args只是一个命名惯例,*才是关键——*numbers、*items完全合法。
怎么做:
import numpy as np
# ====== 1. *args 的基本用法 ======
def collect_numbers(*args):
"""接收任意数量的位置参数"""
print(f"收到了 {len(args)} 个参数")
print(f"args 的类型: {type(args)}") # <class 'tuple'>
print(f"args 的内容: {args}")
print(f"args 的和: {sum(args)}")
collect_numbers(1, 2, 3) # 3 个参数
print("---")
collect_numbers(10, 20, 30, 40, 50) # 5 个参数
# ====== 2. *args 与普通参数混用 ======
def my_max(first, *rest):
"""自定义 max:第一个参数单独拿,其余放 *args"""
# first 是必须的,rest 是可选的(可以 0 个)
max_val = first
for num in rest: # 遍历 rest 元组
if num > max_val:
max_val = num
return max_val
print(f"\nmy_max(5, 2, 8, 1) = {my_max(5, 2, 8, 1)}") # 8
print(f"my_max(3) = {my_max(3)}") # 3 — rest 是空元组
# ====== 3. 解包:用 * 把列表/元组拆成位置参数 ======
# * 有双重身份:定义时是"打包",调用时是"解包"
def multiply(a, b, c):
"""计算三个数的乘积"""
return a * b * c
nums = [2, 3, 4] # 一个列表
# multiply(nums) ❌ 报错——nums 是一个参数(列表),但函数需要 3 个参数
result = multiply(*nums) # *nums 把列表解包成 2, 3, 4 三个独立参数
print(f"\nmultiply(*[2,3,4]) = {result}") # 24
# 解包在 AI 中常用:把超参数列表传给函数
hyperparams = [128, 64, 0.01] # [hidden_size, batch_size, lr]
# 传给训练函数: train(*hyperparams) ← 一行解包,不用写三个变量
# ====== 4. *args 用于构建灵活的汇总函数 ======
def aggregate(operation, *values):
"""灵活的数据聚合函数——operation 指定运算,*values 接收数据"""
if not values:
return None
if operation == "sum":
return sum(values)
elif operation == "mean":
return sum(values) / len(values)
elif operation == "product":
result = 1
for v in values:
result *= v
return result
elif operation == "min":
return min(values)
elif operation == "max":
return max(values)
else:
return f"未知操作: {operation}"
scores = [85, 92, 78, 95, 88]
print(f"\n总和: {aggregate('sum', *scores)}") # 438 — *scores 解包传参
print(f"平均分: {aggregate('mean', *scores)}") # 87.6
print(f"连乘积: {aggregate('product', 2, 3, 4)}") # 24
print(f"最小值: {aggregate('min', *scores)}") # 78
print(f"最大值: {aggregate('max', *scores)}") # 95
# ====== 5. AI 场景:构建可拼接的神经网络层 ======
def sequential_layers(*layers):
"""模拟 PyTorch 的 nn.Sequential:按顺序叠加层"""
print(f"\n构建 Sequential 模型,共 {len(layers)} 层:")
for i, layer in enumerate(layers):
print(f" 第 {i} 层: {layer}")
return layers # 返回层列表
# 随便加多少层都行——*layers 自动接收
model1 = sequential_layers("Conv2D(3→64)", "ReLU", "MaxPool(2×2)")
model2 = sequential_layers(
"Conv2D(3→64)", "BatchNorm", "ReLU",
"Conv2D(64→128)", "BatchNorm", "ReLU",
"Flatten", "Dense(128→10)", "Softmax"
)
# 模拟:用 *args 拼接特征向量
def concat_features(*feature_vectors):
"""拼接多个特征向量(类似 np.concatenate)"""
result = []
for vec in feature_vectors:
result.extend(vec) # 展开每个向量
return result
feat1 = [0.5, 0.8] # 颜色特征
feat2 = [0.1, 0.3, 0.9] # 纹理特征
feat3 = [0.7] # 形状特征
combined = concat_features(feat1, feat2, feat3)
print(f"\n拼接特征: {combined}") # [0.5, 0.8, 0.1, 0.3, 0.9, 0.7]什么用:*args 在 AI 框架中大量使用。tf.keras.layers.concatenate([a, b, c]) 接收任意数量的层进行拼接;torch.cat([tensor1, tensor2, ...]) 拼接任意数量的张量;自定义损失函数中 def custom_loss(*outputs) 接收多任务学习的多个输出。数据预处理中,*args 用于接收任意数量的特征列进行合并或交叉。
四、关键字参数 **kwargs——指名道姓地传参
是什么:**kwargs(Keyword Arguments)让函数接收任意数量的关键字参数(以 key=value 形式传入)。在函数内部,kwargs 是一个字典(dict),键是参数名(字符串),值是对应的值。双星号 ** 是字典的"打包/解包"操作符。
大白话 如果说位置参数是"按座次入座",那关键字参数就是"按名牌入座"——你不关心座位顺序,直接喊名字就行。**kwargs 更进一步——你可以传任意数量的"名牌+人名"组合,函数全收到一个字典里。这就像订酒店时填"特殊需求"——不限制你能填多少条,每一条都是"需求类型=具体要求"的格式。
为什么:关键字参数解决了位置参数的三个致命问题:①参数太多记不住顺序;②可读性差(rect(10, 20, 50, 60, "red", 2) 鬼知道每个数字什么意思);③部分参数可选时调用很痛苦。**kwargs 让函数可以接收"任意额外的配置"——这在 AI 框架的超参数传递中至关重要。
大白话 调用train(data, lr=0.01, epochs=200, batch_size=64, optimizer="adam")和train(data, optimizer="adam", batch_size=64, epochs=200, lr=0.01)是完全等价的——因为每个参数都带名字,顺序不重要。这就是 Python 为"代码可读性"设计的优雅机制。
怎么做:
import numpy as np
# ====== 1. **kwargs 的基本用法 ======
def show_details(**kwargs):
"""接收任意数量的关键字参数"""
print(f"kwargs 类型: {type(kwargs)}") # <class 'dict'>
print(f"kwargs 内容: {kwargs}")
for key, value in kwargs.items():
print(f" {key} = {value} ({type(value).__name__})") # 打印键值对和类型
show_details(name="张三", age=25, city="北京", is_vip=True)
# ====== 2. 四类参数混合使用(完整形态) ======
def full_demo(pos1, pos2, *args, kw1="默认", kw2="默认", **kwargs):
"""展示四种参数类型的完整组合——这是 Python 函数参数的终极形态"""
print(f"位置参数: pos1={pos1}, pos2={pos2}")
print(f"可变位置: *args={args}")
print(f"默认参数: kw1={kw1}, kw2={kw2}")
print(f"可变关键字: **kwargs={kwargs}")
print("\n=== 四种参数混合调用 ===")
full_demo(
"必传1", "必传2", # 两个位置参数(必须)
"额外1", "额外2", "额外3", # *args 接收(可选)
kw1="覆盖默认1", # 关键字参数(可选)
extra1="额外配置A", # **kwargs 接收
extra2="额外配置B", # **kwargs 接收
mode="fast" # **kwargs 接收
)
# ====== 3. 解包字典:用 ** 把字典拆成关键字参数 ======
def create_user(name, age, role="user"):
"""创建用户——接收关键字参数"""
return {
"name": name,
"age": age,
"role": role
}
# 解包字典传参——字典的键就是参数名,值就是参数值
user_data = {"name": "张三", "age": 25, "role": "admin"}
user = create_user(**user_data) # ** 解包字典
print(f"\n解包创建用户: {user}")
# 这在 AI 中非常常用——从配置文件(dict)加载超参数
def train_model(model, **hyperparams):
"""接收任意超参数配置"""
print(f"\n训练模型: {model}")
print(f"超参数: {hyperparams}")
# 实际训练代码...
return hyperparams
# 从 YAML/JSON 配置文件读取的超参数字典
config = {
"lr": 0.001,
"epochs": 100,
"batch_size": 32,
"optimizer": "adam",
"weight_decay": 1e-4
}
train_model("SimpleCNN", **config) # 解包传入所有超参数
# ====== 4. 参数顺序的"铁律" ======
# Python 的参数顺序规则(必须严格遵守):
# 位置参数 → *args → 只限关键字的默认参数 → **kwargs
# 定义示例:
def correct_order(a, b, *args, c=1, d=2, **kwargs):
"""正确的参数顺序示范"""
print(f"a={a}, b={b}, args={args}, c={c}, d={d}, kwargs={kwargs}")
correct_order(1, 2, 3, 4, 5, c=99, e="extra")
# a=1, b=2, args=(3, 4, 5), c=99, d=2, kwargs={'e': 'extra'}
# ====== 5. AI 场景:灵活的模型工厂函数 ======
def build_layer(layer_type, **layer_params):
"""模拟层构建工厂——用 **kwargs 接收各种层参数"""
print(f"\n构建层: {layer_type}")
if layer_type == "dense":
units = layer_params.get("units", 128)
activation = layer_params.get("activation", "relu")
return f"Dense(units={units}, activation='{activation}')"
elif layer_type == "conv2d":
filters = layer_params.get("filters", 32)
kernel = layer_params.get("kernel_size", 3)
return f"Conv2D(filters={filters}, kernel={kernel}×{kernel})"
elif layer_type == "dropout":
rate = layer_params.get("rate", 0.5)
return f"Dropout(rate={rate})"
else:
return f"未知层类型: {layer_type}"
# 不同的层用不同的参数——**kwargs 都能接收
print(build_layer("dense", units=64, activation="sigmoid"))
print(build_layer("conv2d", filters=128, kernel_size=5, padding="same"))
print(build_layer("dropout", rate=0.3))
print(build_layer("dense", units=10, activation="softmax"))什么用:**kwargs 在 AI 框架设计中堪称神器。PyTorch Lightning 的 Trainer(**kwargs) 几乎所有参数都是关键字参数——因为可能有 50+ 个配置项;matplotlib 的 plot(x, y, **kwargs) 让用户指定颜色、线型、标签等任意样式;自定义训练循环中 run_experiment(**config) 从配置文件解包所有超参数。掌握 **kwargs 后,你会发现很多库的 API 设计突然变得清晰了。
概念关系图谱
| 概念 | 核心含义 | 与AI的关系 | 关联概念 |
|---|---|---|---|
| 位置参数 | 按顺序对应传值 | 简单函数的最常用参数形式 | 关键字参数、默认参数 |
| 默认参数 | 定义时赋默认值,调用时可省略 | 框架 API 中大量可选配置项 | 位置参数、可变参数 |
| *args | 接收任意多个位置参数,打包成元组 | 变长序列处理、数据拼接 | **kwargs、解包 |
| **kwargs | 接收任意多个关键字参数,打包成字典 | 超参数配置、模型构建工厂 | *args、字典解包 |
| 解包 * / ** | 调用时把容器拆成独立参数 | 从配置文件注入参数 | 打包、序列 |
| 参数顺序 | 位置→args→关键字→kwargs | 框架 API 设计规范 | 函数签名、PEP 8 |
| 可变默认陷阱 | 默认值用可变对象会导致意外共享 | 训练指标收集器的初始化 | None、is 判断 |
| 关键字传参 | 用 key=value 指名传参 | 提升超参可读性,跳过不需要的参数 | 位置传参、可读性 |
重点答疑
Q1: *args 和 **kwargs 的变量名 args 和 kwargs 是固定的吗?
不是!这只是一个约定俗成的命名习惯。 真正起作用的是星号——一个 * 表示打包位置参数为元组,两个 ** 表示打包关键字参数为字典。你可以用任何合法的变量名:*numbers、*items、**options、**config 都是合法的。但社区约定用 args 和 kwargs 有两个好处:①任何人看到 *args 就知道这是可变位置参数;②代码审查和协作时不需要额外解释。除非有更好的理由(比如 *scores 让语义更清晰),否则建议遵守约定。
Q2: 参数顺序为什么必须严格遵循?能不能把 *args 放在位置参数前面?
不能,这是 Python 语法的硬性规定。 想象一下:如果 *args 在前面,def f(*args, a, b)——当你调用 f(1, 2, 3, 4, 5) 时,*args 会把所有 5 个参数都"吃掉",a 和 b 永远分不到值。所以 Python 强制规定:所有没有默认值的普通参数必须排在 *args 前面。完整顺序是:① 普通位置参数 ② *args(如果存在)③ 只限关键字参数(* 之后或带默认值的)④ **kwargs(如果存在)。
Q3: 调用时用 * 和 ** 解包是什么意思?和定义时有什么不同?
星号有"双重身份"。定义时:def f(*args)——* 是"打包"操作符,把零散参数打包成元组。调用时:f(*my_list)——* 是"解包"操作符,把列表/元组拆成独立的参数。双星号同理——定义时打包成字典,调用时拆解字典。一个实用记忆法:定义时"收拢",调用时"展开"。这在 AI 中非常实用:你可以把训练配置存在字典里(从 YAML 读取),调用时 train(**config) 一行解包。
Q4: 默认参数的可变对象陷阱到底是怎么回事?为什么会累积?
Python 的默认参数值在函数定义时只求值一次,而不是每次调用都重新求值。当默认值是可变对象(如 []、{})时,所有调用共享的是同一个对象。第一次调用 append 修改了这个共享对象,第二次调用时看到的已经是修改后的对象了——这就是"累积"效应的原因。解决方案:默认值用 None,函数体内检查 if arg is None: arg = []。不可变对象(None、0、""、False)没有这个问题,因为对它们的"修改"实际是创建新对象。
Q5: 什么是"只限关键字参数"(keyword-only argument)?
Python 3 引入了"裸星号"语法:def f(a, b, *, c, d)——* 后面的 c 和 d 只能通过关键字方式传入,不能通过位置方式传入。这在你不想让用户靠位置猜参数含义时非常有用。比如 def send_email(to, *, subject, body)——to 用位置传(因为只有 1 个),subject 和 body 必须用关键字传(防止 send_email("a@b.com", "你好", "正文内容") 混淆 subject 和 body)。
章节单词汇总
| 英文 | 音标 | 术语/释义 |
|---|---|---|
| positional | /pəˈzɪʃənəl/ | 位置的;按顺序对应传参 |
| default | /dɪˈfɔːlt/ | 默认的;参数预设的备选值 |
| argument | /ˈɑːrɡjumənt/ | 实参;调用时传入的具体值 |
| variable-length | /ˈveəriəbl leŋθ/ | 可变长度的;*args 接收任意数量参数 |
| keyword | /ˈkiːwɜːrd/ | 关键字;key=value 形式的传参方式 |
| unpack | /ʌnˈpæk/ | 解包;用 * 或 ** 拆解容器为独立参数 |
| None | /nʌn/ | 空值;默认参数最佳安全初始值 |
| tuple | /ˈtʌpl/ | 元组;*args 在函数内部的存储形式 |
面试练习
Q1 [单选] 以下函数调用哪个是正确的?def f(a, b=2, c=3):
- A.
f(1, 2, 3, 4) - B.
f(1, c=5) - C.
f(b=2, 1) - D.
f()
解答:B 正确。a=1(位置),b用默认值 2,c=5(关键字覆盖)。A 参数太多(只有 3 个),C 位置参数不能在关键字参数后面,D 缺少必传参数a。
Q2 [单选] def f(*args): print(len(args)) 执行 f([1,2,3]) 输出什么?
- A. 3
- B. 1
- C. 0
- D. 报错
解答:B 正确。f([1,2,3])只传了一个参数——这个参数本身是一个列表[1,2,3]。*args把它打包成([1,2,3],),长度为 1。如果要传 3 个独立参数,应该用f(*[1,2,3])解包。
Q3 [单选] 关于默认参数的可变对象陷阱,以下哪种写法是安全的?
- A.
def f(lst=[]): pass - B.
def f(lst=None): if lst is None: lst = [] - C.
def f(lst=list()): pass - D.
def f(lst={}): pass
解答:B 正确——用None做默认值、函数内检查并新建,是解决可变默认参数陷阱的标准模式。A、C(list()同样每次都是同一个对象)、D 都有问题。
Q4 [多选] 以下关于 **kwargs 的说法正确的是?
- A. 在函数内部
kwargs是一个字典 - B. 调用时可以用
**dict_var解包字典传参 - C.
**kwargs必须放在参数列表的末尾 - D. 调用时
**kwargs的参数顺序很重要
解答:A、B、C 正确。D 错误——关键字参数不关心顺序,f(a=1, b=2)和f(b=2, a=1)完全等价。
Q5 [单选] 以下哪个是正确的参数顺序?
- A.
def f(**kwargs, *args, a, b): - B.
def f(*args, a, b, **kwargs): - C.
def f(a, b, *args, **kwargs): - D.
def f(**kwargs, a, b, *args):
解答:C 正确。参数顺序铁律:普通位置参数 →*args→**kwargs。A 和 D 把**kwargs放在开头,B 把位置参数放在*args后面(位置参数必须在*args前面)。
Q6 [多选] 以下哪些调用方式是合法的?def f(a, b=0, *args, **kwargs):
- A.
f(1) - B.
f(1, 2) - C.
f(1, 2, 3, 4, x=5) - D.
f(a=1, 2)
解答:A、B、C 合法。A:a=1, b=0(默认)。B:a=1, b=2。C:a=1, b=2, args=(3,4), kwargs={'x':5}。D 不合法——2是位置参数,不能在关键字参数a=1之后。
Q7 [单选] nums = [1,2,3]; print(*nums) 输出什么?
- A.
1 2 3 - B.
[1, 2, 3] - C.
(1, 2, 3) - D. 报错
解答:A 正确。print(*nums)等价于print(1, 2, 3)——*解包把列表拆成三个独立参数,sep=' '默认行为。
Q8 [多选] 关于 *args 和 **kwargs 的命名,以下正确的是?
- A. 可以改成
*numbers和**config - B.
args和kwargs只是社区约定 - C. Python 强制要求用
args和kwargs - D. 实际起作用的是
*和**符号,不是后面的变量名
解答:A、B、D 正确。C 错误——Python 只要求*和**符号,变量名随意。但社区约定用args/kwargs以提高可读性。
Q9 [单选] def f(a, b): return a, b 和 def f(*args): return args 的区别是什么?
- A. 第一个固定 2 参数,第二个任意数量参数
- B. 完全一样,只是写法不同
- C. 第一个返回元组,第二个返回列表
- D. 第一个不能接收列表,第二个只能接收列表
解答:A 正确。def f(a, b)只能接收恰好 2 个参数;def f(*args)接收任意数量(0 到 n 个)。另外return a, b返回元组(a, b),return args返回元组args——都是元组。
Q10 [多选] 要在函数中同时接收「必须的参数 name」和「任意关键字参数」,以下哪些定义正确?
- A.
def f(name, **kwargs): - B.
def f(name, **config): - C.
def f(**kwargs, name): - D.
def f(**kwargs, name=):
解答:A、B 正确(变量名随便取)。C 错误——**kwargs必须在最后,它后面的参数拿不到值。D 语法错误。正确写法就是def 函数名(普通参数, **任意名):。