请添加图片描述
在这里插入图片描述



五、函数作用域

5.1 作用域的概念

作用域是程序中定义的变量所存在的区域,它决定了变量在程序中的可见性和生命周期。在 Python 中,每个函数都创建了一个新的作用域,称为局部作用域,而函数外部的区域称为全局作用域

变量的作用域规则

  • 变量在定义它的作用域内可见。

  • 变量在定义它的作用域之外不可见,除非使用特殊关键字(如 global、nonlocal)。

  • 当在一个作用域中引用变量时,Python 会按照特定的顺序查找变量,直到找到为止。

示例

# 全局作用域
global_var = "I'm global"

def outer_function():
    # 外部函数的局部作用域
    outer_var = "I'm outer"
    
    def inner_function():
        # 内部函数的局部作用域
        inner_var = "I'm inner"
        print(global_var)  # 可以访问全局变量
        print(outer_var)   # 可以访问外部函数的变量
        print(inner_var)   # 只能在内部函数中访问

    inner_function()
    # print(inner_var)  # 这里会报错,因为inner_var在inner_function的作用域内

outer_function()

5.2 作用域链与 LEGB 规则

Python 中变量的查找遵循LEGB 规则,即按照以下顺序查找变量:

  1. L(Local):当前函数的局部作用域。

  2. E(Enclosing):外部嵌套函数的作用域(从内到外)。

  3. G(Global):全局作用域。

  4. B(Built-in):内置作用域(Python 内置的函数和变量)。

当在一个作用域中引用变量时,Python 会首先在当前局部作用域中查找,如果找不到,则在外层嵌套函数的作用域中查找,依此类推,最后在内置作用域中查找。

示例

# 全局作用域
global_var = "Global"

def outer():
    # 外部函数的局部作用域
    outer_var = "Outer"
    
    def inner():
        # 内部函数的局部作用域
        inner_var = "Inner"
        
        # 按照LEGB规则查找变量
        print(inner_var)  # 首先在Local查找,找到inner_var
        print(outer_var)  # 在Local未找到,到Enclosing查找,找到outer_var
        print(global_var) # 在Enclosing未找到,到Global查找,找到global_var
        print(len)        # 在Global未找到,到Built-in查找,找到len函数

    inner()

outer()

5.3 global 和 nonlocal 关键字

在某些情况下,我们需要在函数内部修改全局变量或外部函数的变量,这时可以使用globalnonlocal关键字:

  1. global关键字:用于在函数内部声明变量为全局变量,这样函数内部对变量的修改会影响全局变量。

  2. nonlocal关键字:用于在嵌套函数内部声明变量为外部函数的局部变量,这样内部函数对变量的修改会影响外部函数的变量。

示例

# global关键字示例
count = 0

def increment():
    global count  # 声明count为全局变量
    count += 1
    print(count)

increment()  # 输出: 1
increment()  # 输出: 2

# nonlocal关键字示例
def outer():
    x = 10
    
    def inner():
        nonlocal x  # 声明x为外部函数的变量
        x += 5
        print(x)
    
    inner()
    print(x)  # 这里会输出15,因为inner函数修改了x

outer()

使用 global 和 nonlocal 的注意事项

  • global关键字可以声明多个变量,如global x, y

  • nonlocal关键字只能在嵌套函数中使用。

  • 应谨慎使用globalnonlocal,过度使用会降低代码的可读性和可维护性。

  • 在函数内部,如果对变量进行了赋值操作,Python 默认会将其视为局部变量,除非使用globalnonlocal声明。

六、函数相关易错点

6.1 参数传递相关错误

在函数参数传递过程中,常见的错误包括:

错误 1:参数数量不匹配

# 错误示例:参数数量不足
def greet(name):
    print(f"Hello, {name}!")

greet()  # 错误:缺少1个位置参数

# 错误示例:参数数量过多
def add(a, b):
    return a + b

add(1, 2, 3)  # 错误:传递了3个位置参数,而函数只接受2个

错误原因:函数调用时传递的参数数量与函数定义的参数数量不一致。

解决方案

  • 检查函数定义和调用时的参数数量是否匹配。

  • 使用默认参数或可变参数(*args、**kwargs)增加函数的灵活性。

