P3C-为什么95%初学者踩坑可变默认参数?合格程序员都用args和kwargs写出灵活函数

141 阅读31分钟

P3C-为什么95%初学者踩坑可变默认参数?合格程序员都用args和kwargs写出灵活函数

面试官问:"请解释一下 Python 函数中可变默认参数的问题,并说明如何正确使用 *args 和 **kwargs。" 错误回答:"默认参数就是给参数一个默认值,*args 和 **kwargs 可以接收任意参数..." 正确回答:"可变对象作为默认参数会导致所有函数调用共享同一个对象,应该使用 None 作为默认值。*args 收集位置参数为元组,**kwargs 收集关键字参数为字典,它们让函数更加灵活..." 为什么同样的知识,有人能答得头头是道,有人却支支吾吾?区别就在于对核心概念的理解深度。今天这份指南,让你彻底掌握 Python 参数的正确用法,写出像大厂开发者一样优雅的代码。

摘要

95% 初学者因使用可变默认参数踩坑,导致函数调用结果异常;合格程序员用 None 作为默认值,并用 *args 和 **kwargs 实现灵活函数。本文详解陷阱原理、正确方法和最佳实践,助你写出专业级代码。

目录


1. 可变默认参数的陷阱 🔥 Must(必做实践)

参考资源

1.1 问题描述

🐛 问题:在 Python 中,如果函数定义时使用可变对象(mutable object,如列表 list、字典 dict、集合 set)作为默认参数值,会导致所有函数调用共享同一个对象,从而产生意外的副作用(side effect)。

⚠️ 注意:这是 Python 中一个非常常见的陷阱,95% 的初学者都会踩到这个坑。问题在于 Python 的默认参数只在函数定义时计算一次,而不是每次调用时重新计算。

1.2 错误示例

错误做法:使用可变对象作为默认参数

# 错误示例:使用列表作为默认参数
def append_to(element, items=[]):
    """向列表中添加元素"""
    items.append(element)
    return items

# 第一次调用
result1 = append_to(1)
print(result1)  # 输出: [1]

# 第二次调用 - 问题出现了!
result2 = append_to(2)
print(result2)  # 输出: [1, 2] 而不是预期的 [2]!

# 第三次调用
result3 = append_to(3)
print(result3)  # 输出: [1, 2, 3] 而不是预期的 [3]!

# 验证它们是否是同一个对象
print(result1 is result2)  # 输出: True
print(result2 is result3)  # 输出: True

💡 问题分析:每次调用 append_to() 函数时,如果没有提供 items 参数,函数会使用同一个列表对象。这个列表对象在函数定义时创建,并在所有函数调用之间共享。

1.3 原因分析

🔑 核心原因:Python 的默认参数在函数定义时计算一次,而不是每次调用时重新计算。

函数对象的创建过程

在 Python 中,函数是第一类对象(first-class object),也就是说函数也是对象。当 Python 解释器执行 def 语句时,会在内存中创建一个函数对象(function object),并初始化函数的属性,包括默认参数列表(__defaults__)。

def func(numbers=[], num=1):
    numbers.append(num)
    return numbers

# 查看函数的默认参数
print(func.__defaults__)  # 输出: ([], 1)

# 第一次调用
result1 = func()
print(result1)  # 输出: [1]
print(func.__defaults__)  # 输出: ([1], 1) - 默认参数已经被修改!

# 第二次调用
result2 = func()
print(result2)  # 输出: [1, 1]
print(func.__defaults__)  # 输出: ([1, 1], 1) - 默认参数继续被修改!
内存共享机制

📊 内存共享示意图

函数定义时:
func.__defaults__[空列表对象] (id: 4330472840)

第一次调用 func():
func.__defaults__[1] (id: 4330472840) ← 同一个对象!

第二次调用 func():
func.__defaults__[1, 1] (id: 4330472840) ← 还是同一个对象!

第三次调用 func():
func.__defaults__[1, 1, 1] (id: 4330472840) ← 仍然是同一个对象!

💡 关键点

  • 默认参数 numbers=[] 在函数定义时创建,只创建一次
  • 所有函数调用共享这个列表对象
  • 每次调用时修改列表,会影响后续所有调用
  • 使用 id() 函数可以验证它们是否是同一个对象
def func(numbers=[], num=1):
    numbers.append(num)
    return numbers

# 验证对象 ID
print(id(func.__defaults__[0]))  # 输出: 4330472840

result1 = func()
print(id(result1))  # 输出: 4330472840 - 同一个对象!

result2 = func()
print(id(result2))  # 输出: 4330472840 - 还是同一个对象!
可变对象 vs 不可变对象

