python 函数篇(2)
CONTENTS
- 局部变量与全局变量
- 嵌套函数
- 高阶函数
- 装饰器
- 迭代器与生成器
局部变量与全局变量
局部变量与全局变量的区别在于二者的作用域不同。
- 局部变量只在函数中生效,函数就是局部变量的作用域
- 全局变量也可以在函数中被访问,但不能再函数中修改,除非声明。
1 num = 1 # 定义一个全局变量 2 def func1(): 3 print(num) 4 return print("func1执行完毕") 5 func1() # 可以访问到全局变量
在函数中是可以访问全局变量的值。如果想在函数中修改全局变量,需要利用global声明:
1 def func2(): 2 global num 3 num = num + 1 4 print(num) 5 return print("func2执行完毕") 6 func2() # 虽然可以访问,但未声明的话是不能修改的 7 print(num)
在函数中对全局变量进行修改,num在全局范围中的值也相应发生了变化。
还有一个和局部变量相关的关键字nonlocal,字面意思就是指当前的这个变量不是局部变量。nonlocal是Python3中新增的关键字,python2.x不支持。
nonlocal一般用于嵌套函数中:
1 def func4(): 2 num = 1 3 def func5(): 4 nonlocal num 5 num = num+1 6 print(num) 7 return print("func5执行完毕") 8 func5() 9 return print("func4执行完毕") 10 func4()
func5中num修改的值就是func4中定义的num。
嵌套函数
嵌套函数,顾名思义就是函数内又定义了一个或多个函数。
1 def func4(): 2 num = 1 3 def func5(): 4 nonlocal num 5 num = num+1 6 print(num) 7 return print("func5执行完毕") 8 # func5() 9 return print("func4执行完毕") 10 func4()
执行上述代码,func5并没有被执行,并且在函数声明外部也不能被调用。因为函数即“变量”,嵌套函数相当于是局部变量,不能在函数外部调用。如果要执行func5,需要取消第8行的注释,因为函数只有调用了才会执行。
高阶函数
在讨论高阶函数之前,还是再来讨论一下为什么函数即“变量”。
在使用函数进行调用时,比如说abs(),我们会将一个参数值输入到abs中,比如-1,再用一个变量获取返回值,完成一个函数的调用。
1 a = abs(-1)
那么,abs(-1)是函数调用,而abs是函数本身。
如果将函数本身赋值给变量又会发生什么呢?
1 a = abs 2 print(a(-1))
执行上述代码,会获得与abs(-1)一样的结果。那么,此时abs是不是相当于变量一般的把自己赋值给了a,使得a有了和自己一样的功能呢?也就是说,此时a已指向abs,那么函数名其实也就是指向函数的一个变量。对于函数abs(),完全可以把函数名abs看成是变量,它指向了一个可以计算绝对值的函数。那问题又来了,既然abs可以看成是变量,是不是可以给它赋值呢?
1 abs=10 2 print(abs)
好吧,这样居然也是可以的。也就是或这个时候,abs已经是指向10的变量,而不是那个能计算绝对值的函数了。那再调用一下abs函数试试?
abs=10 print(abs) print(abs(-1))
这时,会返回一个“TypeError: 'int' object is not callable”。也就是说,abs现在是整型10了。
好啦,扯了一大堆,似乎只说明了一个问题:函数即“变量”。那么,函数在传入参数时,是否可以将函数传入呢?这种函数就是高阶函数。
高阶函数:一个函数可以接受另一个函数作为参数。一个高阶函数的例子:
1 def gate_cal(wh, wx, ht_pre, x, b, activation): # activation 直接传入函数 --> 高阶函数 2 '''由于各门的计算公式形式相同,只是参数 激活函数不一样,为此生成一个公式生成函数''' 3 x = x.reshape(-1, 1) 4 return activation(wh.dot(ht_pre) + wx.dot(x) + b)
在计算LSTM遗忘门、输入门、输出门、状态时由于运算公式都一样,所以可以写成上述代码中的函数。其中,activation就是传入的一个函数(激活函数)。
函数作为变量传入时,只需要传入函数名即可,也就是如果传入的激活函数是sigmoid函数:
- 只需要将sigmoid传入参数即可。 √
- 而不是将sigmoid()传入, 这种是把调用后的返回值传入。 ×
装饰器
装饰器本质上是函数,从字面上理解就是起到装饰的作用,装饰其他函数。也就是为其他函数添加附加功能。
原则:不能修改被装饰函数的源代码和调用方式。
在某种意义上,装饰器对于被装饰的函数是完全透明的。
首先,装饰器的实现是基于对之前内容的运用,即:
- 函数即“变量”
- 嵌套函数
- 高阶函数
例如,对于一个函数func1,想要利用装饰器对其添加运行时间的功能。
1 def func1(): 2 time.sleep(3) 3 print("in the func")
首先,如果只是将func1传给另一个函数(应用高阶函数),在另一个函数中添加功能,这样会改变func1的调用方式:
1 def test(func): 2 start_time = time.time() 3 func() 4 print("运行时间:", time.time() - start_time) 5 6 test(func1)
此时,确实实现了添加运算时间的功能,但实现方式变成了test(func1),改变了func1()这样的调用方式。
其次,想着套上嵌套函数的功能看看能不能实现。为了让内部嵌套的韩式能够自己调用,将deco的返回值返回test:
1 import time 2 3 def func1(): 4 time.sleep(3) 5 print("in the func") 6 7 def deco(func): 8 def test(): 9 start_time = time.time() 10 func() 11 print("运行时间:", time.time()-start_time) 12 return test
但这个时候好像调用还是需要deco(func1),和原来的调用还不一样。这时,是不是会想到之前介绍的abs,函数即“变量”,即:
13 func1 = deco(func1) 14 func1()
这样一个装饰器就完成了!!!
其实在python中有专门的语句,可以省去func1 = deco(func1)这样的操作,即:使用@deco这样一条语句对func1(要装饰的函数)进行装饰
@deco 实际上就是完成了func1 = deco(func1)这样的操作
1 import time 2 3 def deco(func): 4 def test(): 5 start_time = time.time() 6 func() 7 print("运行时间:", time.time()-start_time) 8 return test 9 10 @deco # func1 = deco(func1) 11 def func1(): 12 time.sleep(3) 13 print("in the func") 14 15 func1()
这才是python上实现一个简易装饰器的模样。
需求又来了, 如果func1函数想要传参数进去,需要在嵌套函数的内部函数中添加位置参数或关键字参数,然后再在func调用时传入,如代码注释部分所示:
1 import time 2 3 def deco(func): 4 def test(*arg1): #1. 参数添加 5 start_time = time.time() 6 func(*arg1) # 2. 传进的参数传入 7 print("运行时间:", time.time()-start_time) 8 return test 9 10 @deco 11 def func1(*args): 12 time.sleep(3) 13 print("in the func") 14 print(args) 15 16 func1(1,1,1,1,1)
需求再来一波~... 让同一个装饰器对不同的函数装饰,产生不同的装饰效果。
可以在@deco后面进行一些操作,即@deco() 里传入参数,进行一些设定和判断。
如果@deco也可以传入参数,函数体中还需要在最外层增加一层嵌套函数来接收@deco()传入的参数,在函数中,通过传入的参数不同,可以声明不同的功能。
下述代码中,在最外层增加了一层auth()来接收auth_type,在最内层的函数中,可以根据auth_tpye的不同,定义不同的功能。这样在装饰不同函数时,将添加对应auth_type的内的功能。
1 user, passwd = "Iris", "123456" 2 3 def auth(auth_type): 4 print("auth_type:", auth_type) 5 def outer_wrapper(func): 6 def wrapper(*args, **kwargs): 7 if auth_type == "local": # 1.一种auth_type的功能 8 username = input("username:").strip() 9 password = input("password:").strip() 10 if user == username and passwd == password: 11 print("\033[34;1mUser has passed authentication\033[0m") 12 res = func(*args, **kwargs) 13 return res 14 else: 15 exit("\033[41;1mInvalid username or password\033[0m") 16 elif auth_type == "ldap": # 2. 另一种auth_type的功能 17 print("...") 18 return wrapper 19 return outer_wrapper 20 def index(): 21 print("welcome to index page") 22
23@auth(auth_type="local") # home = wrapper() 24 def home(): 25 print("welcome to home page") 26 return "from home" 26
27@auth(auth_type="ldap") 28 def bbs(): 29 print("welcome to bbs page") 30
31 index() 32 home() # wrapper() 33 bbs()
生成器与迭代器
对于将一个列表中的每个元素分别进行一个简单的操作,比如说[2,3,4,5,6,7,8]这样一个列表,每个元素都乘以2这样的需求,可能会想到有三种解决方案:
1. 写一个循环,让列表中每个元素都乘以2:
1 a = [2, 3, 4, 5, 6, 7, 8] 2 for i in range(len(a)): # 方式1 3 a[i] *= 2 4 5 for index, i in enumerate(a): #方式2 6 a[index] *= 2
2. 又或者想到内置函数map,也可以实现这样的功能:
1 a = map(lambda x:x*2, a) 2 print(a) 3 for i in a: 4 print(i) # 这个时候其实就是一个生成器
3. 再或者想到列表推导式
1 a = [i*2 for i in a] 2 print(a)
上面的需求,对于数据量较少,并且生成以后会使用多次,使用列表推导式会显得十分方便,简洁。
但如果数据量特别大,而且只循环使用一次,使用列表推导式将所有的数据经运算再建立一个列表放到内存中就显得有些浪费内存。那么,如果列表中元素可以随用随取,即需要的时候就按循环生成一个,不需要新建一个完整的列表list放到内存中,这样可以节省很大的内存空间。
因此,在Python中,这种一边循环一边计算的机制,称为生成器:generator
通过上面的描述可以看出生成器有如下特性:
- 生成器只有在调用时才会生成相应的数据,一次只能生成一个值,因此,生成器也就不能使用切片的方式进行索引。
- 生成器不会保存结果,只是保持每次的状态,在每次迭代时返回一个值,并记住当前的状态。
- 当迭代超过生成器包含的内容时,会产生一个StopIteration的异常而结束。
python中生成generator的方式:
- 1. 生成器表达式:只需要将列表推导式的 [] 变为 () 即可产生一个简易的生成器, 因为这种形式难以实现较复杂的功能。
- 2. 生成器函数:函数中存在yield关键字,此时函数便是一个生成器。
既然生成器不能够切片索引,那应该如何调用呢?实际上,可以使用next()函数或者generator.__next__() 来调用,每次调用只生成一个值。
示例如下:
1 # 1. 生成器表达式 2 x = (i**2 for i in range(10)) 3 4 print(x) 5 6 print(x.__next__()) # 生成器表达式值的调用 相对于列表推导式更加的省内存 7 print(x.__next__()) 8 print(x.__next__()) 9 print(x.__next__()) 10 print(x.__next__()) 11 print(x.__next__()) 12 print(x.__next__()) 13 print(x.__next__())
执行上述代码,可以看出print(x) 返回的结果是
但一般不直接使用__next__这类方法去生成值,由于生成器都是Iterator对象,即生成器也是迭代器,所以常用的调用生成方式:
1 # 生成器常用的生成方式, 这样可以避免程序抛StopIteration的异常,同时代码更简洁 2 x = (i**2 for i in range(10)) 3 for i in x: # x也是一个迭代器 4 print(i)
生成器可以完成的,列表推导式也能完成,二者的区别就在于列表推导式有时可能比较占内存,而生成器取出来以后不能再取了。
1 # 2. 生成器函数 yield的存在 将函数变为生成器函数 2 def odd(): 3 n = 1 4 while True: 5 yield n # 迭代器函数 6 n += 2 7 odd_num = odd() 8 count = 0 9 for i in odd_num: 10 if count >= 5: 11 break 12 print(i) 13 count += 1
可以将上述代码都加上断点,然后在debug下运行,观察程序的运行次序,以代码的编号来说: 2-7-8-9-3-4-5-10-12-13-6-5-10-12-13-6-5-... 直至if count>=5:成立,程序退出。
可以看出程序每次遇到yield时返回,再次触发调用时,再回到上次yield语句处继续执行。
如果想拿到generator的返回值"----done----",便不能用for循环去取值,只能使用next()方法,然后再进行异常处理,如下述简易的斐波那契数列的代码:
def fib(max): n, a, b = 0, 0, 1 while n < max: #print(b) yield b a, b = b, a + b n = n + 1 return '---done---' g = fib(6) while True: try: x = next(g) print('g:', x) except StopIteration as e: print('Generator return value:', e.value) break
另外,generator.send() 还可以给yield传值,下述为yield实现的单线程并行效果:
1 def consumer(name): 2 print("%s 准备吃包子啦!" %name) 3 while True: 4 baozi = yield 5 print("包子[%s]来了,被[%s]吃了一半!" %(baozi,name)) 6 7 8 def producer(name): 9 c = consumer('A') 10 c2 = consumer('B') 11 c.__next__() 12 c2.__next__() 13 print("%s开始准备做包子啦!"%name) 14 for i in range(10): 15 time.sleep(1) 16 print("做了1个包子,分两半!") 17 c.send(i) 18 c2.send(i) 19 20 producer("Iris")
迭代器(迭代即循环)
可以知道,可以直接作用于for循环的数据类型有:
- 集合数据类型:list, tuple, dict, set 和 str。
- 生成器(生成器都是迭代器)
可以直接用于for循环的对象被称为可迭代对象Iterable。
可以被next()调用,并不断返回下一个值的对象:迭代器Iterator。
list, dict, str 虽然是Iterable,但不是Iterator。如果想将其变为迭代器,可以使用iter()函数
以及 判断一个对象是否是可迭代对象:
1 '''判断一个列表是否是可迭代的,是否是迭代器,以及将其转变为迭代器''' 2 from collections import Iterable, Iterator 3 print(isinstance([], Iterable)) # True 4 print(isinstance([], Iterator)) # False 5 6 a = [1, 2, 3, 4] 7 print(isinstance(a, Iterable)) # True 8 print(isinstance(a, Iterator)) # False 9 a = iter(a) # 转变为Iterator 10 print(isinstance(a, Iterator)) # True 11 print(a.__next__()) # 返回1
小结:
- 通常的for..in...循环中,in后面是一个数组,这个数组就是一个可迭代对象,类似的还有列表,字符串,文件。可以是a = [1,2,3],也可以是a = [x*x for x in range(3)]。
它的缺点也很明显,就是所有数据都在内存里面,如果有海量的数据,将会非常耗内存。
- 生成器是可以迭代的,但是只可以读取它一次。因为用的时候才生成,比如a = (x*x for x in range(3))。!!! 小括号...小括号...小括号。
- 生成器(generator)能够迭代的关键是next()方法,工作原理就是通过重复调用next()方法,直到捕获一个异常。
- 带有yield的函数不再是一个普通的函数,而是一个生成器generator,可用于迭代。
- yield是一个类似return 的关键字,迭代一次遇到yield就返回yield后面或者右面的值。而且下一次迭代的时候,从上一次迭代遇到的yield后面的代码开始执行
- yield就是return返回的一个值,并且记住这个返回的位置。下一次迭代就从这个位置开始。
- 带有yield的函数不仅仅是只用于for循环,而且可用于某个函数的参数,只要这个函数的参数也允许迭代参数。
- send()和next()的区别就在于send可传递参数给yield表达式,这时候传递的参数就会作为yield表达式的值,而yield的参数是返回给调用者的值,也就是说send可以强行修改上一个yield表达式值。
- send()和next()都有返回值,他们的返回值是当前迭代遇到的yield的时候,yield后面表达式的值,其实就是当前迭代yield后面的参数。
- 第一次调用时候必须先next()或send(),否则会报错,send后之所以为None是因为这时候没有上一个yield,所以也可以认为next()等同于send(None)
番外篇
昨天看了电影天才枪手,这里简单的利用yield模拟了一下并发传答案的效果,女主Lynn传前五个题的答案,男主Bank传后五个题的答案:
出于好奇,想要把字典变成迭代器,但直接iter()的话,迭代生成的值只有键,没有值。而在python3中,iteritems()函数已经不存在了。迫于无奈,先items()再iter(),没想到还真可以~
1 import time 2 # 生成一个答案迭代器 3 ans1 = {1: "A", 2: "B", 3: "D", 4: "A", 5: "C"} 4 ans2 = {6: "D", 7: "C", 8: "A", 9: "C", 10: "B"} 5 # f = ans1.iteritems() # 这样可以返回一个字典迭代器,但在python3中已经废除 6 # 字典直接变成迭代器,返回的的只有键,没有值。需要先变成dict_items再转化为迭代器。 7 f = ans1.items() 8 f = iter(f) 9 f1 = ans2.items() 10 f1 = iter(f1) 11 12 def test(name): 13 print("STIC考试开始!") 14 while True: 15 ans = yield 16 print("%s传回答案%s" % (name, ans)) 17 18 def testor(): 19 c1 = test("Lynn") 20 c2 = test("Bank") 21 c1.__next__() 22 c2.__next__() 23 for i in range(5): 24 time.sleep(1) 25 c1.send(f.__next__()) 26 c2.send(f1.__next__()) 27 testor()