1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活

上一篇我们学习了 Python 函数的位置参数、关键字参数和默认参数,知道了如何让函数调用更加灵活。

但是,在实际的 Linux 运维工作中,还有一种情况:我们并不知道调用函数时会传入多少个参数。

比如,我们想编写一个函数,统计多台服务器的 CPU 使用率平均值。今天可能只有 3 台服务器,明天可能就变成了 50 台。如果每增加一台服务器,就要修改一次函数定义,显然不够灵活。

再比如,我们希望编写一个函数,用来打印服务器配置信息。不同服务器的配置项可能不一样,有的需要指定端口,有的需要指定用户名,还有的需要设置连接超时时间。

遇到这些情况该怎么办?

Python 提供了两种非常实用的参数形式:*args 和 **kwargs。学会它们,就能让函数接收可变数量的位置参数和关键字参数。

为什么需要可变参数?

先看一个简单的函数:

1
2
3
4
def sum_numbers(a, b, c):
return a + b + c

print(sum_numbers(10, 20, 30))

运行结果:

1
60

这个函数只能接收三个参数。如果我们想计算四个数字的总和,就必须修改函数定义,或者重新编写另一个函数。

如果需要计算几十个数字呢?

显然,逐个定义参数并不现实。

我们真正需要的是:无论调用函数时传入多少个数字,都能使用同一个函数进行处理。

这就是可变参数的用途。

使用 args 接收任意数量的位置参数

在 Python 中,可以在参数名前添加一个星号 *,让函数接收任意数量的位置参数。

按照 Python 的惯例,这类参数通常命名为 args,因此我们经常看到 *args 这种写法。

注意,args 只是一个普通的变量名,并不是 Python 的保留字。真正起作用的是前面的星号 *。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
def sum_all(*args):
total = 0

for n in args:
total += n

return total

print(sum_all(1, 2, 3))
print(sum_all(1, 2, 3, 4, 5))
print(sum_all(10, 20))

运行结果:

1
2
3
6
15
30

同一个函数,接收了不同数量的参数,却都能正常完成计算。

这是怎么实现的?

当我们调用 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
2
3
4
5
6
7
def show_servers(*args):
print(f"一共接收到 {len(args)} 台服务器")

for server in args:
print(f"服务器名称:{server}")

show_servers("web-01", "web-02", "db-01")

运行结果:

1
2
3
4
一共接收到 3 台服务器
服务器名称:web-01
服务器名称:web-02
服务器名称:db-01

在实际运维中,这种方式可以用于批量处理服务器名称、统计多个指标,或者对一组输入数据执行相同的操作。

当然,*args 本身并不会自动连接服务器或采集监控指标。它只负责接收参数,具体业务仍然需要我们编写代码实现。

使用 kwargs 接收任意数量的关键字参数

前面学习的 *args 解决了位置参数数量不固定的问题。

但如果传入的不是一组数字,而是不同名称的配置项呢?

例如,我们想打印服务器的地址、SSH 端口、用户名和连接超时时间。不同服务器可能具有不同的配置项,难道每增加一个配置项,就必须修改函数定义吗?

这时候就可以使用 **kwargs。

在参数名前添加两个星号 **,Python 就会将额外的关键字参数收集到一个字典中。

按照惯例,这个参数通常命名为 kwargs。同样,kwargs 不是保留字,真正起作用的是 **。

看下面的例子:

1
2
3
4
5
6
7
8
9
def print_config(**kwargs):
for key, value in kwargs.items():
print(f"{key} = {value}")

print_config(
host="192.168.1.10",
port=22,
user="root"
)

运行结果:

1
2
3
host = 192.168.1.10
port = 22
user = root

调用函数时,我们传入了三个关键字参数:

  • host 表示服务器地址。
  • port 表示 SSH 端口。
  • user 表示登录用户名。

在函数内部,这些参数会被收集成一个字典:

1
2
3
4
5
kwargs = {
"host": "192.168.1.10",
"port": 22,
"user": "root"
}