不可变对象(immutable object):数字(int、float)、字符串(str)、元组(tuple)等

  • 作为默认参数是安全的
  • 每次"修改"实际上是创建新对象
# 使用不可变对象作为默认参数 - 安全
def greet(name, message="Hello"):
    """问候函数,使用字符串作为默认参数"""
    return f"{message}, {name}!"

print(greet("Alice"))  # 输出: Hello, Alice!
print(greet("Bob"))    # 输出: Hello, Bob! - 正常

可变对象(mutable object):列表(list)、字典(dict)、集合(set)等

  • 作为默认参数是危险的
  • 所有调用共享同一个对象
# 使用可变对象作为默认参数 - 危险!
def add_item(item, items=[]):  # 列表是可变对象
    items.append(item)
    return items

print(add_item(1))  # 输出: [1]
print(add_item(2))  # 输出: [1, 2] - 问题!

2. 正确使用默认参数的方法 🔥 Must(必做实践)

参考资源

2.1 使用 None 作为默认值

正确做法:使用 None 作为默认值,然后在函数内部检查并初始化可变对象。

# 正确示例:使用 None 作为默认值
def append_to(element, items=None):
    """向列表中添加元素"""
    if items is None:
        items = []  # 每次调用时创建新的列表对象
    items.append(element)
    return items

# 第一次调用
result1 = append_to(1)
print(result1)  # 输出: [1]

# 第二次调用 - 正常了!
result2 = append_to(2)
print(result2)  # 输出: [2] - 符合预期!

# 第三次调用
result3 = append_to(3)
print(result3)  # 输出: [3] - 符合预期!

# 验证它们是否是不同的对象
print(result1 is result2)  # 输出: False - 不同的对象!
print(result2 is result3)  # 输出: False - 不同的对象!

💡 工作原理

  • 使用 None 作为默认值(None 是不可变对象,安全)
  • 在函数内部检查 if items is None
  • 如果为 None,则创建新的列表对象
  • 每次调用都会创建新的列表,不会共享

2.2 函数内部初始化

🎯 核心原则:在函数内部初始化可变对象,而不是在函数定义时。

方法一:使用 None 检查(推荐)
def create_user(name, hobbies=None):
    """创建用户,hobbies 是可选参数"""
    if hobbies is None:
        hobbies = []  # 每次调用时创建新列表
    hobbies.append(name)
    return {"name": name, "hobbies": hobbies}

user1 = create_user("Alice")
print(user1)  # 输出: {'name': 'Alice', 'hobbies': ['Alice']}

user2 = create_user("Bob")
print(user2)  # 输出: {'name': 'Bob', 'hobbies': ['Bob']} - 正常!
方法二:使用 or 运算符(简洁但不推荐)
def create_user(name, hobbies=None):
    """创建用户,使用 or 运算符"""
    hobbies = hobbies or []  # 如果 hobbies 为 None 或空列表,则创建新列表
    hobbies.append(name)
    return {"name": name, "hobbies": hobbies}

⚠️ 注意:使用 or 运算符有一个问题,如果传入空列表 [],也会被替换为新列表。推荐使用 if items is None 的方式。

方法三:使用默认参数工厂函数(高级用法)
def create_user(name, hobbies=None):
    """创建用户,使用工厂函数"""
    if hobbies is None:
        hobbies = list()  # 使用 list() 构造函数创建新列表
    hobbies.append(name)
    return {"name": name, "hobbies": hobbies}

2.3 最佳实践

📋 最佳实践清单

  1. 使用 None 作为默认值:对于可变对象,始终使用 None 作为默认值
  2. 函数内部初始化:在函数内部检查并创建新的可变对象
  3. 使用 is None 检查:使用 if items is None 而不是 if not items
  4. 不可变对象安全:数字、字符串、元组等不可变对象可以直接作为默认值
  5. 文档说明:在函数文档中说明默认参数的行为
完整示例
def process_data(data, cache=None, options=None):
    """
    处理数据函数
    
    参数:
        data: 要处理的数据
        cache: 缓存字典,默认为 None(每次调用创建新字典)
        options: 选项字典,默认为 None(每次调用创建新字典)
    
    返回:
        处理后的数据
    """
    # 初始化缓存
    if cache is None:
        cache = {}
    
    # 初始化选项
    if options is None:
        options = {"verbose": False, "timeout": 30}
    
    # 处理逻辑
    if data in cache:
        return cache[data]
    
    # 处理数据...
    result = f"processed_{data}"
    cache[data] = result
    return result

# 测试
result1 = process_data("test1")
result2 = process_data("test2")
print(result1)  # 输出: processed_test1
print(result2)  # 输出: processed_test2 - 正常!
特殊场景:使用可变对象作为缓存

💡 提示:在某些特殊场景下,我们可以利用可变默认参数的特性来实现缓存功能。

