Глава 6. Замыкания и генераторы

Добавлено 28 апреля 2020 в 23:25

У меня правописание хорошее. Оно хорошее, но почему-то хромает, и буквы опаздывают... на свои места.

Винни Пух

Содержание главы

Погружение

По причинам, превосходящим всяческое понимание, я всегда восхищался языками. Не языками программирования. Хотя да, ими, а также языками естественными. Возьмем, к примеру, английский. Английский язык – это шизофренический язык, который заимствует слова из немецкого, французского, испанского и латинского языков (не говоря уже об остальных). Откровенно говоря, «заимствует» – неуместное слово; он их скорее «крадет». Или, возможно, «ассимилирует» – как Борги. Да, хороший вариант.

«Мы Борги. Ваши лингвистические и этимологические особенности станут нашими. Сопротивление бесполезно.»

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

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

  • если слово заканчивается буквами S, X или Z, следует добавить ES. Bass становится basses, fax становится faxes, а waltzwaltzes;
  • если слово заканчивается звонкой H, следует добавить ES; если заканчивается глухой H, то нужно просто добавить S. Что такое звонкая H? Это такая, которая вместе с другими буквами объединяется в звук, который вы можете расслышать. Соответственно, coach становится coaches, и rash становится rashes, потому что вы слышите звуки CH и SH, когда произносите эти слова. Но cheetah становится cheetahs, потому что H здесь глухая;
  • если слово заканчивается на Y, которая читается как I, то замените Y на IES; если Y объединена с гласной и звучит как-то по-другому, то просто добавьте S. Так что vacancy становится vacancies, но day становится days;
  • если ни одно из правил не подходит, просто добавьте S и надейтесь на лучшее.

(Я знаю, существует множество исключений. Man становится men, а womanwomen, но human становится humans. Mouse – mice, а louse – lice, но house во множественной числе – houses. Knife становится knives, а wife становится wives, но lowlife становится lowlifes. И не заставляйте меня начинать перечислять слова, которые не изменяются во множественном числе, как, например, sheep, deer или haiku.)

Остальные языки, конечно, совершенно другие.

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

6.2 Я знаю, давайте использовать регулярные выражения!

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

import re

def plural(noun):          
    if re.search('[sxz]$', noun):             ①
        return re.sub('$', 'es', noun)        ②
    elif re.search('[^aeioudgkprt]h$', noun):
        return re.sub('$', 'es', noun)       
    elif re.search('[^aeiou]y$', noun):      
        return re.sub('y$', 'ies', noun)     
    else:
        return noun + 's'
  1. Строка 4. Это регулярное выражение, но оно использует синтаксис, который вы не видели в главе «Регулярные выражения». Квадратные скобки означают «найти совпадения ровно с одним из этих символов». Поэтому [sxz] означает «s, или x, или z», но только один из них. Символ $ должен быть знаком вам. Он ищет совпадения с концом строки. Всё регулярное выражение проверяет, заканчивается ли noun на s, x или z.
  2. Строка 5. Функция re.sub() производит замену подстроки на основе регулярного выражения.

Рассмотрим замену с помощью регулярного выражения подробнее.

>>> import re
>>> re.search('[abc]', 'Mark')    ①
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.sub('[abc]', 'o', 'Mark')  ②
'Mork'
>>> re.sub('[abc]', 'o', 'rock')  ③
'rook'
>>> re.sub('[abc]', 'o', 'caps')  ④
'oops'
  1. Строка 2. Содержит ли строка Mark символы a, b или c? Да, содержит a.
  2. Строка 4. Отлично, теперь ищем a, b или c и заменяем на o. Mark становится Mork.
  3. Строка 6. Та же функция превращает rock в rook.
  4. Строка 8. Вы могли подумать, что этот код преобразует caps в oaps, но он этого не делает. re.sub заменяет все совпадения, а не только первое найденное. Так что, данное регулярное выражение превратит caps в oops, потому что оба символа c и a заменяются на o.