因此,我们可以使用字典的方法来处理它们,例如 items()、keys() 和 values()。

再看一个例子:

1
2
3
4
5
6
7
def print_config(**kwargs):
for key, value in kwargs.items():
print(f"{key} = {value}")

print_config(host="192.168.1.10", port=22)
print("---")
print_config(host="192.168.1.20", port=2222, timeout=30, user="admin")

运行结果:

1
2
3
4
5
6
7
host = 192.168.1.10
port = 22
---
host = 192.168.1.20
port = 2222
timeout = 30
user = admin

可以看到,两次调用传入的参数数量不同,配置项也不完全相同,但函数不需要为此修改定义。

这就是 **kwargs 的优势:它能够接收数量不固定、名称也不固定的关键字参数。

需要注意的是,**kwargs 只负责收集数据。如果函数需要验证端口是否为合法整数,或者检查用户名是否符合要求,还需要额外编写验证逻辑。

args 和 kwargs 有什么区别?

到这里,我们已经认识了两种可变参数。它们的区别可以用一张表概括:

对比项*args**kwargs
接收的参数额外的位置参数额外的关键字参数
函数内部类型元组 tuple字典 dict
参数示例1, 2, 3host="192.168.1.10"
常见用途批量处理数值、列表项等接收配置项、选项等

可以简单记忆:

  • 一个星号 *:收集额外的位置参数,形成元组。
  • 两个星号 **:收集额外的关键字参数,形成字典。

但它们并不是只能单独使用。实际编写函数时,我们还可以把普通参数、默认参数和可变参数组合起来。

在一个函数中同时使用 args 和 kwargs

看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
def show_request(method, *args, **kwargs):
print(f"请求方法:{method}")
print(f"位置参数:{args}")
print(f"关键字参数:{kwargs}")

show_request(
"GET",
"/api/servers",
"verbose",
timeout=10,
verify_tls=True
)

运行结果:

1
2
3
请求方法:GET
位置参数:('/api/servers', 'verbose')
关键字参数:{'timeout': 10, 'verify_tls': True}

这里发生了什么?

  • method 接收第一个位置参数 "GET"。
  • *args 收集剩余的位置参数,形成元组。
  • **kwargs 收集关键字参数,形成字典。

这种写法在封装 API 调用、编写通用工具函数时很有用。不过,参数能够被接收,并不意味着函数会自动理解这些参数的含义。我们仍然需要根据实际需求处理它们。

参数解包:把已有的元组和字典传给函数

前面我们学习了如何使用 *args 和 **kwargs 接收参数。

现在换个方向:如果我们已经有一个元组或字典,想把其中的数据传给函数,该怎么办?

这就要用到参数解包。

使用 * 解包元组或列表

假设有一个函数:

1
2
3
4
def show_server(host, port, timeout):
print(f"服务器地址:{host}")
print(f"SSH 端口:{port}")
print(f"连接超时:{timeout} 秒")

正常情况下,我们可以这样调用:

1
show_server("192.168.1.10", 22, 10)

输出

1
2
3
服务器地址:192.168.1.10
SSH 端口:22
连接超时:10 秒

但如果这些参数已经存放在一个元组中呢?

1
2
3
args = ("192.168.1.10", 2222, 30)

show_server(*args)

运行结果:

1
2
3
服务器地址:192.168.1.10
SSH 端口:2222
连接超时:30 秒

这里的 *args 并不是在定义函数参数,而是在调用函数时进行解包。

Python 会把元组中的三个元素分别作为位置参数传入,相当于执行:

1
show_server("192.168.1.10", 2222, 30)

输出

1
2
3
服务器地址:192.168.1.10
SSH 端口:2222
连接超时:10 秒

同样,列表也可以使用 * 解包:

1
2
3
args = ["192.168.1.20", 22, 10]

show_server(*args)

输出

1
2
3
服务器地址:192.168.1.20
SSH 端口:2222
连接超时:10 秒

