skip to Main Content
你的Python高效学习之道 025-5987-6503 contact@lessoncode.com

Python生成器简介

生成器功能

要创建一个生成器,你可以按照通常的方式定义一个函数,但是使用yield语句而不是返回,指示解释器该函数应该被当作一个迭代器:

def countdown(num):
print(‘Starting’)
while num > 0:
yield num
num -= 1

yield语句暂停该函数并保存本地状态,以便可以在停止的位置恢复该状态。

当您调用此功能时会发生什么?

>>> def countdown(num):
… print(‘Starting’)
… while num > 0:
… yield num
… num -= 1

>>> val = countdown(5)
>>> val

调用该函数不执行。 我们知道这是因为字符串Starting没有打印。 相反,该函数返回一个用于控制执行的生成器对象。

当调用next()时,生成器对象执行:

>>> next(val)
Starting
5

当第一次调用next()时,执行从函数体的开始开始,并持续到下一个yield语句,其中返回语句右侧的值,后续的对next()的调用从yield语句继续到 结束函数,并循环并继续从函数体的开始直到另一个yield被调用。 如果yield不被调用(在我们的例子中,我们不会进入if函数,因为num <= 0)会引发一个StopIteration异常:

>>> next(val)
4
>>> next(val)
3
>>> next(val)
2
>>> next(val)
1
>>> next(val)
Traceback (most recent call last):
File “”, line 1, in
StopIteration

生成器表达式

就像列表推导一样,生成器也可以以相同的方式编写,除了它们返回一个生成器对象而不是一个列表:

>>> my_list = [‘a’, ‘b’, ‘c’, ‘d’] >>> gen_obj = (x for x in my_list)
>>> for val in gen_obj:
… print(val)

a
b
c
d

记下第二行的任何一方表示一个生成器表达式,这在大多数情况下,与列表理解相同,但是它是懒惰的:

>>> import sys
>>> g = (i * 2 for i in range(10000) if i % 3 == 0 or i % 5 == 0)
>>> print(sys.getsizeof(g))
72
>>> l = [i * 2 for i in range(10000) if i % 3 == 0 or i % 5 == 0] >>> print(sys.getsizeof(l))
38216
>>>

由于生成器表达式的运行速度比列表推导速度慢(除非当然没有内存使用),请注意不要将列表推导的语法与生成器表达式[]

>>> import cProfile
>>> cProfile.run(‘sum((i * 2 for i in range(10000000) if i % 3 == 0 or i % 5 == 0))’)
4666672 function calls in 3.531 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
4666668 2.936 0.000 2.936 0.000 :1()
1 0.001 0.001 3.529 3.529 :1()
1 0.002 0.002 3.531 3.531 {built-in method exec}
1 0.592 0.592 3.528 3.528 {built-in method sum}
1 0.000 0.000 0.000 0.000 {method ‘disable’ of ‘_lsprof.Profiler’ objects}

>>> cProfile.run(‘sum([i * 2 for i in range(10000000) if i % 3 == 0 or i % 5 == 0])’)
5 function calls in 3.054 seconds

Ordered by: standard name

ncalls tottime percall cumtime percall filename:lineno(function)
1 2.725 2.725 2.725 2.725 :1()
1 0.078 0.078 3.054 3.054 :1()
1 0.000 0.000 3.054 3.054 {built-in method exec}
1 0.251 0.251 0.251 0.251 {built-in method sum}
1 0.000 0.000 0.000 0.000 {method ‘disable’ of ‘_lsprof.Profiler’ objects}

这在上面的例子中是特别容易的(即使是高级开发人员),因为最终输出完全相同的东西。

用例

发生器是读取大量大文件的理想选择,因为无论输入流的大小如何,它们都能同时产生单个数据块的数据。 它们还可以通过将迭代过程分解为更小的组件来获得更清晰的代码。
实施例1

def emit_lines(pattern=None):
lines = [] for dir_path, dir_names, file_names in os.walk(‘test/’):
for file_name in file_names:
if file_name.endswith(‘.py’):
for line in open(os.path.join(dir_path, file_name)):
if pattern in line:
lines.append(line)
return lines

此函数循环指定目录中的一组文件。它打开每个文件,然后循环遍历每一行以测试模式匹配。

这可以很好的与少量的小文件。但是,如果我们处理非常大的文件怎么办?如果他们有很多呢?幸运的是,Python的open()函数是高效的,不会将整个文件加载到内存中。但是如果我们的比赛列表远远超出了我们机器上的可用内存呢?

因此,在处理大量数据时,代替空间不足(大列表)和时间(几乎无限量的数据流),生成器是理想的使用方式,因为它们一次输出数据(而不是创建中间列表)。

我们来看看上面问题的发生器版本,并尝试了解为什么生成器适用于使用处理流水线的这种用例。

我们将整个过程分为三个不同的组成部分:

  • 生成一组文件名
  • 从所有文件生成所有行
  • 在模式匹配的基础上过滤线

def generate_filenames():
“””
generates a sequence of opened files
matching a specific extension
“””
for dir_path, dir_names, file_names in os.walk(‘test/’):
for file_name in file_names:
if file_name.endswith(‘.py’):
yield open(os.path.join(dir_path, file_name))

def cat_files(files):
“””
takes in an iterable of filenames
“””
for fname in files:
for line in fname:
yield line

def grep_files(lines, pattern=None):
“””
takes in an iterable of lines
“””
for line in lines:
if pattern in line:
yield line

py_files = generate_filenames()
py_file = cat_files(py_files)
lines = grep_files(py_file, ‘python’)
for line in lines:
print (line)

在上面的代码片段中,我们不使用任何额外的变量来形成行列表,而是创建一个管道,一次通过迭代过程为一个项目提供它的组件。 grep_files接收* .py文件的所有行的生成器对象。 类似地,cat_files会接收目录中所有文件名的生成器对象。 所以这是整个管道如何通过迭代胶合。
示例2

生成器可以递归地用于网络抓取和爬网:

import requests
import re

def get_pages(link):
links_to_visit = [] links_to_visit.append(link)
while links_to_visit:
current_link = links_to_visit.pop(0)
page = requests.get(current_link)
for url in re.findall(”, str(page.content)):
if url[0] == ‘/’:
url = current_link + url[1:] pattern = re.compile(‘https?’)
if pattern.match(url):
links_to_visit.append(url)
yield current_link

webpage = get_pages(‘http://sample.com’)
for result in webpage:
print(result)

这里,我们一次只能抓取一个页面,然后在执行时在页面上执行某种操作。 如果没有发电机,这将是什么样子? 获取和处理必须在相同的函数内发生(导致难以测试的高度耦合的代码),或者在处理单个页面之前必须获取所有链接。

结论

生成器允许我们在需要时要求价值,使我们的应用程序更有效地提高内存的效率,并为无限数据流提供完美的解决方案。 它们也可用于从循环中重构加工,从而产生更干净,分离的代码。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Back To Top