可变参数详解(不是指针哦)
可变参数*args 和**kwargs 有什么区别?
核心区别:*args 接收的是位置参数(Positional Arguments),而 **kwargs 接收的是关键字参数(Keyword Arguments)
args 和 kwargs 只是约定俗成的名称,关键在于前面的 * 和 **,它们在底层的数据类型、使用场景及函数调用方式上有着本质不同。
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 标准库提供了极少数底层接口:
-
id()函数:获取对象的内存地址(十进制整数)。 -
ctypes模块:可以绕过 Python 的引用机制,强行修改对象内存(极度危险,仅限底层调试)。import ctypes a = "hello world" # 强制将 a 指向的内存地址中的引用计数变回 1(通常用于跨语言调用) -
ctypes.pointer():用于和 C 语言交互时创建真正的 C 指针。
4. 与C语言对比(结合*args 和 **kwargs)
| 对比项 | C 语言 * |
Python * |
|---|---|---|
| 含义 | 声明指针 / 解引用取值 | 收集多余位置参数 / 展开可迭代对象 |
| 例子 | int *p = &a; *p = 10; |
func(*[1,2,3]) |
5. 企业级最佳实践(规避“指针副作用”)
既然 Python 的函数传参是“传引用(传指针)”,在大型项目中,函数内部修改外部列表是灾难性的。遵循以下规则:
-
默认参数绝不用可变对象(经典坑):
# ❌ 错误:默认参数在定义时创建,后续调用会累积 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 -
如果需要隔离影响,显式拷贝:
如果想把列表传给函数且不希望被修改时,传入副本:data = [1, 2, 3] process_data(data.copy()) # 或者 data[:] -
类型注解辅助识别:在函数签名中标明返回的是新对象还是修改原对象,例如
def clean(data: list) -> list:暗示返回新列表,不修改原列表。
一句话总结:Python 没有可以手动操作的“指针变量”,但它的“引用传递”本质上就是指针。因此不需要关心地址运算,但必须清楚“可变对象在赋值和传参时是共享内存的”,这是避免写出诡异 Bug 的关键。