下次别用递归了

Python七号

共 9235字,需浏览 19分钟

 ·

2021-03-05 19:08

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

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

什么是 Python 闭包?

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

def outer():
    x = 1
    def inner():
        print(f'x in outer function: {x}')
    return inner

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

>>> def outer():
...     x = 1
...     def inner():
...         print(f'x in outer function: {x}')
...     return inner
...
>>> outer
<function outer at 0x7fb2ecdac9d0>
>>> outer()
<function outer.<locals>.inner at 0x7fb2ecdaca60>
>>>

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

>>> outer()()
in outer function: 1
>>>

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

闭包内的变量访问

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

>>> def outer():
...     x = 1
...     def inner():
...         print(f'x in outer function (before modifying): {x}')
...         x += 1
...         print(f'x in outer function (after modifying): {x}')
...     return inner
...
>>> f = outer()
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1in <module>
  File "<stdin>", line 4in inner
UnboundLocalError: local variable 'x' referenced before assignment
>>>

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

>>> def outer():
...     x = 1
...     def inner():
...         nonlocal x
...         print(f'x in outer function (before modifying): {x}')
...         x += 1
...         print(f'x in outer function (after modifying): {x}')
...     return inner
...
>>>
>>> f = outer()
>>> f()
in outer function (before modifying): 1
in outer function (after modifying): 2
>>> f()
in outer function (before modifying): 2
in outer function (after modifying): 3
>>> f()
in outer function (before modifying): 3
in outer function (after modifying): 4
>>>

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

用闭包来替换递归

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

def fib():
    x1 = 0
    x2 = 1
    def get_next_number():
        nonlocal x1, x2
        x3 = x1 + x2
        x1, x2 = x2, x3
        return x3
    return get_next_number

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

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

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

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

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


def fib():
    x1 = 0
    x2 = 1
    def get_next_number():
        nonlocal x1, x2
        x3 = x1 + x2
        x1, x2 = x2, x3
        return x3
    return get_next_number

def fib_closure(n):
    f = fib()
    for i in range(2, n+1):
        num = f()
    return num

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

In [4]: fib_closure(20)
Out[4]: 6765

In [5]: fib_recursion(20)
Out[5]: 6765

In [6]:

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

In [6]: %time fib_closure(20)
CPU times: user 10 µs, sys: 1e+03 ns, total: 11 µs
Wall time: 14.1 µs
Out[6]: 6765

In [7]: %time fib_recursion(20)
CPU times: user 2.76 ms, sys: 15 µs, total: 2.78 ms
Wall time: 2.8 ms
Out[7]: 6765

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

闭包的其他用处

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

学生成绩数据:

students = {
    'Alice'98,
    'Bob'67,
    'Chris'85,
    'David'75,
    'Ella'54,
    'Fiona'35,
    'Grace'69
}

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

def make_student_classifier(lower_bound, upper_bound):
    def classify_student(exam_dict):
        return {k:v for (k,v) in exam_dict.items() if lower_bound <= v < upper_bound}
    return classify_student

grade_A = make_student_classifier(80100)
grade_B = make_student_classifier(7080)
grade_C = make_student_classifier(5070)
grade_D = make_student_classifier(050)

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

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

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

最后的话

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

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

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

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

推荐阅读:

学会这个,Python的递归再也不慢了

留言讨论


浏览 9
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报