Python快速入门专业版(二十八):函数参数进阶:默认参数与可变参数(*args/**kwargs)
目录
- 引
- 一、默认参数:给函数参数设置“默认值”
- 1. 基本语法与使用示例
- 示例1:带默认参数的乘法函数
- 2. 默认参数的核心规则:必须放在非默认参数之后
- 示例2:默认参数位置错误(报错)
- 3. 默认参数的“可变对象陷阱”:默认值只初始化一次
- 示例3:默认参数为可变对象(列表)的陷阱
- 解决方案:默认值用None,在函数内初始化可变对象
- 4. 默认参数的适用场景
- 二、可变参数*args:接收任意个位置参数
- 1. 基本语法与工作原理
- 示例4:用*args接收任意个位置参数
- 2. 实战:用*args实现自定义求和函数
- 示例5:自定义求和函数(支持任意个数相加)
- 3. *args与普通参数的混合使用
- 示例6:混合使用普通参数和*args
- 三、可变参数**kwargs:接收任意个关键字参数
- 1. 基本语法与工作原理
- 示例7:用**kwargs接收任意个关键字参数
- 2. 实战:用**kwargs处理用户动态属性
- 示例8:处理用户动态属性的函数
- 3. **kwargs与其他参数的混合使用
- 示例9:所有参数类型的混合使用(正确顺序)
- 示例10:** kwargs位置错误(报错)
- 四、*args与**kwargs的高级应用:参数转发
- 1. 封装函数时的参数转发
- 示例11:封装print函数,转发所有参数
- 2. 装饰器中的参数转发
- 示例12:用装饰器记录函数调用日志(参数转发)
- 五、*args与**kwargs的命名规范与最佳实践
- 1. 命名规范
- 2. 最佳实践
- 实践1:优先明确参数,再用可变参数
- 实践2:限制*args和**kwargs的使用场景
- 实践3:为可变参数添加文档说明
- 六、综合案例:灵活的数据分析函数
- 1. 案例代码实现
- 2. 案例解析
- 七、总结与提升
- 进阶练习
引
在函数参数的世界里,基础的位置参数和关键字参数只是起点。当面对更复杂的场景——比如希望函数某些参数有默认值、需要处理不确定数量的输入,或者灵活传递参数给其他函数时,默认参数和可变参数(*args/**kwargs)就成了不可或缺的工具。它们不仅能让函数更灵活、更易用,还能大幅提升代码的复用性和扩展性。
本文将深入解析默认参数的设计逻辑与常见陷阱,系统讲解*args和**kwargs的工作原理与使用场景,通过“动态求和”“灵活配置”等实战案例,帮助你掌握这些进阶参数的精髓,让函数设计从“满足基本需求”升级到“应对复杂场景”。
一、默认参数:给函数参数设置“默认值”
在很多场景下,函数的某些参数在大多数情况下取值固定,只有少数情况需要修改。例如:计算商品总价时,税率通常固定为13%;发送消息时,默认接收者是系统管理员。这时,默认参数就能派上用场——在函数定义时为参数指定默认值,调用时可省略该参数,使用默认值;若有特殊需求,再显式传递新值覆盖默认值。
1. 基本语法与使用示例
默认参数的定义方式非常简单:在函数形参后加=默认值
即可。调用函数时,若不传递该参数,则使用默认值;若传递,则使用新值。
示例1:带默认参数的乘法函数
def multiply(a, b=10):"""计算a乘以b,b的默认值为10"""return a * b# 调用时省略b,使用默认值10
result1 = multiply(5) # 等价于multiply(5, 10)
print(f"5 × 10(默认) = {result1}") # 输出:5 × 10(默认) = 50# 调用时传递b,覆盖默认值
result2 = multiply(5, 3)
print(f"5 × 3(自定义) = {result2}") # 输出:5 × 3(自定义) = 15
解析:
- 函数定义
def multiply(a, b=10)
中,b
是默认参数,默认值为10;a
是非默认参数(必须传递的参数)。 - 调用
multiply(5)
时,只传递了a=5
,b
自动使用默认值10,计算5×10=50
。 - 调用
multiply(5, 3)
时,b=3
覆盖默认值,计算5×3=15
。
这种设计让函数在“大多数常规场景”下调用更简洁,同时保留了“特殊场景”下的灵活性。
2. 默认参数的核心规则:必须放在非默认参数之后
Python规定:所有默认参数必须位于非默认参数的后面。这是因为函数解析参数时,会先处理位置参数,再处理默认参数。若默认参数放在前面,会导致参数绑定混乱。
示例2:默认参数位置错误(报错)
# 错误:默认参数b放在非默认参数a前面
def multiply(b=10, a):return a * b
报错信息:
SyntaxError: non-default argument follows default argument
解析:错误原因是“非默认参数跟在默认参数之后”。假设允许这种定义,调用multiply(5)
时,解释器无法判断5
是给b
(默认参数)还是a
(非默认参数),导致歧义。因此Python从语法层面禁止了这种写法。
正确的做法是将所有非默认参数放在前面,默认参数放在后面,例如def func(a, b, c=0, d=1): ...
。
3. 默认参数的“可变对象陷阱”:默认值只初始化一次
默认参数的默认值在函数定义时就会被初始化,而非每次调用时重新生成。这对整数、字符串等不可变对象没问题,但对列表、字典等可变对象可能导致意外结果。
示例3:默认参数为可变对象(列表)的陷阱
def add_item(item, items=[]):"""向列表中添加元素,默认列表为空"""items.append(item)return items# 第一次调用:预期返回[1]
print(add_item(1)) # 输出:[1]# 第二次调用:预期返回[2],实际返回[1, 2]
print(add_item(2)) # 输出:[1, 2]
解析:
- 函数定义时,
items=[]
创建了一个空列表,这个列表对象在内存中只会被创建一次。 - 第一次调用
add_item(1)
时,items
指向这个列表,添加1
后变为[1]
。 - 第二次调用时,
items
仍然指向同一个列表(而非重新创建空列表),添加2
后变为[1, 2]
,与预期不符。
这就是“可变对象陷阱”——默认参数为可变对象时,多次调用会共享同一个对象,导致状态累积。
解决方案:默认值用None,在函数内初始化可变对象
def add_item(item, items=None):"""修复可变对象陷阱:用None作为默认值,在函数内初始化列表"""if items is None:items = [] # 每次调用时重新创建空列表items.append(item)return items# 多次调用结果符合预期
print(add_item(1)) # 输出:[1]
print(add_item(2)) # 输出:[2]
解析:将默认值设为None
(不可变对象),每次调用时若未传递items
,则在函数内重新创建空列表,避免了多次调用共享同一个列表的问题。这是处理默认参数为可变对象的标准做法。
4. 默认参数的适用场景
默认参数非常适合以下场景:
- 常用参数固定值:如
def connect(host, port=8080)
,端口号8080是最常用的,无需每次指定。 - 可选功能开关:如
def download(url, proxy=False)
,proxy=False
表示默认不使用代理,需要时传递proxy=True
。 - 参数兼容性:当函数需要新增参数时,给新参数设置默认值,避免影响旧的调用代码(向后兼容)。
二、可变参数*args:接收任意个位置参数
在实际开发中,我们经常会遇到“函数需要处理的参数数量不确定”的情况。例如:实现一个求和函数,支持2个数相加、3个数相加,甚至10个数相加;定义一个打印函数,能接收任意个待打印的对象。这时,可变参数*args就能解决这个问题——它可以接收任意数量的位置参数,并将它们封装成一个元组。
1. 基本语法与工作原理
*args
的语法是在参数名前加*
(星号),表示该参数是可变位置参数。调用函数时,传递的所有位置参数都会被收集到args
中,作为一个元组存在。
示例4:用*args接收任意个位置参数
def print_args(*args):"""打印接收的所有位置参数"""print("参数类型:", type(args)) # 输出:<class 'tuple'>print("参数值:", args)# 传递不同数量的位置参数
print_args(1, 2, 3) # 输出:参数值:(1, 2, 3)
print_args("a", "b") # 输出:参数值:('a', 'b')
print_args(True, 3.14, "hello") # 输出:参数值:(True, 3.14, 'hello')
print_args() # 输出:参数值:()(空元组)
解析:
- 函数定义
def print_args(*args)
中,*args
表示“收集所有位置参数”。 - 无论传递0个、1个还是多个位置参数,
args
都会将它们打包成元组(即使只有一个参数,也是元组(x,)
)。 - 若未传递位置参数,
args
是一个空元组()
,不会报错。
这种特性让函数能灵活处理“参数数量不确定”的场景,无需提前定义固定数量的形参。
2. 实战:用*args实现自定义求和函数
最典型的*args
应用是实现支持任意个数相加的求和函数,替代固定参数的add(a, b)
、add(a, b, c)
等繁琐定义。
示例5:自定义求和函数(支持任意个数相加)
def custom_sum(*args):"""计算任意个数字的和"""# 处理无参数的情况if not args: # 判断元组是否为空return 0total = 0for num in args: # 遍历元组中的所有参数# 只累加数字类型,忽略非数字(可选逻辑)if isinstance(num, (int, float)):total += numelse:print(f"警告:{num}不是数字,已忽略")return total# 测试不同数量和类型的参数
print("求和结果:", custom_sum(1, 2, 3)) # 输出:6
print("求和结果:", custom_sum(10, 20, 30, 40)) # 输出:100
print("求和结果:", custom_sum(3.14, 2.71)) # 输出:5.85
print("求和结果:", custom_sum(5, "a", 3)) # 输出警告,结果:8
print("求和结果:", custom_sum()) # 输出:0
解析:
*args
收集所有传递的位置参数(如1,2,3
),形成元组(1,2,3)
。- 函数通过遍历元组
args
,累加所有数字类型的元素,实现“任意个数求和”。 - 增加了对非数字参数的处理(忽略并警告)和无参数情况的兼容(返回0),让函数更健壮。
相比固定参数的实现(如def sum(a, b, c=0, d=0): ...
),*args
版本的扩展性更好——无论需要加多少个数,函数定义都无需修改。
3. *args与普通参数的混合使用
*args
可以与普通参数(位置参数、默认参数)混合使用,但*args
必须放在普通位置参数之后、默认参数之前,遵循“位置参数→*args→默认参数”的顺序。
示例6:混合使用普通参数和*args
def func(a, b, *args, c=0):"""普通参数a、b → *args → 默认参数c"""print(f"a={a}, b={b}, args={args}, c={c}")# 调用:a和b必须传递,args接收剩余位置参数,c可选
func(1, 2) # 输出:a=1, b=2, args=(), c=0
func(1, 2, 3, 4) # 输出:a=1, b=2, args=(3, 4), c=0
func(1, 2, 3, 4, c=5) # 输出:a=1, b=2, args=(3, 4), c=5
解析:
- 前两个参数
a
和b
是普通位置参数,必须传递。 *args
接收剩余的位置参数(如3,4
被收集为(3,4)
)。- 默认参数
c
放在最后,可省略或通过关键字传递。
这种混合方式既能保证核心参数必须传递,又能灵活接收额外的位置参数,适合“基础参数固定+附加参数灵活”的场景。
三、可变参数**kwargs:接收任意个关键字参数
*args
解决了“任意个位置参数”的问题,但无法处理“任意个关键字参数”。例如:定义一个函数,需要接收用户的任意属性(姓名、年龄、性别、职业等,数量不确定);封装一个API调用函数,需要支持接口的所有可选参数(格式、页码、筛选条件等)。这时,** 可变参数kwargs就派上用场了——它可以接收任意数量的关键字参数,并将它们封装成一个字典。
1. 基本语法与工作原理
**kwargs
的语法是在参数名前加**
(两个星号),表示该参数是可变关键字参数。调用函数时,传递的所有关键字参数都会被收集到kwargs
中,作为一个字典存在(键是参数名,值是参数值)。
示例7:用**kwargs接收任意个关键字参数
def print_kwargs(**kwargs):"""打印接收的所有关键字参数"""print("参数类型:", type(kwargs)) # 输出:<class 'dict'>print("参数值:", kwargs)# 传递不同数量的关键字参数
print_kwargs(name="小明", age=20) # 输出:{'name': '小明', 'age': 20}
print_kwargs(city="北京", temperature=25) # 输出:{'city': '北京', 'temperature': 25}
print_kwargs() # 输出:{}(空字典)
解析:
- 函数定义
def print_kwargs(**kwargs)
中,**kwargs
表示“收集所有关键字参数”。 - 无论传递0个、1个还是多个关键字参数,
kwargs
都会将它们打包成字典(键是参数名,值是参数值)。 - 若未传递关键字参数,
kwargs
是一个空字典{}
,不会报错。
**kwargs
的灵活性在于:函数无需提前知道会接收哪些关键字参数,完全由调用者决定,适合处理“动态属性”或“可选配置”。
2. 实战:用**kwargs处理用户动态属性
假设需要定义一个函数,打印用户的信息,但用户属性不固定(可能有姓名、年龄、性别,也可能有职业、爱好等)。用**kwargs
可以轻松实现这一需求。
示例8:处理用户动态属性的函数
def print_user_attributes(** kwargs):"""打印用户的任意属性(关键字参数形式传递)"""if not kwargs:print("没有提供用户属性")returnprint("用户属性:")for key, value in kwargs.items(): # 遍历字典中的键值对print(f"- {key}:{value}")# 传递不同的用户属性
print_user_attributes(name="小红", age=18, gender="女", hobby="读书")
# 输出:
# 用户属性:
# - name:小红
# - age:18
# - gender:女
# - hobby:读书print_user_attributes(name="小刚", occupation="学生")
# 输出:
# 用户属性:
# - name:小刚
# - occupation:学生print_user_attributes() # 输出:没有提供用户属性
解析:
- 调用者可以传递任意数量、任意名称的关键字参数(如
name
、age
、hobby
等)。 - 函数通过遍历字典
kwargs
,打印所有属性,无需提前定义形参。 - 增加了对空参数的处理,让函数更友好。
这种方式特别适合处理“属性不固定”的对象,如用户信息、商品配置、API参数等,避免函数定义因参数过多而臃肿。
3. **kwargs与其他参数的混合使用
**kwargs
可以与普通参数、*args
混合使用,但其位置有严格限制:** 必须放在所有参数的最后**,遵循“位置参数→*args→默认参数→**kwargs”的顺序。
示例9:所有参数类型的混合使用(正确顺序)
def func(a, b, *args, c=0, **kwargs):"""完整参数顺序:位置参数→*args→默认参数→** kwargs"""print(f"a={a}, b={b}")print(f"args={args}, c={c}")print(f"kwargs={kwargs}")# 调用示例
func(1, 2, 3, 4, c=5, x=6, y=7)
输出结果:
a=1, b=2
args=(3, 4), c=5
kwargs={'x': 6, 'y': 7}
解析:
- 位置参数
a=1
、b=2
被优先处理。 - 剩余位置参数
3,4
被*args
收集为(3,4)
。 - 默认参数
c
被关键字参数c=5
覆盖。 - 最后,额外的关键字参数
x=6
、y=7
被**kwargs
收集为{'x':6, 'y':7}
。
示例10:** kwargs位置错误(报错)
# 错误:**kwargs放在其他参数前面
def func(** kwargs, a, b):pass
报错信息:
SyntaxError: invalid parameter name: '**kwargs'
解析:**kwargs
必须放在所有参数的最后,因为它需要收集“剩余的所有关键字参数”,若前面还有未处理的参数,会导致参数解析混乱。
四、*args与**kwargs的高级应用:参数转发
*args
和**kwargs
的一个强大用途是**“参数转发”**——将一个函数接收的参数原封不动地传递给另一个函数。这在封装函数、编写装饰器等场景中非常有用,能大幅减少代码冗余。
1. 封装函数时的参数转发
假设我们需要封装Python内置的print
函数,增加“打印前先输出分隔线”的功能,但又希望保留print
函数的所有参数(如sep
、end
等)。用*args
和**kwargs
可以轻松实现,无需重复定义print
的所有参数。
示例11:封装print函数,转发所有参数
def my_print(*args, **kwargs):"""封装print函数,先打印分隔线,再转发所有参数给print"""print("-" * 30) # 新增功能:打印分隔线print(*args, **kwargs) # 转发所有参数给内置print# 调用my_print,支持print的所有参数
my_print("Hello, World!") # 基本用法
my_print(1, 2, 3, sep=" | ") # 使用sep参数
my_print("结束", end="\n\n") # 使用end参数
输出结果:
------------------------------
Hello, World!
------------------------------
1 | 2 | 3
------------------------------
结束
解析:
my_print
的*args
接收所有位置参数(如"Hello, World!"
、1,2,3
),**kwargs
接收所有关键字参数(如sep=" | "
、end="\n\n"
)。- 通过
print(*args, **kwargs)
将参数转发给内置print
函数,*
和**
在这里的作用是“解包”(将元组解为位置参数,字典解为关键字参数)。 - 无需知道
print
函数的具体参数,就能完整保留其功能,实现灵活封装。
2. 装饰器中的参数转发
装饰器(用于增强函数功能的函数)是*args
和**kwargs
的典型应用场景。装饰器需要接收被装饰函数的所有参数,并传递给原函数,而*args
和**kwargs
能完美适配任意参数列表的函数。
示例12:用装饰器记录函数调用日志(参数转发)
def log_call(func):"""装饰器:记录函数调用的参数和返回值"""def wrapper(*args, **kwargs):# 记录调用参数print(f"调用函数:{func.__name__}")print(f"位置参数:{args}")print(f"关键字参数:{kwargs}")# 调用原函数,转发所有参数result = func(*args, **kwargs)# 记录返回值print(f"返回值:{result}\n")return resultreturn wrapper# 用装饰器增强函数
@log_call
def add(a, b):return a + b@log_call
def greet(name, message="Hello"):return f"{message}, {name}!"# 调用被装饰的函数
add(3, 5)
greet("小明", message="你好")
输出结果:
调用函数:add
位置参数:(3, 5)
关键字参数:{}
返回值:8调用函数:greet
位置参数:('小明',)
关键字参数:{'message': '你好'}
返回值:你好, 小明!
解析:
- 装饰器
log_call
中的wrapper
函数用*args
和**kwargs
接收任意参数,无论被装饰的函数(add
或greet
)有多少参数、是什么类型,都能兼容。 - 通过
func(*args, **kwargs)
将参数转发给原函数,确保原函数正常执行。 - 这种方式让装饰器具有通用性,可应用于任何函数,无需针对不同参数列表修改装饰器代码。
五、*args与**kwargs的命名规范与最佳实践
虽然*args
和**kwargs
中的args
和kwargs
只是约定俗成的名称(可以换成其他名字,如*params
、**kwargs_dict
),但遵循这些规范能让代码更易读——其他开发者看到*args
就知道是“可变位置参数”,看到**kwargs
就知道是“可变关键字参数”。
1. 命名规范
*args
:args
是“arguments”的缩写,代表“位置参数的集合”。**kwargs
:kwargs
是“keyword arguments”的缩写,代表“关键字参数的集合”。- 尽量使用默认名称,避免自定义名称(如
*my_args
),除非有特殊理由。
2. 最佳实践
实践1:优先明确参数,再用可变参数
不要过度依赖*args
和**kwargs
。如果函数参数数量和名称是确定的,优先使用普通参数(更清晰);只有在参数不确定时,才使用可变参数。
反例:参数明确却滥用*args
# 不好:明明只需要a和b,却用*args
def add(*args):return args[0] + args[1]# 好:明确参数更清晰
def add(a, b):return a + b
实践2:限制*args和**kwargs的使用场景
*args
适合:
- 处理数量不确定的同类型参数(如求和、求最大值)。
- 转发位置参数给其他函数。
**kwargs
适合:
- 处理动态属性或配置(如用户信息、API参数)。
- 转发关键字参数给其他函数。
实践3:为可变参数添加文档说明
由于*args
和**kwargs
的参数不明确,必须在文档字符串中说明其含义和预期类型,帮助使用者正确传递参数。
def send_request(url, *args, **kwargs):"""发送HTTP请求参数:url (str):请求URL*args:传递给requests.get的额外位置参数**kwargs:传递给requests.get的额外关键字参数(如headers、timeout等)"""import requestsreturn requests.get(url, *args, **kwargs)
六、综合案例:灵活的数据分析函数
我们通过一个“灵活的数据分析函数”案例,综合应用默认参数、*args
和**kwargs
,实现以下功能:
- 计算多个数据列的平均值(支持任意列,用
*args
接收)。 - 提供默认的精度设置(保留2位小数,用默认参数)。
- 支持额外的分析选项(如排序、过滤,用
**kwargs
接收)。
1. 案例代码实现
def custom_sum(*args):"""计算任意个数字的和"""# 处理无参数的情况if not args: # 判断元组是否为空return 0total = 0for num in args: # 遍历元组中的所有参数# 只累加数字类型,忽略非数字(可选逻辑)if isinstance(num, (int, float)):total += numelse:print(f"警告:{num}不是数字,已忽略")return total# 测试不同数量和类型的参数
print("求和结果:", custom_sum(1, 2, 3)) # 输出:6
print("求和结果:", custom_sum(10, 20, 30, 40)) # 输出:100
print("求和结果:", custom_sum(3.14, 2.71)) # 输出:5.85
print("求和结果:", custom_sum(5, "a", 3)) # 输出警告,结果:8
print("求和结果:", custom_sum()) # 输出:0
2. 案例解析
-*columns
的作用:接收任意数量的列名(如'math'
、'english'
),若未传递则分析所有列,灵活支持不同的分析需求。
- 默认参数
precision
:默认保留2位小数,调用时可通过precision=1
修改,兼顾默认行为和灵活性。 **kwargs
的作用:接收额外选项(如sort=True
、filter_min=90
),无需提前定义所有可能的选项,让函数易于扩展(后续可新增filter_max
、group
等选项,无需修改函数参数列表)。
运行结果展示了函数的灵活性:既能处理简单分析,也能支持复杂配置,充分体现了默认参数和可变参数的优势。
七、总结与提升
默认参数和可变参数(*args/**kwargs)是Python函数参数体系的重要扩展,它们让函数从“固定输入”走向“灵活适配”:
- 默认参数:为参数设置默认值,简化常规调用,语法
def func(a, b=10)
,注意必须放在非默认参数后,避免使用可变对象作为默认值。 *args
:收集任意个位置参数为元组,解决“参数数量不确定”问题,适合处理同类型可变输入或转发位置参数。**kwargs
:收集任意个关键字参数为字典,适合处理动态属性或转发关键字参数,必须放在所有参数最后。
这些参数的核心价值在于提升函数的灵活性和复用性,尤其在封装函数、编写通用工具或处理动态数据时不可或缺。
进阶练习
- 实现一个
merge_dicts
函数,接收任意个字典(*args
),返回一个合并后的字典(后面的字典覆盖前面的同名键)。 - 改进前面的
custom_sum
函数,支持关键字参数ignore_negative
(默认False
),当为True
时忽略负数。 - 编写一个装饰器
validate_params
,用于检查被装饰函数的参数是否符合指定规则(如age
必须大于0),通过**kwargs
接收规则配置。
通过这些练习,你将能更深入地理解这些参数的应用场景,让函数设计更贴近实际开发需求。记住:** 好的参数设计,能让函数既易用又强大**。