目录
- 创建 Python 内部函数
- 使用内部函数:基础知识
- 提供封装
- 构建助手内部函数
- 使用内部辅助函数与私有辅助函数
- 使用内部函数保留状态:闭包
- 在闭包中保留状态
- 修改关闭状态
- 使用内部函数添加行为:装饰器
- 结论
一、说明
内部函数,也称为嵌套函数,是在其他函数内部定义的函数。在 Python 中,这种函数可以直接访问封闭函数中定义的变量和名称。内部函数有很多用途,最显着的是作为闭包工厂和装饰器函数。
在本教程中,您将学习如何:
- 提供封装并隐藏您的函数以防止外部访问
- 编写辅助函数以促进代码重用
- 创建在调用之间保留状态的闭包工厂函数
- 代码装饰器函数以向现有函数添加行为
免费奖励: 单击此处获取 Python 备忘单并学习 Python 3 的基础知识,例如使用数据类型、字典、列表和 Python 函数。
二、创建 Python 内部函数
在另一个函数内部定义的函数称为内部函数或嵌套函数。在Python中,这种函数可以访问封闭函数中的名称。以下是如何在 Python 中创建内部函数的示例:
>>> def outer_func():
... def inner_func():
... print("Hello, World!")
... inner_func()
...
>>> outer_func()
Hello, World!
在此代码中,您inner_func()
在内部定义outer_func()
将消息打印Hello, World!
到屏幕上。为此,您需要调用 的inner_func()
最后一行outer_func()
。这是在 Python 中编写内部函数的最快方法。然而,内部函数提供了许多有趣的可能性,超出了您在本示例中看到的范围。
内部函数的核心特征是它们能够从其封闭函数访问变量和对象,即使该函数返回后也是如此。封闭函数提供了内部函数可访问的命名空间:
>>> def outer_func(who):
... def inner_func():
... print(f"Hello, {who}")
... inner_func()
...
>>> outer_func("World!")
Hello, World!
现在您可以将字符串作为参数传递给outer_func()
,并inner_func()
通过名称访问该参数who
。然而,这个名称是在 的本地范围内定义的outer_func()
。在外部函数的局部作用域中定义的名称称为非局部名称。inner_func()
从角度来看,它们是非本地的。
以下是如何创建和使用更复杂的内部函数的示例:
>>> def factorial(number):
... # Validate input
... if not isinstance(number, int):
... raise TypeError("Sorry. 'number' must be an integer.")
... if number < 0:
... raise ValueError("Sorry. 'number' must be zero or positive.")
... # Calculate the factorial of number
... def inner_factorial(number):
... if number <= 1:
... return 1
... return number * inner_factorial(number - 1)
... return inner_factorial(number)
...
>>> factorial(4)
24
在 中factorial()
,您首先验证输入数据以确保您的用户提供的整数等于或大于零。然后定义一个名为 的递归内部函数,inner_factorial()
该函数执行阶乘计算并返回结果。最后一步是调用inner_factorial()
.
注意:有关递归和递归函数的更详细讨论,请查看Python 中的递归思考和Python 中的递归:简介。
使用此模式的主要优点是,通过在外部函数中执行所有参数检查,您可以安全地跳过内部函数中的错误检查并专注于手头的计算。
三、使用内部函数:基础知识
Python 内部函数的用例多种多样。您可以使用它们来提供封装并隐藏您的函数以防止外部访问,您可以编写辅助内部函数,还可以创建闭包和装饰器。在本节中,您将了解内部函数的前两个用例,在后面的部分中,您将学习如何创建闭包工厂函数和装饰器。
3.1 提供封装
当您需要保护或隐藏给定函数免受其外部发生的所有事情影响时,就会出现内部函数的常见用例,以便该函数完全隐藏在全局范围之外。这种行为通常称为封装。
下面的例子强调了这个概念:
>>> def increment(number):
... def inner_increment():
... return number + 1
... return inner_increment()
...
>>> increment(10)
11
>>> # Call inner_increment()
>>> inner_increment()
Traceback (most recent call last):
File "<input>", line 1, in <module>
inner_increment()
NameError: name 'inner_increment' is not defined
在本例中,您无法inner_increment()
直接访问。如果你尝试这样做,那么你会得到一个NameError
。那是因为increment()
完全隐藏inner_increment()
,阻止您从全局范围访问它。
3.2 构建助手内部函数
有时,您的函数会在其体内的多个位置执行相同的代码块。例如,假设您要编写一个函数来处理包含纽约市 Wi-Fi 热点信息的CSV 文件。要查找纽约的热点总数以及提供大部分热点的公司,您可以创建以下脚本:
# hotspots.py
import csv
from collections import Counter
def process_hotspots(file):
def most_common_provider(file_obj):
hotspots = []
with file_obj as csv_file:
content = csv.DictReader(csv_file)
for row in content:
hotspots.append(row["Provider"])
counter = Counter(hotspots)
print(
f"There are {len(hotspots)} Wi-Fi hotspots in NYC.\n"
f"{counter.most_common(1)[0][0]} has the most with "
f"{counter.most_common(1)[0][1]}."
)
if isinstance(file, str):
# Got a string-based filepath
file_obj = open(file, "r")
most_common_provider(file_obj)
else:
# Got a file object
most_common_provider(file)
这里,process_hotspots()
takefile
作为一个参数。该函数检查是否是物理文件或文件对象file
的基于字符串的路径。然后它调用辅助内部函数,该函数接受一个文件对象并执行以下操作:most_common_provider()
- 将文件内容读入生成器,该生成器使用csv.DictReader.
- 创建 Wi-Fi 提供商列表。
- 使用对象计算每个提供商的 Wi-Fi 热点数量collections.Counter。
- 打印一条包含检索到的信息的消息。
如果运行该函数,您将得到以下输出:
>>> from hotspots import process_hotspots
>>> file_obj = open("./NYC_Wi-Fi_Hotspot_Locations.csv", "r")
>>> process_hotspots(file_obj)
There are 3319 Wi-Fi hotspots in NYC.
LinkNYC - Citybridge has the most with 1868.
>>> process_hotspots("./NYC_Wi-Fi_Hotspot_Locations.csv")
There are 3319 Wi-Fi hotspots in NYC.
LinkNYC - Citybridge has the most with 1868.
无论您process_hotspots()
使用基于字符串的文件路径还是文件对象进行调用,您都会得到相同的结果。
3.3 使用内部辅助函数与私有辅助函数
通常,您会创建辅助内部函数,例如most_common_provider()
当您想要提供封装时。如果您认为除了包含函数之外不会在其他任何地方调用内部函数,您也可以创建内部函数。
尽管将辅助函数编写为内部函数可以达到预期的结果,但将它们提取为顶级函数可能会更好。_
在这种情况下,您可以在函数名称中使用前导下划线 ( ) 来指示它是当前模块或类的私有函数。这将允许您从当前模块或类中的任何其他位置访问辅助函数,并根据需要重用它们。
将内部函数提取到顶级私有函数中可以使您的代码更清晰、更具可读性。这种实践可以产生应用单一职责原则的函数。
四、使用内部函数保留状态:闭包
在Python中,函数是一等公民。这意味着它们与任何其他对象相同,例如数字、字符串、列表、元组、模块等。您可以动态创建或销毁它们,将它们存储在数据结构中,将它们作为参数传递给其他函数,将它们用作返回值,等等。
您还可以在 Python 中创建高阶函数。高阶函数是通过将其他函数作为参数、返回它们或两者兼而有之来对其他函数进行操作的函数。
到目前为止,您所看到的所有内部函数示例都是普通函数,只是碰巧嵌套在其他函数中。除非您需要向外界隐藏您的函数,否则没有特定理由嵌套它们。您可以将这些函数定义为私有顶级函数,这样就可以了。
在本节中,您将了解闭包工厂函数。闭包是动态创建的函数,由其他函数返回。它们的主要特点是,即使封闭函数已返回并完成执行,它们也可以完全访问在创建闭包的本地命名空间中定义的变量和名称。
在 Python 中,当您返回内部函数对象时,解释器会将该函数及其包含的环境或闭包打包在一起。函数对象保留在其包含范围内定义的所有变量和名称的快照。要定义闭包,您需要执行三个步骤:
- 创建一个内部函数。
- 来自封闭函数的引用变量。
- 返回内部函数。
有了这些基本知识,您就可以立即开始创建闭包并利用其主要功能:在函数调用之间保留状态。
4.1 在闭包中保留状态
闭包导致内部函数在调用时保留其环境的状态。闭包不是内部函数本身,而是内部函数及其封闭环境。闭包捕获包含函数中的局部变量和名称并保留它们。
考虑以下示例:
1# powers.py
2
3def generate_power(exponent):
4 def power(base):
5 return base ** exponent
6 return power
以下是该函数中发生的情况:
- 第 3 行创建
generate_power()
,这是一个闭包工厂函数。这意味着它每次被调用时都会创建一个新的闭包,然后将其返回给调用者。 - 第 4 行定义
power()
,它是一个内部函数,它接受单个参数 ,base
并返回表达式 的结果base ** exponent
。 - 第 6 行
power
作为函数对象返回,但不调用它。
power()
from的值从哪里获取exponent
?这就是闭包发挥作用的地方。在此示例中,从外部函数power()
获取 generate_power()
的值。以下是当你调用generate_power()
时 Python 所做的事情:exponent
- 定义 的一个新实例
power()
,它采用单个参数base
。 - 拍摄 的周围状态的快照
power()
,其中包括exponent
其当前值。 power()
连同其整个周围状态一起返回。
这样,当您调用power()
by 返回的实例时generate_power()
,您将看到该函数记住了 的值exponent
:
>>> from powers import generate_power
>>> raise_two = generate_power(2)
>>> raise_three = generate_power(3)
>>> raise_two(4)
16
>>> raise_two(5)
25
>>> raise_three(4)
64
>>> raise_three(5)
125
在这些例子中,raise_two()
记住了exponent=2
,并且raise_three()
记住了exponent=3
。请注意,两个闭包都会在调用之间记住它们各自的值exponent
。
现在考虑另一个例子:
>>> def has_permission(page):
... def permission(username):
... if username.lower() == "admin":
... return f"'{username}' has access to {page}."
... else:
... return f"'{username}' doesn't have access to {page}."
... return permission
...
>>> check_admin_page_permision = has_permission("Admin Page")
>>> check_admin_page_permision("admin")
"'admin' has access to Admin Page."
>>> check_admin_page_permision("john")
"'john' doesn't have access to Admin Page."
内部函数检查给定用户是否具有访问给定页面的正确权限。您可以快速修改此设置以获取会话中的用户,以检查他们是否具有访问特定路由的正确凭据。
"admin"
您可以查询SQL 数据库来检查权限,然后根据凭据是否正确返回正确的视图,而不是检查用户是否等于 。
您通常会创建不修改其封闭状态的闭包,或具有静态封闭状态的闭包,如上面的示例所示。但是,您还可以创建闭包,通过使用可变对象(例如字典、集合或列表)来修改其封闭状态。
假设您需要计算数据集的平均值。数据来自所分析参数的连续测量值流,并且您需要函数在调用之间保留先前的测量值。在这种情况下,您可以编写一个闭包工厂函数,如下所示:
>>> def mean():
... sample = []
... def inner_mean(number):
... sample.append(number)
... return sum(sample) / len(sample)
... return inner_mean
...
>>> sample_mean = mean()
>>> sample_mean(100)
100.0
>>> sample_mean(105)
102.5
>>> sample_mean(101)
102.0
>>> sample_mean(98)
101.0
分配给的闭包sample_mean
保留连续调用之间的状态sample
。即使您sample
在 in 中定义mean()
,它仍然在闭包中可用,因此您可以修改它。在这种情况下,sample
它起到一种动态封闭状态的作用。
4.2 修改关闭状态
通常,闭包变量对外界完全隐藏。但是,您可以为它们提供getter和setter内部函数:
>>> def make_point(x, y):
... def point():
... print(f"Point({x}, {y})")
... def get_x():
... return x
... def get_y():
... return y
... def set_x(value):
... nonlocal x
... x = value
... def set_y(value):
... nonlocal y
... y = value
... # Attach getters and setters
... point.get_x = get_x
... point.set_x = set_x
... point.get_y = get_y
... point.set_y = set_y
... return point
...
>>> point = make_point(1, 2)
>>> point.get_x()
1
>>> point.get_y()
2
>>> point()
Point(1, 2)
>>> point.set_x(42)
>>> point.set_y(7)
>>> point()
Point(42, 7)
这里,make_point()
返回一个代表point
对象的闭包。该对象附加了 getter 和 setter 函数。您可以使用这些函数来读取和写入变量x
和y
,它们在封闭范围中定义并随闭包一起提供。
尽管此函数创建的闭包可能比等效类运行得更快,但您需要注意此技术不提供主要功能,包括继承、属性、描述符以及类和静态方法。如果您想更深入地研究这项技术,请查看使用闭包和嵌套作用域模拟类的简单工具(Python Recipe)。
五、使用内部函数添加行为:装饰器
Python装饰器是内部函数(尤其是闭包)的另一个流行且方便的用例。装饰器是高阶函数,它接受一个可调用的(函数、方法、类)作为参数并返回另一个可调用的。
您可以使用装饰器函数动态地将职责添加到现有的可调用对象中,并透明地扩展其行为,而不会影响或修改原始可调用对象。
注意:有关 Python 可调用对象的更多详细信息,请查看Python 文档中的标准类型层次结构并向下滚动到“可调用类型”。
要创建装饰器,您只需要定义一个可调用对象(函数、方法或类),它接受函数对象作为参数,对其进行处理,然后返回具有添加行为的另一个函数对象。
一旦装饰器函数就位,您就可以将其应用到任何可调用对象。为此,您需要@
在装饰器名称前面使用 at 符号 ( ),然后将其单独放在装饰可调用之前的行上:
@decorator def decorated_func():
# Function body...
pass
此语法decorator()
自动将decorated_func()
其作为参数并在其主体中对其进行处理。此操作是以下赋值的简写:
decorated_func = decorator(decorated_func)
以下是如何构建装饰器函数以向现有函数添加新功能的示例:
>>> def add_messages(func):
... def _add_messages():
... print("This is my first decorator")
... func()
... print("Bye!")
... return _add_messages
...
>>> @add_messages
... def greet():
... print("Hello, World!")
...
>>> greet()
This is my first decorator
Hello, World!
Bye!
在这种情况下,您可以使用@add_messages
来装饰greet()
. 这为装饰函数添加了新功能。现在,当您调用 时,您的函数将打印两条新消息,而greet()
不仅仅是打印。Hello, World!
Python 装饰器的用例多种多样。这里是其中的一些:
- 调试
- 缓存
- 记录
- 定时
调试 Python 代码的常见做法是插入调用来print()
检查变量的值、确认代码块已执行等。添加和删除呼叫可能print()
会很烦人,并且您可能会忘记其中一些呼叫。为了防止这种情况,你可以编写一个这样的装饰器:
>>> def debug(func):
... def _debug(*args, **kwargs):
... result = func(*args, **kwargs)
... print(
... f"{func.__name__}(args: {args}, kwargs: {kwargs}) -> {result}"
... )
... return result
... return _debug
...
>>> @debug
... def add(a, b):
... return a + b
...
>>> add(5, 6)
add(args: (5, 6), kwargs: {}) -> 11
11
此示例提供了debug()
,它是一个装饰器,它将函数作为参数,并使用每个参数的当前值及其相应的返回值打印其签名。您可以使用此装饰器来调试您的函数。一旦获得所需的结果,您可以删除装饰器调用@debug
,您的函数将为下一步做好准备。
注意:如果您有兴趣深入了解 Python 的工作方式*args
和**kwargs
工作方式,请查看Python args 和 kwargs:揭秘。
这是如何创建装饰器的最后一个示例。这次,您将重新实现generate_power()
为装饰器函数:
>>> def generate_power(exponent):
... def power(func):
... def inner_power(*args):
... base = func(*args)
... return base ** exponent
... return inner_power
... return power
...
>>> @generate_power(2)
... def raise_two(n):
... return n
...
>>> raise_two(7)
49
>>> @generate_power(3)
... def raise_three(n):
... return n
...
>>> raise_three(5)
125
此版本generate_power()
产生的结果与原始实现中获得的结果相同。在这种情况下,您可以使用要记住的闭包exponent
和返回输入函数 的修改版本的装饰器func()
。
这里,装饰器需要接受一个参数(exponent
),所以你需要有两层嵌套的内部函数。第一层由 表示power()
,它将修饰函数作为参数。第二层用 表示,将中的inner_power()
参数打包,最后计算幂并返回结果。exponent
args
六、结论
如果您在另一个函数内定义一个函数,那么您将创建一个内部函数,也称为嵌套函数。在 Python 中,内部函数可以直接访问您在封闭函数中定义的变量和名称。这为您提供了一种创建辅助函数、闭包和装饰器的机制。
在本教程中,您学习了如何:
- 通过在其他函数中嵌套函数来提供封装
- 编写辅助函数以重用代码片段
- 实现在调用之间保留状态的闭包工厂函数
- 构建装饰器函数以提供新功能
您现在已准备好在自己的代码中利用内部函数的多种用途。如果您有任何问题或意见,请务必在下面的评论部分中分享。