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 自定义函数,了解了如何使用 def 定义函数、通过参数向函数传递数据,以及使用 return 返回处理结果。

但是,函数写好了,调用的时候还有讲究。

比如,我们编写了一个用于连接 Linux 服务器的函数,需要指定服务器地址、SSH 端口和连接超时时间。难道每次连接都必须把所有参数写一遍吗?如果函数有好几个参数,调用时又该如何让代码更容易阅读?

这篇文章,我们就来学习 Python 的位置参数、关键字参数和默认参数,让函数调用更加灵活,也为后续学习 *args 和 **kwargs 打好基础。

先从一个简单的函数说起

假设我们正在编写一个用于生成问候语的函数:

1
2
def greet(name, greeting):
print(f"{greeting}, {name}")

调用函数时,需要提供两个参数:

1
2
greet("Alice", "Hello")
greet("Bob", "Hi")

运行结果:

1
2
Hello, Alice
Hi, Bob

这里有一个问题:调用 greet() 时,必须记住第一个参数是姓名,第二个参数是问候语。如果参数比较多,调用时就容易弄混。

有没有办法让调用代码更直观一些?

当然有!这就要从 Python 的参数传递方式说起。

位置参数:按照顺序传递数据

先看下面这段代码:

1
2
3
4
def greet(name, greeting):
print(f"{greeting}, {name}")

greet("Alice", "Hello")

输出

1
Hello, Alice

定义函数时,name 和 greeting 是形式参数。

调用函数时,"Alice" 和 "Hello" 是实际传入的数据。

当我们使用位置参数调用函数时,Python 会按照参数的位置进行匹配:

flowchart TD
    A["调用 greet('Alice', 'Hello')"] --> B["第一个参数 Alice"]
    A --> C["第二个参数 Hello"]
    B --> D["name = Alice"]
    C --> E["greeting = Hello"]
    D --> F["执行函数体"]
    E --> F
    F --> G["输出 Hello, Alice"]

位置参数的优点是简单直接,适合参数含义明确、数量不多的场景。

但如果参数比较多,只看调用代码可能就不那么容易理解了。

例如:

1
2
3
4
5
6
def connect(host, port, timeout):
print(f"连接地址:{host}")
print(f"SSH 端口:{port}")
print(f"超时时间:{timeout} 秒")

connect("192.168.1.10", 2222, 30)

输出

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

看到 2222 和 30,我们可能需要回头查看函数定义,才能确定它们分别代表什么。

这时候,就可以使用关键字参数。

关键字参数:让调用代码更加清晰

关键字参数也常被称为命名参数。它的使用方法很简单:调用函数时,通过参数名明确指定对应的值。

例如:

1
2
3
4
5
def greet(name, greeting):
print(f"{greeting}, {name}")

greet(name="Alice", greeting="Hello")
greet(greeting="Hi", name="Bob")

运行结果:

1
2
Hello, Alice
Hi, Bob

注意第二次调用:

1
greet(greeting="Hi", name="Bob")

虽然参数顺序与函数定义不同,但程序仍然能够正确执行。

这是因为关键字参数通过参数名称进行匹配,而不是依靠位置。

再回到连接 Linux 服务器的例子:

1
2
3
4
5
6
def connect(host, port, timeout):
print(f"连接地址:{host}")
print(f"SSH 端口:{port}")
print(f"超时时间:{timeout} 秒")

connect(host="192.168.1.10", port=2222, timeout=30)

输出

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

这次即使不查看函数定义,也很容易看出每个参数的含义。

对于参数较多的函数,关键字参数通常能让调用代码更容易阅读和维护。

位置参数和关键字参数可以混合使用吗?

可以,但需要遵守规则。

位置参数必须放在关键字参数之前。例如:

1
2
3
4
def greet(name, greeting):
print(f"{greeting}, {name}")

greet("Alice", greeting="Hello")

输出

1
Hello, Alice

这段代码是合法的。

但是,下面这种写法就不行:

1
greet(name="Alice", "Hello")

因为位置参数 "Hello" 出现在关键字参数之后,Python 会报 SyntaxError。

1
2
3
4
  File "<python-input-107>", line 1
greet(name="Alice", "Hello")
^
SyntaxError: positional argument follows keyword argument

记住一个简单的规则:混合使用时,先写位置参数,再写关键字参数。

默认参数:让函数调用更灵活

在 Linux 运维工作中,我们经常会遇到这样的情况:连接 SSH 服务时,大多数服务器使用默认的 22 端口,只有少数服务器修改过端口。

如果每次调用函数都必须填写端口号,就会产生很多重复代码。

能不能让函数提供一个默认端口,只有特殊情况下才手动指定?

当然可以,这就是默认参数。

看下面的例子:

1
2
3
4
5
6
def connect(host, port=22, timeout=10):
print(f"连接地址:{host}")
print(f"SSH 端口:{port}")
print(f"超时时间:{timeout} 秒")

connect("192.168.1.10")

运行结果:

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

定义函数时,我们给 port 设置了默认值 22,给 timeout 设置了默认值 10。

因此,调用 connect() 时,只需要提供必需的 host 参数,其他两个参数就可以省略。

如果遇到使用非标准 SSH 端口的服务器,也可以单独指定:

1
connect("192.168.1.20", port=2222)

运行结果:

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

