拥有诸如yield这样的生成器语言设施是一个好主意吗?


9

PHP,C#,Python和可能的其他几种语言都有yield用于创建生成器函数的关键字。

在PHP中:http//php.net/manual/en/language.generators.syntax.php

在Python中:https//www.pythoncentral.io/python-generators-and-yield-keyword/

在C#中:https//docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield

我担心作为一种语言功能yield会破坏一些约定。其中之一就是我所说的“确定性”。该方法每次调用都会返回不同的结果。使用常规的非生成器函数,您可以调用它,并且如果给定相同的输入,它将返回相同的输出。使用yield时,它会根据其内部状态返回不同的输出。因此,如果您在不知道生成函数的先前状态的情况下随机调用生成函数,则不能指望它返回某个结果。

这样的功能如何适应语言范式?它实际上违反了任何约定吗?拥有并使用此功能是个好主意吗?(举例说明什么是好事,什么是坏事,goto这曾经是许多语言的功能,现在仍然是,但是它被认为是有害的,因此已经从某些语言(例如Java)中消除了)。编程语言的编译器/解释器是否必须突破任何约定才能实现此功能,例如,语言是否必须实现多线程才能使此功能正常工作,或者可以不使用线程技术来完成?


4
yield本质上是一个状态引擎。这并不意味着每次都返回相同的结果。它做绝对肯定是在枚举每次被调用时将返回的下一个项目。不需要线程;您需要关闭(或多或少)以保持当前状态。
罗伯特·哈维

1
关于“确定性”的质量,考虑到给定相同的输入序列,对迭代器的一系列调用将以完全相同的顺序产生完全相同的项。
罗伯特·哈维

4
我不确定您的大多数问题来自哪里,因为C ++没有像Python那样的yield 关键字。它有一个静态方法std::this_thread::yield(),但这不是关键字。因此,this_thread它将几乎对它进行任何调用,这很明显地表明这是仅用于产生线程的库功能,而不是通常用于产生控制流的语言功能。
Ixrec

链接已更新为C#,其中一个已删除C ++
Dennis

Answers:


16

首先要说明-C#是我最了解的语言,尽管它yield似乎与其他语言非常相似yield,但可能没有细微的差别。

我担心作为一种语言功能/特性,收益会破坏某些约定。其中之一就是我所说的“确定性”。该方法每次调用都会返回不同的结果。

胡说。您真的希望Random.NextConsole.ReadLine 每次调用它们都返回相同的结果吗?休息电话怎么样?验证?从收藏中拿取物品?有各种各样(不完善的,有用的)功能。

这样的功能如何适应语言范式?它实际上违反了任何约定吗?

是的,yield玩真的不好用try/catch/finally,而且是不允许的(https://blogs.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/为更多信息)。

拥有并使用此功能是个好主意吗?

拥有此功能当然是个好主意。像C#的LINQ这样的事情真的很好-懒惰地评估集合可以带来很大的性能优势,并且yield可以在代码的一小部分中完成这类事情,而手动迭代器则可以解决部分错误。

就是说,在yieldLINQ样式收集处理之外没有很多用途。我已经将它用于验证处理,计划生成,随机化以及其他一些事情,但是我希望大多数开发人员都从未使用过(或滥用它)。

编程语言的编译器/解释器是否必须打破任何约定才能实现此功能,例如,语言是否必须实现多线程才能使此功能正常工作,还是可以不使用线程技术来完成?

不完全是。编译器生成一个状态机迭代器,该迭代器跟踪停止的位置,以便下次调用时可以从那里重新开始。代码生成的过程类似于Continuation Passing Style,在Continuation Passing Style中,后面的代码yield被拉到其自己的块中(如果有,则将其放入yield另一个子块,依此类推)。这是一种在函数式编程中经常使用的众所周知的方法,并且也出现在C#的async / await编译中。

不需要线程,但是在大多数编译器中确实需要使用不同的方法来生成代码,并且确实与其他语言功能存在一些冲突。

总而言之,这yield是一个影响相对较小的功能,确实有助于解决特定的问题。


我从未认真使用过C#,但是此yield关键字类似于协程,是的,还是有所不同?如果是这样,我希望我在C语言中有一个!我可以想到至少有一些不错的代码段,使用这种语言功能可以使编写起来容易得多。

2
@DrunkCoder-类似,据我了解,但有一些限制。
Telastyn

1
您也不想看到收益率被滥用。一种语言的功能越多,您就越有可能发现用该语言编写的程序不好。我不确定编写一门平易近人的语言的正确方法是将所有内容扔给您,看看有什么用。
尼尔,

1
@DrunkCoder:它是半协程的有限版本。实际上,编译器将其视为一种语法模式,并将其扩展为一系列方法调用,类和对象。(基本上,编译器会生成一个连续对象,以捕获字段中的当前上下文。)集合的默认实现是半协程,但是通过重载编译器使用的“魔术”方法,您实际上可以自定义行为。例如,在将async/ await添加到语言之前,有人使用来实现了该语言yield
约尔格W¯¯米塔格

1
@Neil通常可能实际上滥用任何编程语言功能。如果您说的是正确的,那么使用C进行不良编程要比Python或C#困难得多,但是事实并非如此,因为这些语言有很多工具可以保护程序员免受许多容易犯的错误的侵害。实际上,糟糕的程序的原因是糟糕的程序员-这是一个与语言无关的问题。
Ben Cottrell

12

是否拥有诸如生成器语言之类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

如您所见,不仅语法清晰易读,而且内置函数(如sumaccept生成器)也是如此。

查看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中所有生成器用法的完整回顾。值得注意的是,我跳过了无限生成器,状态机,将值传回以及它们与协程的关系。

我相信足以证明您可以将生成器作为一种完全集成且有用的语言功能。


6

如果您习惯使用经典的OOP语言,则生成器yield可能会感到震撼,因为可变状态是在函数级别而非对象级别捕获的。

但是,“确定性”问题是一个红色的鲱鱼。通常称为参照透明性,基本上意味着函数对于相同的参数总是返回相同的结果。一旦具有可变状态,就会失去参照透明性。在OOP中,对象通常具有可变状态,这意味着方法调用的结果不仅取决于参数,还取决于对象的内部状态。

问题是在哪里捕获可变状态。在经典的OOP中,可变状态存在于对象级别。但是,如果语言支持闭包,则您可能在功能级别上具有可变状态。例如在JavaScript中:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

简而言之,yield在支持闭包的语言中是很自然的,但是在像Java的较旧版本那样的语言中,可变状态仅存在于对象级别是不合适的。


我想,如果语言特征具有一定的范围,那么产出将与功能性相去甚远。那不一定是坏事。OOP曾经非常时尚,后来又进行了功能编程。我认为,这样做的危险实际上就是将诸如yield的功能与功能设计进行混合和匹配,使您的程序以意外的方式运行。
尼尔,

0

我认为这不是一个好功能。这是一个不好的功能,主要是因为需要非常仔细地教它,并且每个人都教错了。人们使用“生成器”一词,在生成器功能和生成器对象之间进行歧义。问题是:到底是谁在做什么?

这不仅仅是我的观点。即使是Guido,在他对此有所规定的PEP公告中,也承认生成器功能不是生成器而是“生成器工厂”。

这很重要,您不觉得吗?但是,阅读那里99%的文档,您会感觉到generator函数是实际的生成器,他们倾向于忽略您还需要一个generator对象的事实。

Guido考虑过将这些功能的“ gen”替换为“ def”,并表示“否”。但是我认为这还是不够的。确实应该是:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.