分享

Python的容器(container)、可迭代对象(Iterables)、迭代器(Iterators)、生成器(Generators)理解...

 imnobody2001 2022-05-18 发布于黑龙江

背景介绍

在学习Python的初期,对容器,迭代器,生产器等概念一直没有去梳理清楚,所以也就是一直糊里糊涂的用着Python。最近,用的多了,总是需要看一些源码文件,想通过源码文件来了解自己写的代码性能瓶颈会在哪里?此时,发现这些概念,我似乎需要仔细的梳理一下。梳理的过程中,会参阅一些参考文献,毕竟站在前人的肩膀上会看的更远嘛,在文末,会一并附上参看文献。

contrainer 容器

出自官方文档的一句定义:Some objects contain references to other objects; these are called containers.
字面意思十分明了:容器是包含其它数据类型的一种数据结构或数据类型。
因此,我们可以认为,容器一种特殊的自定义的数据结构。

容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个地迭代获取,可以用in, not in关键字判断元素是否包含在容器中。通常这类数据结构把所有的元素存储在内存中(也有一些特例,并不是所有的元素都放在内存,比如迭代器和生成器对象)那么在Python中,常见的容器有哪些?

list,
set,
dictionary,
OrderedDictionary
bytearray
array
string,
frozenset,
tuple,
bytes

以上常用的容器,各自都有自己的数据特点。这里本不应该一一阐述,但多数使用者都应该有所了解。
但是有一点似乎无法回避,那就是,哪些容器不能修改,哪些可以修改?传址和传值的对应?哪些查找的复杂度高哪些更快?

可修改与否?

我们常用的容器,如:列表,字典是可以直接修改的,即从地址上修改存储的内容;

有可以修改,就有不可以修改,不然就没有必要区别分了。不可修改的有元组tuple,类似的不可变数据类型包括整型int、浮点型float、字符串型string。

当然不可变的容器和数据类型在我理解并不是真的不可变,如果你要修改,就是改变指针指向位置,将指针指向新的内容位置,那么原始内容是不变的,随着指针的移动,就便成了废弃的了,被程序清楚。

传址与传址的概念

传值是指传入一个参数的值,传址是指传入一个参数的地址,也就是内存的地址(指针)。二者的区别是如果函数里面对传入的参数重新赋值,函数外的全局变量是否相应改变,用传值传入的参数不会改变的,用传址传入就会改变。

Python不允许程序员显性的传值还是传址操作。Python参数传递采用的是“传对象引用”的方式。这种方式也相当于传值和传址的综合形式。当函数收到的是一个可变对象(比如字典或者列表)的引用(指针),就能修改对象的原始值——相当于传址。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象——相当于传值。

所以python的传值和传址是根据传入参数的类型来选择的

传值的参数类型:数字,字符串,元组

传址的参数类型:列表,字典

所以在写程序时候一定要注意这一点,如果不小心就容易出现bug

自定义容器

Python 提供了collections容器类,(待续)

Iterables 可迭代对象

前面说的很多容器其实都是可迭代对象,此外还有更多的对象同样也是可迭代对象,比如处于打开状态的files等等。凡是可以返回一个迭代器的对象都可以称之为可迭代对象,举一个简单的例子:

>>> x = [1, 2, 3, 4]
>>> y = iter(x)
>>> z = iter(x)
>>> next(y)
1
>>> next(y)
2
>>> next(z)
1
>>> type(x)
<class 'list'>
>>> type(y)
<class 'list_iterator'>

这里x是一个可迭代对象,可迭代对象和容器一样是一种通俗的叫法,它们具有包含关系,属于概括的概念,并不是指某种具体的数据类型,list是可迭代对象,dict是可迭代对象,set也是可迭代对象。y和z是两个独立的迭代器,迭代器内部持有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。迭代器有一种具体的迭代器类型,比如list_iterator,set_iterator。可迭代对象实现了__iter__方法,该方法返回一个迭代器对象。

当运行以下代码:

x = [1, 2, 3]
for elem in x:
     ...

背后真实的调用过程如下图所示:

在这里插入图片描述

Iterators 迭代器

迭代的意思就是重复的做一些事情,例如循环的形式。任何具有__next__()方法的对象都是迭代器,对迭代器调用next()方法可以获取下一个值。next()方法不需要任何参数,如果next()方法被调用时,迭代器没有值可以返回,就会引发一个StopIteration的异常。

另外迭代器一般是__iter__() 方法返回。

所以迭代器本质上是一个产生值的工厂,每次向迭代器请求下一个值,迭代器都会进行计算出相应的值并返回。

迭代器的例子很多,例如,所有itertools模块中的函数都会返回一个迭代器,有的还可以产生无穷的序列。

>>> from itertools import count
>>> counter = count(start=13)
>>> next(counter)
13
>>> next(counter)
14

从一个有限序列中生成无限序列:

>>> from itertools import cycle
>>> colors = cycle(['red', 'white', 'blue'])
>>> next(colors)
'red'
>>> next(colors)
'white'
>>> next(colors)
'blue'
>>> next(colors)
'red'

