PEP 323 – 可复制的迭代器
- 作者:
- Alex Martelli <aleaxit at gmail.com>
- 状态:
- 延期
- 类型:
- 标准跟踪
- 创建:
- 2003-10-25
- Python 版本:
- 2.5
- 历史记录:
- 2003-10-29
延期
这个 PEP 已经延期。可复制的迭代器是一个好主意,但在四年后,还没有出现任何实现或广泛的兴趣。
摘要
这个 PEP 建议某些迭代器类型应该通过公开一个满足特定要求的 __copy__
方法来支持其实例的浅拷贝,并指示使用迭代器的代码如何在存在 __copy__
方法时利用它。
更新和评论
对 __copy__
的支持已包含在 Py2.4 的 itertools.tee()
中。
向现有迭代器添加 __copy__
方法将改变 tee()
下的行为。目前,复制的迭代器仍然与原始迭代器绑定。如果原始迭代器前进,则所有副本也前进。良好的做法是覆盖原始迭代器,以避免出现异常:a,b=tee(a)
。如果向迭代器添加了 __copy__
方法,则不遵循该做法的代码可能会观察到语义变化。
动机
在 Python 2.3 及更早版本中,大多数内置迭代器类型不允许用户复制其实例。用户编写的允许其客户端在其实例上调用 copy.copy 的迭代器可能,也可能不会,作为复制的结果返回一个单独的迭代器对象,该对象可以独立于原始迭代器进行迭代。
目前,在用户编写的迭代器类型中对 copy.copy 的“支持”几乎总是“意外的”——也就是说,Python 标准库的 copy 模块中 copy 方法的标准机制确实会构建并返回一个副本。但是,只有当在该类的实例上调用 .next()
恰好仅通过将某些属性重新绑定到新值来改变实例状态,而不是通过改变某些属性的现有值来改变实例状态时,副本才能独立于原始迭代器进行迭代。
例如,一个其“索引”状态以整数属性保存的迭代器很可能会提供可用的副本,因为(整数是不可变的).next()
可能只是重新绑定了该属性。另一方面,另一个其“索引”状态以列表属性保存的迭代器很可能会在执行 .next()
时改变相同的列表对象,因此该迭代器的副本将无法独立于原始迭代器进行迭代。
鉴于这种情况,对某些迭代器对象执行 copy.copy(it)
并不是非常有用,因此,它也并不被广泛使用。但是,在许多情况下,能够获得迭代器的“快照”,作为“书签”,以便能够继续沿着序列迭代,但稍后从书签处开始再次迭代同一个序列,是很有用的。为了支持这种“书签”,模块 itertools 在 2.4 中添加了一个 'tee' 函数,可以像这样使用
it, bookmark = itertools.tee(it)
‘it’ 的先前值不能再使用,这就是为什么这种典型用法习惯用法会重新绑定名称。在这次调用之后,‘it’ 和 ‘bookmark’ 是同一个底层序列上独立可迭代的迭代器:这满足了“迭代器复制”的应用需求。
但是,当 itertools.tee 对传递给它的迭代器的性质没有任何假设时,它必须在内存中保存所有两个“tee”迭代器中的一个已经遍历过的项目,但还没有全部遍历过的项目。如果两个迭代器在遍历过程中相距很远,这在内存方面可能相当昂贵;实际上,在某些情况下,最好从迭代器中创建一个列表,以便能够重复遍历子序列,或者,如果这样做在内存方面太昂贵,则将项目保存到磁盘,同样是为了能够重复遍历它们。
这个 PEP 提出了另一个想法,它将在某些重要情况下允许 itertools.tee
以最小的内存成本完成它的工作;用户代码偶尔也可以利用这个想法来决定是复制一个迭代器,从它创建一个列表,还是使用一个辅助磁盘文件。
关键考虑因素是,某些重要的迭代器,例如那些由内置函数 iter 在序列上构建的迭代器,本质上很容易复制:只需获得对同一个序列的另一个引用,以及整数索引的副本。但是,在 Python 2.3 中,这些迭代器不会公开状态,也不支持 copy.copy
。
因此,这个 PEP 的目的是让这些迭代器类型公开一个合适的 __copy__
方法。类似地,用户编写的迭代器类型,如果能够以有限的时间和空间成本提供其实例的副本,适合独立迭代,也应该公开一个合适的 __copy__
方法。虽然 copy.copy 还支持其他方法让类型控制其实例的复制方式,但为了简单起见,建议支持复制的迭代器类型总是通过公开 __copy__
方法来实现,而不是通过 copy.copy
支持的其他方法。
让迭代器在可行的情况下公开一个合适的 __copy__
将便于对 itertools.tee 和类似用户代码的轻松优化,例如
def tee(it):
it = iter(it)
try: copier = it.__copy__
except AttributeError:
# non-copyable iterator, do all the needed hard work
# [snipped!]
else:
return it, copier()
注意,此函数不会调用“copy.copy(it)” ,即使在实现此 PEP 之后,它也可能仍然“恰好成功”。对于一些作为用户编写的类实现的迭代器类型,它并没有真正提供一个足够的“独立可迭代”的副本对象作为其结果。
规范
任何迭代器类型 X 可能会公开一个方法 __copy__
,该方法可以在 X 的任何实例 x 上被调用,无需参数。只有当迭代器类型能够以合理的计算和内存开销提供可复制性时,才应该公开该方法。此外,方法 __copy__
返回的新对象 y 应该是 X 的一个新实例,它可以独立于 x 进行迭代,沿着相同的“底层序列”项目进行遍历。
例如,假设一个类 Iter 本质上复制了 iter 内置函数的功能,用于迭代序列
class Iter(object):
def __init__(self, sequence):
self.sequence = sequence
self.index = 0
def __iter__(self):
return self
def next(self):
try: result = self.sequence[self.index]
except IndexError: raise StopIteration
self.index += 1
return result
为了使这个 Iter 类符合这个 PEP,只需要在 Iter 类主体中添加以下内容
def __copy__(self):
result = self.__class__(self.sequence)
result.index = self.index
return result
注意,在这种情况下,__copy__
甚至没有尝试复制序列;如果在原始迭代器和复制迭代器中的一个或两个仍在遍历序列时改变了序列,那么迭代行为很可能会出错——这并不是 __copy__
的责任来改变对遍历可变序列的迭代器的这种正常的 Python 行为(这可能是迭代器的 __deepcopy__
方法的规范,但是,这个 PEP 并没有涉及)。
再考虑一个“随机迭代器”,它提供来自随机实例的某个方法(使用给定参数调用)的非终止序列结果
class RandomIterator(object):
def __init__(self, bound_method, *args):
self.call = bound_method
self.args = args
def __iter__(self):
return self
def next(self):
return self.call(*self.args)
def __copy__(self):
import copy, new
im_self = copy.copy(self.call.im_self)
method = new.instancemethod(self.call.im_func, im_self)
return self.__class__(method, *self.args)
这个迭代器类型比其名称略微更通用,因为它支持对任何绑定方法(或其他可调用对象)的调用,但如果可调用对象不是绑定方法,那么方法 __copy__
将会失败。但用例是用于生成随机流,例如
import random
def show5(it):
for i, result in enumerate(it):
print '%6.3f'%result,
if i==4: break
print
normit = RandomIterator(random.Random().gauss, 0, 1)
show5(normit)
copit = normit.__copy__()
show5(normit)
show5(copit)
这将显示一些输出,例如
-0.536 1.936 -1.182 -1.690 -1.184
0.666 -0.701 1.214 0.348 1.373
0.666 -0.701 1.214 0.348 1.373
关键点是第二行和第三行是相等的,因为 normit 和 copit 迭代器将沿着同一个“底层序列”进行遍历。(顺便说一下,请注意,为了获得 self.call.im_self
的副本,我们必须使用 copy.copy
,而不是尝试直接获取 __copy__
方法,因为例如 random.Random
的实例支持通过 __getstate__
和 __setstate__
进行复制,而不是通过 __copy__
;实际上,使用 copy.copy 是获得任何对象的浅拷贝的正常方式——可复制的迭代器与众不同,是因为前面提到的关于 copy.copy
支持这些“可复制迭代器”规范的结果的不确定性)。
详情
除了在 Python 文档中添加一个建议,即用户编写的迭代器类型应该支持一个 __copy__
方法(当且仅当它可以用很少的内存和运行时成本实现,并产生一个独立可迭代的迭代器对象的副本)之外,这个 PEP 的实现将专门包括向内置 iter 返回的序列上的迭代器以及内置类型 dict 的方法 __iter__
、iterkeys、itervalues 和 iteritems 返回的字典上的迭代器添加可复制性。
由生成器函数产生的迭代器将不可复制。但是,由 Python 2.4 的新“生成器表达式” (PEP 289) 产生的迭代器应该可复制,如果它们的底层 iterator[s]
可复制;与生成器相比,生成器表达式中可能出现的内容的严格限制,应该会使这变得可行。类似地,由内置函数 enumerate
和模块 itertools 提供的某些函数产生的迭代器,如果底层迭代器可复制,则应该可复制。
这个 PEP 的实现还将包括对动机部分提到的新的 itertools.tee 函数的优化。
基本原理
对(浅层)迭代器复制的主要用例与函数 itertools.tee
(从 2.4 版本开始)相同。用户代码不会直接尝试复制迭代器,因为它需要单独处理不可复制的情况;调用 itertools.tee
将在适当的时候内部执行复制,并对不可复制的迭代器隐式回退到最高效的非复制策略。(偶尔,用户代码可能需要更直接的控制,特别是为了通过其他策略处理不可复制的迭代器,例如创建列表或将序列保存到磁盘)。
一个 tee’d 迭代器可以充当“参考点”,允许对序列的处理从已知点继续或恢复,而另一个独立的迭代器可以自由地向前推进以“探索”序列的更远部分,根据需要。一个简单的例子:一个生成器函数,它接受一个数字迭代器(假设为正数),返回一个相应的迭代器,其每个项目是与输入迭代器每个对应项目相对应的总和的比例。调用者可以将总和作为值传递,如果事先已知;否则,调用此生成器函数返回的迭代器将首先计算总和。
def fractions(numbers, total=None):
if total is None:
numbers, aux = itertools.tee(numbers)
total = sum(aux)
total = float(total)
for item in numbers:
yield item / total
能够 tee 数字迭代器允许这个生成器在需要时预先计算总和,如果数字迭代器是可复制的,则不一定需要 O(N) 辅助内存。
作为“迭代器书签”的另一个例子,考虑一个数字流,偶尔有一个字符串作为“后缀运算符”。到目前为止,最常见的这种运算符是 ' + ',在这种情况下,我们必须将所有先前的数字(从最后一个先前的运算符开始,或者从开头开始)加起来,并产生结果。有时我们会找到一个 ' * ',它是一样的,只是先前的数字必须相乘,而不是相加。
def filter_weird_stream(stream):
it = iter(stream)
while True:
it, bookmark = itertools.tee(it)
total = 0
for item in it:
if item=='+':
yield total
break
elif item=='*':
product = 1
for item in bookmark:
if item=='*':
yield product
break
else:
product *= item
else:
total += item
类似的 itertools.tee 用例可以支持诸如在由迭代器表示的命令流上“撤消”、在标记流解析上“回溯”等任务。(当然,在每种情况下,还应该考虑更简单的可能性,例如在使用单个迭代器在序列上步进时,将序列的相关部分保存到列表中,具体取决于任务的细节)。
以下是一个纯 Python 示例,说明如何将 'enumerate' 内置函数扩展以支持 __copy__
,如果其底层迭代器也支持 __copy__
class enumerate(object):
def __init__(self, it):
self.it = iter(it)
self.i = -1
def __iter__(self):
return self
def next(self):
self.i += 1
return self.i, self.it.next()
def __copy__(self):
result = self.__class__.__new__()
result.it = self.it.__copy__()
result.i = self.i
return result
以下是一个关于“迭代器的意外可复制性”导致的“脆弱性”类型的示例——为什么不能使用 copy.copy 预期(如果成功,则作为结果接收一个迭代器,该迭代器可以独立于原始迭代器进行迭代)。这是一个迭代器类,它以“先序遍历”方式遍历“树”,为了简单起见,这些树只是嵌套列表——任何是列表的项目都被视为子树,任何其他项目都被视为叶子。
class ListreeIter(object):
def __init__(self, tree):
self.tree = [tree]
self.indx = [-1]
def __iter__(self):
return self
def next(self):
if not self.indx:
raise StopIteration
self.indx[-1] += 1
try:
result = self.tree[-1][self.indx[-1]]
except IndexError:
self.tree.pop()
self.indx.pop()
return self.next()
if type(result) is not list:
return result
self.tree.append(result)
self.indx.append(-1)
return self.next()
现在,例如,以下代码
import copy
x = [ [1,2,3], [4, 5, [6, 7, 8], 9], 10, 11, [12] ]
print 'showing all items:',
it = ListreeIter(x)
for i in it:
print i,
if i==6: cop = copy.copy(it)
print
print 'showing items >6 again:'
for i in cop: print i,
print
没有按预期工作——“cop”迭代器随着原始“it”迭代器一步步地被消耗,因为 copy.copy
执行的意外(而不是故意)复制共享而不是复制“index”列表,它是可变属性 it.indx
(一个数字索引列表)。因此,这个迭代器的“客户端代码”,它试图通过对迭代器进行 copy.copy
来对序列的一部分进行两次迭代,是不正确的。
一些正确的解决方案包括使用 itertools.tee
,即,将第一个 for 循环更改为
for i in it:
print i,
if i==6:
it, cop = itertools.tee(it)
break
for i in it: print i,
(请注意,我们必须将循环分成两部分,否则我们仍然会在原始的 it 值上循环,在调用 tee 之后,它不能再被使用!);或者创建一个列表,即
for i in it:
print i,
if i==6:
cop = lit = list(it)
break
for i in lit: print i,
(同样,循环必须分成两部分,因为迭代器 'it' 会被调用 list(it)
所消耗)。
最后,如果 Listiter 提供了一个合适的 __copy__
方法,所有这些解决方案都将起作用,正如这个 PEP 所建议的那样
def __copy__(self):
result = self.__class__.new()
result.tree = copy.copy(self.tree)
result.indx = copy.copy(self.indx)
return result
不需要进行任何“更深层的”复制,但两个可变的“索引状态”属性确实必须被复制,以便实现“正确的”(独立可迭代的)迭代器复制。
推荐的解决方案是让 Listiter 类提供此 __copy__
方法,并让客户端代码使用 itertools.tee
(使用上面显示的两部分分割循环)。这将使客户端代码对它可能使用的不同迭代器类型具有最大的容忍度,同时在 teeing 这种特定迭代器类型时也能实现良好的性能。
参考
[1] 关于 python-dev 的讨论,从帖子开始:https://mail.python.org/pipermail/python-dev/2003-October/038969.html
[2] 标准库 copy 模块的在线文档:https://docs.pythonlang.cn/release/2.6/library/copy.html
版权
本文档已置于公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0323.rst
最后修改时间:2023-09-09 17:39:29 GMT