Вернемся снова к функции plural()

def plural(noun):          
    if re.search('[sxz]$', noun):            
        return re.sub('$', 'es', noun)         ①
    elif re.search('[^aeioudgkprt]h$', noun):  ②
        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):        ③
        return re.sub('y$', 'ies', noun)     
    else:
        return noun + 's'
  1. Строка 3. Здесь мы заменяем конец строки (найденный с помощью символа $) на строку es. Другими словами, добавляете es к строке. Вы могли бы выполнить то же самое с помощью конкатенации строк, например, как noun + 'es', но я предпочел использовать регулярные выражения для каждого правила по причинам, которые станут ясны позже.
  2. Строка 4. Взгляните-ка, это регулярное выражение содержит кое-что новое. Символ ^ в качестве первого символа в квадратных скобках имеет особый смысл: отрицание. [^abc] означает «любой отдельный символ кроме a, b или c». Так что [^aeioudgkprt] означает любой символ кроме a, e, i, o, u, d, g, k, p, r или t. Затем за этим символом должен быть символ h, следом за ним конец строки. Мы ищем слова, заканчивающиеся на H, которую можно услышать.
  3. Строка 6. То же самое здесь: найти слова, которые заканчиваются на Y, в которых символ перед Y – не a, e, i, o или u. Мы ищем слова, заканчивающиеся на Y, которая звучит как I.

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

>>> import re
>>> re.search('[^aeiou]y$', 'vacancy')  ①
<_sre.SRE_Match object at 0x001C1FA8>
>>> re.search('[^aeiou]y$', 'boy')      ②
>>> 
>>> re.search('[^aeiou]y$', 'day')
>>> 
>>> re.search('[^aeiou]y$', 'pita')     ③
>>> 
  1. Строка 2. vacancy подходит, потому что оно заканчивается на cy, и c – не a, e, i, o или u.
  2. Строка 4. boy не подходит, потому что оно заканчивается на oy, а вы конкретно указали, что символ перед y не может быть o. day не подходит, потому что оно заканчивается на ay.
  3. Строка 9. pita не подходит, потому что оно не заканчивается на y.
>>> re.sub('y$', 'ies', 'vacancy')               ①
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')  ②
'vacancies'
  1. Строка 1. Это регулярное выражение преобразует vacancy в vacancies, а agency – в agencies, что нам и нужно. Заметьте, что оно бы преобразовало boy в boies, но этого в нашей функции никогда не произойдет, потому что мы сначала выполнили re.search с целью выяснить, следует ли выполнять re.sub.
  2. Строка 5. Замечу заодно, что можно объединить эти два регулярных выражения (одно, чтобы выяснить применяется ли правило, а другое, чтобы собственно его применить) в одно регулярное выражение. Вот так выглядел бы результат. Большая часть должна быть вам знакома: вы используете запоминаемую группу, о которой вы узнали из «Учебный пример: обработка телефонных номеров». Группа используется, чтобы запомнить символ перед y. Затем в подстановочной строке, вы используете новый синтаксис, \1, который означает «эй, та первая группа, которую ты запомнил? положи это прямо сюда». Таким образом, вы помните c перед y; когда вы делаете подстановку, вы ставите c на место c, и ies на место y. (Если у вас более одной запоминаемой группы, можете использовать \2 и \3 и так далее.)

Замены с использованием регулярных выражений являются чрезвычайно мощным инструментом, а синтаксис \1 делает их еще более мощным. Но вся операция, объединенная в одно регулярное выражение, также становится сложной для чтения; кроме того, такой способ не соотносится напрямую с тем, как мы изначально описали правила формирования множественного числа. Изначально мы спроектировали правила в форме «если слово заканчивается на S, X или Z, то добавьте ES». А если вы посмотрите на функцию, то у вас – две строки кода, которые говорят «если слово заканчивается на S, X или Z, то добавьте ES». Еще ближе к оригинальному варианту приблизиться никак не получится.

