• 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="
    ")
    
    
    # 函数体,因为并没有去调用这个函数
    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其他函数或者类。

    例如:

  • 相关阅读:
    UltraEdit 注册机使用说明
    sybase 收集常用sql语句
    过渡模式
    JavaScript 钩子
    自定义过渡的类名
    CSS过渡、CSS动画
    单元素/组件的过渡
    通过 v-once 创建低开销的静态组件
    内联模板、X-Template
    程序化的时间侦听器
  • 原文地址:https://www.cnblogs.com/quanquan616/p/8343992.html
Copyright © 2020-2023  润新知