下次别用递归了,试试闭包吧!

开发 后端
今天分享 Python 的另一种牛的技术--闭包,可以用来作为替代递归函数。它可能不会胜过动态规划,但在思考方面要容易得多。换句话说,由于思想的抽象,我们有时可能难以使用动态规划,但是使用闭包会容易一些。

 [[385610]]

递归函数使用起来非常酷,简洁优雅,可以用来炫耀编程技巧。但是,在大多数情况下,递归函数具有非常高的时间和空间复杂性,我们应该避免使用它。更好的解决方案之一是在可能的情况下使用动态规划,对于能够分解为子问题的问题,动态规划可能是最佳方法。然而某些动态规划的状态转移方程不太容易定义。

今天分享 Python 的另一种牛的技术--闭包,可以用来作为替代递归函数。它可能不会胜过动态规划,但在思考方面要容易得多。换句话说,由于思想的抽象,我们有时可能难以使用动态规划,但是使用闭包会容易一些。

什么是 Python 闭包?

首先,让我使用一个简单的示例来说明什么是 Python 中的闭包。看下面的函数:

  1. def outer(): 
  2.     x = 1 
  3.     def inner(): 
  4.         print(f'x in outer function: {x}'
  5.     return inner 

在一个函数内部定义另外一个函数,并返回这个函数,这种特性就是闭包。检查 outer 函数的返回值,可以确认这是一个函数。

  1. >>> def outer(): 
  2. ...     x = 1 
  3. ...     def inner(): 
  4. ...         print(f'x in outer function: {x}'
  5. ...     return inner 
  6. ... 
  7. >>> outer 
  8. <function outer at 0x7fb2ecdac9d0> 
  9. >>> outer() 
  10. <function outer.<locals>.inner at 0x7fb2ecdaca60> 
  11. >>> 

闭包这种特性能做什么呢?因为函数返回的是一个函数,我们就可以调用这个函数,比如:

  1. >>> outer()() 
  2. in outer function: 1 
  3. >>> 

不过我们一般会这么使用闭包,这样太丑陋了。你可能会好奇这个跟递归有什么关系?别着急,让我们慢慢体会闭包的牛逼之处。

闭包内的变量访问

从前述的运行结果来看,inner 函数可以访问 outer 函数内部定义的变量 x,但是却无法修改它,下面的代码运行时会报错:

  1. >>> def outer(): 
  2. ...     x = 1 
  3. ...     def inner(): 
  4. ...         print(f'x in outer function (before modifying): {x}'
  5. ...         x += 1 
  6. ...         print(f'x in outer function (after modifying): {x}'
  7. ...     return inner 
  8. ... 
  9. >>> f = outer() 
  10. >>> f() 
  11. Traceback (most recent call last): 
  12.   File "<stdin>", line 1, in <module> 
  13.   File "<stdin>", line 4, in inner 
  14. UnboundLocalError: local variable 'x' referenced before assignment 
  15. >>> 

为了解决这个问题,我们可以加上 nonlocal 关键字,告诉 inner 函数,这不是一个本地变量:

  1. >>> def outer(): 
  2. ...     x = 1 
  3. ...     def inner(): 
  4. ...         nonlocal x 
  5. ...         print(f'x in outer function (before modifying): {x}'
  6. ...         x += 1 
  7. ...         print(f'x in outer function (after modifying): {x}'
  8. ...     return inner 
  9. ... 
  10. >>> 
  11. >>> f = outer() 
  12. >>> f() 
  13. in outer function (before modifying): 1 
  14. in outer function (after modifying): 2 
  15. >>> f() 
  16. in outer function (before modifying): 2 
  17. in outer function (after modifying): 3 
  18. >>> f() 
  19. in outer function (before modifying): 3 
  20. in outer function (after modifying): 4 
  21. >>> 

有没有发现,x 的值竟然被保存了下来,每次调用一下,就增加了 1,这就是闭包的妙处。

用闭包来替换递归

利用上述闭包会保留调用结果的特性,我们可以用这个来替换递归,比如利用闭包计算斐波那契数列:

  1. def fib(): 
  2.     x1 = 0 
  3.     x2 = 1 
  4.     def get_next_number(): 
  5.         nonlocal x1, x2 
  6.         x3 = x1 + x2 
  7.         x1, x2 = x2, x3 
  8.         return x3 
  9.     return get_next_number 

可以这样调用来生产斐波那契数列:

  1. >>> def fib(): 
  2. ...     x1 = 0 
  3. ...     x2 = 1 
  4. ...     def get_next_number(): 
  5. ...         nonlocal x1, x2 
  6. ...         x3 = x1 + x2 
  7. ...         x1, x2 = x2, x3 
  8. ...         return x3 
  9. ...     return get_next_number 
  10. ... 
  11. >>> fibonacci = fib() 
  12. >>> for i in range(2, 21): 
  13. ...     num = fibonacci() 
  14. ...     print(f'The {i}th Fibonacci number is {num}'
  15. ... 
  16. The 2th Fibonacci number is 1 
  17. The 3th Fibonacci number is 2 
  18. The 4th Fibonacci number is 3 
  19. The 5th Fibonacci number is 5 
  20. The 6th Fibonacci number is 8 
  21. The 7th Fibonacci number is 13 
  22. The 8th Fibonacci number is 21 
  23. The 9th Fibonacci number is 34 
  24. The 10th Fibonacci number is 55 
  25. The 11th Fibonacci number is 89 
  26. The 12th Fibonacci number is 144 
  27. The 13th Fibonacci number is 233 
  28. The 14th Fibonacci number is 377 
  29. The 15th Fibonacci number is 610 
  30. The 16th Fibonacci number is 987 
  31. The 17th Fibonacci number is 1597 
  32. The 18th Fibonacci number is 2584 
  33. The 19th Fibonacci number is 4181 
  34. The 20th Fibonacci number is 6765 
  35. >>> 

而使用递归方法计算斐波那契数列的方法如下所示:

  1. def fib_recursion(n:int) -> int
  2.     if n <= 1: 
  3.         return n 
  4.     return fib_recursion(n-1) + fib_recursion(n-2) 

把之前的闭包版本封装一下:

  1. def fib(): 
  2.     x1 = 0 
  3.     x2 = 1 
  4.     def get_next_number(): 
  5.         nonlocal x1, x2 
  6.         x3 = x1 + x2 
  7.         x1, x2 = x2, x3 
  8.         return x3 
  9.     return get_next_number 
  10.  
  11. def fib_closure(n): 
  12.     f = fib() 
  13.     for i in range(2, n+1): 
  14.         num = f() 
  15.     return num 

这样使用 fib_closure(20) 就可以计算出结果:

  1. In [4]: fib_closure(20) 
  2. Out[4]: 6765 
  3.  
  4. In [5]: fib_recursion(20) 
  5. Out[5]: 6765 
  6.  
  7. In [6]: 

现在使用 IPython 来测试下这两者的性能:

  1. In [6]: %time fib_closure(20) 
  2. CPU times: user 10 µs, sys: 1e+03 ns, total: 11 µs 
  3. Wall time: 14.1 µs 
  4. Out[6]: 6765 
  5.  
  6. In [7]: %time fib_recursion(20) 
  7. CPU times: user 2.76 ms, sys: 15 µs, total: 2.78 ms 
  8. Wall time: 2.8 ms 
  9. Out[7]: 6765 

可以看出两差相差近 1000 倍,这还只是计算到第 20 个数的情况下,如果计算到 100,那使用递归会计算很久甚至无法计算出来。

闭包的其他用处

Python 的闭包不仅仅用于替换递归,还有很多场景可以使用闭包。比如学生成绩的分类函数:

学生成绩数据:

  1. students = { 
  2.     'Alice': 98, 
  3.     'Bob': 67, 
  4.     'Chris': 85, 
  5.     'David': 75, 
  6.     'Ella': 54, 
  7.     'Fiona': 35, 
  8.     'Grace': 69 

现在需要根据学生成绩进行分类,通常情况下我们会写多个函数来进行分类,而分类的标准又会经常变化,这时候闭包就很方便了:

  1. def make_student_classifier(lower_bound, upper_bound): 
  2.     def classify_student(exam_dict): 
  3.         return {k:v for (k,v) in exam_dict.items() if lower_bound <= v < upper_bound} 
  4.     return classify_student 
  5.  
  6. grade_A = make_student_classifier(80, 100) 
  7. grade_B = make_student_classifier(70, 80) 
  8. grade_C = make_student_classifier(50, 70) 
  9. grade_D = make_student_classifier(0, 50) 

如果分类标准变化,直接个性函数的参数即可,主要代码逻辑不变,如果想查找成绩分类为 A 的学生,只需要调用 grade_A(students) 即可:

  1. In [13]: grade_A(students) 
  2. Out[13]: {'Alice': 98, 'Chris': 85} 

闭包使用上述分类函数很容易修改且更加易读。

最后的话

本文介绍了一种称为 Python 闭包的技术。在大多数情况下,可以使用它来重写递归函数,并且在很大程度上优于后者。

实际上,从性能的角度来看,闭包可能不是某些问题的最佳解决方案,尤其是在使用动态规划的情况下。但是,闭包写起来要容易一些,比递归性能高。当我们对性能不是很敏感时,有时写动态计划会有点浪费时间,但是闭包可能就足够了。

闭包也可以用来定义一些逻辑相同但命名不同的函数,比如本文中的分类函数,在这些情况下,它更加整洁而优雅,更加易读。

下次试试闭包吧,别用效率低下的递归了。

本文转载自微信公众号「Python七号」,可以通过以下二维码关注。转载本文请联系Python七号公众号。

 

责任编辑:武晓燕 来源: Python七号
相关推荐

2020-04-08 17:53:40

TypeScriptJavaScript代码

2021-09-09 21:10:23

Lite-XL编辑器Lua

2024-01-22 09:51:32

Swift闭包表达式尾随闭包

2021-02-21 16:21:19

JavaScript闭包前端

2021-01-07 10:15:55

开发 Java开源

2021-05-11 07:10:18

标准库DjangoOS

2016-10-27 19:26:47

Javascript闭包

2013-05-02 09:44:57

PHP闭包

2019-11-07 21:51:18

闭包前端函数

2009-07-22 07:43:00

Scala闭包

2011-05-25 14:48:33

Javascript闭包

2020-10-14 15:15:28

JavaScript(

2023-11-02 08:53:26

闭包Python

2019-12-20 11:17:35

SerialVersionUID

2016-09-14 09:20:05

JavaScript闭包Web

2022-10-24 08:08:27

闭包编译器

2009-05-13 14:15:09

PHP 5.3闭包匿名函数

2009-07-24 17:30:37

Javascript闭

2017-09-14 13:55:57

JavaScript

2020-04-03 14:25:55

diff Meld工具
点赞
收藏

51CTO技术栈公众号