错误 2:参数类型不匹配

# 错误示例:参数类型错误
def multiply(a, b):
    return a * b

result = multiply("2", 3)  # 虽然语法正确,但结果可能不是预期的
print(result)  # 输出: "222",而不是6

错误原因:虽然参数数量正确,但参数的类型不符合函数的预期用途,导致逻辑错误。

解决方案

  • 在函数内部添加类型检查或转换。

  • 使用函数注释(Python 3.5+)明确参数类型和返回值类型。

  • 编写文档字符串说明参数的类型和用途。

错误 3:参数顺序错误

# 错误示例:参数顺序错误
def describe_person(name, age, city):
    print(f"{name} is {age} years old and lives in {city}")

describe_person("30", "Alice", "New York")  # 参数顺序错误
# 输出: 30 is Alice years old and lives in New York,逻辑错误

错误原因:使用位置传递时,参数顺序与函数定义不一致,导致逻辑错误。

解决方案

  • 当参数数量较多或顺序不明显时,使用关键字传递。

  • 在函数调用时明确指定参数名称。

  • 保持函数参数顺序的一致性和逻辑性。

6.2 作用域相关错误

在函数作用域方面,常见的错误包括:

错误 1:在函数内部修改全局变量未使用 global 关键字

# 错误示例:在函数内部修改全局变量未使用global
count = 0

def increment():
    count += 1  # 错误:这里会报UnboundLocalError
    print(count)

increment()

错误原因:在函数内部尝试修改全局变量count,但未使用global关键字声明,导致 Python 将count视为局部变量,而局部变量count在赋值前被引用。

解决方案

  • 在函数内部使用global关键字声明要修改的全局变量。

  • 避免在函数内部修改全局变量,优先通过参数传递和返回值来实现数据交换。

错误 2:变量名遮蔽(Shadowing)

# 错误示例:变量名遮蔽
length = 10  # 全局变量

def calculate_area(width):
    length = 5  # 局部变量,遮蔽了全局变量length
    area = length * width
    return area

print(calculate_area(4))  # 输出: 20,而不是40

错误原因:函数内部定义了与全局变量同名的局部变量,导致全局变量被遮蔽,函数使用的是局部变量。

解决方案

  • 避免在函数内部使用与全局变量同名的局部变量。

  • 使用更具描述性的变量名,提高代码可读性。

  • 当需要使用全局变量时,明确使用global关键字声明。

错误 3:闭包中的延迟绑定

# 错误示例:闭包中的延迟绑定
def create_multipliers():
    multipliers = []
    for i in range(1, 4):
        multipliers.append(lambda x: x * i)  # 这里的i是循环变量,在闭包中被延迟绑定
    return multipliers

# 调用闭包函数
times2, times3, times4 = create_multipliers()
print(times2(2))  # 输出: 6,而不是4
print(times3(2))  # 输出: 6,而不是6
print(times4(2))  # 输出: 6,而不是8

错误原因:在循环中创建闭包时,内部函数引用的是循环变量i,但由于闭包的延迟绑定特性,所有内部函数都引用了循环结束后i的值(即 3)。

解决方案

  • 在循环中创建闭包时,通过默认参数强制绑定当前i的值:
def create_multipliers():
    multipliers = []
    for i in range(1, 4):
        multipliers.append(lambda x, n=i: x * n)  # 使用默认参数绑定当前i的值
    return multipliers

times2, times3, times4 = create_multipliers()
print(times2(2))  # 输出: 4
print(times3(2))  # 输出: 6
print(times4(2))  # 输出: 8

6.3 默认参数相关错误

在使用函数默认参数时,常见的错误包括:

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

# 错误示例:使用可变对象作为默认参数
def add_to_list(item, lst=[]):
    lst.append(item)
    return lst

print(add_to_list(1))  # 输出: [1]
print(add_to_list(2))  # 输出: [1, 2],而不是预期的[2]
print(add_to_list(3))  # 输出: [1, 2, 3],而不是预期的[3]

错误原因:Python 中的默认参数在函数定义时计算一次,而不是每次调用时重新计算。对于可变对象(如列表),这意味着所有后续调用共享同一个默认参数对象,导致意外的副作用。

