目录python
函数的执行须要对函数进行压栈的,什么是压栈呢,简而言之就是在函数执行时在栈中建立栈帧存放须要变量以及指针的意思。具体涉及的知识很是多,这里就已一个Python脚本简单进行分析。闭包
def foo1(b, b1=3): print('call foo1', b, b1) def foo2(c): foo3(c) print('call foo2', c) def foo3(d): print('call foo3', d) def main(): print('call main') foo1(100, 101) foo2(20) print('main ending') main()
当咱们运行上面代码时,它的执行流程以下:async
Python 代码先被编译为字节码后,再由Python虚拟机来执行字节码, Python的字节码是一种相似汇编指令的中间语言, 一个Python语句会对应若干字节码指令,虚拟机一条一条执行字节码指令, 从而完成程序执行。Python dis 模块
支持对Python代码进行反汇编, 生成字节码指令。下面针对上面的例子经过字节码理解函数调用时的过程。函数
import dis print(dis.dis(main)) # ======> result 53 0 LOAD_GLOBAL 0 (print) 2 LOAD_CONST 1 ('call main') 4 CALL_FUNCTION 1 6 POP_TOP 54 8 LOAD_GLOBAL 1 (foo1) 10 LOAD_CONST 2 (100) 12 LOAD_CONST 3 (101) 14 CALL_FUNCTION 2 16 POP_TOP 55 18 LOAD_GLOBAL 2 (foo2) 20 LOAD_CONST 4 (20) 22 CALL_FUNCTION 1 24 POP_TOP 56 26 LOAD_GLOBAL 0 (print) 28 LOAD_CONST 5 ('main ending') 30 CALL_FUNCTION 1 32 POP_TOP 34 LOAD_CONST 0 (None) 36 RETURN_VALUE
字节码含义:性能
LOAD_GLOBAL
:加载全局函数(print)LOAD_CONST
: 加载常量CALL_FUNCTION
: 函数调用POP_TOP
:弹出栈顶RETURN_VALUE
: 返回值def outer(): c = 100 def inner(): nonlocal c c += 200 return c return inner a = outer() a()
执行完后,删除栈空间,可是因为outer返回了内部函数inner,但并无执行,因此不会继续压栈,当执行a的时候,会从新压栈,而此时内部函数已经记住了外部自由变量c,而且每次调用outer都会从新生成一个inner。测试
注意:这种状况叫作闭包,自由变量c会被当成内部函数inner的一个属性,被调用。优化
PS:内存两大区域(栈,堆)。垃圾回收,清理的是堆中的空间。函数的调用就是压栈的过程,而变量的建立都是在堆中完成的。 栈中存储的都是堆中的内存地址的指向,栈清空,并不会使堆中的对象被清除,只是指向已经被删除。函数,变量都是在堆内建立的,函数调用须要压栈
线程
函数直接或者间接的调用自身就叫递归,递归须要有边界条件、递归前进段、递归返回段,当边界条件不知足的时候,递归前进,当边界条件知足时,递归返回。注意:递归必定要有边界条件,不然可能会形成内存溢出。指针
前面咱们学过斐波那契序列,利用递归函数,咱们能够更简洁的编写一个计算斐波那契序列第N项,或者前N项的代码:代码规范
在数学上,斐波纳契数列以以下被以递推的方法定义:
F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
# 公式版本 def fib(n): if n < 3: return 1 return fib(n-1) + fib(n-2) # 公式版本之简洁版 def fib(n): return 1 if n < 3 else fib(n-1) + fib(n-2)
不少人可能不明白其中原理,这里简要说明一下,以fib(6)为例子:
递归的要求:
递归调用的深度不宜过深,Python对递归的深度作了限制,以保护解释器,超过递归深度限制,则抛出RecursionError
异常。
使用
sys.getrecursionlimit()
获取当前解释器限制的最大递归深度
因为Python是预先计算等式右边的,因此咱们发现,上图中,重复计算了fib(4)
和fib(3)
那么效率呢?因为只是计算了fib(6),若是fib(35)
呢?能够预想,它要重复计算多少次啊。这里咱们来测试一下它执行的时间。
# 递归版本 import datetime def fib(n): return 1 if n < 3 else fib(n - 2) + fib(n - 1) start = datetime.datetime.now() fib(35) total_seconds = (datetime.datetime.now() - start).total_seconds() print(total_seconds) # 1.628643 # 循环版本 def fib(n): a = 1 b = 1 count = 2 while count < n: a, b = b, a + b count += 1 return b start = datetime.datetime.now() print(fib(35)) total_seconds = (datetime.datetime.now() - start).total_seconds() print(total_seconds) # 0.0
通过对比,咱们发现使用递归虽然代码更优美了,可是运行时间还不如咱们的普通循环的版本,这是由于递归重复计算了不少次,当规模到达必定程度时,那么这个时间是成指数递增的的。
总结一下如今的问题:
递归复杂
,函数反复压栈
,栈内存就很快溢出了。如何优化呢?前面的版本使用递归函数时会重复计算一些相同的数据,那么咱们来改进一下,在代码层面对递归的特性进行优化。
def fib(n, a=1, b=1): a, b = b, a + b if n < 3: return b return fib(n - 1, a, b)
代码优化后,发现运行时间很快,由于计算的是fib(n),fib(n-1)..fib(1)
并无进行重复计算,因此要使用递归,必需要考虑重复计算以及函数递归调用时产生的内存浪费等。
间接递归,就是经过别的函数,来调用函数自己,下面来看一个例子,来理解间接递归的概念:
def foo1(): foo2() def foo2(): foo1() foo1()
咱们能够看到,这种递归调用是很是危险的,可是每每这种状况在代码复杂的状况下,仍是可能发生这种调用。要用代码规范来避免这种递归调用的发生。
递归是一种很天然的表达,符合逻辑思惟:
能不用递归则不用递归
。 没有名字的函数,在Python中被称为匿名函数,考虑一下,咱们以前都是经过def语句定义函数的名字开始定义一个函数的,那么没有名字改如何定义?没有名字该如何调用呢?
Python中借助lambda表达式构建匿名函数。它的格式为:
lambda '参数列表':'返回值' # 等于: def xxx(参数列表): return 返回值
须要注意的是:
下面来看一下各类匿名函数的写法
(lambda x,y: x + y)(4,5) # 9 (lambda x,y=10: x+y)(10) # 20 (lambda x,y=10: x+y)(x=10) # 20 (lambda x,y=10: x+y)(10,y=10) # 20 (lambda x,y=10,*args: x+y)(10,y=10) # 20 (lambda x,y=10,*args,m,n,**kwargs: x+y)(10,y=10) # 20 (lambda *args:(i for i in args)(1,2,3,4,5) # generate<1,2,3,4,5> (lambda *args:(i for i i in args))(*range(5)) # generate<1,2,3,4,5> [ x for x in (lambda *args: (i for i in args))(*range(5)) ] # [1,2,3,4,5] [ x for x in (lambda *args: map(lambda x:x+1,(i for i in args)))(*range(5))] # [2,3,4,5,6]
还记得,咱们以前的默认值字典吗,这里的:
d = defaultdict(lambda :0)
,其实就等于(lambda :0)()
,即当咱们传入任意值时都返回0
生成器指的生成器对象,能够由生成器表达式获得,也可使用yield关键字获得一个生成器函数,调用这个函数返回一个生成器对象。
生成器函数,函数体中包含yield关键字的函数,就是生成器函数,调用后返回生成器对象。关于生成器对象,咱们能够理解它就是一个可迭代对象
,是一个迭代器
,只不过它是延迟计算
的,惰性求值
的。
咱们说在函数中使用yield关键字来返回数据的函数,叫作生成器函数,那么咱们来写一个生成器函数,看看和return函数有什么区别
In [87]: def func(): ...: for i in range(2): ...: yield i ...: In [90]: g = func() In [91]: next(g) Out[91]: 0 In [92]: next(g) Out[92]: 1 In [93]: next(g) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-93-e734f8aca5ac> in <module> ----> 1 next(g) StopIteration:
这个报错看起来是否是很熟悉?没错,和生成器表达式的结果是相同的,只不过生成器函数能够写的更加的复杂,如今咱们来看下生成器函数的执行过程。
再次执行时会执行到下一个yield语句
yield关键字``,和return关键字
在生成器场景下,不能一块儿使用
。由于return语句会致使当前函数当即返回,没法继续执行,也没法继续获取下一个值,而且return语句的返回值也不能被获取到,还会产生StopIteration的异常.
再来总结一下生成器的特色:
咱们想要生成一个无限天然数的序列时,生成器就是一个很好的方式
def counter(): c = 0 while True: c += 1 yield c c = counter() In [95]: next(c) Out[95]: 1 In [96]: next(c) Out[96]: 2 In [97]: next(c) Out[97]: 3
又或者前面的斐波那契序列,咱们也能够利用生成器的特色,惰性计算。
def fib(n, a=0, b=1): for i in range(n): yield b a, b = b, a + b print(list(fib(5)))
或者包含全部斐波那契序列的生成器
def fib(): a = 0 b = 1 while True: yield b a, b = b, a + b g = fib() for i in range(101): print(next(g))
协程是生成器的一种高级方法,比进程、线程更轻量级,是在用户空间调度函数的一种实现,Python 3 的asyncio就是协程实现,已经加入到了标准库中,Python 3.5 使用async、await关键字直接原生支持写成。协程在现阶段来讲比较复杂,后面会详细进行说明,这里提一下实现思路:
还能够引入调度的策略来实现切换的方式
协程是一种非抢占式调度
在Python 3.3之后,出现了yield from语法糖。它的用法是
def counter(): yield from range(10)
yield from iterable
实际上等同于 for item in iterable: yield item
固然yield from也能够结合生成器来使用,由于生成器也是一个可迭代对象啊。
def fib(n): a = 0 b = 1 for i in range(n): yield b a,b = b,a+b def counter(): yield from fib(10) g = counter() print(list(g))
生成器包生成器,真的是有够懒的了!