可变参数详解(不是指针哦)

2026-08-27 Python,可变参数

可变参数*args**kwargs 有什么区别?

核心区别*args 接收的是位置参数(Positional Arguments),而 **kwargs 接收的是关键字参数(Keyword Arguments)
argskwargs 只是约定俗成的名称,关键在于前面的 ***,它们在底层的数据类型、使用场景及函数调用方式上有着本质不同。

1. 核心区别速览表

特征 *args **kwargs
全称 Arguments Keyword Arguments
作用 接收任意数量的位置参数 接收任意数量的关键字参数
底层类型 打包成一个 元组 (Tuple) 打包成一个 字典 (Dict)
调用传参 my_func(1, 2, 3) my_func(a=1, b=2)
解包操作 * 解包序列(列表/元组) ** 解包字典
参数位置 必须位于普通参数之后,**kwargs 之前 必须放在所有参数的最后

2. 在函数定义中(打包/收集)

当不知道调用者会传入多少个参数时,使用它们来“打包”多余的参数。

def demo(a, b, *args, **kwargs):
    print(f"普通参数 a={a}, b={b}")
    print(f"*args 收集到的多余位置参数: {args}")   # 元组
    print(f"**kwargs 收集到的多余关键字参数: {kwargs}") # 字典

# 调用
demo(1, 2, 3, 4, 5, x=100, y=200)

# 输出:
# 普通参数 a=1, b=2
# *args 收集到的多余位置参数: (3, 4, 5)   <-- 元组
# **kwargs 收集到的多余关键字参数: {'x': 100, 'y': 200} <-- 字典

3. 在函数调用中(解包/展开)

***调用函数时,作用正好相反——它们负责把列表或字典“打散”成独立的参数传给函数。

def calc(x, y, z):
    return x + y + z

# 1. 使用 * 解包列表/元组(按位置匹配)
nums = [10, 20, 30]
print(calc(*nums))  # 等同于 calc(10, 20, 30) -> 输出 60

# 2. 使用 ** 解包字典(按参数名匹配)
data = {'x': 5, 'z': 15, 'y': 10}  # 注意 key 必须与参数名完全一致
print(calc(**data))  # 等同于 calc(x=5, y=10, z=15) -> 输出 30

4. 参数定义的绝对顺序(企业级硬性规则)

在定义一个功能复杂的函数时,若同时包含这几种参数,顺序绝对不能错,否则会报语法错误。

正确顺序

普通位置参数*args默认关键字参数**kwargs

def full_example(pos1, pos2, *args, default_val=10, **kwargs):
    print(pos1, pos2, args, default_val, kwargs)

# 符合规则的调用
full_example(1, 2, 3, 4, default_val=20, name="Alice")
# 1 2 (3, 4) 20 {'name': 'Alice'}

5. 企业级最佳实践与避坑指南

1. 命名并非强制,但请遵守惯例

语法上可以写成 *numbers**options,但业界默认使用 *args**kwargs,因为这几乎是所有 Python 开发者的“通用语言”,能瞬间提升代码可读性。

2. 装饰器中的经典应用(透传参数)

在编写装饰器时,*args**kwargs 是必备工具,用于将原函数的所有参数原封不动地传递下去。

import functools

def logger(func):
    @functools.wraps(func)  # 最佳实践:保留原函数元数据
    def wrapper(*args, **kwargs):
        print(f"调用函数 {func.__name__}")
        # 透传所有参数
        return func(*args, **kwargs)
    return wrapper

@logger
def add(a, b):
    return a + b

3. 避免滥用,保持函数签名清晰

虽然 def func(*args, **kwargs): 很万能,但如果函数内部逻辑复杂,这种写法会让调用者完全不知道该传什么参数(失去了类型注解和 IDE 提示的意义)。

  • ✅ 推荐:明确写出关键业务参数,用 **kwargs 仅接收少量可选配置。
  • ❌ 不推荐:为了偷懒,把所有函数都写成万能通配符。

4. 配合类型注解

Python 3.11+ 支持对 *args**kwargs 进行类型注解:

from typing import Any

# 表示 args 全是 int,kwargs 的值全是 str
def collect(items: list[int], *args: int, **kwargs: str) -> None:
    pass

