不要在 Python 中使用递归了!

欢迎关注 “小白玩转Python”,发现更多 “有趣”

引言

小编以前就是一个非常喜欢递归函数的程序员,仅仅因为它非常酷,可以用来炫耀编程技巧和智慧。然而在大多数情况下,递归函数具有非常高的复杂性,我们应该避免使用。

更好的解决方案之一是在可能的情况下使用动态计划,这可能是解决可分为子问题的问题的最佳方法。

但是,在本文中,我将介绍 Python 中的另一种技术,它可以作为递归函数的一种替代方法。它不会超越动态规划,但是从思考的角度来看更容易。换句话说,我们可能有时会因为抽象的概念而难以使动态规划,但是使用闭包会容易得多。

什么是python闭包

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

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

函数 outer 是用函数 inner 定义的,而 outer 函数返回 inner 函数作为函数的“返回值”。

在这种情况下,这个嵌套函数在 Python 中被称为闭包。如果我们检查 outer 函数的“返回值”,我们会发现返回的值是一个函数。

闭包能做什么? 因为它返回了一个函数,我们当然可以运行这个函数。

我们可以看到,inner 函数可以访问 outer 函数中定义的变量。通常,我们不会像上面那样使用闭包,因为它有点丑陋。我们通常需要定义另一个函数来保存闭包返回的函数。

因此,我们也可以说,在 Python 闭包中,我们定义了一个函数的函数。

从内部函数访问外部变量

那么我们如何使用闭包来代替递归呢?不要着急。让我们看看这里的另一个问题,从内部函数访问外部变量。

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

在上面显示的闭包中,我们希望在 inner 函数中对 outer 函数定义的x进行+1操作。然而,这似乎出了一些问题。

默认情况下,您不能从内部函数访问外部变量。然而,就像我们如何在 Python 中定义全局变量一样,我们可以通过使用非局部关键字来告诉闭包的内部函数,不应该将变量视为“局部变量”。

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

现在,假设我们想把变量 x 乘以1,乘以5。我们可以简单地编写一个 for 循环来实现这一点。

f = outer()
for i in range(5):
    print(f'Run {i+1}')
    f()
    print('\n')

使用闭包编写斐波那契函数

Fibonacci 通常用作递归函数的“ hello world”示例。

斐波那契序列是一系列数字,每个数字都是它前面两个数字的和。前两个数字 X0和 X1是特殊的,它们分别是0和1。X2 是 X0和 X1的和,所以 X2 = 1。然后,X3是 X1 + X2 = 2,X4是 X2 + X3 = 3,X5是 X3 + X4 = 5,等等。

递归函数要求我们从“当前场景”到“以前的场景”反向思考,最终考虑终止条件是什么。但是,通过使用闭包,我们可以更自然地思考这个问题。

参见下面的代码,该斐波那契函数是使用闭包实现的。

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

我们知道斐波那契以两个特殊的数字 X0 = 0和 X1 = 1开始,所以我们只需在外部函数中简单地定义它们。然后,内部函数 get_next_number 是简单地返回它从外部函数得到的两个数字之和。

另外,不要忘记用 X1和 X2更新 X0和 X1。事实上,我们可以简化代码:

...
x3 = x1 + x2
x1, x2 = x2, x3
return x3

x0, x1 = x1, x0 + x1
return x1

这是首先更新两个变量,然后返回第二个变量,这与前面的代码片段是等效的。

然后,我们可以使用这个闭包来计算斐波那契数。例如,我们想要显示直到第20个数字的斐波那契序列。

fibonacci = fib()
for i in range(2, 21):
    num = fibonacci()
    print(f'The {i}th Fibonacci number is {num}')

性能比较

好了,现在知道我们可以使用闭包替换上一部分中的递归函数。性能如何?让我们比较一下!

首先,让我们使用递归函数实现斐波那契函数。

def fib_recursion(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib_recursion(n-1) + fib_recursion(n-2)

我们可以通过输出斐波那契数列的第20个数来验证这个函数。

然后,让我们将闭包版本嵌入到一个函数中,以便进行比较。

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

现在,让我们比较一下速度。

2.79 ms VS 2.75 μs.闭包方法比递归方法快1000倍!最直观的原因是,每个递归级别的所有临时值都分别存储在内存中,但闭包实际上是在每个循环中更新相同的变量。

此外,递归还有一个深度限制。对于闭包,因为它基本上是一个 For 循环,所以不存在任何约束。

这里有一个获得第1000个斐波那契数列的例子

这的确是一个庞大的数字,但闭包方法可以在大约100 μs 内完成计算,而递归函数却被限制了。

闭包的其他用例

Python 闭包不仅对于替换递归函数非常有用。在某些情况下,它还可以用更整洁的解决方案替换 Python 类,特别是类中没有太多的属性和方法。

假设我们有一个带有考试成绩的学生词典。

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

我们希望有几个功能,帮助我们过滤学生的分数,把他们分成不同的年级班。然而,标准可能会随着时间的推移而改变。在这种情况下,我们可以如下定义一个 Python 闭包:

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 类的下限和上限,闭包将返回执行此操作的函数。

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)

上面的代码将给我们4个函数,它将根据我们给出的边界将学生分类到相应的年级班级。请注意,我们可以随时改变边界作出另一个函数。

现在让我们验证一下函数。

非常简洁! 请记住,当情况更复杂时,我们仍然需要定义类。

总结

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

实际上,从性能角度来看,关闭可能不是解决某些问题的最佳方案,特别是在动态规划适用的情况下。当我们对性能不是很敏感时,动态规划有点过了,但是闭包可能已经足够了,简单又方便!

闭包也可以用来替换一些我们可能想要定义一个类来满足的用例。在这种情况下,它更加整洁和优雅。

·  END  ·

HAPPY LIFE

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值