Python 基础语法与数据类型(八) - 函数参数:位置参数、关键字参数、默认参数、可变参数 (*args, **kwargs)
文章目录
- 1. 位置参数 (Positional Arguments)
- 2. 关键字参数 (Keyword Arguments)
- 3. 默认参数 (Default Parameters)
- 4. 可变参数 (`*args`, `**kwargs`)
- 4.1 可变位置参数 (`*args`)
- 4.2 可变关键字参数 (`**kwargs`)
- 5. 参数的组合与顺序
- 6. 解包实参列表/字典 (`*` 和 `**` 在调用时使用)
- 总结
- 练习题
- 练习题答案
- 关注我,带你从基础入门到大模型开发全流程细节学习
在上一篇博客中,我们学习了如何定义和调用函数,以及如何使用参数传递数据和使用
return
返回结果。当时我们主要接触的是最基本的参数形式。事实上,Python 提供了多种灵活的方式来处理函数参数,让函数能够适应不同的调用场景。
本篇将详细介绍 Python 函数参数的四种主要类型:位置参数、关键字参数、默认参数和可变参数 (*args
, **kwargs
)。
1. 位置参数 (Positional Arguments)
位置参数是最基础的参数类型。在函数调用时,实参会按照它们出现的位置顺序依次赋值给函数定义中的参数。位置参数是必需的,除非为它们提供了默认值。
# 定义一个函数,接受两个位置参数
def describe_pet(animal_type, pet_name):"""显示宠物的信息。Args:animal_type: 宠物的类型 (例如: 狗, 猫)。pet_name: 宠物的名字。"""print(f"\n我有一只 {animal_type}。")print(f"它的名字叫 {pet_name}。")# 调用函数,实参按照位置顺序传递
describe_pet("仓鼠", "哈姆太郎") # "仓鼠" 赋值给 animal_type, "哈姆太郎" 赋值给 pet_name
describe_pet("狗", "旺财") # "狗" 赋值给 animal_type, "旺财" 赋值给 pet_name# 注意:如果实参数量或顺序不匹配,会引发 TypeError
# describe_pet("猫") # 缺少 pet_name 参数,会报错
# describe_pet("大黄", "牛") # 顺序颠倒,虽然不报错但逻辑不对
输出:
我有一只 仓鼠。
它的名字叫 哈姆太郎。我有一只 狗。
它的名字叫 旺财。
位置参数要求调用时提供的实参数量和定义时的参数数量严格一致,并且顺序要匹配。
2. 关键字参数 (Keyword Arguments)
关键字参数允许你在函数调用时,使用 参数名=值
的语法来传递实参。使用关键字参数时,实参的顺序就不重要了,Python 会根据参数名将值正确地赋给对应的参数。
# 使用上面定义的 describe_pet 函数
# 调用时使用关键字参数
describe_pet(animal_type="兔子", pet_name="小白") # 顺序与定义时不同,但通过关键字指定
describe_pet(pet_name="皮卡丘", animal_type="老鼠") # 顺序再次不同,依然正确# 也可以混合使用位置参数和关键字参数,但位置参数必须在关键字参数之前
describe_pet("金鱼", pet_name="泡泡") # "金鱼" 是位置参数,"泡泡" 是关键字参数# 注意:位置参数必须在关键字参数之前
# describe_pet(pet_name="丁丁", "鹦鹉") # 会引发 SyntaxError
输出:
我有一只 兔子。
它的名字叫 小白。我有一只 老鼠。
它的名字叫 皮卡丘。我有一只 金鱼。
它的名字叫 泡泡。
关键字参数提高了函数调用的可读性,尤其当函数有很多参数时,不容易弄错参数的含义。
3. 默认参数 (Default Parameters)
你可以在函数定义时,为参数指定一个默认值。这样,在调用函数时,如果该参数没有被传递实参,就会使用其默认值。如果传递了实参,则实参会覆盖默认值。
默认参数必须定义在所有位置参数之后。
# 定义一个带默认参数的函数
def describe_pet_with_default(pet_name, animal_type="狗"): # animal_type 有默认值 "狗""""显示宠物的信息 (默认宠物类型为狗)。Args:pet_name: 宠物的名字。animal_type: 宠物的类型 (默认为 "狗")。"""print(f"\n我有一只 {animal_type}。")print(f"它的名字叫 {pet_name}。")# 调用函数,不提供 animal_type,使用默认值
describe_pet_with_default("旺财") # animal_type 使用默认值 "狗"# 调用函数,提供 animal_type,覆盖默认值
describe_pet_with_default("咪咪", animal_type="猫") # animal_type 变为 "猫"
describe_pet_with_default(pet_name="小黄", animal_type="鸭子") # 使用关键字参数指定# 注意:带默认值的参数必须放在不带默认值的参数后面
# def wrong_order(p1=10, p2): # 会引发 SyntaxError
# pass
输出:
我有一只 狗。
它的名字叫 旺财。我有一只 猫。
它的名字叫 咪咪。我有一只 鸭子。
它的名字叫 小黄。
⚠️ 重要提示:默认参数的陷阱 (Mutable Default Arguments)
切勿使用可变对象(如列表 []
或字典 {}
)作为默认参数的默认值。因为函数的默认值在函数被定义时只会被创建一次,而不是每次调用时创建。这意味着如果你在函数内部修改了这个默认值(比如向默认列表添加元素),这个修改会在后续所有不传递该参数的函数调用中保留。
# 这是一个错误的示例!
def append_item_wrong(item, my_list=[]): # my_list 的默认值 [] 只创建一次"""向列表添加元素 (错误的默认参数用法)。"""my_list.append(item)print(my_list)append_item_wrong(1) # 输出: [1]
append_item_wrong(2) # 期望是 [2],实际输出: [1, 2]! 因为使用的是同一个默认列表对象
append_item_wrong(3, [4, 5]) # 正常工作,因为提供了新的列表 [4, 5]
append_item_wrong(4) # 输出: [1, 2, 4]!
正确的做法: 使用 None
作为默认值,然后在函数内部检查参数是否为 None
,如果是,则创建一个新的可变对象。
# 正确的使用方法
def append_item_correct(item, my_list=None):"""向列表添加元素 (正确的默认参数用法)。"""if my_list is None: # 检查是否使用了默认值my_list = [] # 如果是默认值,创建一个新的空列表my_list.append(item)print(my_list)append_item_correct(1) # 输出: [1]
append_item_correct(2) # 输出: [2] (每次调用都创建了新的列表)
append_item_correct(3, [4, 5]) # 正常工作,因为提供了新的列表 [4, 5]
append_item_correct(4) # 输出: [4]
这是一种非常常见的 Python 编程模式,务必记住。
4. 可变参数 (*args
, **kwargs
)
有时候你不知道函数会被调用时会传递多少个位置参数或关键字参数。可变参数可以解决这个问题。
4.1 可变位置参数 (*args
)
使用 *参数名
(通常使用 *args
) 定义的参数可以接收任意数量的位置参数。这些位置参数会被收集到一个元组 (tuple) 中,然后赋值给 *args
参数。
# 定义一个函数,接受任意数量的位置参数
def sum_all(*numbers):"""计算任意数量数字的总和。Args:*numbers: 任意数量的数字 (会被收集到一个元组中)。Returns:所有数字的总和。"""print(f"numbers 实际上是一个元组: {numbers}, 类型是: {type(numbers)}")total = 0for num in numbers:total += numreturn total# 调用函数,传递不同数量的位置参数
print(sum_all(1, 2, 3)) # 输出: numbers 实际上是一个元组: (1, 2, 3), 类型是: <class 'tuple'>\n6
print(sum_all(10, 20)) # 输出: numbers 实际上是一个元组: (10, 20), 类型是: <class 'tuple'>\n30
print(sum_all()) # 输出: numbers 实际上是一个元组: (), 类型是: <class 'tuple'>\n0
print(sum_all(1, 2, 3, 4, 5, 6))
4.2 可变关键字参数 (**kwargs
)
使用 **参数名
(通常使用 **kwargs
) 定义的参数可以接收任意数量的关键字参数。这些关键字参数会被收集到一个字典 (dict) 中,其中键是参数名(字符串),值是对应的参数值,然后赋值给 **kwargs
参数。
**kwargs
通常用于接收函数不需要处理,但可能需要传递给其他函数的信息。
# 定义一个函数,接受任意数量的关键字参数
def display_info(**details):"""显示任意数量的详细信息。Args:**details: 任意数量的关键字参数 (会被收集到一个字典中)。"""print(f"details 实际上是一个字典: {details}, 类型是: {type(details)}")for key, value in details.items():print(f"{key}: {value}")# 调用函数,传递不同数量和名称的关键字参数
display_info(name="Alice", age=30, city="London")
display_info(product="Laptop", price=1200)
display_info()
输出:
details 实际上是一个字典: {'name': 'Alice', 'age': 30, 'city': 'London'}, 类型是: <class 'dict'>
name: Alice
age: 30
city: London
details 实际上是一个字典: {'product': 'Laptop', 'price': 1200}, 类型是: <class 'dict'>
product: Laptop
price: 1200
details 实际上是一个字典: {}, 类型是: <class 'dict'>
5. 参数的组合与顺序
在一个函数定义中,不同类型的参数必须按照特定的顺序排列:
- 位置参数 (必须提供的,没有默认值)
- 默认参数 (有默认值的位置参数)
- 可变位置参数 (
*args
) - 关键字参数 (Python 3 引入的强制关键字参数,通常放在
*
或*args
之后,用单独的*
标记) - 此处为基础篇,先不深入介绍强制关键字参数的语法,知道*args
后面的参数只能用关键字传递即可。 - 可变关键字参数 (
**kwargs
)
简化的常见顺序:必需位置参数 -> 默认参数 -> *args
-> **kwargs
def complex_function(arg1, arg2="default", *args, **kwargs):"""演示多种参数类型的组合。"""print(f"arg1 (位置参数): {arg1}")print(f"arg2 (默认参数): {arg2}")print(f"*args (可变位置参数元组): {args}")print(f"**kwargs (可变关键字参数字典): {kwargs}")# 调用示例
complex_function(10) # arg1=10, arg2="default", args=(), kwargs={}
# 输出:
# arg1 (位置参数): 10
# arg2 (默认参数): default
# *args (可变位置参数元组): ()
# **kwargs (可变关键字参数字典): {}complex_function(10, 20) # arg1=10, arg2=20, args=(), kwargs={}
# 输出:
# arg1 (位置参数): 10
# arg2 (默认参数): 20
# *args (可变位置参数元组): ()
# **kwargs (可变关键字参数字典): {}complex_function(10, 20, 30, 40) # arg1=10, arg2=20, args=(30, 40), kwargs={}
# 输出:
# arg1 (位置参数): 10
# arg2 (默认参数): 20
# *args (可变位置参数元组): (30, 40)
# **kwargs (可变关键字参数字典): {}complex_function(10, 20, 30, 40, key1="value1", key2=100) # args=(30, 40), kwargs={'key1': 'value1', 'key2': 100}
# 输出:
# arg1 (位置参数): 10
# arg2 (默认参数): 20
# *args (可变位置参数元组): (30, 40)
# **kwargs (可变关键字参数字典): {'key1': 'value1', 'key2': 100}complex_function(10, arg2="override_default", keyA="A", keyB="B") # arg2 覆盖默认值
# 输出:
# arg1 (位置参数): 10
# arg2 (默认参数): override_default
# *args (可变位置参数元组): ()
# **kwargs (可变关键字参数字典): {'keyA': 'A', 'keyB': 'B'}# 传递实参时的顺序规则:位置实参必须在关键字实参之前。
# complex_function(arg2="hello", 10) # 会引发 SyntaxError
6. 解包实参列表/字典 (*
和 **
在调用时使用)
你不仅可以在函数定义时使用 *
和 **
来收集参数,还可以在函数调用时使用它们来解包列表、元组或字典,将它们的内容作为独立的实参传递。
- 在列表或元组前加
*
会将其元素作为位置实参传递。 - 在字典前加
**
会将其键值对作为关键字实参传递。
# 定义一个接受位置参数和关键字参数的函数
def greet(name, greeting="Hello", **kwargs):message = f"{greeting}, {name}!"if kwargs:message += " Your extra info: " + ", ".join([f"{k}={v}" for k, v in kwargs.items()])print(message)# 使用列表/元组解包传递位置实参
my_args_list = ["Bob", "Hi"]
greet(*my_args_list) # 相当于 greet("Bob", "Hi")
# 输出: Hi, Bob!my_args_tuple = ("Charlie",) # 单元素元组,注意逗号
greet(*my_args_tuple) # 相当于 greet("Charlie")
# 输出: Hello, Charlie!# 使用字典解包传递关键字实参
my_kwargs_dict = {"greeting": "Hola", "city": "Madrid"}
greet("David", **my_kwargs_dict) # 相当于 greet("David", greeting="Hola", city="Madrid")
# 输出: Hola, David! Your extra info: city=Madrid# 结合使用解包
all_args = ["Eve", "Hey"]
all_kwargs = {"job": "Artist", "country": "France"}
greet(*all_args, **all_kwargs) # 相当于 greet("Eve", "Hey", job="Artist", country="France")
# 输出: Hey, Eve! Your extra info: job=Artist, country=France
解包实参非常有用,当你需要将一个列表或字典中的数据直接传递给一个需要多个独立参数的函数时。
总结
理解 Python 函数参数的各种类型,能让你编写出更加灵活、健壮和易于使用的函数:
- 位置参数: 按顺序匹配,必需。
- 关键字参数: 按名称匹配,顺序无关,提高可读性。
- 默认参数: 提供默认值,让参数变为可选,注意可变默认值的陷阱。
- 可变参数 (
*args
,**kwargs
): 接收任意数量的位置参数(作为元组)或关键字参数(作为字典),增加了函数的通用性。 - 了解参数的组合顺序和实参的解包 (
*
,**
在调用时使用)。
熟练运用这些参数类型,是写出更“Pythonic”代码的重要一步。
练习题
尝试独立完成以下练习题,并通过答案进行对照:
-
位置参数和关键字参数:
- 定义一个函数
create_profile(name, age, location)
。 - 至少用两种不同的方式(纯位置参数、混合位置和关键字参数、纯关键字参数)调用这个函数,并打印结果。
- 定义一个函数
-
默认参数:
- 定义一个函数
send_email(recipient, subject="No Subject", body="")
。 - 调用这个函数,只提供收件人。
- 调用这个函数,提供收件人和主题。
- 调用这个函数,提供所有参数。
- 定义一个函数
-
默认参数陷阱:
- 编写一个函数
Notes(item, my_list=[])
(故意使用错误的可变默认参数)。 - 调用这个函数两次,只传入
item
,观察输出。 - 按照正确的方式修改这个函数,使用
None
作为默认值,并再次测试。
- 编写一个函数
-
可变位置参数 (
*args
):- 定义一个函数
calculate_average(*numbers)
,它接受任意数量的数字,计算并返回它们的平均值。 - 调用这个函数,传入不同数量的数字,并打印结果。
- 定义一个函数
-
可变关键字参数 (
**kwargs
):- 定义一个函数
print_config(**config_items)
,它接受任意数量的关键字参数,并打印每个配置项及其值,格式为 “Key: Value”。 - 调用这个函数,传入不同的配置信息。
- 定义一个函数
-
多种参数类型组合:
- 定义一个函数
user_summary(username, status="active", *roles, **user_info)
。 - 调用这个函数,至少包含用户名。
- 调用这个函数,提供用户名和状态。
- 调用这个函数,提供用户名、状态、一些角色(作为位置参数)和一些额外信息(作为关键字参数)。
- 定义一个函数
-
实参解包:
- 定义一个函数
draw_point(x, y, color="black")
。 - 创建一个列表
point_coords = [10, 20]
。使用*
解包这个列表调用draw_point
。 - 创建一个字典
point_style = {"color": "red"}
。使用**
解包这个字典调用draw_point
。 - 创建一个列表
coords = [30, 40]
和一个字典style = {"color": "blue", "size": "medium"}
。尝试使用*
和**
同时解包调用draw_point
(注意哪些参数会被传递)。
- 定义一个函数
练习题答案
1. 位置参数和关键字参数:
# 1. 位置参数和关键字参数:
def create_profile(name, age, location):"""创建并打印用户资料。"""print(f"--- 用户资料 ---")print(f"姓名: {name}")print(f"年龄: {age}")print(f"地点: {location}")print("-" * 15)# 纯位置参数调用
create_profile("Alice", 30, "New York")# 混合位置和关键字参数调用
create_profile("Bob", location="Paris", age=25) # 位置参数在前# 纯关键字参数调用
create_profile(location="Tokyo", name="Charlie", age=35)
2. 默认参数:
# 2. 默认参数:
def send_email(recipient, subject="No Subject", body=""):"""模拟发送邮件。"""print(f"发送邮件给: {recipient}")print(f"主题: {subject}")print(f"内容: {body}")print("-" * 10)# 只提供收件人
send_email("alice@example.com")# 提供收件人和主题
send_email("bob@example.com", subject="Meeting Reminder")# 提供所有参数
send_email("charlie@example.com", subject="Project Update", body="Please see attached report.")
3. 默认参数陷阱:
# 3. 默认参数陷阱 (错误示例):
def add_to_list_wrong(item, my_list=[]):"""向列表添加元素 (错误的默认参数用法)。"""print(f"Original list ID (inside): {id(my_list)}") # 打印列表的内存地址my_list.append(item)print(f"List after append: {my_list}")print(f"Modified list ID (inside): {id(my_list)}")print("-" * 10)print("--- 错误示例 ---")
add_to_list_wrong(1)
add_to_list_wrong(2) # 注意看输出,列表累加了!而且内存地址一样
add_to_list_wrong(3, [4, 5]) # 提供了新列表,内存地址不同# 3. 默认参数陷阱 (正确示例):
def add_to_list_correct(item, my_list=None):"""向列表添加元素 (正确的默认参数用法)。"""if my_list is None:my_list = [] # 如果是默认值,创建一个新的空列表print(f"Original list ID (inside): {id(my_list)}") # 打印列表的内存地址my_list.append(item)print(f"List after append: {my_list}")print(f"Modified list ID (inside): {id(my_list)}")print("-" * 10)print("--- 正确示例 ---")
add_to_list_correct(1) # 创建新的列表
add_to_list_correct(2) # 创建新的列表
add_to_list_correct(3, [4, 5]) # 提供了新列表,内存地址不同
4. 可变位置参数 (*args
):
# 4. 可变位置参数 (*args):
def calculate_average(*numbers):"""计算任意数量数字的平均值。"""print(f"Received numbers: {numbers}")if not numbers: # 检查元组是否为空,避免除以零return 0return sum(numbers) / len(numbers)# 调用函数
print(f"平均值 (1, 2, 3): {calculate_average(1, 2, 3)}")
print(f"平均值 (10, 20, 30, 40): {calculate_average(10, 20, 30, 40)}")
print(f"平均值 (): {calculate_average()}") # 调用时不传参数
5. 可变关键字参数 (**kwargs
):
# 5. 可变关键字参数 (**kwargs):
def print_config(**config_items):"""打印配置项。"""print("--- Configuration ---")if not config_items:print("No configuration items provided.")for key, value in config_items.items():print(f"{key}: {value}")print("-" * 20)# 调用函数
print_config(database="mydb", user="admin", password="secret")
print_config(host="localhost", port=8080)
print_config() # 调用时不传关键字参数
6. 多种参数类型组合:
# 6. 多种参数类型组合:
def user_summary(username, status="active", *roles, **user_info):"""打印用户总结信息。Args:username: 用户名 (位置参数)。status: 用户状态 (默认参数,默认为 "active")。*roles: 用户角色 (可变位置参数元组)。**user_info: 额外用户信息 (可变关键字参数字典)。"""print(f"用户名: {username}")print(f"状态: {status}")print(f"角色: {roles}") # roles 是一个元组print(f"额外信息: {user_info}") # user_info 是一个字典print("-" * 20)# 调用示例
user_summary("Alice") # 只提供必需位置参数
user_summary("Bob", "inactive") # 提供位置参数和覆盖默认参数
user_summary("Charlie", "active", "admin", "editor") # 提供位置参数和可变位置参数
user_summary("David", "active", "viewer", country="USA", city="SF") # 提供位置参数,可变位置参数,和可变关键字参数
user_summary("Eve", address="123 Main St", phone="555-1234") # 仅提供位置参数和可变关键字参数
7. 实参解包:
# 7. 实参解包:
def draw_point(x, y, color="black"):"""模拟绘制一个点。"""print(f"绘制点: ({x}, {y}) 颜色: {color}")# 使用 * 解包列表传递位置实参
point_coords = [10, 20]
draw_point(*point_coords) # 相当于 draw_point(10, 20)# 使用 ** 解包字典传递关键字实参
point_style = {"color": "red"}
draw_point(30, 40, **point_style) # 相当于 draw_point(30, 40, color="red")# 结合使用 * 和 ** 解包
coords = [50, 60]
style = {"color": "blue", "size": "medium"} # size 参数 draw_point 不接受# 注意:** 解包的字典键必须是函数接受的关键字参数名
# draw_point(*coords, **style) # 相当于 draw_point(50, 60, color="blue", size="medium")
# 这里的 size="medium" 会导致 TypeError,因为 draw_point 函数没有 size 参数!
# 我们修改 draw_point 函数来接受 **kwargs 以演示
def draw_point_flexible(x, y, color="black", **extra_args):print(f"绘制点: ({x}, {y}) 颜色: {color}", end="")if extra_args:print(" 额外信息:", extra_args)else:print()coords = [50, 60]
style = {"color": "blue", "size": "medium"}
draw_point_flexible(*coords, **style) # 现在 size 会被 **extra_args 捕获coords2 = (70, 80) # 元组也可以用 * 解包
style2 = {"color": "green"}
draw_point_flexible(*coords2, **style2)