零基础学 Python 第10章 | 函数进阶:参数灵活用法与作用域
本章摘要
- 核心目标:掌握函数参数的灵活用法与变量作用域,能写出更通用、更健壮的自定义函数
- 核心知识点:默认参数、关键字参数、
*args、**kwargs、参数顺序规则、全局/局部变量、函数嵌套、global关键字、nonlocal关键字、闭包入门 - 实操产出:可配置的学生信息统计工具,支持任意数量成绩统计与格式控制
- 前置基础:第9章函数定义与调用、第7章字典、第4-5章循环与条件
一、写在前面
上一章你学会了函数的基本用法:用 def 定义函数,传参调用,return 返回结果。但你可能已经发现一个问题——每次调用函数,参数的个数和顺序都得严格对应,少一个不行,多一个也不行。
现实需求可没这么死板。比如你写了一个打招呼的函数,大多数时候说的是"你好",偶尔才需要说"早上好"——每次都得把"你好"传一遍,太啰嗦了。再比如,你想写一个能统计任意数量学生成绩的函数,但参数个数写死了就只能接收固定数量——这也不够灵活。
本章就是来解决这些问题的。你将学会默认参数(给参数一个保底值)、关键字参数(按名字传参,顺序随意)、*args(接收任意数量的位置参数)、**kwargs(接收任意数量的关键字参数),以及变量作用域(哪些变量在哪里能用)。
本章对应第 11 天 + 第 12 天的学习内容,知识点较多但环环相扣,建议跟着代码一步步敲。
二、默认参数:给参数一个"保底值"
默认参数的核心思想:定义函数时给参数预设一个值,调用时不传就用默认值,传了就覆盖。
# 定义带默认参数的函数
def greet(name, greeting="你好"):
print(f"{greeting},{name}!")
# 不传 greeting,用默认值"你好"
greet("小明") # 你好,小明!
# 传了 greeting,覆盖默认值
greet("小红", "早上好") # 早上好,小红!
就这么简单——greeting="你好" 就是默认参数。调用时省略它,自动用"你好";传了新值,就用新值。
2.1 默认参数的实用场景
默认参数最常见的用途是让函数既能简单调用,又能灵活配置:
# 计算购物折扣,默认打 9 折
def calculate_price(price, discount=0.9):
return price * discount
# 大多数情况用默认折扣
print(calculate_price(100)) # 90.0
# 特殊情况自定义折扣
print(calculate_price(100, 0.8)) # 80.0
2.2 默认参数必须是不可变对象
⚠️ 重要规则:默认参数的值只能用不可变对象(数字、字符串、元组、
None),千万不要用列表、字典等可变对象。
# ❌ 错误示范:用列表做默认参数
def add_item(item, item_list=[]):
item_list.append(item)
return item_list
print(add_item("苹果")) # ['苹果']
print(add_item("香蕉")) # ['苹果', '香蕉'] ← 不是 ['香蕉']!
第二次调用时,item_list 还是第一次调用后那个列表,因为默认参数只在函数定义时创建一次,后续调用共用同一个对象。
# ✅ 正确写法:用 None 做默认值,函数内创建
def add_item(item, item_list=None):
if item_list is None:
item_list = []
item_list.append(item)
return item_list
print(add_item("苹果")) # ['苹果']
print(add_item("香蕉")) # ['香蕉']
记住这条铁律:默认参数用
None占位,在函数内部再创建可变对象。
三、关键字参数:按名字传值,顺序随意
默认情况下,调用函数时参数是按位置一一对应的:第一个实参给第一个形参,第二个给第二个。但参数一多,谁是谁就容易搞混。
关键字参数就是调用时用 参数名=值 的方式传参,让顺序不再重要:
def student_info(name, age, city):
print(f"姓名:{name},年龄:{age},城市:{city}")
# 位置参数:必须按顺序
student_info("小明", 18, "北京") # 姓名:小明,年龄:18,城市:北京
# 关键字参数:顺序随意
student_info(city="上海", name="小红", age=20) # 姓名:小红,年龄:20,城市:上海
# 混合使用:位置参数在前,关键字参数在后
student_info("小刚", age=19, city="广州") # 姓名:小刚,年龄:19,城市:广州
关键字参数最大的好处是可读性强——看到
city="上海"就知道传的是城市,不用去数第几个参数是城市。
3.1 混合传参的规则
位置参数和关键字参数可以混用,但有一条铁律:位置参数必须在关键字参数前面。
# ✅ 正确:位置参数在前
student_info("小明", age=18, city="北京")
# ❌ 错误:关键字参数不能在位置参数前面
# student_info(name="小明", 18, "北京") # SyntaxError
四、*args:接收任意数量的位置参数
有时候你不确定用户会传多少个参数。比如一个求和函数,可能传 2 个数,也可能传 10 个数。写死参数个数显然不行。
*args 就是干这个的——它能把你传入的所有多余位置参数打包成一个元组:
def sum_all(*args):
print(f"收到 {len(args)} 个参数:{args}")
print(f"类型:{type(args)}")
return sum(args)
print(sum_all(1, 2, 3)) # 收到 3 个参数:(1, 2, 3) → 6
print(sum_all(10, 20, 30, 40)) # 收到 4 个参数:(10, 20, 30, 40) → 100
print(sum_all()) # 收到 0 个参数:() → 0
注意三点:
*args收到的是一个元组(tuple),不是列表args只是约定俗成的名字,写成*numbers也行,关键是那个*- 不传任何参数时,
args是空元组()
4.1 位置参数和 *args 混用
*args 可以和普通参数一起用,但 *args 要放在普通参数后面:
# 第一个参数是学生名,后面是任意数量的成绩
def report_grades(name, *scores):
print(f"{name} 的成绩:{scores}")
if scores:
print(f" 平均分:{sum(scores) / len(scores):.1f}")
report_grades("小明", 85, 90, 78) # 小明 的成绩:(85, 90, 78)
report_grades("小红", 92, 88) # 小红 的成绩:(92, 88)
"小明" 匹配 name,后面的 85, 90, 78 全部被 *scores 收进元组。
五、**kwargs:接收任意数量的关键字参数
*args 收集多余的位置参数,**kwargs 则收集多余的关键字参数,打包成一个字典:
def print_info(name, **kwargs):
print(f"姓名:{name}")
print(f"其他信息:{kwargs}")
print(f"类型:{type(kwargs)}")
print_info("小明", age=18, city="北京", grade="高一")
运行结果:
姓名:小明
其他信息:{'age': 18, 'city': '北京', 'grade': '高一'}
类型:<class 'dict'>
name 被第一个参数接住,后面的 age=18, city="北京", grade="高一" 全部被 **kwargs 打包成字典。
kwargs也是约定俗成的名字(keyword arguments的缩写),写成**options也行,关键是两个**。
5.1 **kwargs 的实用场景
**kwargs 非常适合"可选配置"的场景:
# 创建用户,必传姓名,其他信息随意
def create_user(name, **options):
user = {"name": name}
user.update(options)
return user
user1 = create_user("小明")
print(user1) # {'name': '小明'}
user2 = create_user("小红", age=20, email="xh@qq.com", vip=True)
print(user2) # {'name': '小红', 'age': 20, 'email': 'xh@qq.com', 'vip': True}
六、参数组合的顺序规则
把前面学的参数类型放在一起,Python 规定了一个严格的顺序:
位置参数 → 默认参数 → *args → **kwargs
def func(a, b=10, *args, **kwargs):
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"kwargs={kwargs}")
func(1, 2, 3, 4, x=100, y=200)
运行结果:
a=1, b=2
args=(3, 4)
kwargs={'x': 100, 'y': 200}
拆解一下:
a=1:位置参数,匹配第一个形参b=2:传了值就覆盖默认值 103, 4:多余的位置参数,被*args收进元组x=100, y=200:关键字参数,被**kwargs收进字典
不需要每次都把四种参数全用上,但顺序不能乱。写错了 Python 会直接报
SyntaxError。
6.1 参数类型速查表
| 参数类型 | 定义方式 | 调用方式 | 收集结果 |
|---|---|---|---|
| 位置参数 | def f(a, b) |
f(1, 2) |
一一对应 |
| 默认参数 | def f(a, b=10) |
f(1) 或 f(1, 20) |
不传用默认值 |
*args |
def f(*args) |
f(1, 2, 3) |
元组 (1, 2, 3) |
**kwargs |
def f(**kwargs) |
f(x=1, y=2) |
字典 {'x': 1, 'y': 2} |
七、变量作用域:全局变量与局部变量
学完参数,我们来聊一个更容易踩坑的话题——变量的作用域,也就是"一个变量在哪些范围内能被访问"。
结论先放这里:函数内部定义的变量叫局部变量,只在函数内有效;函数外部定义的变量叫全局变量,整个程序都能访问。
# 全局变量
message = "我是全局变量"
def test():
# 局部变量
local_msg = "我是局部变量"
print(message) # ✅ 能访问全局变量
print(local_msg) # ✅ 能访问局部变量
test()
print(message) # ✅ 全局变量到处能用
# print(local_msg) # ❌ NameError: 函数外访问不到局部变量
7.1 局部变量:函数的"私房钱"
局部变量就像函数的私房钱——只有函数自己能用,外面谁也碰不到:
def calculate():
result = 100 # 局部变量,只在 calculate 内有效
calculate()
# print(result) # ❌ NameError: name 'result' is not defined
7.2 全局变量:程序的"公共账户"
全局变量定义在所有函数之外,任何函数都能读取:
# 全局变量
school_name = "阳光中学"
def show_school():
print(f"学校:{school_name}") # ✅ 函数内能读取全局变量
def show_school_again():
print(f"校名:{school_name}") # ✅ 另一个函数也能读取
show_school() # 学校:阳光中学
show_school_again() # 校名:阳光中学
7.3 函数内修改全局变量?直接赋值不行
这里有一个经典陷阱——在函数内读取全局变量没问题,但直接赋值会创建一个新的局部变量,而不是修改全局变量:
count = 0 # 全局变量
def increment():
count = count + 1 # ❌ 这里会报错!
print(count)
# increment() # UnboundLocalError: local variable 'count' referenced before assignment
Python 看到 count = count + 1 中的赋值操作,就把 count 当成了局部变量。但右边的 count + 1 还没赋值就要用,所以报错。
要解决这个问题,就需要下一节的 global 关键字。
八、函数嵌套与 global 关键字与 nonlocal 关键字
8.1 函数嵌套:函数里面套函数
Python 允许在函数内部再定义函数,这就是函数嵌套:
def outer():
print("外层函数开始")
def inner():
print("内层函数执行")
print("外层函数继续")
inner() # 在外层函数内调用内层函数
print("外层函数结束")
outer()
运行结果:
外层函数开始
外层函数继续
内层函数执行
外层函数结束
内层函数只在
outer()内部定义和调用,外部无法直接访问inner()。它就是个局部函数。
8.2 global 关键字:在函数内修改全局变量
回到上一节的问题——要在函数内修改全局变量,需要用 global 关键字声明:
count = 0 # 全局变量
def increment():
global count # 声明:我要用的是外面的全局变量 count
count = count + 1 # 现在修改的是全局变量
print(f"count = {count}")
increment() # count = 1
increment() # count = 2
increment() # count = 3
print(count) # 3(全局变量真的被改了)
global count 这句话告诉 Python:这个函数里的 count 不是新建的局部变量,而是外面那个全局变量。之后对 count 的赋值操作,改的就是全局变量本身。
8.3 什么时候用 global
能不用
global就不用。函数最好通过参数接收数据、通过返回值输出结果,这样代码更清晰、更不容易出 bug。global只在少数场景下使用(比如全局计数器、全局配置开关)。
# ✅ 推荐写法:用参数和返回值
def increment(count):
return count + 1
count = 0
count = increment(count) # 通过返回值更新
# ⚠️ 仅在必要时用 global
call_count = 0
def log_call():
global call_count
call_count += 1
print(f"第 {call_count} 次调用")
8.4 nonlocal 关键字:
在嵌套函数中修改外层函数的变量
global
解决的是"函数内修改全局变量"的问题。但如果你的变量既不是全局的,也不是当前函数局部的——而是外层函数的变量呢?先看一个没有
nonlocal会出问题的例子:
def outer():
count = 0 # 外层函数的变量
def inner():
count += 1 # ❌ UnboundLocalError!
print(f"内层:count = {count}")
inner()
print(f"外层:count = {count}")
outer()
和 global 之前的报错一模一样——inner() 里的 count += 1 有赋值操作,Python 就把 count 当成了 inner() 的局部变量,但右边 count + 1 还没赋值就要用,所以报错。
加上 nonlocal 就解决了:
def outer():
count = 0 # 外层函数的变量
def inner():
nonlocal count # 声明:我要用的是外层函数的 count
count += 1 # 现在修改的是外层函数的变量
print(f"内层:count = {count}")
inner() # 内层:count = 1
inner() # 内层:count = 2
inner() # 内层:count = 3
print(f"外层:count = {count}")
outer()
# 内层:count = 1
# 内层:count = 2
# 内层:count = 3
# 外层:count = 3
nonlocal count 告诉 Python:这个 count 不是新建的局部变量,也不是全局变量,而是**离我最近的那层外层函数里的 count **。
8.5 global vs nonlocal vs 默认行为:一张表搞懂
| 场景 | 不加声明 | 加 global | 加 nonlocal |
|---|---|---|---|
| 修改全局变量 | ❌ 创建局部变量 | ✅ 修改全局变量 | ❌ 报错(全局不是外层函数) |
| 修改外层函数的变量 | ❌ 创建局部变量 | ❌ 可能误改全局变量 | ✅ 修改最近外层函数的变量 |
| 修改当前函数的变量 | ✅ 正常操作 | 不需要 | 不需要 |
一句话记忆:
global= 我要改的是全局变量 (整个程序最外层)nonlocal= 我要改的是外层函数的变量(套着我的那层函数)- 什么都不加 = 我创建了一个新的局部变量
8.6 nonlocal 实用场景:闭包计数器
nonlocal 最经典的用法是制作闭包计数器 ——一个能"记住"状态的函数:
def make_counter():
"""创建一个计数器函数,每次调用 +1"""
count = 0 # 外层函数的变量,被"记住"了
def counter():
nonlocal count
count += 1
return count
return counter # 返回内层函数(注意:不调用,只返回)
# 创建两个独立的计数器
my_counter = make_counter()
print(my_counter()) # 1
print(my_counter()) # 2
print(my_counter()) # 3
your_counter = make_counter()
print(your_counter()) # 1(独立计数,互不影响)
print(your_counter()) # 2
这个例子展示了
nonlocal的精髓:count不是全局变量(外面访问不到),也不是局部变量(每次调用不会重置),它被"困"在了make_counter和counter之间——这就是**闭包 **。
闭包是 Python 进阶的重要概念,现在只需"理解"nonlocal让内层函数能修改外层函数的变量"就够了。
九、Scratch 积木对照
| Scratch | Python | 说明 |
|---|---|---|
| 全局变量 vs 角色私有变量 | 全局变量 vs 局部变量 | Scratch 全局变量所有角色可用,私有变量仅当前角色;Python 同理 |

