02-8.python入门基础一函数的高级使用
四、Python 函数的作用域
(一)局部作用域
在 Python 中,局部作用域指的是函数内定义变量的区域。这些在函数内部定义的变量被称为局部变量,它们有着明确的使用范围限制,那就是只能在定义它们的函数内部进行访问和修改。
例如下面这段代码:
def local_scope_example(): x = 10 # 这里的x就是局部变量 print(x) local_scope_example() # 尝试在函数外部访问局部变量x,下面这行代码会报错 # print(x) |
在 local_scope_example 函数中,我们定义了变量 x,在函数内部可以正常打印它的值。但如果我们在函数外部尝试去访问这个变量 x,Python 解释器就会提示错误,因为局部变量的生命周期是和函数的执行过程绑定在一起的。当函数开始执行时,局部变量被创建,一旦函数执行完毕,这些局部变量就会被销毁(除非它们作为返回值被返回出去,从而可以在函数外部继续使用)。
再看另一个例子:
def add_numbers(a, b): result = a + b # result是局部变量 return result sum_result = add_numbers(3, 5) # 这里不能直接访问函数内部的result变量,只能通过函数返回值来获取它的值 # print(result) 这行代码同样会报错 print(sum_result) |
在 add_numbers 函数里,result 变量用于存储两数相加的结果,它是局部变量,我们无法在函数外部直接访问它,只能通过函数的返回值机制将其传递到函数外部使用,像代码中通过 sum_result 接收 add_numbers 函数的返回值,进而可以在函数外部对结果进行后续操作。
(二)嵌套作用域
嵌套作用域主要存在于嵌套函数的情况中,也就是在一个函数内部又定义了另一个函数。在这种结构下,内部函数可以访问外部函数的局部变量,不过要注意这是一种单向的访问关系。
例如:
def outer_function(): outer_variable = 20 def inner_function(): print(outer_variable) # 内部函数可以访问外部函数的局部变量 inner_function() outer_function() |
在上述代码中,outer_function 里定义了变量 outer_variable,内部的 inner_function 能够访问并打印这个变量的值。
然而,如果想要在内部函数中修改外层函数的非全局作用域变量(也就是外部函数的局部变量),就需要使用 nonlocal 关键字了。比如下面的代码:
def outer_func(): num = 10 def inner_func(): nonlocal num # 使用nonlocal声明,表明要修改的是外部函数的num变量 num += 5 print(num) inner_func() print(num) # 可以看到外部函数中的num变量已经被修改了 outer_func() |
在这个例子里,inner_func 函数通过 nonlocal 关键字声明了要修改外部函数 outer_func 中的 num 变量,执行代码后可以发现,在内部函数修改了这个变量的值后,外部函数中的该变量也相应地发生了改变,体现了嵌套作用域下变量访问和修改的规则特点。
(三)全局作用域
全局作用域包含的是模块级别定义的变量,这些变量在整个模块内的所有函数中都可以被访问到。
例如:
global_variable = 50 # 这是一个全局变量 def access_global_variable(): print(global_variable) access_global_variable() print(global_variable) |
在这段代码中,global_variable 是在函数外部定义的全局变量,无论是在 access_global_variable 函数内部,还是在函数外部,都可以直接访问它并获取其值。
但如果要在函数内部对全局变量进行修改,那就需要使用 global 关键字进行声明了,不然 Python 解释器会认为你在函数内部定义了一个新的局部变量,而不是去修改那个全局变量。看下面的示例:
counter = 0 # 全局变量 def increment_counter(): global counter # 声明使用全局变量 counter += 1 increment_counter() print(counter) |
这里通过 global 关键字声明后,increment_counter 函数内部对 counter 的操作就是针对那个全局变量进行的了,每次调用这个函数,counter 的值就会增加 1。
不过需要提醒的是,过多地使用全局变量并且在函数中随意修改它们,会对代码的封装性和可维护性产生不良影响。因为全局变量可以在很多地方被访问和修改,这样代码的逻辑流向就会变得复杂,不利于后续的调试以及他人对代码的理解,所以在实际编程中,应该谨慎使用全局变量,尽量通过函数的参数传递和返回值机制来进行数据的交互。
(四)内置作用域
内置作用域包含了 Python 的内置函数(比如 print()、len() 等)和异常等内容。它在 Python 启动时就被创建,并且会一直存在,直到 Python 解释器退出。
内置作用域里的这些名字在程序的任何地方都是可见的,也就是说,我们在编写 Python 代码时,无论在哪个模块、哪个函数里,都可以直接调用这些内置函数或者使用内置的异常类型等,而不需要额外进行导入等操作。
例如:
print(len([1, 2, 3])) # 直接使用内置的len函数来获取列表长度 |
不过通常情况下,内置作用域里的这些元素不由程序员直接去定义或者修改,它们是 Python 语言本身提供的基础功能支撑,对 Python 程序的正常执行起着至关重要的作用,确保了我们能够方便快捷地使用各种常见的操作和处理各种异常情况。
五、Python 函数的高级特性
(一)高阶函数
1. 函数作为参数传递
在 Python 中,高阶函数是一个很强大且有趣的概念,它允许函数作为参数传递给另一个函数,这种特性极大地提高了代码的灵活性与复用性。
例如,我们来看一个简单的高阶函数示例:
def func1(x, y, f): return f(x) + f(y) num = func1(-10, 2, abs) print(num) |
在上述代码中,func1 就是一个高阶函数,它接收三个参数,其中 f 就是作为参数传入的函数。在这里我们传入了内置的 abs 函数(求绝对值函数),func1 函数内部会先通过 f(x) 和 f(y) 分别对 x 和 y 应用传入的函数 f (也就是求它们的绝对值),然后再将这两个结果相加并返回。需要注意的是,把函数作为参数传入的时候,不要再传入函数后面加括号(比如这里不能写成 abs(),因为 abs() 表示调用这个函数获取它的返回值,而我们是要把函数本身当作参数传递进去)。
Python 中还有一些常用的内置高阶函数,像 map、filter、sorted、reduce 等,下面来具体介绍一下它们的用法。
map 函数一般来说接受两个参数,第一个参数是要作用的函数,第二个参数是要作用的可迭代对象,它返回值是一个迭代器。例如:
lst = [1, 2, 3, 4, 5, 6, 7] lst2 = [10, 100, 1000, 10000] def f1(x, y): return x + y print(list(map(f1, lst, lst2))) print(list(map(lambda x, y: x + y, lst, lst2))) |
在这个例子中,map 后面可以接受多个可迭代对象(这里传入了两个列表 lst 和 lst2),那传入几个可迭代对象,前面的函数就要接受几个参数。代码中先是定义了 f1 函数用于将两个参数相加,然后通过 map 函数将 f1 作用到 lst 和 lst2 的对应元素上,最后将得到的迭代器转换为列表输出(也可以直接用 lambda 匿名函数来简化写法)。
再来看 filter 函数,它的第一个参数传入一个函数,第二个参数是可迭代对象,会将可迭代对象里的每一个值,交给传入的函数处理,如果结果为真,就保留这个值,如果结果为假,就去掉这个值,同样返回一个迭代器。例如:
# 去掉偶数,保留奇数[1,2,3,4,5,6,7,8,9] print(list(filter(lambda x: x % 2, [1, 2, 3, 4, 5, 6, 7, 8, 9]))) |
在这个示例里,通过 lambda 表达式定义了一个判断是否为奇数的函数,传递给 filter 函数后,它就会遍历后面的列表,把满足是奇数(即 x % 2 结果为真)的元素保留下来,最终返回一个包含奇数的列表迭代器,我们再将其转换为列表进行输出查看。
还有 sorted 函数,它也是一个高阶函数,还可以接收一个 key 函数来实现自定义的排序。key 指定的函数将作用于列表的每一个元素上,并根据 key 函数返回的结果进行排序。比如:
# 把一个序列中的字符串,忽略大小写排序 list1 = ['bob', 'about', 'Zoo', 'Credit'] print(sorted(list1, key=lambda x: x.lower())) print(sorted(list1, key=str.lower)) |
在这段代码中,通过传入 lambda 函数(也可以直接使用内置的 str.lower 函数)作为 key 参数,使得 sorted 函数在排序时按照字符串的小写形式来比较大小,从而实现忽略大小写的排序效果。
最后说说 reduce 函数(使用时需要从 functools 模块导入),它必须接收两个参数,reduce 把结果继续和序列的下一个元素做累积计算。例如:
from functools import reduce s = [1, 3, 5, 7, 9] print(reduce(lambda x, y: x * 10 + y, s)) |
上述代码中,reduce 函数利用 lambda 表达式实现了将列表中的数字组合成一个整数的功能,它会依次取出列表中的元素进行累积计算,最终得到一个合并后的整数值。
通过这些高阶函数的示例,可以看出函数作为参数传递在 Python 编程中有着广泛且实用的应用场景,能够帮助我们更高效地处理各种数据和实现复杂的逻辑。
2. 函数作为返回值
除了可以把函数作为参数传递外,Python 中的函数还可以返回另一个函数,这也是高阶函数的一种表现形式。当一个函数返回另一个函数时,往往会涉及到闭包(Closure)的概念。
例如,我们来实现一个可变参数的求和函数,不过不是立刻返回求和的结果,而是返回求和的函数:
def lazy_sum(*args): def sum(): ax = 0 for n in args: ax = ax + n return ax return sum |
在上述代码中,lazy_sum 函数内部又定义了 sum 函数,并且内部函数 sum 可以引用外部函数 lazy_sum 的参数 args,当 lazy_sum 返回函数 sum 时,相关参数 args 就保存在返回的函数中了,这就是所谓的 “闭包” 结构。我们调用 lazy_sum 函数时,返回的并不是求和结果,而是求和函数:
f = lazy_sum(1, 3, 5, 7, 9) print(f) print(f()) |
可以看到,第一次打印 f 时,输出的是函数对象本身,只有当再次调用 f() 时,才真正计算求和的结果并返回。
需要注意的是,返回的函数引用了外部函数的局部变量,所以当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,使用时要格外留意。例如下面这种情况:
def count(): fs = [] for i in range(1, 4): def f(): return i * i fs.append(f) return fs f1, f2, f3 = count() print(f1()) print(f2()) print(f3()) |
你可能预想的结果是 1、4、9,但实际结果却是 9、9、9。原因就在于返回的函数引用了变量 i,但它并非立刻执行,等到三个函数都返回时,它们所引用的变量 i 已经变成了 3,因此最终结果都是 3 * 3 = 9。所以返回闭包时牢记的一点就是:返回函数不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用循环变量,可以再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变,像下面这样修改代码:
def count(): def f(j): def g(): return j * j return g fs = [] for i in range(1, 4): fs.append(f(i)) return fs f1, f2, f3 = count() print(f1()) print(f2()) print(f3()) |
这样就能得到正确的结果 1、4、9 了。总之,函数作为返回值的情况虽然很强大,但在使用过程中要清楚闭包的特性以及变量引用的相关注意事项,避免出现不符合预期的结果。
(二)装饰器
装饰器是 Python 中用于在不修改原函数代码的情况下给函数添加新功能的一种函数,它本质上是接收一个函数作为参数并返回新函数的特殊函数。通过使用装饰器,可以将一些与函数核心功能无关的通用逻辑(比如日志记录、性能测试、权限校验等)提取出来,让代码更加简洁、易维护且符合开闭原则(对扩展开放,对修改关闭)。
下面来看一个简单的装饰器示例:
import time def count_time(func): def wrapper(): t1 = time.time() func() t2 = time.time() print('运行时间为:{:.5} s'.format(t2 - t1)) return wrapper @count_time def my_python(): time.sleep(1) print('我的Python教程,微信公众号:wdPython') my_python() |
在这个例子中,count_time 就是一个装饰器函数,它接受一个函数 func 作为参数,并在内部定义了 wrapper 函数。wrapper 函数会在调用原函数 func 之前记录开始时间,调用之后记录结束时间,然后计算并输出函数的运行时间,最后返回原函数的执行结果(这里原函数 my_python 没有返回值,只是简单打印一句话并睡眠一秒模拟耗时操作)。通过 @count_time 这种语法糖的形式,就可以将 my_python 函数用 count_time 装饰器进行装饰,当调用 my_python 函数时,实际上执行的是 count_time 装饰器返回的 wrapper 函数,从而实现了在不改动 my_python 函数内部代码的前提下添加计算运行时间的新功能。
如果被装饰的函数带有参数,那装饰器函数内部的 wrapper 函数也要相应地接收这些参数,示例如下:
import time def count_time(func): def wrapper(*args, **kwargs): t1 = time.time() result = func(*args, **kwargs) t2 = time.time() print('运行时间为:{:.5} s'.format(t2 - t1)) return result return wrapper @count_time def my_python(times): time.sleep(1) print('我的Python教程,微信公众号:wdPython\n' * times) return 888888 constant = my_python(3) print(constant) |
这里的 my_python 函数带有参数 times,所以在 count_time 装饰器的 wrapper 函数中通过 *args 和 **kwargs 来接收任意数量的位置参数和关键字参数,然后再将这些参数传递给原函数 func 进行调用,这样就能正确处理带参数的函数被装饰的情况了。
此外,还可以有带参数的装饰器,也就是装饰器函数本身也接收参数,例如:
import time def my_decorator(name): def count_time(func): def wrapper(*args, **kwargs): t1 = time.time() func(*args, **kwargs) t2 = time.time() print(f'[{name}]执行时间为:', t2 - t1) return wrapper return count_time @my_decorator(name='李白') def libai(): time.sleep(1) print('我是李白') @my_decorator(name='杜甫') def dufu(): time.sleep(1) print('我是杜甫') libai() dufu() |
在上述代码中,my_decorator 函数外层接收一个参数 name,它返回的内部函数 count_time 才是真正的装饰器函数,这样就可以根据传入不同的参数来为不同的被装饰函数定制一些额外的行为(这里是根据 name 来输出不同的标识信息以及记录执行时间)。
装饰器在 Python 开发中应用非常广泛,能够帮助我们更优雅地扩展函数功能,提升代码的复用性和可维护性,是进阶 Python 编程需要掌握的重要特性之一。
(三)其他高级特性(如列表推导式、生成器等可选内容)
1. 列表推导式
列表推导式是 Python 中一种非常强大且简洁的特性,它可以将循环和条件判断等操作压缩成一行代码,快速地创建新的列表。
基本格式为:[out_exp_res for out_exp in input_list if out_exp == 2],其中 out_exp_res 是列表生成元素表达式,可以是有返回值的函数;for out_exp in input_list 表示迭代 input_list 将 out_exp 传入 out_exp_res 表达式中;if out_exp == 2 部分则是根据条件过滤哪些值可以用来生成列表元素(这里的条件 == 2 只是示例,可以根据实际需求修改)。
下面通过不同的实例来详细了解列表推导式的各种用法。
例如,过滤掉长度小于 3 的字符串列表,并将剩下的转换成大写字母:
names = ['Bob', 'Tom', 'alice', 'Jerry', 'Wendy', 'Smith'] print([name.upper() for name in names if len(name) > 3]) |
在这个例子中,对于 names 列表中的每个元素 name,首先通过 if len(name) > 3 条件进行筛选,只留下长度大于 3 的字符串,然后对这些符合条件的字符串调用 upper 方法将其转换为大写形式,最终生成一个新的符合要求的列表并输出,结果为 ['ALICE', 'JERRY', 'WENDY', 'SMITH']。
再看生成间隔 5 分钟的时间列表序列的例子:
print(["%02d:%02d" % (h, m) for h in range(0, 24) for m in range(0, 60, 5)]) |
这里通过两层循环,外层循环遍历小时数(range(0, 24)),内层循环遍历分钟数(每隔 5 分钟,即 range(0, 60, 5)),然后利用字符串格式化将小时和分钟组合成时间格式的字符串,最终生成一个包含所有间隔 5 分钟时间的列表。
还有求 (x, y),其中 x 是 0 - 5 之间的偶数,y 是 0 - 5 之间的奇数组成的元组列表的情况:
print([(x, y) for x in range(5) if x % 2 == 0 for y in range(5) if y % 2 == 1]) |
在这段代码中,先是通过外层循环结合条件 x % 2 == 0 筛选出 0 - 5 之间的偶数作为元组的第一个元素,然后内层循环结合条件 y % 2 == 1 筛选出 0 - 5 之间的奇数作为元组的第二个元素,每一对符合条件的 x 和 y 就组成一个元组放入最终的列表中,结果为 [(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)]。
另外,对于多维列表也可以使用列表推导式来提取元素,比如有一个嵌套列表 M = [[1, 2, 3], [4, 5, 6], [7, 8, 9]],要提取其中每个子列表的第三个元素组成新列表,可以这样写:
M = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] print([row[2] for row in M]) |
这里通过循环 M 中的每一个子列表 row,然后取出每个 row 的第三个元素(索引为 2),最终生成包含这些元素的新列表 [3, 6, 9]。
从这些示例可以看出,列表推导式能够以简洁的语法实现各种复杂的列表生成需求,让代码更加紧凑和高效,但也要注意合理使用,避免过于复杂的推导式导致代码可读性变差。
2. 生成器
生成器是 Python 中一种特殊的迭代器,它使用 yield 关键字来定义,具有懒加载数据、节省内存的特点,非常适合处理大量数据或者在需要逐个生成数据的场景中使用。
例如,下面是一个简单的生成器函数,用于生成从 1 到 n 的整数序列:
def generate_numbers(n): number = 1 while number <= n: yield number number += 1 numbers = generate_numbers(10) for number in numbers: print(number) |
在这个 generate_numbers 函数中,通过 while 循环结合 yield 语句来逐个生成整数。当函数执行到 yield 语句时,它会返回当前的 number 值,并暂停函数的执行,保存当前的执行状态(包括局部变量的值等)。下次再通过循环调用生成器(比如这里的 for 循环)时,函数会从上次暂停的位置继续执行,直到再次遇到 yield 语句或者循环结束。这样就实现了按需生成数据的效果,而不是像普通列表那样一次性把所有数据都生成并存储在内存中,如果要生成的序列很长,使用生成器就能避免占用过多内存。
除了使用 def 和 yield 关键字来创建