只要解包后的参数数量和函数所需的参数能够正确匹配,就可以正常调用。

使用 ** 解包字典

如果参数已经存放在字典中,就可以使用 ** 解包:

1
2
3
4
5
6
7
8
9
10
11
12
def show_server(host, port, timeout):
print(f"服务器地址:{host}")
print(f"SSH 端口:{port}")
print(f"连接超时:{timeout} 秒")

config = {
"host": "192.168.1.30",
"port": 2200,
"timeout": 20
}

show_server(**config)

运行结果:

1
2
3
服务器地址:192.168.1.30
SSH 端口:2200
连接超时:20 秒

Python 会把字典中的键作为参数名,把对应的值作为参数值。

因此:

1
show_server(**config)

相当于:

1
2
3
4
5
show_server(
host="192.168.1.30",
port=2200,
timeout=20
)

这在运维自动化中非常实用。例如,我们可以先从配置文件或 API 响应中读取服务器配置,再将字典解包后传给函数。

不过要注意,字典中的键必须与函数能够接收的参数名匹配。如果出现无法匹配的参数名,或者缺少必需参数,就可能触发 TypeError。

同时解包元组和字典

位置参数解包和关键字参数解包也可以一起使用。

例如:

1
2
3
4
5
6
7
8
9
10
def connect(host, port, user, timeout):
print(host, port, user, timeout)

args = ("192.168.1.10", 2222)
kwargs = {
"user": "root",
"timeout": 30
}

connect(*args, **kwargs)

运行结果:

1
192.168.1.10 2222 root 30

这里,*args 提供 host 和 port,**kwargs 提供 user 和 timeout。

这比手动提取每个值再逐个传入函数更加简洁。

需要注意:解包后仍然必须满足函数的参数绑定规则,不能让同一个参数同时从位置参数和关键字参数获得两个值。

综合实战:灵活打印服务器配置

最后,我们把刚才学习的内容组合起来,编写一个可以接收不同数量服务器名称和额外配置项的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def show_servers(*servers, **config):
print("服务器列表:")

for server in servers:
print(f"- {server}")

print("\n连接配置:")

for key, value in config.items():
print(f"{key} = {value}")

show_servers(
"web-01",
"web-02",
"db-01",
port=22,
user="root",
timeout=10
)

运行结果:

1
2
3
4
5
6
7
8
9
服务器列表:
- web-01
- web-02
- db-01

连接配置:
port = 22
user = root
timeout = 10

在这个函数中:

  • *servers 接收任意数量的位置参数,用来表示服务器名称。
  • **config 接收任意数量的关键字参数,用来表示连接配置。

如果以后需要处理更多服务器,或者增加 retry、verify_tls 等配置项,就可以直接在调用时传入,而不必频繁修改函数的参数定义。

当然,这个函数只是展示参数接收与处理,并没有真正建立连接。实际项目中,还需要根据配置实现服务器连接、错误处理和结果记录等功能。

写在最后

通过这篇文章,我们学习了 Python 中两种常见的可变参数:

  • *args:接收任意数量的位置参数,在函数内部形成元组。
  • **kwargs:接收任意数量的关键字参数,在函数内部形成字典。
  • * 和 ** 也可以用于函数调用时的参数解包,将已有的列表、元组或字典传递给函数。

在实际项目中,可变参数能够让函数更灵活,尤其适合通用工具函数、API 封装和自动化任务。但也不要为了灵活而滥用它们:如果函数只需要固定的几个参数,直接定义明确的参数通常更容易理解和维护。

函数参数学到这里,又向前迈进了一步。

不过,还有一个问题值得继续研究:为什么有些变量只能在函数内部使用,而有些变量却可以在函数外部访问?如果函数内部定义了一个和外部同名的变量,会发生什么?

下一篇,我们将学习 Python 的变量作用域,弄清楚局部变量、全局变量,以及 global 和 nonlocal 的使用方法。