一、推导式
(一).列表推导式(集合推导式也同理于此)
(1).例1:基本的列表推导式
(2).例2:利用列表推导式,取出1-20内所有偶数
li = [i for i in range(1, 21) if i % 2 == 0] # 如果只有一个条件,要把if语句写在最后面 # 第一个i是放入列表的值,后面都是推导的公式
print(li) # 第一个 i 相当于下面等价方案中的 append(i)。把 i 换成 "a" ,输出的列表中全是 "a"
# 上面的列表推导式等价于: for i in range(1,21): if i%2==0: li.append(li) print(li)
(3).例3:利用列表推导式,取出1-20内所有数,其中奇数用字符"a"代替,偶数则正常输出
li = [i if i % 2 == 0 else "a" for i in range(1, 21)] # 添加多个条件的时候,要写在前面 print(li)
(4).例4:列表推导式练习:将列表li=['a','b','c','d','e']倒序
# print([li.pop() for i in range(len(li))]) # pop() -> remove and return the last element
(二).字典推导式
(1).例1:
a = {str(i): i for i in range(5)} print(a) # {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
(2).例2:
a = {"i": i for i in range(5)} print(a) # {'i': 4}
注意:因为字典的键是唯一的,此例的字典推导式中,把键写死了,每一次推导其实都是在修改原先的值。
(3).例3:
print({i: 'a' for i in range(5)}) # {0: 'a', 1: 'a', 2: 'a', 3: 'a', 4: 'a'}
此例中是把值写死了,相当于dict.fromkeys([0,1,2,3,4],"a")
(4).例4:
bag = [2, 3, 1, 2, 5, 6, 7, 9, 2, 7] print({i: bag.count(i) for i in bag}) # {2: 3, 3: 1, 1: 1, 5: 1, 6: 1, 7: 2, 9: 1} # 开销很大,因为每次count()时,都会对列表进行遍历
更佳的方式,是采用已有的库:
from collections import Counter bag = [2, 3, 1, 2, 5, 6, 7, 9, 2, 7] count = Counter(bag) print(type(count)) # <class 'collections.Counter'> print(dict(count)) # {2: 3, 3: 1, 1: 1, 5: 1, 6: 1, 7: 2, 9: 1}
count是一个类似字典的对象
(5).例5:
(三).小括号包裹的话,是返回一个生成器对象:
print((i for i in range(5))) # <generator object <genexpr> at 0x01760960>
可以用tuple()或者list()来查看:
print(tuple(i for i in range(5))) # (0, 1, 2, 3, 4)
(四).小技巧
(1).对推导式中的元素进行运算操作
""" 你有一个list: bag = [1, 2, 3, 4, 5] 现在你想让所有元素翻倍,让它看起来是这个样子:[2, 4, 6, 8, 10] """ print([i*2 for i in bag]) # [2, 4, 6, 8, 10]
二、迭代器(iterator)
迭代器就是一种实现了迭代器协议的对象,是一个伴随着迭代器模式(Iterator Pattern)而生的抽象概念。
其目的是:分离并统一不同的数据结构访问其中数据的方式,从而使得各种需要访问数据结构的函数,对于不同的数据结构可以保持相同的接口。
迭代器存在的意义,不是为了空间换时间,也不是为了时间换空间,而是一种适配器(Adapter)。迭代器的存在,使得我们可以使用同样的for语句去遍历各种容器,或者说:使用同样的接口去处理各种容器。
这些容器可以是一个列表,甚至是一个完全不连续内存的链表或是哈希表等等,我们完全不需要关注迭代器对于不同的容器究竟是怎么取得数据的。
在Python中,迭代器基于鸭子类型(Duck Type)下的迭代器协议(Iterator Protocol)实现。迭代器协议规定:如果一个类想要成为可迭代对象(Iterable Object),则其必须实现__iter__方法,且其返回值需要是一个实现了__next__方法的对象。
迭代器只能进行单向,单步前进操作,且不可作为左值,属于单向只读迭代器。
一旦向前移动便不可回头,如果遍历一个已耗尽迭代器,则for循环将直接退出,且无任何错误产生。但如果再次使用已穷尽的迭代器,则会报错!
迭代器它也是一个可被迭代的对象,同时它是一个返回数据的对象,但一次只返回一个元素,拥有着一边循环一边计算的机制。
一个迭代器,它必须同时实现__iter__(iterable)和__next__(iterator)这两个魔法方法,这是迭代器的协议。
__iter__()是内建函数iter(iterable)的隐式调用,将一个可迭代对象构建成一个迭代器。
__next__()是内建函数next(iterator)的隐式调用,实现迭代器,本质是去一个迭代器中进行"取值"操作,取出迭代器的下一个元素。
(一).构建一个迭代器
(1).使用内建函数iter()
内建函数iter()会为自动地将一个可迭代对象构建成一个迭代器,python会自动去调用__iter__(),并同时实现__next__()
# define a list my_list = [4, 7, 0, 3] # get an iterator using iter() my_iter = iter(my_list) # iterate through it using next() # prints 4 print(next(my_iter)) # prints 7 print(next(my_iter)) # next(obj) is same as obj.__next__() # prints 0 print(my_iter.__next__()) # prints 3 print(my_iter.__next__()) # This will raise error, no items left next(my_iter) """ Traceback (most recent call last): File "D:/python_local/test2.py", line 28, in <module> next(my_iter) StopIteration """
迭代器对象必须要有一个变量去接收它,next(iterator)隐式去调用__next__(),__next__()是类中的魔法方法,没有给next()指定迭代器对象,去哪个对象里next元素?
(2).自定义构建一个迭代器
一个迭代器必须同时实现__iter__()和__next__():
__iter__()返回迭代器对象本身,把一个可迭代的对象变成迭代器。如果需要,可以执行一些初始化操作。
__next__()返回序列中的下一个元素。当序列穷尽时,如果再继续next(),那么就会抛出StopIteration异常。通俗理解,__next__就是在迭代器中进行"取值"。
class MyIter: def __iter__(self): self.num = 1 return self def __next__(self): num = self.num self.num += 2 return num mi = MyIter() mi.__iter__() # 必须显示调用,不然无法变成一个迭代器 while 1: print(next(mi)) """ 1 3 5 7 ... 直到因撑爆内存后,死机了才会结束 """
(3).去迭代器进行"取值""
可以使用next()手动一个个去取,也可以使用for循环对迭代器对象进行遍历。
(二).查看一个对象是否为迭代器
迭代器对象必须同时拥有__iter__和__next__
(1).使用dir()函数
my_list只出现了__iter__,而没有__next__,那它只可一个可迭代对象,并不是一个迭代器对象。
之后的iter(my_list)操作,python会隐式地去将my_list构建成为一个迭代器对象。
my_iter同时出现了__iter__和__next__,那么它就是一个迭代器对象。
# 枚举就是个迭代器 g = enumerate([1, 2, 3]) print(dir(g)) # dir(g)可以看到有__iter__和__next__
(三).迭代器只能往前,不能后退
例:
li = [1, 2, 3] g = iter(li) print(next(g)) # 打印出1,把第一个元素取出了 print("---") for i in g: # 这里只会取出后面的2个值 print(i) # 2 3
此例中,当取出迭代器中的第一个元素后,第一个元素就不存在了,"定位"定到了第二个元素。当使用for循环遍历时,因为第一个元素已经不存在了,所以就会从"定位"的第二个元素开始取值,然后全部取完。
(四).迭代器穷尽,则将永久损坏
先看下图中的案例:
数据来源于(二)-(1)中的my_iter,之前没有对my_iter进行任何的取值操作。
当for循环去遍历一个迭代器后,迭代器就耗尽了,将被标为耗尽状态。耗尽状态的迭代器不可以再使用,即使继续往容器中增加元素也不行。
如果遍历一个已耗尽迭代器,则for循环将直接退出,且无任何错误产生。但是,next()取不到元素了,就会抛出StopIteration异常。
示例:
个人认为:Python的迭代器中可能存在某种用于指示迭代器是否被耗尽的标记,一旦迭代器被标记为耗尽状态,便永远不可继续使用了(除非重新创建并赋值给一个变量)。
(五).迭代器有效性
由于迭代器本身并不是独立的数据结构,而是指向其他数据结构中的值的泛化指针,一旦指针指向的内存发生变动,则迭代器也将随之失效。
如果迭代器指向的数据结构是只读的,那么直到析构函数(__del__)被调用,迭代器都不会失效。但如果迭代器所指向的是可变的数据结构,那么在其存在时发生了修改操作,则迭代器将可能失效。
(1).List迭代器
List迭代器一旦穷尽,如果再进行添加操作,就会报错。因为一个迭代器一旦穷尽,就会永久性损坏,不能对它进行next操作了。如下示例:
List迭代器没有死掉之前,都可以对其中的List对象进行元素添加操作,next()的时候,只要后面有值,依然会往后走。
总结:List类型的迭代器,应该不是运用指针原理,而是下标原理。取值的时候记录这个元素的下标,如果此时在这个元素的前面,进行了插入操作,那么下标的值依然还是之前的那个下标,等到再次next的时候,只是在原来的下标基础上,往后走而已。
下标的值不变,在这个元素前面添加item,就相当于把整个List往后推了,下次next的时候,只是取了当前下标后面那个下标的元素。
简单来说:List类型的迭代器,只要还能让next()取到值,你可以尽情地耍它。无论往后面append()还是extend(),只要后面有元素,next()的时候,就会继续往下走,取出元素。但如果抛出StopIteration异常之后,再去添加,那么就没用了。
一般不会去做一边遍历一边修改list的事。
(2).插入、删除操作将损坏dict迭代器
(3).set与dict具有相同的迭代器失效性质
(六).迭代器不同于可迭代对象
迭代器和可迭代对象都是可被遍历的对象,但两者有着本质的区别。
(1).首先,迭代器比可迭代对象多了一个专属的魔法方法:__next__()。可迭代对象中,只有__iter__(),而没有__next__()
(2).迭代器的特性是惰性计算,创建迭代器的时候不会马上就生成所有的元素,只在对迭代器进行实质性地操作时,才会生成元素,而且是边生成边返回,一次只返回一个元素。
而可迭代对象,在创建的时候就将所有的已有元素,都放进内存中去了。
(3).迭代器使用完毕后,序列中的元素就不复存在,想要再从头取值,必须进行手动重新创建。而可迭代对象,只要这个对象不被销毁,就可以一直使用。
(4).迭代器只能往前不能后退,而可迭代对象可以随意操作前进后退。
(5).迭代器不可以被循环遍历两次、不能访问其长度,也不能使用索引。
(七).示例
(1).例1:
首先迭代器是一个可以被迭代的对象,所以符合zip的规则。同时,此例很好地证明了,迭代器是逐个计算返回的特性。
第一次组合的时候,先返回1,再返回2,于是组成了(1,2);第二次组合的时候,先返回3,再返回4,于是组成了(3,4)
(八).无限迭代器
itertools模块中实现了三个特殊的无限迭代器(Infinite Iterator):count、cycle、repeat,其有别于普通的表示范围的迭代器。如果对无限迭代器进行迭代将导致无限循环,故无限迭代器通常只可使用next函数进行取值。
(九).参考文献
https://www.programiz.com/python-programming/iterator
https://zhuanlan.zhihu.com/p/34157478
https://zhuanlan.zhihu.com/p/39640305
三、生成器(generator)
(一).生成器的本质
生成器的本质就是迭代器,主要目的:提供一个基于函数而不是类迭代器的定义方式。
生成器它有两种表现形式:(1).用小括号包裹起来的推导式;(2).函数中出现了yield这个关键字。
生成器一旦被构造,其会自动实现完整的迭代器协议,调用此构造函数即可产生一个生成器对象。
生成器与迭代器一样,有同样的两种取值方式:next()或for循环遍历。
(二).yield关键词
只要函数中,出现了yield关键词,此函数将不再是一个函数,而成为一个“生成器构造函数”,调用此构造函数即可产生一个生成器对象。
当函数中出现了yield关键词时,next()或for循环遍历操作生成器的时候,每次遇到yield时,函数会暂停并保存当前所有的运行信息,然后返回紧跟在yield后面的值。当下一次执行next()时,就从当前位置继续运行。
(三).生成器和迭代器的共同点
生成器和迭代器最大的优点就是节省内存开销,因为生成器同样具有惰性计算的特性,只在需要计算的时候才计算。而且计算完就释放,无法再次去循环遍历。
(四).生成器和迭代器的区别
生成器的本质就是迭代器,硬要说区别,我个人的理解就是两者的表现形式不一样。生成器只有两种表现形式,而迭代器的表现形式则是多样的,enumerate、zip、reversed和其他一些内置函数会返回迭代器
(五).示例
如下演示一个简单的生成器:
def fun(): i = 1 while i < 5: print("before yield", i) # yield # 实现生成器的功能。1、暂停,2、返回值。 yield "yield message:pause" # 将会返回一个信息 i += 1 print("after yield", i, end=" ") # 函数体,因为并没有去调用这个函数 print(fun) # <function fun at 0x0032E810> # 函数中有yield这个关键词,那么这个函数就是生成器了 print(fun()) # <generator object fun at 0x002900B0> print("-----------------") print(next(fun())) print(next(fun())) print(next(fun())) """ 打印结果: before yield 1 yield message:stop before yield 1 yield message:stop before yield 1 yield message:stop 相当于每一次都重新创建了生成器,每一次都在操作新的生成器,所以结果一直都是1 """ print("-----------------") for i in fun(): print(i) """ before yield 1 yield message:stop after yield 2 before yield 2 yield message:stop after yield 3 before yield 3 yield message:stop after yield 4 before yield 4 yield message:stop after yield 5 yield会暂停函数的运行并返回紧跟在后面的值,相当于打了一个断点,停在了断点处。 当下一次next()或者是遍历的时候,会从断点处恢复,并往下执行。 """
(六).陷阱之一:部分消耗生成器
两次询问9是否存在于同一个生成器中,得到了不同的答案。
这是因为,第一次询问时,Python已经对这个生成器进行了遍历,也就是调用next()函数去逐个逐个地查找9,找到后就会返回True。当第二次再询问9是否存在时,会从上次的位置继续next()查找。
(七).练习
(1).阅读以下代码,写出程序的输出内容
def func(): print(111) yield 222 yield 333 g = func() g1 = (i for i in g) g2 = func() g3 = (i for i in g2) g4 = (i for i in g3) print(list(g)) print("-" * 20) print(list(g1)) print("-" * 20) print(list(g2)) print("-" * 20) print(list(g3)) print("-" * 20) print(list(g4))
答案:
111 [222, 333] -------------------- [] -------------------- 111 [222, 333] -------------------- [] -------------------- []
原因:生成器的本质就是迭代器,与迭代器的协议一样:一次性消费。一次遍历后生成器便永久性损坏,再次用去遍历会得到一个None结果,如果next()则会报异常StopIteration。
(八).参考文献
https://www.programiz.com/python-programming/generator
https://zhuanlan.zhihu.com/p/39640305
五、模块和包
(一).模块:本质上就是py文件。分为:内置模块,第三方模块。
(1).内置模块的使用:
导入所有内容:import modulename;很直观,但很占内存。
指定导入:from modulename import funcationname;明确知道自己要用什么方法。
(2).自定义模块的使用:
与当前py文件是同级路径:直接导入。
不同路径导入的参考:
import sys # 别忘了先导入这个 sys.path # 路径先调出来。返回一个列表,是python的安装目录 sys.path.append(r"") # 小括号内可添加自己py文件的绝对路径,记得取消转义 # 再 import modulename 就可以了
(3).得在自己写的py文件的最后一行加入:
if __name__ == '__main__': functionname1() functionname2()
# 有这段代码。测试是本身就有,还是导入进来的。
# 一定要对这个py文件本身执行,运行了,才会有结果。
(二).包:很多py文件放在一个文件夹中。
(三).if __name__ == '__main__':
就是个if判断,'__main__'就是个字符串,判断是导入的还是直接执行的。
当import一个py模块(文件)的时候,会把那个py模块(文件)执行一遍。
例1:
我有一个"test1.py"的模块(文件),如下代码:
import test2 print(__name__) # __main__ print(test2.__name__) # test2 """ 运行结果: zyb111 __main__ test2 """
有另一个"test2.py"的文件,如下代码:
print("zyb111") if __name__ == '__main__': print("zyb222")
"test1.py"中,import了"test2",那么"test2.py"就被执行了一遍。所以在"test1.py"的运行结果中,会出现zyb111,因为 import test2 的时候,"test2.py"被执行了一遍。
为什么打印不出zyb222?
"test2.py"是被引入进"test1.py"中的。"test2.py"中就有了if判断,判断的结果:它们两个不是同一个name。
看"test1.py"文件中的这条代码 print(test2.__name__),这条代码特意显示了一下"test2.py"是什么名字。返回的结果是 test2,但现在执行的是"test1.py"这个文件呀!"test1"=="test2"吗?显然是False,那就不会有zyb222了。
(四).相对路径导入
(五).限制外部import
当发布python第三方包时, 有时候并不是希望代码中所有的函数或者类可以被外部import。
在__init__.py中添加一个__all__列表,该列表中填写可以import的类或者函数名,可以起到限制import的作用,防止外部import其他函数或者类。
例如: