Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

  1. 首页
  2. 博客
  3. 软件开发

软件开发

Python *args 和 **kwargs 解释:具有清晰示例的灵活函数

函数签名中的那些星星并不像看上去那么神秘。了解 *args 和 **kwargs 如何收集参数、相同的星号如何解压它们,以及如何设计难以滥用的函数签名。

阅读约 8 分钟
Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

要点速览

  • *args 将额外的位置参数收集到 元组 中。
  • **kwargs 将额外的关键字参数收集到 字典 中。
  • 这些名称只是一种约定,因为 * 和 ** 才是最重要的。
  • 当您调用函数时,同样会解包列表或字典。

迟早,您会阅读像 def handler(*args, **kwargs) 这样的函数并想知道它接受什么。 Python *args 和 **kwargs 让函数接受灵活数量的参数。它们为装饰器、包装器和许多库 API 提供支持。

本指南介绍了它们如何在两个方向上工作,以及 Python 让您控制函数调用方式的新方法。我们在 Python 3.13 上运行每个示例,显示的输出来自该运行。

*args 的作用是什么

用一颗星编写的参数将所有额外的位置参数收集到一个元组中:

def total(*args):
    print(type(args).__name__, args)
    return sum(args)

print(total(1, 2, 3))
# tuple (1, 2, 3)
# 6

print(total())
# tuple ()
# 0

如果没有参数,您只需得到一个空元组,因此该函数仍然有效。名称 args 是一种约定。 *numbers 的行为是相同的,并且更具描述性的名称通常更好。

**kwargs 做什么

两颗星将所有额外的关键字参数收集到字典中:

def describe(**kwargs):
    print(type(kwargs).__name__, kwargs)

describe(name="Asha", city="Pune")
# dict {'name': 'Asha', 'city': 'Pune'}

因为它是一个普通的 字典 ,所以您可以循环它,使用 get() 查找键并将其传递。密钥保持其传递的顺序。

参数顺序

当混合各种参数时,Python 需要特定的顺序:

  1. 常规位置参数
  2. *参数
  3. 仅限关键字参数
  4. **夸格
def order(item, qty=1, *extras, priority=False, **notes):
    print(item, qty, extras, priority, notes)

order("pen")
# pen 1 () False {}

order("pen", 2, "gift wrap", "card", priority=True, note="leave at door")
# pen 2 ('gift wrap', 'card') True {'note': 'leave at door'}

注意priority。因为它位于 *extras 之后,所以只能通过名称设置,因此杂散的位置参数永远不会意外设置它。不允许将 **kwargs 放在前面,我们的测试提出了 SyntaxError:“参数不能跟随 var-keyword 参数”。

打电话时拆箱

相同的星星在呼叫站点以相反的方式工作。一颗星将列表或元组扩展为位置参数,两颗星将字典扩展为关键字参数:

nums = [4, 5, 6]
total(*nums)            # same as total(4, 5, 6) -> 15

settings = {"name": "Ben", "city": "Delhi"}
describe(**settings)    # same as describe(name="Ben", city="Delhi")

解包适用于任何函数,而不仅仅是那些使用 *args 和 **kwargs 的函数。当配置以字典形式出现时,这很方便:

def connect(host, port, timeout=5):
    return f"{host}:{port} (timeout {timeout})"

connect(**{"host": "db.local", "port": 5432})
# 'db.local:5432 (timeout 5)'

connect(**{"host": "db.local", "port": 5432, "ssl": True})
# TypeError: connect() got an unexpected keyword argument 'ssl'

这个错误很有用。没有 **kwargs 的函数会拒绝未知选项,因此设置名称中的拼写错误会大声失败,而不是被默默地忽略。

仅关键字参数

签名中的裸 * 没有名称,表示“此后的所有内容都必须通过关键字传递”:

def make_user(name, *, admin=False):
    return name, admin

make_user("Asha", admin=True)
# ('Asha', True)

make_user("Asha", True)
# TypeError: make_user() takes 1 positional argument but 2 were given

这是保护布尔标志的好方法。像 make_user("Asha", True) 这样的调用迫使读者猜测 True 的含义。要求 admin=True 使每个调用都不言自明。

仅位置参数

从Python 3.8开始,a /标记只能按位置传递的参数:

def area(width, height, /):
    return width * height

area(3, 4)                 # 12
area(width=3, height=4)
# TypeError: area() got some positional-only arguments
#   passed as keyword arguments: 'width, height'

这使得库可以在以后重命名这些参数而不会破坏任何人的代码,因为没有人可以依赖这些名称。一些内置函数就是这样工作的。例如,len(obj=[1]) 不起作用。

提示:完整模式为def f(pos_only, /, normal, *, kw_only)。您很少需要全部三个,但了解 / 和 * 的含义可以帮助您阅读文档和库签名。

装饰器和包装器

这是同时使用两者的主要原因。包装器通常需要接受原始函数采用的任何参数,并将它们原封不动地传递:

import functools, time

