Управление оператором yield в Python

PythonPythonBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом практическом занятии (лабораторной работе) вы научитесь управлять тем, что происходит на операторах yield в Python. Вы поймете, как эффективно обрабатывать операции и поведение, связанные с этими операторами.

Кроме того, вы узнаете о времени жизни генераторов и обработке исключений в генераторах. Во время этого обучения будут модифицированы файлы follow.py и cofollow.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ErrorandExceptionHandlingGroup(["Error and Exception Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/ErrorandExceptionHandlingGroup -.-> python/catching_exceptions("Catching Exceptions") python/ErrorandExceptionHandlingGroup -.-> python/finally_block("Finally Block") python/AdvancedTopicsGroup -.-> python/generators("Generators") subgraph Lab Skills python/catching_exceptions -.-> lab-132525{{"Управление оператором yield в Python"}} python/finally_block -.-> lab-132525{{"Управление оператором yield в Python"}} python/generators -.-> lab-132525{{"Управление оператором yield в Python"}} end

Понимание времени жизни и закрытия генераторов

В этом шаге мы рассмотрим время жизни генераторов Python и научимся правильно их закрывать. Генераторы в Python - это особый тип итераторов, которые позволяют генерировать последовательность значений по мере необходимости, а не вычислять их все сразу и хранить в памяти. Это может быть очень полезно при работе с большими наборами данных или бесконечными последовательностями.

Что такое генератор follow()?

Начнем с рассмотрения файла follow.py в директории проекта. Этот файл содержит функцию - генератор с именем follow(). Функция - генератор определяется как обычная функция, но вместо ключевого слова return она использует yield. Когда вызывается функция - генератор, она возвращает объект - генератор, по которому можно итерироваться, чтобы получить значения, которые он выдает.

Функция - генератор follow() непрерывно считывает строки из файла и выдает каждую строку по мере ее считывания. Это похоже на команду Unix tail -f, которая непрерывно отслеживает файл на предмет новых строк.

Откройте файл follow.py в редакторе WebIDE:

import os
import time

def follow(filename):
    with open(filename,'r') as f:
        f.seek(0,os.SEEK_END)
        while True:
            line = f.readline()
            if line == '':
                time.sleep(0.1)    ## Sleep briefly to avoid busy wait
                continue
            yield line

В этом коде оператор with open(filename, 'r') as f открывает файл в режиме чтения и гарантирует, что он будет правильно закрыт при выходе из блока. Строка f.seek(0, os.SEEK_END) перемещает указатель файла в конец файла, чтобы генератор начал чтение с конца. Цикл while True непрерывно считывает строки из файла. Если строка пустая, это означает, что пока нет новых строк, поэтому программа засыпает на 0,1 секунды, чтобы избежать активного ожидания, и затем переходит к следующей итерации. Если строка не пустая, она выдается.

Этот генератор работает в бесконечном цикле, что вызывает важный вопрос: что происходит, когда мы перестаем использовать генератор или хотим остановить его раньше?

Модификация генератора для обработки закрытия

Мы должны модифицировать функцию follow() в файле follow.py, чтобы обработать случай, когда генератор корректно закрывается. Для этого мы добавим блок try - except, который перехватывает исключение GeneratorExit. Исключение GeneratorExit возникает, когда генератор закрывается, либо в результате сборки мусора, либо при вызове метода close().

import os
import time

def follow(filename):
    try:
        with open(filename,'r') as f:
            f.seek(0,os.SEEK_END)
            while True:
                line = f.readline()
                if line == '':
                    time.sleep(0.1)    ## Sleep briefly to avoid busy wait
                    continue
                yield line
    except GeneratorExit:
        print('Following Done')

В этом модифицированном коде блок try содержит основную логику генератора. Если возникает исключение GeneratorExit, блок except его перехватывает и выводит сообщение 'Following Done'. Это простой способ выполнить действия по очистке, когда генератор закрывается.

Сохраните файл после внесения этих изменений.

Эксперименты с закрытием генераторов

Теперь проведем несколько экспериментов, чтобы увидеть, как ведут себя генераторы при сборке мусора или явном закрытии.

Откройте терминал и запустите интерпретатор Python:

cd ~/project
python3

Эксперимент 1: Сборка мусора работающего генератора

>>> from follow import follow
>>> ## Experiment: Garbage collection of a running generator
>>> f = follow('stocklog.csv')
>>> next(f)
'"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314\n'
>>> del f  ## Delete the generator object
Following Done  ## This message appears because of our GeneratorExit handler

В этом эксперименте мы сначала импортируем функцию follow из файла follow.py. Затем создаем объект - генератор f, вызвав follow('stocklog.csv'). Мы используем функцию next() для получения следующей строки из генератора. Наконец, удаляем объект - генератор с помощью оператора del. Когда объект - генератор удаляется, он автоматически закрывается, что вызывает наш обработчик исключения GeneratorExit, и выводится сообщение 'Following Done'.

Эксперимент 2: Явное закрытие генератора

>>> f = follow('stocklog.csv')
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         f.close()  ## Explicitly close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
Following Done
>>> for line in f:
...     print(line, end='')  ## No output: generator is closed
...

В этом эксперименте мы создаем новый объект - генератор f и итерируемся по нему с помощью цикла for. Внутри цикла мы выводим каждую строку и проверяем, содержит ли строка строку 'IBM'. Если содержит, мы вызываем метод close() у генератора, чтобы явно закрыть его. Когда генератор закрывается, возникает исключение GeneratorExit, и наш обработчик исключений выводит сообщение 'Following Done'. После закрытия генератора, если мы попытаемся итерироваться по нему снова, не будет вывода, так как генератор больше не активен.

Эксперимент 3: Выход из итерации и продолжение работы генератора

>>> f = follow('stocklog.csv')
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         break  ## Break out of the loop, but don't close the generator
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
"GM",31.45,"6/11/2007","09:34.31",0.45,31.00,31.50,31.45,582429
"IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550
>>> ## Resume iteration - the generator is still active
>>> for line in f:
...     print(line, end='')
...     if 'IBM' in line:
...         break
...
"CAT",78.36,"6/11/2007","09:37.19",-0.16,78.32,78.36,77.99,237714
"VZ",42.99,"6/11/2007","09:37.20",-0.08,42.95,42.99,42.78,268459
"IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859
>>> del f  ## Clean up
Following Done

В этом эксперименте мы создаем объект - генератор f и итерируемся по нему с помощью цикла for. Внутри цикла мы выводим каждую строку и проверяем, содержит ли строка строку 'IBM'. Если содержит, мы используем оператор break для выхода из цикла. Выход из цикла не закрывает генератор, поэтому генератор все еще активен. Затем мы можем продолжить итерацию, начав новый цикл for по тому же объекту - генератору. Наконец, мы удаляем объект - генератор для очистки, что вызывает обработчик исключения GeneratorExit.

Основные выводы

  1. Когда генератор закрывается (либо в результате сборки мусора, либо при вызове close()), внутри генератора возникает исключение GeneratorExit.
  2. Вы можете перехватить это исключение, чтобы выполнить действия по очистке при закрытии генератора.
  3. Выход из итерации по генератору (с помощью break) не закрывает генератор, что позволяет продолжить его работу позже.

Выйдите из интерпретатора Python, введя exit() или нажав Ctrl + D.

✨ Проверить решение и практиковаться

Обработка исключений в генераторах

В этом шаге мы научимся обрабатывать исключения в генераторах и корутинах. Но сначала разберемся, что такое исключения. Исключение - это событие, которое происходит во время выполнения программы и нарушает нормальный поток выполнения инструкций программы. В Python мы можем использовать метод throw() для обработки исключений в генераторах и корутинах.

Понимание корутин

Корутина - это особый тип генератора. В отличие от обычных генераторов, которые в основном выдают значения, корутины могут как потреблять значения (с помощью метода send()), так и выдавать их. Файл cofollow.py содержит простую реализацию корутины.

Откройте файл cofollow.py в редакторе WebIDE. Вот код, который находится внутри:

def consumer(func):
    def start(*args,**kwargs):
        c = func(*args,**kwargs)
        next(c)
        return c
    return start

@consumer
def printer():
    while True:
        item = yield
        print(item)

Теперь разберем этот код. consumer - это декоратор. Декоратор - это функция, которая принимает другую функцию в качестве аргумента, добавляет к ней некоторую функциональность и затем возвращает модифицированную функцию. В данном случае декоратор consumer автоматически перемещает генератор к его первому оператору yield. Это важно, так как делает генератор готовым к приему значений.

Корутина printer() определена с использованием декоратора @consumer. Внутри функции printer() есть бесконечный цикл while. Оператор item = yield - это то, где происходит магия. Он приостанавливает выполнение корутины и ждет получения значения. Когда значение отправляется в корутину, она возобновляет выполнение и выводит полученное значение.

Добавление обработки исключений в корутину

Теперь мы модифицируем корутину printer() для обработки исключений. Обновим функцию printer() в файле cofollow.py следующим образом:

@consumer
def printer():
    while True:
        try:
            item = yield
            print(item)
        except Exception as e:
            print('ERROR: %r' % e)

Блок try содержит код, который может вызвать исключение. В нашем случае это код, который получает и выводит значение. Если в блоке try возникает исключение, выполнение переходит в блок except. Блок except перехватывает исключение и выводит сообщение об ошибке. После внесения этих изменений сохраните файл.

Эксперименты с обработкой исключений в корутинах

Начнем экспериментировать с выбрасыванием исключений в корутину. Откройте терминал и запустите интерпретатор Python с помощью следующих команд:

cd ~/project
python3

Эксперимент 1: Базовое использование корутины

>>> from cofollow import printer
>>> p = printer()
>>> p.send('hello')  ## Send a value to the coroutine
hello
>>> p.send(42)  ## Send another value
42

Здесь мы сначала импортируем корутину printer из модуля cofollow. Затем создаем экземпляр корутины printer с именем p. Мы используем метод send() для отправки значений в корутину. Как вы видите, корутина обрабатывает отправленные нам значения без проблем.

Эксперимент 2: Выбрасывание исключения в корутину

>>> p.throw(ValueError('It failed'))  ## Throw an exception into the coroutine
ERROR: ValueError('It failed')

В этом эксперименте мы используем метод throw() для внедрения исключения ValueError в корутину. Блок try - except в корутине printer() перехватывает исключение и выводит сообщение об ошибке. Это показывает, что наша обработка исключений работает как ожидалось.

Эксперимент 3: Выбрасывание реального исключения в корутину

>>> try:
...     int('n/a')  ## This will raise a ValueError
... except ValueError as e:
...     p.throw(e)  ## Throw the caught exception into the coroutine
...
ERROR: ValueError("invalid literal for int() with base 10: 'n/a'")

Здесь мы сначала пытаемся преобразовать строку 'n/a' в целое число, что вызывает исключение ValueError. Мы перехватываем это исключение и затем используем метод throw() для передачи его в корутину. Корутина перехватывает исключение и выводит сообщение об ошибке.

Эксперимент 4: Проверка, что корутина продолжает работу

>>> p.send('still working')  ## The coroutine continues to run after handling exceptions
still working

После обработки исключений мы отправляем еще одно значение в корутину с помощью метода send(). Корутина все еще активна и может обработать новое значение. Это показывает, что наша корутина может продолжать работу даже после возникновения ошибок.

Основные выводы

  1. Генераторы и корутины могут обрабатывать исключения в точке оператора yield. Это означает, что мы можем перехватывать и обрабатывать ошибки, которые возникают, когда корутина ожидает или обрабатывает значение.
  2. Метод throw() позволяет внедрять исключения в генератор или корутину. Это полезно для тестирования и обработки ошибок, которые возникают вне корутины.
  3. Правильная обработка исключений в генераторах позволяет создавать надежные, устойчивые к ошибкам генераторы, которые могут продолжать работу даже при возникновении ошибок. Это делает ваш код более надежным и легким в поддержке.

Для выхода из интерпретатора Python вы можете ввести exit() или нажать Ctrl + D.

✨ Проверить решение и практиковаться

Практические применения управления генераторами

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

Создание надежной системы мониторинга файлов

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

Сначала откройте редактор WebIDE и создайте новый файл с именем robust_follow.py. Вот код, который нужно написать в этом файле:

import os
import time
import signal

class TimeoutError(Exception):
    pass

def timeout_handler(signum, frame):
    raise TimeoutError("Operation timed out")

def follow(filename, timeout=None):
    """
    A generator that yields new lines in a file.
    With timeout handling and proper cleanup.
    """
    try:
        ## Set up timeout if specified
        if timeout:
            signal.signal(signal.SIGALRM, timeout_handler)
            signal.alarm(timeout)

        with open(filename, 'r') as f:
            f.seek(0, os.SEEK_END)
            while True:
                line = f.readline()
                if line == '':
                    ## No new data, wait briefly
                    time.sleep(0.1)
                    continue
                yield line
    except TimeoutError:
        print(f"Following timed out after {timeout} seconds")
    except GeneratorExit:
        print("Following stopped by request")
    finally:
        ## Clean up timeout alarm if it was set
        if timeout:
            signal.alarm(0)
        print("Follow generator cleanup complete")

В этом коде мы сначала определяем пользовательский класс TimeoutError. Функция timeout_handler используется для вызова этого ошибки при истечении времени ожидания. Функция follow является генератором, который читает файл и возвращает новые строки. Если задано время ожидания, она устанавливает сигнал с использованием модуля signal. Если в файле нет новых данных, она ждет короткое время перед повторной попыткой. Блок try - except - finally используется для обработки различных исключений и обеспечения правильной очистки.

После написания кода сохраните файл.

Эксперименты с надежной системой мониторинга файлов

Теперь протестируем нашу улучшенную систему мониторинга файлов. Откройте терминал и запустите интерпретатор Python с помощью следующих команд:

cd ~/project
python3

Эксперимент 1: Базовое использование

В интерпретаторе Python протестируем базовую функциональность нашего генератора follow. Вот код для запуска:

>>> from robust_follow import follow
>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
...     print(f"Line {i+1}: {line.strip()}")
...     if i >= 2:  ## Just read a few lines for the example
...         break
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Line 3: "HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169

Здесь мы импортируем функцию follow из файла robust_follow.py. Затем создаем объект - генератор f, который отслеживает файл stocklog.csv. Мы используем цикл for для итерации по строкам, возвращаемым генератором, и выводим первые три строки.

Эксперимент 2: Использование таймаута

Посмотрим, как работает функция таймаута. Запустите следующий код в интерпретаторе Python:

>>> ## Create a generator that will time out after 3 seconds
>>> f = follow('stocklog.csv', timeout=3)
>>> for line in f:
...     print(line.strip())
...     time.sleep(1)  ## Process each line slowly
...
"MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
"VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
"HPQ",45.76,"6/11/2007","09:34.29",0.06,45.80,45.76,45.59,257169
Following timed out after 3 seconds
Follow generator cleanup complete

В этом эксперименте мы создаем генератор с таймаутом в 3 секунды. Мы обрабатываем каждую строку медленно, засыпая на 1 секунду между каждой строкой. После примерно 3 секунд генератор вызывает исключение по таймауту, и выполняется код очистки в блоке finally.

Эксперимент 3: Явное закрытие

Протестируем, как генератор обрабатывает явное закрытие. Запустите следующий код:

>>> f = follow('stocklog.csv')
>>> for i, line in enumerate(f):
...     print(f"Line {i+1}: {line.strip()}")
...     if i >= 1:
...         print("Explicitly closing the generator...")
...         f.close()
...
Line 1: "MO",70.29,"6/11/2007","09:30.09",-0.01,70.25,70.30,70.29,365314
Line 2: "VZ",42.91,"6/11/2007","09:34.28",-0.16,42.95,42.91,42.78,210151
Explicitly closing the generator...
Following stopped by request
Follow generator cleanup complete

Здесь мы создаем генератор и начинаем итерироваться по его строкам. После обработки двух строк мы явно закрываем генератор с помощью метода close. Затем генератор обрабатывает исключение GeneratorExit и выполняет необходимую очистку.

Создание конвейера обработки данных с обработкой ошибок

Далее мы создадим простой конвейер обработки данных с использованием корутин. Этот конвейер должен уметь обрабатывать ошибки на разных этапах.

Откройте редактор WebIDE и создайте новый файл с именем pipeline.py. Вот код, который нужно написать в этом файле:

def consumer(func):
    def start(*args,**kwargs):
        c = func(*args,**kwargs)
        next(c)
        return c
    return start

@consumer
def grep(pattern, target):
    """Filter lines containing pattern and send to target"""
    try:
        while True:
            line = yield
            if pattern in line:
                target.send(line)
    except Exception as e:
        target.throw(e)

@consumer
def printer():
    """Print received items"""
    try:
        while True:
            item = yield
            print(f"PRINTER: {item}")
    except Exception as e:
        print(f"PRINTER ERROR: {repr(e)}")

def follow_and_process(filename, pattern):
    """Follow a file and process its contents"""
    import time
    import os

    output = printer()
    filter_pipe = grep(pattern, output)

    try:
        with open(filename, 'r') as f:
            f.seek(0, os.SEEK_END)
            while True:
                line = f.readline()
                if not line:
                    time.sleep(0.1)
                    continue
                filter_pipe.send(line)
    except KeyboardInterrupt:
        print("Processing stopped by user")
    finally:
        filter_pipe.close()
        output.close()

В этом коде декоратор consumer используется для инициализации корутин. Корутина grep фильтрует строки, содержащие определенный шаблон, и отправляет их в другую корутину. Корутина printer выводит полученные элементы. Функция follow_and_process читает файл, фильтрует его строки с использованием корутины grep и выводит совпадающие строки с использованием корутины printer. Она также обрабатывает исключение KeyboardInterrupt и обеспечивает правильную очистку.

После написания кода сохраните файл.

Тестирование конвейера обработки данных

Протестируем наш конвейер обработки данных. В терминале запустите следующую команду:

cd ~/project
python3 -c "from pipeline import follow_and_process; follow_and_process('stocklog.csv', 'IBM')"

Вы должны увидеть вывод, похожий на следующий:

PRINTER: "IBM",102.86,"6/11/2007","09:34.44",-0.21,102.87,102.86,102.77,147550

PRINTER: "IBM",102.91,"6/11/2007","09:37.31",-0.16,102.87,102.91,102.77,190859

PRINTER: "IBM",102.95,"6/11/2007","09:39.44",-0.12,102.87,102.95,102.77,225350

Этот вывод показывает, что конвейер работает правильно, фильтруя и выводя строки, содержащие шаблон "IBM".

Для остановки процесса нажмите Ctrl + C. Вы должны увидеть следующее сообщение:

Processing stopped by user

Основные выводы

  1. Правильная обработка исключений в генераторах позволяет создавать надежные системы, которые могутGracefully обрабатывать ошибки. Это означает, что ваши программы не будут аварийно завершаться, если что - то пойдет не так.
  2. Вы можете использовать такие техники, как таймауты, чтобы предотвратить бесконечное выполнение генераторов. Это помогает управлять системными ресурсами и гарантирует, что ваша программа не застрянет в бесконечном цикле.
  3. Генераторы и корутины могут образовывать мощные конвейеры обработки данных, где ошибки могут распространяться и обрабатываться на соответствующем уровне. Это упрощает построение сложных систем обработки данных.
  4. Блок finally в генераторах гарантирует выполнение операций очистки, независимо от того, как генератор завершается. Это помогает сохранить целостность вашей программы и предотвратить утечку ресурсов.
✨ Проверить решение и практиковаться

Резюме

В этом практическом занятии (лабораторной работе) вы изучили важные техники управления операторами yield в генераторах и корутинах Python. Вы рассмотрели управление жизненным циклом генераторов, включая обработку исключения GeneratorExit при закрытии или сборке мусора, а также управление прерыванием и возобновлением итерации. Кроме того, вы узнали о обработке исключений в генераторах, таких как использование метода throw() и написание надежных генераторов для элегантной обработки исключений.

Эти техники являются фундаментальными для создания надежных и поддерживаемых приложений на Python. Они полезны для обработки данных, асинхронных операций и управления ресурсами. Правильное управление жизненным циклом генераторов и обработка исключений позволяют создавать устойчивые системы, которые элегантно обрабатывают ошибки и освобождают ресурсы, когда они больше не нужны.