def factorial(num, cache={}):
    """
    计算阶乘,使用字典作为缓存
    
    注意:这里故意使用可变对象作为默认参数,利用其共享特性实现缓存
    """
    if num == 0:
        return 1
    if num not in cache:
        print(f"计算 {num} 的阶乘...")
        cache[num] = factorial(num - 1, cache) * num
    return cache[num]

# 第一次调用
print(factorial(4))  # 输出: 计算 4 的阶乘... 计算 3 的阶乘... 计算 2 的阶乘... 计算 1 的阶乘... 24

# 第二次调用 - 直接从缓存获取
print(factorial(4))  # 输出: 24 - 没有打印"计算",直接从缓存获取!

⚠️ 注意:这种用法虽然可以实现缓存,但通常不推荐,因为:

  • 代码可读性差,容易引起误解
  • 缓存无法清除,可能导致内存泄漏
  • 更好的做法是使用装饰器或专门的缓存库(如 functools.lru_cache

3. *args 的用法与技巧 ⭐ Should(建议实践)

参考资源

3.1 基本概念

🔑 核心概念*args 是 Python 中用于处理可变数量位置参数(variable positional arguments)的语法。它允许函数接收任意数量的位置参数,这些参数会被收集到一个元组(tuple)中。

💡 关键点

  • *args 中的 * 是必需的,args 只是一个变量名(可以改成其他名字,如 *params
  • *args 收集所有未匹配的位置参数,组成一个元组
  • *args 必须放在普通位置参数之后,默认参数之前

3.2 基础用法

基本语法
def my_function(*args):
    """接收任意数量的位置参数"""
    print(f"args 类型: {type(args)}")
    print(f"args 内容: {args}")
    for arg in args:
        print(f"参数: {arg}")

# 调用函数
my_function('Hello', 'world', 'Python', 'is', 'awesome')

输出结果

args 类型: <class 'tuple'>
args 内容: ('Hello', 'world', 'Python', 'is', 'awesome')
参数: Hello
参数: world
参数: Python
参数: is
参数: awesome
与其他参数组合使用
def greet(greeting, *names):
    """问候函数,greeting 是必需参数,names 是可变参数"""
    print(f"{greeting}!")
    for name in names:
        print(f"  - {name}")

# 调用函数
greet("Hello", "Alice", "Bob", "Charlie")

输出结果

Hello!
  - Alice
  - Bob
  - Charlie

💡 说明

  • greeting 是普通位置参数,必须提供
  • *names 收集所有剩余的位置参数
  • 可以传入 0 个或多个 names 参数

3.3 高级技巧

动态参数传递

🛠️ 技巧:使用 *args 可以将参数从一个函数动态传递到另一个函数。

def adder(*numbers):
    """计算所有数字的和"""
    return sum(numbers)

def caller(func, *args):
    """调用函数并传递参数"""
    return func(*args)  # 使用 *args 解包参数

# 动态调用
result = caller(adder, 1, 2, 3, 4, 5)
print(result)  # 输出: 15

💡 工作原理

  • caller 函数接收一个函数对象和任意数量的参数
  • 使用 *args 解包参数,传递给目标函数
  • 这种方式可以实现函数调用的动态转发
参数解包

📦 解包功能:在函数调用时,* 可以解包序列(tuple、list 等),将元素作为位置参数传递。

def print_numbers(a, b, c):
    """打印三个数字"""
    print(f"a={a}, b={b}, c={c}")

# 方法一:直接传递参数
print_numbers(1, 2, 3)  # 输出: a=1, b=2, c=3

# 方法二:使用解包
numbers = (1, 2, 3)
print_numbers(*numbers)  # 输出: a=1, b=2, c=3 - 等同于方法一

# 方法三:使用列表解包
numbers_list = [4, 5, 6]
print_numbers(*numbers_list)  # 输出: a=4, b=5, c=6

💡 关键点

  • 在函数定义时,*args 用于收集参数(pack)
  • 在函数调用时,*args 用于解包参数(unpack)
  • 解包功能是独立的,不依赖于函数定义中是否有 *args

3.4 实际应用场景

场景一:处理可变数量的输入参数

💻 应用:创建通用函数,处理不确定数量的输入。

def log_message(*messages):
    """记录多条日志消息"""
    for message in messages:
        print(f"[LOG] {message}")

# 可以传入任意数量的消息
log_message("Starting the program")
log_message("Loading modules", "Program started successfully", "Ready to process")
场景二:数学运算函数

🔢 应用:创建灵活的数学运算函数。

def calculate_sum(*numbers):
    """计算所有数字的和"""
    return sum(numbers)

def calculate_product(*numbers):
    """计算所有数字的乘积"""
    result = 1
    for num in numbers:
        result *= num
    return result

# 使用示例
print(calculate_sum(1, 2, 3, 4, 5))  # 输出: 15
print(calculate_product(2, 3, 4))    # 输出: 24
场景三:格式化输出函数

📝 应用:创建灵活的格式化输出函数。

def format_output(*items, separator=", "):
    """格式化输出多个项目"""
    return separator.join(str(item) for item in items)

# 使用示例
result = format_output("Apple", "Banana", "Orange")
print(result)  # 输出: Apple, Banana, Orange

result = format_output(1, 2, 3, separator=" | ")
print(result)  # 输出: 1 | 2 | 3

4. **kwargs 的用法与技巧 ⭐ Should(建议实践)

参考资源

4.1 基本概念

🔑 核心概念**kwargs 是 Python 中用于处理可变数量关键字参数(variable keyword arguments)的语法。它允许函数接收任意数量的关键字参数,这些参数会被收集到一个字典(dict)中。

💡 关键点

  • **kwargs 中的 ** 是必需的,kwargs 只是一个变量名(可以改成其他名字,如 **params
  • **kwargs 收集所有未匹配的关键字参数,组成一个字典
  • **kwargs 必须放在所有参数的最后

4.2 基础用法

基本语法
def greet_me(**kwargs):
    """接收任意数量的关键字参数"""
    print(f"kwargs 类型: {type(kwargs)}")
    print(f"kwargs 内容: {kwargs}")
    for key, value in kwargs.items():
        print(f"{key} = {value}")

# 调用函数
greet_me(name="Alice", age=25, city="Beijing")

输出结果

kwargs 类型: <class 'dict'>
kwargs 内容: {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
name = Alice
age = 25
city = Beijing
访问关键字参数
def greet_me(**kwargs):
    """问候函数,使用关键字参数"""
    if 'name' in kwargs:
        print(f"Hello {kwargs['name']}!")
    else:
        print("Hello there!")
    
    # 使用 get 方法安全访问
    age = kwargs.get('age', 'unknown')
    print(f"Age: {age}")

# 调用函数
greet_me(name="Alice")  # 输出: Hello Alice! Age: unknown
greet_me(name="Bob", age=30)  # 输出: Hello Bob! Age: 30
greet_me()  # 输出: Hello there! Age: unknown

💡 说明

  • 使用 in 关键字检查字典中是否存在某个键
  • 使用 get() 方法安全访问字典值,可以设置默认值
  • 可以传入 0 个或多个关键字参数

4.3 高级技巧

与其他参数组合使用

🛠️ 技巧**kwargs 可以与位置参数、默认参数、*args 组合使用。

def complex_function(first, *args, **kwargs):
    """复杂函数,包含所有类型的参数"""
    print(f"第一个参数: {first}")
    
    # 处理位置参数
    for arg in args:
        print(f"位置参数: {arg}")
    
    # 处理关键字参数
    for key, value in kwargs.items():
        print(f"{key}: {value}")

# 调用函数
complex_function('Python', 'is', 'awesome', editor='VS Code', version='3.8')

输出结果

第一个参数: Python
位置参数: is
位置参数: awesome
editor: VS Code
version: 3.8
参数解包

📦 解包功能:在函数调用时,** 可以解包字典,将键值对作为关键字参数传递。

def create_user(name, age, city):
    """创建用户"""
    return {"name": name, "age": age, "city": city}

# 方法一:直接传递关键字参数
user1 = create_user(name="Alice", age=25, city="Beijing")
print(user1)  # 输出: {'name': 'Alice', 'age': 25, 'city': 'Beijing'}

# 方法二:使用字典解包
user_info = {"name": "Bob", "age": 30, "city": "Shanghai"}
user2 = create_user(**user_info)  # 使用 ** 解包字典
print(user2)  # 输出: {'name': 'Bob', 'age': 30, 'city': 'Shanghai'}

💡 关键点

  • 在函数定义时,**kwargs 用于收集关键字参数(pack)
  • 在函数调用时,**kwargs 用于解包字典(unpack)
  • 字典的键必须与函数参数名匹配

4.4 实际应用场景

场景一:动态配置参数

💻 应用:创建 API 请求函数,支持动态配置选项。

def api_request(**kwargs):
    """API 请求函数,支持动态配置"""
    # 设置默认值
    base_url = kwargs.get('base_url', 'https://api.example.com')
    endpoint = kwargs.get('endpoint', '/')
    method = kwargs.get('method', 'GET')
    timeout = kwargs.get('timeout', 30)
    
    print(f"Making a {method} request to {base_url}{endpoint}")
    print(f"Timeout: {timeout} seconds")
    # 实际应用中,这里会发送 HTTP 请求
    return {"status": "success"}

# 使用示例
api_request(method='POST', endpoint='/users', base_url='https://customapi.com')
api_request(method='GET', endpoint='/data', timeout=60)
场景二:函数参数转发

🔄 应用:将关键字参数从一个函数转发到另一个函数。

def process_data(data, **options):
    """处理数据,支持多种选项"""
    # 设置默认选项
    verbose = options.get('verbose', False)
    timeout = options.get('timeout', 30)
    retry = options.get('retry', 3)
    
    if verbose:
        print(f"Processing data: {data}")
        print(f"Options: timeout={timeout}, retry={retry}")
    
    # 处理数据...
    return f"processed_{data}"

def wrapper_function(data, **kwargs):
    """包装函数,转发参数"""
    # 可以在这里添加额外的逻辑
    result = process_data(data, **kwargs)  # 转发所有关键字参数
    return result

# 使用示例
result = wrapper_function("test", verbose=True, timeout=60, retry=5)
场景三:灵活的初始化函数

🎨 应用:创建灵活的类初始化或配置函数。

def create_config(**settings):
    """创建配置字典"""
    # 默认配置
    config = {
        "debug": False,
        "log_level": "INFO",
        "max_connections": 100,
        "timeout": 30
    }
    
    # 更新用户提供的配置
    config.update(settings)
    
    return config

# 使用示例
config1 = create_config()  # 使用默认配置
config2 = create_config(debug=True, log_level="DEBUG")  # 部分覆盖
config3 = create_config(debug=True, max_connections=200, timeout=60)  # 完全自定义

5. 组合使用 *args 和 **kwargs ⭐ Should(建议实践)

参考资源

5.1 参数顺序

🔑 核心规则:在函数定义中,参数必须按照以下顺序排列:

  1. 普通位置参数(positional arguments)
  2. 默认参数(default arguments)
  3. *args(可变位置参数)
  4. 仅关键字参数(keyword-only arguments,可选)
  5. **kwargs(可变关键字参数)

📋 标准格式

def function(positional, default="value", *args, keyword_only="value", **kwargs):
    pass

⚠️ 重要提醒*args 必须放在 **kwargs 之前,否则会导致语法错误。

5.2 组合方式

基本组合
def function_with_both(*args, **kwargs):
    """同时使用 *args 和 **kwargs"""
    print("位置参数:", args)
    print("关键字参数:", kwargs)

# 调用函数
function_with_both(1, 2, 3, a='A', b='B', c='C')

输出结果

位置参数: (1, 2, 3)
关键字参数: {'a': 'A', 'b': 'B', 'c': 'C'}
完整参数组合
def complete_function(first, second="default", *args, keyword_only="value", **kwargs):
    """包含所有类型参数的函数"""
    print(f"第一个位置参数: {first}")
    print(f"默认参数: {second}")
    print(f"可变位置参数: {args}")
    print(f"仅关键字参数: {keyword_only}")
    print(f"可变关键字参数: {kwargs}")

# 调用函数
complete_function(1, 2, 3, 4, keyword_only="custom", a='A', b='B')

输出结果

第一个位置参数: 1
默认参数: 2
可变位置参数: (3, 4)
仅关键字参数: custom
可变关键字参数: {'a': 'A', 'b': 'B'}

5.3 实际应用案例

案例一:通用装饰器

🎨 应用:创建通用装饰器,适用于任意参数签名的函数。

def my_decorator(func):
    """通用装饰器,可以装饰任何函数"""
    def wrapper(*args, **kwargs):
        print("函数调用前执行...")
        result = func(*args, **kwargs)  # 转发所有参数
        print("函数调用后执行...")
        return result
    return wrapper

@my_decorator
def say_hello(name):
    """问候函数"""
    print(f"Hello, {name}!")

@my_decorator
def add_numbers(a, b, c=0):
    """加法函数"""
    return a + b + c

# 使用装饰器
say_hello("Alice")
# 输出:
# 函数调用前执行...
# Hello, Alice!
# 函数调用后执行...

result = add_numbers(1, 2, c=3)
print(result)  # 输出: 6

💡 优势:使用 *args**kwargs 可以让装饰器适用于任何函数,无论其参数签名如何。

案例二:动态函数调用

🔄 应用:根据条件动态调用不同的函数。

def add(x, y):
    """加法函数"""
    return x + y

def multiply(x, y):
    """乘法函数"""
    return x * y

def operate(operation, *args, **kwargs):
    """根据操作类型动态调用函数"""
    operations = {
        'add': add,
        'multiply': multiply
    }
    
    func = operations.get(operation)
    if func:
        return func(*args, **kwargs)  # 动态传递参数
    else:
        raise ValueError(f"Unknown operation: {operation}")

# 使用示例
result1 = operate('add', 5, 7)
print(result1)  # 输出: 12

result2 = operate('multiply', 5, 7)
print(result2)  # 输出: 35
案例三:API 请求处理

🌐 应用:创建灵活的 API 请求处理函数。

def api_call(endpoint, *args, **kwargs):
    """API 调用函数,支持灵活的参数传递"""
    print(f"Endpoint: {endpoint}")
    print(f"位置参数: {args}")
    print(f"关键字参数: {kwargs}")
    
    # 提取常用参数
    method = kwargs.get('method', 'GET')
    timeout = kwargs.get('timeout', 30)
    headers = kwargs.get('headers', {})
    
    print(f"Method: {method}, Timeout: {timeout}")
    # 实际应用中,这里会发送 HTTP 请求
    return {"status": "success", "endpoint": endpoint}

# 使用示例
api_call('/users', 1, 2, method='POST', data={'name': 'Alice'}, timeout=60)
案例四:参数转发和包装

📦 应用:创建包装函数,转发参数到其他函数。

def original_function(name, age, city="Unknown", *hobbies, **options):
    """原始函数"""
    print(f"Name: {name}, Age: {age}, City: {city}")
    print(f"Hobbies: {hobbies}")
    print(f"Options: {options}")

def wrapper_function(*args, **kwargs):
    """包装函数,添加额外逻辑后转发参数"""
    print("包装函数:准备调用原始函数...")
    
    # 可以在这里修改参数
    if 'verbose' not in kwargs:
        kwargs['verbose'] = True
    
    # 转发所有参数到原始函数
    result = original_function(*args, **kwargs)
    
    print("包装函数:调用完成")
    return result

# 使用示例
wrapper_function("Alice", 25, "Beijing", "reading", "coding", debug=True)

6. 对比示例 🆚

6.1 不使用 *args/**kwargs 的问题

传统方式:需要为每个可能的参数数量定义不同的函数。

# 传统方式:需要定义多个函数
def add_two(a, b):
    """计算两个数的和"""
    return a + b

def add_three(a, b, c):
    """计算三个数的和"""
    return a + b + c

def add_four(a, b, c, d):
    """计算四个数的和"""
    return a + b + c + d

# 如果需要计算 5 个数的和,需要再定义一个函数
def add_five(a, b, c, d, e):
    """计算五个数的和"""
    return a + b + c + d + e

# 使用示例
result1 = add_two(1, 2)
result2 = add_three(1, 2, 3)
result3 = add_four(1, 2, 3, 4)
result4 = add_five(1, 2, 3, 4, 5)

🐛 问题

  • 代码重复,维护困难
  • 无法处理任意数量的参数
  • 每次需要新功能都要修改函数定义
  • 代码可扩展性差

✅ *使用 args 的方式:一个函数解决所有问题。

# 灵活方式:使用 *args
def add(*numbers):
    """计算任意数量数字的和"""
    return sum(numbers)

# 使用示例 - 可以传入任意数量的参数
result1 = add(1, 2)           # 2 个参数
result2 = add(1, 2, 3)        # 3 个参数
result3 = add(1, 2, 3, 4)     # 4 个参数
result4 = add(1, 2, 3, 4, 5)  # 5 个参数
result5 = add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)  # 10 个参数也可以!

💡 优势

  • 代码简洁,一个函数解决所有问题
  • 可以处理任意数量的参数
  • 易于维护和扩展
  • 代码可读性强

6.2 传统方式 vs 灵活方式

场景一:日志记录函数

传统方式

# 传统方式:需要为不同数量的消息定义不同函数
def log_one(message1):
    print(f"[LOG] {message1}")

def log_two(message1, message2):
    print(f"[LOG] {message1}")
    print(f"[LOG] {message2}")

def log_three(message1, message2, message3):
    print(f"[LOG] {message1}")
    print(f"[LOG] {message2}")
    print(f"[LOG] {message3}")

# 使用示例
log_one("Starting")
log_two("Starting", "Loading")
log_three("Starting", "Loading", "Ready")

灵活方式

# 灵活方式:使用 *args
def log_message(*messages):
    """记录任意数量的日志消息"""
    for message in messages:
        print(f"[LOG] {message}")

# 使用示例 - 可以传入任意数量的消息
log_message("Starting")
log_message("Starting", "Loading")
log_message("Starting", "Loading", "Ready")
log_message("Step 1", "Step 2", "Step 3", "Step 4", "Step 5")  # 5 个也可以!
场景二:配置函数

传统方式

# 传统方式:需要定义所有可能的参数
def create_config(debug=False, log_level="INFO", max_connections=100, timeout=30, retry=3, cache_size=1000):
    """创建配置 - 参数列表很长,难以维护"""
    return {
        "debug": debug,
        "log_level": log_level,
        "max_connections": max_connections,
        "timeout": timeout,
        "retry": retry,
        "cache_size": cache_size
    }

# 使用示例 - 需要记住所有参数名
config = create_config(debug=True, log_level="DEBUG", max_connections=200, timeout=60, retry=5, cache_size=2000)

灵活方式

# 灵活方式:使用 **kwargs
def create_config(**settings):
    """创建配置 - 使用 **kwargs 接收任意配置项"""
    # 默认配置
    config = {
        "debug": False,
        "log_level": "INFO",
        "max_connections": 100,
        "timeout": 30,
        "retry": 3,
        "cache_size": 1000
    }
    
    # 更新用户提供的配置
    config.update(settings)
    return config

# 使用示例 - 只需要传入需要修改的配置
config1 = create_config()  # 使用默认配置
config2 = create_config(debug=True)  # 只修改 debug
config3 = create_config(debug=True, timeout=60)  # 修改多个配置
config4 = create_config(debug=True, log_level="DEBUG", max_connections=200)  # 任意组合
场景三:API 调用函数

传统方式

# 传统方式:需要定义所有可能的参数
def api_request(endpoint, method="GET", headers=None, data=None, timeout=30, retry=3, verify=True):
    """API 请求函数 - 参数列表很长"""
    if headers is None:
        headers = {}
    # ... 实现逻辑
    pass

# 使用示例 - 需要记住所有参数
api_request("/users", method="POST", headers={"Content-Type": "application/json"}, 
            data={"name": "Alice"}, timeout=60, retry=5, verify=False)

灵活方式

# 灵活方式:使用 **kwargs
def api_request(endpoint, **kwargs):
    """API 请求函数 - 使用 **kwargs 接收任意参数"""
    method = kwargs.get('method', 'GET')
    headers = kwargs.get('headers', {})
    data = kwargs.get('data', None)
    timeout = kwargs.get('timeout', 30)
    retry = kwargs.get('retry', 3)
    verify = kwargs.get('verify', True)
    
    # ... 实现逻辑
    print(f"Making {method} request to {endpoint}")
    print(f"Options: {kwargs}")

# 使用示例 - 只需要传入需要的参数
api_request("/users", method="POST", data={"name": "Alice"})
api_request("/data", method="GET", timeout=60, retry=5)
api_request("/upload", method="PUT", headers={"Auth": "token"}, verify=False)

📊 对比总结

特性传统方式灵活方式(*args/**kwargs)
代码量多,需要定义多个函数少,一个函数解决
可扩展性差,需要修改函数定义好,无需修改函数
参数数量固定,无法处理任意数量灵活,可以处理任意数量
维护成本高,需要维护多个函数低,只需维护一个函数
代码可读性一般,参数列表可能很长好,参数清晰明了
使用便利性一般,需要记住所有参数好,只需传入需要的参数

7. 常见错误与修正 ⚠️

参考资源

7.1 参数顺序错误

错误示例**kwargs 放在 *args 之前。

# 错误:**kwargs 在 *args 之前
def wrong_function(**kwargs, *args):
    pass
# SyntaxError: invalid syntax

正确做法*args 必须放在 **kwargs 之前。

# 正确:*args 在 **kwargs 之前
def correct_function(*args, **kwargs):
    print("位置参数:", args)
    print("关键字参数:", kwargs)

🔑 规则:参数顺序必须是:位置参数 → 默认参数 → *args → 仅关键字参数 → **kwargs

7.2 过度使用

错误示例:在不必要的情况下使用 *args**kwargs

# 错误:过度使用 *args,导致代码可读性差
def process_data(*args):
    """处理数据 - 参数不明确"""
    name = args[0]  # 不知道第一个参数是什么
    age = args[1]   # 不知道第二个参数是什么
    city = args[2]  # 不知道第三个参数是什么
    # 代码难以理解和维护

正确做法:明确参数名称,只在真正需要时使用 *args**kwargs

# 正确:明确参数名称
def process_data(name, age, city):
    """处理数据 - 参数清晰明确"""
    print(f"Name: {name}, Age: {age}, City: {city}")

# 或者:在需要处理可变参数时使用
def process_multiple_items(*items):
    """处理多个项目 - 使用 *args 是合理的"""
    for item in items:
        print(f"Processing: {item}")

💡 原则

  • 如果参数数量固定且明确,使用明确的参数名
  • 只有在需要处理可变数量的参数时,才使用 *args**kwargs
  • 平衡灵活性和代码可读性

7.3 参数覆盖问题

错误示例:使用 *args**kwargs 时,可能意外覆盖默认参数。

def my_function(a, b=2, *args, **kwargs):
    """函数定义"""
    print(a, b, args, kwargs)

# 可能意外覆盖默认参数 b
my_function(1, 3, 4, d=5)  # b 被 3 覆盖了,而不是使用默认值 2
# 输出: 1 3 (4,) {'d': 5}

正确做法:明确指定所有非动态参数,特别是当存在默认参数时。

def my_function(a, b=2, *args, **kwargs):
    """函数定义"""
    print(f"a={a}, b={b}, args={args}, kwargs={kwargs}")

# 正确:明确指定参数
my_function(1, b=2, 3, 4, d=5)  # 明确指定 b=2
# 或者:使用关键字参数
my_function(1, 2, 3, 4, d=5)  # 按位置传递,b=2

7.4 字典解包时的键名错误

错误示例:字典的键与函数参数名不匹配。

def create_user(name, age, city):
    """创建用户"""
    return {"name": name, "age": age, "city": city}

# 错误:字典的键与参数名不匹配
user_info = {"username": "Alice", "age": 25, "city": "Beijing"}
user = create_user(**user_info)  # TypeError: create_user() got an unexpected keyword argument 'username'

正确做法:确保字典的键与函数参数名匹配。

def create_user(name, age, city):
    """创建用户"""
    return {"name": name, "age": age, "city": city}

# 正确:字典的键与参数名匹配
user_info = {"name": "Alice", "age": 25, "city": "Beijing"}
user = create_user(**user_info)  # 正常工作

7.5 可变默认参数陷阱(再次强调)

错误示例:使用可变对象作为默认参数。

# 错误:使用列表作为默认参数
def add_item(item, items=[]):
    items.append(item)
    return items

result1 = add_item(1)  # [1]
result2 = add_item(2)  # [1, 2] - 问题!

正确做法:使用 None 作为默认值,在函数内部初始化。

# 正确:使用 None 作为默认值
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

result1 = add_item(1)  # [1]
result2 = add_item(2)  # [2] - 正常!

7.6 最佳实践总结

📋 避免常见错误的检查清单

  1. 参数顺序正确:确保 *args**kwargs 之前
  2. 避免过度使用:只在真正需要时使用 *args**kwargs
  3. 明确参数名称:优先使用明确的参数名,提高代码可读性
  4. 避免参数覆盖:明确指定所有非动态参数
  5. 字典键名匹配:使用 **kwargs 解包时,确保键名与参数名匹配
  6. 避免可变默认参数:使用 None 作为默认值,在函数内部初始化
  7. 文档说明:在函数文档中说明参数的使用方式

8. 总结

核心要点回顾

📚 本文核心内容总结

  1. 可变默认参数陷阱 🔥

    • 使用可变对象(list、dict、set)作为默认参数会导致所有函数调用共享同一个对象
    • 解决方案:使用 None 作为默认值,在函数内部初始化可变对象
  2. *args 的用法

    • 用于接收可变数量的位置参数,收集为元组(tuple)
    • 在函数定义时收集参数,在函数调用时解包参数
    • 适用于处理不确定数量的输入参数
  3. **kwargs 的用法

    • 用于接收可变数量的关键字参数,收集为字典(dict)
    • 在函数定义时收集参数,在函数调用时解包字典
    • 适用于动态配置和参数转发
  4. 组合使用

    • 参数顺序:位置参数 → 默认参数 → *args → 仅关键字参数 → **kwargs
    • 可以创建灵活强大的函数,适用于装饰器、API 调用等场景
  5. 最佳实践

    • 避免可变默认参数陷阱
    • 只在真正需要时使用 *args 和 **kwargs
    • 保持代码可读性和可维护性

写在最后

💪 继续加油:通过掌握可变默认参数的正确用法和 *args/**kwargs 的灵活应用,你已经具备了写出专业级 Python 代码的基础。记住,95% 的初学者会踩的坑,你已经成功避开了!

🚀 实践建议

  • 在实际项目中应用这些技巧,写出更灵活的函数
  • 遇到不确定参数数量的场景时,考虑使用 *args 和 **kwargs
  • 始终使用 None 作为可变对象的默认值,避免共享状态问题

🎯 学习路径

  • 从理解原理开始,掌握内存共享机制
  • 通过实践加深理解,编写自己的示例代码
  • 在项目中应用,逐步形成良好的编程习惯

🎉 恭喜你完成了 Python 参数类型的学习! 现在你已经掌握了如何避免可变默认参数陷阱,以及如何使用 *args 和 **kwargs 写出灵活强大的函数。继续学习,你的代码质量将不断提升,最终成为像合格程序员一样优秀的开发者!


作者:郑恩赐
机构:厦门工学院人工智能创作坊
日期:2025 年 11 月 08 日