Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
Python系列文章目录
01-【Python-Day 1】告别编程恐惧:轻松掌握 Python 安装与第一个程序的 6 个步骤
02-【Python-Day 2】掌握Python基石:变量、内存、标识符及int/float/bool数据类型
03-【Python-Day 3】玩转文本:字符串(String)基础操作详解 (上)
04-【Python-Day 4】玩转文本:Python 字符串常用方法深度解析 (下篇)
05-【Python-Day 5】Python 格式化输出实战:%、format()、f-string 对比与最佳实践
06- 【Python-Day 6】从零精通 Python 运算符(上):算术、赋值与比较运算全解析
07-【Python-Day 7】从零精通 Python 运算符(下):逻辑、成员、身份运算与优先级规则全解析
08-【Python-Day 8】从入门到精通:Python 条件判断 if-elif-else 语句全解析
09-【Python-Day 9】掌握循环利器:for 循环遍历序列与可迭代对象详解
10-【Python-Day 10】Python 循环控制流:while 循环详解与 for 循环对比
11-【Python-Day 11】列表入门:Python 中最灵活的数据容器 (创建、索引、切片)
12-【Python-Day 12】Python列表进阶:玩转添加、删除、排序与列表推导式
13-【Python-Day 13】Python 元组 (Tuple) 详解:从创建、操作到高级应用场景一网打尽
14-【Python-Day 14】玩转Python字典(上篇):从零开始学习创建、访问与操作
15-【Python-Day 15】深入探索 Python 字典 (下):常用方法、遍历、推导式与嵌套实战
16-【Python-Day 16】代码复用基石:详解 Python 函数的定义与调用
17-【Python-Day 17】玩转函数参数(上):轻松掌握位置、关键字和默认值
18-【Python-Day 18】玩转函数参数:*args 与 **kwargs 终极指南
文章目录
前言
在上一篇文章【Python-Day 17】中,我们学习了 Python 函数参数的基础知识,包括位置参数、关键字参数和默认参数。这些参数类型为我们编写函数提供了基础的灵活性。然而,在实际开发中,我们常常会遇到一些情况:我们无法预知函数需要接收多少个参数。例如,编写一个计算任意数量数字之和的函数,或者一个能接收任意配置项的函数。这时,Python 提供的可变参数就派上了大用场!
本文将深入探讨 Python 函数参数的进阶主题:可变位置参数 (*args
) 和可变关键字参数 (**kwargs
)。我们将学习如何定义和使用它们,理解它们的内部工作原理,掌握参数传递的正确顺序,并探索强大的参数解包技巧。通过本文的学习,您将能够编写出更加灵活、强大且适应性更强的 Python 函数。
一、可变位置参数 (*args
):接收任意数量的位置参数
想象一下,你想写一个函数来计算任意个数的和。如果使用我们之前学的知识,可能需要传入一个列表或元组。但如果我们希望像 sum(1, 2, 3)
这样直接传入多个数字呢?*args
就能优雅地解决这个问题。
1.1 什么是 *args
?
*args
(星号 *
加上参数名 args
,args
只是一个惯例,你也可以用其他名字,但强烈推荐使用 args
) 是一种特殊的参数形式,它允许函数接收任意数量的位置参数。
当你在函数定义中使用 *args
时,Python 会将所有传递给该参数的多余的位置参数收集起来,并打包成一个元组 (tuple)。
1.2 如何定义和使用 *args
?
语法非常简单,在参数名前加上一个星号 *
即可。
1.2.1 基础语法
def function_name(param1, *args):
# param1 是一个普通的位置参数
# args 将会是一个包含所有额外位置参数的元组
print(f"第一个参数: {param1}")
print(f"可变位置参数 (元组): {args}")
print(f"args 的类型是: {type(args)}")
1.2.2 示例:计算任意数字之和
让我们来实现之前提到的计算任意数字之和的函数:
def calculate_sum(*numbers):
"""计算传入的所有数字的总和。"""
print(f"接收到的参数 (元组): {numbers}")
total = 0
for number in numbers:
total += number
return total
# 调用函数
print(f"总和: {calculate_sum(1, 2, 3)}")
print("-" * 20)
print(f"总和: {calculate_sum(10, 20, 30, 40, 50)}")
print("-" * 20)
print(f"总和: {calculate_sum()}") # 也可以不传参数,此时 numbers 是空元组 ()
输出:
接收到的参数 (元组): (1, 2, 3)
总和: 6
--------------------
接收到的参数 (元组): (10, 20, 30, 40, 50)
总和: 150
--------------------
接收到的参数 (元组): ()
总和: 0
代码解析:
- 我们定义了
calculate_sum
函数,它只包含一个参数*numbers
。 - 当我们调用
calculate_sum(1, 2, 3)
时,numbers
变量就变成了(1, 2, 3)
这个元组。 - 当我们调用
calculate_sum(10, 20, 30, 40, 50)
时,numbers
就变成了(10, 20, 30, 40, 50)
。 - 当不传递任何参数时,
numbers
是一个空元组()
。 - 函数内部,我们可以像遍历普通元组一样遍历
numbers
并进行计算。
1.3 *args
的应用场景
- 处理不确定数量的输入: 如数学计算 (求和、求平均值)、字符串拼接等。
- 函数包装/代理: 当你想编写一个函数来接收另一个函数的参数时,
*args
非常有用,它可以确保所有位置参数都被传递过去。 - 日志记录: 记录函数调用时传入的所有位置参数。
二、可变关键字参数 (**kwargs
):接收任意数量的关键字参数
与 *args
类似,**kwargs
(两个星号 **
加上参数名 kwargs
,kwargs
也是惯例) 允许函数接收任意数量的关键字参数。
2.1 什么是 **kwargs
?
当你在函数定义中使用 **kwargs
时,Python 会将所有传递给该参数的多余的关键字参数收集起来,并打包成一个字典 (dictionary)。字典的键是参数名 (字符串),值是参数值。
2.2 如何定义和使用 **kwargs
?
语法是在参数名前加上两个星号 **
。
2.2.1 基础语法
def function_name(param1, **kwargs):
# param1 是一个普通参数
# kwargs 将会是一个包含所有额外关键字参数的字典
print(f"第一个参数: {param1}")
print(f"可变关键字参数 (字典): {kwargs}")
print(f"kwargs 的类型是: {type(kwargs)}")
2.2.2 示例:创建用户配置信息
假设我们要编写一个函数,用于创建用户配置,但允许用户传入任意数量的配置项。
def create_user_profile(username, **user_info):
"""创建一个包含用户基本信息和额外配置的字典。"""
profile = {'username': username}
print(f"接收到的关键字参数 (字典): {user_info}")
# 将 **kwargs 中的键值对更新到 profile 字典中
profile.update(user_info)
return profile
# 调用函数
user1 = create_user_profile("Alice", age=30, city="New York")
print(user1)
print("-" * 20)
user2 = create_user_profile("Bob", email="bob@example.com", status="active", country="Canada")
print(user2)
print("-" * 20)
user3 = create_user_profile("Charlie") # 不传入额外关键字参数
print(user3)
输出:
接收到的关键字参数 (字典): {'age': 30, 'city': 'New York'}
{'username': 'Alice', 'age': 30, 'city': 'New York'}
--------------------
接收到的关键字参数 (字典): {'email': 'bob@example.com', 'status': 'active', 'country': 'Canada'}
{'username': 'Bob', 'email': 'bob@example.com', 'status': 'active', 'country': 'Canada'}
--------------------
接收到的关键字参数 (字典): {}
{'username': 'Charlie'}
代码解析:
create_user_profile
函数接收一个必需的位置参数username
和任意数量的关键字参数**user_info
。- 当我们调用
create_user_profile("Alice", age=30, city="New York")
时,user_info
变量就变成了{'age': 30, 'city': 'New York'}
这个字典。 - 我们使用
profile.update(user_info)
将user_info
字典中的所有项合并到profile
字典中。 - 如果不传入额外的关键字参数,
user_info
就是一个空字典{}
。
2.3 **kwargs
的应用场景
- 处理不确定的配置项: 如创建对象、设置参数等。
- 函数包装/代理: 传递关键字参数给其他函数。
- 灵活的 API 设计: 允许 API 用户传递自定义的元数据。
- 修改或扩展现有函数的行为: 在装饰器中经常使用。
三、参数的黄金法则:定义与传递顺序
当一个函数同时包含多种类型的参数时,必须遵循特定的顺序来定义它们。这条规则至关重要,否则 Python 解释器将无法正确解析参数。
3.1 函数定义的顺序
Python 函数参数的标准定义顺序是:
- 标准位置参数 (Positional arguments)
- 默认参数 (Default arguments) (它们也是位置参数,但必须在非默认参数之后)
- 可变位置参数 (
*args
) - 标准关键字参数 (Keyword-only arguments) (Python 3.x 特有,必须在
*args
之后) - 默认关键字参数 (Default keyword-only arguments)
- 可变关键字参数 (
**kwargs
)
一个包含所有类型的参数(简化版,不含关键字参数)的函数定义看起来像这样:
def complex_function(pos1, pos2, default_arg='default', *args, **kwargs):
print(f"位置参数1: {pos1}")
print(f"位置参数2: {pos2}")
print(f"默认参数: {default_arg}")
print(f"可变位置参数: {args}")
print(f"可变关键字参数: {kwargs}")
# 正确调用示例
complex_function(1, 2, 'custom_value', 3, 4, 5, key1='value1', key2='value2')
# 输出:
# 位置参数1: 1
# 位置参数2: 2
# 默认参数: custom_value
# 可变位置参数: (3, 4, 5)
# 可变关键字参数: {'key1': 'value1', 'key2': 'value2'}
complex_function(1, 2, 6, 7, name='Python', version=3.8)
# 输出:
# 位置参数1: 1
# 位置参数2: 2
# 默认参数: 6 <-- 注意这里,6 被当作了 default_arg
# 可变位置参数: (7,)
# 可变关键字参数: {'name': 'Python', 'version': 3.8}
重要提示: 你不需要在每个函数中都使用所有类型的参数,但只要使用了多种类型,就必须遵循这个顺序。
我们可以用 Mermaid 流程图来形象地表示这个顺序:
3.2 函数调用的顺序
在调用函数时,位置参数必须在关键字参数之前。一旦你开始使用关键字参数,之后的所有参数(如果还有的话)都必须是关键字参数。
def my_func(a, b, c):
print(a, b, c)
# 正确调用
my_func(1, 2, 3) # 全部是位置参数
my_func(1, b=2, c=3) # 位置参数 + 关键字参数
my_func(a=1, b=2, c=3) # 全部是关键字参数
# 错误调用 (关键字参数在位置参数之前)
# my_func(a=1, 2, 3) # SyntaxError: positional argument follows keyword argument
四、参数解包:*
与 **
的魔法
除了在函数定义中使用 *
和 **
来收集参数外,我们还可以在调用函数时使用它们来解包 (unpack) 序列(如列表、元组)和字典,将它们的内容作为独立的参数传递给函数。这是一种非常强大和便捷的技巧。
4.1 使用 *
解包序列
如果你有一个列表或元组,你想将其中的每个元素作为独立的位置参数传递给一个函数,你可以在序列前加上一个 *
。
4.1.1 示例:解包列表传递给 print
my_list = [1, 2, 3]
# 不解包,print 会把整个列表当作一个参数打印
print(my_list) # 输出: [1, 2, 3]
# 解包,相当于调用 print(1, 2, 3)
print(*my_list) # 输出: 1 2 3
4.1.2 示例:解包元组传递给自定义函数
def add(a, b, c):
return a + b + c
numbers_tuple = (10, 20, 30)
# 使用 * 解包元组
result = add(*numbers_tuple)
print(f"解包元组后的和: {result}") # 输出: 解包元组后的和: 60
4.2 使用 **
解包字典
类似地,如果你有一个字典,你想将其中的键值对作为关键字参数传递给一个函数,你可以在字典前加上两个 **
。字典的键必须是字符串,并且与函数期望的参数名相匹配。
4.2.1 示例:解包字典创建用户配置
让我们回到 create_user_profile
函数,但这次使用字典解包来传递参数:
def create_user_profile(username, age=None, city=None, email=None):
"""创建用户配置。"""
profile = {'username': username}
if age:
profile['age'] = age
if city:
profile['city'] = city
if email:
profile['email'] = email
return profile
user_data = {
'age': 25,
'city': 'London',
'email': 'dave@example.com'
}
# 使用 ** 解包字典
user_dave = create_user_profile("Dave", **user_data)
print(user_dave)
# 输出: {'username': 'Dave', 'age': 25, 'city': 'London', 'email': 'dave@example.com'}
代码解析:
**user_data
相当于age=25, city='London', email='dave@example.com'
。- Python 将这些解包后的关键字参数传递给了
create_user_profile
函数。
4.3 *
和 **
同时使用
你甚至可以在一次函数调用中同时使用 *
和 **
来解包序列和字典。
def process_data(item_id, name, *tags, **metadata):
print(f"ID: {item_id}")
print(f"Name: {name}")
print(f"Tags: {tags}")
print(f"Metadata: {metadata}")
item_info = (101, 'Widget A')
tag_list = ['electronics', 'new']
meta_dict = {'color': 'blue', 'weight_kg': 0.5}
process_data(*item_info, *tag_list, **meta_dict)
# 输出:
# ID: 101
# Name: Widget A
# Tags: ('electronics', 'new')
# Metadata: {'color': 'blue', 'weight_kg': 0.5}
五、实战演练与常见问题
5.1 实战:构建一个灵活的 HTML 标签生成器
让我们结合所学,创建一个可以生成简单 HTML 标签的函数。它应该接收标签名作为必需参数,可以接收任意数量的 HTML 属性(如 class
, id
, href
等)作为关键字参数,还可以接收任意数量的子元素或文本作为位置参数。
def make_html_tag(tag_name, *content, **attributes):
"""
生成一个简单的 HTML 标签。
:param tag_name: HTML 标签的名称 (如 'p', 'a', 'div')
:param content: 标签内部的内容 (可以是文本或其他标签)
:param attributes: HTML 标签的属性 (如 id='main', class_='text')
:return: 生成的 HTML 字符串
"""
# 构建属性字符串
attr_str = ""
for key, value in attributes.items():
# 处理 Python 关键字 'class' -> 'class_'
key = key.replace('_', '') if key.endswith('_') else key
attr_str += f' {key}="{value}"'
# 构建内容字符串
content_str = "".join(str(c) for c in content)
# 组装 HTML 标签
html = f"<{tag_name}{attr_str}>{content_str}</{tag_name}>"
return html
# 示例调用
p_tag = make_html_tag('p', 'This is a paragraph.', id='intro-p', class_='important')
print(p_tag)
# 输出: <p id="intro-p" class="important">This is a paragraph.</p>
a_tag = make_html_tag('a', 'Click me!', href='https://www.example.com', target='_blank')
print(a_tag)
# 输出: <a href="https://www.example.com" target="_blank">Click me!</a>
div_tag = make_html_tag('div',
make_html_tag('h1', 'Welcome!'),
make_html_tag('p', 'This is inside a div.'),
id='container')
print(div_tag)
# 输出: <div id="container"><h1>Welcome!</h1><p>This is inside a div.</p></div>
这个例子充分展示了 *args
和 **kwargs
如何让我们创建一个既强大又灵活的函数。
5.2 常见问题与排查建议
SyntaxError: positional argument follows keyword argument
:- 原因: 在函数调用时,将位置参数放在了关键字参数之后。
- 解决: 确保所有位置参数都在关键字参数之前。
TypeError: function_name() got an unexpected keyword argument 'xxx'
:- 原因: 传递了一个函数定义中没有对应参数名的关键字参数,并且该函数没有定义
**kwargs
来接收它。 - 解决: 检查关键字参数名是否拼写正确,或者如果需要接收任意关键字参数,请在函数定义中添加
**kwargs
。
- 原因: 传递了一个函数定义中没有对应参数名的关键字参数,并且该函数没有定义
- 参数顺序混淆:
- 原因: 在定义函数时没有遵循
位置参数 -> *args -> **kwargs
的顺序。 - 解决: 调整函数定义中的参数顺序。
- 原因: 在定义函数时没有遵循
- 解包错误:
- 原因: 尝试使用
*
解包字典,或使用**
解包列表/元组;或者解包后的参数数量/名称与函数定义不匹配。 - 解决: 确保使用
*
解包序列,**
解包字典,并检查解包结果是否符合函数签名。
- 原因: 尝试使用
六、总结
恭喜你!通过本文的学习,你已经掌握了 Python 函数参数中的高级技巧——可变参数和参数解包。现在,让我们来回顾一下核心知识点:
- 可变位置参数 (
*args
):- 使用
*
定义,允许函数接收任意数量的位置参数。 - 在函数内部,
args
是一个包含这些参数的元组 (tuple)。
- 使用
- 可变关键字参数 (
**kwargs
):- 使用
**
定义,允许函数接收任意数量的关键字参数。 - 在函数内部,
kwargs
是一个包含这些参数的字典 (dictionary)。
- 使用
- 参数定义顺序:
- 必须遵循严格的顺序:标准位置参数 ->
*args
-> 关键字参数 ->**kwargs
。
- 必须遵循严格的顺序:标准位置参数 ->
- 参数调用顺序:
- 位置参数必须在关键字参数之前。
- 参数解包:
- 在函数调用时,使用
*
可以将列表或元组解包成位置参数。 - 在函数调用时,使用
**
可以将字典解包成关键字参数。
- 在函数调用时,使用
掌握了 *args
和 **kwargs
,你就如同拥有了为函数装备“无限弹药”的能力,能够编写出更加通用、灵活和强大的代码,轻松应对各种复杂的参数传递场景。在下一篇文章中,我们将探讨函数的另一个重要组成部分——返回值 (return
),学习如何让函数有效地输出处理结果。敬请期待!