飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • 第1章 Python简介

    • Python是什么
    • 安装与运行
    • 交互式解释器
    • 注释与编码规范
  • 第2章 变量与数据类型

    • 变量与对象
    • 整数 int
    • 浮点数 float
    • 复数 complex
    • 布尔值 bool
    • 字符串 str
    • 空值 None
    • 类型转换
  • 第3章 运算符与表达式

    • 算术运算符
    • 比较运算符
    • 赋值运算符
    • 逻辑运算符
    • 位运算符
    • 身份与成员运算符
    • 海象运算符
    • 运算符优先级
  • 第4章 流程控制

    • if 语句
    • if-else 语句
    • if-elif-else 语句
    • match-case 语句
    • 条件表达式(三元运算符)
    • while 循环
    • for 循环
    • range 函数
    • break 与 continue
    • 循环的 else 子句
    • pass 语句
  • 第5章 数据结构

    • 列表创建与索引
    • 列表方法
    • 列表推导式
    • 元组
    • 序列解包
    • 集合
    • 字典创建与访问
    • 字典方法
    • 字典推导式
    • range 对象
  • 第6章 函数

    • 定义函数
    • 位置参数与关键字参数
    • 默认参数
    • 可变参数
    • 解包实参
    • 函数返回值
    • lambda 表达式
    • 文档字符串与注解
    • 作用域与命名空间
    • global 与 nonlocal
  • 第7章 模块与包

    • 模块导入
    • 模块搜索路径
    • 包与相对导入
    • 标准库概览
  • 第8章 文件与输入输出

    • 文件读写
    • 上下文管理器
    • 字符串格式化
    • JSON 与 CSV
  • 第9章 面向对象

    • 类与对象
    • 方法
    • 实例变量与类变量
    • 私有变量
    • 继承
    • 多重继承
    • 魔术方法
    • 属性装饰器
    • 数据类 dataclass
  • 第10章 异常处理

    • 语法错误与异常
    • try-except
    • 异常链与 raise
    • 清理操作
    • 自定义异常
  • 第11章 迭代器与生成器

    • 迭代器协议
    • 生成器
    • 生成器表达式
    • 迭代工具
  • 第12章 高级特性

    • 装饰器
    • 函数式编程
  • 第13章 工程实践

    • 测试与调试
    • 代码质量
    • 虚拟环境

默认参数

默认参数让函数在调用时可以省略某些形参,使用预先定义好的默认值。这个机制看似简单,但涉及求值时机、可变对象共享等深层细节,是 Python 函数中最容易踩坑的地方之一。

默认值的基本语法

在形参名后加 =表达式 即可指定默认值:

def greet(name, greeting="你好"):
    return f"{greeting},{name}!"

print(greet("小明"))              # 你好,小明!
print(greet("小明", "早上好"))      # 早上好,小明!
print(greet("小明", greeting="再见"))  # 再见,小明!

有默认值的参数必须放在无默认值参数之后,否则解析器无法判断调用时省略了哪些参数:

# def bad(a=1, b):      # SyntaxError: non-default argument follows default argument
#     pass

def good(a, b=1):       # 合法
    pass

默认值的求值时机

默认参数表达式在函数定义时求值,而不是每次调用时。这意味着默认值在函数对象创建时就被固定下来:

i = 10
def f(arg=i):
    print(arg)

i = 20
f()   # 输出 10,不是 20

这个特性对不可变对象(数字、字符串、元组)通常没有负面影响,因为不可变对象无法原地修改。但对可变对象(列表、字典、集合),后果非常严重。

可变默认参数陷阱

如果默认值是可变对象,所有未提供该参数的调用会共享同一个对象:

def append_item(item, items=[]):
    items.append(item)
    return items

print(append_item(1))   # [1]
print(append_item(2))   # [1, 2]  —— 保留了上次的结果!
print(append_item(3))   # [1, 2, 3]

为什么会这样?因为 [] 在函数定义时求值一次,创建了一个列表对象。每次调用 append_item 不传入 items 时,都使用这个同一个列表对象。函数内部 append 是原地修改,所以副作用累积了下来。

这个陷阱也适用于字典和集合:

def add_key(key, value, mapping={}):
    mapping[key] = value
    return mapping

print(add_key("a", 1))   # {'a': 1}
print(add_key("b", 2))   # {'a': 1, 'b': 2}  —— 同样共享了!

None 哨兵模式

避免可变默认参数陷阱的标准做法是使用 None 作为默认值,在函数体内创建新对象:

def append_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(append_item(1))       # [1]
print(append_item(2))       # [2]  —— 每次独立
print(append_item(3, [0]))  # [0, 3] —— 传入显式列表也正常工作

None 是不可变的单例对象,用它做哨兵值可以明确区分"调用者未传参"和"调用者传了空列表"两种情况。注意判断时必须用 is None,不要用 == None 或 if not items(后者会把空列表也误判为需要新建)。

# 错误的判断方式
def bad(item, items=None):
    if not items:           # 危险!空列表也会被当作 None 处理
        items = []
    items.append(item)
    return items

print(bad(1, []))   # 预期 [1],实际也是 [1],但逻辑不严谨

默认值为函数调用的情况

有时默认值需要动态计算,但记住它只在定义时求值一次:

from datetime import datetime

def log(msg, timestamp=datetime.now()):
    return f"{timestamp}: {msg}"

# timestamp 被固定为模块导入时的时间,不是每次调用时
print(log("启动"))

如果需要每次调用时重新计算,必须使用 None 哨兵:

def log(msg, timestamp=None):
    if timestamp is None:
        timestamp = datetime.now()
    return f"{timestamp}: {msg}"

默认参数与关键字参数的配合

默认参数天然适合与关键字参数结合使用。设计函数时,把最常用的默认值放在后面,调用者可以按需覆盖:

def connect(host, port=3306, user="root", password=None, database=None):
    pass

# 只覆盖需要的部分
connect("localhost", database="test")
connect("localhost", 5432, user="admin")

常见错误

忘记可变默认参数的陷阱:

def collect_tags(tag, tags=[]):
    tags.append(tag)
    return tags

用可变对象做默认值但意图是每次新建:

def create_user(name, roles=["user"]):   # 危险!所有默认用户共享同一角色列表
    return {"name": name, "roles": roles}

正确做法:

def create_user(name, roles=None):
    if roles is None:
        roles = ["user"]
    return {"name": name, "roles": roles}

小结

默认参数在定义时求值,不可变默认值安全,可变默认值会导致共享状态。使用 None 哨兵模式是处理可变默认参数的标准做法。理解求值时机和对象引用机制,才能避免这个经典陷阱。

上一页
位置参数与关键字参数
下一页
可变参数