1-Python - 递归与面向过程编程
递归
我们来聊点函数部分的高级话题,递归调用。
递归调用与无限递归
程序在调用一个函数的过程中,直接或者间接调用了该函数本身,我们称为递归调用:
def recursion():
print('recursion function')
recursion()
recursion() # RecursionError: maximum recursion depth exceeded while calling a Python object
def foo():
print('foo function')
bar()
def bar():
print('bar function')
foo()
foo() # RecursionError: maximum recursion depth exceeded while calling a Python object
上例中,recursion函数在执行打印任务后又再直接次调用自己,再次执行打印任务,然后再次调用自己,这样无休止的打印、调用自己。而foo和bar函数,在foo函数中,调用bar函数,在bar函数中调用foo函数,这样称为间接调用自己。而无论哪种调用,函数执行最后都报了同样的错误RecursionError
。Python解释器自动终止了函数的无休止的调用。这种递归也称为无限递归(死递归)。而Python解释器为了阻止这种死递归,设置了递归深度。也就是限制了递归的次数。如下示例:
import sys
print(sys.getrecursionlimit()) # 1000
sys.setrecursionlimit(1100) # 手动设置递归深度
print(sys.getrecursionlimit()) # 1100
通过sys模块在第2行获取Python解释器默认的递归深度(根据平台不同数值可能有所出入,比如997),通过第3行手动设置递归深度并立即生效(第4行)。再通过一些例子来学习一下递归函数的应用。
递归的应用
计算1+2+3+…+x
的和
def mySum(x):
if x == 1: # 递归结束条件
return 1
return mySum(x - 1) + x
print(mySum(5)) # 15
'''
mySum(5)
mySum(4) + 5 15
mySum(3) + 4 10
mySum(2) + 3 6
mySum(1) + 2 3
1
'''
上例中,第6行执行mySum递归函数并传递参数x,第2行为递归设立结束条件,第4行开始进行递归循环。而循环过程正如第8行开始执行递归到最深层第12行,当变量x为1的时候,结束递归,开始执行第4行的return计算,而计算过程就是从第12行往上执行到了第8行。计算过程是,在递归结束时返回了1,而1就是第12行的mySum函数这一层递归时返回的结果为1,加上这一层的变量x,结果为3。而3则是第11行mySum执行结果,再加上当前层变量值为6,就这样,当前层的执行结果为上一层的函数执行结果,加上当前层的变量值,直到第9行,得到最终的结果15。
上面的例子还可以有另一种更加优美的写法——三元表达式:
def mySum(x):
return 1 if x == 1 else mySum(x - 1) + x
print(mySum(5)) # 15
但有时,递归更偏技巧一些,不如循环语句自然:
def whileSum(x):
sum = 0
i = 1
while i <= x:
sum += i
i = i + 1
return sum
print(whileSum(5)) # 15
def forSum(x):
sum = 0
for i in range(1, x + 1):
sum += i
return sum
print(forSum(5)) # 15
最后对递归做个总结:
-
递归必须有明确的结束条件,避免陷入死递归。
-
递归使代码更加整洁、优美。
-
递归调用过程晦涩难懂。
-
递归非常的占用内存空间,因为每层递归都保留当前的层的状态信息,也会造成递归的时间成本更高,效率低下。
面向过程编程
我们通过一个简单的计算器来了解面向过程编程:
import re
def cal_mini_exp(mini):
'''
乘除计算
'''
if '*' in mini:
val1, val2 = mini.split('*')
return str(float(val1) * float(val2)) # 为了后面的替换,在这里把int转为str
elif '/' in mini:
val1, val2 = mini.split('/')
return str(float(val1) / float(val2))
def dealwith(exp):
'''
整理表达式内的符号
'''
return exp.replace('--', '+').replace('+-', '-').replace('-+', '-').replace('++', '+')
def calculate(son_exp):
'''
计算没有括号的表达式
'''
son_exp = son_exp.strip('()')
while 1: # 完成了表达式中乘除法的计算
ret = re.search('\d+\.?\d*[*/]-?\d+\.?\d*', son_exp)
if ret:
mini_exp = ret.group()
res = cal_mini_exp(mini_exp) # 乘除计算结果并返回结果
son_exp = son_exp.replace(mini_exp, res, 1)
else:
break
son_exp = dealwith(son_exp) # 整理那些加加减减去重 3-+1--2之类的
# 最后的加减法计算
res = re.findall('[+-]?\d+\.?\d*', son_exp)
sum = 0
for i in res:
sum += float(i)
return str(sum)
def remove_bracket(express):
'''
去括号
把内部不再有小括号的表达式匹配出来 :\([^()]+\)
'''
while 1:
ret = re.search('\([^()]+\)', express) # 是否匹配上的对象
if ret:
son_exp = ret.group() # 子表达式
# 计算,先乘除后加减
ret = calculate(son_exp)
express = express.replace(son_exp, ret, 1)
else:
break
return express
def main(express):
express = express.replace(' ', '') # 首先是去空格
express = remove_bracket(express)
ret = calculate(express)
return ret
def core():
print('输入计算的内容或输入Q退出'.center(30, '*'))
while 1:
express = '-1 + 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2) )'
# express = input('please enter: ')
# express = '1 + 1'
if express == 'Q' or express == 'q':
break
elif '/0' in express:
print('0不能为被除数')
elif express.count('(') != express.count(')') or '=' in express:
print('表达式错误,请重新输入')
else:
ret = main(express)
print('计算结果:', ret)
break
print('eval计算结果: ', eval(express))
core()
上例中。该计算器的逻辑就是core函数首先判断该字符串(可以是用户输入的)是否合法。然后交给另一个函数main执行去空格,然后交给remove_bracket函数去括号,然后取出其中最小括号内的算式,再将这个算式交给calculate函数执行计算,calculate函数再调用cal_mini_exp来计算乘除,然后调用dealwith函数来整理符号,经过一系列的处理后,calculate函数将计算结果替换到原来的字符串中。然后经过下一次循环,正则再匹配出一个最小括号内的算式,再去计算,然后在将计算结果替换到原来的字符串中。最后再算出结果。
由上面的例子可以可以看到,各函数相互依赖,每个函数都负责简单的任务,一起完成大的功能。这种编程方式为函数式编程,函数式编程的思想为面向过程的程序思想(Procedure Oriented Programming,简称POP),即将一组函数按照规定顺序执行,为了简化程序设计难度,面向过程的编程思想是把大的功能拆分为小的功能函数来实现。那么,面向过程思想编程有什么特点呢?
面向过程编程的优点:
-
思路清晰。
-
代码可读性高。
面向过程编程的缺点:
-
由于各功能环环相套,程序扩展性差。
-
程序的耦合性较高,导致可维护性差。
-
在整个大的功能中,其中一个小的环节出问题,可能导致整个程序的崩溃。
我们使用函数时,应该遵循以下规则:
-
不到万不得已,不要使用全局变量。由于全局变量的特点,多个函数同时使用一个全局变量时,如果处理不当,会增加程序的调试难度和降低可维护性,通常return语句就是解决依赖全局变量的有效手段。
-
解耦合性,不要轻易的改变别的模块中的变量,以免导致别的模块工作异常。
-
通常建议函数的功能应该足够精简,功能单一,一个函数完成一件事,如果一个函数有很深的嵌套,或者占了很多的代码行,这是在暗示可能函数设计的有缺陷,那么就要考虑拆分成若干的函数了。
-
函数保持简洁,尽量简单。
-
在函数中,注释是必要的。
欢迎斧正,that's all