一.闭包python
因为闭包这个概念比较难以理解,尤为是初学者来讲,相对难以掌握,因此咱们经过示例去理解学习闭包。安全
给你们提个需求,而后用函数去实现:完成一个计算不断增长的系列值的平均值的需求。微信
例如:整个历史中的某个商品的平均收盘价。什么叫平局收盘价呢?就是从这个商品一出现开始,天天记录当天价格,而后计算他的平均值:平均值要考虑直至目前为止全部的价格。闭包
好比大众推出了一款新车:小白轿车。app
第一天价格为:100000元,平均收盘价:100000元函数
次日价格为:110000元,平均收盘价:(100000 + 110000)/2 元工具
第三天价格为:120000元,平均收盘价:(100000 + 110000 + 120000)/3 元学习
........指针
series = [] def make_averager(new_value): series.append(new_value) total = sum(series) return total / len(series) print(make_averager(100000)) print(make_averager(110000)) print(make_averager(120000))
从上面的例子能够看出,基本上完成了咱们的要求,可是这个代码相对来讲是不安全的,由于你的这个series列表是一个全局变量,只要是全局做用域的任何地方,均可能对这个列表进行改变。code
series = [] def make_averager(new_value): series.append(new_value) total = sum(series) return total / len(series) print(make_averager(100000)) print(make_averager(110000)) series.append(666) # 若是对数据进行相应改变,那么你的平均收盘价就会出现很大的问题。 print(make_averager(120000))
那么怎么办呢?有人说,你把他放在函数中不就好了,这样不就是局部变量了么?数据不就相对安全了么?
def make_averager(new_value): series = [] series.append(new_value) total = sum(series) return total / len(series) print(make_averager(100000)) # 100000.0 print(make_averager(110000)) # 110000.0 print(make_averager(120000)) # 120000.0
这样计算的结果是不正确的,那是由于执行函数,会开启一个临时的名称空间,随着函数的结束而消失,因此你每次执行函数的时候,都是从新建立这个列表,那么这怎么作呢?这种状况下,就须要用到咱们讲的闭包了,咱们用闭包的思想改一下这个代码。
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager avg = make_averager() print(avg(100000)) print(avg(110000)) print(avg(120000))
你们仔细看一下这个代码,我是在函数中嵌套了一个函数。那么avg 这个变量接收的实际是averager函数名,也就是其对应的内存地址,我执行了三次avg 也就是执行了三次averager这个函数。那么此时大家有什么问题?
确定有学生就会问,那么个人make_averager这个函数只是执行了一次,为何series这个列表没有消失?反而还能够被调用三次呢?这个就是最关键的地方,也是闭包的精华所在。我给你们说一下这个原理,以图为证:
上面被红色方框框起来的区域就是闭包,被蓝色圈起来的那个变量应该是make_averager()函数的局部变量,它应该是随着make_averager()函数的执行结束以后而消失。可是他没有,是由于此区域造成了闭包,series变量就变成了一个叫自由变量的东西,averager函数的做用域会延伸到包含自由变量series的绑定。也就是说,每次我调用avg对应的averager函数 时,均可以引用到这个自用变量series,这个就是闭包。
闭包的定义:
如何判断判断闭包?举例让同窗回答:
# 例一: def wrapper(): a = 1 def inner(): print(a) return inner ret = wrapper() # 例二: a = 2 def wrapper(): def inner(): print(a) return inner ret = wrapper() # 例三: def wrapper(a,b): def inner(): print(a) print(b) return inner a = 2 b = 3 ret = wrapper(a,b)
以上三个例子,最难判断的是第三个,其实第三个也是闭包,若是咱们每次去研究代码判断其是否是闭包,有一些不科学,或者过于麻烦了,那么有一些函数的属性是能够获取到此函数是否拥有自由变量的,若是此函数拥有自由变量,那么就能够侧面证实其是不是闭包函数了(了解):
def make_averager(): series = [] def averager(new_value): series.append(new_value) total = sum(series) return total/len(series) return averager avg = make_averager() # 函数名.__code__.co_freevars 查看函数的自由变量 print(avg.__code__.co_freevars) # ('series',) 固然还有一些参数,仅供了解: # 函数名.__code__.co_freevars 查看函数的自由变量 print(avg.__code__.co_freevars) # ('series',) # 函数名.__code__.co_varnames 查看函数的局部变量 print(avg.__code__.co_varnames) # ('new_value', 'total') # 函数名.__closure__ 获取具体的自由变量对象,也就是cell对象。 # (<cell at 0x0000020070CB7618: int object at 0x000000005CA08090>,) # cell_contents 自由变量具体的值 print(avg.__closure__[0].cell_contents) # []
闭包的做用:保存局部信息不被销毁,保证数据的安全性。
闭包的应用:
本节咱们讲列表推导式,生成器表达式以及其余推导式,我认为推导式就是构建比较有规律的列表,生成器,字典等一种简便的方式。那么他如何简便呢?看下面的例题:
这里让学生本身作一下,首先咱们先看一下这样的代码,给出一个列表,经过循环,想列表中添加1~10:
li = [] for i in range(10): li.append(i) print(li)
那么按照上面的要求咱们用列表推导式写一下:
ls = [i for i in range(10)] print(ls)
怎么样?一行搞定,上面这个代码就是列表推导式,接下来咱们将列表推导式进行一个分类:
列表推导式分为两种模式:
1.循环模式:[变量(加工的变量) for 变量 in iterable]
2.筛选模式: [变量(加工的变量) for 变量 in iterable if 条件]
固然还有多层循环的,这个咱们一会就会讲到,那么咱们先来看循环模式。
刚才咱们看到的就是循环模式,那么有同窗会问到,什么叫' 加工的变量'? 这个也比较简单,接下来咱们作几道题:
l1 = [i*i for i in range(1,11)] print(l1)
l1 = [i for i in range(2,101,2)] print(l1)
lst = [f'python{i}' % i for i in range(1,25)] print(lst)
上面那个格式化输出的变量f'python{i}',就是加工的变量。
上面作的那三个就是循环模式,比较简单,接下来咱们研究筛选模式。
筛选模式就是在上面的基础上加上一个判断条件,将知足条件的变量留到列表中。
带着同窗们作一个题:
将这个列表中大于3的元素留下来。
l1 = [4, 3, 2, 6, 5, 5, 7, 8] print([i for i in l1 if i > 3])
经过我给你们的演示,你们作几道题:
三十之内能够被三整除的数。
multiples = [i for i in range(30) if i % 3 is 0] print(multiples)
过滤掉长度小于3的字符串列表,并将剩下的转换成大写字母
l = ['wusir', 'laonanhai', 'aa', 'b', 'taibai'] # print([i.upper() for i in l if len(i) > 3])
找到嵌套列表中名字含有两个‘e’的全部名字(有难度)
names = [['Tom', 'Billy', 'Jefferson', 'Andrew', 'Wesley', 'Steven', 'Joe'], ['Alice', 'Jill', 'Ana', 'Wendy', 'Jennifer', 'Sherry', 'Eva']] print([name for lst in names for name in lst if name.count('e') >= 2]) # 注意遍历顺序,这是实现的关键
列表推导式基本上讲完了,固然今天会作一些有关列表推导式的题,让你们更加深刻的了解。
生成器表达式和列表推导式的语法上如出一辙,只是把[]换成()就好了。好比将十之内全部数的平方放到一个生成器表达式中
gen = (i**2 for i in range(10)) print(gen) # 结果: <generator object <genexpr> at 0x0000026046CAEBF8>
生成器表达式也能够进行筛选
# 获取1-100内能被3整除的数 gen = (i for i in range(1,100) if i % 3 == 0) for num in gen: print(num)
生成器表达式和列表推导式的区别:
不管是生成器表达式,仍是列表推导式,他只是Python给你提供了一个相对简单的构造方式,由于使用推导式很是简单,因此大多数都会为之着迷,这个必定要慎重,推导式只能构建相对复杂的而且有规律的对象,对于没有什么规律,并且嵌套层数比较多(for循环超过三层)这样就不建议你们用推导式构建。
生成器的惰性机制: 生成器只有在访问的时候才取值,说白了.你找他要才给你值.不找他要.他是不会执行的.
根据名字应该也能猜到,推到出来的是字典
lst1 = ['jay','jj','meet'] lst2 = ['周杰伦','林俊杰','郭宝元'] dic = {lst1[i]:lst2[i] for i in range(len(lst1))} print(dic)
集合推导式
集合推导式能够帮咱们直接生成一个集合,集合的特色;无序,不重复 因此集合推导式自带去重功能
lst = [1,2,3,-1,-3,-7,9] s = {abs(i) for i in lst} print(s)
3.1.1 可迭代对象定义
对于迭代器来讲,咱们更熟悉的应该是可迭代对象,以前不管是源码仍是讲课中或多或少咱们提到过可迭代对象这个词。以前为了便于你们理解可迭代对象,可能解释的不是很正确,因此今天咱们正式的聊一聊什么是可迭代对象。从字面意思来讲,咱们先对其进行拆解:什么是对象?Python中一切皆对象,以前咱们讲过的一个变量,一个列表,一个字符串,文件句柄,函数名等等均可称做一个对象,其实一个对象就是一个实例,就是一个实实在在的东西。那么什么叫迭代?其实咱们在平常生活中常常遇到迭代这个词儿,更新迭代等等,迭代就是一个重复的过程,可是不能是单纯的重复(若是只是单纯的重复那么他与循环没有什么区别)每次重复都是基于上一次的结果而来。好比你爹生你,你生你爹,哦不对,你生你儿子,你儿子生你孙子等等,每一代都是不同的;还有你使用过得app,微信,抖音等,隔一段时间就会基于上一次作一些更新,那么这就是迭代。可迭代对象从字面意思来讲就是一个能够重复取值的实实在在的东西。
那么刚才咱们是从字面意思分析的什么是可迭代对象,到目前为止咱们接触到的可迭代对象有哪些呢?
str list tuple dic set range 文件句柄等,那么int,bool这些为何不能称为可迭代对象呢?虽然在字面意思这些看着不符合,可是咱们要有必定的判断标准或者规则去判断该对象是否是可迭代对象。
在python中,但凡内部含有*iter*方法的对象,都是可迭代对象。
3.1.2 查看对象内部方法
该对象内部含有什么方法除了看源码还有什么其余的解决方式么?固然有了, 能够经过dir() 去判断一个对象具备什么方法
s1 = 'alex' print(dir(s1))
dir()会返回一个列表,这个列表中含有该对象的以字符串的形式全部方法名。这样咱们就能够判断python中的一个对象是否是可迭代对象了:
s1 = 'alex' i = 100 print('__iter__' in dir(i)) # False print('__iter__' in dir(s1)) # True
3.1.3 小结:
从字面意思来讲:可迭代对象就是一个能够重复取值的实实在在的东西。
从专业角度来讲:但凡内部含有__iter__方法的对象,都是可迭代对象。
可迭代对象能够经过判断该对象是否有__iter__方法来判断。
可迭代对象的优势:
能够直观的查看里面的数据。
可迭代对象的缺点:
1.占用内存。
2.可迭代对象不能迭代取值(除去索引,key之外)。
那么这个缺点有人就提出质疑了,即便抛去索引,key之外,这些我能够经过for循环进行取值呀!对,他们均可以经过for循环进行取值,其实for循环在底层作了一个小小的转化,就是先将可迭代对象转化成迭代器,而后在进行取值的。那么接下来,咱们就看看迭代器是个什么鬼。
3.2.1 迭代器的定义
从字面意思来讲迭代器,是一个能够迭代取值的工具,器:在这里当作工具比较合适。
从专业角度来讲:迭代器是这样的对象:实现了无参数的__next__方法,返回序列中的下一个元素,若是没有元素了,那么抛出StopIteration异常.python中的迭代器还实现了__iter__方法,所以迭代器也能够迭代。 出自《流畅的python》
那么对于上面的解释有一些超前,和难以理解,不用过于纠结,咱们简单来讲:在python中,内部含有__Iter__方法而且含有__next__方法的对象就是迭代器。
3.2.2 如何判断该对象是不是迭代器
ok,那么咱们有了这个定义,咱们就能够判断一些对象是否是迭代器或者可迭代对象了了,请判断这些对象:str list tuple dict set range 文件句柄 哪一个是迭代器,哪一个是可迭代对象:
o1 = 'alex' o2 = [1, 2, 3] o3 = (1, 2, 3) o4 = {'name': '宝元','age': 18} o5 = {1, 2, 3} f = open('file',encoding='utf-8', mode='w') print('__iter__' in dir(o1)) # True print('__iter__' in dir(o2)) # True print('__iter__' in dir(o3)) # True print('__iter__' in dir(o4)) # True print('__iter__' in dir(o5)) # True print('__iter__' in dir(f)) # True print('__next__' in dir(o1)) # False print('__next__' in dir(o2)) # False print('__next__' in dir(o3)) # False print('__next__' in dir(o4)) # False print('__next__' in dir(o5)) # False print('__next__' in dir(f)) # True f.close()
经过以上代码能够验证,以前咱们学过的这些对象,只有文件句柄是迭代器,剩下的那些数据类型都是可迭代对象。
3.2.3 可迭代对象如何转化成迭代器:
l1 = [1, 2, 3, 4, 5, 6] obj = l1.__iter__() # <list_iterator object at 0x000002057FE1A3C8> # 或 obj = iter(l1) print(obj) # <list_iterator object at 0x102cc67f0>
3.2.4 迭代器取值:
可迭代对象是不能够一直迭代取值的(除去用索引,切片以及Key),可是转化成迭代器就能够了,迭代器是利用__next__()进行取值:
l1 = [1, 2, 3,] obj = l1.__iter__() # 或者 iter(l1) # print(obj) # <list_iterator object at 0x000002057FE1A3C8> ret = obj.__next__() print(ret) ret = obj.__next__() print(ret) ret = obj.__next__() print(ret) ret = obj.__next__() # StopIteration print(ret) # 迭代器利用next取值:一个next取对应的一个值,若是迭代器里面的值取完了,还要next, # 那么就报StopIteration的错误。
3.2.5 while模拟for的内部循环机制:
刚才咱们提到了,for循环的循环对象必定要是可迭代对象,可是这不意味着可迭代对象就能够取值,由于for循环的内部机制是:将可迭代对象转换成迭代器,而后利用next进行取值,最后利用异常处理处理StopIteration抛出的异常。
l1 = [1, 2, 3, 4, 5, 6] # 1 将可迭代对象转化成迭代器 obj = iter(l1) # 2,利用while循环,next进行取值 while 1: # 3,利用异常处理终止循环 try: print(next(obj)) except StopIteration: break
3.2.6 小结:
从字面意思来讲:迭代器就是能够迭代取值的工具。
从专业角度来讲:在python中,内部含有__Iter__方法而且含有__next__方法的对象就是迭代器。
迭代器的优势:
节省内存。 迭代器在内存中至关于只占一个数据的空间:由于每次取值都上一条数据会在内存释放,加载当前的此条数据。
惰性机制。 next一次,取一个值,毫不过多取值。
有一个迭代器模式能够很好的解释上面这两条:迭代是数据处理的基石。扫描内存中放不下的数据集时,咱们要找到一种惰性获取数据项的方式,即按需一次获取一个数据项。这就是迭代器模式。
迭代器的缺点:
不能直观的查看里面的数据。
取值时不走回头路,只能一直向下取值。
l1 = [1, 2, 3, 4, 5, 6] obj = iter(l1) for i in range(2): print(next(obj)) for i in range(2): print(next(obj))
咱们今天比较深刻的了解了可迭代对象与迭代器,接下来咱们说一下这二者之间比较与应用:
可迭代对象:
是一个私有的方法比较多,操做灵活(好比列表,字典的增删改查,字符串的经常使用操做方法等),比较直观,可是占用内存,并且不能直接经过循环迭代取值的这么一个数据集。
应用:当你侧重于对于数据能够灵活处理,而且内存空间足够,将数据集设置为可迭代对象是明确的选择。
迭代器:
是一个很是节省内存,能够记录取值位置,能够直接经过循环+next方法取值,可是不直观,操做方法比较单一的数据集。
应用:当你的数据量过大,大到足以撑爆你的内存或者你以节省内存为首选因素时,将数据集设置为迭代器是一个不错的选择。(可参考为何python把文件句柄设置成迭代器)。
什么是生成器?这个概念比较模糊,各类文献都有不一样的理解,可是核心基本相同。生成器的本质就是迭代器,在python社区中,大多数时候都把迭代器和生成器是作同一个概念。不是相同么?为何还要建立生成器?生成器和迭代器也有不一样,惟一的不一样就是:迭代器都是Python给你提供的已经写好的工具或者经过数据转化得来的,(好比文件句柄,iter([1,2,3])。生成器是须要咱们本身用python代码构建的工具。最大的区别也就如此了。
在python中有三种方式来建立生成器:
咱们先来研究经过生成器函数构建生成器。
首先,咱们先看一个很简单的函数:
def func(): print(11) return 22 ret = func() print(ret) # 运行结果: 11 22
将函数中的return换成yield,这样func就不是函数了,而是一个生成器函数
def func(): print(11) yield 22
咱们这样写没有任何的变化,这是为何呢? 咱们来看看函数名加括号获取到的是什么?
def func(): print(11) yield 22 ret = func() print(ret) # 运行结果: <generator object func at 0x000001A575163888>
为何在函数中添加了yield在调用函数的时候就发现结果不是咱们预想的结果呢,是由于当咱们调用函数的时候函数体里的代码会进行执行当执行到yield的关键字的时候,发现咱们是想声明一个生成器.程序就会返回一个生成器给我们
那么生成器对象如何取值呢?
以前咱们说了,生成器的本质就是迭代器.迭代器如何取值,生成器就如何取值。因此咱们能够直接执行next()来执行如下生成器
def func(): print("111") yield 222 gener = func() # 这个时候函数不会执⾏. ⽽是获取到⽣成器 ret = gener.__next__() # 这个时候函数才会执⾏ print(ret) # 而且yield会将func生产出来的数据 222 给了 ret。 结果: 111 222
而且个人生成器函数中能够写多个yield。
def func(): print("111") yield 222 print("333") yield 444 gener = func() ret = gener.__next__() print(ret) ret2 = gener.__next__() print(ret2) ret3 = gener.__next__() # 最后⼀个yield执⾏完毕. 再次__next__()程序报错 print(ret3)
当程序运行完最后一个yield,那么后面继续运行next()程序会报错,一个yield对应一个next,next超过yield数量,就会报错,与迭代器同样。
yield与return的区别:
return通常在函数中只设置一个,他的做用是终止函数,而且给函数的执行者返回值。
yield在生成器函数中可设置多个,他并不会终止函数,next会获取对应yield生成的元素。
举例:
咱们来看一下这个需求:老男孩向楼下卖包子的老板订购了10000个包子.包子铺老板很是实在,一下就所有都作出来了
def eat(): lst = [] for i in range(1,10000): lst.append('包子'+str(i)) return lst e = eat() print(e)
这样作没有问题,可是咱们因为学生没有那么多,只吃了2000个左右,剩下的8000个,就只能占着必定的空间,放在一边了。若是包子铺老板效率够高,我吃一个包子,你作一个包子,那么这就不会占用太多空间存储了,完美。
def eat(): for i in range(1,10000): yield '包子'+str(i) e = eat() for i in range(200): next(e)
这二者的区别:
第一种是直接把包子所有作出来,占用内存。
第二种是吃一个生产一个,很是的节省内存,并且还能够保留上次的位置。
def eat(): for i in range(1,10000): yield '包子'+str(i) e = eat() for i in range(200): next(e) for i in range(300): next(e) # 屡次next包子的号码是按照顺序记录的。
接下来咱们再来认识一个新的东西,send方法
# next只能获取yield生成的值,可是不能传递值。 def gen(name): print(f'{name} ready to eat') while 1: food = yield print(f'{name} start to eat {food}') dog = gen('alex') next(dog) next(dog) next(dog) # 而使用send这个方法是能够的。 def gen(name): print(f'{name} ready to eat') while 1: food = yield 222 print(f'{name} start to eat {food}') dog = gen('alex') next(dog) # 第一次必须用next让指针停留在第一个yield后面 # 与next同样,能够获取到yield的值 ret = dog.send('骨头') print(ret) def gen(name): print(f'{name} ready to eat') while 1: food = yield print(f'{name} start to eat {food}') dog = gen('alex') next(dog) # 还能够给上一个yield发送值 dog.send('骨头') dog.send('狗粮') dog.send('香肠')
send和next()区别:
相同点:
send 和 next()均可以让生成器对应的yield向下执行一次。
均可以获取到yield生成的值。
不一样点:
第一次获取yield值只能用next不能用send(能够用send(None))。
send能够给上一个yield置传递值。
在python3中提供一种能够直接把可迭代对象中的每个数据做为生成器的结果进行返回
# 对比yield 与 yield from def func(): lst = ['卫龙','老冰棍','北冰洋','牛羊配'] yield lst g = func() print(g) print(next(g)) # 只是返回一个列表 def func(): lst = ['卫龙','老冰棍','北冰洋','牛羊配'] yield from lst g = func() print(g) # 他会将这个可迭代对象(列表)的每一个元素当成迭代器的每一个结果进行返回。 print(next(g)) print(next(g)) print(next(g)) print(next(g)) ''' yield from ['卫龙','老冰棍','北冰洋','牛羊配'] 等同于: yield '卫龙' yield '老冰棍' yield '北冰洋' yield '牛羊配'
def func(): lst1 = ['卫龙', '老冰棍', '北冰洋', '牛羊配'] lst2 = ['馒头', '花卷', '豆包', '大饼'] yield from lst1 yield from lst2 g = func() for i in g: print(i)
返回的结果是将第一个列表的元素所有返回后,在返回第二个列表