【函数式编程】Python闭包:结合计数器的例子

784 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

1. 计数器的实现

需求:我们想要实现一个函数add1,这个函数每次运行都会改变一个变量的值,并返回这个变量改变后的当前值。比如,第一次运行add1把变量的值变成1,函数返回1;第二次运行时把变量的值改为2,并返回2。

我们写出一个原型,但Python语法不允许像下面这样在函数内部访问并修改外部的值:

counter = 0

def add1():
  counter += 1
  return counter

print(add1()) # expected: return = 1, counter = 1; but failed
print(add1()) # expected: return = 2, counter = 2; but failed

以上代码在执行时会报错,表示解释器将函数体内部对counter的mention认为是一个函数内部作用域的局部变量counter,因找不到这个变量而报错。即,默认函数体内部是不能引用外部作用域的变量的。

这个时候我们可以用global关键字,指明add1中对counter的mention指的是全局变量counter:

counter = 0

def add1():
  global counter
  counter += 1
  return counter

print(add1()) # return = counter = 1
print(add1()) # return = counter = 2
print(counter) # 2

2. 封装成计数器函数

当把计数器的逻辑封装成函数时,简单地把上面的过程塞进一个函数,我们大概想要一个这样的原型:

def add1():
  counter = 0

  def add1c():
    ???1 # 使counter在子函数内部能被访问到
    counter += 1
    return counter

  return ???2 # 使counter值被改变的情况反馈到外层

print(add1()) # expected: 1
print(add1()) # expected: 2

对于第一处需求,类似上面的global关键字,我们使用nonlocal关键字,代码大致跟上面相似。

  • global关键字:语法是global name,表示函数体内对name的mention指向名字为name的全局变量,而不是局部作用域中的变量。

  • nonlocal关键字:语法是nonlocal name,表示函数体内对name的mention指向名字为name的外层作用域(非全局)中的变量,而不是局部作用域中的变量。

对于第二处需求,考虑到add1内的add1c函数,每次调用这个函数会让counter的值增加;这个函数就是我们最终想要的,所以我们让返回值直接为add1c这个函数,然后在后面的过程中,先调用add1使add1c实例化,再多次调用这个实例化的add1c函数,来实现每次计数器+1的效果。

def add1():
  counter = 0

  def add1c():
    nonlocal counter
    counter += 1
    return counter
  
  return add1c

a = add1() # add1c函数的实例

print(a()) # 1
print(a()) # 2

在这段代码里,可以从控制台看到变量a的类型是<class 'function'>,值是<function add1.<locals>.add1c at 0x7f8bba5d4a60>,即a是函数,是add1c的一个实例。在下面调用a时,每次a修改函数内部私有的变量,并返回变量的值;每次返回的值不同。

3. 闭包

注意到这里,我们使函数返回自身内部定义的子函数。这就是闭包的概念。

对于外部函数的作用域变量,包括函数的参数和函数体内部定义的局部变量,Python闭包只能引用值,不能修改;但通过nonlocal关键字,就可以实现子函数内修改外部变量。

闭包的核心思想在于,获得一个函数,这个函数可以访问函数上一层作用域的变量。这些变量受这个函数的作用域保护,只能通过这个函数的上层函数修改,从而使得函数拥有私有的变量。

参考

zhuanlan.zhihu.com/p/22229197 介绍闭包概念

www.cnblogs.com/tallme/p/11… 使用了nonlocal+闭包的计数器实现

www.cnblogs.com/yssjun/p/98… 闭包陷阱