为什么在这种特殊情况下要使用生成器对象?

4

我在查看从互联网上下载的一段代码,它是一个基本的网络爬虫。我遇到了下面这个 for 循环:

for link in (links.pop(0) for _ in xrange(len(links))):
    ...

现在,我认为下面的代码也可以工作:

for link in links:
    ....
links=[]

研究后发现,第一次实例会清除链接并生成一个生成器对象 (genexpr)。在 for 循环中从未使用”链接”,因此它的长度减少与代码无关。为什么要使用 xrange 并每次弹出元素?即使用生成器对象是否有优势而非调用标准列表的元素?此外,在哪些情况下将使用生成器对象是有用的?

1
也许 for 循环内部的代码(在你的问题中被省略了)依赖于 len(links) 每次迭代都会减少这一事实? - Frédéric Hamidi
@FrédéricHamidi len(links) 只会被调用一次。 - Rusty Rob
@robert,我指的是循环内部的代码,我们看不到,而不是在调用xrange()时对len(links)的初始调用。 - Frédéric Hamidi
@FrédéricHamidi,啊抱歉,你是正确的。 - Rusty Rob
1
不。就像我之前提到的那样,在循环内部从未使用过 links。甚至没有用到它的长度。 - shashwat
在其他线程中呢?如果还是没有,那么作者可能从其他需要它的地方复制了这段代码片段... - glglgl
2个回答

6

很难看到你所引用的代码有什么合理的解释。

我唯一能想到的是,links 中的对象可能很大,或者以某种方式与稀缺资源相关联,因此尽快释放它们可能非常重要(而不是等到循环结束时才释放它们)。但是,如果是这样的话,最好的做法是在创建每个链接时处理它(可能使用生成器来组织代码),而不是在开始处理之前建立整个链接列表;即使你别无选择,只能在处理之前建立整个列表,清除每个列表项的成本也比弹出列表低:

for i, link in enumerate(links):
    links[i] = None
    ...

从一个有 n 个元素的列表中弹出第一个元素需要 O(n) 的时间,但实际上它会非常快,因为它是使用 memmove 实现的。

即使你坚持要在迭代列表时反复弹出列表,也最好像这样写循环:

while links:
    link = links.pop(0)
    ...

3
使用生成器版本确实更短、更清晰,因为操作较少,并且避免了构造不必要的xrange和生成器对象。但是每个人都有自己的喜好,所以使用生成器版本也不是错误的。 - Gareth Rees
在类似的情况下,我甚至使用了一个ListPart案例,它支持迭代和索引,以便从列表中取出部分。如果注释允许换行,我会把它放在这里(所以可能最好不要这样做);它可以像这样使用:a = ListPart([1, 2, 3, 4]); b = a[2:4]; c=list(a); print a, b, c,它会打印出 myiter.ListPart([]) [3, 4] [1, 2] - glglgl

0
生成器的目的是避免构建大量中间对象,这些对象不会提供任何外部用途。
如果代码所做的只是构建页面上链接的集合,则第二个代码片段就可以了。但也许需要的是根网站名称的集合(例如google.com而不是google.com/q=some_search_term....)。如果是这种情况,那么您将获取链接列表,然后浏览整个列表,仅剥离第一部分。
正是在这个第二个剥离部分,使用生成器可以获得更多好处。您现在可以逐个通过每个链接,获取网站名称,而无需构建所有链接的大型中间列表,从而节省内存和时间。

但即使在第二种情况下,假设我们正在剥离链接,最终我们也是一次处理一个链接。for循环中的所有操作都涉及到link,而不是links。无论link被改变多少,Python都不会生成新的列表。 - shashwat
@shashwat 是的,但是你制作的中间对象越少,性能就会越好。通过形成生成器并在最后一刻调用list()来返回速度。向列表进行大量附加操作将会更慢。 - Noel Evans
@shashwat 我不明白为什么要这样使用xrange(len(...)) - Noel Evans

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接