Python嵌套函数变量作用域

188

我已经阅读了关于这个主题的几乎所有其他问题,但我的代码仍然无法正常工作。

我认为我在Python变量作用域方面漏掉了一些东西。

以下是我的代码:

PRICE_RANGES = {
                64:(25, 0.35),
                32:(13, 0.40),
                16:(7, 0.45),
                8:(4, 0.5)
                }

def get_order_total(quantity):
    global PRICE_RANGES
    _total = 0
    _i = PRICE_RANGES.iterkeys()
    def recurse(_i):
        try:
            key = _i.next()
            if quantity % key != quantity:
                _total += PRICE_RANGES[key][0]
            return recurse(_i) 
        except StopIteration:
            return (key, quantity % key)

    res = recurse(_i)

我得到了:

"全局名称'_total'未定义"

我知道问题在于_total的赋值,但我不明白为什么。 recurse() 不应该可以访问父函数的变量吗?

有人能向我解释一下 Python 变量作用域方面我可能遗漏的内容吗?


2
这不是对你实际问题的回答,只是一个提示,你整个函数可以写成return sum(lower for (key, (lower, upper)) in PRICE_RANGES.iteritems() if quantity % key != quantity) - chthonicdaemon
实际上,仔细检查后,您返回了 (key, quantity % key),所以我发布的部分只是计算 _total 的方式。您的函数似乎总是返回最后一个访问的键 - 您可能认为字典项的顺序将被保留,但它们并没有,因此您的函数的最终返回值有些随机。 - chthonicdaemon
10个回答

378
在Python 3中,您可以使用nonlocal语句访问非本地和非全局作用域。 nonlocal语句会导致变量定义绑定到最近作用域中先前创建的变量。以下是一些示例以说明:
def sum_list_items(_list):
    total = 0

    def do_the_sum(_list):
        for i in _list:
            total += i

    do_the_sum(_list)

    return total

sum_list_items([1, 2, 3])

上述示例将失败并显示以下错误:UnboundLocalError: local variable 'total' referenced before assignment 使用nonlocal我们可以使代码正常工作:
def sum_list_items(_list):
    total = 0

    def do_the_sum(_list):

        # Define the total variable as non-local, causing it to bind
        # to the nearest non-global variable also called total.
        nonlocal total

        for i in _list:
            total += i

    do_the_sum(_list)

    return total

sum_list_items([1, 2, 3])

但是"nearest"是什么意思呢?这里有另一个例子:

def sum_list_items(_list):

    total = 0

    def do_the_sum(_list):

        # The nonlocal total binds to this variable.
        total = 0

        def do_core_computations(_list):

            # Define the total variable as non-local, causing it to bind
            # to the nearest non-global variable also called total.
            nonlocal total

            for i in _list:
                total += i

        do_core_computations(_list)

    do_the_sum(_list)

    return total

sum_list_items([1, 2, 3])

在上面的示例中,total将绑定到do_the_sum函数内定义的变量,而不是sum_list_items函数中定义的外部变量,因此代码将返回0。请注意,仍然可以进行双重嵌套,例如:如果在do_the_sum中声明totalnonlocal,则上述示例将按预期工作。
def sum_list_items(_list):

    # The nonlocal total binds to this variable.
    total = 0

    def do_the_sum(_list):

        def do_core_computations(_list):

            # Define the total variable as non-local, causing it to bind
            # to the nearest non-global variable also called total.
            nonlocal total

            for i in _list:
                total += i

        do_core_computations(_list)

    do_the_sum(_list)

    return total

sum_list_items([1, 2, 3])

在上面的例子中,非本地赋值向上遍历两个级别,然后定位到对sum_list_items本地的total变量。

22
这是一项非常重要的贡献!我不知道这个声明。谢谢你指出来。也许你可以扩展你的回答,包括一个例子。 - JGC
如果您愿意,欢迎进行编辑。 - Michael Hoffman
这真的很有帮助。nonlocal 对我有用。 - Prabo
4
现在这确实是正确的答案。自2020年初起,Python 2就已经不再维护了,所以Python 3特定的答案现在适用于几乎所有人。 - bobsbeenjamin
这应该是标记的答案。 - EuberDeveloper
显示剩余6条评论

195

这里有一张插图,能够简洁明了地阐述David的答案要点。

def outer():
    a = 0
    b = 1

    def inner():
        print a
        print b
        #b = 4

    inner()

outer()

