
Python 函数参数分隔符 *:Keyword-Only Arguments 原理与实践
- 一、原理与实践
-
- 1、引言
- [2、从 `*args` 到分隔符:`*` 的双重身份](#2、从
*args到分隔符:*的双重身份) -
- [2.1 、打包参数:`*args`](#2.1 、打包参数:
*args) - [2.2 、分隔参数:裸 `*`](#2.2 、分隔参数:裸
*)
- [2.1 、打包参数:`*args`](#2.1 、打包参数:
- [3、 Keyword-Only Arguments 的语法规则](#3、 Keyword-Only Arguments 的语法规则)
-
- [3.1、 基本形式](#3.1、 基本形式)
- [3.2 、与默认值结合](#3.2 、与默认值结合)
- [3.3 、与 `**kwargs` 结合](#3.3 、与
**kwargs结合)
- [4、 为什么需要 Keyword-Only Arguments?](#4、 为什么需要 Keyword-Only Arguments?)
-
- [4.1、 提升代码可读性](#4.1、 提升代码可读性)
- [4.2 、避免参数顺序错误](#4.2 、避免参数顺序错误)
- [4.3、 为未来扩展预留空间](#4.3、 为未来扩展预留空间)
- [4.4、 配合 `*args` 使用](#4.4、 配合
*args使用)
- [5、 深入原理:Python 如何解析参数](#5、 深入原理:Python 如何解析参数)
-
- [5.1、 参数解析的两阶段模型](#5.1、 参数解析的两阶段模型)
- [5.2、 字节码视角](#5.2、 字节码视角)
- [5.3 、与 `inspect` 模块的交互](#5.3 、与
inspect模块的交互)
- [6、 实践案例](#6、 实践案例)
-
- [6.1、 配置类函数](#6.1、 配置类函数)
- [6.2 、API 封装](#6.2 、API 封装)
- [6.3、 数据校验函数](#6.3、 数据校验函数)
- 7、常见误区与注意事项
-
- [7.1 、`*` 之后不能再有位置参数](#7.1 、
*之后不能再有位置参数) - [7.2、 不要与解包混淆](#7.2、 不要与解包混淆)
- [7.3 、与位置限定符 `/` 的对比](#7.3 、与位置限定符
/的对比)
- [7.1 、`*` 之后不能再有位置参数](#7.1 、
- [8、 总结](#8、 总结)
- 二、代码示例

一、原理与实践
1、引言
在 Python 的函数定义中,* 是一个容易被忽视却又极其重要的符号。很多初学者第一次见到它,是在 *args 可变参数中;但单独出现在参数列表中间的 *,则扮演着完全不同的角色------它将后面的所有参数强制变为「仅限关键字参数」(Keyword-Only Arguments)。
本文将深入剖析这个分隔符的工作原理、使用场景与最佳实践,帮助你彻底掌握这一语言特性。
2、从 *args 到分隔符:* 的双重身份
要理解 Keyword-Only Arguments,首先需要厘清 * 在函数定义中的两种用法。
2.1 、打包参数:*args
当 * 后面紧跟一个变量名时,它负责收集所有多余的位置参数,打包成元组:
python
def func(*args):
print(args)
func(1, 2, 3) # 输出: (1, 2, 3)
2.2 、分隔参数:裸 *
当 * 单独出现、后面不跟变量名时,它不收集任何参数,只作为一个「分界线」:
python
def func(a, *, b):
print(a, b)
func(1, b=2) # 正确
func(1, 2) # TypeError: func() takes 1 positional argument but 2 were given
此时,* 之后的所有参数(如 b)只能通过关键字传递,无法再按位置传入。
3、 Keyword-Only Arguments 的语法规则
3.1、 基本形式
python
def func(a, *, b, c):
pass
a:普通位置参数,可位置传参,也可关键字传参。b、c:Keyword-Only 参数,只能以keyword=value形式传入。
3.2 、与默认值结合
Keyword-Only 参数同样支持默认值,且不要求放在无默认值参数之后(这与普通位置参数不同):
python
def func(a, *, b=10, c=20):
print(a, b, c)
func(1) # 输出: 1 10 20
func(1, b=2) # 输出: 1 2 20
func(1, c=3) # 输出: 1 10 3
3.3 、与 **kwargs 结合
* 分隔符可以与 **kwargs 同时使用,此时 * 之后、**kwargs 之前的参数是显式声明的 Keyword-Only 参数:
python
def func(a, *, b, **kwargs):
print(a, b, kwargs)
func(1, b=2, c=3, d=4) # 输出: 1 2 {'c': 3, 'd': 4}
4、 为什么需要 Keyword-Only Arguments?
4.1、 提升代码可读性
当函数参数较多且含义容易混淆时,强制关键字传参能让调用意图一目了然:
python
# 不推荐:位置参数过多,调用时难以分辨
def create_user(name, age, city, phone):
pass
create_user("张三", 25, "北京", "13800000000")
# 推荐:关键信息用 Keyword-Only 强制声明
def create_user(name, *, age, city, phone):
pass
create_user("张三", age=25, city="北京", phone="13800000000")
4.2 、避免参数顺序错误
位置参数一旦顺序写错,程序不会报错,但会产生难以察觉的逻辑 bug。强制关键字传参可以从语法层面杜绝这类问题。
4.3、 为未来扩展预留空间
在函数签名中插入 *,可以在不破坏现有调用方式的前提下,后续安全地新增 Keyword-Only 参数。
4.4、 配合 *args 使用
当函数同时需要可变位置参数和具名可选参数时,* 分隔符几乎是必需品:
python
def log(level, *messages, timestamp=None):
print(f"[{level}]", *messages, timestamp or "")
log("INFO", "hello", "world", timestamp="2026-01-01")
5、 深入原理:Python 如何解析参数
5.1、 参数解析的两阶段模型
CPython 在调用函数时,对参数的处理分为两个阶段:
- 位置参数匹配 :按顺序将实参绑定到形参,遇到
*分隔符时停止。 - 关键字参数匹配 :将剩余的
key=value实参按名字绑定到形参。
* 分隔符的本质,是在第一阶段与第二阶段之间划出一道不可逾越的边界。
5.2、 字节码视角
通过 dis 模块可以观察到,带 * 分隔符的函数在字节码层面并无特殊指令,其约束是在参数解析阶段由解释器强制执行的:
python
import dis
def func(a, *, b):
pass
dis.dis(func)
输出中可以看到 LOAD_FAST 等常规指令,说明 * 的约束发生在函数调用协议层,而非函数体内部。
5.3 、与 inspect 模块的交互
使用 inspect.signature 可以清晰地看到参数的分类:
python
import inspect
def func(a, *, b, c=10):
pass
sig = inspect.signature(func)
for name, param in sig.parameters.items():
print(name, param.kind)
输出:
a POSITIONAL_OR_KEYWORD
b KEYWORD_ONLY
c KEYWORD_ONLY
param.kind 为 KEYWORD_ONLY 的参数,正是 * 分隔符之后的参数。
6、 实践案例
6.1、 配置类函数
python
def connect(host, *, port=3306, timeout=5, use_ssl=False):
"""连接数据库,连接参数必须显式声明。"""
print(f"连接 {host}:{port},超时 {timeout}s,SSL={use_ssl}")
connect("localhost")
connect("localhost", port=5432, use_ssl=True)
6.2 、API 封装
python
def request(url, *, method="GET", headers=None, timeout=3):
"""HTTP 请求封装,method/headers/timeout 均为 Keyword-Only。"""
headers = headers or {}
print(f"{method} {url} headers={headers} timeout={timeout}")
request("https://api.example.com")
request("https://api.example.com", method="POST", headers={"Content-Type": "application/json"})
6.3、 数据校验函数
python
def validate(data, *, required_fields=None, min_length=0):
"""校验数据,必填字段与最小长度均需关键字指定。"""
required_fields = required_fields or []
missing = [f for f in required_fields if f not in data]
if missing:
raise ValueError(f"缺少字段: {missing}")
if len(data) < min_length:
raise ValueError("数据长度不足")
return True
validate({"name": "张三"}, required_fields=["name", "age"])
7、常见误区与注意事项
7.1 、* 之后不能再有位置参数
python
def func(*, a, b): # 正确
pass
def func(*, a, b=1): # 正确
pass
7.2、 不要与解包混淆
函数定义中的 * 是分隔符,而函数调用中的 *iterable 是序列解包,二者作用完全不同:
python
def func(a, *, b):
pass
args = [1]
kwargs = {"b": 2}
func(*args, **kwargs) # 调用时 * 用于解包,合法
7.3 、与位置限定符 / 的对比
Python 3.8 引入了 /,用于限定仅限位置参数(Positional-Only)。两者可同时使用,顺序为:
python
def func(a, /, b, *, c):
pass
a:仅限位置参数。b:位置或关键字均可。c:仅限关键字参数。
8、 总结
| 特性 | 说明 |
|---|---|
| 语法 | def func(a, *, b) |
| 作用 | 强制 * 之后的参数只能以关键字方式传入 |
| 适用场景 | 参数较多、含义易混淆、需要为扩展预留空间 |
与 *args 区别 |
*args 收集位置参数;裸 * 只做分隔 |
与 / 区别 |
/ 限定仅位置参数;* 限定仅关键字参数 |
Keyword-Only Arguments 是 Python 函数签名设计中一项优雅而实用的特性。它通过语法层面的约束,帮助开发者写出更清晰、更健壮的代码。建议在编写公共 API、配置类函数或参数较多的函数时,主动考虑使用 * 分隔符,让函数接口的意图更加明确。
二、代码示例
1、示例代码
python
from typing import TypeVar
T = TypeVar("T")
def full_function(pos1: int, pos2: str, *args, kw_mandatory: float, kw_default: int = 100, **kwargs):
"""
参数规则拆解
pos1, pos2 : 普通位置参数(支持位置 / 关键字传参)
*args : 收集多余的位置参数(可选)
kw_mandatory : *后面,无默认 → 必须关键字传参,必填
kw_default : *后面,带默认 → 关键字传参,可选
**kwargs : 捕获额外自定义关键字参数,放最后
"""
print("=====函数输出=====")
print(f"pos1 = {pos1}")
print(f"pos2 = {pos2}")
print(f"args = {args}")
print(f"kw_mandatory = {kw_mandatory}")
print(f"kw_default = {kw_default}")
print(f"kwargs = {kwargs}\n")
# ---------------------- ✅合法调用示例 ----------------------
# 1.基础用法
full_function(10, "modbus", kw_mandatory=3.14)
# 2.携带多余位置参数给 *args
full_function(20, "rtu", 99, 88, kw_mandatory=5.20, kw_default=666)
# 3.追加额外自定义参数给 **kwargs
full_function(30, "tcp", kw_mandatory=1.23, timeout=0.5, slave=1)
# 4.前面全部改成关键字传参(Python允许)
full_function(pos1=40, pos2="uart", kw_mandatory=9.99)
# ---------------------- ❌非法调用(放开注释运行会报错) ----------------------
# 1. kw_mandatory 使用位置传参
# full_function(10, "test", 3.14)
# 2. 缺失必填关键字参数 kw_mandatory
# full_function(10, "test")
# 3. **kwargs 不能使用位置传参
# full_function(10,"test",kw_mandatory=1, 0.5)
# ==================== 类方法工程实例(复刻 pymodbus API风格) ====================
class ModbusClient:
def read_holding_register(
self,
address: int,
*,
count: int, # 必填关键字参数(无默认)
device_id: int = 1, # 可选关键字参数(带默认值)
timeout: float = 0.8,
no_response_expected: bool = False
) -> T:
"""Modbus 03功能码:读取保持寄存器"""
print(f"【Modbus 03】")
print(f"起始地址:{address}")
print(f"读取寄存器数量:{count}")
print(f"从站ID:{device_id}")
print(f"超时时间:{timeout}")
print(f"无需应答:{no_response_expected}\n")
return None
if __name__ == "__main__":
cli = ModbusClient()
# ✅正确调用
cli.read_holding_register(0, count=10)
cli.read_holding_register(100, count=5, device_id=2, timeout=1.5)
cli.read_holding_register(200, count=1, no_response_expected=True)
# ❌错误调用,count禁止位置传参
# cli.read_holding_register(0, 10)
2、运行输出
python
(.venv) PS D:\user\01417804\桌面\PythonProject> python .\main.py
=====函数输出=====
pos1 = 10
pos2 = modbus
args = ()
kw_mandatory = 3.14
kw_default = 100
kwargs = {}
=====函数输出=====
pos1 = 20
pos2 = rtu
args = (99, 88)
kw_mandatory = 5.2
kw_default = 666
kwargs = {}
=====函数输出=====
pos1 = 30
pos2 = tcp
args = ()
kw_mandatory = 1.23
kw_default = 100
kwargs = {'timeout': 0.5, 'slave': 1}
=====函数输出=====
pos1 = 40
pos2 = uart
args = ()
kw_mandatory = 9.99
kw_default = 100
kwargs = {}
【Modbus 03】
起始地址:0
读取寄存器数量:10
从站ID:1
超时时间:0.8
无需应答:False
【Modbus 03】
起始地址:100
读取寄存器数量:5
从站ID:2
超时时间:1.5
无需应答:False
【Modbus 03】
起始地址:200
读取寄存器数量:1
从站ID:1
超时时间:0.8
无需应答:True
(.venv) PS D:\user\01417804\桌面\PythonProject>
