本章摘要

  1. 核心目标:掌握函数参数的灵活用法与变量作用域,能写出更通用、更健壮的自定义函数
  2. 核心知识点:默认参数、关键字参数、*args**kwargs、参数顺序规则、全局/局部变量、函数嵌套、global关键字、nonlocal关键字、闭包入门
  3. 实操产出:可配置的学生信息统计工具,支持任意数量成绩统计与格式控制
  4. 前置基础:第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

注意三点:

  1. *args 收到的是一个元组tuple),不是列表
  2. args 只是约定俗成的名字,写成 *numbers 也行,关键是那个 *
  3. 不传任何参数时,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:传了值就覆盖默认值 10
  • 3, 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_countercounter之间——这就是**闭包 **。
闭包是 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
==========================================

分步拆解:

  1. 全局变量 call_count:记录函数被调用的次数,用 global 在函数内修改
  2. *students:接收任意数量的学生字典,每个字典包含 name 和各科成绩。传 3 个学生也行,传 1 个也行
  3. 默认参数 fmt="simple":默认简洁格式,传 fmt="detail" 切换详细格式,多出"学生列表"部分
  4. 默认参数 show_max=True:默认显示最高分,传 show_max=False 只看平均分
  5. **extra:接收额外配置,比如 title="期中考试成绩",用 extra.get("title", "默认标题") 安全取值(第7章学的字典 get 方法)
  6. 收集科目成绩:遍历每个学生字典,跳过 name 字段,把成绩按科目分组存入 subjects 字典
  7. 统计输出:遍历 subjects 字典,计算每科平均分(sum / len)和最高分(max),根据 show_max 控制是否显示

这个案例把本章所有知识点串了起来:*args、默认参数、**kwargsglobal、全局/局部变量、字典遍历、循环。如果你能独立写出来,说明本章内容真正掌握了。


十一、常见报错速查表

报错信息 出现场景 原因 解决方法
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 同一变量同时声明了 nonlocalglobal 两者语义冲突 一个变量只能选一种声明方式
TypeError: got an unexpected keyword argument 传了函数不接受的参数 函数没有对应参数,也没有 **kwargs 添加 **kwargs 接收多余参数,或检查参数名拼写

十二、本章小结

本章你掌握了函数参数的进阶用法和变量作用域:

  1. 默认参数——def f(a, b=10),不传用默认值,传了覆盖。默认值只能用不可变对象,可变对象用 None 占位
  2. 关键字参数——调用时用 参数名=值 传参,顺序随意,可读性更强
  3. *args——接收任意数量的位置参数,打包成元组
  4. **kwargs——接收任意数量的关键字参数,打包成字典
  5. 参数顺序——位置参数 → 默认参数 → *args → **kwargs,顺序不可乱
  6. 变量作用域——全局变量到处可用,局部变量只在函数内有效
  7. 函数嵌套——函数内部可以再定义函数,内层函数外部无法直接访问
  8. global 关键字——在函数内修改全局变量需要先声明 global,但能不用就不用
  9. **nonlocal 关键字 **——在嵌套函数中修改外层函数的变量需要声明 nonlocal,常用于闭包场景

这些知识让你的函数从"只能接收固定参数"升级为"灵活适配各种场景",是写真实项目的基础能力。globalnonlocal 虽然不常用,但理解了作用域的层级关系,写代码时就不会被"变量怎么改不了"的问题困扰。

往期回顾:

《Python 零基础入门:从积木到代码》专栏序

零基础学 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 第8章 | 字符串操作与格式化输出

零基础学 Python 第9章 | 函数基础:def、参数与返回值

下一章预告:学了这么多函数用法,有些场景只需要一行代码的小函数,写 def 又太啰嗦。下一章学习匿名函数(lambda)与高阶函数(mapfiltersorted),让你的代码更简洁、更 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零基础学习内容

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