将语句b = 4注释掉后,此代码输出0 1,正如您所期望的那样。

但是如果您取消对该行的注释,在print b行上,您会得到错误。

UnboundLocalError: local variable 'b' referenced before assignment
似乎很神秘,为什么b = 4的存在可能导致在它之前的行中b消失。但是David引用的文字解释了原因:在静态分析期间,解释器确定binner中被赋值,并且它因此是inner的本地变量。打印行尝试在它被赋值之前打印这个内部作用域中的b

41
+1 我曾感到困惑,但现在我明白了发生了什么。我是一名C#程序员,每当我开始喜欢Python时,就会出现像这样的事情,让我失望。 - nima
1
Python 之所以这样工作,有非常好的原因。 - sudo
12
@sudo,您能详细说明一下您的评论吗? - radpotato
1
@radpotato 哎呀,我不小心评论到了错误的答案上。我不记得我原本想评论哪里了。无论如何,我不知道这有多么令人信服,但问题在于 b 否则会从函数内部的“全局”范围变为“局部”范围。Python 似乎在低层次上不支持这种操作,而且这样做会让人感到困惑和不必要。 - sudo

74

当我运行您的代码时,我遇到了这个错误:

UnboundLocalError: local variable '_total' referenced before assignment

这个问题是由这行代码引起的:

_total += PRICE_RANGES[key][0]

关于Python作用域和命名空间的文档中这样说:

Python的一个特别之处是如果没有global语句生效,那么对变量的赋值总是在最内层作用域中进行。赋值不会复制数据——它们只是将名称绑定到对象。

因此,由于这行代码实际上是在说:

_total = _total + PRICE_RANGES[key][0]

它在recurse()的命名空间中创建了_total。由于_total是新的且未赋值,因此您无法在加法中使用它。


35

不必声明特殊的对象、映射或数组,也可以使用函数属性。 这使得变量的作用域非常清晰。

def sumsquares(x,y):
  def addsquare(n):
    sumsquares.total += n*n

  sumsquares.total = 0
  addsquare(x)
  addsquare(y)
  return sumsquares.total

当然,这个属性属于函数(定义),而不是函数调用。因此,我们必须谨慎处理线程和递归。


2
对于Python 2,只要注意上面的免责声明,这应该是被接受的答案。我发现它甚至允许您嵌套一个信号处理程序,可以操作外部函数的状态 - 虽然我不建议这样做 - 只是重构一些到处都有全局变量的可怕遗留代码的第一步。现在再次进行重构,并以正确的方式完成... - ckot
虽然这对于重构可怕的全局代码可能很好,但如果您正在编写自己的代码并考虑使用此方法,您应该将其制作成一个类。但是,这肯定比随机的全局变量要好。并且在涉及某些静态值时应优先考虑使用它。 - Tatarize

17

这是 redman 解决方案的变体,但使用适当的命名空间而不是数组来封装变量:

def foo():
    class local:
        counter = 0
    def bar():
        print(local.counter)
        local.counter += 1
    bar()
    bar()
    bar()

foo()
foo()

我不确定在Python社区中是否将这种使用类对象的方法视为丑陋的黑客行为或正确的编码技巧,但在Python 2.x和3.x中它可以正常工作(已在2.7.3和3.2.3中测试)。此解决方案的运行时效率也让我不确定。


当本地方法有许多本地变量需要修改时,这种方法非常方便,但我也想知道“在Python社区中,以这种方式使用类对象是否被认为是一种丑陋的黑客行为还是一种适当的编码技术”。 - Mr_and_Mrs_D

8
您可能已经得到了问题的答案。但是我想指出一种我通常使用的方法,那就是使用列表。例如,如果我想要做到这一点:
X=0
While X<20:
    Do something. ..
    X+=1

我会这样做:

相反,我会这样做:

X=[0]
While X<20:
   Do something....
   X[0]+=1

这样X就不会成为一个局部变量。

3
不要这样做,拜托了。那是一个可怕且低效的解决方案。 - Chris Morgan
10
它为什么很可怕?有更好的解决方案吗? - undefined
1
我不是太喜欢它,但你是唯一提供解决方法的人。在某种程度上, X 成为了一个中间命名空间...... - Juh_
3
请注意,while循环与其所在代码块共享命名空间,因此对于您的示例来说并不是一个合适的选择。 - Juh_

4

虽然我曾经使用过@redman的基于列表的方法,但从可读性的角度来看,这并不是最优的。

