摘要
Python 函数系统以“形参定义”和“实参绑定”为核心,形成了位置参数、关键字参数、默认参数、可变位置参数、可变关键字参数、仅位置参数和仅关键字参数等多种参数形式。Python 官方文档将函数调用描述为一种参数槽位绑定过程:位置实参首先绑定到形式参数槽位,随后关键字实参根据名称绑定到对应槽位,未绑定槽位再由默认值补齐;若发生重复绑定、缺失必需参数或未知关键字参数,则触发 TypeError。在此基础上,PEP 3102 引入关键字专用参数,PEP 570 引入仅位置参数语法 /,使函数作者能够在函数签名层面明确控制调用方式。本文基于 Python 官方文档、语言参考手册和相关 PEP,系统说明 Python 为什么同时保留位置参数与关键字参数,为什么设计 / 与 * 作为形式参数调用边界,以及该机制在标准库、内置函数、API 演化、可读性约束和关键字透传场景中的具体应用。
关键词:Python;函数设计;位置参数;关键字参数;仅位置参数;仅关键字参数;API 兼容性
1 引言
函数是 Python 程序组织逻辑、封装行为和暴露接口的基本单位。Python 中的函数并不只是一段可复用代码块,同时也是一种具备调用协议的对象。调用者通过位置实参、关键字实参、解包实参以及可变参数向函数传入数据,函数定义者则通过形式参数列表声明函数可接受的调用形态。
Python 函数参数设计的特殊性在于:同一个函数签名可以同时表达“按顺序传入”“按名称传入”“只能按顺序传入”“只能按名称传入”“接收任意多位置实参”和“接收任意多关键字实参”等多种约束。这种设计不是单纯的语法便利,而是用于描述函数接口契约、降低调用歧义、支持 API 演化并兼容 Python 内置函数和 C 扩展模块已有行为的语言机制。
从官方资料看,* 与 / 的作用并不相同。* 之后的参数是仅关键字参数,必须通过 name=value 的形式传入;/ 之前的参数是仅位置参数,调用者不能通过参数名传入。两者共同构成 Python 函数签名中的调用边界,使函数作者可以精确控制参数名称是否成为公开 API 的一部分。
2 Python 函数调用的参数绑定模型
Python 语言参考手册将函数调用定义为对 callable object 的调用。一次调用可以包含位置实参、关键字实参、* 可迭代对象解包和 ** 映射对象解包。调用发生前,所有实参表达式都会先被求值;随后解释器按照参数绑定规则将实参分配给函数形参。
其基本过程可以概括为以下规则:
第一,位置实参优先绑定。若有 N 个位置实参,它们会依次填入前 N 个可接收位置实参的形参槽位。
第二,关键字实参按名称绑定。关键字名称用于匹配形式参数名;如果对应槽位已被位置实参填充,再次通过关键字绑定会造成重复绑定错误。
第三,未绑定的形参由默认值补齐。若某个必需形参既没有接收到实参,也没有默认值,则调用失败。
第四,若函数定义包含 *args,多余的位置实参会被收集进一个元组;若函数定义包含 **kwargs,多余的关键字实参会被收集进一个字典。
第五,*iterable 会在调用处展开为额外的位置实参,**mapping 会在调用处展开为额外的关键字实参。
例如:
def connect(host, port=5432, *, timeout=3, **options): # Build connection config return { "host": host, "port": port, "timeout": timeout, "options": options, }config = connect( "127.0.0.1", timeout=5, ssl=True, application_name="reporter",)
在该示例中,host 通过位置实参绑定,port 使用默认值,timeout 是仅关键字参数,ssl 与 application_name 被收集进 options。这个例子体现了 Python 函数调用模型的核心特征:位置负责顺序绑定,关键字负责命名绑定,可变参数负责扩展输入集合。
3 位置参数与关键字参数的定义理由
Python 保留位置参数与关键字参数,是因为它们表达的是两类不同的调用信息。
位置参数用于表达天然存在顺序关系的输入。对于数学运算、序列操作、二元比较、范围构造等函数,参数顺序本身就是语义的一部分。例如 pow(x, y)、divmod(x, y)、range(start, stop, step) 等调用中,参数的含义由其位置决定。
def distance(x1, y1, x2, y2): # Compute Euclidean distance between two points return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5d = distance(0, 0, 3, 4)
关键字参数用于表达名称携带语义的输入。对于配置项、可选行为、开关参数、回调函数、超时设置、编码格式等场景,参数名可以直接说明传入值的用途。
def export_report(data, *, format="pdf", compress=False, timeout=30): # Export report with explicit options return { "data": data, "format": format, "compress": compress, "timeout": timeout, }export_report(records, format="html", compress=True)
上述函数中,format、compress 和 timeout 不适合依赖位置传入。若写成 export_report(records, "html", True, 60),调用者需要记忆参数顺序;若写成关键字形式,调用表达式中直接保留参数含义。
因此,位置参数与关键字参数并不是重复设计。位置参数服务于顺序语义和紧凑调用;关键字参数服务于命名语义、可读性和配置扩展。Python 将二者合并进同一个调用系统,使函数作者可以根据接口语义选择不同参数形式。
4 / 与 * 的形式参数边界设计
Python 函数签名中的 / 和 * 是两种方向相反的调用限制。
/ 用于声明仅位置参数。出现在 / 左侧的参数只能通过位置实参传入,调用者不能使用参数名绑定它们。
def normalize(value, /, *, min_value=0, max_value=1): # Normalize a value into a target interval return (value - min_value) / (max_value - min_value)normalize(5, min_value=0, max_value=10)
在该函数中,value 是仅位置参数,min_value 和 max_value 是仅关键字参数。调用者可以写 normalize(5, min_value=0, max_value=10),但不应写成 normalize(value=5, min_value=0, max_value=10)。
* 用于声明仅关键字参数。出现在裸 * 之后,或者出现在 *args 之后的参数,只能通过关键字传入,不能通过位置传入。
def read_file(path, *, encoding="utf-8", errors="strict"): # Read text from a file using explicit text options with open(path, encoding=encoding, errors=errors) as file: return file.read()
这里 path 可以按位置传入,encoding 和 errors 必须按名称传入。该形式避免了 read_file("a.txt", "utf-8", "ignore") 这种可读性较弱的调用方式。
完整的参数分区形式如下:
def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2): # Demonstrate all parameter regions return pos1, pos2, pos_or_kwd, kwd1, kwd2
该签名被分为三个区域:
pos1, pos2 位于 / 左侧,只能按位置传入。
pos_or_kwd 位于 / 与 * 之间,可以按位置传入,也可以按关键字传入。
kwd1, kwd2 位于 * 右侧,只能按关键字传入。
5 设计目的与主要应用场景
5.1 参数名不应成为公开 API 时使用 /
PEP 570 指出,仅位置参数没有外部可用名称。调用者只能根据位置传参,因此参数名不会成为调用契约的一部分。这一点对库作者具有直接意义:如果某个参数名没有稳定的外部语义,或者未来可能重命名,则应避免让调用者依赖该名称。
def as_user_id(value, /): # Convert input to user id return int(value)
在该例中,value 只是函数内部使用的局部名称。若允许调用者写 as_user_id(value="1001"),那么 value 这个名字就会变成外部 API 的一部分。若未来将内部参数名改为 raw,依赖 value= 的调用代码会受到影响。使用 / 后,调用者只能写 as_user_id("1001"),内部参数名可以安全调整。
5.2 参数天然有顺序时使用 /
对于具有固定顺序的输入,关键字形式可能制造多种等价但风格不一致的调用方式。例如范围、坐标、二元运算、切片边界等场景,参数顺序通常就是语义结构的一部分。
def between(value, lower, upper, /): # Check whether value is within the closed interval return lower <= value <= upperbetween(5, 1, 10)
该函数中,value、lower、upper 依赖固定顺序。仅位置参数可以限制调用方式,避免出现 between(upper=10, value=5, lower=1) 这类语义上可成立但顺序结构被打散的调用表达。
5.3 函数接收任意关键字时使用 / 避免名称冲突
PEP 570 给出的重要场景之一是:函数既需要接收一个位置对象,又需要接收任意关键字参数。典型模式类似 dict.update:第一个输入可以是映射或键值对迭代器,其余输入可以是任意关键字。如果第一个参数不是仅位置参数,那么该参数名会与 **kwargs 中的同名键发生冲突。
def merge(mapping=None, /, **items): # Merge a positional mapping and arbitrary keyword items result = {} if mapping is not None: result.update(mapping) result.update(items) return resultmerge({"name": "Alice"}, name="Bob", age=18)
这里 mapping 只是函数内部用于接收第一个位置对象的参数名。如果允许 mapping= 作为关键字传入,那么调用者既可能想传入被合并的映射,也可能想设置结果中的 "mapping" 键。使用 / 后,mapping 不会占用关键字命名空间,**items 可以完整接收调用者想表达的数据字段。
5.4 选项参数必须显式命名时使用 *
PEP 3102 引入关键字专用参数的核心动机之一,是允许函数在接收任意数量位置参数的同时,仍然定义只能通过关键字传入的选项。这样可以避免选项被位置实参意外填充。
def join_words(*words, separator=" "): # Join words with an explicit separator option return separator.join(words)join_words("Python", "function", "design", separator="-")
该函数可以接收任意数量的单词,同时使用 separator 作为命名选项。若没有关键字专用参数,调用者很难区分最后一个位置实参到底是待拼接单词,还是分隔符配置。
5.5 API 扩展时使用 keyword-only 保持兼容
仅关键字参数适合用于新增配置项。已有调用者若使用位置参数,新增 keyword-only 选项不会改变原有位置参数的绑定关系。
def fetch(url, *, timeout=3): # Fetch resource with timeout option return url, timeout
若后续需要增加重试次数,可以继续追加 keyword-only 参数:
def fetch(url, *, timeout=3, retries=0): # Fetch resource with timeout and retry options return url, timeout, retries
已有调用 fetch("https://example.com", timeout=5) 仍然有效;新增选项通过名称传入,不会改变原有参数顺序。
5.6 标准库和内置函数中的落地形式
Python 内置函数文档已经大量使用 / 标记仅位置参数。例如 hex(integer, /)、id(object, /) 和 input(prompt, /) 等签名表示其 / 左侧参数不能通过关键字传入。这类函数的参数名通常只是文档说明或实现内部名称,并不意图成为可依赖的外部调用接口。
标准库、内置函数和 C 扩展模块的历史行为也是 / 进入 Python 函数定义语法的重要原因。PEP 570 指出,在没有语言级仅位置参数语法时,许多 CPython 内置函数已经具备仅位置参数语义,但纯 Python 函数无法用同样的签名形式表达这种接口。引入 / 后,纯 Python 实现与 C 实现可以在接口层面保持一致。
6 Python 函数定义与调用的其它机制
6.1 默认参数
默认参数允许函数在调用时省略部分实参。Python 官方文档指出,默认值在函数定义执行时求值,而不是在每次函数调用时重新求值。因此,可变对象作为默认值时会在多次调用之间共享。
不推荐写法:
def append_item(item, items=[]): # This default list is shared across calls items.append(item) return items
推荐写法:
def append_item(item, items=None): # Create a new list when no list is provided if items is None: items = [] items.append(item) return items
该设计意味着默认参数不仅是调用便利机制,也是函数对象创建阶段的一部分。默认值属于函数定义时确定的对象,而不是调用时动态创建的对象。
6.2 可变位置参数 *args
*args 用于接收未被普通形参绑定的多余位置实参,函数内部看到的是一个元组。
def total(*numbers): # Sum arbitrary positional numbers return sum(numbers)total(1, 2, 3, 4)
该机制适合聚合、转发、包装器、高阶函数和命令式接口。
6.3 可变关键字参数 **kwargs
**kwargs 用于接收未被普通形参绑定的多余关键字实参,函数内部看到的是一个字典。
def build_user(**fields): # Build a user dictionary from keyword fields return dict(fields)build_user(name="Alice", age=18, active=True)
该机制适合配置透传、数据对象构造、插件参数传递和装饰器包装。
6.4 调用处的 * 与 ** 解包
函数调用处的 * 与 ** 与函数定义处的 *args、**kwargs方向相反。调用处的 * 用于把可迭代对象展开为位置实参;调用处的 ** 用于把映射对象展开为关键字实参。
def rectangle_area(width, height): # Compute rectangle area return width * heightsize = (4, 5)rectangle_area(*size)options = {"width": 4, "height": 5}rectangle_area(**options)
调用处解包使调用者可以在运行时构造参数集合,并按函数签名完成绑定。
6.5 Lambda 表达式
lambda 用于创建匿名函数。Python 官方教程说明,lambda 在语义上是普通函数定义的语法糖,但只能包含单个表达式。
users = [ {"name": "Alice", "score": 90}, {"name": "Bob", "score": 80},]users.sort(key=lambda user: user["score"])
该机制常用于排序键、简单回调和局部高阶函数场景。
6.6 文档字符串
函数体第一条字符串字面量通常作为函数的文档字符串。文档字符串可通过 __doc__ 访问,也会被 help() 等工具使用。
def add(a, b): """Return the sum of two values.""" return a + b
文档字符串使函数签名之外的行为说明能够以结构化方式附着在函数对象上。
6.7 函数注解与类型提示
Python 支持为函数参数和返回值添加注解。typing 官方文档说明,Python 运行时不会强制执行函数和变量类型注解;这些注解主要供类型检查器、IDE、linter 等工具使用。
def scale(value: float, factor: float = 1.0) -> float: # Scale a numeric value return value * factor
因此,类型注解属于静态分析和工具生态中的接口信息,而不是运行时参数绑定规则本身。
6.8 装饰器
装饰器是在函数定义时执行的可调用对象。装饰器接收原函数对象,并返回绑定到函数名的新对象。多个装饰器会以嵌套方式应用。
import functoolsdef log_call(func): # Wrap function to log calls @functools.wraps(func) def wrapper(*args, **kwargs): # Print function name before invocation print(f"calling {func.__name__}") return func(*args, **kwargs) return wrapper@log_calldef greet(name): # Return greeting text return f"Hello, {name}"
装饰器常用于日志、鉴权、缓存、重试、事务、指标采集和参数校验等横切逻辑。
6.9 闭包
函数可以在内部定义函数,内部函数可以引用外部函数作用域中的变量。
def make_multiplier(factor): # Return a closure that captures factor def multiply(value): # Use captured factor return value * factor return multiplydouble = make_multiplier(2)double(10)
闭包使函数可以携带环境状态,常见于工厂函数、回调构造和装饰器实现。
6.10 生成器函数
函数体中出现 yield 时,函数调用不会立即执行完整函数体,而是返回生成器对象。生成器按需产出值。
def count_up_to(limit): # Yield numbers from zero to limit - 1 current = 0 while current < limit: yield current current += 1
生成器函数用于惰性序列、流式处理和大规模数据遍历。
6.11 异步函数
async def 定义协程函数,调用后返回协程对象;协程对象通常需要通过 await 或事件循环执行。
async def fetch_text(client, url, *, timeout=3): # Await asynchronous client request response = await client.get(url, timeout=timeout) return response.text
异步函数将函数调用与异步调度机制结合,常用于网络 I/O、数据库 I/O 和并发任务编排。
6.12 可调用对象
Python 调用协议不仅适用于函数,也适用于实现 __call__() 方法的对象。
class Prefixer: def __init__(self, prefix): # Store prefix for later calls self.prefix = prefix def __call__(self, value): # Make instances callable return f"{self.prefix}{value}"add_user_prefix = Prefixer("user:")add_user_prefix("1001")
这说明 Python 的“函数调用”是一种更一般的 callable 协议,而不仅限于 def 定义的函数。
7 函数签名设计的工程化示例
7.1 数据访问接口
def query(sql, /, *params, timeout=3, readonly=True, **driver_options): # Execute SQL with positional parameters and keyword-only options return { "sql": sql, "params": params, "timeout": timeout, "readonly": readonly, "driver_options": driver_options, }query( "select * from user where id = ?", 1001, timeout=5, readonly=True, trace_id="abc-123",)
该设计中,sql 是仅位置参数,因为它是主输入;params 接收 SQL 占位符参数;timeout 与 readonly 是仅关键字配置;driver_options 接收驱动层扩展参数。这个签名同时体现了顺序输入、显式配置和扩展参数透传。
7.2 API 客户端接口
def request(method, url, /, *, headers=None, json=None, timeout=10, retries=0): # Build an HTTP request description return { "method": method, "url": url, "headers": headers or {}, "json": json, "timeout": timeout, "retries": retries, }request( "POST", "https://api.example.com/users", json={"name": "Alice"}, timeout=5, retries=2,)
method 与 url 具有固定顺序,是请求的核心输入;headers、json、timeout、retries 是配置项,适合 keyword-only。该设计避免了 request("POST", url, None, {"name": "Alice"}, 5, 2) 这类位置含义不清的调用。
7.3 数据模型构造接口
def make_record(kind, /, **fields): # Build a record with arbitrary user-defined fields return { "kind": kind, "fields": fields, }make_record("event", kind="login", user_id=1001)
该示例展示了 / 的关键价值:第一个 kind 是内部形参名,但调用者仍然可以在 **fields 中使用 "kind" 作为业务字段。若没有 /,kind="login" 会与第一个形参发生名称冲突。
7.4 插件注册接口
def register_plugin(name, factory, /, *, enabled=True, priority=0, **metadata): # Register plugin with explicit options and arbitrary metadata return { "name": name, "factory": factory, "enabled": enabled, "priority": priority, "metadata": metadata, }
name 和 factory 是注册动作的主输入,使用仅位置参数可避免名称成为未来稳定 API 负担;enabled 和 priority 是配置项,使用 keyword-only 提高调用明确性;metadata 支持扩展。
8 讨论
Python 函数设计中的 / 与 * 不是装饰性语法,而是接口契约的一部分。/ 控制参数名称不外泄,使函数作者可以改变内部形参名而不破坏外部调用;* 控制配置项必须显式命名,使调用表达式携带参数含义。二者与默认参数、*args、**kwargs、调用处解包、注解、装饰器和 callable 协议共同构成 Python 函数系统。
与以函数重载、静态类型签名或命名参数为核心的语言机制相比,Python 的函数设计更集中于运行时参数绑定和调用协议表达。Python 的参数列表既是调用入口,也是 API 兼容性边界。参数是否允许按名称传入,决定了该参数名是否成为外部契约;参数是否只能按关键字传入,决定了调用者是否必须显式表达配置语义。
9 结论
Python 设计位置参数和关键字参数,是为了同时支持顺序语义与命名语义。位置参数适用于具有自然顺序、名称外部意义较弱或主输入明确的场景;关键字参数适用于配置项、可选行为和可读性要求较高的场景。/ 和 * 则进一步把这种参数分类固化为函数签名级别的接口约束。
/ 的主要目的包括:隐藏内部参数名、避免参数名成为公开 API、支持 API 安全演化、表达内置函数和 C 扩展已有的仅位置语义,以及避免与 **kwargs 的关键字命名空间冲突。* 的主要目的包括:强制显式传入选项参数、防止调用者依赖配置项的位置、支持可变位置参数后的命名选项,并为未来新增配置项保留兼容空间。
因此,Python 函数设计并不只是“如何传参”的问题,而是“如何定义稳定、清晰、可演化 API”的问题。对于库函数、框架接口、SDK、数据访问层、HTTP 客户端、插件系统和装饰器等场景,合理使用位置参数、关键字参数、/、*、*args 和 **kwargs,能够直接影响接口的可读性、兼容性和扩展性。
参考文献
[1] Python Documentation. The Python Tutorial: More on Defining Functions.
[2] Python Documentation. The Python Language Reference: Function Definitions.
[3] Python Documentation. The Python Language Reference: Calls.
[4] PEP 3102. Keyword-Only Arguments.
[5] PEP 570. Python Positional-Only Parameters.
[6] Python Documentation. inspect — Inspect live objects.
[7] Python Documentation. typing — Support for type hints.
[8] Python Documentation. Built-in Functions.