Python 基础语法与数据类型(九) - 变量作用域:局部变量、全局变量 (global 关键字) 和匿名函数 (lambda)

在编写 Python 代码时,我们创建变量来存储数据。但这些变量在哪里可以被访问?它们的“生命周期”是多久?这就是变量作用域的概念。理解作用域对于避免程序中的意外行为至关重要。同时,我们还将学习一种快速定义简单函数的方法:lambda 函数。

1. 变量作用域 (Variable Scope)

作用域决定了程序中变量的可访问性。Python 主要有以下几种作用域(这里我们重点关注局部和全局):

  • 局部 (Local): 在函数内部定义的变量。
  • 外层/闭包 (Enclosing): 在嵌套函数中,外层函数的局部变量对于内层函数而言。
  • 全局 (Global): 在模块(文件)的最顶层定义的变量。
  • 内置 (Built-in): Python 内置的名称,如 print, len 等。

Python 查找变量时遵循一个称为 LEGB 的规则(Local -> Enclosing -> Global -> Built-in),它会按这个顺序在作用域中查找变量。

1.1 局部变量 (Local Variables)

在函数内部定义的变量具有局部作用域。它们只存在于函数执行期间,在函数外部无法访问。函数的参数也视为局部变量。

Python

def my_function():
    x = 10 # x 是局部变量
    print(f"函数内部: x = {x}")

my_function()
# print(x) # 这会引发 NameError,因为 x 在函数外部不存在

输出:

函数内部: x = 10
Traceback (most recent call last):
  ...
NameError: name 'x' is not defined
1.2 全局变量 (Global Variables)

在模块(.py 文件)的最顶层定义,不属于任何函数或类的变量,具有全局作用域。全局变量可以在程序的任何地方被访问(读取)。

Python

y = 20 # y 是全局变量

def another_function():
    print(f"函数内部(读取全局变量): y = {y}") # 可以在函数内部读取全局变量 y

print(f"函数外部: y = {y}") # 可以在函数外部访问全局变量 y
another_function()

输出:

函数外部: y = 20
函数内部(读取全局变量): y = 20
1.3 从函数内部修改全局变量 - global 关键字

这是初学者常会遇到的一个点。默认情况下,如果你在函数内部对一个变量进行赋值操作,Python 会认为你要创建一个新的局部变量,即使存在同名的全局变量。这个新的局部变量会遮蔽 (shadow) 同名的全局变量,使得在函数内部访问该名称时,优先使用局部变量。

Python

count = 0 # 全局变量

def increment_count_wrong():
    count = 1 # 错误!这里创建了一个新的局部变量 count,并没有修改全局的 count
    print(f"函数内部(错误修改): count = {count}")

print(f"修改前全局: count = {count}")
increment_count_wrong()
print(f"修改后全局: count = {count}") # 全局 count 的值并没有改变!

输出:

修改前全局: count = 0
函数内部(错误修改): count = 1
修改后全局: count = 0

可以看到,函数内部的赋值并没有影响到外部的全局 count

如果你确实需要在函数内部修改全局变量,必须使用 global 关键字来声明该变量是全局的。

Python

count = 0 # 全局变量

def increment_count_correct():
    global count # 声明 count 是全局变量
    count += 1 # 现在这里的 count 指的是全局的 count
    print(f"函数内部(正确修改): count = {count}")

print(f"修改前全局: count = {count}")
increment_count_correct()
print(f"修改后全局: count = {count}") # 全局 count 的值被成功修改了!
increment_count_correct() # 再次调用
print(f"再次修改后全局: count = {count}")

输出:

修改前全局: count = 0
函数内部(正确修改): count = 1
修改后全局: count = 1
函数内部(正确修改): count = 2
再次修改后全局: count = 2

最佳实践: 尽管 global 关键字很有用,但过度使用全局变量,特别是在函数内部频繁修改它们,会让代码难以跟踪和理解,降低可维护性。通常推荐的做法是通过参数将数据传递进函数,并通过返回值将结果传出。在某些特定场景(如计数器、全局配置)下,谨慎使用 global 是可以接受的。

