Python中“绑定到变量”和“绑定到对象”有什么区别

3 投票
3 回答
2153 浏览
提问于 2025-04-16 08:16

当我学习Python中的“命名和绑定”时,看到一个例子:

>>> def testClosure(maxIndex):
        def closureTest(maxIndex=maxIndex):
            return maxIndex
        maxIndex += 5
        return closureTest()
>>> print(testClosure(10))
10


>>> def testClosure(maxIndex):
        def closureTest():
            return maxIndex
       maxIndex += 5
       return closureTest()
>>> print(testClosure(10))
15

作者解释说: 在后面的函数中,内层作用域的自由变量绑定到外层作用域的变量,而不是绑定到对象。

那么,我的问题是:在Python中,“绑定到变量”和“绑定到对象”有什么区别呢?

而且,这个问题很复杂:如果我重新排列代码,结果会不同。

>>> def testClosure(maxIndex):
        maxIndex += 5
        def closureTest(maxIndex=maxIndex):
            return maxIndex
        return closureTest()

>>> print(testClosure(10))
15

提前谢谢你。

3 个回答

0
>>> def testClosure(maxIndex):
        def closureTest(maxIndex=maxIndex): # You're creating a function with a kwarg of maxIndex
            return maxIndex                 # which references the int passed in from outside
        maxIndex += 5                       # Incrementing maxIndex means that the maxIndex in the outer
        return closureTest()                # scope now points to a different int.
>>> print(testClosure(10))
10

考虑一下:

>>> a = b = 1
>>> a += 1
>>> a
2
>>> b
1

我不太明白你说的“绑定到对象”和“绑定到变量”是什么意思。“变量”其实就是指向某些东西的引用,当你增加 a 的值时,你实际上是在修改它,让它指向一个不同的值,而 b 仍然指向原来的值。

当你没有把 maxIndex 的值传递给你的内部函数,然后又去请求它时,因为它在本地范围内没有定义,所以系统会去更大的范围内查找。

我在这里做了一个相对较大的假设,但你可以看到执行所需的时间有差异,我觉得这可能是因为查找的成本比较高:

>>> import timeit
>>> def testClosure(maxIndex):
...     def closureTest(maxIndex=maxIndex):
...         return maxIndex
...     maxIndex += 5
...     return closureTest()
...
>>> def testClosure2(maxIndex):
...     def closureTest():
...         return maxIndex
...     maxIndex += 5
...     return closureTest()
...
>>> timeit.Timer('testClosure(10)','from __main__ import testClosure').timeit()
1.4626929759979248
>>> timeit.Timer('testClosure2(10)','from __main__ import testClosure2').timeit()
1.7869210243225098
8

有两个关键点:

  1. Python使用一种叫做 LEGB规则 来查找变量的值。LEGB代表本地(Local)、扩展(Extended)、全局(Global)和内置(Builtins)。这意味着一个变量名首先会绑定到本地的值,如果本地没有这个值,就会在扩展范围内查找,如果还是没有,就会在全局范围内查找,最后在内置范围内查找。
  2. 当定义一个函数时,比如

    def closureTest(maxIndex=maxIndex):
        return maxIndex
    

    默认值是在定义时固定的,而不是在运行时。这里的定义时是指处理def语句的时刻,也就是函数被定义的时刻。而运行时是指函数被调用的时刻。需要注意的是,当你有嵌套函数时,内层函数的定义时发生在外层函数被调用之后。


第一个例子因为变量名maxIndex的重复使用变得有些复杂。你要先理解这一点,才能理解第一个例子:

>>> def testClosure(maxIndex):              
        def closureTest(index=maxIndex):     # (1)
            return index                     
        maxIndex += 5
        return closureTest()                 # (2)
>>> print(testClosure(10))
  • (1) 在定义时,index的默认值被设定为10。
  • (2) 当调用closureTest()时没有传入参数,index就会被设定为默认值10。所以返回的就是这个值。

def testClosure(maxIndex):
    def closureTest():
        return maxIndex                 # (3)
   maxIndex += 5
   return closureTest()                 # (4)
print(testClosure(10))
  • (3) LEGB规则告诉Python在本地范围内查找maxIndex的值。在本地范围内没有定义maxIndex,所以它会在扩展范围内查找。它找到了作为testClosure参数的maxIndex

  • (4) 当调用closureTest()时,maxIndex的值是15。因此,closureTest()返回的maxIndex就是15。


>>> def testClosure(maxIndex):
        maxIndex += 5                           # (5)    
        def closureTest(maxIndex=maxIndex):     # (6)
            return maxIndex
        return closureTest()                    # (7)
  • (5) maxIndex是15。

  • (6) closureTestmaxIndex在定义时被设定为默认值15。

  • (7) 当调用closureTest()时没有传入参数,就会使用maxIndex的默认值。返回的值是15。

3

如果你把'定义'语句中的参数表达式的绑定过程想得简单一点,可能会更容易理解。当你看到'def closureTest(maxIndex=maxIndex):'时,这就像是'如果'或'当'这样的语句,后面跟着一段代码,这段代码会被解析并绑定到这个函数上(也就是可调用的对象)。

'def'语句是在它所在的作用域中被执行的(可以想象成在同一层级的缩进下)。它的参数表达式声明了函数内部的参数是如何与传入的值对应的。任何提供默认值的参数(比如你例子中的maxIndex)会创建一个函数对象,并将对应的参数名绑定到当时在'def'语句作用域内的对象上。

当这个函数被调用时,它的每个参数(在函数内部的名字)都会绑定到传入的参数上。任何可选的参数就会保持绑定到在'def'语句中评估的那些参数上。

在你所有的例子中,每次外部函数被调用时,都会创建一个内部函数。在你的第二个例子中,参数列表是空的,内部函数只是通过一层嵌套作用域看到了这个名字。在第一个例子中,内部函数的定义语句在新函数的命名空间内创建了一个默认的maxIndex名字,这样就避免了与外部作用域中的值发生冲突,就像你对任何函数中的局部变量的预期一样。

在最后一个例子中,maxIndex的值在内部函数被(重新)定义之前就已经被修改了。当你意识到这个函数在每次外部函数调用时都被(重新)定义时,就不会觉得这么复杂了。

Python是一种动态语言。每次控制流经过代码中的那一行时,'def'语句都会被虚拟机执行。(是的,代码已经被编译成字节码,但'def'会被编译成虚拟机的操作码,这些操作码在运行时执行代码评估和名称绑定(到函数的名字))。

如果你定义一个参数列表像'(myList=list())'这样的函数,那么在定义被执行时,一个列表就会被实例化。每次调用这个函数而不传入参数时,这个列表都可以被访问。任何传入参数的调用都会将这个参数名绑定到调用时提供的参数上。(在'def'时实例化的对象仍然被定义的代码对象引用,也就是在'def'语句后面缩进的代码块)。

如果你不区分参数和实参,这些内容都不会有意义。记住,参数是函数定义的一部分;它们定义了实参如何映射到函数的局部命名空间。实参是调用的一部分;它们是在调用函数时传入的值。

希望这些解释能帮到你。我知道这个区分很微妙,而且这些术语经常被错误使用,就好像它们是可以互换的一样(包括在Python文档中)。

撰写回答