Day 012 — 函数进阶
"函数不只是写几行代码——参数传递的底层机制、默认参数的陷阱、可变参数的灵活性、类型注解的规范,这些才是真正拉开 Python 程序员水平的分水岭。"
目录
1. 默认参数陷阱(可变对象问题)
1.1 什么是默认参数陷阱?
看下面这段代码,猜猜输出是什么:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item("a")) # 预期: ['a']
print(add_item("b")) # 预期: ['b'] ?
print(add_item("c")) # 预期: ['c'] ?
实际输出:
['a']
['a', 'b'] # 咦?多了个 'a'
['a', 'b', 'c'] # 越来越多!
这就是默认参数陷阱 — 当你使用可变对象(如 list、dict、set)作为默认值时,所有调用共享同一个对象。
1.2 陷阱的原因:def 语句的执行机制
这个陷阱的核心在于理解 Python 在什么时候创建默认参数。
关键原理:
def 语句是一个可执行语句,它在 Python 中与其他语句(如赋值、if)一样,
在特定时刻执行。但有一个关键区别:
┌─────────────────────────────────────────────────────────┐
│ def 语句的执行(模块加载阶段) │
│ │
│ 当 Python 执行到 def add_item(item, items=[]): 时: │
│ │
│ ① 创建一个函数对象 │
│ ② 计算默认参数表达式(这里是 []) │
│ ③ 将计算出的默认值存入函数对象的 __defaults__ 属性中 │
│ ④ 将函数对象绑定到名字 add_item │
│ │
│ 重点: 第②步在 def 语句执行时发生,而不是在每次调用时! │
└─────────────────────────────────────────────────────────┘
用代码证明:
def add_item(item, items=[]):
items.append(item)
return items
# 默认参数存在函数对象的 __defaults__ 属性中
print(add_item.__defaults__) # 输出: ([],)
# 调用一次
add_item("a")
print(add_item.__defaults__) # 输出: (['a'],) ← 同一个对象被修改了!
id() 证明:
def demo(lst=[]):
print(f"默认参数的 id: {id(lst)}")
lst.append("x")
demo() # id: 140234567890 (举例)
demo() # id: 140234567890 (完全相同!是同一个对象)
demo() # id: 140234567890 (还是同一个!)
1.3 内存图解
函数定义时 (def 语句执行):
add_item 函数对象
┌─────────────────────────────────────┐
│ __defaults__: ([]) │
│ │ │
│ ▼ │
│ ┌─────┐ │
│ │ [] │ ← 唯一的列表对象 │
│ └─────┘ │
└─────────────────────────────────────┘
第一次调用 add_item("a"):
传入 "a"
┌─────────────────────────────────────┐
│ items 指向默认参数 │
│ │ │
│ ▼ │
│ ┌─────┐ │
│ │[ ] │── items.append("a") │
│ │ │ ↓ │
│ └─────┘ ┌─────┐ │
│ │[a] │ │
│ └─────┘ │
│ __defaults__ 中的列表被修改了! │
└─────────────────────────────────────┘
第二次调用 add_item("b"):
传入 "b"
┌─────────────────────────────────────┐
│ items 仍然指向同一个默认参数 │
│ │ │
│ ▼ │
│ ┌─────┐ │
│ │[a] │── items.append("b") │
│ │ │ ↓ │
│ └─────┘ ┌─────┐ │
│ │[a,b]│ │
│ └─────┘ │
│ 上个调用的 'a' 还在! │
└─────────────────────────────────────┘
1.4 哪些类型会触发陷阱?
| 默认参数类型 | 可变? | 是否触发陷阱 | 说明 |
|---|---|---|---|
[] (list) |
✅ 可变 | ✅ 会 | 所有调用共享同一个列表 |
{} (dict) |
✅ 可变 | ✅ 会 | 所有调用共享同一个字典 |
set() |
✅ 可变 | ✅ 会 | 所有调用共享同一个集合 |
"" (str) |
❌ 不可变 | ❌ 不会 | 每次使用是同一对象,但不可修改 |
0 (int) |
❌ 不可变 | ❌ 不会 | 每次使用是同一对象,但不可修改 |
None |
❌ 不可变 | ❌ 不会 | 最安全的默认值选择 |
True/False |
❌ 不可变 | ❌ 不会 | 布尔值不可变 |
1.5 正确的做法
黄金法则:使用不可变默认值 + 函数体内赋值
# ✅ 正确做法:用 None 代替可变默认值
def add_item(item, items=None):
if items is None:
items = [] # 每次调用时创建新列表
items.append(item)
return items
print(add_item("a")) # ['a']
print(add_item("b")) # ['b'] ← 正确!
print(add_item("c")) # ['c'] ← 正确!
为什么这样就行了?
def add_item(item, items=None):
│ │
│ def 执行时创建 None 对象 │
│ None 不可变,无法修改 │
│ │
│ items is None → True │
│ items = [] → 每次调用都 │
│ 创建一个新的空列表 │
│ │
│ 每个调用有自己独立的 [] │
└───────────────────────────┘
更多示例:
# ❌ 陷阱
def add_student(name, grades=[]):
grades.append(name)
return grades
# ✅ 正确
def add_student(name, grades=None):
if grades is None:
grades = []
grades.append(name)
return grades
# ❌ 陷阱
def cache_result(key, cache={}):
cache[key] = key.upper()
return cache
# ✅ 正确
def cache_result(key, cache=None):
if cache is None:
cache = {}
cache[key] = key.upper()
return cache
1.6 什么时候可以用可变默认值?
Python 官方文档其实说过:了解原理后,如果你故意要这个行为,那可以用。
合法使用场景——缓存/记忆化:
def fibonacci(n, cache={}):
"""带缓存的斐波那契数列(利用默认参数陷阱进行记忆化)"""
if n in cache:
return cache[n]
if n < 2:
return n
cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache)
return cache[n]
# 每次调用都共享同一个 cache 字典——这正是我们想要的!
print(fibonacci(10)) # 55
但这仍然是反直觉的写法,生产代码中应使用明确的类或闭包。
2. 关键字参数与可变参数(args, *kwargs)
2.1 位置参数 vs 关键字参数
先搞清楚这两个概念:
def greet(name, greeting, punctuation):
return f"{greeting}, {name}{punctuation}"
# 位置参数(Positional Arguments)— 按顺序传递
print(greet("张三", "你好", "!")) # 你好, 张三!
# 关键字参数(Keyword Arguments)— 指定参数名传递
print(greet(name="张三", greeting="你好", punctuation="!"))
# 混合使用 — 位置参数必须在关键字参数之前
print(greet("张三", greeting="你好", punctuation="!")) # ✅
# print(greet(name="张三", "你好", "!")) # ❌ SyntaxError
位置参数 vs 关键字参数对比:
| 对比维度 | 位置参数 | 关键字参数 |
|---|---|---|
| 传递方式 | 按位置匹配 | 按名称匹配 |
| 顺序要求 | 必须遵守参数顺序 | 可以不按顺序 |
| 可读性 | 差(需要知道参数位置) | 好(一目了然) |
| 必须性 | 调用时必不可少 | 可选(搭配默认值) |
| 性能 | 稍快(少一步名称匹配) | 稍慢 |
2.2 *args — 可变位置参数
需求场景:
假如你要写一个求和函数,但调用者可能传入 2 个、3 个、甚至 10 个数字:
# ❌ 笨办法:为每个数量写一个函数
def sum2(a, b): return a + b
def sum3(a, b, c): return a + b + c
def sum4(a, b, c, d): return a + b + c + d
# ... 没完没了
# ✅ 优雅的解法:*args
def sum_all(*args):
"""接收任意多个位置参数"""
total = 0
for num in args:
total += num
return total
print(sum_all(1, 2)) # 3
print(sum_all(1, 2, 3, 4, 5)) # 15
print(sum_all()) # 0
*args 的本质:
def sum_all(*args):
│
▼
*args 将所有传入的位置参数
打包(packing)成一个元组
sum_all(1, 2, 3, 4, 5)
│
▼
args = (1, 2, 3, 4, 5) ← 元组(tuple)
│
▼
在函数体内可以像普通元组一样操作:
len(args) → 5
args[0] → 1
for x in args: ...
语法规则:
def 函数名(普通参数, *args, 关键字参数):
│ │
│ └── 收集所有"多余"的位置参数
│
└── 普通参数正常接收值
*args 必须在普通位置参数之后,关键字参数之前。
def multi_param(a, b, *others):
"""a 和 b 是固定参数,others 收集剩余的位置参数"""
print(f"a = {a}, b = {b}, others = {others}")
multi_param(1, 2) # a = 1, b = 2, others = ()
multi_param(1, 2, 3) # a = 1, b = 2, others = (3,)
multi_param(1, 2, 3, 4, 5) # a = 1, b = 2, others = (3, 4, 5)
2.3 **kwargs — 可变关键字参数
需求场景:
你想写一个函数,调用者可以传入任意命名的配置参数:
def create_profile(name, **kwargs):
"""创建用户档案,支持任意额外信息"""
profile = {"name": name}
profile.update(kwargs) # 将 kwargs 中的键值对加入
return profile
print(create_profile("张三", age=25, city="北京", job="工程师"))
# {'name': '张三', 'age': 25, 'city': '北京', 'job': '工程师'}
**kwargs 的本质:
def create_profile(name, **kwargs):
│
▼
**kwargs 将所有传入的关键字参数
打包(packing)成一个字典
create_profile("张三", age=25, city="北京")
│
▼
kwargs = {"age": 25, "city": "北京"} ← 字典(dict)
│
▼
在函数体内可以像普通字典一样操作:
kwargs["age"] → 25
kwargs.get("city") → "北京"
for k, v in kwargs.items(): ...
2.4 args 和 *kwargs 解包(Unpacking)
* 和 ** 不仅能用在函数定义中(打包),也能用在函数调用中解包:
# 定义
def introduce(name, age, city):
print(f"我叫{name}, {age}岁, 来自{city}")
# 解包列表/元组 — 使用 *
person_info = ["张三", 25, "北京"]
introduce(*person_info) # 相当于 introduce("张三", 25, "北京")
# 解包字典 — 使用 **
person_dict = {"name": "李四", "age": 30, "city": "上海"}
introduce(**person_dict) # 相当于 introduce(name="李四", age=30, city="上海")
解包的数据流图:
调用时解包(Unpacking):
列表解包:
numbers = [1, 2, 3, 4, 5]
func(*numbers)
│
▼
func(1, 2, 3, 4, 5)
│
▼
如果函数有 *args: 再次打包成元组
字典解包:
config = {"key1": "val1", "key2": "val2"}
func(**config)
│
▼
func(key1="val1", key2="val2")
│
▼
如果函数有 **kwargs: 再次打包成字典
2.5 完整参数顺序(最复杂的情况)
def complex_func(a, b, *args, c=10, d=20, **kwargs):
"""
参数顺序(从左到右):
1. a, b — 普通位置参数
2. *args — 可变位置参数
3. c=10, d=20 — 默认关键字参数
4. **kwargs — 可变关键字参数
"""
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"c={c}, d={d}")
print(f"kwargs={kwargs}")
complex_func(1, 2, 3, 4, 5, c=100, x="foo", y="bar")
# a=1, b=2
# args=(3, 4, 5)
# c=100, d=20
# kwargs={'x': 'foo', 'y': 'bar'}
参数传递总览图:
调用: complex_func(1, 2, 3, 4, 5, c=100, x="foo", y="bar")
位置参数匹配 ───────────┐ 关键字参数匹配 ────────┐
▼ ▼
传入参数: 1 2 3 4 5 c=100 x="foo" y="bar"
│ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
函数参数: a b *args ─────► c=10 **kwargs ────────►
打包成元组 (默认20) 打包成字典
(3,4,5) 被100覆盖 {'x':'foo','y':'bar'}
2.6 常见使用模式
模式 1:转发参数(装饰器模式的基础)
def logger(func):
"""一个简单的装饰器:调用函数前打印参数"""
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__},参数: args={args}, kwargs={kwargs}")
return func(*args, **kwargs) # 原封不动转发
return wrapper
@logger
def say_hello(name, greeting="你好"):
print(f"{greeting}, {name}!")
say_hello("张三")
say_hello("李四", greeting="Hello")
模式 2:鸭子类型编程
def configure(**kwargs):
"""通用的配置函数"""
settings = {
"host": "localhost",
"port": 8080,
"debug": False,
}
settings.update(kwargs) # 用户配置覆盖默认
return settings
print(configure(host="example.com", debug=True))
# {'host': 'example.com', 'port': 8080, 'debug': True}
模式 3:适配不同数据源的统一接口
def fetch_data(source_type, **params):
"""同一接口适配不同数据源"""
if source_type == "api":
return f"从 API 获取: {params}"
elif source_type == "database":
return f"从数据库获取: {params}"
elif source_type == "file":
return f"从文件获取: {params}"
else:
raise ValueError(f"未知数据源: {source_type}")
# 调用者传入不同的关键字参数
print(fetch_data("api", url="https://api.example.com", timeout=30))
print(fetch_data("database", table="users", where="age > 18"))
print(fetch_data("file", path="/data/input.csv", encoding="utf-8"))
2.7 仅限关键字参数(Python 3 特性)
def create_user(name, age, *, city, phone):
"""
* 之后的参数是"仅限关键字参数"(Keyword-only arguments):
调用时必须使用关键字形式传递,不能使用位置参数。
"""
return f"{name}({age}岁) - {city}, {phone}"
# ✅ 正确
print(create_user("张三", 25, city="北京", phone="13800138000"))
# ❌ 错误 — city 和 phone 必须用关键字形式
# print(create_user("张三", 25, "北京", "13800138000"))
# TypeError: create_user() takes 2 positional arguments but 4 were given
为什么要有仅限关键字参数?
┌─────────────────────────────────────────────────────────┐
│ 防止位置参数混淆 │
│ │
│ def register(name, age, *, email, phone): │
│ │
│ 调用时: │
│ register("张三", 25, email="a@b.com", phone="123") │
│ ──────── 清晰明了,不会搞混 │
│ │
│ 如果没有 *: │
│ register("张三", 25, "a@b.com", "123") │
│ ──────── 谁是谁全靠位置猜,容易出错 │
└─────────────────────────────────────────────────────────┘
3. 函数注解(Type Hints)
3.1 什么是函数注解?
函数注解(Function Annotations)是 Python 3 引入的语法,让你可以为函数的参数和返回值添加"类型提示"(Type Hints):
def greet(name: str) -> str:
return f"你好, {name}!"
# ↑ ↑ ↑
# 参数名 类型 返回类型
# 注解 注解
关键理解:注解只是"提示",不是"强制"
# ✅ 尽管注解说 name 应该是 str,但传入 int 也能运行
print(greet(42)) # 输出: 你好, 42!
# Python 不检查、不强制、不报错——注解只存储在 __annotations__ 中
3.2 注解的存储机制
def add(x: int, y: int) -> int:
return x + y
# 所有注解存在函数的 __annotations__ 属性中
print(add.__annotations__)
# 输出: {'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}
annotations 是一个字典:
┌──────────────────────────────────────────────────────┐
│ __annotations__ │
│ │
│ def add(x: int, y: int) -> int: │
│ ↑ ↑ ↑ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ {'x': int, 'y': int, 'return': int} │
│ │
│ 键: 参数名 / 'return' (字符串) │
│ 值: 类型对象 / 任何表达式 │
└──────────────────────────────────────────────────────┘
注解的值可以是任何表达式:
# 字符串注解
def f(name: "用户名", age: "年龄(岁)") -> "欢迎信息":
return f"欢迎, {name}!"
print(f.__annotations__)
# {'name': '用户名', 'age': '年龄(岁)', 'return': '欢迎信息'}
# 复杂类型(Python 3.9+ 直接用)
def process(items: list[int], mapping: dict[str, int]) -> tuple[int, str]:
result = sum(items)
key = list(mapping.keys())[0]
return result, key
3.3 Python 为什么不是"强类型"?
动态类型 vs 静态类型:
┌─────────────────────────────────────────────────────────┐
│ Python(动态类型) │
│ │
│ def double(x): # x 可以是任何类型 │
│ return x * 2 # 只要支持 * 操作 │
│ │
│ double(5) → 10 # 整数 │
│ double("Hi") → "HiHi" # 字符串 │
│ double([1,2]) → [1,2,1,2] # 列表 │
│ double(3.14) → 6.28 # 浮点数 │
│ │
│ 这就是"鸭子类型"(Duck Typing): │
│ 如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子。 │
│ │
│ 优点: 灵活、通用 │
│ 缺点: 大项目中容易隐藏类型错误 │
└─────────────────────────────────────────────────────────┘
Type Hints 的作用:
无注解时:
def process(data):
# data 是什么类型?
# 返回什么类型?
# 全靠文档和人脑记忆
有注解时:
def process(data: list[dict]) -> list[str]:
# 明确知道 data 是 "列表套字典"
# 明确知道返回 "字符串列表"
# IDE 可以自动补全、静态检查
3.4 常用类型注解速查表
| 类型 | 语法 | 说明 |
|---|---|---|
| 基础类型 | int, str, float, bool |
Python 内置类型 |
| 列表 | list[int] |
元素为 int 的列表 |
| 字典 | dict[str, int] |
键 str → 值 int 的字典 |
| 元组 | tuple[int, str] |
固定结构 (int, str) |
| 集合 | set[str] |
元素为 str 的集合 |
| 可选值 | Optional[int] 或 int \| None |
可能是 int 或 None |
| 任意类型 | Any |
可以是任何类型 |
| 联合类型 | Union[int, str] 或 int \| str |
int 或 str |
| 可调用对象 | Callable[[int, int], int] |
接收两个 int 返回 int 的函数 |
| 无返回值 | None |
函数没有返回值 |
from typing import Optional, Any, Union, Callable
# 可选值(可能是 None)
def find_user(id: int) -> Optional[dict]:
# 返回 dict 或 None
...
# 联合类型(Python 3.10+ 可以用 | 语法)
def process(value: int | str) -> str:
return str(value)
# 可调用对象
def apply_func(func: Callable[[int], str], x: int) -> str:
return func(x)
# Any - 你能传入任何东西
def debug_log(msg: Any) -> None:
print(str(msg))
3.5 静态类型检查工具:mypy
注解本身不检查类型,但工具可以:
pip install mypy
mypy your_code.py
示例:
# greet.py
def greet(name: str) -> str:
return f"你好, {name}!"
result = greet(42) # 类型错误!42 不是 str
$ mypy greet.py
greet.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
mypy 的工作流:
┌─────────────┐
│ 写代码 │
│ + 类型注解 │
└──────┬──────┘
│
▼
┌─────────────┐
│ 运行 mypy │
└──────┬──────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 没有错误 │ │ 发现类型错误 │
│ 提交代码 ✅ │ │ 修复后再提交 ❌│
└──────────────┘ └──────────────┘
3.6 实战:带类型注解的数据处理
from typing import List, Dict, Optional
def clean_data(raw_data: List[Dict[str, str]]) -> List[Dict[str, str]]:
"""
清洗原始数据,去除空值记录。
Args:
raw_data: 原始数据列表,每条记录是字典
Returns:
清洗后的数据列表
"""
cleaned = []
for record in raw_data:
# 去除空值记录
if any(record.values()):
cleaned.append(record)
return cleaned
def analyze_scores(scores: List[Union[int, float]]) -> Dict[str, float]:
"""
分析分数数据。
Args:
scores: 分数列表
Returns:
包含统计结果的字典
"""
if not scores:
return {"mean": 0.0, "max": 0.0, "min": 0.0}
return {
"mean": sum(scores) / len(scores),
"max": max(scores),
"min": min(scores),
}
3.7 何时该用 Type Hints?
| 场景 | 建议 | 理由 |
|---|---|---|
| 学习/练习代码 | 可选 | 帮助理解类型,但非必须 |
| 个人脚本(< 200 行) | 可不用 | 自己看懂就行 |
| 小型项目 | 核心函数加注解 | 提高可读性 |
| 中大型项目 | 全员加注解 | 用 mypy 做 CI 检查 |
| 库/API 开发 | 必须加注解 | 使用者需要明确的接口 |
| 教学代码 | 强烈推荐 | 帮助学习者理解类型 |
4. 实战:通用数据处理器
4.1 需求分析
构建一个通用数据处理器,它利用今天学到的所有进阶函数特性:
| 功能 | 涉及的技术 |
|---|---|
| 支持任意类型的数据传入 | *args, **kwargs |
| 可配置预处理函数 | 函数作为参数(Callback) |
| 带缓存的耗时计算 | 默认参数陷阱(缓存字典) |
| 安全的默认配置 | None 默认值模式 |
| 类型注解 | Type Hints |
| 多种聚合方式 | **kwargs 配置聚合参数 |
| 自动生成报告 | 关键字参数控制输出 |
4.2 架构设计
┌──────────────────────────────────────────────────┐
│ 通用数据处理器 │
│ │
│ User Code │
│ ┌──────────────────────────────────────────┐ │
│ │ data = [1, 2, 3, 4, 5] │ │
│ │ processor = DataProcessor() │ │
│ │ result = processor.process(*data, │ │
│ │ preprocess=square, │ │
│ │ agg_method="sum", │ │
│ │ report=True) │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ DataProcessor │ │
│ │ │ │
│ │ ① 预处理(preprocess callback) │ │
│ │ ② 聚合计算(方法名称通过 **kwargs 传入) │ │
│ │ ③ 缓存结果(cache 字典) │ │
│ │ ④ 报告生成(通过关键字参数开关) │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
4.3 完整代码
详见 code/04-universal-data-processor.py,以下是核心逻辑概览:
from typing import List, Union, Optional, Callable, Dict, Any
import time
class DataProcessor:
"""
通用数据处理器
利用函数进阶特性实现灵活的数据处理管道:
- 变参接收任意数量数据
- 回调函数作为预处理
- 缓存 + None 默认值安全模式
- 完整类型注解
"""
def __init__(self, name: str = "default"):
self.name = name
# 缓存字典 — 利用函数进阶知识:用 None 避免陷阱
self._cache: Dict[str, Any] = {}
def process(
self,
*data: Union[int, float],
preprocess: Optional[Callable] = None,
agg_method: str = "sum",
use_cache: bool = True,
**options: Any,
) -> Dict[str, Any]:
"""
核心处理方法
Args:
*data: 要处理的数据(可变位置参数)
preprocess: 可选的预处理函数
agg_method: 聚合方法(sum/mean/max/min)
use_cache: 是否使用缓存
**options: 其他选项(verbose, report 等)
Returns:
处理结果字典
"""
# 应用预处理
if preprocess is not None:
processed = [preprocess(x) for x in data]
else:
processed = list(data)
# 缓存检查
if use_cache:
cache_key = self._make_cache_key(processed, agg_method)
if cache_key in self._cache:
return self._cache[cache_key]
# 聚合计算
if agg_method == "sum":
result_value = sum(processed)
elif agg_method == "mean":
result_value = sum(processed) / len(processed) if processed else 0.0
elif agg_method == "max":
result_value = max(processed) if processed else 0.0
elif agg_method == "min":
result_value = min(processed) if processed else 0.0
else:
raise ValueError(f"不支持的聚合方法: {agg_method}")
result = {
"processor": self.name,
"method": agg_method,
"data_count": len(processed),
"result": result_value,
"timestamp": time.time(),
}
# 缓存结果
if use_cache:
cache_key = self._make_cache_key(processed, agg_method)
self._cache[cache_key] = result
# 详细输出
if options.get("verbose"):
print(f"[{self.name}] 处理完成: 共 {len(processed)} 条数据")
return result
def _make_cache_key(self, data: list, method: str) -> str:
return f"{method}_{hash(tuple(data))}"
5. 思考题
- 默认参数陷阱的深度理解:试试下面的代码,解释为什么第一次和第二次的输出不同:
def surprise(x, lst=[]):
lst.append(len(lst))
return lst
print(surprise(1)) # 输出: [0]
print(surprise(2)) # 输出: [0, 1] — 为什么是 0, 1 而不是 0, 0?
思考顺序:第一次调用时 lst 是 [],len(lst) 是 0,所以 lst 变成 [0]。第二次调用时 lst 还是那个列表,len(lst) 是 1,所以追加 1。这就证明了调用之间共享同一个列表对象。
-
args 和 *kwargs 的对比:
*args和**kwargs各自什么时候使用?如果函数需要同时使用两者,为什么*args必须在**kwargs之前?尝试从 Python 参数解析机制的底层逻辑思考。 -
类型注解的"谎言":Type Hints 只是"注解",不强制检查类型。那么以下代码会怎样?
def divide(a: int, b: int) -> float:
return a / b
result = divide("10", "5")
print(result)
测试后思考:这说明了类型注解的什么特性?在实际项目中如何弥补这个"不强制"的不足?
-
仅限关键字参数的设计:写一个函数
send_message(recipient, subject, *, urgency, attachments=None),解释为什么urgency和attachments要设计成仅限关键字参数?如果允许用位置参数调用send_message("a", "b", "high", ["file.pdf"])会有什么问题? -
混合所有概念:设计一个函数签名,它至少包含:2 个位置参数、
*args、1 个仅限关键字参数、**kwargs,并且包含完整的类型注解。描述这个函数应该做什么,并解释为什么需要这么复杂的参数结构。