6.3 Список функций

Сейчас мы добавим уровень абстракции. Мы начали с определения списка правил: если верно это, сделай то, иначе обращайтесь к следующему правилу. Давайте временно усложним один фрагмент программы, так чтобы мы смогли упростить другой ее фрагмент.

import re

def match_sxz(noun):
    return re.search('[sxz]$', noun)

def apply_sxz(noun):
    return re.sub('$', 'es', noun)

def match_h(noun):
    return re.search('[^aeioudgkprt]h$', noun)

def apply_h(noun):
    return re.sub('$', 'es', noun)

def match_y(noun):                             ①
    return re.search('[^aeiou]y$', noun)
        
def apply_y(noun):                             ②
    return re.sub('y$', 'ies', noun)

def match_default(noun):
    return True

def apply_default(noun):
    return noun + 's'

rules = ((match_sxz, apply_sxz),               ③
         (match_h, apply_h),
         (match_y, apply_y),
         (match_default, apply_default)
         )

def plural(noun):           
    for matches_rule, apply_rule in rules:       ④
        if matches_rule(noun):
            return apply_rule(noun)
  1. Строка 15. Теперь каждое правило (условие совпадения) является отдельной функцией которая возвращает результаты вызова функции re.search().
  2. Строка 18. Каждое правило-действие также является отдельной функцией, которая вызывает функцию re.sub(), чтобы применить соответствующее правило формирования множественного числа.
  3. Строка 27. Вместо одной функции (plural()) с несколькими правилами, у вас теперь есть структура данных rules, являющаяся последовательностью пар функций.
  4. Строка 34. Поскольку правила развернуты в отдельной структуре данных, новая функция plural() может быть сокращена до нескольких строк кода. Используя цикл for, из структуры rules можно извлечь правила условия и замены одновременно. При первой итерации цикла formatch_rules станет match_sxz, а apply_rule станет apply_sxz. Во время второй итерации, если мы до нее дойдем, matches_rule будет присвоено match_h, а apply_rule станет apply_h. Функция гарантированно вернет что-нибудь по окончании работы, потому что последнее правило совпадения (match_default) просто возвращает True, подразумевая, что соответствующее правило замены (apply_default) всегда будет применено.

Причиной, по которой этот пример работает, является тот факт, что в Python всё является объектом, даже функции. Структура данных rules содержит функции – не имена функций, а реальные функции-объекты. Когда они присваиваются в цикле for, matches_rule и apply_rule являются настоящими функциями, которые вы можете вызывать. При первой итерации цикла for это эквивалентно вызову matches_sxz(noun) и, если она возвращает совпадение, вызову apply_sxz(noun).

Переменная "rules" – это последовательность пар функций.

Если этот дополнительный уровень абстракции сбивает вас с толку, попробуйте развернуть функцию, чтобы увидеть, что мы получаем то же самое. Весь цикл for эквивалентен следующему:

def plural(noun):
    if match_sxz(noun):
        return apply_sxz(noun)
    if match_h(noun):
        return apply_h(noun)
    if match_y(noun):
        return apply_y(noun)
    if match_default(noun):
        return apply_default(noun)

Преимуществом здесь является то, что функция plural() упрощена. Она принимает последовательность правил, определенных где-либо, и проходит по ним.

  1. Получить правило совпадения
  2. Правило срабатывает? Тогда применить правило замены и вернуть результат.
  3. Нет совпадений? Начать с пункта 1.

Правила могут быть определены где угодно, любым способом. Для функции plural() абсолютно нет никакой разницы.

Итак, добавление этого уровня абстракции стоило того? Вообще-то пока нет. Попробуем представить, что потребуется для добавления нового правила в функцию. В первом примере это потребовало бы добавить новую конструкцию if в функцию plural(). Во втором примере это потребовало бы добавить две функции, match_foo() и apply_foo(), а затем обновить последовательность rules, чтобы указать, когда новые правила совпадения и замены должны быть вызваны по отношению к остальным правилам.

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

6.4 Список шаблонов

Определение отдельных именованных функций для каждого условия и правила замены вовсе не является необходимостью. Вы никогда не вызываете их напрямую; вы добавляете их в последовательность rules и вызываете их через нее. Более того, каждая функция следует одному из двух шаблонов. Все функции совпадения вызывают re.search(), а все функции замены вызывают re.sub(). Давайте вынесем шаблоны, чтобы объявление новых правил было более простым.

import re

def build_match_and_apply_functions(pattern, search, replace):
    def matches_rule(word):                                     ①
        return re.search(pattern, word)
    def apply_rule(word):                                       ②
        return re.sub(search, replace, word)
    return (matches_rule, apply_rule)                           ③
  1. Строка 4. build_match_and_apply_functions() – это функция, которая динамически создает другие функции. Она принимает pattern, search и replace, а затем определяет функцию matches_rule(), которая вызывает re.search() с шаблоном pattern, переданным функции build_match_and_apply_functions() в качестве аргумента, и word, который передается функции matches_rule(), которую вы определяете.
  2. Строка 6. Строим функцию apply_rule() тем же способом. Функция apply_rule() – это функция, которая принимает один параметр, и вызывает re.sub() с параметрами search и replace, переданными функции build_match_and_apply_functions(), и word, переданным функции apply_rule(), которую вы создаете. Подход, заключающийся в использовании значений внешних параметров внутри динамической функции называется замыканиями. По сути, вы определяете константы в функции замены: она принимает один параметр (word), но затем действует используя его и два других значения (search и replace), которые были установлены в момент определения функции замены.
  3. Строка 8. В конце концов, функция build_match_and_apply_functions() возвращает кортеж с двумя значениями, двумя функциями, которые вы только что создали. Константы, которые вы определили внутри этих функций (pattern внутри функции match_rule(), search и replace в функции apply_rule()) остаются с этими функциями, даже после возвращения из build_match_and_apply_functions(). Это безумно круто.

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

patterns = \                                                        ①
  (
    ('[sxz]$',           '$',  'es'),
    ('[^aeioudgkprt]h$', '$',  'es'),
    ('(qu|[^aeiou])y$',  'y$', 'ies'),
    ('$',                '$',  's')                                 ②
  )
rules = [build_match_and_apply_functions(pattern, search, replace)  ③
         for (pattern, search, replace) in patterns]
  1. Строка 1. Наши правила формирования множественного числа теперь определены как кортеж из кортежей строк (не функций). Первая строка в каждой группе – это регулярное выражение, которое вы бы использовали в re.search(), чтобы определить, подходит ли данное правило. Вторая и третья строки в каждой группе – это выражения для поиска и замены, которые вы бы использовали в re.sub(), чтобы применить правило и преобразовать существительное во множественное число.
  2. Строка 6. В правиле по умолчанию есть небольшое изменение. В прошлом примере функция match_default() просто возвращает True, подразумевая, что если ни одно конкретное правило не применилось, код должен просто добавить s в конец данного слова. Функционально данный пример делает то же самое. Конечное регулярное выражение узнает, заканчивается ли слово ($ ищет конец строки). Конечно же, у каждой строки есть конец, даже у пустой, так что выражение всегда срабатывает. Таким образом, оно служит той же цели, что и функция match_default(), которая всегда возвращала True: оно гарантирует, что если нет других конкретных выполненных правил, код добавляет s в конец заданного слова.
  3. Строка 8. Это волшебная строка. Она принимает последовательность строк в patterns и превращает их в последовательность функций. Как? «Отображением» строк в функцию build_and_apply_functions(). То есть она берет каждую тройку строк и вызывает функцию build_match_and_apply_functions() с этими тремя строками в качестве аргументов. Функция build_match_and_apply_functions() возвращает кортеж из двух функций. Это означает, что переменная rules, в конце концов, функционально становится эквивалентной предыдущему примеру: список кортежей, где каждый кортеж – это пара функций. Первая функция – это функция совпадения, которая вызывает re.search(), а вторая функция – применение правила (замена), которое вызывает re.sub().

Завершим эту версию скрипта главной точкой входа, функцией plural().

def plural(noun):
    for matches_rule, apply_rule in rules:  ①
        if matches_rule(noun):
            return apply_rule(noun)
  1. Строка 2. Поскольку список rules – тот же самый, что и в предыдущем примере (да, так и есть), нет ничего удивительного в том, что функция plural() совсем не изменилась. Она является полностью обобщенной; она принимает список функций-правил и вызывает их по порядку. Ее не волнует, как определены правила. В предыдущем примере они были определены как отдельные именованные функции. Теперь же они создаются динамически сопоставлением результата функции build_match_and_apply_functions() списку обычных строк. Это не играет никакой роли, функция plural() продолжает работать как и раньше.

6.5 Файл шаблонов

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

Во-первых, давайте создадим текстовый файл, содержащий нужные нам правила. Никаких сложных структур данных, просто разделенные на три колонки данные. Назовем его plural4-rules.txt.

[sxz]$               $    es
[^aeioudgkprt]h$     $    es
[^aeiou]y$          y$    ies
$                    $    s

Теперь давайте посмотрим, как вы можете использовать этот файл с правилами.

import re

def build_match_and_apply_functions(pattern, search, replace):     ①
    def matches_rule(word):
        return re.search(pattern, word)
    def apply_rule(word):
        return re.sub(search, replace, word)
    return (matches_rule, apply_rule)

rules = []
with open('plural4-rules.txt', encoding='utf-8') as pattern_file:  ②
    for line in pattern_file:                                      ③
        pattern, search, replace = line.split(None, 2)             ④
        rules.append(build_match_and_apply_functions(              ⑤
                pattern, search, replace))
  1. Строка 3. Функция build_match_and_apply_functions() не изменилась. Вы, по-прежнему, используете замыкания, чтобы динамически создать две функции, которые будут использовать переменные из внешней функции.
  2. Строка 11. Глобальная функция open() открывает файл и возвращает файловый объект. В данном случае файл, который мы открываем, содержит строки-шаблоны для правил формирования множественного числа. Утверждение with создает то, что называется контекстом: когда блок with заканчивается, Python автоматически закроет файл, даже если внутри блока with было выброшено исключение. Подробнее о блоках with и файловых объектах вы узнаете из главы «Файлы».
  3. Строка 12. Форма «for line in <fileobject>» читает данные из открытого файла построчно и присваивает текст переменной line. Подробнее про чтение файлов вы узнаете из главы «Файлы».
  4. Строка 13. Каждая строка в файле действительно содержит три значения, но они разделены пустым пространством (табуляцией или пробелами, без разницы). Чтобы разделить их, используйте строковый метод split(). Первый аргумент для split()None, что означает «разделить любым пробельным символом (табуляцией или пробелом, без разницы)». Второй аргумент – 2, что означает «разбить пробельными символами 2 раза» (разделение один раз возвращает два значения, разделение два раза возвращает три значения, и так далее). Строка вида «[sxz]$ $ es» будет разбита и преобразована в список ['[sxz]$', '$', 'es'], что означает, что pattern станет равен '[sxz]$', search – '$', а replace получит значение 'es'. Это довольно мощно для одной маленькой строки кода
  5. Строка 14. В конце концов, вы передаете pattern, search и replace функции build_match_and_apply_function(), которая возвращает кортеж функций. Вы добавляете этот кортеж в список rules, и в завершении rules хранит список функций поиска совпадений и выполнения замен, который ожидает функция plural().

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

6.6 Генераторы

Но ведь будет круто, если обобщенная функция plural() будет разбирать файл с правилами? Извлеки правила, найди совпадения, примени соответствующие изменения, переходи к следующему правилу. Это всё, что функции plural() придется делать, и больше ничего от нее не требуется.

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 2)
            yield build_match_and_apply_functions(pattern, search, replace)

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))

Как это работает? Давайте сначала посмотрим на пример с пояснениями.

>>> def make_counter(x):
...     print('entering make_counter')
...     while True:
...         yield x                    ①
...         print('incrementing x')
...         x = x + 1
... 
>>> counter = make_counter(2)          ②
>>> counter                            ③
<generator object at 0x001C9C10>
>>> next(counter)                      ④
entering make_counter
2
>>> next(counter)                      ⑤
incrementing x
3
>>> next(counter)                      ⑥
incrementing x
4
  1. Строка 4. Присутствие ключевого слова yield в make_counter означает, что это не обычная функция. Это особый вид функции, которая генерирует значения по одному за раз. Вы можете думать о ней как о возобновляемой функции. Её вызов вернёт генератор, который может быть использован для генерирования последовательных значений x.
  2. Строка 8. Чтобы создать экземпляр генератора make_counter, просто вызовите его как и любую другую функцию. Заметьте, что на самом деле это не выполняет код функции. Вы можете так сказать, потому что первая строка функции make_counter() вызывает print(), но ничего не печатается.
  3. Строка 9. Функция make_counter() возвращает объект-генератор.
  4. Строка 11. Функция next() принимает генератор и возвращает его следующее значение. Первый раз, когда вы вызываете next() с генератором counter, он исполняет код в make_counter() до первого оператора yield и возвращает значение, которое было возвращено yield. В данном случае, это будет 2, поскольку изначально вы создали генератор вызовом make_counter(2).
  5. Строка 14. Повторный вызов next() с тем же генератором продолжает вычисления точно там, где они были прерваны, и продолжает до тех пор, пока не встретит следующий yield. Все переменные, локальные состояния и т. д. сохраняются во время yield, и восстанавливаются при вызове next(). Следующая строка кода, ожидающая исполнения, вызывает print(), который печатает incrementing x. После этого следует утверждение x = x + 1. Затем снова исполняется цикл while, и первое, что в нём встречается, – оператор yield x, который сохраняет состояние всего и возвращает текущее значение x (сейчас это 3).
  6. Строка 17. После второго вызова next(counter) происходит всё то же самое, только теперь x становится равным 4.

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

6.6.1 Генератор последовательности Фибоначчи

def fib(max):
    a, b = 0, 1          ①
    while a < max:
        yield a          ②
        a, b = b, a + b  ③
  1. Строка 2. Последовательность Фибоначчи – это последовательность чисел, в которой каждое число является суммой двух предыдущих. Она начинается с 0 и 1 растет сначала постепенно, а потом всё быстрее и быстрее. Чтобы начать последовательность, вам необходимо две переменные: а начинает с 0, а b – с 1.
  2. Строка 4. a является начальным значением последовательности, поэтому её следует вернуть.
  3. Строка 5. b является следующим числом последовательности, поэтому присвойте ее a и посчитайте следующее значение (a + b) и присвойте его b для последующего использования. Заметьте, что это происходит одновременно. Если a равно 3, а b равно 5, то a, b = b, a + b установит a в 5 (предыдущее значение b), а b в 8 (сумма предыдущих значений a и b).

Теперь у вас есть функция, выдает последовательные числа Фибоначчи. Конечно, вы могли бы сделать это с помощью рекурсии, но данная реализация читается проще. Помимо этого, она лучше работает с циклами for.

yield приостанавливает функцию, next() снова запускает ее в том же состоянии

