Python中“绑定到变量”和“绑定到对象”有什么区别
当我学习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 个回答
>>> 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
有两个关键点:
- Python使用一种叫做 LEGB规则 来查找变量的值。LEGB代表本地(Local)、扩展(Extended)、全局(Global)和内置(Builtins)。这意味着一个变量名首先会绑定到本地的值,如果本地没有这个值,就会在扩展范围内查找,如果还是没有,就会在全局范围内查找,最后在内置范围内查找。
当定义一个函数时,比如
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)
closureTest
的maxIndex
在定义时被设定为默认值15。(7) 当调用
closureTest()
时没有传入参数,就会使用maxIndex
的默认值。返回的值是15。
如果你把'定义'语句中的参数表达式的绑定过程想得简单一点,可能会更容易理解。当你看到'def closureTest(maxIndex=maxIndex):'时,这就像是'如果'或'当'这样的语句,后面跟着一段代码,这段代码会被解析并绑定到这个函数上(也就是可调用的对象)。
'def'语句是在它所在的作用域中被执行的(可以想象成在同一层级的缩进下)。它的参数表达式声明了函数内部的参数是如何与传入的值对应的。任何提供默认值的参数(比如你例子中的maxIndex)会创建一个函数对象,并将对应的参数名绑定到当时在'def'语句作用域内的对象上。
当这个函数被调用时,它的每个参数(在函数内部的名字)都会绑定到传入的参数上。任何可选的参数就会保持绑定到在'def'语句中评估的那些参数上。
在你所有的例子中,每次外部函数被调用时,都会创建一个内部函数。在你的第二个例子中,参数列表是空的,内部函数只是通过一层嵌套作用域看到了这个名字。在第一个例子中,内部函数的定义语句在新函数的命名空间内创建了一个默认的maxIndex名字,这样就避免了与外部作用域中的值发生冲突,就像你对任何函数中的局部变量的预期一样。
在最后一个例子中,maxIndex的值在内部函数被(重新)定义之前就已经被修改了。当你意识到这个函数在每次外部函数调用时都被(重新)定义时,就不会觉得这么复杂了。
Python是一种动态语言。每次控制流经过代码中的那一行时,'def'语句都会被虚拟机执行。(是的,代码已经被编译成字节码,但'def'会被编译成虚拟机的操作码,这些操作码在运行时执行代码评估和名称绑定(到函数的名字))。
如果你定义一个参数列表像'(myList=list())'这样的函数,那么在定义被执行时,一个列表就会被实例化。每次调用这个函数而不传入参数时,这个列表都可以被访问。任何传入参数的调用都会将这个参数名绑定到调用时提供的参数上。(在'def'时实例化的对象仍然被定义的代码对象引用,也就是在'def'语句后面缩进的代码块)。
如果你不区分参数和实参,这些内容都不会有意义。记住,参数是函数定义的一部分;它们定义了实参如何映射到函数的局部命名空间。实参是调用的一部分;它们是在调用函数时传入的值。
希望这些解释能帮到你。我知道这个区分很微妙,而且这些术语经常被错误使用,就好像它们是可以互换的一样(包括在Python文档中)。