MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Python函数的作用域与闭包

2022-08-131.4k 阅读

一、Python函数的作用域

在Python编程中,作用域是指程序中定义的变量所存在的区域,在这个区域内变量是可见的,并且可以被访问。作用域的概念对于理解程序如何查找和使用变量至关重要,尤其是在复杂的代码结构中。

1.1 局部作用域(Local Scope)

局部作用域是指在函数内部定义的变量的作用域。在函数内部定义的变量,只能在该函数内部被访问和修改,函数外部无法直接访问这些变量。这有助于将函数内部的实现细节封装起来,避免与外部代码产生不必要的冲突。

def local_scope_example():
    local_variable = 10  # local_variable 是局部变量,作用域仅限于此函数内部
    print(local_variable)


local_scope_example()
# print(local_variable)  # 这行代码会报错,因为 local_variable 在函数外部不可见

在上述代码中,local_variable 是在 local_scope_example 函数内部定义的局部变量。当我们在函数内部打印它时,一切正常。但如果在函数外部尝试打印 local_variable,Python会抛出 NameError,提示该变量未定义。这清楚地表明了局部变量的作用域限制。

1.2 全局作用域(Global Scope)

全局作用域是指在模块(即整个Python文件)顶层定义的变量的作用域。全局变量在整个模块内的任何函数外部都可以被访问和修改。不过,在函数内部访问全局变量时需要注意一些规则。

global_variable = 20  # global_variable 是全局变量


def access_global_variable():
    print(global_variable)


access_global_variable()
print(global_variable)

在这个例子中,global_variable 是在模块顶层定义的全局变量。access_global_variable 函数可以顺利打印该全局变量的值,在函数外部也可以正常访问和打印它。

然而,如果在函数内部想要修改全局变量,就需要使用 global 关键字进行声明。

global_variable = 20


def modify_global_variable():
    global global_variable
    global_variable = 30
    print(global_variable)


modify_global_variable()
print(global_variable)

modify_global_variable 函数中,使用 global 关键字声明 global_variable 是全局变量,这样才能在函数内部对其进行修改。否则,Python会认为在函数内部创建了一个新的局部变量,而不是修改全局变量。

1.3 嵌套作用域(Enclosing Scope)

嵌套作用域出现在函数嵌套的情况下。当一个函数定义在另一个函数内部时,内部函数可以访问外部函数的变量,这些变量的作用域就构成了嵌套作用域。

def outer_function():
    outer_variable = 40

    def inner_function():
        print(outer_variable)

    inner_function()


outer_function()

outer_function 内部定义了 inner_functioninner_function 可以访问 outer_function 中的 outer_variable。这里 outer_variable 处于嵌套作用域中,对于 inner_function 来说是可见的。

1.4 内置作用域(Built - in Scope)

内置作用域包含了Python解释器内置的变量和函数,例如 printlenint 等。这些内置的标识符在整个程序中都可以直接使用,无需额外的导入或声明。

print(len([1, 2, 3]))

在上述代码中,len 是Python内置作用域中的函数,我们可以直接在代码中使用它来获取列表的长度。

1.5 作用域查找规则(LEGB规则)

Python在查找变量时遵循LEGB规则,即Local(局部作用域)、Enclosing(嵌套作用域)、Global(全局作用域)、Built - in(内置作用域)。当Python遇到一个变量引用时,它会按照这个顺序依次查找变量:

  1. 首先在局部作用域中查找,如果找到则使用该变量。
  2. 如果在局部作用域中未找到,则在嵌套作用域中查找。
  3. 如果嵌套作用域中也未找到,则在全局作用域中查找。
  4. 最后,如果全局作用域中也未找到,则在内置作用域中查找。如果在内置作用域中也未找到,就会抛出 NameError
builtin_variable = 'built - in'  # 模拟内置作用域变量
global_variable = 'global'


def outer():
    enclosing_variable = 'enclosing'

    def inner():
        local_variable = 'local'
        print(local_variable)
        print(enclosing_variable)
        print(global_variable)
        print(builtin_variable)


    inner()


outer()

inner 函数中,依次打印了局部变量 local_variable、嵌套作用域变量 enclosing_variable、全局变量 global_variable 和模拟的内置作用域变量 builtin_variable,演示了LEGB规则的查找顺序。

二、Python中的闭包

闭包(Closure)是Python中一个强大而有趣的概念,它基于函数的作用域特性。

2.1 闭包的定义

闭包是指一个函数对象,它记住了其定义时的环境,即使在该环境已经不存在的情况下,仍然可以访问和操作那些环境中的变量。简单来说,当一个嵌套函数在其外部函数返回后,仍然可以访问外部函数的局部变量,就形成了闭包。

def outer_function():
    outer_variable = 10

    def inner_function():
        return outer_variable

    return inner_function


closure = outer_function()
print(closure())

在上述代码中,outer_function 返回了 inner_function。当 outer_function 执行完毕,其局部作用域通常应该消失,但 inner_function 形成了闭包,它记住了 outer_variable。所以当我们调用 closure()(即 inner_function)时,仍然可以访问并返回 outer_variable 的值。