2. 匿名函数 (Lambda Functions)

lambda 函数是一种创建小型、一次性、匿名函数的方法。它们通常用于那些只需要简单功能的场景,而无需使用 def 关键字来正式定义一个完整的函数。

  • 特点:
    • 匿名: 没有名字。
    • 单表达式: 函数体只能是一个表达式,表达式的计算结果会被隐式地返回。不能包含语句(如 if, for, while, return 语句本身,尽管表达式中可以使用条件表达式)。
    • 简洁: 适用于简单操作。
  • 语法: lambda arguments: expression

<!-- end list -->

Python

# 定义一个 lambda 函数,计算两个数的和
add = lambda a, b: a + b

# 调用 lambda 函数 (像调用普通函数一样)
print(f"使用 lambda 求和: {add(5, 3)}") # 输出: 使用 lambda 求和: 8

# 定义一个 lambda 函数,判断一个数是否为偶数
is_even_lambda = lambda num: num % 2 == 0
print(f"4 是偶数吗 (lambda)? {is_even_lambda(4)}") # 输出: 4 是偶数吗 (lambda)? True
print(f"7 是偶数吗 (lambda)? {is_even_lambda(7)}") # 输出: 7 是偶数吗 (lambda)? False

lambda 函数的常见使用场景:

lambda 函数通常作为参数传递给其他需要函数作为输入的内置函数或方法,比如:

  • sorted(iterable, key=func): 根据 key 参数指定的函数对可迭代对象进行排序。
  • map(func, iterable): 将函数应用于可迭代对象的每个元素,返回一个迭代器。
  • filter(func, iterable): 根据函数返回 True 的元素过滤可迭代对象,返回一个迭代器。

<!-- end list -->

Python

# 示例:使用 lambda 和 sorted() 对列表中的元组按第二个元素排序
pairs = [(1, 'one'), (3, 'three'), (2, 'two'), (4, 'four')]
# 排序依据是每个元组的第二个元素 (索引 1)
sorted_pairs = sorted(pairs, key=lambda pair: pair[1])
print(f"按元组第二个元素排序: {sorted_pairs}") # 输出: 按元组第二个元素排序: [(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')] (按字母顺序)

# 示例:使用 lambda 和 map() 将列表中的每个数字加 10
numbers = [1, 2, 3, 4]
add_ten = list(map(lambda x: x + 10, numbers)) # map 返回迭代器,需要转换为 list
print(f"使用 map 和 lambda 加 10: {add_ten}") # 输出: 使用 map 和 lambda 加 10: [11, 12, 13, 14]

# 示例:使用 lambda 和 filter() 筛选出列表中的偶数
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers)) # filter 返回迭代器
print(f"使用 filter 和 lambda 筛选偶数: {even_numbers}") # 输出: 使用 filter 和 lambda 筛选偶数: [2, 4, 6]

在这些场景下,使用 lambda 可以避免为了一个简单的操作而单独定义一个完整的命名函数,使得代码更加紧凑。

什么时候使用 def,什么时候使用 lambda?

  • 如果函数逻辑比较复杂,包含多条语句,或者需要编写文档字符串,或者需要在代码中多次重用,使用 def 定义命名函数是更好的选择。
  • 如果只需要一个简单的、一次性的功能,可以在需要函数对象的地方直接使用 lambda

总结

本篇我们探讨了 Python 中两个重要的概念:

  • 变量作用域: 理解局部变量和全局变量的区别,以及使用 global 关键字在函数内部修改全局变量的必要性。
  • 匿名函数 (lambda): 学习了如何使用 lambda 创建简洁的单表达式函数,及其在配合 sorted(), map(), filter() 等函数时的便利性。

掌握变量作用域能够帮助你避免因变量可见性问题导致的 bug,而 lambda 函数则为编写简洁的代码提供了便利。

练习题