这里发生了什么?

  • host 没有默认值,因此调用时必须提供。
  • port 有默认值 22,但调用时传入了 2222,所以使用传入的值。
  • timeout 有默认值 10,调用时没有指定,因此继续使用默认值。

默认参数让函数既能应对常见场景,也能满足特殊需求。

默认参数必须放在什么位置?

Python 对默认参数的位置有一个要求:在普通函数定义中,有默认值的参数必须放在没有默认值的参数之后。

例如,下面的定义是正确的:

1
2
def connect(host, port=22, timeout=10):
print(host, port, timeout)

而下面的定义会报 SyntaxError:

1
2
def connect(port=22, host):
print(host, port)

输出

1
2
3
4
  File "<python-input-111>", line 1
def connect(port=22, host):
^^^^
SyntaxError: parameter without a default follows parameter with a default

因为 port 已经有默认值,后面的 host 却没有默认值。

如果确实需要这样的参数组合,就应该调整顺序:

1
2
def connect(host, port=22):
print(host, port)

这样就符合 Python 的语法规则了。

默认参数需要注意的一个坑:可变对象

前面我们给参数设置的默认值都是数字,例如 22 和 10。

但是,如果默认值是列表、字典等可变对象,就需要格外小心。

看下面这个例子:

1
2
3
4
5
6
7
def add_server(server, servers=[]):
servers.append(server)
return servers

print(add_server("web-01"))
print(add_server("web-02"))
print(add_server("web-03"))

你可能会认为,每次调用 add_server() 时,servers 都会自动变成一个新的空列表。

但实际输出是:

1
2
3
['web-01']
['web-01', 'web-02']
['web-01', 'web-02', 'web-03']

为什么会这样?

因为 Python 在执行 def 语句、创建函数时,就会计算默认参数的值。后续调用函数时,如果没有传入 servers,就会继续使用同一个默认列表,而不是每次创建一个新列表。

于是,每次调用都会向同一个列表追加服务器名称。

在某些情况下,这种共享状态可能是有意设计的;但在一般业务代码中,它往往不是我们想要的结果。

更稳妥的写法是使用 None 作为默认值,在函数内部创建新列表:

1
2
3
4
5
6
7
8
9
10
def add_server(server, servers=None):
if servers is None:
servers = []

servers.append(server)
return servers

print(add_server("web-01"))
print(add_server("web-02"))
print(add_server("web-03"))

运行结果:

1
2
3
['web-01']
['web-02']
['web-03']

这样每次不传入 servers 时,函数都会创建一个新的列表,避免意外共享数据。

如果调用者主动传入一个已有列表,函数仍然会向这个列表追加数据:

1
2
3
4
5
servers = ["web-01"]

result = add_server("web-02", servers)

print(result)

运行结果:

1
['web-01', 'web-02']

这里需要注意:传入的列表本身被修改了。如果不希望调用者传入的列表发生变化,还需要在函数内部复制列表,再进行操作。

综合实战:编写更灵活的服务器连接配置函数

前面我们分别学习了位置参数、关键字参数和默认参数。现在把它们组合起来,编写一个简单的服务器连接配置函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def show_ssh_config(host, port=22, timeout=10):
print("=" * 30)
print(f"服务器地址:{host}")
print(f"SSH 端口:{port}")
print(f"连接超时:{timeout} 秒")
print("=" * 30)

# 使用默认端口和默认超时时间
show_ssh_config("192.168.1.10")

# 修改 SSH 端口
show_ssh_config("192.168.1.20", port=2222)

# 同时修改端口和超时时间
show_ssh_config(
host="192.168.1.30",
port=2200,
timeout=30
)

输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
==============================
服务器地址:192.168.1.10
SSH 端口:22
连接超时:10 秒
==============================
==============================
服务器地址:192.168.1.20
SSH 端口:2222
连接超时:10 秒
==============================
==============================
服务器地址:192.168.1.30
SSH 端口:2200
连接超时:30 秒
==============================

这段代码演示了三种不同的调用方式:

  • 第一次调用:只提供服务器地址,其余参数使用默认值。
  • 第二次调用:只修改端口,超时时间仍然使用默认值。
  • 第三次调用:使用关键字参数明确指定所有参数。

在实际的运维自动化项目中,这种设计方式很常见。例如,封装 API 请求、数据库连接、SSH 连接配置时,都可以为常用参数设置合理的默认值,同时允许调用者根据需要覆盖它们。

当然,这个例子只是打印连接配置,并没有真正建立 SSH 连接。实际连接还需要使用相应的 SSH 库,并处理认证、超时和连接异常等问题。

写在最后

学到这里,我们已经掌握了 Python 函数的几种常见参数传递方式:

参数类型主要特点
位置参数按照参数位置传递值
关键字参数通过参数名称指定值,调用更清晰
默认参数为参数提供默认值,调用时可以省略
可变对象默认参数需要注意默认对象可能被多次调用共享

实际编写函数时,可以根据场景灵活组合这些方式:必需的信息使用普通参数,常用配置提供默认值,参数较多时使用关键字参数提高可读性。

不过,还有一个问题:如果我们事先不知道调用者会传入多少个参数,该怎么办?

例如,我们想编写一个函数,一次接收任意数量的服务器名称;或者接收不同数量的配置项,又该如何处理?

下一篇,我们就来学习 Python 中的 *args 和 **kwargs,看看函数如何接收可变数量的参数。