0
点赞
收藏
分享

微信扫一扫

Python进阶 - 高性能计算之协程

迭代器

可迭代对象

什么是可迭代对象

可迭代对象就是对象的类中实现了__iter__方法的对象。对于可迭代对象,可以使用for循环直接从中取得元素。原生数据中的list, set, tuple, str都是可迭代对象。

如何判断对象是否可以迭代

判断对象是否可以迭代,可以用from collections import Iterable,然后用isinstance标识符将对象与之对比,如果可以迭代,就会返回True。

如何让自定义的类成为可迭代对象

通过定义类的__iter__方法,可以让自定义的类创建的对象可迭代,示例如下:

from collections import Iterable


class myInt(int):
    """"""

    def __init__(self, num):
        self._num = num

    def __iter__(self):
        for i in range(self._num):
            yield i


def main():
    some_int = myInt(10)
    print("myInt is iterable: ", isinstance(some_int, Iterable))
    # 输出myInt is iterable:  True
    for i in some_int:
        print(i, end=' ')
    print()


if __name__ == '__main__':
    main()

迭代器的创建和调用

迭代器是访问元素集合的一种方式,迭代器是一个可以记住遍历位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有元素访问结束。迭代器只能前进,不能后退。类似于C++中的forward_iterator。

一个迭代器有两个基本要素:__iter__方法,__next__方法。__iter__方法返回迭代器自身,__next__方法返回集合中的下一个值,如果集合中没有更多元素,则抛出StopIteration异常。

在实际使用迭代器时,一般让可迭代对象类的__iter__方法返回其迭代器,而迭代器的__next__方法设定访问元素的顺序,一个例子如下:

from collections import Iterable
from collections import Iterator


class MyList(object):  # 定义可迭代对象类
    def __init__(self, num):
        self.data = num  # 迭代上边界

    def __iter__(self):
        return MyListIterator(self.data)


class MyListIterator(object):  # 定义迭代器类
    def __init__(self, data):
        self.data = data  # 迭代上边界
        self.current = 0  # 当前迭代值
        self.step = 2  # 迭代步长

        
    def __iter__(self):
        return self

      
    def __next__(self):
        while self.current <= self.data - self.step:
            self.current += self.step
            return self.current
        raise StopIteration  # 迭代超出上边界时抛出异常


def main():
    smObj = MyList(8)
    print("MyList() is iterable: ", isinstance(smObj, Iterable))
    smObj_iterator = iter(smObj)
    print("iter(Mylist()) is iterator: ", isinstance(smObj_iterator, Iterator))
    for i in smObj:
        print(i, end=' ')
    print()


if __name__ == '__main__':
    main()

for循环进行调用时,会调用smObj中的 __iter__方法,返回一个迭代器后,调用迭代器中的__next__方法获取返回值。

这里要注意,如果不手动设定越界后抛出StepIterationfor循环是不会终止的,而是不断会取到None

迭代器的作用

使用迭代器,可以只记录在集合中生成下一个元素的方式,而不去记录生成的具体结果。这样在生成一个大集合时,可以只占用很小的内存空间,更加memory-efficient。

例如用迭代器生成斐波那契数列:

class Fibonacci(object):
    def __init__(self, n):
        self.prev_last_item = 0
        self.last_item = 1
        self.current = 0
        self.upper_bound = n

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < 2:
            tmp = self.current
            self.current += 1
            return tmp
        elif self.current < self.upper_bound:
            self.current += 1
            tmp = self.prev_last_item + self.last_item
            self.prev_last_item, self.last_item = self.last_item, tmp
            return tmp
        else:
            raise StopIteration


def main():
    # 用生成器打印斐波那契数列的前n项
    n = 20
    fib = Fibonacci(n)
    for i in fib:
        print(i)


if __name__ == '__main__':
    main()

除了for以外的迭代器使用

除了使用for循环以外,其实在进行类型转换时,也会使用迭代器。如:

fib = Fibonacci(15)
print(list(fib)) # 转换为列表,实际上会调用迭代器
print(tuple(fib)) # 转换为元组,实际上会调用迭代器

所以实际上在这个过程中,是先生成一个空列表或者空元组,再调用迭代器的__next__方法,依次向内填充数据。

生成器

什么是生成器

生成器就是一种特殊的迭代器。它是一个返回迭代器的函数,只能用于迭代操作。

创建生成器的方式

  • 第一种方式:将列表推导式的[] 换成()
def main():
    G = (i * i for i in range(10))
    print(G) # 输出<generator object main.<locals>.<genexpr> at 0x10ddb7228>


if __name__ == '__main__':
    main()

要从生成器中取值,可以使用next()函数。

  • 第二种方式:在函数中使用yield关键字

当函数中包含yield关键字时,那么创建的就是一个生成器,而非普通函数。函数会顺序执行,遇到return或者最后一行函数语句就返回。但是generator会在每次调用next()时执行,遇到yield语句是暂停并返回一个值,再次执行时,<u>从上次返回的yield语句处执行。</u>