# Scratch「适用于所有角色」的变量 → Python 全局变量
# Scratch「仅适用于当前角色」的变量 → Python 局部变量
total = 0 # 全局变量,函数内外都能用
def add_score():
score = 10 # 局部变量,只在函数内有效
# score 出了函数就没了,就像 Scratch 角色的私有变量
在 Scratch 中,"适用于所有角色"的变量就是全局变量,每个角色都能读写;"仅适用于当前角色"的变量就是局部变量,别的角色看不到。Python 的作用域概念完全一样——全局变量到处可用,局部变量只在函数内有效。
十、综合实战:可配置的学生信息统计工具
综合运用函数定义、默认参数、*args、**kwargs、作用域、字典、循环——写一个能接收任意数量学生成绩、按科目统计平均分和最高分、用默认参数控制输出格式的工具。
# ========================================
# 可配置的学生信息统计工具
# ========================================
# 全局变量:统计调用次数
call_count = 0
def stat_grades(*students, fmt="simple", show_max=True, **extra):
"""
可配置的学生成绩统计工具
参数:
*students : 任意数量的学生成绩字典,如 {"name": "小明", "语文": 85, "数学": 90}
fmt : 输出格式,"simple" 或 "detail",默认 "simple"
show_max : 是否显示最高分,默认 True
**extra : 额外配置,如 title="自定义标题"
"""
global call_count
call_count += 1
# 从额外配置中取标题,默认"学生成绩统计报告"
title = extra.get("title", "学生成绩统计报告")
# 收集各科目所有学生的成绩
subjects = {}
for student in students:
for key, value in student.items():
if key == "name":
continue # 跳过姓名字段
if key not in subjects:
subjects[key] = [] # 第一次出现的科目,新建列表
subjects[key].append(value) # 把成绩加入对应科目
# 输出报告
sep = "=" * 42
print(sep)
print(f" {title}(第 {call_count} 次统计)")
print(sep)
# 详细格式:先列出每个学生
if fmt == "detail":
print("\n【学生列表】")
for student in students:
name = student["name"]
grades = {k: v for k, v in student.items() if k != "name"}
grade_str = ",".join(f"{k}:{v}" for k, v in grades.items())
print(f" {name} → {grade_str}")
print()
# 按科目统计
print("【科目统计】")
for subject, scores in subjects.items():
avg = sum(scores) / len(scores)
line = f" {subject}:平均分 {avg:.1f}"
if show_max:
line += f",最高分 {max(scores)}"
print(line)
print(sep)
print()
# ====== 测试调用 ======
# 调用1:默认格式,显示最高分
stat_grades(
{"name": "小明", "语文": 85, "数学": 90, "英语": 78},
{"name": "小红", "语文": 92, "数学": 88, "英语": 95},
{"name": "小刚", "语文": 76, "数学": 85, "英语": 82},
)
# 调用2:详细格式 + 自定义标题 + 不显示最高分
stat_grades(
{"name": "小明", "语文": 85, "数学": 90},
{"name": "小红", "语文": 92, "数学": 88},
fmt="detail",
show_max=False,
title="期中考试成绩",
)
# 调用3:单个学生也能统计
stat_grades(
{"name": "小刚", "语文": 76, "数学": 85, "英语": 82, "物理": 90},
title="小刚个人成绩单",
)
运行结果:
==========================================
学生成绩统计报告(第 1 次统计)
==========================================
【科目统计】
语文:平均分 84.3,最高分 92
数学:平均分 87.7,最高分 90
英语:平均分 85.0,最高分 95
==========================================
==========================================
期中考试成绩(第 2 次统计)
==========================================
【学生列表】
小明 → 语文:85,数学:90
小红 → 语文:92,数学:88
【科目统计】
语文:平均分 88.5
数学:平均分 89.0
==========================================
==========================================
小刚个人成绩单(第 3 次统计)
==========================================
【科目统计】
语文:平均分 76.0,最高分 76
数学:平均分 85.0,最高分 85
英语:平均分 82.0,最高分 82
物理:平均分 90.0,最高分 90
==========================================
分步拆解:
- 全局变量
call_count:记录函数被调用的次数,用global在函数内修改 *students:接收任意数量的学生字典,每个字典包含name和各科成绩。传 3 个学生也行,传 1 个也行- 默认参数
fmt="simple":默认简洁格式,传fmt="detail"切换详细格式,多出"学生列表"部分 - 默认参数
show_max=True:默认显示最高分,传show_max=False只看平均分 **extra:接收额外配置,比如title="期中考试成绩",用extra.get("title", "默认标题")安全取值(第7章学的字典get方法)- 收集科目成绩:遍历每个学生字典,跳过
name字段,把成绩按科目分组存入subjects字典 - 统计输出:遍历
subjects字典,计算每科平均分(sum / len)和最高分(max),根据show_max控制是否显示
这个案例把本章所有知识点串了起来:
*args、默认参数、**kwargs、global、全局/局部变量、字典遍历、循环。如果你能独立写出来,说明本章内容真正掌握了。
十一、常见报错速查表
| 报错信息 | 出现场景 | 原因 | 解决方法 |
|---|---|---|---|
TypeError: missing required positional argument |
调用函数少传了必传参数 | 位置参数没传够 | 补全必传参数,或给参数加默认值 |
TypeError: got multiple values for argument |
同一参数既用位置又用关键字传 | 如 f(1, a=2) 中 a 被传了两次 |
同一参数只用一种方式传 |
SyntaxError: non-default argument follows default argument |
参数顺序写反 | 默认参数放在了位置参数前面 | 调整顺序:位置参数 → 默认参数 → *args → **kwargs |
NameError: name 'xxx' is not defined |
函数外访问局部变量 | 局部变量出了函数就销毁了 | 用 return 把值传出来,或改为全局变量 |
UnboundLocalError: local variable 'xxx' referenced before assignment |
函数内修改全局变量 | 赋值操作让 Python 把全局变量当成局部变量 | 在函数内加 global xxx 声明 |
SyntaxError: no binding for nonlocal 'xxx' found |
在内层函数用了 nonlocal 但外层函数没有这个变量 |
nonlocal 找不到对应的外层变量 |
确保外层函数中已定义该变量,或改用 global |
SyntaxError: name 'xxx' is nonlocal and global |
同一变量同时声明了 nonlocal 和 global |
两者语义冲突 | 一个变量只能选一种声明方式 |
TypeError: got an unexpected keyword argument |
传了函数不接受的参数 | 函数没有对应参数,也没有 **kwargs |
添加 **kwargs 接收多余参数,或检查参数名拼写 |
十二、本章小结
本章你掌握了函数参数的进阶用法和变量作用域:
- 默认参数——
def f(a, b=10),不传用默认值,传了覆盖。默认值只能用不可变对象,可变对象用None占位 - 关键字参数——调用时用
参数名=值传参,顺序随意,可读性更强 *args——接收任意数量的位置参数,打包成元组**kwargs——接收任意数量的关键字参数,打包成字典- 参数顺序——位置参数 → 默认参数 → *args → **kwargs,顺序不可乱
- 变量作用域——全局变量到处可用,局部变量只在函数内有效
- 函数嵌套——函数内部可以再定义函数,内层函数外部无法直接访问
global关键字——在函数内修改全局变量需要先声明global,但能不用就不用- **
nonlocal关键字 **——在嵌套函数中修改外层函数的变量需要声明nonlocal,常用于闭包场景
这些知识让你的函数从"只能接收固定参数"升级为"灵活适配各种场景",是写真实项目的基础能力。global 和 nonlocal 虽然不常用,但理解了作用域的层级关系,写代码时就不会被"变量怎么改不了"的问题困扰。
往期回顾:
零基础学 Python 第1章 | 全平台装环境 + VS Code 配置 + 第一个 Hello World
零基础学 Python 第2章 | print 输出 + 变量赋值 + input 交互输入
零基础学 Python 第3章 | 条件判断:if / elif / else
零基础学 Python 第 4 章 | for 循环与 range():让程序帮你重复干活
零基础学 Python 第 5 章 | while 循环与循环嵌套:让程序看着条件自己干活
零基础学 Python 第 6 章 | 元组与集合:不可变数据与自动去重
零基础学 Python 第7章 | 字典 dict:键值对存储
零基础学 Python 第9章 | 函数基础:def、参数与返回值
下一章预告:学了这么多函数用法,有些场景只需要一行代码的小函数,写 def 又太啰嗦。下一章学习匿名函数(lambda)与高阶函数(map、filter、sorted),让你的代码更简洁、更 Pythonic。
文末资源区
| 资源 | 说明 |
|---|---|
| Python 官方文档 - 函数定义 | https://docs.python.org/3/tutorial/controlflow.html#defining-functions |
| Python 官方文档 - 默认参数值 | https://docs.python.org/3/tutorial/controlflow.html#more-on-defining-functions |
| Python 官方文档 - 作用域与命名空间 | https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces |
| 本章完整源码 | chapter10_all.py(关注梅雅达编程笔记,评论领资料) |
每节课都配套课后练习题与完整源码。代码报错、看不懂知识点,可在评论区粘贴问题,逐一解答!
欢迎关注梅雅达编程笔记,持续分享Python零基础学习内容
更多推荐





所有评论(0)