Python-2.函数(进阶篇)

前言

上节记录了一些函数的基础,如函数的参数,函数的返回值,参数结构,函数的作用域等概念

这节说下函数的几个特性,比如闭包,应用传递,函数执行流程,生成器

下节课说下高阶函数和装饰器

闭包

很多无编程基础的小伙伴在刚学python时,说到闭包概念就开始晕(包括我),从而搞不懂后面的高阶函数,装饰圈等概念。

看看下面写的内容希望有帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#上一篇我们知道作用域的概念LEGB,下面我们来看个代码
def outer():
y = 10
def inner(): #inner是一个内部函数
print(y) #inner函数调用y(这个变量是上级变量E)
return inner
outer()()
#这个其实就是一个闭包啦。

#什么是闭包:
#条件一:函数体内部有个函数。
#条件二:并且内部函数如果调用上级变量
#满足上面两个条件就是一个闭包啦。


#上面为什么使用outter()()来调用?
def outer():
y = 10
def inner():
print(y)
return inner #这里只是返回一个函数,并没有调用
f = outer() #这个只是调用outer函数,outer函数返回inner函数
f() #这里才是调用inner函数,注意这里是调用
#所以使用outter()()需要两个括号来调用inner()函数

#那么可能会说为什么使用return inner()在函数内部调用?
def outer():
y = 10
def inner():
print(y)
return inner()
outer()
#这样也能将y显示出来

#想个问题,如果当我们写一个程序,每当执行这个程序都会计数加1,如何实现?
def time():
global x
x = 0
def func():
global x
x += 1
return x
return func()
f = time()
f
out>1
f
out>1
#为什么这样?
#当调用time()函数的时候返回是func()的返回值(return x)
#这里只是将0+1返回了,所以一直都是1.

def time():
global x
x = 0
def func():
global x
x += 1
return x
return func
f = time()
f()
out>1
f()
out>2
#这时候才实现了当调用函数时候累计加1,
for _ in range(10):
print(f())
#返回1,2,3,..12
x = 100
f()
out>101
#为什么会这样,因为使用了global,
#当执行f()就是直接调用了函数内部的func()函数.global就是100,然后x+=1就是101
#所以说了,在使用global时候一定要知道干了什么。


#改如何优化呢?
#python2中
def counter():
x = [0]
def inc():
x[0]+=1
return x[0]
return inc
c = counter()
c()
out>1
c()
out>2
x = 100
c()
out>3
####这是在python2中实现闭包的唯一方式

#python3中
def count():
x = 0
def inc():
nonlocal x
x += 1
return x
return inc
c = count()
c()
out>1
c()
out>2
x = 100
c()
out>3

#可以看出nonlocal关键字用于表示一个变量由上级作用于定义,通过nonlocal标记变量,对上级可读可写

##如果上级没有定义(函数体内的上级)会抛出SyntaxError语法错误,
x = 1
def fn():
nonlocal x
x += 1
return x
>>>SyntaxError

小结

闭包:函数内部有函数,并且该函数调用上级函数变量,函数已经接受,但是函数部分变量引用还在这就是闭包。

引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def fn(x=[]):
x.append(1)
print(x)
fn() #打印[1]
fn() #打印[1,1]
#为什么会这样?
#这里的x.append(1)只是增加,并不是赋值
#python的参数是通过值来传递的,如果变量是是可变对象,如列表,在返回到调用程序后,对象会出现被修改状态

fn.__defaults__
out>([1,1],)
#fn的值都存在defaults下,是个元祖类型,不可变


#在python中一切皆对象
#函数也是对象,参数是函数对象的属性,函数参数的作用域会伴随整个生命周期,如果函数存在则函数参数也存在

#对于定义全局作用域的函数: 重新定义,del,程序退出时
#对于定义局部作用域的函数: 重新定义,del,上级作用域被销毁
#当使用可变类型作为参数默认值的时候需要注意


#在看个例子
def fn(x=1,y=2):
x = 3 #在函数体内,赋值即定义
y = 4
return x,y
fn.__defaults__
out>(1,2)

#如何使 函数体内 数字是不可变类型?
#使用不可变类型作为函数值,如元祖
#函数体内不改变默认值,这里可以在函数体加个判断

#实例
def fn(lst=[]):
lst = lst[:] #相当于拷贝(影子拷贝)
lst.append(1)
print(lst)
fn.__defaults__
out>([],)
fn() #返回1
fn([1,2]) #返回1,2,1 1,2传到lst,然后在添加了一个append

#实例2
def fn(lst=None):
if lst is None:
lst = []
else:
lse = [:]
lst.append(1)
print(lst)
fn.__defaults__
out>(None,)
fn() #打印1
fn.__defaults__
out>(None,)
#通常如果使用可变类型作为默认参数时候会使用None来代替.