2.2 闭包的实际应用场景

  • 延迟计算:闭包可以用于实现延迟计算,将一些计算操作推迟到需要的时候执行。
def multiplier(factor):
    def multiply_by_factor(number):
        return number * factor

    return multiply_by_factor


double = multiplier(2)
triple = multiplier(3)
print(double(5))  # 延迟计算,5 * 2
print(triple(5))  # 延迟计算,5 * 3

在这个例子中,multiplier 函数返回一个闭包 multiply_by_factor。我们可以先创建 doubletriple 这样的闭包,然后在需要的时候传入参数进行计算,实现了延迟计算的功能。

  • 数据封装和隐藏:闭包可以将一些数据和操作封装起来,只暴露必要的接口,类似于面向对象编程中的封装概念。
def counter():
    count = 0

    def increment():
        nonlocal count
        count = count + 1
        return count

    return increment


my_counter = counter()
print(my_counter())
print(my_counter())

counter 函数中,count 变量被封装在闭包 increment 内部,外部无法直接访问和修改 count。只能通过调用 increment 函数来增加 count 的值,实现了数据的封装和隐藏。

2.3 闭包与变量作用域的关系

闭包能够记住并访问外部函数的局部变量,这与Python的作用域规则密切相关。在闭包形成时,它会将外部函数的相关变量绑定到自己的环境中。

def outer():
    x = 10

    def inner():
        print(x)

    return inner


closure = outer()
x = 20  # 这里修改全局变量 x
print(closure())

在上述代码中,虽然在 outer 函数返回后,我们在外部修改了全局变量 x,但闭包 closure 打印的仍然是 outer 函数内部定义的 x 的值(即10)。这表明闭包记住的是其定义时外部函数作用域中的变量状态,而不是全局变量的最新状态。

2.4 nonlocal关键字

在闭包中,如果想要修改外部函数(非全局)的变量,就需要使用 nonlocal 关键字。在Python 3之前,这是一个比较棘手的问题,因为直接对外部函数变量赋值会创建一个新的局部变量。有了 nonlocal 关键字,我们可以明确表示要修改的是外部函数的变量。

def outer():
    num = 5

    def inner():
        nonlocal num
        num = num + 1
        return num

    return inner


func = outer()
print(func())
print(func())

inner 函数中,使用 nonlocal 声明 num,这样在函数内部对 num 的修改就是对 outer 函数中 num 的修改,而不是创建新的局部变量。

三、闭包的注意事项

3.1 内存管理

闭包可能会导致内存问题,因为闭包会保存对外部函数变量的引用,即使外部函数已经执行完毕。如果闭包长时间存在且引用的变量占用大量内存,可能会导致内存泄漏。

def memory_leak_example():
    large_list = list(range(1000000))

    def inner():
        return sum(large_list)

    return inner


closure = memory_leak_example()
# 这里 closure 一直存在,并且引用着 large_list,导致 large_list 无法被垃圾回收

在上述代码中,closure 形成的闭包引用了 large_list,如果 closure 一直存在,large_list 就无法被垃圾回收,占用大量内存。

3.2 变量绑定时机

闭包中的变量绑定是在闭包定义时发生的,而不是在调用时。这可能会导致一些意想不到的结果,尤其是在循环中创建闭包的情况下。

functions = []
for i in range(3):
    def inner():
        return i

    functions.append(inner)

for func in functions:
    print(func())

在这个例子中,我们期望每个闭包 inner 打印出不同的 i 值(0、1、2),但实际上每个闭包打印的都是2。这是因为 i 的绑定是在闭包定义时,而循环结束后 i 的值为2,所有闭包都引用了这个最终的 i 值。要解决这个问题,可以使用默认参数,因为默认参数的绑定是在函数定义时。

functions = []
for i in range(3):
    def inner(x = i):
        return x

    functions.append(inner)

for func in functions:
    print(func())

通过将 i 作为默认参数,每个闭包在定义时就绑定了不同的 i 值,从而得到我们期望的结果。

3.3 与类的比较

闭包和类都可以用于封装数据和行为,但它们有不同的特点。闭包更轻量级,适合简单的封装和延迟计算场景,而类提供了更丰富的面向对象特性,如继承、多态等。在选择使用闭包还是类时,需要根据具体的需求来决定。

# 闭包实现计数器
def counter_closure():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment


closure_counter = counter_closure()
print(closure_counter())
print(closure_counter())


# 类实现计数器
class CounterClass:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count


class_counter = CounterClass()
print(class_counter.increment())
print(class_counter.increment())

在上述代码中,闭包和类都实现了一个简单的计数器功能。闭包的实现更简洁,而类的实现则更符合面向对象的编程风格,并且可以更容易地添加其他方法和属性。

四、总结

Python函数的作用域和闭包是两个紧密相关且非常重要的概念。作用域规定了变量的可见性和访问范围,通过LEGB规则,Python能够准确地查找变量。而闭包则是基于作用域特性,让函数在其定义环境消失后仍能访问外部函数的变量,实现延迟计算、数据封装等功能。在使用闭包时,需要注意内存管理、变量绑定时机等问题,同时要根据具体需求合理选择闭包或类来实现功能。深入理解作用域和闭包,将有助于编写更清晰、高效和健壮的Python代码。