解决方案

  • 将可变对象的默认参数设置为None,并在函数内部进行检查和初始化:
def add_to_list(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

print(add_to_list(1))  # 输出: [1]
print(add_to_list(2))  # 输出: [2]
print(add_to_list(3))  # 输出: [3]

错误 2:默认参数值在函数定义时计算

# 错误示例:默认参数值在函数定义时计算
import time

def show_time(timestamp=time.time()):
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)))

# 第一次调用使用默认参数
show_time()  # 输出函数定义时的时间

# 10秒后再次调用
time.sleep(10)
show_time()  # 仍然输出函数定义时的时间,而不是当前时间

错误原因:默认参数值在函数定义时计算,而不是在函数调用时计算,导致默认参数值固定为函数定义时的值。

解决方案

  • 在函数内部动态获取需要的值,而不是使用默认参数:
import time

def show_time(timestamp=None):
    if timestamp is None:
        timestamp = time.time()
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(timestamp)))

# 第一次调用使用默认值
show_time()  # 输出当前时间

# 10秒后再次调用
time.sleep(10)
show_time()  # 输出当前时间

七、函数的实际应用案例

7.1 代码复用案例:计算图形面积

假设我们需要在程序中多次计算不同图形的面积,包括矩形、三角形和圆形。通过将每个图形的面积计算封装成函数,我们可以实现代码的复用和逻辑的清晰组织。

示例代码

# 定义计算矩形面积的函数
def rectangle_area(length, width):
    """计算矩形的面积"""
    return length * width

# 定义计算三角形面积的函数
def triangle_area(base, height):
    """计算三角形的面积"""
    return 0.5 * base * height

# 定义计算圆形面积的函数
def circle_area(radius):
    """计算圆形的面积"""
    return 3.14159 * radius ** 2

# 使用这些函数进行计算
print("矩形面积:", rectangle_area(5, 3))  # 输出: 矩形面积:15
print("三角形面积:", triangle_area(4, 6))  # 输出: 三角形面积:12
print("圆形面积:", circle_area(2))  # 输出: 圆形面积:12.56636

代码分析

  • 每个图形的面积计算逻辑被封装在独立的函数中,提高了代码的可读性和可维护性。

  • 当需要修改某个图形的面积计算公式时,只需要在对应的函数中修改一次。

  • 可以方便地在其他部分的程序中复用这些函数。

7.2 逻辑封装案例:用户注册验证

假设我们需要实现一个用户注册功能,需要对用户输入的信息进行验证,包括用户名长度、密码强度、邮箱格式等。通过将这些验证逻辑封装成函数,可以使主程序更加简洁,逻辑更加清晰。

示例代码

import random

def generate_secret_number():
    """生成一个1到100之间的随机数"""
    return random.randint(1, 100)

def get_guess():
    """获取用户的猜测,并确保是有效的整数"""
    while True:
        guess = input("请输入你的猜测(1-100):")
        if guess.isdigit():
            return int(guess)
        else:
            print("错误:请输入一个有效的整数。")

def check_guess(guess, secret):
    """检查猜测是否正确,并返回提示信息"""
    if guess < secret:
        return "太小了!"
    elif guess > secret:
        return "太大了!"
    else:
        return "正确!"

def play_game():
    """主游戏函数"""
    secret = generate_secret_number()
    attempts = 0
    max_attempts = 5
    
    print("欢迎来到猜数字游戏!你有5次机会猜一个1到100之间的数字。")
    
    while attempts < max_attempts:
        guess = get_guess()
        attempts += 1
        
        result = check_guess(guess, secret)
        print(result)
        
        if result == "正确!":
            print(f"恭喜你在第{attempts}次猜对了!")
            return
    
    print(f"很遗憾,你用完了所有机会。正确答案是{secret}。")

# 开始游戏
play_game()

代码分析

  • 每个验证逻辑被封装在独立的函数中,主程序只负责流程控制和用户交互。

  • 每个验证函数都有明确的职责和返回值,便于测试和维护。

  • 如果需要修改验证规则,只需要在对应的函数中修改,不会影响其他部分。

7.3 实战练习:猜数字游戏