#通常 值保持在栈空间,引用是在堆空间

函数执行流程

函数执行过程,函数执行过程可视化地址

在内存空间的划分:

1
2
3
4
5
6
> *堆		随机访问,用于保存数据,变量
> *栈 先进后出,用于保存现场
> *指令 顺序访问,用于存储程序指令
> *静态区
> *保留区
>

堆(heap),队列优先,先进先出(FIFO-First in First out)

栈(stack),先进后出(FILO-First-in/Last-out)

​ #先入后弹出,后入先弹出。类似弹夹,先放进去的最后出来,最后放进来的先出去

假设程序是单进程,单执行流,在某一时刻,能运行的程序只能有一个,但函数调用会打开新的执行上下文,因此为了确保main函数可以恢复现场

在main函数调用其他函数时,需要先把main现场保存下来,放一边,即栈中,这时候被调用的函数即可执行,且完成后可加到调用者,’回到’调用

者main,main可以继续向前运行.

1
2
3
4
5
6
7
8
9
10
执行流程:
main #主流程认为是全局作用域
f1() #进入f1函数 开启新的作用域,单核cpu只能执行一个指令(单核),需要保存现场(压栈),执行f1完成后,还原现场
f2()
f3()
f4()
main
#当调用函数的时候,解释器会把当前的现场压栈,压栈完成后开始执行被调函数,被调函数执行完成后,解释器弹出当前栈顶恢复现场,没次调用函数都有压栈,出栈操作。
#压栈的时候叫保存现场,会保存当前各种变量的地址
#出栈的时候叫恢复现场,也是恢复当前各种变量的地址

注:严格来说 引用真正的数据保存在堆里面,数据的引用地址保存在栈里面。

所有语言都是这么实现的

递归函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#什么是递归函数?
#递归函数总是涉及到压栈和出栈的过程,一直到有退出条件才出栈
#函数内部调用自身,递归必须有退出条件,否则会出现死循环,


#求个阶加
#1+2+3+4+5+6+N
#公式:
#(N+1)*(N)/2
#这里不需要调用函数本身
def sum(x):
return (x+1)*x/2
sum(100)
out>5050.0


#求个阶乘:
#1*2*3*4*n
#公式:
#g(0)=1
#g(1)=1
#g(x)= x*g(x-1)

#代码如下
def g(x):
if x == 0 or x == 1:
return 1
return x*g(x-1)
g(10)
out>3628800

#在求个斐波那契
#1+1+2+3+5+8+13+21+N
#公式:
#f(0)=1
#f(1)=1
#f(x) = f(x-1)+f(x-2)
def f(x):
if x == 0 or x == 1:
return 1
return f(x-1)+f(x-2)
f(10)
out>89


#这样一个递归是不是很容易计算出数学中的公式呢?是不是很快捷?
#注意:一定要有退出条件,否则会成死循环。
#在python内部,为了保护解释器,python对最大递归深度有限制,默认为1000,函数返回只能调用1000次
import sys
sys.getrecursionlimit() #查看递归深度
out>1000
sys.setrecursionlimit(5000) #设置递归深度为5000.(需要注意如果设置值小于26则抛出recursionError)

#不过虽然很快捷,但是通常不会使用递归。
#因为 运行起来很慢 (计算时深度很深时要压栈直到有退出条件才出栈,这样及其消耗资源)


#拿阶乘的例子来说
#可以这样
def g(n):
ret = 1
for x in range(1,n+1):
ret *=x
return ret
g(1000)

#可以这样
def g(n):
ret = 1
for _ in range(1,n+1):
ret * = g(n-1)
return ret
g(1000)

#pyton可以完全不用递归,小问题可以使用很方便,比如树的处理
#但是:冯诺依曼模型本质就是递归,还有erlang对递归依赖很大

#在python中还可以使用生成器来取代递归。

#需要注意点
def f():
g()
def g():
f()
#当执行f()时候,解释器都会挂掉,通常很容易出现这种情况,
#这种并不是递归,但是类似递归

####生成器

在介绍生成器前,需要介绍下生成式。

生成式

列表生成式字典生成式集合生成式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#列表生成式
#如何将列表[1,2,3,4,5]每个值*2
l = [x*2 for x in [1,2,3,4,5]]
print(type(l)) #打印出<class 'list'>
l
out>[2, 4, 6, 8, 10]
#这个就是一个列表生成器
#在列表内部进行计算


#字典生成式
#字典生成式假设有10个学生,筛选出60分以下的学生
import random
stuInfo = {'student{}'.format(i):random.randint(0,100) for i in range(10)}
d = {stu:score for stu,score in stuInfo.items() if score < 60 }
print(type(d)) #打印出<class 'dict'>
d
out >{'student1': 40, 'student7': 11, 'student9': 49}
#这个就是字典生成式


