python中可迭代对象,迭代器和生成器实例详解

这几天在弄scrapy爬虫的时候,发现scrapy将爬取的网页用for循环去对每一小块进行处理,但是为了尽量少占用内存,在循环体内采用的是yield代替的return,从而通过生成器的方式实现了异步非阻塞的流水作业,边爬取边解析。这一篇就从原理来说一说python中必须要掌握但是又不太好区分的三个概念:可迭代对象,迭代器和生成器。

生成器

既然提到了生成器和yield,就先从它开始说起。不一次性生成全部结果,而是根据某种算法在需要的时候再推算出后续元素的方式,叫做生成器(generator)。

举了简单例子,某个函数调用一次可以返回列表[1,2,3],而另一个函数,调用第一次返回1,第二次返回2,第三次返回3,每次调用之间可以执行别的任务。第二个函数就是一个生成器。

生成器的好处之一就是节约内存。想想一次计算要生成1万个元素的列表,如果等这1万个元素都生成再读进内存,不仅耗时而且占内存。而每生成一个元素就往后先执行,等下一个元素生成了再回来读取,就不用一次占用那么多的内存,同时也节约了时间。

但是前提条件就是后续操作是针对生成的单个元素的,假设后续操作是要对上面的一万个元素进行排序,那就必须得等所有元素生成完毕,这时生成器就不适用了。

补充:对于大文件无法一次性读进内存来进行排序操作感兴趣的朋友,可以参考另一篇博客《python3利用归并算法对超过内存限制的超大文件进行排序》

生成器函数

创建一个生成器的第一种方式是带yield的函数,以耳熟能详的斐波那契数列举例,下面是个普通函数

def fib_list(n):
    """返回列表"""
    a = 0
    b = 1
    result = []
    for i in range(n):
        a, b = b, a + b
        i += 1
        result.append(a)
    return result

fib=fib_list(8)
print(fib)  # [1, 1, 2, 3, 5, 8, 13, 21]

如果去掉return,将每一次希望返回的值用yield关键字来返回,如下

def fib_generator(n):
    """返回生成器"""
    a = 0
    b = 1
    for i in range(n):
        a, b = b, a + b
        i += 1
        yield a

调用这个函数会返回一个生成器

fib = fib_generator(8)
print(fib)  # <generator object fib_generator at 0x0000024D1579C048>

想要逐个获取生成器的元素,可以调用生成器的__next__()方法,或者直接用内置next()函数,例如

fib = fib_generator(8)
print(next(fib))  # 1
print(next(fib))  # 1
print(fib.__next__())  # 2
print(fib.__next__())  # 3
print(fib.__next__())  # 5
print(next(fib))  # 8
print(next(fib))  # 13
print(next(fib))  # 21

每次生成器都会在yield关键字处阻塞,下次调用的时候再继续执行。

如果想获取全部元素,直接用for循环

fib = fib_generator(8)
for i in fib:
    print(i)

这里还要提一下,有的时候还会对生成器进行send(value)操作,该操作会传递一个值到yield的位置,可以用于赋值给变量,但是要注意的是,一旦yield进行了赋值就只能返回send进去的内容,例如

def fib_generator(n):
    """返回生成器"""
    a = 0
    b = 1
    for i in range(n):
        a, b = b, a + b
        i += 1
        # yield a
        tmp = yield a
        print(tmp)
        
fib = fib_generator(8)
fib.__next__()
fib.__next__()  # None
fib.send('test')  # test
fib.__next__()  # None
fib.__next__()  # None

第一次调用__next__()程序停在了yield处,但没有任何操作,第二次调用程序首先返回了None给tmp,然后打印tmp又再次停在yield处。依次类推,每次都是打印传递进去的内容。

实际使用中主要还是从程序获取返回值,send用的并不多。

生成器表达式

创建一个生成器的第二种方式就是生成器表达式,非常简单,就是将列表生成器的中括号换成小括号。

例如

# result = [x ** 2 for x in range(5)]
result = (x ** 2 for x in range(5))
print(result)  # <generator object <genexpr> at 0x00000148B6671DC8>

顺便温习一下列表生成器,基本上记住一下四种格式即可

result = [x ** 2 for x in range(5)]
result1 = [x + y for x in range(3) for y in range(4)]
result2 = [x for x in range(10) if x % 2 == 0]
result3 = [x if x % 2 == 0 else x + 1 for x in range(10)]
print(result)  # [0, 1, 4, 9, 16]
print(result1)  # [0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5]
print(result2)  # [0, 2, 4, 6, 8]
print(result3)  # [0, 2, 2, 4, 4, 6, 6, 8, 8, 10]

获取元素的方式也是调用__next__()或者是for循环,就不再展开了。

迭代器

生成器的本质是一种特殊的迭代器(iterator)。

在python中,任何实现了__next__()__iter__()方法的都是迭代器,而这两个方法分别保证了前面提到的获取单个元素和for循环的实现。

from collections.abc import Iterator

class MyIterator:
    def __init__(self):
        pass

    def __next__(self):
        pass

    def __iter__(self):
        pass


myIterator = MyIterator('this is crazy')
print(isinstance(myIterator,Iterator))  # True

但是通常情况下很少会自己去创建迭代器类,而是要么直接使用生成器,要么用iter()方法将一个容器,例如列表,字符串等,变成迭代器

a = [1, 2, 3]
b = iter(a)
print(isinstance(a, Iterator))  # False
print(isinstance(b, Iterator))  # True

可迭代对象

既然说到了iter()方法,就可以引出可迭代对象(iterable)的概念了。

迭代,就是从迭代器中依次获取元素的意思。可迭代对象,就是能够变成迭代器的一个对象。在python中,凡是能够通过iter()方法返回一个迭代器的都是可迭代对象

那么iter()方法究竟做了什么呢?首先会检查对象是否实现了__iter__()方法,如果实现了就调用它,返回一个迭代器;如果没有实现__iter__()但是实现了__getitem__(),并且可以返回从下标0开始的元素,就会调用它,返回一个迭代器;如果前面两个方法都没有,就会抛出异常。

所以想要成为迭代器,必须能通过__iter__()方法或者__getitem__()方法返回一个迭代器。但是只有实现了__iter__()方法的对象可以用isinstance(xx,Iteratable)来判断。

下面直接用实例来演示下。

实例演示

首先创建如下类,其实现了__getitem__()方法,自带的一个参数就是下标,要确保可以从0开始进行元素获取

class MyIterable:
    def __init__(self, n):
        self.n = n

    def __getitem__(self, item):
        return range(self.n)[item]
    
myIterable = MyIterable(8)
myIterator = iter(myIterable)

print(isinstance(myIterator, Iterator))  # True
print(isinstance(myIterable, Iterable))  # False
for i in myIterator:
    print(i, end=' ')  # 0 1 2 3 4 5 6 7

可以发现,单独用__getitem__()虽然可以达成Iterable的效果,但是并不能用isinstance()来判断是一个Iterable。

再加一个__iter__()方法,用yield返回一个生成器

class MyIterable:
    def __init__(self, n):
        self.n = n

    def __getitem__(self, item):
        return range(self.n)[item]

    def __iter__(self):
        a = 1
        b = 1
        for i in range(self.n):
            yield a
            a, b = b, a + b
            i += 1
            
myIterable = MyIterable(8)
myIterator = iter(myIterable)

print(isinstance(myIterator, Iterator))  # True
print(isinstance(myIterable, Iterable))  # True
print(myIterator)  # generator
for i in myIterator:
    print(i, end=' ')  # 1 1 2 3 5 8 13 21 

可见iter()方法会优先调用__iter__()方法返回迭代器,而且可以通过isinstance()来进行判断。

总结

总结一下知识点

  • 能够通过iter()方法返回迭代器的对象叫做可迭代对象,python中的大多数容器例如列表,字符串都可以

  • 可迭代对象是通过调用__iter__()或者__getitem__()来返回迭代器的

  • 迭代器是实现了__next__()__iter__()方法的对象,前者用来获取单个元素,后者用来实现迭代

  • 生成器是一种特殊的迭代器,通常使用生成器的场景比较多

  • 生成器有两种方式产生,循环中使用yield返回结果,或者用列表生成式来转换

我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值