尝试独立完成以下练习题,并通过答案进行对照:

  1. 局部 vs 全局变量:

    • 定义一个全局变量 message = "Global message"
    • 定义一个函数 print_message(),在函数内部定义一个同名局部变量 message = "Local message",然后在函数内部打印 message
    • 在函数外部打印全局 message。观察两个输出。
  2. 修改全局变量 (错误演示):

    • 定义一个全局变量 counter = 10
    • 定义一个函数 increment_counter_wrong(),在函数内部尝试 counter = counter + 1
    • 在函数外部打印修改前的 counter,调用函数,再打印修改后的 counter。观察结果和可能出现的错误。
  3. 修改全局变量 (正确演示):

    • 再次定义全局变量 counter = 10
    • 定义一个函数 increment_counter_correct(),使用 global 关键字正确修改全局 counter
    • 重复步骤 2 的打印操作,观察这次全局 counter 的变化。
  4. 匿名函数 (lambda):

    • 使用 lambda 创建一个函数,计算一个数字的立方。
    • 使用 lambda 创建一个函数,拼接两个字符串。
    • 调用并测试这两个 lambda 函数。
  5. lambda 与内置函数:

    • 创建一个包含字典的列表,每个字典有两个键:'name' 和 'score'。
    • 使用 sorted()lambda 对这个列表按照 'score' 进行升序排序,打印结果。
    • 使用 filter()lambda 筛选出分数大于等于 80 的学生字典,打印结果。
  6. 综合 (作用域与 lambda):

    • 定义一个全局变量 multiplier = 2
    • 定义一个函数 apply_operation(numbers, operation),它接受一个数字列表和一个函数 operation 作为参数。函数内部遍历列表,对每个数字应用 operation 函数,并将结果存储在一个新列表中返回。
    • 调用 apply_operation 函数,传入一个数字列表,并使用 lambda 作为 operation 参数,实现将列表中每个数字乘以全局变量 multiplier 的功能。

练习题答案

1. 局部 vs 全局变量:

Python

# 1. 局部 vs 全局变量:
message = "Global message" # 全局变量

def print_message():
    message = "Local message" # 局部变量,遮蔽了全局变量
    print(f"函数内部的 message: {message}")

print(f"函数外部的 message: {message}") # 访问全局变量
print_message() # 调用函数,访问函数内部的局部变量

输出:

函数外部的 message: Global message
函数内部的 message: Local message

2. 修改全局变量 (错误演示):

Python

# 2. 修改全局变量 (错误演示):
counter = 10 # 全局变量

def increment_counter_wrong():
    # 尝试在函数内部读取全局 counter 的值并进行计算,
    # 然后将结果赋值给 counter。
    # 但因为 Python 在函数内部看到赋值操作 `=`,它会认为你要创建一个新的局部变量 counter
    # 在赋值之前尝试读取 counter 的值,局部 counter 尚未定义,因此会引发 UnboundLocalError
    # 或者如果先执行了 counter = 0 这样的简单赋值,就只会修改局部 counter 的值
    # print(f"函数内部读取前: {counter}") # 如果之前没有赋值,会报错
    # counter = counter + 1 # 如果上面没有 simple assignment,这行会报错
    counter = 0 # 例如,先进行简单赋值
    counter = counter + 1 # 此时操作的是局部 counter

    print(f"函数内部(错误修改): counter = {counter}")

print(f"修改前全局: counter = {counter}")
increment_counter_wrong()
print(f"修改后全局: counter = {counter}") # 全局变量没有改变

输出:

修改前全局: counter = 10
函数内部(错误修改): counter = 1
修改后全局: counter = 10

或者如果你直接写 counter = counter + 1 没有前面的简单赋值,输出会是:

修改前全局: counter = 10
Traceback (most recent call last):
  ...
UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value

这是因为 Python 知道函数内部有一个名为 counter 的局部变量(因为它有赋值操作),但在赋值之前尝试使用它,所以报错。

3. 修改全局变量 (正确演示):

Python

