Подтвердить что ты не робот

Next() не играет хорошо с любым/все в python

Сегодня я столкнулся с ошибкой, потому что я использовал next() для извлечения значения, а "не найден" выбрал StopIteration.

Обычно это останавливает программу, но функция, использующая next, вызывается внутри итерации all(), поэтому all просто заканчивается раньше и возвращается True.

Это ожидаемое поведение? Существуют ли стильные руководства, которые помогают избежать такого рода вещей?

Упрощенный пример:

def error(): return next(i for i in range(3) if i==10)
error() # fails with StopIteration
all(error() for i in range(2)) # returns True
4b9b3361

Ответ 1

Хотя это поведение по умолчанию в версиях Python до 3.6 включительно, оно считается ошибкой в ​​языке и должно изменяться в Python 3.7, так что вместо этого возникает исключение.

Как PEP 479 говорит:

Взаимодействие генераторов и StopIteration в настоящее время несколько удивительно и может скрывать неясные ошибки. Неожиданное исключение не должно приводить к тонко измененному поведению, но должно вызывать шумную и легко отлаженную трассировку. В настоящее время StopIteration, случайно вызванный внутри функции генератора, будет интерпретироваться как конец итерации контуром, управляющим генератором.

Начиная с версии Python 3.5, можно изменить поведение по умолчанию на 3,7. Этот код:

# gs_exc.py

from __future__ import generator_stop

def error():
    return next(i for i in range(3) if i==10)

all(error() for i in range(2))

... вызывает следующее исключение:

Traceback (most recent call last):
  File "gs_exc.py", line 8, in <genexpr>
    all(error() for i in range(2))
  File "gs_exc.py", line 6, in error
    return next(i for i in range(3) if i==10)
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "gs_exc.py", line 8, in <module>
    all(error() for i in range(2))
RuntimeError: generator raised StopIteration

В Python 3.5 и 3.6 без импорта __future__ возникает предупреждение. Например:

# gs_warn.py

def error():
    return next(i for i in range(3) if i==10)

all(error() for i in range(2))

$ python3.5 -Wd gs_warn.py 
gs_warn.py:6: PendingDeprecationWarning: generator '<genexpr>' raised StopIteration
  all(error() for i in range(2))

$ python3.6 -Wd gs_warn.py 
gs_warn.py:6: DeprecationWarning: generator '<genexpr>' raised StopIteration
  all(error() for i in range(2))

Ответ 2

Проблема заключается не в использовании all, а в том, что у вас есть выражение генератора в качестве параметра all. StopIteration распространяется на выражение генератора, которое на самом деле не знает, откуда оно возникло, поэтому оно делает обычное дело и завершает итерацию.

Это можно увидеть, заменив вашу функцию error тем, что вызывает ошибку напрямую:

def error2(): raise StopIteration

>>> all(error2() for i in range(2))
True

Последний фрагмент головоломки - это знать, что делает all с пустой последовательностью:

>>> all([])
True

Если вы собираетесь использовать next напрямую, вы должны быть готовы поймать StopIteration самостоятельно.

Изменить: приятно видеть, что разработчики Python считают эту ошибку и предпринимают шаги по ее изменению в версии 3.7.