Python中的嵌套函数是如何工作的?

60
def maker(n):
    def action(x):
        return x ** n
    return action

f = maker(2)
print(f)
print(f(3))
print(f(4))

g = maker(3)
print(g(3))

print(f(3)) # still remembers 2

即使在 maker() 返回并退出时,为什么嵌套函数仍然记住第一个值 2?


这与6小时前提出的一个问题略有相似:https://dev59.com/H0vSa4cB1Zd3GeqPbyK5 - James Polley
可能是Python中的嵌套函数的重复问题。 - PolyGeo
9个回答

40

你实际上是在创建一个闭包

在计算机科学中,闭包是一个具有自由变量并被绑定在词法环境中的一等函数。这样的函数被称为“封闭于”它的自由变量。

相关阅读:闭包:为什么它们如此有用?

闭包只是一种更方便的方式,让函数可以访问本地状态。

来自http://docs.python.org/reference/compound_stmts.html

程序员的注释:函数是一等对象。在函数定义内部执行的 'def' 表达式定义了一个局部函数,该函数可以返回或传递。嵌套函数中使用的自由变量可以访问包含 def 的函数的局部变量。有关详细信息,请参见“名称和绑定”部分。


33

您可以将其看作父函数中所有变量被替换为它们在子函数内的实际值。这样,就不需要跟踪父函数的作用域来使子函数正确运行。

将其视为 "动态创建函数"。

def maker(n):
  def action(x):
    return x ** n
  return action

f = maker(2)
--> def action(x):
-->   return x ** 2

这是Python的基本行为,它对多重赋值也是相同的。

a = 1
b = 2
a, b = b, a

Python将其解读为

a, b = 2, 1

它基本上是先插入这些值,再执行任何操作。


如果n被替换为实际数字,那么为什么它仍然可以在action的locals()字典中找到? - Victor Yan
1
因为它是本地变量,所以在action中对n进行的任何操作都不会在外部显示。 - Tor Valamo

15

您定义了两个函数。当您调用

f = maker(2)

你正在定义一个返回两倍数字的函数,因此

f(2) --> 4
f(3) --> 6

接着,你需要定义另一个不同的函数

g = maker(3)

返回三倍的数字

g(3) ---> 9

但它们是两个不同的函数,引用的不是同一个函数,每个都是独立的。即使在函数“maker”的内部作用域中调用相同的名称,也不是同一个函数,每次调用maker()时,您都定义了一个不同的函数。就像本地变量一样,每次调用函数时使用相同的名称,但可以包含不同的值。 在这种情况下,变量'action'包含一个函数(可能不同)


9
这就是所谓的 "闭包 "。简单地说,对于大多数编程语言将函数视为一等公民对象的情况,只要函数仍然存在,函数对象中使用的任何变量都会被封闭(即记住)。如果您知道如何利用它,它是一个强大的概念。
在您的示例中,嵌套的 action 函数使用变量 n ,因此它形成了一个围绕该变量的闭包,并记住它以供稍后的函数调用使用。

7

让我们看一下编写内部函数的三个常见原因。

1. 闭包和工厂函数

即使变量超出范围或函数本身从当前命名空间中删除,封闭作用域中的值也会被记住。

def print_msg(msg):
    """This is the outer enclosing function"""

    def printer():
        """This is the nested function"""
        print(msg)

    return printer  # this got changed

现在让我们尝试调用这个函数。
>>> another = print_msg("Hello")
>>> another()
Hello

那很不寻常。函数print_msg()被调用时带有字符串"Hello",返回的函数绑定到名称another。在调用another()时,即使我们已经执行了print_msg()函数,消息仍然被记住。这种将一些数据("Hello")附加到代码的技术称为Python中的闭包。
那么闭包有什么好处呢?闭包可以避免使用全局值,并提供某种形式的数据隐藏。它还可以提供面向对象的解决方案。当类中只有少量方法(大多数情况下只有一个方法)需要实现时,闭包可以提供替代和更优雅的解决方案。参考 2. 封装:
封装的一般概念是隐藏和保护内部世界不受外部影响,这里内部函数只能在外部函数内部访问,并且受到来自函数外部的任何影响的保护。
3. DRY原则

也许你有一个巨大的函数,在多个地方执行相同的代码块。例如,你可能会编写一个处理文件的函数,并希望接受打开的文件对象或文件名:

def process(file_name):
    def do_stuff(file_process):
        for line in file_process:
            print(line)
    if isinstance(file_name, str):
        with open(file_name, 'r') as f:
            do_stuff(f)
    else:
        do_stuff(file_name)

更多内容请参考this博客。


这不是一个答案。仅提供链接的答案在SO中不受欢迎,因为删除链接页面将使此帖子无用。请提供更多信息并总结或移动链接的基本部分到您的答案中。 - FallenAngel
请尽量避免使用HTML,只有在最后的情况下才使用。花些时间查看Markdown SO语法。 - Michele d'Amico

2

因为在创建函数时,n2,所以您的函数是:

def action(x):
    return x ** 2

当你调用 f(3) 时,x 被设置为 3,因此你的函数将返回 3 ** 2

2

关于闭包,人们正确地回答了:在“maker”被调用时,“action”内部的“n”的有效值是它最后一次拥有的值。

克服这个问题的一种简单方法是将你的自由变量(n)作为“action”函数的一个变量,该函数在运行时接收“n”的副本:

最简单的方法是将“n”设置为参数,其默认值为创建时的“n”。因为函数的默认参数存储在元组中,而元组是函数本身的属性(在这种情况下是action.func_defaults),所以“n”的这个值保持不变:

def maker(n):
    def action(x, k=n):
        return x ** k
    return action

使用方法:

f = maker(2) # f is action(x, k=2)
f(3)   # returns 3^2 = 9
f(3,3) # returns 3^3 = 27

1

其中一种用途是返回一个维护参数的函数。

def outer_closure(a):
    #  parm = a               <- saving a here isn't needed
    def inner_closure():
        #return parm
        return a              # <- a is remembered 
    return inner_closure

# set parm to 5 and return address of inner_closure function
x5 = outer_closure(5)
x5()
>5

x6 = outer_closure(6)
x6()
>6

# x5 inner closure function instance of parm persists 
x5()
>5

0

当你使用def关键字创建一个函数时,你正在做的就是:创建一个新的函数对象并将其分配给一个变量。在你提供的代码中,你将这个新的函数对象分配给了一个名为action的局部变量。

当你第二次调用它时,你正在创建第二个函数对象。因此,f指向第一个函数对象(square-the-value),g指向第二个函数对象(cube-the-value)。当Python看到"f(3)"时,它会将其解释为"执行由变量f指向的函数对象,并传递值3"。f和g是不同的函数对象,因此返回不同的值。


等一下。任何函数都是由关键字 def 定义的。你的意思是说没有办法调用一个函数,因为每次引用一个定义,都会创建第二个对象? - Val

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接