参数类型:位置参数、默认参数、可变参数、关键字参数

一句话概述

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 个参数时已经记不清谁是谁了,这就是为什么需要关键字参数(见第四节)。

大白话 两三个参数用位置参数很舒服。五个以上?你大概率会把 usernamepassword 的位置搞反——然后 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_channelsout_channelskernel_size 这三个是位置参数(必须提供),其他如 stride=1padding=0dilation=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+ 个配置项;matplotlibplot(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 的变量名 argskwargs 是固定的吗?

不是!这只是一个约定俗成的命名习惯。 真正起作用的是星号——一个 * 表示打包位置参数为元组,两个 ** 表示打包关键字参数为字典。你可以用任何合法的变量名:*numbers*items**options**config 都是合法的。但社区约定用 argskwargs 有两个好处:①任何人看到 *args 就知道这是可变位置参数;②代码审查和协作时不需要额外解释。除非有更好的理由(比如 *scores 让语义更清晰),否则建议遵守约定。

Q2: 参数顺序为什么必须严格遵循?能不能把 *args 放在位置参数前面?

不能,这是 Python 语法的硬性规定。 想象一下:如果 *args 在前面,def f(*args, a, b)——当你调用 f(1, 2, 3, 4, 5) 时,*args 会把所有 5 个参数都"吃掉",ab 永远分不到值。所以 Python 强制规定:所有没有默认值的普通参数必须排在 *args 前面。完整顺序是:① 普通位置参数 ② *args(如果存在)③ 只限关键字参数(* 之后或带默认值的)④ **kwargs(如果存在)。

Q3: 调用时用 *** 解包是什么意思?和定义时有什么不同?

星号有"双重身份"。定义时def f(*args)——* 是"打包"操作符,把零散参数打包成元组。调用时f(*my_list)——* 是"解包"操作符,把列表/元组拆成独立的参数。双星号同理——定义时打包成字典,调用时拆解字典。一个实用记忆法:定义时"收拢",调用时"展开"。这在 AI 中非常实用:你可以把训练配置存在字典里(从 YAML 读取),调用时 train(**config) 一行解包。

Q4: 默认参数的可变对象陷阱到底是怎么回事?为什么会累积?

Python 的默认参数值在函数定义时只求值一次,而不是每次调用都重新求值。当默认值是可变对象(如 []{})时,所有调用共享的是同一个对象。第一次调用 append 修改了这个共享对象,第二次调用时看到的已经是修改后的对象了——这就是"累积"效应的原因。解决方案:默认值用 None,函数体内检查 if arg is None: arg = []。不可变对象(None0""False)没有这个问题,因为对它们的"修改"实际是创建新对象。

Q5: 什么是"只限关键字参数"(keyword-only argument)?

Python 3 引入了"裸星号"语法:def f(a, b, *, c, d)——* 后面的 cd 只能通过关键字方式传入,不能通过位置方式传入。这在你不想让用户靠位置猜参数含义时非常有用。比如 def send_email(to, *, subject, body)——to 用位置传(因为只有 1 个),subjectbody 必须用关键字传(防止 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)——* 解包把列表拆成三个独立参数,print 用空格分隔输出它们。注意输出中间有空格是因为 printsep=' ' 默认行为。

Q8 [多选] 关于 *args**kwargs 的命名,以下正确的是?

  • A. 可以改成 *numbers**config
  • B. argskwargs 只是社区约定
  • C. Python 强制要求用 argskwargs
  • D. 实际起作用的是 *** 符号,不是后面的变量名
解答:A、B、D 正确。C 错误——Python 只要求 *** 符号,变量名随意。但社区约定用 args/kwargs 以提高可读性。

Q9 [单选] def f(a, b): return a, bdef 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 函数名(普通参数, **任意名):