例如:

from time import sleep


def Fibonacci(num):
    a, b = 0, 1
    cnt = 0
    while cnt < num:
        print("Before yield, current cnt: {}".format(cnt))
        yield a
        print("After yield")
        cnt += 1
        a, b = b, a + b


def main():
    G = Fibonacci(10)
    print(G)  # <generator object Fibonacci at 0x104079228>
    next(G)  # Before yield, current cnt: 0
    sleep(1)
    next(G)  # After yield
             # Before yield, current cnt: 1


if __name__ == '__main__':
    main()

可以看到在执行next时,是不会执行到函数的最后一句的,而是到yield就暂停并返回了。

获得生成器函数的返回值

在生成器函数中,如果要获得其返回值,需要用异常捕获到方式,如下所示:

from time import sleep


def Fibonacci(num):
    a, b = 0, 1
    cnt = 0
    while cnt < num:
        yield a
        cnt += 1
        a, b = b, a + b
    return "Done!"


def main():
    G = Fibonacci(10)
    try:
        while True:
            ret = next(G)
            print(ret)
    except Exception as ret:
        print(ret.value)


if __name__ == '__main__':
    main()

这样就会在最后打印出函数的返回值Done

使用send操作生成器

除了使用next()函数启动生成器以外,还可以用send方法来使生成器运行。它与next()的不同在于可以向生成器内传入一些值,形如generator().send(someValue)。这样就使得我们可以在生成器运行的过程中操作它,极大增加了生成器的灵活性。

例如:

def generator(num):
    """一个生成器,生成斐波那契数列"""
    a, b = 0, 1
    cnt = 0
    while cnt <= num:
        yield a
        a, b = b, a + b
        cnt += 1


def main():
    g = generator(20)
    try:
        while True:
            print(g.send(None)) # 此时并未传值,效果等同于next(g)
    except StopIteration:
        pass


if __name__ == '__main__':
    main()

如果说希望在打印的过程中,让生成器重启,可以修改上面的代码:

def generator(num):
    """一个生成器,生成斐波那契数列"""
    a, b = 0, 1
    cnt = 0
    while cnt <= num:
        val = yield a # val用来接收send传入的值
        # 根据val是否有传入值,来决定是否重置
        if val is None:
            a, b = b, a + b
        else:
            a, b = 0, 1
            cnt = val
        cnt += 1


def main():
    g = generator(5)
    reset = 0
    try:
        while True:
            print(g.send(None)) # 此时并未传值,效果等同于next(g)
            reset += 1
            # 在打印两项之后,重新启动生成器
            if reset == 2:
                print(g.send(0)) # 让函数内的cnt重置为0
    except StopIteration:
        pass


if __name__ == '__main__':
    main()

此时的结果为:

0
1
0
1
1
2
3

可以看到按照我们的需要,重启了生成器。

这里注意,yield返回的值对应next函数的返回值,而send方法传入的值对应的是yield语句执行完成,下次重新从这里开始运行后,yield表达式的值。也就是说,在运行时用send传入值时,实际上第一次暂停,是在someVal = yield a的等式右边,这里a被返回给了next函数作为返回值,而yield a表达式的值会等于send(someVal)中传入的值。

另外需要注意的是,如果需要用到send方法传入值,一定不能在生成器第一次运行时(没有调用过next或者是send(None))进行传值,因为函数还没有运行到yield语句,没有东西可以接收传入值。

协程 - yield实现多任务

我们需要注意到yield关键字的重要特点之一是可以暂停函数,利用这个特点,可以用yield实现多任务并发。这里的原理非常类似操作系统的时间片轮转。

如以下例子:

def task1(num):
    while True:
        print("Now in task1 - %d" % num)
        num = yield


def task2(num):
    while True:
        print("Now in test2 - %d" % num)
        num = yield


def main():
    current_num = 0
    num = 10
    g1 = task1(current_num)
    g2 = task2(current_num)
    next(g1)
    next(g2)
    while current_num < num:
        current_num += 1
        g1.send(current_num)
        g2.send(current_num)


if __name__ == '__main__':
    main()

输出结果如下:

Now in task1 - 0
Now in test2 - 0
Now in task1 - 1
Now in test2 - 1
Now in task1 - 2
Now in test2 - 2
Now in task1 - 3
Now in test2 - 3
Now in task1 - 4
Now in test2 - 4
Now in task1 - 5
Now in test2 - 5
Now in task1 - 6
Now in test2 - 6
Now in task1 - 7
Now in test2 - 7
Now in task1 - 8
Now in test2 - 8
Now in task1 - 9
Now in test2 - 9
Now in task1 - 10
Now in test2 - 10

协程的特点在于调用的资源非常少,仅相当于调用了一个函数。但是弱点在于无法合理利用计算机的多CPU,实际上就是用一个线程模拟了多任务的同时进行。

举报

相关推荐

0 条评论