现在,我们将综合运用函数的知识,实现一个猜数字游戏。游戏规则如下:

  1. 程序生成一个 1 到 100 之间的随机数。

  2. 用户有 5 次机会猜测这个数字。

  3. 每次猜测后,程序提示用户猜测的数字是太大、太小还是正确。

  4. 如果用户在 5 次内猜对,显示成功消息;否则,显示正确答案。

示例代码

import random

def generate_secret_number():
    """生成一个1到100之间的随机数"""
    return random.randint(1, 100)

def get_guess():
    """获取用户的猜测,并确保是有效的整数"""
    while True:
        guess = input("请输入你的猜测(1-100):")
        if guess.isdigit():
            return int(guess)
        else:
            print("错误:请输入一个有效的整数。")

def check_guess(guess, secret):
    """检查猜测是否正确,并返回提示信息"""
    if guess < secret:
        return "太小了!"
    elif guess > secret:
        return "太大了!"
    else:
        return "正确!"

def play_game():
    """主游戏函数"""
    secret = generate_secret_number()
    attempts = 0
    max_attempts = 5
    
    print("欢迎来到猜数字游戏!你有5次机会猜一个1到100之间的数字。")
    
    while attempts < max_attempts:
        guess = get_guess()
        attempts += 1
        
        result = check_guess(guess, secret)
        print(result)
        
        if result == "正确!":
            print(f"恭喜你在第{attempts}次猜对了!")
            return
    
    print(f"很遗憾,你用完了所有机会。正确答案是{secret}。")

# 开始游戏
play_game()

代码分析

  • 游戏的各个功能被分解为多个函数,每个函数负责一个特定的任务。

  • generate_secret_number负责生成随机数,get_guess负责获取用户输入,check_guess负责验证猜测,play_game负责游戏流程控制。

  • 这种模块化设计使得代码结构清晰,易于理解和维护。

  • 每个函数都有明确的职责和返回值,便于测试和复用。

八、课程总结

今天的课程中,我们学习了 Python 函数的定义与调用,这是 Python 编程中非常基础且重要的内容。通过本节课的学习,你应该能够:

重点回顾

  1. 函数的定义与调用
  • 使用def关键字定义函数,包括参数列表、文档字符串、函数体和返回值。

  • 函数调用时传递实际参数,可以通过位置传递、关键字传递或混合传递。

  • 区分无返回值函数(返回 None)和有返回值函数的调用方式。

  1. 参数传递机制
  • 对于不可变对象(如数字、字符串),传递的是值的副本,函数内部修改不影响外部变量。

  • 对于可变对象(如列表、字典),传递的是对象的引用,函数内部修改会影响外部变量。

  • 理解参数解包(* 和 ** 运算符)的使用方法。

  1. 函数作用域
  • 变量的作用域决定了其可见性和生命周期。

  • 变量查找遵循 LEGB 规则:Local → Enclosing → Global → Built-in。

  • 使用globalnonlocal关键字修改全局变量和外部函数的变量。

  1. 函数相关易错点
  • 参数数量不匹配、类型不匹配和顺序错误。

  • 作用域相关错误,如未使用global声明而修改全局变量。

  • 默认参数的陷阱,尤其是使用可变对象作为默认参数。

  1. 函数的实际应用
  • 通过函数实现代码复用,如计算不同图形的面积。

  • 通过函数封装逻辑,如用户注册验证。

  • 综合运用函数实现复杂程序,如猜数字游戏。

课后任务

  1. 复习本节课的内容,确保完全理解函数的定义、调用和参数传递机制。

  2. 完成课堂练习中的猜数字游戏,并尝试扩展其功能。

  3. 预习下一节课的内容:函数进阶(包括递归函数、匿名函数等)。

下一步学习建议

  1. 练习将常用的代码块封装成函数,提高代码的复用性。

  2. 尝试使用不同类型的参数(如默认参数、可变参数)编写更灵活的函数。

  3. 学习使用文档字符串和函数注释,提高代码的可读性和可维护性。

  4. 尝试编写一些简单的递归函数,理解递归的基本思想。

函数是编程的核心概念,掌握函数的使用是成为一名优秀程序员的必经之路。通过不断练习和实践,你将能够更加熟练地使用函数来组织和管理代码,编写更高效、更清晰的程序。

如果你在学习过程中遇到任何问题,请随时提问。
在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