Рубрики
Без рубрики

Dead Simple Python: генераторы и коратики

Разве не было бы хорошо, если бы ваш код ждал вас? Генераторы и коратики на вашем сервисе. Теги с питоном, новичками, ООП, функциональный.

Dead Simple Python (13 серии деталей)

Программирование часто о ожидании. В ожидании функции, ожидания ввода, ожидания расчета, ожидая прохода тестов …

… Жду Джейсона написать еще один Dead Simple Python уже.

Разве не было бы неплохо, если бы ваша программа ждала ты однажды? Это именно то, что делают генераторы и коратики! Мы создали это в течение последних трех статей, но я рад сообщить, что ожидание закончилось.

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

Для всех остальных давайте погрузимся прямо.

Как бы вы генерировали последовательность Fibonacci любой длины? Очевидно, что есть некоторые данные, которые вам необходимы, чтобы отслеживать, и им нужно манипулировать определенным образом для создания следующего элемента.

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

class Fibonacci:

    def __init__(self, limit):
        self.n1 = 0
        self.n2 = 1
        self.n = 1
        self.i = 1
        self.limit = limit

    def __iter__(self):
        return self

    def __next__(self):
        if self.i > self.limit:
            raise StopIteration

        if self.i > 1:
            self.n = self.n1 + self.n2
            self.n1, self.n2 = self.n2, self.n

        self.i += 1
        return self.n


fib = Fibonacci(10)
for i in fib:
    print(i)

Хранили более компактно, и – если вы следовали серии, вероятно, там, вероятно, нет никаких сюрпризов. Тем не менее, этот подход может показаться немного подавленным для чего -то столь же простого, как последовательность. Конечно, много шаблонов.

Такая ситуация – это именно то, для чего предназначен генератор.

def fibonacci(limit):
    if limit >= 1:
        yield (n2 := 1)

    n1 = 0

    for _ in range(1, limit):
        yield (n := n1 + n2)
        n1, n2 = n2, n


for i in fibonacci(10):
    print(i)

Генератор, безусловно, более компактный – всего 9 строк, против 22 для класса – но он столь же читабелен.

Секретный соус – это доход Ключевое слово, которое возвращает значение, не выходя из функции. доход функционально идентичен __next __ () функция в нашем классе. Генератор будет подходить (и включая) его доход заявление, а затем будет ждать другого __next __ () Позвоните, прежде чем он сделает что -нибудь еще. Как только это делает Получите этот звонок, он будет продолжать работать, пока не попадет в другой доход Анкет