#集合生成式
#看个例子
s = {x+1 for x in range(2)}
print(type(s)) #打印出 <class 'set'>
s
out>{1, 2}

#这些生成式只是一个生成式,不会改变他们的类型,效率比那些map,reduce高。


#那么有元祖生成式么?问的好。元祖生成式就是生成器啦,
#既然你这么厉害,为何不打个赏

在字典,列表,集合中创建一个生成式,但是受内存的限制,容量肯定是有限的,比如一个包含100万个元素字典或者列表,只需要访问几个元素,这样不仅仅占用很大空间,而且绝大部分空间都浪费了。

生成器

在生成式中可以通过某种方式推算出来,那就可以在循环的过程中不断去推算出后续的元素,这样就不需要直接创建完整的元素,从而省去大量空间以及空间浪费等问题。

python中这一种循环的过程推算后续的元素 叫生成器

生成器的特性就是:惰性求值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#还是看刚才的例子,将列表[1,2,3,4,5]每个值*2
#生成器实现
(x*2 for x in [1,2,3,4,5])
out>generator object <genexpr> at 0x7f5cfc54c468>
a = (x*2 for x in [1,2,3,4,5])
print(type(a))
out><class 'generator'>
#可以看出这里就是generator类型了。

#如何调用呢?
next(a) out>2
next(a) out>4
next(a) out>6
next(a) out>8
next(a) out>10
#可以看出使用next()直接调用,(在python2中,使用a.next()来调用)
next(a) 抛出异常StopIteration
#当生存器执行完成时候抛出StopIteration异常
生成器函数

什么是生成器函数?

一个函数返回的是yield,name这个函数就是生成器函数

需要next调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#实例
def gen():
yield 1
yield 2
return 3
g = gen()
g
<generator object gen at 0x7fe0086d7360>
#当调用函数时,并没有执行里面的内容
next(g)
out>1
#next调用函数时候,第一个yield 1停止
next(g)
out>2
#再次调用执行第二个yield 2停止
next(g)
StopIteration:3
#当没有yield出现时候抛出异常,并返回3
next(g)
StopIteration
#再次next调用时候,抛出异常,没有返回值

#通常带yield语句的函数我们称之为生成器函数,
#生成器函数返回值是生成器

#特性:
#1.生成器函数执行时,不会执行函数体 g = gen() 不会执行
#2.当next生成器的时候,会从当前代码执行到yield,会弹出值,并且暂停函数
#3.当next再次执行,会从上次暂停出往下执行
#4.当没有多余yield的时候抛出StopIteration异常,异常的value是函数返回值,如果没有返回值则返回空



#需要注意函数体里面如果yield和return并存
def fn():
yield 1
return 1
yield 2
f = fn()
next(f) #out>1
next(f) #StopIteration: 1
next(f) #StopIteration:
#return后的yield不会执行。return依然可以中断生成器
#next(f)始终是往下执行,不可逆序


#还需要注意的是
def fn(x):
if x == 0:
yield x
next(fn(0)) #out>0
id(fn(0)) #out>140600190793072
next(fn(0)) #out>0
id(fn(0)) #out>140600190793336

#这里并不等价
#f = fn() next(f) ====不等于===> next(fn())
#f = fn() #相当于将fn函数赋值了,每次next(f)的时候都是调用这个函数之后的
#next(fn()) #则表示每次都是重新执行该函数



#利用yield写个计时器
def count():
c = 0
while True:
c += 1
yield c
countime = count()
next(countime) #out>1
next(countime) #out>2
#思考一个问题,如何统计并发?
#单线程没有必要考虑
#如果在多线程情况下,可以加锁来统计并发

#每次要调用next方法才能计数,如何直接调用这个函数就能计时呢?

#想想看怎么办?
#调用上一个函数不就行了么
def count():
c = 0
while True:
c += 1
yield c
def counter():
c = count()
return next(c)
coun = counter()
coun #out>1
coun #out>1
#为什么这样,因为将counter赋值给了coun,然而里面定义了c = count()
#当执行coun每次都会重新赋值,每次执行coun都会从新赋值返回这个next(c)

#之前貌似遇过,我们使用闭包这个概念来进行计数
#可以改写
def count():
c = 0
while True:
c += 1
yield c
def counter():
c = count()
def func():
return next(c)
return func
c = counter()
c() #out>1
c() #out>2

#如何写在一起呢?
def counter():
def count():
c = 0
while True:
c += 1
yield c
c = count()
def func():
return next(c)
return func
coun = cunter()
coun() #out>1
coun() #out>2

#问题又来了。
#python肯定是个优雅的语言,使用lambda简写
def counter():
def count():
c = 0
while True:
c += 1
yield c
c = count()
return lambda:next(c)
coun = counter()
coun() #out>1
coun() #out>2
#关于lambda只是个匿名函数,我们下面就会说
迭代器

