是否拥有诸如生成器语言之类yield
的好主意?
我想从Python的角度回答这个问题,这是个好主意。
我将首先解决您的问题中的一些问题和假设,然后证明生成器的普遍性及其后在Python中的不合理使用。
使用常规的非生成器函数,您可以调用它,如果给定相同的输入,它将返回相同的输出。使用yield时,它会根据其内部状态返回不同的输出。
这是错误的。可以将对象上的方法视为具有自身内部状态的函数本身。在Python中,由于一切都是对象,因此实际上您可以从对象中获取方法,然后传递该方法(该方法绑定到它来自的对象,因此它会记住其状态)。
其他示例包括故意随机的函数以及诸如网络,文件系统和终端之类的输入方法。
这样的功能如何适应语言范式?
如果语言范例支持一流的功能,而生成器支持其他语言功能(如Iterable协议),那么它们将无缝地融合在一起。
它实际上违反了任何约定吗?
不会。由于语言中已经融入了约定,所以约定是围绕约定进行的,并且包括(或要求!)使用生成器。
编程语言编译器/解释器是否必须突破任何约定才能实现此功能
与任何其他功能一样,只需将编译器设计为支持该功能。对于Python,函数已经是带有状态的对象(例如默认参数和函数注释)。
语言是否必须实现多线程才能使此功能正常工作,还是可以不用线程技术来完成?
有趣的事实:默认的Python实现根本不支持线程。它具有全局解释器锁(GIL),因此除非您启动了另一个进程来运行其他Python实例,否则实际上并不会并行运行。
注意:示例在Python 3中
超越产量
尽管yield
可以在任何函数中使用该关键字将其转换为生成器,但这并不是制作关键字的唯一方法。Python具有Generator Expressions(生成器表达式)功能,这是一种用另一种可迭代的方式(包括其他生成器)清楚地表达生成器的强大方法
>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155
如您所见,不仅语法清晰易读,而且内置函数(如sum
accept生成器)也是如此。
用
查看With语句的Python增强建议。这与其他语言的With语句所带来的期望完全不同。在标准库的一点帮助下,Python的生成器可以很好地用作它们的上下文管理器。
>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
print("preprocessing", arg)
yield arg
print("postprocessing", arg)
>>> with debugWith("foobar") as s:
print(s[::-1])
preprocessing foobar
raboof
postprocessing foobar
当然,打印内容是您在这里可以做的最无聊的事情,但是它确实显示出可见的结果。更有趣的选项包括资源自动管理(打开和关闭文件/流/网络连接),并发锁定,临时包装或替换功能以及解压缩然后重新压缩数据。如果调用函数就像将代码注入到代码中,那么with语句就像将代码的一部分包装在其他代码中。不管您使用它是什么,它都是轻松挂钩到语言结构的可靠示例。基于收益的生成器不是创建上下文管理器的唯一方法,但是它们无疑是一种方便的方法。
局部疲劳
Python中的For循环以一种有趣的方式工作。它们具有以下格式:
for <name> in <iterable>:
...
首先,对我调用的表达式<iterable>
求值以获得一个可迭代的对象。其次,迭代器已对其进行__iter__
调用,并且将生成的迭代器存储在幕后。随后,__next__
在迭代器上调用,以获取一个值以绑定到您输入的名称<name>
。重复此步骤,直到对的调用__next__
引发StopIteration
。for循环将异常吞没,然后从那里继续执行。
回到生成器:调用__iter__
生成器时,它只会返回自身。
>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272
这意味着您可以将迭代与想要进行的操作分开,然后在整个过程中更改行为。在下面,请注意在两个循环中如何使用同一个生成器,在第二个循环中,它从与第一个循环不同的地方开始执行。
>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
print(ord(letter))
if letter > 'p':
break
109
111
114
>>> for letter in generator:
print(letter)
e
b
o
r
i
n
g
s
t
u
f
f
懒惰评估
与列表相比,生成器的缺点之一是生成器中唯一可以访问的内容是生成的下一件东西。您不能返回上一个结果,也不能不经历中间结果就跳到下一个结果。好处是,与其等效的列表相比,生成器几乎不占用任何内存。
>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
sys.getsizeof([x for x in range(10000000000)])
File "<pyshell#10>", line 1, in <listcomp>
sys.getsizeof([x for x in range(10000000000)])
MemoryError
生成器也可以延迟链接。
logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))
第一,第二和第三行分别定义了一个生成器,但没有做任何实际的工作。当最后一行被调用时,sum要求numericcolumn提供一个值,numericcolumn需要lastcolumn中的一个值,lastcolumn从logfile中要求一个值,然后日志文件实际上从文件中读取一行。该堆栈展开,直到sum获得其第一个整数。然后,第二行再次发生该过程。此时,sum有两个整数,并将它们加在一起。请注意,尚未从文件中读取第三行。然后,Sum继续从numericalcolumn(完全忽略链的其余部分)中请求值,并将它们相加,直到numericalcolumn用尽为止。
这里真正有趣的部分是单独读取,使用和丢弃这些行。整个文件都不会一次全部存储在内存中。如果此日志文件为TB级,会发生什么情况?它之所以有效,是因为它一次只能读取一行。
结论
这不是对Python中所有生成器用法的完整回顾。值得注意的是,我跳过了无限生成器,状态机,将值传回以及它们与协程的关系。
我相信足以证明您可以将生成器作为一种完全集成且有用的语言功能。
yield
本质上是一个状态引擎。这并不意味着每次都返回相同的结果。它会做绝对肯定是在枚举每次被调用时将返回的下一个项目。不需要线程;您需要关闭(或多或少)以保持当前状态。