装饰器中不同类型的变量是否有不同的作用域? (Python)

Are there different scopes for different types of variables in a decorator? (Python)

我很难理解 python-装饰器中变量的范围。我在某处读到非局部变量存储为只读。但不知何故字典似乎是个例外。

def outer(f):
    def inner():
        print val
        return f()
    val =1
    return inner

def outer2(f):
    def inner2():
        val+=1
        print val
        return f()
    val =1
    return inner2

def outer3(f):
    def inner3():
        d[0]+=1
        print d
        return f()
    d ={0:0}
    return inner3

import doctest

class Test: """
>>> function = lambda : 'Function called'

>>> f1=outer(function)
>>> f1()
1
'Function called'

>>> f2=outer2(function)
>>> f2()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in inner2
UnboundLocalError: local variable 'val' referenced before assignment

>>> f3=outer3(function)
>>> f3()
{0: 1}
'Function called'
"""

print (doctest.testmod())

为什么 val 不在 f2 的范围内?
为什么字典没有和整数一样的问题?

提前致谢!

我认为 "read-only" 你的意思是你不能将名称重新绑定到非本地范围内的对象(这在 Python 2 中是正确的)。对于您的字典示例,您没有将名称重新绑定到对象——您正在改变字典实例——d[0] += 1 要求 d 对象本身检索与键 0 关联的值,并且然后要求对象放回加法的结果。

所以你对 dict 所做的与你对 int 所做的在本质上是不同的,这就是为什么你认为字典不会遇到与 int 相同的问题。这种误解经常出现,因为字典是可变的,代码可以很容易地改变字典的内容,程序员很容易陷入陷阱,认为他们在真正重新绑定整数时同样改变了整数的内容。

整数根据定义是不可变的——你不能改变它们——所以为了"change"一个整数变量,你总是必须将标识符重新绑定到一个不同的对象。但是如果你反弹了 dict,例如使用像 d = d.copy() 这样的代码,您会遇到同样的问题。

在Python中,一个变量的id是它的内存地址——不同的地址,不同的对象。因此,正如您在这里看到的,当您修改列表的一个元素时,您仍然拥有相同的列表对象,但是当您修改一个整数时——您并没有真正修改它——您得到的是一个不同的对象:

>>> x = [0]
>>> id(x), id(x[0])
(3063883308, 138405104)
>>> x[0] += 1
>>> id(x), id(x[0])
(3063883308, 138405120)

正如 BrenBarn 在下面和他的回答中指出的那样,我最初误解了你问题的全部范围,因为我关注的是你最初的困惑,即为什么某些东西与字典实例中的项目一起工作,而在使用直接int。

在函数内部,对裸变量的赋值(例如,不在对象上调用 __setattr__ 之类的函数)总是对局部变量进行赋值,除非它们被声明为 global 或(在 Python 3) nonlocal.

诸如+=之类的增强赋值遵循与赋值相同的规则,只是它们自然要求初始值先存在才能修改。所以当对一个没有被声明为非局部或全局的变量进行赋值时,它必须是一个局部变量,并且当它不存在时(还没有被前面的语句绑定)这样它就可以在扩充赋值表达式中使用,将引发您看到的异常。通过添加 nonlocal 语句,您的失败示例可以与 Python 3 一起使用:

>>> def outer2(f):
...     def inner2():
...         nonlocal val
...         val+=1
...         print(val)
...         return f()
...     val = 1
...     return inner2
... 
>>> function = lambda : 'Function called'
>>> 
>>> print(outer2(function)())
2
Function called

原因是像+=这样的操作的性质是由左边的目标类型决定的,所以var += 1和[=不是一回事12=]。如果左侧是裸名,则这是一个变量重新绑定。如果不是,那就不是。在d[0] += 1的情况下,+=由字典对象处理,不涉及重新绑定名称d

相关文档是here。请注意第一个要点之间的区别,其中结果只是 "the name is bound",而所有其他要点的结果是 "the object is asked to...".