【Python-Day 18】玩转函数参数(下):*args 与 **kwargs 终极指南

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 (星号 * 加上参数名 argsargs 只是一个惯例,你也可以用其他名字,但强烈推荐使用 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 (两个星号 ** 加上参数名 kwargskwargs 也是惯例) 允许函数接收任意数量的关键字参数

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 函数参数的标准定义顺序是:

  1. 标准位置参数 (Positional arguments)
  2. 默认参数 (Default arguments) (它们也是位置参数,但必须在非默认参数之后)
  3. 可变位置参数 (*args)
  4. 标准关键字参数 (Keyword-only arguments) (Python 3.x 特有,必须在 *args 之后)
  5. 默认关键字参数 (Default keyword-only arguments)
  6. 可变关键字参数 (**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 流程图来形象地表示这个顺序:

Y
N
Y
N
Y
N
Y
N
标准位置参数
默认参数?
默认参数
`*args`?
`*args`
关键字参数?
标准关键字参数/默认关键字参数
`**kwargs`?
`**kwargs`
结束

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 常见问题与排查建议

  1. SyntaxError: positional argument follows keyword argument:
    • 原因: 在函数调用时,将位置参数放在了关键字参数之后。
    • 解决: 确保所有位置参数都在关键字参数之前。
  2. TypeError: function_name() got an unexpected keyword argument 'xxx':
    • 原因: 传递了一个函数定义中没有对应参数名的关键字参数,并且该函数没有定义 **kwargs 来接收它。
    • 解决: 检查关键字参数名是否拼写正确,或者如果需要接收任意关键字参数,请在函数定义中添加 **kwargs
  3. 参数顺序混淆:
    • 原因: 在定义函数时没有遵循 位置参数 -> *args -> **kwargs 的顺序。
    • 解决: 调整函数定义中的参数顺序。
  4. 解包错误:
    • 原因: 尝试使用 * 解包字典,或使用 ** 解包列表/元组;或者解包后的参数数量/名称与函数定义不匹配。
    • 解决: 确保使用 * 解包序列,** 解包字典,并检查解包结果是否符合函数签名。

六、总结

恭喜你!通过本文的学习,你已经掌握了 Python 函数参数中的高级技巧——可变参数和参数解包。现在,让我们来回顾一下核心知识点:

  1. 可变位置参数 (*args):
    • 使用 * 定义,允许函数接收任意数量的位置参数
    • 在函数内部,args 是一个包含这些参数的元组 (tuple)
  2. 可变关键字参数 (**kwargs):
    • 使用 ** 定义,允许函数接收任意数量的关键字参数
    • 在函数内部,kwargs 是一个包含这些参数的字典 (dictionary)
  3. 参数定义顺序:
    • 必须遵循严格的顺序:标准位置参数 -> *args -> 关键字参数 -> **kwargs
  4. 参数调用顺序:
    • 位置参数必须在关键字参数之前。
  5. 参数解包:
    • 在函数调用时,使用 * 可以将列表或元组解包成位置参数
    • 在函数调用时,使用 ** 可以将字典解包成关键字参数

掌握了 *args**kwargs,你就如同拥有了为函数装备“无限弹药”的能力,能够编写出更加通用、灵活和强大的代码,轻松应对各种复杂的参数传递场景。在下一篇文章中,我们将探讨函数的另一个重要组成部分——返回值 (return),学习如何让函数有效地输出处理结果。敬请期待!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴师兄大模型

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值