Python 函数参数分隔符 *:Keyword-Only Arguments 原理与实践

Python 函数参数分隔符 *:Keyword-Only Arguments 原理与实践

  • 一、原理与实践
    • 1、引言
    • [2、从 `*args` 到分隔符:`*` 的双重身份](#2、从 *args 到分隔符:* 的双重身份)
      • [2.1 、打包参数:`*args`](#2.1 、打包参数:*args)
      • [2.2 、分隔参数:裸 `*`](#2.2 、分隔参数:裸 *)
    • [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 、与位置限定符 / 的对比)
    • [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:普通位置参数,可位置传参,也可关键字传参。
  • bc: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 在调用函数时,对参数的处理分为两个阶段:

  1. 位置参数匹配 :按顺序将实参绑定到形参,遇到 * 分隔符时停止。
  2. 关键字参数匹配 :将剩余的 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.kindKEYWORD_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> 
相关推荐
2601_962297251 小时前
Authlib 0.13通用Python认证授权库wheel安装包(支持Python 2/3)
python·jwt·oauth2.0·authlib·openidconnect
SamChan901 小时前
大文件多语言PDF翻译性能实测:300页文档的耗时、内存占用与失败率分析
python·ai·pdf·机器翻译
染的人1 小时前
Java 10x15cm面单PDF转换A4格式PDF
java·开发语言·pdf
光电笑映1 小时前
Linux 线程同步与互斥:锁的本质、条件变量与信号量的底层原理
linux·运维·服务器·开发语言·c++
GIS数据转换器1 小时前
遥感GIS一体化技术应用平台
大数据·人工智能·python·安全·数据挖掘
Json____1 小时前
从零构建在线拍卖系统:Java 全栈开发实践
java·开发语言·管理系统·it学习·wwwoop.com
2601_966949651 小时前
批量 K 线不只是提速:因子研究中的数据质量、复权与回测偏差
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
matlab代码1 小时前
基于matlab水果识别系统(西红柿、香蕉、梨、青椒)【源码64期】
开发语言·matlab
一位正在转型AI全栈的前端工程师1 小时前
AI 全栈学习之旅 -Week 11:从 CLI 到浏览器:用 FastAPI、SSE 和 Vue 3 做一个可审核的 ReAct Agent
前端·python