深入了解闭包

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将深入了解 Python 中的闭包。闭包是一种强大的编程概念,它允许函数记住并访问其封闭作用域中的变量,即使外部函数已经执行完毕。

你还将把闭包理解为一种数据结构,探索它们作为代码生成器的用途,并了解如何使用闭包实现类型检查。这个实验将帮助你发现 Python 闭包中一些更不寻常且强大的特性。


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ErrorandExceptionHandlingGroup -.-> python/raising_exceptions("Raising Exceptions") subgraph Lab Skills python/conditional_statements -.-> lab-132506{{"深入了解闭包"}} python/function_definition -.-> lab-132506{{"深入了解闭包"}} python/arguments_return -.-> lab-132506{{"深入了解闭包"}} python/scope -.-> lab-132506{{"深入了解闭包"}} python/classes_objects -.-> lab-132506{{"深入了解闭包"}} python/raising_exceptions -.-> lab-132506{{"深入了解闭包"}} end

作为数据结构的闭包

在 Python 中,闭包提供了一种强大的封装数据的方式。封装意味着将数据私有化并控制对其的访问。使用闭包,你可以创建管理和修改私有数据的函数,而无需使用类或全局变量。全局变量可以在代码的任何地方被访问和修改,这可能会导致意外的行为。而类则需要更复杂的结构。闭包为数据封装提供了一种更简单的替代方案。

让我们创建一个名为 counter.py 的文件来演示这个概念:

  1. 打开 WebIDE,在 /home/labex/project 目录下创建一个名为 counter.py 的新文件。我们将在这里编写定义基于闭包的计数器的代码。

  2. 在文件中添加以下代码:

def counter(value):
    """
    Create a counter with increment and decrement functions.

    Args:
        value: Initial value of the counter

    Returns:
        Two functions: one to increment the counter, one to decrement it
    """
    def incr():
        nonlocal value
        value += 1
        return value

    def decr():
        nonlocal value
        value -= 1
        return value

    return incr, decr

在这段代码中,我们定义了一个名为 counter() 的函数。这个函数接受一个初始 value 作为参数。在 counter() 函数内部,我们定义了两个内部函数:incr()decr()。这些内部函数共享对同一个 value 变量的访问。nonlocal 关键字用于告诉 Python 我们想要修改封闭作用域(即 counter() 函数)中的 value 变量。如果没有 nonlocal 关键字,Python 会在内部函数中创建一个新的局部变量,而不是修改外部作用域中的 value

  1. 现在让我们创建一个测试文件来看看它的实际效果。创建一个名为 test_counter.py 的新文件,内容如下:
from counter import counter

## Create a counter starting at 0
up, down = counter(0)

## Increment the counter several times
print("Incrementing the counter:")
print(up())  ## Should print 1
print(up())  ## Should print 2
print(up())  ## Should print 3

## Decrement the counter
print("\nDecrementing the counter:")
print(down())  ## Should print 2
print(down())  ## Should print 1

在这个测试文件中,我们首先从 counter.py 文件中导入 counter() 函数。然后通过调用 counter(0) 创建一个从 0 开始的计数器,并将返回的函数解包到 updown 中。接着我们多次调用 up() 函数来增加计数器的值并打印结果。之后,我们调用 down() 函数来减少计数器的值并打印结果。

  1. 在终端中执行以下命令来运行测试文件:
python3 test_counter.py

你应该会看到以下输出:

Incrementing the counter:
1
2
3

Decrementing the counter:
2
1

注意这里没有涉及类的定义。up()down() 函数正在操作一个共享的值,这个值既不是全局变量也不是实例属性。这个值存储在闭包中,只有 counter() 函数返回的函数才能访问它。

这是一个闭包如何用作数据结构的示例。封闭变量 value 在函数调用之间得以保留,并且对于访问它的函数来说是私有的。这意味着你的代码的其他部分无法直接访问或修改这个 value 变量,从而提供了一定程度的数据保护。

作为代码生成器的闭包

在这一步中,你将学习如何使用闭包动态生成代码。具体来说,你将使用闭包为类属性构建一个类型检查系统。

首先,让我们了解一下什么是闭包。闭包是一个函数对象,即使封闭作用域中的值不在内存中,它也能记住这些值。在 Python 中,当一个嵌套函数引用其封闭函数中的值时,就会创建闭包。

现在,你将开始实现类型检查系统。

  1. /home/labex/project 目录下创建一个名为 typedproperty.py 的新文件,并添加以下代码:
## typedproperty.py

def typedproperty(name, expected_type):
    """
    Create a property with type checking.

    Args:
        name: The name of the property
        expected_type: The expected type of the property value

    Returns:
        A property object that performs type checking
    """
    private_name = '_' + name

    @property
    def value(self):
        return getattr(self, private_name)

    @value.setter
    def value(self, val):
        if not isinstance(val, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, val)

    return value

在这段代码中,typedproperty 函数是一个闭包。它接受两个参数:nameexpected_type@property 装饰器用于为属性创建一个 getter 方法,该方法用于获取私有属性的值。@value.setter 装饰器创建一个 setter 方法,用于检查设置的值是否为预期的类型。如果不是,则会引发 TypeError

  1. 现在,让我们创建一个使用这些类型化属性的类。创建一个名为 stock.py 的文件,并添加以下代码:
from typedproperty import typedproperty

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Stock 类中,你使用 typedproperty 函数为 namesharesprice 创建类型检查属性。当你创建 Stock 类的实例时,类型检查将自动应用。

  1. 让我们创建一个测试文件来查看实际效果。创建一个名为 test_stock.py 的文件,并添加以下代码:
from stock import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.shares = "hundred"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

在这个测试文件中,你首先创建一个具有正确类型的 Stock 对象。然后,你尝试将 shares 属性设置为字符串,这应该会引发 TypeError,因为预期的类型是整数。

  1. 运行测试文件:
python3 test_stock.py

你应该会看到类似于以下的输出:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>

这个输出表明类型检查正常工作。

  1. 现在,让我们通过为常见类型添加便捷函数来增强 typedproperty.py。在文件末尾添加以下代码:
def String(name):
    """Create a string property with type checking."""
    return typedproperty(name, str)

def Integer(name):
    """Create an integer property with type checking."""
    return typedproperty(name, int)

def Float(name):
    """Create a float property with type checking."""
    return typedproperty(name, float)

这些函数只是 typedproperty 函数的包装器,使创建常见类型的属性更加容易。

  1. 创建一个名为 stock_enhanced.py 的新文件,使用这些便捷函数:
from typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = String('name')
    shares = Integer('shares')
    price = Float('price')

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

这个 Stock 类使用便捷函数创建类型检查属性,使代码更具可读性。

  1. 创建一个测试文件 test_stock_enhanced.py 来测试增强版本:
from stock_enhanced import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.price = "490.1"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

这个测试文件与之前的类似,但它测试的是增强后的 Stock 类。

  1. 运行测试:
python3 test_stock_enhanced.py

你应该会看到类似于以下的输出:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>

在这一步中,你已经演示了如何使用闭包生成代码。typedproperty 函数创建执行类型检查的属性对象,而 StringIntegerFloat 函数为常见类型创建专门的属性。

✨ 查看解决方案并练习

使用描述符消除属性名冗余

在上一步中,当创建类型化属性时,你必须显式指定属性名。这是多余的,因为属性名已经在类定义中指定了。在这一步中,你将使用描述符(descriptor)来消除这种冗余。

在 Python 中,描述符是一种特殊的对象,它控制属性访问的方式。当你在描述符中实现 __set_name__ 方法时,它可以自动从类定义中获取属性名。

让我们从创建一个新文件开始。

  1. 创建一个名为 improved_typedproperty.py 的新文件,并添加以下代码:
## improved_typedproperty.py

class TypedProperty:
    """
    A descriptor that performs type checking.

    This descriptor automatically captures the attribute name from the class definition.
    """
    def __init__(self, expected_type):
        self.expected_type = expected_type
        self.name = None

    def __set_name__(self, owner, name):
        ## This method is called when the descriptor is assigned to a class attribute
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f'Expected {self.expected_type}')
        instance.__dict__[self.name] = value

## Convenience functions
def String():
    """Create a string property with type checking."""
    return TypedProperty(str)

def Integer():
    """Create an integer property with type checking."""
    return TypedProperty(int)

def Float():
    """Create a float property with type checking."""
    return TypedProperty(float)

这段代码定义了一个名为 TypedProperty 的描述符类,用于检查分配给属性的值的类型。当描述符被分配给类属性时,__set_name__ 方法会自动调用。这使得描述符可以自动捕获属性名,而无需手动指定。

接下来,你将创建一个使用这些改进后的类型化属性的类。

  1. 创建一个名为 stock_improved.py 的新文件,使用改进后的类型化属性:
from improved_typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    ## No need to specify property names anymore
    name = String()
    shares = Integer()
    price = Float()

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

注意,在创建类型化属性时,你不再需要指定属性名。描述符会自动从类定义中获取属性名。

现在,让我们测试改进后的类。

  1. 创建一个测试文件 test_stock_improved.py 来测试改进后的版本:
from stock_improved import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try setting attributes with wrong types
try:
    s.name = 123  ## Should raise TypeError
    print("Name type check failed")
except TypeError as e:
    print(f"Name type check succeeded: {e}")

try:
    s.shares = "hundred"  ## Should raise TypeError
    print("Shares type check failed")
except TypeError as e:
    print(f"Shares type check succeeded: {e}")

try:
    s.price = "490.1"  ## Should raise TypeError
    print("Price type check failed")
except TypeError as e:
    print(f"Price type check succeeded: {e}")

最后,你将运行测试,看看一切是否按预期工作。

  1. 运行测试:
python3 test_stock_improved.py

你应该会看到类似于以下的输出:

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Name type check succeeded: Expected <class 'str'>
Shares type check succeeded: Expected <class 'int'>
Price type check succeeded: Expected <class 'float'>

在这一步中,你通过使用描述符和 __set_name__ 方法改进了类型检查系统。这消除了多余的属性名指定,使代码更简洁,减少了出错的可能性。

__set_name__ 方法是描述符的一个非常有用的特性。它允许描述符自动收集它们在类定义中的使用信息。这可以用于创建更易于理解和使用的 API。

总结

在本次实验中,你学习了 Python 闭包的高级特性。首先,你探索了将闭包用作数据结构,它可以封装数据,并使函数在多次调用之间保持状态,而无需依赖类或全局变量。其次,你了解了闭包如何作为代码生成器,生成具有类型检查功能的属性对象,从而以更函数式的方式进行属性验证。

你还学会了如何使用描述符协议(descriptor protocol)和 __set_name__ 方法来创建优雅的类型检查属性,这些属性可以自动从类定义中捕获它们的名称。这些技术展示了闭包的强大功能和灵活性,使你能够简洁地实现复杂的行为。理解闭包和描述符将为你提供更多创建可维护且健壮的 Python 代码的工具。