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”是与“it”的原始值相同的底层序列上的独立可迭代迭代器:这满足了应用程序对“迭代器复制”的需求。
然而,当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__甚至没有尝试复制序列;如果序列在原始迭代器和/或复制的迭代器仍在其上迭代时被更改,迭代行为很可能仍然会出错——改变这种迭代可变序列的迭代器的正常Python行为不是__copy__的责任(这也许是迭代器__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处理的迭代器可以作为“参考点”,允许序列处理从已知点继续或恢复,而另一个独立迭代器可以自由前进以根据需要“探索”序列的更深部分。一个简单的例子:一个生成器函数,给定一个数字迭代器(假定为正数),返回一个对应的迭代器,其每个项都是输入迭代器每个对应项占总数的比例。如果已知总数,调用者可以将其作为值传递;否则,通过调用此生成器函数返回的迭代器将首先计算总数。
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
如果数字迭代器是可复制的,那么teeing数字迭代器的能力允许这个生成器在需要时预先计算总数,而不一定需要O(N)的辅助内存。
作为“迭代器书签”的另一个例子,考虑一个数字流,其中偶尔会出现一个字符串作为“后缀运算符”。最常见的这种运算符是“+”,此时我们必须将之前的所有数字(如果存在上一个运算符,则从上一个运算符开始;否则从头开始)求和并返回结果。有时我们Instead遇到“*”,这与“+”相同,只是之前的数字必须相乘而不是求和。
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__,那么“enumerate”如何可以扩展以支持__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”列表,而不是复制它,而“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