def timed(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

@timed
def slow_add(a, b, delay=0.01):
    time.sleep(delay)
    return a + b

print(slow_add(2, 3, delay=0.02))
# slow_add took 0.0205s
# 5

包装器根本不需要知道 slow_add 的参数,因此它适用于任何函数。您的时间数字会略有不同。我们的 Python 装饰器 指南解释了 functools.wraps 和其他细节。

合并选项

** 也适用于字典文字,它提供了一种在默认值之上应用覆盖的巧妙方法:

defaults = {"theme": "light", "lang": "en"}

def build(**overrides):
    return {**defaults, **overrides}

build(theme="dark")
# {'theme': 'dark', 'lang': 'en'}

后来的键获胜,因此覆盖替换默认值。子类也使用此模式(如 super().__init__(*args, **kwargs) 中所示)将额外的参数传递给父类。有关 super() 的更多信息,请参阅 Python 类和对象 。

备忘单

语法 哪里 含义
def f(*args) 定义 将额外的位置参数收集到元组中
def f(**kwargs) 定义 将额外的关键字参数收集到字典中
def f(a, *, b) 定义 b 仅限关键字
def f(a, /, b) 定义 a 仅限位置
f(*items) 致电 将序列分散到位置参数中
f(**options) 致电 将字典传播到关键字参数中
{**a,**b} 字面意思 合并字典
[*a,*b] 字面意思 将可迭代对象合并到一个列表中

何时避免它们

灵活性是有代价的。定义为 def create(**kwargs) 的函数不会告诉读者存在哪些选项,并且编辑器无法自动完成它们。在任何失败之前,打字错误可能会悄悄地通过几层。

因此,当您知道函数需要什么时,更喜欢显式参数。当您真正需要灵活性时,请使用 *args 和 **kwargs:包装器、装饰器、将参数转发到另一个函数,或像 total(*numbers) 这样的真正的可变长度输入。如果一个函数需要很多命名选项,dataclass也可以更清晰。

常见错误

使用可变默认值

这与星星无关,但它经常出现在星星附近。默认值在定义函数时创建一次,然后在调用之间共享:

def bad(items=[]):
    items.append(1)
    return items

print(bad(), bad())
# [1, 1] [1, 1]

两次调用都返回了完全相同的列表,其中包含两个项目。使用 items=None 并在函数内创建一个新列表。

其他错误

  • 忘记解压。调用total(nums)传递一个列表而不是三个数字。写total(*nums)。
  • 吞掉拼写错误。 接受 **kwargs 但忽略未知键的函数会隐藏拼写错误的选项。验证它们,或者避免**kwargs。
  • 两次传递相同的参数。 f(1, **{"a": 2})(其中 a 是第一个参数)会引发“多值”错误。
  • 不在包装器中转发。 调用 func(args, kwargs) 传递一个元组和一个字典。正确的形式是func(*args, **kwargs)。

*args 和 **kwargs 的类型提示

*args 和 **kwargs 上的类型提示描述每个单独的值,而不是整个集合。这让很多人感到惊讶:

def total(*prices: float) -> float:
    return sum(prices)      # prices is tuple[float, ...]

def tag(**attrs: str) -> str:
    return " ".join(f'{k}="{v}"' for k, v in attrs.items())   # dict[str, str]

total(1.5, 2.5)                  # 4.0
tag(id="x", role="button")       # id="x" role="button"

这里 prices 被视为浮点数元组,attrs 被视为字符串字典。当关键字参数具有不同类型时,Python 3.12 添加了一个精确选项:TypedDict 与 Unpack 组合。

from typing import TypedDict, Unpack

class Options(TypedDict, total=False):
    timeout: float
    retries: int

def fetch(url: str, **opts: Unpack[Options]) -> None: ...

然后,诸如 mypy 或 Pyright 之类的类型检查器可以拒绝 fetch(url, retries="3") 或拼写错误的选项名称,而普通的 **kwargs 永远无法做到这一点。但运行时行为不会改变。当我们在 Python 3.13 下运行 fetch("u", retries="3") 时,它执行时没有任何抱怨,因为 Python 本身不强制执行类型提示。

如果您发现自己记录了 **kwargs 的一长串可接受的键,请考虑用显式的仅关键字参数替换它们。然后编辑可以自动完成它们,并且错误会立即被发现。

最后的话

记住两个方向。在定义中,星星聚集。在通话中,他们打开行李。添加仅关键字参数以使调用更清晰,并为真正转发参数的代码保留 **kwargs。为了练习,编写一个小的装饰器来记录每个调用的参数,并将其用于几个不同的函数。

常见问题

*args 和 **kwargs 有什么区别?

*args 将额外的位置参数收集到一个元组中。 **kwargs 将额外的关键字参数收集到字典中。一个函数可以同时使用两者。

我必须将它们称为 args 和 kwargs 吗?

不会。星星负责工作,因此 * 值或 ** 选项同样有效。 args 和 kwargs 只是一个广泛认可的约定。

*args 和 **kwargs 应该按什么顺序排列?

首先是常规参数,然后是 *args,然后是仅关键字参数,最后是 **kwargs。任何其他顺序都会导致语法错误。

函数定义中单个 * 意味着什么?

裸露的 * 表示后面的每个参数都必须通过关键字传递。它使调用更加清晰,特别是对于布尔标志。

Python 函数中 / 是什么意思?

/ 之前的参数仅是位置参数,因此调用者无法通过名称传递它们。该功能是在 Python 3.8 中添加的。

继续学习

与Stack Quora合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →