【问题标题】:Python3: Does the built-in function "map" have a bug?Python3:内置函数“map”有bug吗?
【发布时间】:2020-03-27 01:17:46
【问题描述】:

以下是我使用 Python 3.8.1(在 macOS Mojave 上,10.14.6 上,如 以及其他一些平台上的 Python 3.7(或更早版本))。我是新来的 计算,不知道如何要求改进 语言,但我想我发现了内置的奇怪行为 函数map

由于代码 next(iter(())) 引发 StopIteration,我希望 从以下代码中获取StopIteration

tuple(map(next, [iter(())]))

令我惊讶的是,这会默默地返回元组 ()

所以看起来地图对象的解包在什么时候停止了 StopIteration 来自 next 击中“空”迭代器 由iter(()) 返回。但是,我不认为例外是 处理得当,因为 StopIteration 在“空”之前没有提出 从列表中选择迭代器(被next 命中)。

  1. 我是否正确理解了该行为?
  2. 这种行为是否有意为之?
  3. 这会在不久的将来改变吗?或者我怎样才能得到它?

编辑:如果我以不同的方式解包地图对象,例如list、for-loop、在列表中解包、函数参数解包、setdict,则行为相似。所以我认为不是tuple 而是map 这是错误的。

编辑:实际上,在 Python 2 (2.7.10) 中,“相同”的代码会引发 StopIteration。我认为这是理想的结果(除了 map 在这种情况下不返回迭代器)。

【问题讨论】:

  • 这种行为看起来是正确的,因为map(next, [iter(())]) 返回一个(空)地图对象
  • map 没有捕捉到 StopIteration 异常。它让它传播,看起来就像地图的尽头。
  • @chepner map 没有捕捉到StopIteration。它冒泡然后 tuple(...) 认为它是可迭代的结束。
  • @JoshAbraham:你能解释一下为什么应该返回一个(空)地图对象而不会出错吗?
  • Josh 错了,或者至少没有清楚地沟通。 map(next, [iter(())]) 返回看起来像普通空地图的东西,因为 StopIterationnext 传播出来,但它不是普通的空地图。

标签: python python-3.x behavior built-in map-function


【解决方案1】:

这不是map 错误。这是 Python 决定依赖异常来控制流的一个丑陋后果:实际错误看起来像正常的控制流。

mapiter(()) 上调用next 时,next 引发StopIteration。这个StopIteration 传播出map.__next__ 并进入tuple 调用。这个StopIteration 看起来像map.__next__ 通常会提升以表示地图结束的StopIteration,所以tuple 认为地图只是缺少元素。

这会导致比你看到的更奇怪的后果。例如,当映射函数引发异常时,map 迭代器不会将自身标记为已耗尽,因此您甚至可以在之后继续迭代它:

m = map(next, [iter([]), iter([1])])

print(tuple(m))
print(tuple(m))

输出:

()
(1,)

(CPython map 实现实际上并没有办法将自己标记为耗尽 - 它依赖于底层迭代器。)

这种 StopIteration 问题很烦人,以至于他们实际上 changed generator 处理 StopIteration 来缓解它。 StopIteration 过去通常从生成器中传播出去,但是现在,如果 StopIteration 会从生成器中传播出来,它会被 RuntimeError 替换,因此它看起来不像生成器正常结束。不过,这只影响生成器,不会影响其他迭代器,例如 map

【讨论】:

  • 很好解释 (+1)。 hasattr 有一个类似的“错误异常”陷阱:如果属性 getter 中的一些错误代码导致 AttributeErrorhasattr 不明白发生了什么,只是报告该属性不存在。
  • 感谢您的详细回答。所以我的问题 (1) 的答案是否定的:这不是map 的问题。对于我的问题(2),无论如何,整个行为都不是有意的,这是正确的吗?
  • 好吧,我不认为可以知道意图。这种行为对某些人来说很烦人(我认为这是出于正确的原因)。
  • 我仍然不相信“(依赖)控制流异常”是一个坏主意。事实上,即使使用旧的(即,在 PEP 479 之前)处理 StopIteration 的生成器,以下函数 map2 将(尽管我没有超过 3.5 的 Python 3)可以正常工作(尽管不是在之后PEP 479)。
  • 代码:def map2(function, iterable): "This is a 2-argument version for simplicity." iterator = iter(iterable) while True: arg = next(iterator) # StopIteration out here would have been propagated. try: yield function(arg) except StopIteration: raise RuntimeError("generator raised StopIteration")
【解决方案2】:
  1. 我是否正确理解了该行为?

