Python 内部函数:它们有什么用?

目录

内部函数,也被称为嵌套函数,是函数,你其他函数中定义。在 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 内部函数的用例多种多样。您可以使用它们来提供封装隐藏您的函数以防止外部访问,您可以编写辅助内部函数,还可以创建闭包装饰器。在本节中,您将了解内部函数的前两个用例,在后面的部分中,您将学习如何创建闭包工厂函数装饰器

提供封装

当您需要保护或隐藏给定的函数以使其免受外部发生的一切影响时,就会出现内部函数的常见用例,以便该函数完全隐藏在全局范围之外。这种行为通常称为封装

这是一个突出该概念的示例:

>>>
>>> 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(),阻止您从全局范围访问它。

构建助手内部函数

有时,您的函数在其主体内的多个位置执行相同的代码块。例如,假设您要编写一个函数来处理包含有关纽约市 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()需要file作为一个参数。该函数检查是否file是物理文件或文件对象的基于字符串的路径。然后它调用辅助内部函数most_common_provider(),它接受一个文件对象并执行以下操作:

  1. 读取文件内容到发电机,其收益率的字典使用csv.DictReader
  2. 创建 Wi-Fi 提供商列表。
  3. 使用collections.Counter对象计算每个提供商的 Wi-Fi 热点数量。
  4. 用检索到的信息打印一条消息。

如果运行该函数,则会得到以下输出:

>>>
>>> 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()使用基于字符串的文件路径还是使用文件对象调用,您都会得到相同的结果。

使用内部与私有帮助函数

通常,您创建助手内部函数就像most_common_provider()您想要提供封装一样。如果您认为除了包含函数之外不会在其他任何地方调用它们,您也可以创建内部函数。

尽管将您的辅助函数编写为内部函数可以达到预期的结果,但将它们提取为顶级函数可能会更好地为您服务。在这种情况下,您可以_在函数名称中使用前导下划线 ( ) 来指示它对当前模块或类是私有的。这将允许您从当前模块或类中的任何其他位置访问您的辅助函数,并根据需要重用它们。

将内部函数提取为顶级私有函数可以使您的代码更清晰、更具可读性。这种做法可以产生因此应用单一职责原则的功能

使用内部函数保持状态:闭包

在 Python 中,函数是一等公民。这意味着它们与任何其他对象相同,例如数字字符串列表、元组、模块等。您可以动态地创建或销毁它们,将它们存储在数据结构中,将它们作为参数传递给其他函数,将它们用作返回值,等等。

您还可以在 Python 中创建高阶函数高阶函数是通过将其他函数作为参数、返回它们或两者兼而有之的方式对其他函数进行操作的函数。

到目前为止,您看到的所有内部函数示例都是恰好嵌套在其他函数中的普通函数。除非您需要对外部世界隐藏您的函数,否则它们没有特定的嵌套原因。您可以将这些函数定义为私有顶级函数,这样就可以了。

在本节中,您将了解闭包工厂函数。闭包是由其他函数返回的动态创建的函数。他们的主要特点是他们可以完全访问在创建闭包的本地命名空间中定义的变量和名称,即使封闭函数已经返回并完成执行。

在 Python 中,当您返回内部函数对象时,解释器会将函数与其包含的环境或闭包一起打包。函数对象保留在其包含范围内定义的所有变量和名称的快照。要定义闭包,您需要执行三个步骤:

  1. 创建一个内部函数。
  2. 来自封闭函数的引用变量。
  3. 返回内部函数。

有了这些基本知识,您就可以立即开始创建闭包并利用它们的主要功能:在函数调用之间保持状态

在闭包中保持状态

闭包会导致内部函数在调用时保留其环境状态。闭包不是内部函数本身,而是内部函数及其封闭环境。闭包捕获包含函数中的局部变量和名称并保留它们。

考虑以下示例:

 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()获得价值exponent?这就是闭包发挥作用的地方。在本例中,从外部函数 中power()获取 的值。当您调用 时,Python 会执行以下操作:exponentgenerate_power()generate_power()

  1. 定义一个新的 实例power(),它接受一个参数base
  2. 拍摄 周围状态的快照power(),其中包括exponent其当前值。
  3. power()连同其整个周围的状态一起返回。

这样,当您调用由power()返回的实例时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连续调用之间的状态。即使你定义了samplein mean(),它在闭包中仍然可用,所以你可以修改它。在这种情况下,sample作为一种动态封闭状态工作。

修改关闭状态

通常,闭包变量对外界是完全隐藏的。但是,您可以为它们提供gettersetter内部函数:

>>>
>>> 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 函数。您可以使用这些函数来获取阅读和变量写访问xy,这是在封闭范围和船舶与封闭定义。

尽管此函数创建的闭包可能比等效的类工作得更快,但您需要注意,此技术不提供主要功能,包括继承、属性、描述符以及类和静态方法。如果您想更深入地研究这种技术,请查看使用闭包和嵌套作用域模拟类的简单工具(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:Demystified

这是如何创建装饰器的最后一个示例。这一次,您将重新实现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(),将参数打包在exponentargs,进行最后的幂计算,并返回结果。

结论

如果您在另一个函数内定义一个函数,那么您就是在创建一个内部函数,也称为嵌套函数。在 Python 中,内部函数可以直接访问您在封闭函数中定义的变量和名称。这为您提供了一种创建辅助函数、闭包和装饰器的机制。

在本教程中,您学习了如何:

  • 通过在其他函数中嵌套函数来提供封装
  • 编写辅助函数以重用代码片段
  • 实现在调用之间保持状态的闭包工厂函数
  • 构建装饰器函数以提供新功能

您现在已准备好在自己的代码中利用内部函数的多种用途。如果您有任何问题或意见,请务必在下面的评论部分分享。

(完)