参数传递机制:位置、关键字、默认与可变

Python 的函数参数,说白了就是给函数递东西的方式。这玩意看着简单,但坑其实不少。我在做代码审查时,发现好多注释不一致的问题,源头就是参数传递搞混了。

咱们今天把这四种参数机制彻底捋一遍。你想想看,函数调用时参数怎么“对号入座”?顺序、名字、默认值、可变数量,每一种都有它的脾气。

Python 参数传递机制结构图 参数传递机制 位置参数 关键字参数 默认参数 可变参数 位置参数 - 按顺序映射 - 数量必须匹配 - 最基础的传参方式 关键字参数 - 指定形参名传参 - 顺序可任意 - 提高代码可读性 默认参数 - 缺少时使用默认值 - 避免重复传递 - 注意可变默认值坑 可变参数 - *args 接收元组 - **kwargs 接收字典 - 灵活的扩展性

位置参数:顺序就是一切

先聊最基础的——位置参数。调用函数时,实参按顺序一一对应到形参。这个逻辑很直觉,但容易出事。

我遇到过一个小伙伴传参时顺序写反了,结果日期当成金额传进去,查了好久才找到。嗯,这就是位置参数的双刃剑——简单直接,但顺序错了就全错。

def create_user(name, age, city):
    return f"{name}, {age}岁, 来自{city}"

# 位置参数调用
print(create_user("林月", 28, "深圳"))
# 输出: 林月, 28岁, 来自深圳

# 顺序错了会怎样?
print(create_user(28, "深圳", "林月"))
# 输出: 28, 深圳岁, 来自林月  --- 完全乱套了
位置参数的关键:实参与形参按位置一一绑定。数量、顺序都不能错。

关键字参数:明确的映射

关键字参数就聪明多了。调用时写上形参名 = 实参,顺序随便你怎么排。说白了,就是告诉函数:“这个值给这个参数,那个值给那个参数”。

我个人习惯在参数超过三个时,强制使用关键字参数。这样调用代码本身就成了文档,不用猜每个位置对应什么。

def train_model(data_path, model_type, epochs, learning_rate):
    print(f"数据: {data_path}")
    print(f"模型: {model_type}, 轮数: {epochs}, 学习率: {learning_rate}")

# 关键字参数 —— 顺序任意
train_model(
    learning_rate=0.001,
    model_type="随机森林",
    data_path="./data/train.csv",
    epochs=100
)

你看看,这个调用即使不写注释,别人也能看懂每个参数的含义。这就是关键字参数的价值。

关键字参数和位置参数可以混用,但位置参数必须放在前面。先写位置,再写关键字,这是硬规则。

默认参数:减少冗余

有的参数大部分情况下取值相同,每次都写很烦。默认参数就能解决这个问题。定义函数时直接给形参赋值,调用时如果不传,就使用默认值。

说白了就是:你给就按你的来,你不给我就按我的来。

def connect_db(host="localhost", port=5432, user="admin", password="123456"):
    print(f"连接 {host}:{port} 用户 {user}")

# 使用默认参数 —— 只需传需要改的
connect_db()                              # 全部默认
connect_db(host="10.0.0.1")               # 只改host
connect_db(host="10.0.0.2", port=3306)     # 改host和port
我曾经踩过一个经典坑:默认参数如果用的是可变对象(比如列表、字典),会被所有调用共享。你用一次改一次,下次调用的默认值就不是原来的样子了。
# 踩坑示例:不要把可变对象做默认参数
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("苹果"))    # ['苹果']
print(add_item("香蕉"))    # ['苹果', '香蕉']  —— 默认的列表被改了

# 正确的做法
def add_item_safe(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

这就是我常跟团队说的:默认参数用 None,然后在函数内部初始化。别偷懒直接给空列表。

可变参数:应对不确定性

有些场景,你事先不知道要传多少个参数。比如日志记录、配置合并、装饰器包装。这时候就需要可变参数上场了。

Python 提供了两套:*args 和 **kwargs。一个收位置参数,打包成元组;一个收关键字参数,打包成字典。

def log_event(timestamp, *messages, **metadata):
    print(f"[{timestamp}]")
    for msg in messages:
        print(f"  - {msg}")
    for key, value in metadata.items():
        print(f"  {key}: {value}")

# 调用:位置参数+可变位置+可变关键字
log_event(
    "2024-06-15 10:30:00",
    "用户登录成功",
    "IP地址: 192.168.1.100",
    "浏览器: Chrome",
    user_id=10086,
    level="info"
)
# 输出:
# [2024-06-15 10:30:00]
#   - 用户登录成功
#   - IP地址: 192.168.1.100
#   - 浏览器: Chrome
#   user_id: 10086
#   level: info
*args 收位置参数变元组,**kwargs 收关键字参数变字典。这是最灵活的参数设计模式。

你想想看,函数签名如果写死了参数个数,后面加需求就得改接口。用可变参数,向前兼容,优雅多了。

参数类型定义方式调用方式典型场景
位置参数def f(a, b)f(1, 2)简单、确定性调用
关键字参数def f(a, b)f(a=1, b=2)提高可读性、参数多时
默认参数def f(a=1)f() 或 f(2)配置项、可选参数
可变参数def f(*args, **kwargs)f(1,2, x=3, y=4)扩展性、包装器、日志
在参数优化注释一致性检查中,我会特别关注默认参数的可变对象陷阱。注释写“默认空列表”,实际行为却是共享列表,这种不一致最容易导致隐蔽 bug。

好了,四种参数机制就讲到这里。下次写函数签名时,想想你这参数到底是不变的(位置)、需要明确的(关键字)、可有可无的(默认)还是不确定数量的(可变)。选对了,代码就清晰了。