深入浅出 Python Generators

原文


在上篇文章中介绍了如何用class构造iterator:必须实现__iter__()`__next__(),必须keep internal states,在结束迭代时必须明确raise StopIteration。这一系列操作比较复杂,本篇介绍更简单的方法。

Python generators are a simple way of creating iterators.

Python generator 就是一个可以返回 iterator 的函数

如何构造generator?

在一个函数定义中,至少一处使用了yield,则该函数就成了一个generator。
generator可以包含任意个yield和return语句,区别在于:

  • yield语句暂时退出该函数,保存函数的各种状态,下次继续从这里执行。
  • 而return语句相当于一个raise StopIteration。

Generator函数和Normal函数的差别

Generator和Normal函数的差别:

  • 至少包含一个yield
  • 被调用时,它不是立即执行,而是返回一个iterator;一个next()会让它执行至第一个yield
  • __iter__()__next__()方法被自动创建
  • generator function 发生yield时,该function相当于被“暂停”了,然后控制转交给caller。
  • Local variables 和其状态被保存
  • 最后,当该函数结束时,StopIteration被自动raised。
    下面是一个例子:
# A simple generator function
def my_gen():
    n = 1
    print('This is printed first')
    # Generator function contains yield statements
    yield n

    n += 1
    print('This is printed second')
    yield n

    n += 1
    print('This is printed at last')
    yield n

然后进行一些测试:

>>> # It returns an object but does not start execution immediately.
>>> a = my_gen()

>>> # We can iterate through the items using next().
>>> next(a)
This is printed first
1
>>> # Once the function yields, the function is paused and the control is transferred to the caller.

>>> # Local variables and theirs states are remembered between successive calls.
>>> next(a)
This is printed second
2

>>> next(a)
This is printed at last
3

>>> # Finally, when the function terminates, StopIteration is raised automatically on further calls.
>>> next(a)
Traceback (most recent call last):
...
StopIteration
>>> next(a)
Traceback (most recent call last):
...
StopIteration

需要注意的是,generator 对象只能被 iterate 一次,如果想再次 iterate,需要重新定义一个该对象a = my_gen()

一般generators用在for loop中:

# A simple generator function
def my_gen():
    n = 1
    print('This is printed first')
    # Generator function contains yield statements
    yield n

    n += 1
    print('This is printed second')
    yield n

    n += 1
    print('This is printed at last')
    yield n

# Using for loop
for item in my_gen():
    print(item) 

运行上面的代码,可以看到的结果是:

This is printed first
1
This is printed second
2
This is printed at last
3

这种写法也称为 Co-routine,利用 Generator 可以把一些异步的操作写成看似同步一样。具体的例子参见

在Loop中yield

一般会在一个loop中进行yield来构造一个generator,下面是一个例子:

def rev_str(my_str):
    length = len(my_str)
    for i in range(length - 1,-1,-1):
        yield my_str[i]

# For loop to reverse the string
# Output:
# o
# l
# l
# e
# h
for char in rev_str("hello"):
     print(char)

Generator Expression, Generator表达式

类似lambda function创建了一个anonymous function,generator expression用来创建一个anonymous generator function。
generator expression的语法类似于 list comprehension, 但是要把 [ ] 方括号改成 ( ) 圆括号。
区别在于,list comprehension 创造了整个list,而 generator expression 一次创建一个item。
因此,generator expression 更节省内存。

# Initialize the list
my_list = [1, 3, 6, 10]

# square each term using list comprehension
# Output: [1, 9, 36, 100]
[x**2 for x in my_list]

# same thing can be done using generator expression
# Output: <generator object <genexpr> at 0x0000000002EBDAF8>
(x**2 for x in my_list)

可以看到,上面的 generator expression 并不是立马返回一个列表,而是返回了一个 generator 对象,需要使用 next 来产生一个值。

# Intialize the list
my_list = [1, 3, 6, 10]

a = (x**2 for x in my_list)
# Output: 1
print(next(a))

# Output: 9
print(next(a))

# Output: 36
print(next(a))

# Output: 100
print(next(a))

# Output: StopIteration
next(a)

注意: Generator expression 可以用在函数括号中,此时其圆括号可以省略掉:

>> sum(x**2 for x in my_list)
146

>>> max(x**2 for x in my_list)
100

Generator 的优点

1. 简洁

对比一下 PowTwo 用 iterator 类的实现和 Generator 的实现:

lass PowTwo:
    def __init__(self, max = 0):
        self.max = max

    def __iter__(self):
        self.n = 0
        return self

    def __next__(self):
        if self.n > self.max:
            raise StopIteration

        result = 2 ** self.n
        self.n += 1
        return result

同样的功能,Generator 会更加简洁:

def PowTwo(max = 0):
    n = 0
    while n < max:
        yield 2 ** n
        n += 1

因为 generator 默认实现了很多细节

2. 节省内存

这一点在比较 list comprehension 和 generator 时已经解释过了。

3. 可以利用 Infinite Generator 产生 Infinite Stream

这一点在上一篇中也解释过了。

4. Pipelining Generators

利用 Generator 可以把一系列操作放入管道中,效率更高。
在下面的例子中,假设一个文件每一行都是一个数字,或者是一个 ‘N/A’ ,我们想把所有的数字加起来:

with open('afile.log') as file:
    col = (line for line in file)
    num = (float(x) for x in col if x != 'N/A')
    print("The sum is: ", sum(num))

问题:是否效率更高?还没验证过。如何验证?

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值