基本原理
您正在使这对自己更具挑战性,因为您将 sum 函数与类纠缠在一起。 sum 函数只需要处理一个列表,但对象的上下文 self 让您难以集中注意力。
递归是一种函数式遗产,这意味着以函数式和模块化的方式编写代码。它更容易读/写小的、单一用途的函数,它促进了程序其他区域的重用。下次需要对另一个类的列表求和时,要不要再重写sumRecursive?
编写一次求和函数,然后在需要的地方import -
# mymath.py
def mysum(t):
if not t:
return 0
else:
return t.pop() + mysum(t)
看看mysum 是如何不关心上下文的,self?为什么要呢?它所做的一切都是对列表的元素求和。
现在编写您的 recursive 模块 -
# recursive.py
from mymath import mysum
class Recursive:
def __init__(self, arr): self.arr = arr
def sum(self): return mysum(self.arr)
看看Recursive.sum 是如何将self.arr 交给mysum 的?它不必比这更复杂。 mysum 将适用于每个列表,而不仅仅是 Recursive 模块中的列表。我们不必知道mysum 是如何工作的,这是mymath 模块所关心的。
现在我们编写main 模块。每个模块代表一个抽象的障碍。这意味着一个模块的细节不应该溢出到其他模块中。我们不知道Recursive 实际上是如何对输入求和的,从调用者的角度来看,我们不在乎。这就是Recursive模块的关注点。
# main.py
from recursive import Recursive
recur = Recursive([1,2,3])
print(recur.sum())
6
正常运行
上面我们在您的问题中使用.pop 技术编写了mysum。我这样做是因为这似乎是您现在理解问题的方式。但是请注意当我们这样做时会发生什么 -
x = [1,2,3]
print(mysum(x)) # 6
print(mysum(x)) # 0
为什么mysum 第二次返回不同的答案?因为正如现在所写,mysum 使用了t.pop(),它变异 t。当mysum 运行完毕后,t 就完全清空了!
为什么我们要这样写我们的函数?如果5 + x 每次调用都返回不同的结果怎么办?
x = 3
print(5 + x) # 8
print(5 + x) # 8
如果我们不能依赖不改变的价值观,那该有多烦人。输入的总和,[1,2,3],是6。但正如它所写,输入的总和是返回6 和清空输入。清空(更改)输入的第二部分称为副作用。即,期望的效果是求和并清空输入列表是无意的,但我们使用.pop 来计算结果的结果。这不是功能方式。函数式风格意味着避免突变、变量重新分配和其他副作用。
def mysum(t):
if not t:
return 0
else:
return t[0] + mysum(t[1:])
以这种方式编写时,t 不会更改。这使我们可以使用equational reasoning,从而我们可以将任何函数调用替换为其返回值,并且总是得到正确的答案
x = [1,2,3]
mysum(x)
== 1 + mysum([2,3])
== 1 + 2 + mysum([3])
== 1 + 2 + 3 + mysum([])
== 1 + 2 + 3 + 0
== 1 + 2 + 3
== 1 + 5
== 6
并且x 没有因为运行mysum 而被更改-
print(x)
# [1,2,3]
请注意,Recursive 模块无需进行任何更改即可获得以这种方式重写 mysum 的好处。在更改之前,我们会看到这种行为 -
# main.py
from recursive import Recursive
recur = Recursive([1,2,3])
print(recur.sum())
print(recur.sum())
6
0
因为对sum 的第一次调用将self.arr 传递给mysum,这会清空self.arr 作为副作用。对recur.sum() 的第二次调用将得出一个空的self.arr!修复 mysum 后,我们得到了预期的行为 -
# main.py
from recursive import Recursive
recur = Recursive([1,2,3])
print(recur.sum())
print(recur.sum())
6
6
补充阅读
我已经写了大量关于此答案中使用的技术的文章。按照链接查看它们在其他上下文中的使用,并提供了额外的解释 -