python 可迭代对象、迭代器对象、生成器

迭代器对象 VS 可迭代对象 VS 生成器

为何在 p y t h o n python python中可以很方便的使用 f o r for for循环来遍历列表、字符串甚至是文件?它的背后实现离不开迭代器可迭代对象

迭代器协议

首先来谈谈 p y py py中的迭代器协议:

  • 实现内置函数 i t e r iter iter,它返回一个迭代器对象。
  • 通过调用迭代器对象的 n e x t next next函数,取得每次迭代的内容。
  • 多次调用 n e x t next next函数后,抛出 S t o p I t e r a t i o n StopIteration StopIteration异常(迭代终止)。

可以猜测 p y t h o n python python f o r for for循环的语义如下:


lst = [1, 2, 3]

for v in lst:
    print v

it = iter(lst)
try:
    while True:
        v = it.next()
        print v
except StopIteration:
    pass

从上面的描述中,我们也能看出一些端倪:迭代器对象必须实现 i t e r iter iter n e x t next next函数。那么只实现了 i t e r iter iter函数的算什么呢?其实他们就是可迭代对象。可迭代对象也是可以直接用 f o r for for循环遍历的,因为他们的 i t e r iter iter函数会返回一个迭代器对象,只要迭代器对象实现了 n e x t next next方法就可以了。也就是说迭代器对象是可迭代对象的子集
在这里插入图片描述
下面通过一段代码来加深对他们的认识:

注:本文中的代码如非特殊标注,运行环境均为 p y t h o n   2.7 python\ 2.7 python 2.7

from collections import Iterator, Iterable

lst = [1, 2, 3]

list_it = iter(lst)

print isinstance(lst, Iterator)
print isinstance(lst, Iterable)

print isinstance(list_it, Iterator)
print isinstance(list_it, Iterable)

res1 = []
for i in xrange(2):
    for j in lst:
        res1.append(j)

res2 = []
for i in xrange(2):
    for j in list_it:
        res2.append(j)

print res1
print res2

在这里插入图片描述
可以看出来 p y t h o n python python内置的列表是一个可迭代对象,但不是迭代器。通过调用其 i t e r iter iter函数可以返回一个迭代器对象,而且每次循环都会耗尽一个迭代器对象,因此在大部分情况下,我们不能重复迭代同一个迭代器对象

下面继续看一段代码:


lst = [1, 2, 3]

list_it = iter(lst)

print next(list_it)  # lst[0]
lst[1] = 10
print next(list_it)  # lst[1]
lst.pop()
print next(list_it)  # the end of list

在这里插入图片描述
据此可以猜测出列表的 i t e r iter iter函数的实现:无非就是创建了一个新的对象,让其存有自身的引用以及当前的索引位置,然后在 n e x t next next函数中递增索引,当达到列表末尾时抛出异常。因为整个过程中没有创建新的列表对象——仅创建了一个迭代器对象,因此空间占用非常少。不过要注意,当原数组发生变化时,迭代器的内容也会发生变换。

接下来,让我们实现自己的迭代器:

# coding=utf-8
from collections import Iterable, Iterator


class TestA(object):

    def __init__(self, dis=1, n=3):  # 一个等差数列
        self.value = 0
        self.dis = dis
        self.num = n

    def __iter__(self):
        return self

    def next(self):
        if self.num == 0:
            raise StopIteration
        self.num -= 1
        tmp = self.value
        self.value += self.dis
        return tmp


class TestB(object):

    def __init__(self, dis=1, n=3):
        self.dis = dis
        self.num = n

    def __iter__(self):  # 一般不推荐该做法
        return TestA(self.dis, self.num)


a = TestA()
print isinstance(a, Iterable)
print isinstance(a, Iterator)

res1 = []
for i in xrange(2):
    for j in a:
        res1.append(j)

b = TestB()
print isinstance(b, Iterable)
print isinstance(b, Iterator)

res2 = []
for i in xrange(2):
    for j in b:
        res2.append(j)

print res1
print res2

在这里插入图片描述
在上述代码中, T e s t A TestA TestA是一个真正的迭代器对象, T e s t B TestB TestB是一个可迭代对象,其 i t e r iter iter方法会创建一个 T e s t A TestA TestA对象并返回。不过一般不太推荐这种写法,尤其在合作开发时,因为别人写的类可能会变动,从而导致错误。对于内置类型的话,使用这种方法还是比较方便的。

生成器

ok,接下来让我们看看 p y py py中的另外一个概念——生成器。据官方文档所言,生成器是一个用于创建迭代器的简单而强大的工具。 它们的写法类似于标准的函数,但当它们要返回数据时会使用 yield 语句。 每次在生成器上调用 next() 时,它会从上次离开的位置恢复执行(它会记住上次执行语句时的所有数据值)。

也就是说,生成器一定是迭代器
在这里插入图片描述
话不多说,直接上代码:

