Глава 6. Замыкания и генераторы
У меня правописание хорошее. Оно хорошее, но почему-то хромает, и буквы опаздывают... на свои места.
Содержание главы
Погружение
По причинам, превосходящим всяческое понимание, я всегда восхищался языками. Не языками программирования. Хотя да, ими, а также языками естественными. Возьмем, к примеру, английский. Английский язык – это шизофренический язык, который заимствует слова из немецкого, французского, испанского и латинского языков (не говоря уже об остальных). Откровенно говоря, «заимствует» – неуместное слово; он их скорее «крадет». Или, возможно, «ассимилирует» – как Борги. Да, хороший вариант.
«Мы Борги. Ваши лингвистические и этимологические особенности станут нашими. Сопротивление бесполезно.»
В данной главе вы узнаете о существительных во множественном числе. Также вы узнаете о функциях, которые возвращают другие функции, о сложных регулярных выражениях и генераторах. Но сначала давайте поговорим о том, как образуются существительные во множественном числе. (Если вы не читали главу, посвященную регулярным выражениям, сейчас самое время. Материал данной главы подразумевает, что вы понимаете основы регулярных выражений и довольно быстро перейдете к их нетривиальному использованию.)
Если вы выросли в англоязычной стране или изучали английский язык в формальной школьной обстановке, вы, вероятно, знакомы с основными правилами:
- если слово заканчивается буквами S, X или Z, следует добавить ES. Bass становится basses, fax становится faxes, а waltz – waltzes;
- если слово заканчивается звонкой 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, а woman – women, но 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'
- Строка 4. Это регулярное выражение, но оно использует синтаксис, который вы не видели в главе «Регулярные выражения». Квадратные скобки означают «найти совпадения ровно с одним из этих символов». Поэтому
[sxz]
означает «s, или x, или z», но только один из них. Символ$
должен быть знаком вам. Он ищет совпадения с концом строки. Всё регулярное выражение проверяет, заканчивается лиnoun
на s, x или z. - Строка 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'
- Строка 2. Содержит ли строка Mark символы a, b или c? Да, содержит a.
- Строка 4. Отлично, теперь ищем a, b или c и заменяем на o. Mark становится Mork.
- Строка 6. Та же функция превращает rock в rook.
- Строка 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'
- Строка 3. Здесь мы заменяем конец строки (найденный с помощью символа $) на строку es. Другими словами, добавляете es к строке. Вы могли бы выполнить то же самое с помощью конкатенации строк, например, как
noun + 'es'
, но я предпочел использовать регулярные выражения для каждого правила по причинам, которые станут ясны позже. - Строка 4. Взгляните-ка, это регулярное выражение содержит кое-что новое. Символ ^ в качестве первого символа в квадратных скобках имеет особый смысл: отрицание.
[^abc]
означает «любой отдельный символ кроме a, b или c». Так что[^aeioudgkprt]
означает любой символ кроме a, e, i, o, u, d, g, k, p, r или t. Затем за этим символом должен быть символ h, следом за ним конец строки. Мы ищем слова, заканчивающиеся на H, которую можно услышать. - Строка 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') ③
>>>
- Строка 2. vacancy подходит, потому что оно заканчивается на cy, и c – не a, e, i, o или u.
- Строка 4. boy не подходит, потому что оно заканчивается на oy, а вы конкретно указали, что символ перед y не может быть o. day не подходит, потому что оно заканчивается на ay.
- Строка 9. pita не подходит, потому что оно не заканчивается на y.
>>> re.sub('y$', 'ies', 'vacancy') ①
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'
>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy') ②
'vacancies'
- Строка 1. Это регулярное выражение преобразует vacancy в vacancies, а agency – в agencies, что нам и нужно. Заметьте, что оно бы преобразовало boy в boies, но этого в нашей функции никогда не произойдет, потому что мы сначала выполнили
re.search
с целью выяснить, следует ли выполнятьre.sub
. - Строка 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)
- Строка 15. Теперь каждое правило (условие совпадения) является отдельной функцией которая возвращает результаты вызова функции
re.search()
. - Строка 18. Каждое правило-действие также является отдельной функцией, которая вызывает функцию
re.sub()
, чтобы применить соответствующее правило формирования множественного числа. - Строка 27. Вместо одной функции (
plural()
) с несколькими правилами, у вас теперь есть структура данныхrules
, являющаяся последовательностью пар функций. - Строка 34. Поскольку правила развернуты в отдельной структуре данных, новая функция
plural()
может быть сокращена до нескольких строк кода. Используя циклfor
, из структурыrules
можно извлечь правила условия и замены одновременно. При первой итерации циклаfor
match_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.
Правила могут быть определены где угодно, любым способом. Для функции 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) ③
- Строка 4.
build_match_and_apply_functions()
– это функция, которая динамически создает другие функции. Она принимаетpattern
,search
иreplace
, а затем определяет функциюmatches_rule()
, которая вызываетre.search()
с шаблономpattern
, переданным функцииbuild_match_and_apply_functions()
в качестве аргумента, иword
, который передается функцииmatches_rule()
, которую вы определяете. - Строка 6. Строим функцию
apply_rule()
тем же способом. Функцияapply_rule()
– это функция, которая принимает один параметр, и вызываетre.sub()
с параметрамиsearch
иreplace
, переданными функцииbuild_match_and_apply_functions()
, иword
, переданным функцииapply_rule()
, которую вы создаете. Подход, заключающийся в использовании значений внешних параметров внутри динамической функции называется замыканиями. По сути, вы определяете константы в функции замены: она принимает один параметр (word
), но затем действует используя его и два других значения (search
иreplace
), которые были установлены в момент определения функции замены. - Строка 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. Наши правила формирования множественного числа теперь определены как кортеж из кортежей строк (не функций). Первая строка в каждой группе – это регулярное выражение, которое вы бы использовали в
re.search()
, чтобы определить, подходит ли данное правило. Вторая и третья строки в каждой группе – это выражения для поиска и замены, которые вы бы использовали вre.sub()
, чтобы применить правило и преобразовать существительное во множественное число. - Строка 6. В правиле по умолчанию есть небольшое изменение. В прошлом примере функция
match_default()
просто возвращаетTrue
, подразумевая, что если ни одно конкретное правило не применилось, код должен просто добавить s в конец данного слова. Функционально данный пример делает то же самое. Конечное регулярное выражение узнает, заканчивается ли слово ($ ищет конец строки). Конечно же, у каждой строки есть конец, даже у пустой, так что выражение всегда срабатывает. Таким образом, оно служит той же цели, что и функцияmatch_default()
, которая всегда возвращалаTrue
: оно гарантирует, что если нет других конкретных выполненных правил, код добавляет s в конец заданного слова. - Строка 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)
- Строка 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))
- Строка 3. Функция
build_match_and_apply_functions()
не изменилась. Вы, по-прежнему, используете замыкания, чтобы динамически создать две функции, которые будут использовать переменные из внешней функции. - Строка 11. Глобальная функция
open()
открывает файл и возвращает файловый объект. В данном случае файл, который мы открываем, содержит строки-шаблоны для правил формирования множественного числа. Утверждениеwith
создает то, что называется контекстом: когда блокwith
заканчивается, Python автоматически закроет файл, даже если внутри блокаwith
было выброшено исключение. Подробнее о блокахwith
и файловых объектах вы узнаете из главы «Файлы». - Строка 12. Форма «
for line in <fileobject>
» читает данные из открытого файла построчно и присваивает текст переменнойline
. Подробнее про чтение файлов вы узнаете из главы «Файлы». - Строка 13. Каждая строка в файле действительно содержит три значения, но они разделены пустым пространством (табуляцией или пробелами, без разницы). Чтобы разделить их, используйте строковый метод
split()
. Первый аргумент дляsplit()
–None
, что означает «разделить любым пробельным символом (табуляцией или пробелом, без разницы)». Второй аргумент – 2, что означает «разбить пробельными символами 2 раза» (разделение один раз возвращает два значения, разделение два раза возвращает три значения, и так далее). Строка вида «[sxz]$ $ es» будет разбита и преобразована в список['[sxz]$', '$', 'es']
, что означает, чтоpattern
станет равен '[sxz]$',search
– '$', аreplace
получит значение 'es'. Это довольно мощно для одной маленькой строки кода - Строка 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
- Строка 4. Присутствие ключевого слова
yield
вmake_counter
означает, что это не обычная функция. Это особый вид функции, которая генерирует значения по одному за раз. Вы можете думать о ней как о возобновляемой функции. Её вызов вернёт генератор, который может быть использован для генерирования последовательных значенийx
. - Строка 8. Чтобы создать экземпляр генератора
make_counter
, просто вызовите его как и любую другую функцию. Заметьте, что на самом деле это не выполняет код функции. Вы можете так сказать, потому что первая строка функцииmake_counter()
вызываетprint()
, но ничего не печатается. - Строка 9. Функция
make_counter()
возвращает объект-генератор. - Строка 11. Функция
next()
принимает генератор и возвращает его следующее значение. Первый раз, когда вы вызываетеnext()
с генераторомcounter
, он исполняет код вmake_counter()
до первого оператораyield
и возвращает значение, которое было возвращеноyield
. В данном случае, это будет 2, поскольку изначально вы создали генератор вызовомmake_counter(2)
. - Строка 14. Повторный вызов
next()
с тем же генератором продолжает вычисления точно там, где они были прерваны, и продолжает до тех пор, пока не встретит следующийyield
. Все переменные, локальные состояния и т. д. сохраняются во времяyield
, и восстанавливаются при вызовеnext()
. Следующая строка кода, ожидающая исполнения, вызываетprint()
, который печатает incrementing x. После этого следует утверждениеx = x + 1
. Затем снова исполняется циклwhile
, и первое, что в нём встречается, – операторyield x
, который сохраняет состояние всего и возвращает текущее значениеx
(сейчас это 3). - Строка 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 ③
- Строка 2. Последовательность Фибоначчи – это последовательность чисел, в которой каждое число является суммой двух предыдущих. Она начинается с 0 и 1 растет сначала постепенно, а потом всё быстрее и быстрее. Чтобы начать последовательность, вам необходимо две переменные:
а
начинает с 0, аb
– с 1. - Строка 4.
a
является начальным значением последовательности, поэтому её следует вернуть. - Строка 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]
- Строка 2. Вы можете использовать генератор типа
fib()
непосредственно в циклеfor
. Циклfor
автоматически вызывает функциюnext()
, чтобы получить значения из генератораfib()
и присвоить их переменнойn
циклаfor
. - Строка 3. Каждый раз, проходя цикл
for
,n
принимает новое значение отyield
в функцииfib()
, и всё, что вам нужно сделать, – напечатать его. Как толькоfib()
выходит за пределы чисел (a
становится больше, чемmax
, которое в данном случае равно 1000), так циклfor
сразу заканчивает работу. - Строка 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))
- Строка 4. Никакой магии. Помните, что строки файла правил содержат по три значения, разделенных пустым пространством, поэтому мы используем
line.split(None, 3)
, чтобы получить три «колонки» и присвоить их трем локальным переменным. - Строка 5. А затем мы вызываем
yield
. Что мы возвращаем? Две функции, созданные динамически вашим старым помощником,build_match_and_apply_functions()
, который такой же как и в предыдущих примерах. Другими словами,rules()
– это генератор, который отдаёт правила совпадения и изменения по требованию. - Строка 8. Поскольку
rules()
– это генератор, вы можете использовать его напрямую в циклеfor
. При первом прохождении циклаfor
, вы вызовете функциюrules()
, которая откроет файл шаблонов, прочитает первую строку, динамически построит функции условия и модификации из шаблона в этой строке, и вернет динамически созданные функции. При прохождении через циклfor
второй раз, вы продолжите ровно с того места, в котором покинулиrules()
(это было внутри циклаfor line in pattern_file
). Первое, что он сделает, – прочитает следующую строку файла (который до сих пор открыт), динамически создаст следующие функции условия и модификации на основании шаблонов этой строки файла, и вернет эти две функции.
Чего мы достигли по сравнению с вариантом 4? Меньшее время запуска. В варианте 4 когда вы импортировали модуль plural4
, он читал весь файл шаблонов и строил список всех возможных правил ещё до того, как вы вообще подумали о вызове функции plural()
. С генераторами вы можете выполнять все действия лениво: вы читаете первое правило, создаете функции и пробуете их, и, если правило срабатывает, вы не читаете остальной файл и не создаете другие функции.
В чем вы теряете? В производительности! Каждый раз, когда вы вызываете функцию plural()
, генератор rules()
начинает всё с начала – что означает открытие заново файла шаблонов и чтение с самого начала построчно.
Что если бы вы могли взять лучшее из двух миров: минимальные расходы на запуск (не исполнять никакого кода во время import
) и максимальная производительность (не создавать одни и те же функции снова и снова). И да, мы по-прежнему хотим хранить правила в отдельном файле (потому что код – это код, а данные – это данные), настолько долго, пока нам вдруг не потребуется прочитать одну и ту же строку дважды.
Чтобы сделать это, нам необходимо будет построить свой собственный итератор. Но перед тем, как мы это сделаем, нам необходимо изучить классы в Python.
6.7 Материалы для дальнейшего чтения
- PEP 255: Simple Generators
- Understanding Python’s «with» statement
- Closures in Python
- Числа Фибоначчи
- English Irregular Plural Nouns