Python 推导式、迭代器、生成器、模块和包

一、推导式

(一).列表推导式(集合推导式也同理于此)

(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()时,都会对列表进行遍历
View Code

更佳的方式,是采用已有的库:

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}
View Code

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]
View Code

 

二、迭代器(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
"""
View Code

迭代器对象必须要有一个变量去接收它,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
...
直到因撑爆内存后,死机了才会结束
"""
View Code

(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
View Code

此例中,当取出迭代器中的第一个元素后,第一个元素就不存在了,"定位"定到了第二个元素。当使用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="\n")


# 函数体,因为并没有去调用这个函数
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()或者是遍历的时候,会从断点处恢复,并往下执行。
"""
View Code

(六).陷阱之一:部分消耗生成器

两次询问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))
View Code

答案:

111
[222, 333]
--------------------
[]
--------------------
111
[222, 333]
--------------------
[]
--------------------
[]
View Code

原因:生成器的本质就是迭代器,与迭代器的协议一样:一次性消费。一次遍历后生成器便永久性损坏,再次用去遍历会得到一个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其他函数或者类。

例如:

 

posted @ 2018-01-24 23:05  root01_barry  阅读(225)  评论(0编辑  收藏  举报