Почему Contextmanager генерирует ошибку времени выполнения "генератор не остановился после throw()"?

В моей утилите у меня есть

@contextmanager
def rate_limit_protection(max_tries=3, wait=300):
    tries = 0
    while max_tries > tries:
        try:
            yield
            break
        except FacebookRequestError as e:
            pprint.pprint(e)
            if e._body['error']['message'] == '(#17) User request limit reached':
                print("waiting...")
                time.sleep(wait)
                tries += 1

В моем task.py я звоню:

for date in interval:
   with utility.rate_limit_protection():
      stats = account.get_insights(params=params)

После запуска задачи для заданного диапазона дат, после того, как ограничение скорости Facebook вступает в силу, программа ждет 300 секунд, после чего завершается с ошибкой.

File "/Users/kamal/.pyenv/versions/3.4.0/lib/python3.4/contextlib.py", line 78, in __exit__
    raise RuntimeError("generator didn't stop")
RuntimeError: generator didn't stop

1 ответ

Решение

with оператор не является циклической конструкцией. Его нельзя использовать для повторного выполнения кода. Менеджер контекста, созданный с @contextmanager должен только yield один раз.

Менеджер контекста делает (в основном) три вещи:

  1. Он запускает некоторый код перед блоком кода.
  2. Он запускает некоторый код после блока кода.
  3. Необязательно, он подавляет исключения, возникающие в блоке кода.

Если вы хотите сделать что-то подобное, вам нужно переписать это так, чтобы цикл перемещался за пределы диспетчера контекста или чтобы вообще не было диспетчера контекста.

Одним из вариантов может быть написание функции, которая принимает обратный вызов в качестве аргумента, а затем вызывает обратный вызов в цикле, подобном тому, который вы сейчас используете в вашем диспетчере контекста:

def do_rate_protection(callback, max_tries=3):
    tries = 0
    while max_tries > tries:
        try:
            callback()
            break
        except FacebookRequestError as e:
            # etc.

Затем вы можете назвать это так:

for date in interval:
    def callback():
        # code
    do_rate_protection(callback)

Если обратный вызов не нужен date переменная, вы можете переместить его за пределы цикла, чтобы избежать повторного создания одной и той же функции (что приводит к расточительству ресурсов). Вы также можете сделать date параметр из callback() функция и передать его с помощью functools.partial,

Другие вопросы по тегам