>>> from fibonacci import fib
>>> for n in fib(1000):      ①
...     print(n, end=' ')    ②
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> list(fib(1000))          ③
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]
  1. Строка 2. Вы можете использовать генератор типа fib() непосредственно в цикле for. Цикл for автоматически вызывает функцию next(), чтобы получить значения из генератора fib() и присвоить их переменной n цикла for.
  2. Строка 3. Каждый раз, проходя цикл for, n принимает новое значение от yield в функции fib(), и всё, что вам нужно сделать, – напечатать его. Как только fib() выходит за пределы чисел (a становится больше, чем max, которое в данном случае равно 1000), так цикл for сразу заканчивает работу.
  3. Строка 5. Еще один полезный способ реализации: отдайте генератор функции list(), и она пройдет в цикле весь генератор (точно так же, как и цикл for в предыдущем примере) и вернет список всех значений.

6.6.2 Генератор правил образования множественного числа

Давайте вернемся к plural5.py и посмотрим, как работает эта версия функции plural().

def rules(rules_filename):
    with open(rules_filename, encoding='utf-8') as pattern_file:
        for line in pattern_file:
            pattern, search, replace = line.split(None, 2)                   ①
            yield build_match_and_apply_functions(pattern, search, replace)  ②

def plural(noun, rules_filename='plural5-rules.txt'):
    for matches_rule, apply_rule in rules(rules_filename):                   ③
        if matches_rule(noun):
            return apply_rule(noun)
    raise ValueError('no matching rule for {0}'.format(noun))
  1. Строка 4. Никакой магии. Помните, что строки файла правил содержат по три значения, разделенных пустым пространством, поэтому мы используем line.split(None, 3), чтобы получить три «колонки» и присвоить их трем локальным переменным.
  2. Строка 5. А затем мы вызываем yield. Что мы возвращаем? Две функции, созданные динамически вашим старым помощником, build_match_and_apply_functions(), который такой же как и в предыдущих примерах. Другими словами, rules() – это генератор, который отдаёт правила совпадения и изменения по требованию.
  3. Строка 8. Поскольку rules() – это генератор, вы можете использовать его напрямую в цикле for. При первом прохождении цикла for, вы вызовете функцию rules(), которая откроет файл шаблонов, прочитает первую строку, динамически построит функции условия и модификации из шаблона в этой строке, и вернет динамически созданные функции. При прохождении через цикл for второй раз, вы продолжите ровно с того места, в котором покинули rules() (это было внутри цикла for line in pattern_file). Первое, что он сделает, – прочитает следующую строку файла (который до сих пор открыт), динамически создаст следующие функции условия и модификации на основании шаблонов этой строки файла, и вернет эти две функции.

Чего мы достигли по сравнению с вариантом 4? Меньшее время запуска. В варианте 4 когда вы импортировали модуль plural4, он читал весь файл шаблонов и строил список всех возможных правил ещё до того, как вы вообще подумали о вызове функции plural(). С генераторами вы можете выполнять все действия лениво: вы читаете первое правило, создаете функции и пробуете их, и, если правило срабатывает, вы не читаете остальной файл и не создаете другие функции.

В чем вы теряете? В производительности! Каждый раз, когда вы вызываете функцию plural(), генератор rules() начинает всё с начала – что означает открытие заново файла шаблонов и чтение с самого начала построчно.

Что если бы вы могли взять лучшее из двух миров: минимальные расходы на запуск (не исполнять никакого кода во время import) и максимальная производительность (не создавать одни и те же функции снова и снова). И да, мы по-прежнему хотим хранить правила в отдельном файле (потому что код – это код, а данные – это данные), настолько долго, пока нам вдруг не потребуется прочитать одну и ту же строку дважды.

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

6.7 Материалы для дальнейшего чтения

Источник:

  • Mark Pilgrim. Dive Into Python 3

Теги

PythonОбучениеПрограммированиеЯзыки программирования

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.