Monty 中 itertools 模块的实现子集:可用适配器、行为差异与资源限制指南
【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/monty
导读
itertools是 Monty(用 Rust 编写的、面向 AI 的极简安全 Python 解释器)标准库中少数几个被部分实现的模块之一。本文以 limitations/itertools.md 为骨架,完整梳理 Monty 中itertools已实现的 14 个可调用对象、未实现名称的处理方式、与 CPython 3.14 的逐条行为差异,以及无限迭代器与资源限制(内存、时长、递归深度)的交互机制。读完本文,你将能在沙箱内安全、正确地使用itertools,并能在遇到行为差异时快速定位原因——例如为什么takewhile遇到外部函数会抛NotImplementedError,为什么list(itertools.count())会触发MemoryError,以及如何通过islice、takewhile等适配器为无限序列加上边界。
一、Monty 的 itertools:一个“够用即止”的实现子集
Monty 的定位是“为模型提供足够表达意图的 Python 子集”,而非追求完整性的 Python 实现。itertools正是这一理念的缩影:模块中只注册了 CPythonitertools的一小部分可调用对象,其余名字一律不出现。
从源码看,模块入口 用一个静态映射ITERTOOLS_FUNCTIONS把 14 个函数名绑定到ItertoolsFunctions枚举变体,再由create_module逐个挂到模块对象上:
// crates/monty/src/modules/itertools.rs const ITERTOOLS_FUNCTIONS: &[(StaticStrings, ItertoolsFunctions)] = &[ (StaticStrings::Count, ItertoolsFunctions::Count), (StaticStrings::Repeat, ItertoolsFunctions::Repeat), (StaticStrings::Pairwise, ItertoolsFunctions::Pairwise), (StaticStrings::Compress, ItertoolsFunctions::Compress), (StaticStrings::Islice, ItertoolsFunctions::Islice), (StaticStrings::Chain, ItertoolsFunctions::Chain), (StaticStrings::Cycle, ItertoolsFunctions::Cycle), (StaticStrings::Takewhile, ItertoolsFunctions::Takewhile), (StaticStrings::Dropwhile, ItertoolsFunctions::Dropwhile), (StaticStrings::Filterfalse, ItertoolsFunctions::Filterfalse), (StaticStrings::Starmap, ItertoolsFunctions::Starmap), (StaticStrings::Accumulate, ItertoolsFunctions::Accumulate), (StaticStrings::Batched, ItertoolsFunctions::Batched), (StaticStrings::ZipLongest, ItertoolsFunctions::ZipLongest), ];所有适配器对象共享同一个HeapData::Itertools(ItertoolsIter)堆变体(见 类型定义),Python 层面可见的类型名与错误消息则交由Type区分——这是 Monty 整个迭代器家族的统一表示方式。
1.1 已实现的调用约定(与 CPython 3.14 对齐)
Monty 的itertools函数在参数、返回值、repr()与错误消息上尽量对齐 CPython 3.14(除下文注明的差异外):
| 可调用对象 | 签名 |
|---|---|
count | count(start=0, step=1) |
repeat | repeat(object, times=?) |
pairwise | pairwise(iterable) |
compress | compress(data, selectors) |
islice | islice(iterable, [start,] stop[, step]) |
chain | chain(*iterables) |
cycle | cycle(iterable) |
takewhile | takewhile(predicate, iterable) |
dropwhile | dropwhile(predicate, iterable) |
filterfalse | filterfalse(predicate, iterable) |
starmap | starmap(function, iterable) |
accumulate | accumulate(iterable, func=None, *, initial=None) |
batched | batched(iterable, n, *, strict=False) |
zip_longest | zip_longest(*iterables, fillvalue=None) |
每个可调用对象都有独立参数绑定结构体,并严格复刻 CPython 的参数解析风格:例如pairwise、cycle采用PyArg_UnpackTuple语义(pairwise expected 1 argument, got 2、pairwise() takes no keyword arguments),而count、repeat、compress采用具名 C 家族语义(参数与关键字合并计数,count() takes at most 2 arguments (3 given)),accumulate、batched则复刻 Argument Clinic 的措辞(如batched() takes exactly 2 positional arguments (3 given))。这些细节在 测试用例 中逐条断言。
1.2 未实现:名字缺席而非占位
除上述 14 个外,itertools的其余 API 全部未实现:combinations、combinations_with_replacement、groupby、permutations、product、tee等都不在模块命名空间中。
关键点:这些名字是从模块命名空间整体缺席,而不是用占位 stub 撑场。因此:
- 在类型检查阶段就会被拒绝(
Module 'itertools' has no member 'chain'这类提示); - 运行时访问抛
AttributeError。
这一“缺席优于 stub”的设计贯穿 Monty 整个标准库(见 limitations/index.md 对标准库的总体说明),让不可用 API 尽量在运行前失败。
1.3chain.from_iterable为何不可用
即使chain本身已实现,chain.from_iterable依然缺席:它本是chain内置对象上的一个类方法,而 Monty 的模块函数不暴露任何属性。因此:
import itertools itertools.chain.from_iterable([[1], [2]]) # AttributeError: 'builtin_function_or_method' object has no attribute 'from_iterable'原文档给出的替代方案是直接用星号展开:chain(*iterables)。在 Python 里这等价于chain.from_iterable(iterables)的常见用法,代价仅是参数需要可解包(列表、元组皆可)。
二、与 CPython 的行为差异清单
Monty 对差异采取“写下来才算数”的策略:凡是未在本文档记录的行为,均假定与 CPython 3.14 一致。以下是已记录的逐条差异。
2.1repeat.__length_hint__()抛AttributeError
CPython 通过__length_hint__暴露剩余产出次数:
# CPython repeat(9, 3).__length_hint__() == 3Monty 内部确实用剩余次数来为list()/tuple()的结果预分配容量,但不把它暴露成 Python 可见属性,因此访问即抛AttributeError。也就是说,你在沙箱内无法通过该魔法方法获知repeat还剩多少项。
2.2count与repeat对象不可哈希
hash(itertools.count()) # TypeError: unhashable type: 'itertools.count'CPython 对这类对象回退到身份哈希(identity hashing),Monty 则直接拒绝。这并非count/repeat独有,而是 Monty 迭代器的普遍规则。
2.3count只接受int、float与bool
CPython 接受一切满足PyNumber_Check的类型(如Decimal、Fraction、complex),而 Monty 没有这些数值类型,因此统一的TypeError: a number is required覆盖所有非数值参数。从源码看,is_number的检查正是枚举 Monty 仅有的三种数值类型:
fn is_number(value: &Value, vm: &VM<'_>) -> bool { matches!(value.py_type_heap(vm.heap), Type::Int | Type::Float | Type::Bool) }顺带一提,start/step传入bool时会被“展宽”成对应的int——repr(count(True))是count(1)而非count(True),这与 CPythoncount_new的行为一致(源码注释)。同时count支持大整数推进,count(2**62, 2**62)会正常升格为长整数(测试见 itertools__count_repeat.py)。
2.4 嵌套循环的repr()提前一层解卷
对“容器回指到持有它的repeat”这种自引用结构,Monty 打印repeat([...]),而 CPython 打印repeat([repeat([...])])。这是 Monty 在repr()中通用的循环检测所致,并非itertools特有。
2.5 会挂起的可调用对象被拒绝而非暂停(重要)
takewhile、dropwhile、filterfalse、starmap和accumulate通过同步的evaluate_function路径应用其可调用对象:该路径会把一个帧跑到结束,无法向宿主机让出(yield)。因此,一旦谓词/函数触达外部函数、os操作或宿主方法调用,就会抛出:
NotImplementedError: takewhile(): external function 'f' is not yet supported in this contextCPython 会直接调用它。这是与__init__、__next__、__repr__相同的限制(参见 limitations/classes.md);普通的沙箱内定义函数与 lambda 不受影响。也就是说,takewhile(lambda x: x < 3, ...)这类用法完全没问题,但谓词里调用宿主暴露的函数则会失败。
2.6zip_longest会点名被拒绝的关键字
非法关键字在 Monty 下报zip_longest() got an unexpected keyword argument 'bogus',而 CPython 手写校验、不带参数名地报zip_longest() got an unexpected keyword argument——CPython 是唯一省略参数名的家族成员。其余每个适配器的措辞都与 CPython 一致(源码注释见 itertools.rs)。
2.7 可重入的zip_longest停止而非无限填充
若某个源在它自己的__next__内部去步进同一个zip_longest,它可能在外层轮次抵达前耗尽其余槽位。两者的分歧从下一行开始:
- 两个引擎对外层轮次产出的那一行意见一致(该行会用
fillvalue补齐空槽); - 但从下一行起,Monty 观察到所有槽位都已耗尽,于是抛
StopIteration; - CPython 则让
numactive计数跌到负值,之后每次next()都无限地产生一个全fillvalue的元组。
只有可重入才会触发:普通源不可能在拥有它的适配器处于轮次中途时运行。测试用例 itertools__adaptors.py 用一个Reentrant源固定了这一行为:next(reentrant_zip)得到('a', None, None),随后即停止。
2.8 跨宿主边界丢失repr
count/repeat对象返回宿主机时,呈现为<itertools.count object>/<itertools.repeat object>,而不是沙箱内的repr()(如count(0)、repeat(7, 3))。Monty 对所有迭代器都这样表示,不会递归进它们持有的内容。
2.9batched的n受 worker 指针宽度限制
在 wasm worker(32 位指针)上,n达到或超过2**31会抛:
OverflowError: Python int too large to convert to C ssize_tCPython 则接受该值。根源在于 batched_n:n最终被转换为isize(对齐Py_ssize_t),32 位宿主上isize的转换边界自然更小。
2.10batched类型无法下传至低于 Python 3.12 的宿主
itertools.batched是 Python 3.12 才加入的。把type(itertools.batched(...))返回给低于 3.12 的宿主机时会抛:
TypeError: Cannot convert itertools.batched to a host type: this Python does not define it其余所有适配器的类型在所有受支持宿主上都能解析。
三、无限迭代器与“急切”的内置函数
这是最容易踩坑的组合,值得单列一节。Monty 的map()、filter()和enumerate()是急切的:它们会把源全部榨干进一个列表再返回具体结果,而不是像 CPython 那样返回惰性迭代器(这是内置函数的既有属性,参见 limitations/builtins.md)。
把它们套在无限itertools迭代器上,就意味着永不返回(直到某个资源限制被触发):
import itertools map(str, itertools.count()) # CPython: 惰性。Monty: 一直运行到限制触发。 filter(bool, itertools.repeat(1)) # 同理 enumerate(itertools.count()) # 同理作为对照,zip()在最短输入处停止,所以zip(itertools.count(), 'ab')与 CPython 行为一致;手工用next()切片无限迭代器也一样安全。count()/repeat()是沙箱代码最容易触达这种“无限 + 急切”组合的入口,写作时必须牢记。
四、资源限制:把无限序列关进笼子
Monty 的资源限制体系(ResourceLimits,涉及内存max_memory、时长max_duration、递归深度)是它作为“安全解释器”的核心,而itertools的无限迭代器正是检验这些限制的最佳对象。
4.1count()与repeat(x):无界消费的唯一终局是资源上限
count()与repeat(x)(省略times)是无穷的。不带边界地消费(如list(itertools.count()))只有在宿主配置了内存或时长限制时才会终止,且抛的是MemoryError而非自然耗尽。
而ResourceLimits::default()两者都不设置(只设递归深度),因此在这种默认配置下,list(itertools.count())会一直跑到宿主自身内存耗尽。原文档明确指出:这与while True:循环的暴露面完全相同,并非itertools特有。
4.2 丢弃型适配器主动轮询max_duration
有一类适配器在产出任何东西之前会先“丢”项目,它们在循环期间自己轮询max_duration:
dropwhile与filterfalse在首个被接受项之前;compress穿过一段 falsy 选择器;islice跳到start;chain跨过一个已耗尽的源。
因此,对无限源做丢弃式扫描会抛TimeoutError而不是空转。轮询是摊还的(每 64 项一次),所以时长限制最多可能被超出这么多工作量。CPython 根本没有时长限制,会永远循环。
相关测试见 resource_limits.rs,其中明确列出next(itertools.dropwhile(bool, itertools.count(1)))、next(itertools.filterfalse(bool, itertools.count(1)))等会本地空转的适配器场景。
4.3batched:整批填充与内存预检
batched(iterable, n)在一次next()内填满一整批,所以大n配长源同样是非让出循环,并按同样方式轮询max_duration。
内存方面有两套策略:
- 若源有精确的 size hint,
batched会按min(hint, n)预检一批对max_memory的占用——batched(range(10**9), 10**9)因此会一开始就抛MemoryError,而不是在填充途中; - 没有 size hint 的源没有预检,填充循环在同样的摊还节奏上同时轮询
max_memory与max_duration——batched(count(), 10**9)会在填充途中抛MemoryError。
4.4cycle:缓冲计入内存上限
cycle(iterable)必须缓存迄今见过的每个项目以便回放,该缓冲随增长计入max_memory。所以在超长源上循环会在达到限制时抛MemoryError,而不是等到源耗尽。CPython 缓存同样的项目却没有这个上限。
4.5 嵌套适配器受max_recursion_depth约束
除count与repeat外,所有“驱动源”的适配器在把next()委托给被包裹的迭代器时计一个递归层级。因此嵌套深度超过限制的适配器链,在消费时会抛RecursionError。
反过来,从自身状态作答、不触碰源的适配器不计层级:
- 已耗尽的
batched; - 已闩锁(latched)的
takewhile(谓词已拒绝过一项); - 正在产出
initial的accumulate。
CPython 没有这种按适配器计的边界,深嵌套在那里只受 C 栈限制。对应的递归测试(nested_itertools_adaptors_are_bounded_by_the_recursion_limit等)见 resource_limits.rs,而itertools的垃圾回收接线(缺失即编译错误)见 types/itertools/mod.rs。
五、沙箱内安全使用的实践清单
结合上述行为差异与资源限制,给出可直接落地的使用建议:
- 给无限序列加边界再消费:优先用
islice、takewhile、zip(截断到最短)或chain组合出有限序列。测试用例中的常见范式如:
import itertools list(itertools.pairwise(itertools.islice(itertools.count(), 4))) # [(0, 1), (1, 2), (2, 3)] list(itertools.islice(itertools.chain(itertools.repeat(1, 2), [2]), 2)) # [1, 1] list(itertools.islice(itertools.cycle([1, 2, 3]), 7)) # [1, 2, 3, 1, 2, 3, 1]勿对无限源使用急切内置:
map、filter、enumerate在 Monty 中会榨干源,套上count()/repeat()即永不返回;改用zip_longest、islice等适配器表达。谓词保持“沙箱内可完成”:
takewhile/dropwhile/filterfalse/starmap/accumulate的回调若触达外部函数或宿主方法会抛NotImplementedError,请只用沙箱内定义函数与 lambda。不要依赖
__length_hint__、可哈希性与跨宿主repr:repeat不暴露长度提示,count/repeat不可哈希,跨界对象以<itertools.xxx object>形式呈现。留意
batched的平台差异:wasm 32 位 worker 上n >= 2**31抛OverflowError;向低于 Python 3.12 的宿主回传batched类型会失败。托管长循环:宿主侧配置
max_duration/max_memory后,丢弃型适配器、batched填充与cycle缓冲都会在限制处抛TimeoutError/MemoryError而非无限空转。
相关文档与源码
- 差异总览与标准库模块清单:limitations/index.md
- 内置函数(
map/filter/enumerate的急切性):limitations/builtins.md - 类 dunder 与可挂起上下文的限制:limitations/classes.md
- 模块实现与参数解析:crates/monty/src/modules/itertools.rs
- 迭代器类型族与 GC 接线:crates/monty/src/types/itertools/mod.rs
- 功能测试:itertools__adaptors.py、itertools__count_repeat.py、refcount__itertools_adaptors.py
- 资源限制(时长/内存/递归)测试:crates/monty/tests/resource_limits.rs
【免费下载链接】montyA minimal, secure Python interpreter written in Rust for use by AI项目地址: https://gitcode.com/GitHub_Trending/monty3/monty
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考