不完全是。 map 接受它的第一个参数,一个函数,并将它应用于某个可迭代的每个项目,它的第二个参数,直到它捕获到 StopIteration 异常。这是一个内部异常,用于告诉函数它已到达对象的末尾。如果您手动提升StopIteration,它会看到并停止,然后才有机会处理列表中的任何(不存在的)对象。

【讨论】:

  • 感谢您的回答!我认为 map 应该只在它的第二个参数停止迭代时停止迭代,而不是当它的第一个参数因任何原因引发 StopIteration 时才停止迭代。这就是为什么我觉得当前的行为很奇怪。
  • @TakuoMatsuoka 但是map 怎么能分辨出来呢?您是否建议自省异常实例的回溯?这可能会很慢而且很不稳定。
  • @TakuoMatsuoka 好笑? 好的,这个答案的措辞不太好,也许“catch”应该换成“reaches”。实际上, map 根本不迭代,它只是返回一个迭代器,你必须用其他东西进行迭代。无论迭代地图的“其他事物”通常是什么捕获了StopIteration 异常。这有意义吗?
  • 这可能只是措辞,但map没有应用该功能;它创建一个保存对函数和可迭代对象的引用的对象,当您调用此对象的 __next__ 方法时,它会调用可迭代对象的 __next__ 方法,将函数应用于结果,并返回该结果。
  • @wim 是的,迭代的不是map。正确的表达是“只有当map(创建地图对象)的第二个参数停止迭代时,地图对象才应该停止迭代”或者进一步,“地图对象的__next__方法应该引发(或让传播)@ 987654334@ 仅当StopIteration 来自map 的第二个参数的__next__ 方法时。感谢您指出困惑。
【解决方案3】:

我是问题的发布者,我愿意 想在这里总结一下我学到的东西和我的想法 左。 (我不打算将其作为新问题发布。)

在 Python 中,StopIteration 来自 __next__ 方法 iterator 被视为迭代器已到达末尾的信号。 (否则,这是错误信号。)因此,__next__ 迭代器的方法必须捕获所有StopIteration 意味着结束的信号。

使用map(func, *iterables) 形式的代码创建地图对象,其中func 是一个函数,*iterables 代表 对于一个(从 Python 3.8.1 开始)或多个迭代的有限序列。 __next__ 进程有(至少)两种子进程 可能会引发StopIteration的结果地图对象:

  1. 其中一个可迭代对象的__next__ 方法在其中的过程 序列*iterables 被调用。
  2. 调用参数func的进程。

map 的意图,据我所知,document (或由help(map) 显示)是StopIteration 来自类型 (2) 的子进程不是地图对象的结尾。 但是,地图对象的__next__ 的当前行为是这样的 在这种情况下,它的进程会发出StopIteration。 (我没有 检查它是否真的捕获StopIteration。如果它 确实如此,然后无论如何它都会再次引发StopIteration。)这会出现 我问的问题的原因。

在上面的答案中,user2357112 支持 Monica(让我友好地将名称缩写为“User Primes”)发现了这种丑陋的后果,但回答是 Python 的错, 而不是map's。不幸的是,我在答案中没有找到令人信服的支持这一结论。我怀疑修复map 会更好,但是 出于性能原因,其他一些人似乎不同意这一点。一世 对Python内置函数的实现一无所知 并且无法判断。所以这一点留给了我。尽管如此,User Primes 的回答足够丰富,可以留下左边的问题 现在对我来说不重要了。 (感谢user2357112再次支持莫妮卡!)

顺便说一下,我试图在 User Primes 的评论中发布的代码 答案如下。 (我认为它会在 PEP 479 之前起作用。)

def map2(function, iterable):
    "This is a 2-argument version for simplicity."
    iterator = iter(iterable)
    while True:
        arg = next(iterator) # StopIteration out here would have been propagated.
        try:
            yield function(arg)
        except StopIteration:
            raise RuntimeError("generator raised StopIteration")

下面是一个稍微不同的版本(同样,一个 2参数版本),这可能更方便(发布希望得到 改进建议!)。:

import functools
import itertools

class StopIteration1(RuntimeError):
    pass

class map1(map):
    def __new__(cls, func, iterable):
        iterator = iter(iterable)
        self = super().__new__(cls, func, iterator)
        def __next__():
            arg = next(iterator)
            try:
                return func(arg)
            except StopIteration:
                raise StopIteration1(0)
            except StopIteration1 as error:
                raise StopIteration1(int(str(error)) + 1)
        self.__next__ = __next__
        return self
    def __next__(self):
        return self.__next__()

# tuple(map1(tuple,
#            [map1(next,
#                  [iter([])])]))
# ---> <module>.StopIteration1: 1

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-01-27
    • 2010-09-20
    • 1970-01-01
    • 2021-02-17
    • 2017-04-26
    • 2018-12-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多