下次别用递归了

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

今天分享 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()()
x 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 1, in <module>
  File "<stdin>", line 4, in 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()
x in outer function (before modifying): 1
x in outer function (after modifying): 2
>>> f()
x in outer function (before modifying): 2
x in outer function (after modifying): 3
>>> f()
x in outer function (before modifying): 3
x 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(2, 21):
...     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(80, 100)
grade_B = make_student_classifier(70, 80)
grade_C = make_student_classifier(50, 70)
grade_D = make_student_classifier(0, 50)

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

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

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

最后的话

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

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

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

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

推荐阅读:

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

留言讨论

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 递归是一种解决问题的方法,它通过将问题分解为更小的、相同的子问题来解决问题。在 Python 中,可以使用递归函数来实现递归处理。 递归函数是在函数内部调用自身的函数。在递归函数中,需要设置一个递归终止条件,以避免函数无限递归递归终止条件应该是可以直接得到答案的情况,例如,当问题规模缩小到一定程度时,可以直接计算出答案。 以下是一个使用递归函数实现阶乘的例子: ``` def factorial(n): if n == 1: return 1 else: return n * factorial(n-1) # 测试 print(factorial(5)) ``` 在这个例子中,`factorial` 是一个递归函数,用于计算阶乘。当 `n == 1` 时,递归终止,返回结果 1。否则,递归调用 `factorial(n-1)` 并将结果乘以 `n`,直到递归终止。 递归函数的优点是它可以使代码更加简洁和易于理解。然而,递归函数也存在一些缺点,例如,它可能会导致堆栈溢出,因为每个函数调用都需要在堆栈上分配一些内存。因此,在使用递归函数时,需要谨慎处理递归终止条件,以避免出现无限递归的情况。 ### 回答2: Python递归是一种函数调用自身的方式,用于解决可以被拆分为相同类型子问题的问题。使用递归可以简化代码,并且能够很好地解决一些数学问题,如阶乘、斐波那契数列等。 在使用递归时,需要注意以下几点: 1. 定义递归的边界条件:递归必须要有终止条件,否则会形成无限递归导致程序崩溃。例如,在计算阶乘时,边界条件可以是n=0或n=1时返回1。 2. 将问题拆分为子问题:递归的过程中,需要将原问题拆分为一个或多个同类型的子问题,并逐个解决子问题。例如,在计算斐波那契数列时,可以将n的计算分为计算n-1和n-2的子问题。 3. 适当地调用自身:在求解子问题时,需要调用自身来解决较小规模的问题。递归函数的调用需要满足问题规模逐渐变小的条件,否则可能导致栈溢出。例如,计算斐波那契数列时,可以使用递归公式F(n) = F(n-1) + F(n-2)来逐步求解。 递归的优点在于可以使代码更加简洁且易于理解,但同时也有一定的性能损耗。大量的递归调用会导致函数堆栈的不断增长,可能会消耗较多的内存和时间。 因此,在使用递归时需要注意边界条件和递归的终止条件,避免出现无限递归的情况。此外,也可以考虑使用非递归的方式来实现相同的功能,以提高代码的执行效率。 ### 回答3: Python递归是一种函数调用自身的技术。递归处理是指在解决问题时,使用函数自身来进行多次循环,从而达到简化程序的目的。 递归处理的优点是可以简化代码并解决一些复杂的问题。例如,可以使用递归来计算斐波那契数列,即每个数是前两个数的和。通过使用递归,可以将问题简化为计算前两个斐波那契数列的和,并不断地重复这个过程直到得到所需的数。 然而,递归也有一些缺点。由于递归调用自身,会导致函数多次被调用,从而可能引起内存溢出的问题。此外,递归的运行速度通常较慢,因为每次调用都需要保存当前执行状态并在下次调用时恢复。 在编写递归函数时,需要考虑以下几个要点。首先,确定基本情况,即停止递归的条件。其次,将问题分解为更小的子问题,并使用递归调用解决这些子问题。最后,确保每次递归调用都朝着基本情况靠近,避免出现无限循环的情况。 总之,递归处理是一种强大的技术,可以简化编程过程并解决一些复杂的问题。然而,需要注意在编写递归函数时避免出现内存溢出和无限循环的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值