Примечание: Это странно выглядящий : = является новым «оператором моржа» в Python 3.8, который назначает и возвращает значение. Если вы находитесь на Python 3.7 или раньше, вы можете разбить эти операторы на две строки (отдельное назначение и

Вы также заметите отсутствие Поднимите остановку утверждение. Генераторы не требуют их; На самом деле, так как PEP 479 , они даже не позволяют им. Когда функция генератора заканчивается, либо естественно, либо с возврат утверждение, Остановка поднимается автоматически за кулисами.

Генераторы и попробуйте

Пересмотренный: 29 ноября 2019 г.

Раньше это было доход Не мог появиться с попробуйте пункт о Попробуйте-finally утверждение. PEP 255 , который определил синтаксис генератора, объясняет, почему:

Трудность заключается в том, что нет никакой гарантии, что генератор когда -либо возобновится, поэтому не гарантирует, что наконец -то блок когда -либо будет выполнен; Это слишком много нарушения цели, наконец, вынести.

Это было изменено в PEP 342 PEP 342 , который был завершен в Python 2.5.

Так зачем вообще обсуждать такое старое изменение? Просто: до сегодняшнего дня у меня сложилось впечатление, что доход не мог появиться в Попробуйте финально . Некоторые статьи по теме неправильно приводят старое правило.

Генератор как объект

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

Например, что, если я захотел распечатать только 10-20 значений последовательности Фибоначчи?

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

fib = fibonacci(100)

Далее я использую петлю, чтобы пропустить первые 10 элементов.

for _ in range(10):
    next(fib)

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

Кстати, я мог бы также позвонить Fib .__ Next __ () – вот что Далее (Fib) вызовы В любом случае – Но я предпочитаю чистый вид подхода, который я принял. Обычно это сводится к предпочтению; Оба одинаково действительны.

Теперь я готов получить доступ к некоторым значениям из генератора, но не все из них. Таким образом, я все еще буду использовать range () и извлечь значения из генератора напрямую с Next () Анкет

for n in range(10, 21):
    print(f"{n}th value: {next(fib)}")

Это довольно хорошо распечатывает желаемые значения:

10th value: 89
11th value: 144
12th value: 233
13th value: 377
14th value: 610
15th value: 987
16th value: 1597
17th value: 2584
18th value: 4181
19th value: 6765
20th value: 10946

Вы помните, что мы установили наш предел на 100 ранее. Сейчас мы закончили с нашим генератором, но мы действительно не должны просто уходить и оставлять его в ожидании другого Next () вызов! Оставить его сидя в памяти для остальной части нашей программы было бы расточительным от ресурсов (как бы мало).

Вместо этого мы можем вручную сказать нашему генератору, что мы сделали с ним.

fib.close()

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

Генераторы позволяют нам быстро определить итерабируемое, которое хранит свое состояние между вызовами. Однако что, если мы хотим противоположное: передать информацию в И иметь функцию терпеливо подождать, пока она ее не получит? Python предоставляет Coroutines Для этого.

Для тех, кто уже немного знаком с Coroutines, вы должны понимать, что то, о чем я имею в виду, специально известно как Простые коратики (Хотя я просто говорю «Куртика» повсюду для здравомыслия читателя.) Если вы видели какой -либо код Python с использованием параллелизма, вы, возможно, уже столкнулись с его младшим двоюродным братом, Нативная Coroutine (также называется «асинкновой коратиной»).

На данный момент понимайте, что оба Простые Coroutines и Нативные Coroutines официально считаются «Coroutines», и они разделяют много принципов; Нативные коратики основаны на понятиях, представленных с простыми корешками. Мы вернемся к этому, когда обсудим Асинхронизация в более поздней статье.

Опять же, пока просто предположим, что когда я говорю «Coroutine», я имею в виду простую корутину.

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

Очевидно, этот подход должен:

  • Быть многоразовым.
  • Иметь состояние (общие буквы до сих пор).
  • Будьте итеративным по своей природе, поскольку мы не знаем, сколько струн мы получим.

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

Мы могли бы попробовать класс, хотя это много шаблонов. Давайте все равно начнем там, просто чтобы лучше понять, с чем мы имеем дело.

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

class CommonLetterCounter:

    def __init__(self, results):
        self.letters = {}
        self.counted = []
        self.results = results
        self.i = 0

    def add_word(self, word):
        word = word.lower()
        for c in word:
            if c.isalpha():
                if c not in self.letters:
                    self.letters[c] = 0
                self.letters[c] += 1

        self.counted = sorted(self.letters.items(), key=lambda kv: kv[1])
        self.counted = self.counted[::-1]

        self.results.clear()
        for item in self.counted:
            self.results.append(item)


names = ['Skimpole', 'Sloppy', 'Wopsle', 'Toodle', 'Squeers',
         'Honeythunder', 'Tulkinghorn', 'Bumble', 'Wegg',
         'Swiveller', 'Sweedlepipe', 'Jellyby', 'Smike', 'Heep',
         'Sowerberry', 'Pumblechook', 'Podsnap', 'Tox', 'Wackles',
         'Scrooge', 'Snodgrass', 'Winkle', 'Pickwick']

results = []
counter = CommonLetterCounter(results)

for name in names:
    counter.add_word(name)

for letter, count in results:
    print(f'{letter} apppears {count} times.')

Согласно моей выходе, Чарльзу Диккенсу особенно нравились имена с E, O, S, L и P. Кто знал?

Мы можем достичь того же результата с Coroutine Анкет

def count_common_letters(results):
    letters = {}

    while True:
        word = yield
        word = word.lower()
        for c in word:
            if c.isalpha():
                if c not in letters:
                    letters[c] = 0
                letters[c] += 1

        counted = sorted(letters.items(), key=lambda kv: kv[1])
        counted = counted[::-1]

        results.clear()
        for item in counted:
            results.append(item)


names = ['Skimpole', 'Sloppy', 'Wopsle', 'Toodle', 'Squeers',
         'Honeythunder', 'Tulkinghorn', 'Bumble', 'Wegg',
         'Swiveller', 'Sweedlepipe', 'Jellyby', 'Smike', 'Heep',
         'Sowerberry', 'Pumblechook', 'Podsnap', 'Tox', 'Wackles',
         'Scrooge', 'Snodgrass', 'Winkle', 'Pickwick']

results = []
counter = count_common_letters(results)
counter.send(None)  # prime the coroutine

for name in names:
    counter.send(name)  # send data to the coroutine

counter.close()  # manually end the coroutine

for letter, count in results:
    print(f'{letter} apppears {count} times.')

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

В коратине, однако, доход Выступает за «подождите, пока вы не получите ввод, а затем используйте его прямо здесь».

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

Основное различие между классом и коратиной – это использование. Мы отправляем данные в Coroutine, используя его Send () Функция:

for name in names:
    counter.send(name)

Прежде чем мы сможем сделать это, однако, мы должны сначала Prime Куртика с вызовом любого counter.send (нет) (используется выше) или счетчик .__ Далее __ () Анкет Корука не может сразу получить значение; Сначала он должен пройти через все свои кодовые ведущие вверх к своему первому доход Анкет

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

counter.close()

Короче говоря, чтобы использовать коратину:

  • Сохраните экземпляр как переменную, например, счетчик В
  • Запланировать это counter.send (нет) , счетчик .__ Далее __ () , или Далее (счетчик) ,
  • Отправить данные в него с counter.send () В
  • При необходимости, закройте его counter.close () Анкет

Краутины и попробуйте

Помните это правило о генераторах и не ставив доход в попробуйте пункт о Попробуйте-finally утверждение? Это не применяется здесь! Потому что доход ведет себя совсем по -разному в коратике (обработка входящих данных, а не исходящих данных), вполне приемлемо использовать их таким образом.

Генераторы и коратики также имеют Throw () Функция, которая используется для повышения исключения в том месте, которое они приостановили. Вы помните из «Ошибки» статья что исключения могут быть использованы в качестве нормальной части потока выполнения.

Представьте, что вы хотите отправить данные на удаленный сервер. У вас есть удобные маленькие объекты подключения, и вы используете Coroutine для отправки данных по этому соединению.

Где -то еще в вашем коде вы обнаруживаете, что потеряли сетевое соединение, но из -за того, как вы общаетесь со своим сервером, все эти данные, которые Coroutine так старательно отправляет, просто попадает в черную дыру без жалоб. Упс.

Рассмотрим этот пример кода, который я заглушал. (Предположим, что фактическая логика соединения не поддается ни самой обработке ошибок подключения или отчетности.)

class Connection:
    """ Stub object simulating connection to a server """

    def __init__(self, addr):
        self.addr = addr

    def transmit(self, data):
        print(f"X: {data[0]}, Y: {data[1]} sent to {self.addr}")


def send_to_server(conn):
    """ Coroutine demonstrating sending data """
    while True:
        raw_data = yield
        raw_data = raw_data.split(' ')
        coords = (float(raw_data[0]), float(raw_data[1]))
        conn.transmit(coords)


conn = Connection("example.com")

sender = send_to_server(conn)
sender.send(None)

for i in range(1, 6):
    sender.send(f"{100/i} {200/i}")

# Simulate connection error...
conn.addr = None
# ...but assume the sender knows nothing about it.

for i in range(1, 6):
    sender.send(f"{100/i} {200/i}")

Запустив этот пример, мы видим, что первые пять Send () звонки перейти к Пример.com , но последние пять попадают в Нет Анкет Это, очевидно, не сделает – мы хотим сообщить о проблеме и начать отправлять данные в файл Вместо этого это не потеряно навсегда.

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

Сначала добавляем Три-за исключением Наш кораку:

def send_to_server(conn):
    while True:
        try:
            raw_data = yield
            raw_data = raw_data.split(' ')
            coords = (float(raw_data[0]), float(raw_data[1]))
            conn.transmit(coords)
        except ConnectionError:
            print("Oops! Connection lost. Creating fallback.")
            # Create a fallback connection!
            conn = Connection("local file")

Наш пример использования нуждается только в одном изменении: как только мы узнаем, что потеряли соединение, мы используем Sender.Throw (ConnectionError) :

conn = Connection("example.com")

sender = send_to_server(conn)
sender.send(None)

for i in range(1, 6):
    sender.send(f"{100/i} {200/i}")

# Simulate connection error...
conn.addr = None
# ...but assume the sender knows nothing about it.

sender.throw(ConnectionError) # ALERT THE SENDER!

for i in range(1, 6):
    sender.send(f"{100/i} {200/i}")

Это все! Теперь мы получаем сообщение о проблеме с подключением, как только коратика будет предупреждена, а остальные сообщения направляются в наш локальный файл.

При использовании генератора или Coroutine вы не ограничены только локальным доход Анкет На самом деле вы можете получить другие итерации, генераторы или коратики, использующие с использованием урожай из Анкет

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

def fibonacci():
    starter = [1, 1, 2, 3, 5]
    yield from starter

    n1 = starter[-2]
    n2 = starter[-1]

    while True:
        yield (n := n1 + n2)
        n1, n2 = n2, n

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

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

fib = fibonacci()

for n in range(1,11):
    print(f"{n}th value: {next(fib)}")

fib.close()

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

class Connection:
    """ Stub object simulating connection to a server """

    def __init__(self, addr):
        self.addr = addr

    def transmit(self, data):
        print(f"X: {data[0]}, Y: {data[1]} sent to {self.addr}")


def save_to_file():
    while True:
        raw_data = yield
        raw_data = raw_data.split(' ')
        coords = (float(raw_data[0]), float(raw_data[1]))
        print(f"X: {coords[0]}, Y: {coords[1]} sent to local file")


def send_to_server(conn):
    while True:
        if conn is None:
            yield from save_to_file()
        else:
            try:
                raw_data = yield
                raw_data = raw_data.split(' ')
                coords = (float(raw_data[0]), float(raw_data[1]))
                conn.transmit(coords)
            except ConnectionError:
                print("Oops! Connection lost. Using fallback.")
                conn = None


conn = Connection("example.com")

sender = send_to_server(conn)
sender.send(None)

for i in range(1, 6):
    sender.send(f"{100/i} {200/i}")

# Simulate connection error...
conn.addr = None
# ...but assume the sender knows nothing about it.

sender.throw(ConnectionError) # ALERT THE SENDER!

for i in range(1, 6):
    sender.send(f"{100/i} {200/i}")

Такое поведение было определено в PEP 380 , так что прочитайте это для получения дополнительной информации.

Вы можете задаться вопросом: «Могу ли я объединить два возврата данных прямо из коратики, как я могу из генератора?»

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

Ключ к этому прост: __next __ () и отправить (нет) фактически то же самое для коратики.

def count_common_letters():
    letters = {}

    word = yield
    while word is not None:
        word = word.lower()
        for c in word:
            if c.isalpha():
                if c not in letters:
                    letters[c] = 0
                letters[c] += 1
        word = yield

    counted = sorted(letters.items(), key=lambda kv: kv[1])
    counted = counted[::-1]

    for item in counted:
        yield item


names = ['Skimpole', 'Sloppy', 'Wopsle', 'Toodle', 'Squeers',
         'Honeythunder', 'Tulkinghorn', 'Bumble', 'Wegg',
         'Swiveller', 'Sweedlepipe', 'Jellyby', 'Smike', 'Heep',
         'Sowerberry', 'Pumblechook', 'Podsnap', 'Tox', 'Wackles',
         'Scrooge', 'Snodgrass', 'Winkle', 'Pickwick']

counter = count_common_letters()
counter.send(None)

for name in names:
    counter.send(name)

for letter, count in counter:
    print(f'{letter} apppears {count} times.')

Мне нужно было наблюдать только о том, когда коратика начала получать Нет (После первоначального заполнения, конечно). Поскольку я хранил результат доход в слово , Я мог бы вырваться из петли для получение информация один раз Слово был Нет Анкет

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

Кроме того, помните, что Coroutine/Generator все еще является функцией. Он просто останавливается каждый раз, когда сталкивается с доход Анкет В моем примере я не мог внезапно вернуться к использованию счетчик Как корова, потому что нет никакого поток исполнения, который бы вернул меня к слово . Это идеально возможно Чтобы написать это, чтобы вы могли переключаться вперед и назад, хотя, возможно, не рекомендуется, если это происходит за счет читабельности или становится слишком сложным.

Генераторы и коратики позволяют вам быстро писать функции, которые «ждут» вас. Позже мы встретимся с Нативная Coroutine , тип коратики, используемой в параллелистике.

Давайте рассмотрим основы из этого раздела:

  • Генераторы Являются ли итераблевы, которые ждут, пока вы запрашиваете вывод.
  • Генераторы написаны как обычные функции, за исключением того, что они используют доход Ключевое слово для возврата значений так же, как и класс с его __next __ () функция
  • Когда генератор достигает естественного конца своего порядка выполнения, или поражает возврат утверждение, это поднимает Остановка и заканчивается.
  • Coroutines похожи на генераторы, за исключением того, что они ждут, пока информация будет отправлено к этому через foo.send () функция
  • Как генератор, так и коратика могут быть продвинуты к следующему оператору урожая с Далее (Foo) или foo .__ Next __ () Анкет
  • Прежде чем коратика сможет что -то отправить на него с foo.send () , это должно быть «заправлено» с foo.send (нет) , Далее (Foo) , или foo .__ Next __ () Анкет
  • Исключение может быть поднято на текущем доход с foo.throw () Анкет
  • Генератор или коратика могут быть остановлены вручную с `foo.close ().
  • Одна функция может вести себя сначала как коратика, а затем как генератор.

Как всегда, вы можете узнать гораздо больше из документации:

Благодаря Дениска (Freenode IRC #python ), @rhymes , а также @florimondmanca (Dev.to) Для предлагаемых изменений.

Dead Simple Python (13 серии деталей)

Оригинал: “https://dev.to/codemouse92/dead-simple-python-generators-and-coroutines-21ll”