在编写 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
函数则为编写简洁的代码提供了便利。
练习题
尝试独立完成以下练习题,并通过答案进行对照:
-
局部 vs 全局变量:
- 定义一个全局变量
message = "Global message"
。 - 定义一个函数
print_message()
,在函数内部定义一个同名局部变量message = "Local message"
,然后在函数内部打印message
。 - 在函数外部打印全局
message
。观察两个输出。
- 定义一个全局变量
-
修改全局变量 (错误演示):
- 定义一个全局变量
counter = 10
。 - 定义一个函数
increment_counter_wrong()
,在函数内部尝试counter = counter + 1
。 - 在函数外部打印修改前的
counter
,调用函数,再打印修改后的counter
。观察结果和可能出现的错误。
- 定义一个全局变量
-
修改全局变量 (正确演示):
- 再次定义全局变量
counter = 10
。 - 定义一个函数
increment_counter_correct()
,使用global
关键字正确修改全局counter
。 - 重复步骤 2 的打印操作,观察这次全局
counter
的变化。
- 再次定义全局变量
-
匿名函数 (lambda):
- 使用
lambda
创建一个函数,计算一个数字的立方。 - 使用
lambda
创建一个函数,拼接两个字符串。 - 调用并测试这两个 lambda 函数。
- 使用
-
lambda
与内置函数:- 创建一个包含字典的列表,每个字典有两个键:'name' 和 'score'。
- 使用
sorted()
和lambda
对这个列表按照 'score' 进行升序排序,打印结果。 - 使用
filter()
和lambda
筛选出分数大于等于 80 的学生字典,打印结果。
-
综合 (作用域与 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 灵活性的重要组成部分。