Python3: есть ли во встроенной функции "карта" ошибка? - PullRequest
3 голосов
/ 27 марта 2020

Следующее у меня было с 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-l oop, распаковка внутри список, распаковывающий для аргументов функции, set, dict. Поэтому я считаю, что это не tuple, а map, это неправильно.

Редактировать: На самом деле, в Python 2 (2.7.10), "тот же" код повышает StopIteration. Я думаю, что это желаемый результат (за исключением того, что map в этом случае не возвращает итератор).

Ответы [ 3 ]

4 голосов
/ 27 марта 2020

Это не ошибка map. Это неприятное последствие решения Python полагаться на исключения для потока управления: фактические ошибки выглядят как нормальный поток управления.

Когда map вызывает next на iter(()), next вызывает StopIteration. Это StopIteration распространяется из map.__next__ в tuple вызов. Этот StopIteration выглядит как StopIteration, который map.__next__ обычно поднимает, чтобы обозначить конец карты, поэтому tuple считает, что на карте просто нет элементов.

Это приводит к более странным последствиям чем то, что вы видели. Например, итератор map не помечает себя исчерпанным, когда отображенная функция вызывает исключение, поэтому вы можете продолжать итерацию по нему даже после этого:

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

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

Вывод:

()
(1,)

(Реализация CPython map на самом деле не имеет способа пометить себя исчерпанной - для этого она опирается на лежащий в основе итератор (ы).)

Этот тип проблемы StopIteration было достаточно раздражающим, что они на самом деле изменили генераторную обработку StopIteration, чтобы смягчить это. Стоп-изменение использовалось для нормального распространения из генератора, но теперь, если стоп-изменение будет распространяться из генератора, он заменяется на RuntimeError, поэтому он не выглядит так, как будто генератор завершился нормально. Это влияет только на генераторы, но не на другие итераторы, такие как map.

1 голос
/ 27 марта 2020
  1. Правильно ли я понял поведение?

Не совсем. map принимает свой первый аргумент, функцию и применяет его к каждому элементу в некотором итерируемом, своем втором аргументе, пока не перехватит исключение StopIteration. Это внутреннее исключение, которое вызывается, чтобы сообщить функции, что она достигла конца объекта. Если вы вручную поднимаете StopIteration, он видит это и останавливается до того, как у него появится возможность обработать любой из (несуществующих) объектов в списке.

0 голосов
/ 29 марта 2020

Я являюсь автором вопроса, и я хотел бы кратко изложить здесь то, что я узнал и что я думаю, осталось. (Я не планирую публиковать его как новый вопрос.)

В Python, StopIteration из метода __next__ итератора трактуется как сигнал о том, что итератор достиг конца , (В противном случае это сигнал об ошибке.) Таким образом, метод итератора __next__ должен перехватывать все StopIteration, что не является сигналом конца.

Создается объект карты с кодом вида map(func, *iterables), где func является функцией, а *iterables обозначает конечную последовательность из одного (по состоянию на Python 3.8.1) или более итераций. Существует (как минимум) два вида подпроцесса процесса __next__ результирующего объекта карты, который может вызвать StopIteration:

  1. Процесс, в котором метод __next__ одного из итераций в вызывается последовательность *iterables.
  2. Процесс, в котором вызывается аргумент func.

Намерение map, как я понимаю из его документа (или отображается help(map)) означает, что StopIteration из подпроцесса вида (2) НЕ является концом объекта карты. Однако текущее поведение __next__ объекта карты таково, что в этом случае его процесс генерирует StopIteration. (Я не проверял, действительно ли он ловит StopIteration или нет. Если это так, то в любом случае он снова поднимает StopIteration.) Это является причиной проблемы, о которой я спрашивал.

В ответе выше, user2357112 поддерживает Monica (позвольте мне дружелюбно сокращать название до «User Primes»), находит последствия этого уродства, но ответил, что это ошибка Python, а не map. К сожалению, я не нахожу убедительной поддержки этого вывода в ответе. Я подозреваю, что исправление map было бы лучше, но некоторые другие люди, похоже, не согласны с этим по соображениям производительности. Я ничего не знаю о реализации встроенных функций Python и не могу судить. Так что этот вопрос оставлен для меня. Тем не менее, ответ пользователя Primes был достаточно информативным, чтобы оставить левый вопрос для меня неважным. (Спасибо, user2357112 снова поддерживает Монику!)

Кстати, код, который я пытался опубликовать в комментарии к ответу пользователя, выглядит следующим образом. (Я думаю, что это сработало бы до 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")

Ниже приведена немного другая версия этого (опять же, версия с двумя аргументами), которая может быть более удобной (опубликовано с надеждой получать предложения по улучшению!).:

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
...