这里是修改版的@Hans的方法,除了我使用内部函数的属性而不是外部函数的属性。这应该更加适用于递归,甚至可能是多线程:

def outer(recurse=2):
    if 0 == recurse:
        return

    def inner():
        inner.attribute += 1

    inner.attribute = 0
    inner()
    inner()
    outer(recurse-1)
    inner()
    print "inner.attribute =", inner.attribute

outer()
outer()

这将打印:

inner.attribute = 3
inner.attribute = 3
inner.attribute = 3
inner.attribute = 3

如果我执行s/inner.attribute/outer.attribute/g,我们将得到:

outer.attribute = 3
outer.attribute = 4
outer.attribute = 3
outer.attribute = 4

因此,将它们设置为内部函数的属性似乎更好。

从可读性的角度来看,这也是有道理的:因为这样变量在概念上与内部函数相关联,这种表示法提醒读者变量在内部和外部函数的范围之间是共享的。唯一稍微影响可读性的缺点是inner.attribute只能在def inner(): ...语法之后进行设置。


4
从哲学的角度来看,一个答案可能是:“如果你正在遇到命名空间的问题,那就给它一个独立的命名空间!”将其提供在自己的类中不仅允许您封装问题,而且还使测试更加容易,消除了那些讨厌的全局变量,并减少了在各种顶级功能之间移动变量的需要(毫无疑问,不止 get_order_total 这一个)。保留 OP 的代码以便关注基本变化。
class Order(object):
  PRICE_RANGES = {
                  64:(25, 0.35),
                  32:(13, 0.40),
                  16:(7, 0.45),
                  8:(4, 0.5)
                  }


  def __init__(self):
    self._total = None

  def get_order_total(self, quantity):
      self._total = 0
      _i = self.PRICE_RANGES.iterkeys()
      def recurse(_i):
          try:
              key = _i.next()
              if quantity % key != quantity:
                  self._total += self.PRICE_RANGES[key][0]
              return recurse(_i) 
          except StopIteration:
              return (key, quantity % key)

      res = recurse(_i)

#order = Order()
#order.get_order_total(100)

作为补充,有一种方法是对另一个答案中的列表想法进行变体,但可能更加清晰。
def outer():
  order = {'total': 0}

  def inner():
    order['total'] += 42

  inner()

  return order['total']

print outer()

0

我在编程中的解决方案...

def outer():

class Cont(object):
    var1 = None
    @classmethod
    def inner(cls, arg):
        cls.var1 = arg


Cont.var1 = "Before"
print Cont.var1
Cont.inner("After")
print Cont.var1

outer()

-1
>>> def get_order_total(quantity):
    global PRICE_RANGES

    total = 0
    _i = PRICE_RANGES.iterkeys()
    def recurse(_i):
    print locals()
    print globals()
        try:
            key = _i.next()
            if quantity % key != quantity:
                total += PRICE_RANGES[key][0]
            return recurse(_i)
        except StopIteration:
            return (key, quantity % key)
    print 'main function', locals(), globals()

    res = recurse(_i)


>>> get_order_total(20)
main function {'total': 0, 'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20} {'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}
{'recurse': <function recurse at 0xb76baed4>, '_i': <dictionary-keyiterator object at 0xb6473e64>, 'quantity': 20}
{'__builtins__': <module '__builtin__' (built-in)>, 'PRICE_RANGES': {64: (25, 0.34999999999999998), 32: (13, 0.40000000000000002), 16: (7, 0.45000000000000001), 8: (4, 0.5)}, '__package__': None, 's': <function s at 0xb646adf4>, 'get_order_total': <function get_order_total at 0xb646ae64>, '__name__': '__main__', '__doc__': None}

Traceback (most recent call last):
  File "<pyshell#32>", line 1, in <module>
    get_order_total(20)
  File "<pyshell#31>", line 18, in get_order_total
    res = recurse(_i)
  File "<pyshell#31>", line 13, in recurse
    return recurse(_i)
  File "<pyshell#31>", line 13, in recurse
    return recurse(_i)
  File "<pyshell#31>", line 12, in recurse
    total += PRICE_RANGES[key][0]
UnboundLocalError: local variable 'total' referenced before assignment
>>> 

正如你所看到的,total 在主函数的本地范围内,但它不在递归的本地范围内(显然),也不在全局范围内,因为它仅在 get_order_total 的本地范围中定义。

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