说到迭代器,大家可能会想到可迭代对象。

什么是可迭代对象?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
for i in [1,2,3,4,5]:
print(i)
#[1,2,3,4,5]就是一个可迭代对象,凡是可迭代对象都可以循环它

def gen(x):
for i in range(x):
yield i
g = gen(10)
for x in g:
print(x) #这里打印0~9
#for也可以循环生成器函数。

#list,tuple,dict,set,str这些都是可迭代对象。
#定义了"yield生成器函数"也可以被迭代。

#等等,这里for调用后为什么没有抛出异常?
#for循环内部做了三件事
#调用可迭代对象iter方法,返回可迭代对象
#调用迭代器的next方法
#处理Stopiteration异常
while True:
try:
x = next(g)
print('g',x)
except StopIteration as e:
print('Generator return value:',e.value)
break



#如何判断一个数据是否是可迭代对象呢?
#判断迭代器和迭代对象需要导入collections模块里面的Iterator,Iterable方法
#Iterable可迭代对象,Iterator迭代器
from collections import Iterator,Iterable
l = [1,2,3,4]
a = iter([1,2,3,4])

isinstance(l,Iterable)
out>True
isinstance(a,Iterable)
out>True
isinstance(l,Iterator) #可以看出list不是迭代器
out>False
isinstance(a,Iterator)
out>True

isinstance还可以判断是否是list,str,dict等类型
isinstance([1,2,3,4],list)
out>True
isinstance([1,2,3,4],dict)
out>False


#但list、dict、str虽然是Iterable(可迭代对象),却不是Iterator(迭代器)
#把list、dict、str这些可迭代对象 变成 迭代器 可使用iter()函数:
#例子
a = iter([1,2,3,4])
isinstance(a,Iterator)

#只要函数里面定义了yield那么该函数就是迭代器
#例子
def fn():
yield 1
isinstance(fn(),Iterator)
isinstance(fn(),Iterable)
help(fn())
#可以看出fn()里面有__iter__()和__next__()方法


#通常迭代器满足两个条件
#有iter方法
#有next方法
总结

生成器都是迭代器,但是迭代器并不一定是生成器

生成器生成迭代器就是生成器本身。

通常我们叫迭代器也叫生成器。

生成器高级用法是用在协程

协程: 是用户空间的轻量线程,跑在一个线程内,由用户空间调度

进程和线程是操作系统来调度

调度:由调度器来决定哪段代码来占用cpu时间)

内核态到用户态进行调度需要消耗时间跟资源,用户态跟用户态之间调度消耗资源小

协程也叫轻量线程非抢占式调度

生成器用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#生成器send()用法
#看例子
def gen(x):
while True:
n = yield x
print(n)
g = gen(10)
next(g)
out>10
next(g)
None
out>10
#这里的n = yield x 是赋值的意思么?
#第一次执行next时候 返回 10 可以理解
#第二次为毛有None?难道说在yield之前有个None?,
#send是传递?传递给None么?
g.send(2)
2
out>10
#还真是。。。

def gen(x):
while True:
n = yield x
print(n)
g = gen(10)
g.send(100000)
TypeError: can't send non-None value to a just-started generator
#为什么为报错呢?
#g.send(100) (第一次执行这个,并不能给n赋值,因为找不到n)
#当第一次执行
#next(g)的时候执行yield x,这里x就是10,n就为10,yield结束后,n就在这个函数体里面存在
#在此next(g)的时候,打印刚才10,到yield 10结束
#如果这是g.send(1000),这里n就变成1000了,而yield 10还是刚才你传入的值
#如果没有传参数,那么就会显示刚才的 打印None,返回10


#协程例子
#看个示例:
def gen1():
while True:
yield 'gen1'
def gen2(g):
for x in range(10):
yield 'gen2'
print(next(g))
g = gen2(gen1())
next(g) #out>'gen2'
next(g) #打印‘gen1' out>'gen2'


#生产者消费者模型
import time
def consumer(pro):
print('{}开始消费'.format(pro))
while True:
con = yield
print('{}生产完成,{}消费结束'.format(con,pro))
def producer(pro):
c1 = consumer('C1')
c2 = consumer('C2')
next(c1)
next(c2)
print('{}准备生产'.format(pro))
for i in range(1,5):
time.sleep(1)
print('{}生产两个'.format(pro))
c1.send(i)
c2.send(i)
producer('server')

#打印如下
C1开始消费
C2开始消费
server准备生产
server生产两个
1生产完成,C1消费结束
1生产完成,C2消费结束
server生产两个
2生产完成,C1消费结束
2生产完成,C2消费结束
server生产两个
3生产完成,C1消费结束
3生产完成,C2消费结束
server生产两个
4生产完成,C1消费结束
4生产完成,C2消费结束
看完了?赏个鸡腿钱,谢谢老板!