5. 小心参数名冲突(仅限 **kwargs

解包字典时,如果字典中的键(Key)与函数已定义的普通参数重名,会触发 TypeError

def test(name, age):
    pass

data = {'name': 'Bob', 'age': 25, 'gender': 'M'}
test(**data)  # 报错!因为 test 没有 'gender' 这个参数
# 解决方案:在函数内用 kwargs.pop('gender', None) 过滤掉

总结一句话

  • *args 管“位置”的零头(打包成元组),多用于处理不确定数量的数值列表。
  • **kwargs 管“名字”的零头(打包成字典),多用于处理配置项或扩展选项。
  • 星号 * 在定义时是“收集”,在调用时是“分发”,记住这个双向作用,就彻底掌握它们了。

补充:那Python中有没有指针呢?

Python 语言本身没有暴露像 C/C++ 中那种可以进行“指针运算”的显式指针类型(比如 int *p = &a; p++ 这种操作是不存在的)。

但是,“指针”的概念在 Python 底层无处不在。更准确的表述是:Python 中的一切变量本质上都是“对象的引用”。这个“引用”在底层就是通过指针(内存地址)来实现的。

1. 核心机制:

在 C 语言中,变量是一个内存盒子,赋值是把数据放进盒子里。
在 Python 中,变量是贴在对象上的“标签”。

a = 100
b = a  # 并不是拷贝了100,而是让 b 也指向 a 所指向的那个内存地址
print(id(a))  # id() 函数返回内存地址,比如 140735268397328
print(id(b))  # 输出和上面完全一样,说明 a 和 b 指向同一块内存

唯一的区别点:Python 不允许手动修改内存地址的值(解引用),只能通过变量名来操作对象。

2. 可变对象 vs 不可变对象

既然都是引用(指针),为什么有时修改一个变量会影响另一个,有时又不会?这取决于对象是否可变(Mutable)

  • 不可变对象(int, str, tuple):修改时,会新建一个对象,原对象不变。

    a = 10
    b = a
    a = a + 1  # 这里的 'a' 重新指向了新的整数 11
    print(b)   # 输出 10 (b 依然指向原来的整数 10,不受影响)
  • 可变对象(list, dict, set):修改内部元素时,不会新建对象,原对象被修改。

    a = [1, 2, 3]
    b = a         # b 和 a 指向同一个列表对象(指针复制)
    a.append(4)   # 修改了列表内部
    print(b)      # 输出 [1, 2, 3, 4]  —— 这就是“指针”效应

3. Python 中有没有显式操作内存地址的方法?

虽然不能像 C 那样操作指针运算,但 Python 标准库提供了极少数底层接口:

  1. id() 函数:获取对象的内存地址(十进制整数)。

  2. ctypes 模块:可以绕过 Python 的引用机制,强行修改对象内存(极度危险,仅限底层调试)。

    import ctypes
    a = "hello world"
    # 强制将 a 指向的内存地址中的引用计数变回 1(通常用于跨语言调用)
  3. ctypes.pointer():用于和 C 语言交互时创建真正的 C 指针。

4. 与C语言对比(结合*args 和 **kwargs)

对比项 C 语言 * Python *
含义 声明指针 / 解引用取值 收集多余位置参数 / 展开可迭代对象
例子 int *p = &a; *p = 10; func(*[1,2,3])

5. 企业级最佳实践(规避“指针副作用”)

既然 Python 的函数传参是“传引用(传指针)”,在大型项目中,函数内部修改外部列表是灾难性的。遵循以下规则:

  1. 默认参数绝不用可变对象(经典坑):

    # ❌ 错误:默认参数在定义时创建,后续调用会累积
    def add_item(item, lst=[]): 
       lst.append(item)
       return lst
    
    # ✅ 正确:使用 None 占位,内部新建
    def add_item(item, lst=None):
       if lst is None:
           lst = []
       lst.append(item)
       return lst
  2. 如果需要隔离影响,显式拷贝
    如果想把列表传给函数且不希望被修改时,传入副本:

    data = [1, 2, 3]
    process_data(data.copy())  # 或者 data[:]
  3. 类型注解辅助识别:在函数签名中标明返回的是新对象还是修改原对象,例如 def clean(data: list) -> list: 暗示返回新列表,不修改原列表。

一句话总结:Python 没有可以手动操作的“指针变量”,但它的“引用传递”本质上就是指针。因此不需要关心地址运算,但必须清楚“可变对象在赋值和传参时是共享内存的”,这是避免写出诡异 Bug 的关键。