关于范围:在Python中返回累加器的函数

Function that returns an accumulator in Python

我读的是黑客和画家,我对作者提到的一个问题感到困惑,这个问题是为了说明不同编程语言的威力。

问题是:

We want to write a function that generates accumulators—a function that takes a number n, and returns a function that takes another number i and returns n incremented by i. (That’s incremented by, not plus. An accumulator has to accumulate.)

作者提到了几种不同编程语言的解决方案。例如,公共lisp:

1
2
(defun foo (n)
  (lambda (i) (incf n i)))

和JavaScript:

1
function foo(n) { return function (i) { return n += i } }

但是,当涉及到python时,以下代码不起作用:

1
2
3
4
5
6
7
8
9
def foo(n):
    s = n
    def bar(i):
        s += i
        return s
    return bar

f = foo(0)
f(1)  # UnboundLocalError: local variable 's' referenced before assignment

一个简单的修改就能使它工作:

1
2
3
4
5
6
def foo(n):
    s = [n]
    def bar(i):
        s[0] += i
        return s[0]
    return bar

我是Python的新手。为什么第一个解决方案不起作用,而第二个解决方案起作用?作者提到了词汇变量,但我还是不明白。


s += i只是s = s + i的糖。

这意味着您要为变量s分配一个新值(而不是就地对其进行变异)。当您分配给一个变量时,python假定它是函数的局部变量。但是,在分配之前,需要对s + i进行评估,但s是局部的,仍然是未分配的->错误。

在第二种情况下,s[0] += i从不直接分配给s,而是只从s访问项目。所以python可以清楚地看到它不是一个局部变量,并在外部范围内查找它。

最后,更好的选择(在python 3中)是明确地告诉它s不是局部变量:

1
2
3
4
5
6
7
def foo(n):
    s = n
    def bar(i):
        nonlocal s
        s += i
        return s
    return bar

(实际上没有必要使用s——你只需在bar内使用n。)

*情况稍微复杂一些,但重要的问题是计算和分配是分两步执行的。


无限生成器是一种实现。可以在生成器实例上调用__next__,以迭代地提取连续的结果。

1
2
3
4
5
6
7
8
9
10
def incrementer(n, i):
    while True:
        n += i
        yield n

g = incrementer(2, 5)

print(g.__next__())  # 7
print(g.__next__())  # 12
print(g.__next__())  # 17

如果您需要一个灵活的增量器,一种可能是面向对象的方法:

1
2
3
4
5
6
7
8
9
10
11
12
class Inc(object):
    def __init__(self, n=0):
        self.n = n
    def incrementer(self, i):
        self.n += i
        return self.n

g = Inc(2)

g.incrementer(5)  # 7
g.incrementer(3)  # 10
g.incrementer(7)  # 17


以下内容有效:

1
2
3
4
5
6
def foo(n):
    s = n
    def bar(i):
        s_ = s + i
        return s_
    return bar

内部函数bar在其作用域内寻找局部的s,如果找不到,则在封闭作用域内查找一个级别,在封闭作用域内查找s这是foo的局部变量。但是如果你说s = s + 1,你就把s声明为bar范围内的一个新的局部变量("赋值语句在局部范围内创建变量"),这会导致一个错误,因为你在添加(引用它)之前没有给s赋值。

在另一个例子中,说s[0] = s[0] + 1是不同的,因为您没有在条内声明一个新的局部变量,所以您访问了在条的封闭范围内找到的s的第一个元素。


在python中,如果我们使用一个变量并将其传递给一个函数,那么无论您对该变量做什么更改,它都将按值调用,而不会反映到原始变量中。

但是,当使用列表而不是变量时,对函数列表所做的更改将反映在函数外部的原始列表中,因此称为引用调用。

这就是第二个选项起作用而第一个选项不起作用的原因。