简介
在这个实验中,你将深入了解 Python 中的闭包。闭包是一种强大的编程概念,它允许函数记住并访问其封闭作用域中的变量,即使外部函数已经执行完毕。
你还将把闭包理解为一种数据结构,探索它们作为代码生成器的用途,并了解如何使用闭包实现类型检查。这个实验将帮助你发现 Python 闭包中一些更不寻常且强大的特性。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在这个实验中,你将深入了解 Python 中的闭包。闭包是一种强大的编程概念,它允许函数记住并访问其封闭作用域中的变量,即使外部函数已经执行完毕。
你还将把闭包理解为一种数据结构,探索它们作为代码生成器的用途,并了解如何使用闭包实现类型检查。这个实验将帮助你发现 Python 闭包中一些更不寻常且强大的特性。
在 Python 中,闭包提供了一种强大的封装数据的方式。封装意味着将数据私有化并控制对其的访问。使用闭包,你可以创建管理和修改私有数据的函数,而无需使用类或全局变量。全局变量可以在代码的任何地方被访问和修改,这可能会导致意外的行为。而类则需要更复杂的结构。闭包为数据封装提供了一种更简单的替代方案。
让我们创建一个名为 counter.py
的文件来演示这个概念:
打开 WebIDE,在 /home/labex/project
目录下创建一个名为 counter.py
的新文件。我们将在这里编写定义基于闭包的计数器的代码。
在文件中添加以下代码:
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
。
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 开始的计数器,并将返回的函数解包到 up
和 down
中。接着我们多次调用 up()
函数来增加计数器的值并打印结果。之后,我们调用 down()
函数来减少计数器的值并打印结果。
python3 test_counter.py
你应该会看到以下输出:
Incrementing the counter:
1
2
3
Decrementing the counter:
2
1
注意这里没有涉及类的定义。up()
和 down()
函数正在操作一个共享的值,这个值既不是全局变量也不是实例属性。这个值存储在闭包中,只有 counter()
函数返回的函数才能访问它。
这是一个闭包如何用作数据结构的示例。封闭变量 value
在函数调用之间得以保留,并且对于访问它的函数来说是私有的。这意味着你的代码的其他部分无法直接访问或修改这个 value
变量,从而提供了一定程度的数据保护。
在这一步中,你将学习如何使用闭包动态生成代码。具体来说,你将使用闭包为类属性构建一个类型检查系统。
首先,让我们了解一下什么是闭包。闭包是一个函数对象,即使封闭作用域中的值不在内存中,它也能记住这些值。在 Python 中,当一个嵌套函数引用其封闭函数中的值时,就会创建闭包。
现在,你将开始实现类型检查系统。
/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
函数是一个闭包。它接受两个参数:name
和 expected_type
。@property
装饰器用于为属性创建一个 getter 方法,该方法用于获取私有属性的值。@value.setter
装饰器创建一个 setter 方法,用于检查设置的值是否为预期的类型。如果不是,则会引发 TypeError
。
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
函数为 name
、shares
和 price
创建类型检查属性。当你创建 Stock
类的实例时,类型检查将自动应用。
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
,因为预期的类型是整数。
python3 test_stock.py
你应该会看到类似于以下的输出:
Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>
这个输出表明类型检查正常工作。
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
函数的包装器,使创建常见类型的属性更加容易。
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
类使用便捷函数创建类型检查属性,使代码更具可读性。
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
类。
python3 test_stock_enhanced.py
你应该会看到类似于以下的输出:
Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>
在这一步中,你已经演示了如何使用闭包生成代码。typedproperty
函数创建执行类型检查的属性对象,而 String
、Integer
和 Float
函数为常见类型创建专门的属性。
在上一步中,当创建类型化属性时,你必须显式指定属性名。这是多余的,因为属性名已经在类定义中指定了。在这一步中,你将使用描述符(descriptor)来消除这种冗余。
在 Python 中,描述符是一种特殊的对象,它控制属性访问的方式。当你在描述符中实现 __set_name__
方法时,它可以自动从类定义中获取属性名。
让我们从创建一个新文件开始。
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__
方法会自动调用。这使得描述符可以自动捕获属性名,而无需手动指定。
接下来,你将创建一个使用这些改进后的类型化属性的类。
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
注意,在创建类型化属性时,你不再需要指定属性名。描述符会自动从类定义中获取属性名。
现在,让我们测试改进后的类。
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}")
最后,你将运行测试,看看一切是否按预期工作。
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 代码的工具。