Понимание обработки StopIteration внутри генераторов для нетривиального случая - PullRequest
0 голосов
/ 29 октября 2018

Я помогаю поддерживать некоторый код, который теперь включает в себя автоматическое тестирование Python 3.7. Это привело меня к некоторым проблемам, связанным с PEP 479 «Изменение обработки StopIteration внутри генераторов». Мое наивное понимание заключалось в том, что вы можете использовать блок try-Кроме того, чтобы изменить старый код, чтобы он был совместим со всеми версиями Python, например,

Старый код:

def f1():
    it = iter([0])
    while True:
        yield next(it)

print(list(f1()))
# [0] (in Py 3.6)
# "RuntimeError: generator raised StopIteration" (in Py 3.7;
# or using from __future__ import generator_stop)

становится:

def f2():
    it = iter([0])
    while True:
        try:
            yield next(it)
        except StopIteration:
            return 

print(list(f2()))
# [0] (in all Python versions)

Для этого тривиального примера это работает, но я нашел для более сложного кода, который я рефакторинг это не делает. Вот минимальный пример с Py 3.6:

class A(list):
    it = iter([0])
    def __init__(self):
        while True:
            self.append(next(self.it))

class B(list):
    it = iter([0])
    def __init__(self):
        while True:
            try:
                self.append(next(self.it))
            except StopIteration:
                raise

class C(list):
    it = iter([0])
    def __init__(self):
        while True:
            try:
                self.append(next(self.it))
            except StopIteration:
                return  # or 'break'

def wrapper(MyClass):
    lst = MyClass()
    for item in lst:
        yield item

print(list(wrapper(A)))
# [] (wrong output)
print(list(wrapper(B)))
# [] (wrong output)
print(list(wrapper(C)))
# [0] (desired output)

Я знаю, что примеры A и B в точности эквивалентны и что случай C является правильным способом, совместимым с Python 3.7 (я также знаю, что рефакторинг в цикл for будет иметь смысл для многих примеров, включая этот придуманный).

Но вопрос в том, почему примеры с A и B создают пустой список [], а не [0]?

1 Ответ

0 голосов
/ 29 октября 2018

Первые два случая имеют непогашенную StopIteration, поднятую в классе __init__. Конструктор list прекрасно справляется с этим в Python 3.6 (возможно, с предупреждением, в зависимости от версии). Тем не менее, исключение распространяется до того, как wrapper получит возможность повторения: строка, которая фактически завершается с ошибкой, равна lst = MyClass(), и цикл for item in lst: никогда не запускается, в результате чего генератор становится пустым.

Когда я запускаю этот код в Python 3.6.4, я получаю следующее предупреждение в обеих print строках (для A и B):

DeprecationWarning: generator 'wrapper' raised StopIteration

Вывод здесь двоякий:

  1. Не позволяйте итератору работать самостоятельно. Это ваша работа, чтобы проверить, когда он останавливается. Это легко сделать с помощью петли for, но это нужно сделать вручную с помощью петли while. Случай A - хорошая иллюстрация.
  2. Не повышайте внутреннее исключение. Вместо этого верните None. Дело B это просто не тот путь. break или return будут работать правильно в блоке except, как вы это делали в C.

Учитывая, что циклы for являются синтаксическим сахаром для блока try-кроме в C, я бы вообще рекомендовал их использовать, даже при ручном вызове iter:

class D(list):
    it = iter([0])
    def __init__(self):
        for item in it:
            self.append(item)

Эта версия функционально эквивалентна C и выполняет всю бухгалтерию за вас. В очень редких случаях требуется фактический цикл while (пропускаются вызовы на next, что приходит на ум, но даже эти случаи можно переписать с помощью вложенного цикла).

...