Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
1 | 作者:李晓辉 |
- Python 系列(一) 从零开始认识 Python,入门其实没那么难
- Python 系列(二) 学 Python 的第一步,把开发环境搭建好
- Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
- Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
- Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
- Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
- Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
- Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
- Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
- Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
- Python 系列(十一) list、tuple、set、dict,到底该怎么选?
- Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
- Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
- Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
- Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
- Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
- Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
- Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
- Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
上一篇我们学习了 Python 函数的位置参数、关键字参数和默认参数,知道了如何让函数调用更加灵活。
但是,在实际的 Linux 运维工作中,还有一种情况:我们并不知道调用函数时会传入多少个参数。
比如,我们想编写一个函数,统计多台服务器的 CPU 使用率平均值。今天可能只有 3 台服务器,明天可能就变成了 50 台。如果每增加一台服务器,就要修改一次函数定义,显然不够灵活。
再比如,我们希望编写一个函数,用来打印服务器配置信息。不同服务器的配置项可能不一样,有的需要指定端口,有的需要指定用户名,还有的需要设置连接超时时间。
遇到这些情况该怎么办?
Python 提供了两种非常实用的参数形式:
*args和**kwargs。学会它们,就能让函数接收可变数量的位置参数和关键字参数。
为什么需要可变参数?
先看一个简单的函数:
1 | def sum_numbers(a, b, c): |
运行结果:
1 | 60 |
这个函数只能接收三个参数。如果我们想计算四个数字的总和,就必须修改函数定义,或者重新编写另一个函数。
如果需要计算几十个数字呢?
显然,逐个定义参数并不现实。
我们真正需要的是:无论调用函数时传入多少个数字,都能使用同一个函数进行处理。
这就是可变参数的用途。
使用 args 接收任意数量的位置参数
在 Python 中,可以在参数名前添加一个星号 *,让函数接收任意数量的位置参数。
按照 Python 的惯例,这类参数通常命名为 args,因此我们经常看到 *args 这种写法。
注意,args 只是一个普通的变量名,并不是 Python 的保留字。真正起作用的是前面的星号 *。
看下面的例子:
1 | def sum_all(*args): |
运行结果:
1 | 6 |
同一个函数,接收了不同数量的参数,却都能正常完成计算。
这是怎么实现的?
当我们调用 sum_all(1, 2, 3) 时,Python 会把这些额外的位置参数收集到一个元组中。
也就是说,在函数内部:
1 | args = (1, 2, 3) |
当我们调用 sum_all(1, 2, 3, 4, 5) 时,函数内部对应的元组则是:
1 | args = (1, 2, 3, 4, 5) |
可以用下面的流程理解这个过程:
flowchart LR
A["调用 sum_all(1, 2, 3)"] --> B["收集位置参数"]
B --> C["args = (1, 2, 3)"]
C --> D["遍历元组中的数字"]
D --> E["累加计算"]
E --> F["return 返回总和"]
F --> G["输出计算结果"]这里有一个关键点:*args 在函数内部接收到的是元组,而不是列表。
因此,我们可以遍历 args,也可以使用 len(args) 获取参数数量。
例如:
1 | def show_servers(*args): |
运行结果:
1 | 一共接收到 3 台服务器 |
在实际运维中,这种方式可以用于批量处理服务器名称、统计多个指标,或者对一组输入数据执行相同的操作。
当然,*args 本身并不会自动连接服务器或采集监控指标。它只负责接收参数,具体业务仍然需要我们编写代码实现。
使用 kwargs 接收任意数量的关键字参数
前面学习的 *args 解决了位置参数数量不固定的问题。
但如果传入的不是一组数字,而是不同名称的配置项呢?
例如,我们想打印服务器的地址、SSH 端口、用户名和连接超时时间。不同服务器可能具有不同的配置项,难道每增加一个配置项,就必须修改函数定义吗?
这时候就可以使用 **kwargs。
在参数名前添加两个星号 **,Python 就会将额外的关键字参数收集到一个字典中。
按照惯例,这个参数通常命名为 kwargs。同样,kwargs 不是保留字,真正起作用的是 **。
看下面的例子:
1 | def print_config(**kwargs): |
运行结果:
1 | host = 192.168.1.10 |
调用函数时,我们传入了三个关键字参数:
host表示服务器地址。port表示 SSH 端口。user表示登录用户名。
在函数内部,这些参数会被收集成一个字典:
1 | kwargs = { |
因此,我们可以使用字典的方法来处理它们,例如 items()、keys() 和 values()。
再看一个例子:
1 | def print_config(**kwargs): |
运行结果:
1 | host = 192.168.1.10 |
可以看到,两次调用传入的参数数量不同,配置项也不完全相同,但函数不需要为此修改定义。
这就是 **kwargs 的优势:它能够接收数量不固定、名称也不固定的关键字参数。
需要注意的是,**kwargs 只负责收集数据。如果函数需要验证端口是否为合法整数,或者检查用户名是否符合要求,还需要额外编写验证逻辑。
args 和 kwargs 有什么区别?
到这里,我们已经认识了两种可变参数。它们的区别可以用一张表概括:
| 对比项 | *args | **kwargs |
|---|---|---|
| 接收的参数 | 额外的位置参数 | 额外的关键字参数 |
| 函数内部类型 | 元组 tuple | 字典 dict |
| 参数示例 | 1, 2, 3 | host="192.168.1.10" |
| 常见用途 | 批量处理数值、列表项等 | 接收配置项、选项等 |
可以简单记忆:
- 一个星号
*:收集额外的位置参数,形成元组。 - 两个星号
**:收集额外的关键字参数,形成字典。
但它们并不是只能单独使用。实际编写函数时,我们还可以把普通参数、默认参数和可变参数组合起来。
在一个函数中同时使用 args 和 kwargs
看下面这个例子:
1 | def show_request(method, *args, **kwargs): |
运行结果:
1 | 请求方法:GET |
这里发生了什么?
method接收第一个位置参数"GET"。*args收集剩余的位置参数,形成元组。**kwargs收集关键字参数,形成字典。
这种写法在封装 API 调用、编写通用工具函数时很有用。不过,参数能够被接收,并不意味着函数会自动理解这些参数的含义。我们仍然需要根据实际需求处理它们。
参数解包:把已有的元组和字典传给函数
前面我们学习了如何使用 *args 和 **kwargs 接收参数。
现在换个方向:如果我们已经有一个元组或字典,想把其中的数据传给函数,该怎么办?
这就要用到参数解包。
使用 * 解包元组或列表
假设有一个函数:
1 | def show_server(host, port, timeout): |
正常情况下,我们可以这样调用:
1 | show_server("192.168.1.10", 22, 10) |
输出
1 | 服务器地址:192.168.1.10 |
但如果这些参数已经存放在一个元组中呢?
1 | args = ("192.168.1.10", 2222, 30) |
运行结果:
1 | 服务器地址:192.168.1.10 |
这里的 *args 并不是在定义函数参数,而是在调用函数时进行解包。
Python 会把元组中的三个元素分别作为位置参数传入,相当于执行:
1 | show_server("192.168.1.10", 2222, 30) |
输出
1 | 服务器地址:192.168.1.10 |
同样,列表也可以使用 * 解包:
1 | args = ["192.168.1.20", 22, 10] |
输出
1 | 服务器地址:192.168.1.20 |
只要解包后的参数数量和函数所需的参数能够正确匹配,就可以正常调用。
使用 ** 解包字典
如果参数已经存放在字典中,就可以使用 ** 解包:
1 | def show_server(host, port, timeout): |
运行结果:
1 | 服务器地址:192.168.1.30 |
Python 会把字典中的键作为参数名,把对应的值作为参数值。
因此:
1 | show_server(**config) |
相当于:
1 | show_server( |
这在运维自动化中非常实用。例如,我们可以先从配置文件或 API 响应中读取服务器配置,再将字典解包后传给函数。
不过要注意,字典中的键必须与函数能够接收的参数名匹配。如果出现无法匹配的参数名,或者缺少必需参数,就可能触发 TypeError。
同时解包元组和字典
位置参数解包和关键字参数解包也可以一起使用。
例如:
1 | def connect(host, port, user, timeout): |
运行结果:
1 | 192.168.1.10 2222 root 30 |
这里,*args 提供 host 和 port,**kwargs 提供 user 和 timeout。
这比手动提取每个值再逐个传入函数更加简洁。
需要注意:解包后仍然必须满足函数的参数绑定规则,不能让同一个参数同时从位置参数和关键字参数获得两个值。
综合实战:灵活打印服务器配置
最后,我们把刚才学习的内容组合起来,编写一个可以接收不同数量服务器名称和额外配置项的函数。
1 | def show_servers(*servers, **config): |
运行结果:
1 | 服务器列表: |
在这个函数中:
*servers接收任意数量的位置参数,用来表示服务器名称。**config接收任意数量的关键字参数,用来表示连接配置。
如果以后需要处理更多服务器,或者增加 retry、verify_tls 等配置项,就可以直接在调用时传入,而不必频繁修改函数的参数定义。
当然,这个函数只是展示参数接收与处理,并没有真正建立连接。实际项目中,还需要根据配置实现服务器连接、错误处理和结果记录等功能。
写在最后
通过这篇文章,我们学习了 Python 中两种常见的可变参数:
*args:接收任意数量的位置参数,在函数内部形成元组。**kwargs:接收任意数量的关键字参数,在函数内部形成字典。*和**也可以用于函数调用时的参数解包,将已有的列表、元组或字典传递给函数。
在实际项目中,可变参数能够让函数更灵活,尤其适合通用工具函数、API 封装和自动化任务。但也不要为了灵活而滥用它们:如果函数只需要固定的几个参数,直接定义明确的参数通常更容易理解和维护。
函数参数学到这里,又向前迈进了一步。
不过,还有一个问题值得继续研究:为什么有些变量只能在函数内部使用,而有些变量却可以在函数外部访问?如果函数内部定义了一个和外部同名的变量,会发生什么?
下一篇,我们将学习 Python 的变量作用域,弄清楚局部变量、全局变量,以及 global 和 nonlocal 的使用方法。