# 3. 修改全局变量 (正确演示):
counter = 10 # 全局变量

def increment_counter_correct():
    global counter # 声明 counter 是全局变量
    counter = counter + 1 # 现在操作的是全局变量
    print(f"函数内部(正确修改): counter = {counter}")

print(f"修改前全局: counter = {counter}")
increment_counter_correct()
print(f"修改后全局: counter = {counter}") # 全局变量被修改
increment_counter_correct()
print(f"再次修改后全局: counter = {counter}")

输出:

修改前全局: counter = 10
函数内部(正确修改): counter = 11
修改后全局: counter = 11
函数内部(正确修改): counter = 12
再次修改后全局: counter = 12

4. 匿名函数 (lambda):

Python

# 4. 匿名函数 (lambda):
# 计算一个数字的立方
cube = lambda x: x ** 3
print(f"5 的立方是: {cube(5)}")

# 拼接两个字符串
concatenate_strings = lambda s1, s2: s1 + s2
print(f"拼接 'Hello' 和 'World': {concatenate_strings('Hello', 'World')}")

5. lambda 与内置函数:

Python

# 5. lambda 与内置函数:
students = [
    {'name': 'Alice', 'score': 90},
    {'name': 'Bob', 'score': 75},
    {'name': 'Charlie', 'score': 92},
    {'name': 'David', 'score': 88},
    {'name': 'Eve', 'score': 60}
]
print("原始学生列表:", students)

# 使用 sorted() 和 lambda 按 'score' 升序排序
sorted_students_by_score = sorted(students, key=lambda student: student['score'])
print("按分数升序排序:", sorted_students_by_score)

# 使用 filter() 和 lambda 筛选分数 >= 80 的学生
passed_students = list(filter(lambda student: student['score'] >= 80, students))
print("分数 >= 80 的学生:", passed_students)

6. 综合 (作用域与 lambda):

Python

# 6. 综合 (作用域与 lambda):
multiplier = 2 # 全局变量

def apply_operation(numbers, operation):
    """对数字列表中的每个数字应用给定的操作函数。

    Args:
        numbers: 一个数字列表。
        operation: 一个接受一个数字作为参数的函数。

    Returns:
        应用操作后得到的新列表。
    """
    result_list = []
    for num in numbers:
        processed_num = operation(num) # 调用传入的操作函数
        result_list.append(processed_num)
    return result_list

my_numbers = [1, 2, 3, 4, 5]

# 使用 lambda 作为 operation 参数,实现乘以全局变量 multiplier
# 注意 lambda 可以在其定义时“看到”外层作用域(这里是全局作用域)的变量 multiplier
multiply_by_global = lambda x: x * multiplier

processed_numbers = apply_operation(my_numbers, multiply_by_global)
print(f"原始列表: {my_numbers}")
print(f"乘以全局变量 {multiplier} 后: {processed_numbers}")

# 尝试修改全局变量 multiplier,再次调用函数
multiplier = 3
processed_numbers_again = apply_operation(my_numbers, multiply_by_global)
print(f"全局变量改为 {multiplier} 后再次调用: {processed_numbers_again}") # lambda 捕获的是定义时的 multiplier,还是调用时的 multiplier?

# lambda 捕获的是其定义时的外层作用域变量的“引用”,而不是值。
# 在这个例子中,lambda 内部的 multiplier 指向全局 multiplier 对象。
# 当全局 multiplier 的值改变时,lambda 内部访问的 multiplier 的值也会随之改变。

输出:

原始列表: [1, 2, 3, 4, 5]
乘以全局变量 2 后: [2, 4, 6, 8, 10]
全局变量改为 3 后再次调用: [3, 6, 9, 12, 15]

这个例子也稍微涉及了“闭包”的概念(lambda 函数记住了其定义时的外层作用域变量),这通常是进阶话题,但在这里用于演示 lambda 和作用域的交互是很合适的。

希望这些练习能帮助你更好地理解变量作用域和匿名函数的用法!它们是 Python 灵活性的重要组成部分。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值