Настройка итерации с использованием генераторов

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

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

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

Введение

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

В ходе эксперимента будет изменен файл structure.py, а также будет создан новый файл с именем follow.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python(("Python")) -.-> python/FileHandlingGroup(["File Handling"]) python(("Python")) -.-> python/AdvancedTopicsGroup(["Advanced Topics"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/FileHandlingGroup -.-> python/file_reading_writing("Reading and Writing Files") python/FileHandlingGroup -.-> python/file_operations("File Operations") python/AdvancedTopicsGroup -.-> python/iterators("Iterators") python/AdvancedTopicsGroup -.-> python/generators("Generators") subgraph Lab Skills python/conditional_statements -.-> lab-132522{{"Настройка итерации с использованием генераторов"}} python/classes_objects -.-> lab-132522{{"Настройка итерации с использованием генераторов"}} python/file_reading_writing -.-> lab-132522{{"Настройка итерации с использованием генераторов"}} python/file_operations -.-> lab-132522{{"Настройка итерации с использованием генераторов"}} python/iterators -.-> lab-132522{{"Настройка итерации с использованием генераторов"}} python/generators -.-> lab-132522{{"Настройка итерации с использованием генераторов"}} end

Понимание генераторов Python

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

Что такое генератор?

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

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

  1. Сначала вам нужно открыть новый терминал Python в WebIDE. Для этого нажмите на меню "Terminal" и выберите "New Terminal".
  2. После открытия терминала введите следующий код в терминале. Этот код определяет функцию - генератор и затем тестирует ее.
def frange(start, stop, step):
    current = start
    while current < stop:
        yield current
        current += step

## Test the generator with a for loop
for x in frange(0, 2, 0.25):
    print(x, end=' ')

В этом коде функция frange является функцией - генератором. Она инициализирует переменную current значением start. Затем, пока current меньше значения stop, она выдает значение current и затем увеличивает current на значение step. Затем цикл for перебирает значения, порождаемые функцией - генератором frange, и выводит их.

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

0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Одноразовый характер генераторов

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

## Create a generator object
f = frange(0, 2, 0.25)

## First iteration works fine
print("First iteration:")
for x in f:
    print(x, end=' ')
print("\n")

## Second iteration produces nothing
print("Second iteration:")
for x in f:
    print(x, end=' ')
print("\n")

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

Вывод:

First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Second iteration:

Обратите внимание, что вторая итерация не дала никакого вывода, потому что генератор уже был исчерпан.

Создание переиспользуемых генераторов с помощью классов

Если вам нужно несколько раз перебирать одну и ту же последовательность значений, вы можете обернуть генератор в класс. Таким образом, каждый раз, когда вы начинаете новую итерацию, будет создан новый генератор.

class FRange:
    def __init__(self, start, stop, step):
        self.start = start
        self.stop = stop
        self.step = step

    def __iter__(self):
        n = self.start
        while n < self.stop:
            yield n
            n += self.step

## Create an instance
f = FRange(0, 2, 0.25)

## We can iterate multiple times
print("First iteration:")
for x in f:
    print(x, end=' ')
print("\n")

print("Second iteration:")
for x in f:
    print(x, end=' ')
print("\n")

В этом коде мы определяем класс FRange. Метод __init__ инициализирует значения start, stop и step. Метод __iter__ - это специальный метод в классах Python. Он используется для создания итератора. Внутри метода __iter__ у нас есть генератор, который порождает значения аналогично функции frange, которую мы определили ранее.

Когда мы создаем экземпляр f класса FRange и перебираем его несколько раз, каждая итерация вызывает метод __iter__, который создает новый генератор. Таким образом, мы можем получить одну и ту же последовательность значений несколько раз.

Вывод:

First iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

Second iteration:
0 0.25 0.5 0.75 1.0 1.25 1.5 1.75

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

Добавление итерации в пользовательские классы

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

Понимание метода __iter__()

Метод __iter__() является важной частью создания итерируемого класса. Он должен возвращать объект - итератор. Итератор - это объект, который можно перебирать (проходить в цикле). Простой и эффективный способ добиться этого - определить __iter__() как функцию - генератор. Функция - генератор использует ключевое слово yield для пошагового создания последовательности значений. Каждый раз, когда встречается оператор yield, функция приостанавливается и возвращает значение. В следующий раз, когда вызывается итератор, функция продолжает выполнение с того места, где остановилась.

Изменение класса Structure

В рамках этого лабораторного занятия мы предоставили базовый класс Structure. Другие классы, такие как Stock, могут наследоваться от этого класса Structure. Наследование - это способ создания нового класса, который наследует свойства и методы существующего класса. Добавив метод __iter__() в класс Structure, мы можем сделать все его подклассы итерируемыми. Это означает, что любой класс, который наследуется от Structure, автоматически получит возможность быть перебираемым в цикле.

  1. Откройте файл structure.py в WebIDE:
cd ~/project

Эта команда изменяет текущую рабочую директорию на директорию project, где находится файл structure.py. Вам нужно находиться в правильной директории, чтобы получить доступ к файлу и изменить его.

  1. Посмотрите на текущую реализацию класса Structure:
class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

Класс Structure имеет список _fields, который хранит имена атрибутов. Метод __init__() является конструктором класса. Он инициализирует атрибуты объекта, проверяя, равно ли количество переданных аргументов количеству полей. Если нет, он вызывает исключение TypeError. В противном случае он устанавливает атрибуты с помощью функции setattr().

  1. Добавьте метод __iter__(), который последовательно возвращает каждое значение атрибута:
def __iter__(self):
    for name in self._fields:
        yield getattr(self, name)

Этот метод __iter__() является функцией - генератором. Он проходит по списку _fields и использует функцию getattr() для получения значения каждого атрибута. Затем ключевое слово yield возвращает значения по одному.

Полностью обновленный файл structure.py должен выглядеть следующим образом:

class StructureMeta(type):
    def __new__(cls, name, bases, clsdict):
        fields = clsdict.get('_fields', [])
        for name in fields:
            clsdict[name] = property(lambda self, name=name: getattr(self, '_'+name))
        return super().__new__(cls, name, bases, clsdict)

class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

    def __iter__(self):
        for name in self._fields:
            yield getattr(self, name)

Теперь обновленный класс Structure имеет метод __iter__(), который делает его и его подклассы итерируемыми.

  1. Сохраните файл.
    После внесения изменений в файл structure.py вам нужно сохранить его, чтобы изменения вступили в силу.

  2. Теперь давайте протестируем возможность итерации, создав экземпляр класса Stock и перебрав его элементы:

python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('Iterating over Stock:'); [print(val) for val in s]"

Эта команда создает экземпляр класса Stock, который наследуется от класса Structure. Затем она перебирает экземпляр с использованием списочного выражения и выводит каждое значение.

Вы должны увидеть такой вывод:

Iterating over Stock:
GOOG
100
490.1

Теперь любой класс, который наследуется от Structure, автоматически станет итерируемым, и итерация будет возвращать значения атрибутов в порядке, определенном списком _fields. Это означает, что вы можете легко перебирать атрибуты любого подкласса Structure без необходимости писать дополнительный код для итерации.

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

Улучшение классов с помощью возможностей итерации

Теперь мы сделали так, чтобы наш класс Structure и его подклассы поддерживали итерацию. Итерация - это мощная концепция в Python, которая позволяет поочередно перебирать элементы коллекции. Когда класс поддерживает итерацию, он становится более гибким и может работать с множеством встроенных функций Python. Давайте рассмотрим, как поддержка итерации позволяет использовать многие мощные возможности Python.

Использование итерации для преобразования в последовательности

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

  1. Давайте попробуем эти операции с экземпляром класса Stock. Класс Stock является подклассом класса Structure. Запустите следующую команду в терминале:
python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); print('As list:', list(s)); print('As tuple:', tuple(s))"

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

As list: ['GOOG', 100, 490.1]
As tuple: ('GOOG', 100, 490.1)

Распаковка

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

python3 -c "from stock import Stock; s = Stock('GOOG', 100, 490.1); name, shares, price = s; print(f'Name: {name}, Shares: {shares}, Price: {price}')"

В этом коде мы создаем экземпляр класса Stock, а затем распаковываем его элементы в три переменные: name, shares и price. Затем мы выводим эти переменные. Вывод показывает значения этих переменных:

Name: GOOG, Shares: 100, Price: 490.1

Добавление возможностей сравнения

Когда класс поддерживает итерацию, становится проще реализовать операции сравнения. Операции сравнения используются для проверки равенства двух объектов. Давайте добавим метод __eq__() в наш класс Structure для сравнения экземпляров.

  1. Откройте снова файл structure.py. Метод __eq__() - это специальный метод в Python, который вызывается, когда вы используете оператор == для сравнения двух объектов. Добавьте следующий код в класс Structure в файле structure.py:
def __eq__(self, other):
    return isinstance(other, type(self)) and tuple(self) == tuple(other)

Этот метод сначала проверяет, является ли объект other экземпляром того же класса, что и self, с помощью функции isinstance(). Затем он преобразует как self, так и other в кортежи и проверяет, равны ли эти кортежи.

Полностью обновленный файл structure.py должен выглядеть следующим образом:

class StructureMeta(type):
    def __new__(cls, name, bases, clsdict):
        fields = clsdict.get('_fields', [])
        for name in fields:
            clsdict[name] = property(lambda self, name=name: getattr(self, '_'+name))
        return super().__new__(cls, name, bases, clsdict)

class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')
        for name, val in zip(self._fields, args):
            setattr(self, '_'+name, val)

    def __iter__(self):
        for name in self._fields:
            yield getattr(self, name)

    def __eq__(self, other):
        return isinstance(other, type(self)) and tuple(self) == tuple(other)
  1. После добавления метода __eq__() сохраните файл structure.py.

  2. Давайте протестируем возможность сравнения. Запустите следующую команду в терминале:

python3 -c "from stock import Stock; a = Stock('GOOG', 100, 490.1); b = Stock('GOOG', 100, 490.1); c = Stock('AAPL', 200, 123.4); print(f'a == b: {a == b}'); print(f'a == c: {a == c}')"

Этот код создает три экземпляра класса Stock: a, b и c. Затем он сравнивает a с b и a с c с помощью оператора ==. Вывод показывает результаты этих сравнений:

a == b: True
a == c: False
  1. Теперь, чтобы убедиться, что все работает правильно, нам нужно запустить модульные тесты. Модульные тесты - это набор кода, который проверяет, работает ли различные части вашей программы так, как ожидается. Запустите следующую команду в терминале:
python3 teststock.py

Если все работает правильно, вы должны увидеть вывод, указывающий на то, что тесты прошли успешно:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Добавив всего два простых метода (__iter__() и __eq__()), мы значительно улучшили наш класс Structure, сделав его более "питоническим" и удобным в использовании.

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

Создание генератора для потоковых данных

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

Настройка источника данных

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

Сначала вам нужно открыть новый терминал в WebIDE. Именно здесь вы будете запускать команды для запуска симуляции.

После открытия терминала вы запустите программу симуляции фондового рынка. Вот команды, которые вам нужно ввести:

cd ~/project
python3 stocksim.py

Первая команда cd ~/project изменяет текущую директорию на директорию project в вашей домашней директории. Вторая команда python3 stocksim.py запускает программу симуляции фондового рынка. Эта программа будет генерировать данные о фондовом рынке и записывать их в файл с именем stocklog.csv в текущей директории. Дайте этой программе работать в фоновом режиме, пока мы работаем над кодом мониторинга.

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

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

  1. Сначала создайте новый файл с именем follow.py в WebIDE. Для этого вам нужно изменить директорию на директорию project с помощью следующей команды в терминале:
cd ~/project
  1. Затем добавьте следующий код в файл follow.py. Этот код открывает файл stocklog.csv, перемещает указатель файла в конец файла, а затем постоянно проверяет наличие новых строк. Если найдена новая строка и она представляет собой отрицательное изменение цены, он выводит название акции, цену и изменение.
## follow.py
import os
import time

f = open('stocklog.csv')
f.seek(0, os.SEEK_END)   ## Move file pointer 0 bytes from end of file

while True:
    line = f.readline()
    if line == '':
        time.sleep(0.1)   ## Sleep briefly and retry
        continue
    fields = line.split(',')
    name = fields[0].strip('"')
    price = float(fields[1])
    change = float(fields[4])
    if change < 0:
        print('%10s %10.2f %10.2f' % (name, price, change))
  1. После добавления кода сохраните файл. Затем запустите программу с помощью следующей команды в терминале:
python3 follow.py

Вы должны увидеть вывод, показывающий акции с отрицательными изменениями цен. Он может выглядеть примерно так:

      AAPL     148.24      -1.76
      GOOG    2498.45      -1.55

Если вы хотите остановить программу, нажмите Ctrl+C в терминале.

Преобразование в функцию - генератор

Хотя предыдущий код работает, мы можем сделать его более переиспользуемым и модульным, преобразовав его в функцию - генератор. Функция - генератор - это особый тип функции, которая может быть приостановлена и возобновлена, и которая выдаёт значения по одному.

  1. Откройте снова файл follow.py и измените его так, чтобы он использовал функцию - генератор. Вот обновлённый код:
## follow.py
import os
import time

def follow(filename):
    """
    Generator function that yields new lines in a file as they are added.
    Similar to the 'tail -f' Unix command.
    """
    f = open(filename)
    f.seek(0, os.SEEK_END)   ## Move to the end of the file

    while True:
        line = f.readline()
        if line == '':
            time.sleep(0.1)   ## Sleep briefly and retry
            continue
        yield line

## Example usage - monitor stocks with negative price changes
if __name__ == '__main__':
    for line in follow('stocklog.csv'):
        fields = line.split(',')
        name = fields[0].strip('"')
        price = float(fields[1])
        change = float(fields[4])
        if change < 0:
            print('%10s %10.2f %10.2f' % (name, price, change))

Функция follow теперь является функцией - генератором. Она открывает файл, перемещается в конец и затем постоянно проверяет наличие новых строк. Когда найдена новая строка, она выдаёт эту строку.

  1. Сохраните файл и запустите его снова с помощью команды:
python3 follow.py

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

Понимание мощности генераторов

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

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

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

Резюме

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

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