# coding=utf-8
from collections import Iterable, Iterator


def testFunc():
    lst = [1, 2, 3]
    for ele in lst:
        yield ele


def normalFunc():
    pass


fun = testFunc()
print isinstance(fun, Iterator)
print isinstance(fun, Iterable)
print hasattr(fun, '__call__')

normal_fun = normalFunc
print isinstance(normal_fun, Iterator)
print isinstance(normal_fun, Iterable)
print hasattr(normal_fun, '__call__')

print next(fun)
print fun.next()
print fun.next()
print next(fun)

在这里插入图片描述
可以看到生成器和一般的函数还是有区别的:生成器实现了迭代器协议,但是没有实现 c a l l call call方法,生成器的局部变量和执行状态会在每次调用之间自动保存,但普通函数每次都会重新开始执行。

再看一段代码:

# coding=utf-8

def testFunc():
    lst = [1, 2, 3]
    for ele in lst:
        yield ele


print testFunc()
print testFunc()
print testFunc()

在这里插入图片描述
为什么结果是这样的?难道每次调用生成器函数返回的都是同一个对象吗?显然不是,出现这个结果的原因可能是内存局部性,因为上述输出语句创建了一个生成器对象,但是在语句结束后这个对象的引用计数就变成0了,于是它会被销毁掉,然后由于内存局部性,新创建的对象大概率还是会在原来的内存上。

我们可以测试一下:
在这里插入图片描述
ok,最后再简单的提一下生成器表达式。它和列表推导很像,只不过返回的结果是一个生成器,也因此更加节省内存:

# coding=utf-8

a = (i for i in xrange(5))
print a
print list(a)

在这里插入图片描述
不过我们依然要注意之前提到的小细节:

# coding=utf-8

lst = [1, 3, 5]
a = (ele for ele in lst)
print a
lst[1] = 10
print a
print list(a)

在这里插入图片描述

用闭包实现迭代器

我们还可以使用闭包来实现迭代器,这牵扯到 i t e r iter iter函数的第二种使用方式: i t e r ( c a l l a b l e , s e n t i n e l ) iter(callable,sentinel) iter(callable,sentinel)。当使用这种方式时,第一个参数必须是一个可调用对象,第二个参数是哨兵,也可以理解为终止符号。这种情况下生成的迭代器,每次迭代调用它的 n e x t ( ) next() next() 方法时都会不带实参地调用 c a l l a b l e callable callable;如果返回的结果是 s e n t i n e l sentinel sentinel 则触发 S t o p I t e r a t i o n StopIteration StopIteration,否则返回调用结果。

实现方式如下(运行环境为 p y t h o n 3 python3 python3):

# coding=utf-8

class Test(object):

    def __init__(self, *data):
        self.data = list(data)

    def __iter__(self):
        index = 0

        def getElement():
            ele = None
            nonlocal index
            if index < len(self.data):
                ele = self.data[index]
            index += 1
            return ele

        return iter(getElement, None)


t = Test(1, 2, 3)

for i in range(3):
    res = [val for val in t]
    print(res)

如果在 p y t h o n 2 python2 python2中这段代码该如何实现呢?如果直接把运行环境改为 p y 2 py2 py2,大概率会看到语法错误的提示:
在这里插入图片描述
其背后原理涉及到 p y t h o n python python中的名空间和名空间查找规则。先来简单认识一下 p y py py中的命名空间

  • 命名空间(Namespace)是从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。
  • 命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。

p y py py中的名空间可以分为以下几种类型:

  • 局部名空间( l o c a l   n a m e s local\ names local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。类中定义的同理。
  • 全局名称( g l o b a l   n a m e s global\ names global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
  • 内置名称( b u i l t − i n   n a m e s built-in\ names builtin names), P y t h o n Python Python 语言内置的名称,比如函数名 a b s 、 c h a r abs、char abschar 和异常名称 B a s e E x c e p t i o n 、 E x c e p t i o n BaseException、Exception BaseExceptionException等等。

那么名空间查找规则会按照 L E G B LEGB LEGB的顺序进行,即: l o c a l − > e n c l o s i n g − > g l o b a l − > b u i l t − i n local->enclosing->global->built-in local>enclosing>global>builtin e n c l o s i n g enclosing enclosing其实就是闭包外的函数的作用域,对于上述例子中的 g e t E l e m e n t getElement getElement来说,就是函数 i t e r iter iter的作用域。当然这个还取决于嵌套的层级,反正按照这个规则一层一层往上找就完事了。不过有一点需要注意,那就是写操作会打断向外查找的过程。因此在上述示例中, g e t E l e m e n t getElement getElement中的 i n d e x index index实际位于该函数的局部名空间,但是它在赋值前先被引用了,因此产生了错误。

有一个处理方式就是把 i n d e x index index定义为一个列表(只有一个元素),这样在闭包内修改第一个元素的值就不会报错了,不过代码看起来会很丑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值