为了更直观地感受迭代器内部的执行过程,我们定义一个迭代器,如斐波那契数列:

class Fib:
    def __init__(self):
        self.prev = 0
        self.curr = 1
 	#返回迭代器
    def __iter__(self):
        return self
 	#下一个元素
    def __next__(self):
        value = self.curr
        self.curr += self.prev
        self.prev = value
        return value
 
>>> f = Fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Fib既是一个可迭代对象(包含__iter__方法),又是一个迭代器(因为实现了__next__方法)。实例变量prev和curr用户维护迭代器内部的状态。每次调用next()方法的时候做两件事:

为下一次调用next()方法修改状态,为当前这次调用生成返回结果。迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。

Generators 生成器

生成器算得上是Python语言中最吸引人的特性之一,生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。
生成器不需要再像上面的类一样写__iter__()和__next__()方法了,只需要一个yield关键字。 生成器一定是迭代器(反之不成立),因此任何生成器也是以一种懒加载的模式生成值。用生成器来实现斐波那契数列的例子是:

def fib():
    prev, curr = 0, 1
    while True:
        yield curr
        prev, curr = curr, curr + prev

>>> f = fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

fib就是一个普通的python函数,它特殊的地方在于函数体中没有return关键字,函数的返回值是一个生成器对象。当执行f=fib()返回的是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用next的时候才会真正执行里面的代码。

来剖析代码:首先,fib是一个很普通的函数,但是函数中没有return语句,函数的返回值是一个生成器。

当调用f = fib()时,生成器被实例化并返回,这时并不会执行任何代码,生成器处于空闲状态,注意这里prev, curr = 0, 1并未执行。

然后这个生成器被包含在isslice()中,而这又是一个迭代器,所以还是没有执行上面的代码。

然后这个迭代器又被包含在list()中,它会根据传进来的参数生成一个列表。所以它首先对isslice()对象调用next()方法,isslice()对象又会对实例f调用next()。
我们来看其中的一步操作,在第一次调用中,会执行prev, curr = 0, 1, 然后进入while循环,当遇到yield curr的时候,返回当前curr值,然后又进入空闲状态。
生成的值传递给外层的isslice(),也相应生成一个值,然后传递给外层的list(),外层的list将这个值1添加到列表中。
然后继续执行后面的九步操作,每步操作的流程都是一样的。

然后执行到底11步的时候,isslice()对象就会抛出StopIteration异常,意味着已经到达末尾了。注意生成器不会接收到第11次next()请求,后面会被垃圾回收掉。

生成器在Python中是一个非常强大的编程结构,可以用更少地中间变量写流式代码,此外,相比其它容器对象它更能节省内存和CPU,当然它可以用更少的代码来实现相似的功能。现在就可以动手重构你的代码了,但凡看到类似:

def something():
    result = []
    for ... in ...:
        result.append(x)
    return result

都可以用生成器函数来替换实现,效果会更好:

def iter_something():
    for ... in ...:
        yield x

生成器的类型

在Python中生成器有两种类型:生成器函数以及生成器表达式。生成器函数就是包含yield参数的函数。生成器表达式与列表解析式类似。
假设使用如下语法创建一个列表:

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> [x * x for x in numbers]
[1, 4, 9, 16, 25, 36]

使用set解析式也可以达到同样的目的:

>>> {x * x for x in numbers}{1, 4, 36, 9, 16, 25}

或者dict解析式:

>>> {x: x * x for x in numbers}
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}

还可以使用生成器表达式:

>>> lazy_squares = (x * x for x in numbers)
>>> lazy_squares
<generator object <genexpr> at 0x10d1f5510>
>>> next(lazy_squares)
1
>>> list(lazy_squares)
[4, 9, 16, 25, 36]

注意我们第一次调用next()之后,lazy_squares对象的状态已经发生改变,所以后面后面地调用list()方法只会返回部分元素组成的列表。

总结

生成器是Python中一种非常强大的特性,它让我们能够编写更加简洁的代码,同时也更加节省内存,使用CPU也更加高效。

容器,迭代对象,迭代器,生成器具有着一种必然的联系,是一种进步和推广。

容器是一系列元素的集合,str、list、set、dict、file、sockets对象都可以看作是容器,容器都可以被迭代(用在for,while等语句中),因此他们被称为可迭代对象。

可迭代对象实现了__iter__方法,该方法返回一个迭代器对象。

迭代器持有一个内部状态的字段,用于记录下次迭代返回值,它实现了__next__和__iter__方法,迭代器不会一次性把所有元素加载到内存,而是需要的时候才生成返回结果。

生成器是一种特殊的迭代器,它的返回值不是通过return而是用yield。

学习一个东西,梳理好内在联系,深层次理解一个概念很重要。

参考文献:

1.https:///posts/iterators-vs-generators/
2.https://docs./2/library/stdtypes.html#iterator-types
3.http://python./87805/
4.https:///iterators-vs-generators.html
5.https://blog.csdn.net/github_39261590/article/details/74002290

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多