Перейти до вмісту

Пориньте у Python 3/Замикання та генератори

Матеріал з Вікіпідручника

Мій правопис якийсь кульгавий. Узагалі він гарний правопис, тільки от чогось трохи накульгує, і букви заскакують не на свої місця...
Вінні Пух

Вирісши як син бібліотекаря та вчителя англійської, я завжди був зачарований мовами. Не мовами програмування. Ну, так, і мовами програмування, але і природніми мовами теж. Візьмімо наприклад англійську. Англійська - це шизофренічна мова яка запозичує слова з німецької, французької, іспанської та латинської (і це не всі). Насправді "позичила" - не те слово, "награбувала" підходить більше. Чи можливо "асимілювала" - як Борґ. Так, мені подобається ця аналогія.

Ми Борґ. Ваша лінгвістична та етимологічна відмінність буде додана до нашої. Опір марний.

В цьому розділі ми вивчимо множину іменників в англійській мові. А також, функції що повертають інші функції, генератори та поглибимо знання регулярних виразів. Але спершу, давайте поговоримо про те як створювати множину іменників. (Якщо ви ще не прочитали розділ про Регулярні вирази зараз саме пора. Цей розділ передбачає знання основ регулярних виразів, і швидко переходить до деяких поглиблених речей.)

Якщо ви вчили англійську в школі, ви напевне знайомі з основними правилами:

  • Якщо слово закінчується на 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, яка буде автоматично створювати множину іменників в англійській мові. Ми почнемо з вищезгаданих чотирьох правил, але пам'ятайте що неминуче доведеться додати більше.


* * *


Я знаю, давайте використаємо регулярні вирази!

[ред.]

Отож, коли ви дивитесь на слова, принаймі в англійській мові це означає що ви дивитесь на послідовності символів. У нас є правила які кажуть що потрібно знаходити певні комбінації символів, а потім робити з ними різні речі. Це звучить як робота для регулярних виразів!


import re

def plural(noun):          
    if re.search('[sxz]$', noun):

Це регулярний вираз, але він використовує синтаксис який ви не побачите в регулярних виразах. Квадратні дужки означають "співставити з одним з перелічених символів". Отож, [sxz] означає "s, або x, або z", але лише один з них. $ повинен бути знайомим - він співставляється з кінцем рядка. Разом вони перевіряють чи рядок noun закінчується на s, x, або z.

        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'

Функція re.sub() виконує заміну рядків на основі регулярних виразів.


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

>>> import re
>>> re.search('[abc]', 'Mark')
<_sre.SRE_Match object at 0x001C1FA8>

Рядок 'Mark' містить a, b чи c.

>>> re.sub('[abc]', 'o', 'Mark')
'Mork'

Ок, тепер давайте знайдемо a, b чи c і замінимо на o. Mark перетворюється на Mork.

>>> re.sub('[abc]', 'o', 'rock')
'rook'

Ця ж функція замінює rock на rook.

>>> re.sub('[abc]', 'o', 'caps')
'oops'

Можна було б подумати що вона перетворить caps в oaps, але ні re.sub замінює всі співпадіння, а не тільки перше. Тому цей вираз перетворює caps в oops, тому що і c і a відповідають шаблону і замінюються на o.


А тепер повернімось назад до функції plural()...

def plural(noun):          
    if re.search('[sxz]$', noun):            
        return re.sub('$', 'es', noun)

Тут ми замінюємо кінець рядка (шаблон $) рядком es. Іншими словами, додаємо es до рядка. Можна досягти цього ж, за допомогою конкатенації рядків, наприклад noun + 'es', але я вирішив використати регулярні вирази, з причин які стануть зрозумілими пізніше в розділі.

    elif re.search('[^aeioudgkprt]h$', noun):

Уважно придивіться до цього нового регулярного виразу. Знак ^ розміщений першим всередині квадратних дужок означає дещо особливе: доповнення. [^abc] означає "всі символи крім a, b, чи ". Тому [^aeioudgkprt] означає будь-який символ, окрім a, e, i, o, u, d, g, k, p, r, t. Після одного з таких символів повинна йти h, за якою рядок закінчується. Ми шукаємо слова що закінчуються на H, і вона вимовляється вголос.

        return re.sub('$', 'es', noun)
    elif re.search('[^aeiou]y$', noun):

Аналогічно тут: знайти слова що закінчуються на Y, де символ перед Y не є a, e, i, o, чи u. Шукаємо слова що закінчуються на Y, яке звучить як I.

        return re.sub('y$', 'ies', noun)    
    else:
        return noun + 's'

Давайте розглянемо такі регулярні вирази більш детально.

>>> import re
>>> re.search('[^aeiou]y$', 'vacancy')
<_sre.SRE_Match object at 0x001C1FA8>

vacancy розпізнається цим регулярним виразом тому що закінчується на cy, а c не a, не e, i, o, чи u.

>>> re.search('[^aeiou]y$', 'boy')
>>>
>>> re.search('[^aeiou]y$', 'day')
>>>

boy не розпізнається, тому що закінчується на oy, а ми спеціально наголосили що перед y не повинно йти o. day не розпізнається тому що закінчується на ay.

>>> re.search('[^aeiou]y$', 'pita') >>>

pita не розпізнається тому що взагалі не закінчується на y.

>>> re.sub('y$', 'ies', 'vacancy')
'vacancies'
>>> re.sub('y$', 'ies', 'agency')
'agencies'

Цей регулярний вираз перетворює vacancy в vacancies і agency в agencies, а це якраз те що нам потрібно. Правда варто зауважити що він також перетворить boy на boies, але цього не станеться в функції тому що ми спочатку виконали re.search щоб з’ясувати чи взагалі потрібно викликати re.sub.

>>> re.sub('([^aeiou])y$', r'\1ies', 'vacancy')
'vacancies'

Побічно хочеться вказати на те що можна об’єднати два регулярні вирази (один що перевіряє чи можна застосовувати правило і інший що застосовує правило) в єдиний регулярний вираз. Його можна побачити в коді вище. Більша його частина повинна б бути знайомою, він використовує запам’ятовувану групу, яку ми вивчили в прикладі з аналізом телефонних номерів. Ця група використовується для того щоб запам’ятати символ перед y. Після цього в рядку що підставляється ми використовуємо синтаксис \1, який означає "поклади першу групу яку ми запам’ятали прямо тут". В даному випадку перед y ми запам’ятали c, і коли робиться заміна на місці c так і залишається c, а замість y ставиться ies. (Якщо в нас є більше одної запам’ятовуваної групи, можна використати \2, \3 і так далі.)

Заміни з допомогою регулярних виразів досить гнучкі, і синтаксис \1 робить їх ще гнучкішими. Але об’єднання всієї операції в один регулярний вираз робить його важчим для читання, і не відповідає прямо способу нашого опису правил утворення множини. Ми з самого початку описали правила на зразок "Якщо слово закінчується на S, X, чи Z, потрібно додати ES." А якщо ми подивимось на нашу функцію то ми побачимо два рядки коду в яких написано "Якщо слово закінчується на S, X, чи Z, потрібно додати ES." Це важко виразити більш прямо.


* * *


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

[ред.]

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

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'

Тепер кожен регулярний вираз що визначає чи можна застосовувати правило є окремою функцією що викликає метод re.search().

Кожне застосування правила теж окрема функція яка викликає re.sub() для виконання перетворення.

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

Замість однієї функції (plural()) з багатьма правилами, ми маємо структуру даних з цими правилами, яка є просто послідовністю пар функцій.

def plural(noun):
    for matches_rule, apply_rule in rules:
        if matches_rule(noun):
            return apply_rule(noun)

Так як правила були винесені в окрему структуру даних, нова функція plural() зменшується аж до кількох рядків коду. Використовуючи цикл for можна вибирати з нашої структури по парі правил за раз (одне для визначення можливості застосування, інше для заміни). На першій ітерації циклу, matches_rule отримає значення match_sxz, а apply_rule отримає apply_sxz. На другій ітерації (якщо припустити що до неї дійде), matches_rule отримає значення match_h, а apply_rule - apply_h. Функція обов’язково колись щось поверне, тому що останнє правило (match_default) просто повертає True, що означає що відповідне правило apply_default обов’язково застосується.

Змінна rules є послідовністю пар функцій

Такий підхід працює тому що все в Python - об’єкти, включно з функціями. Структура даних rules місить функції - не імена функцій, а самі функції. Коли відбувається ітерація циклу for - змінні matches_rule та apply_rule стають функціями які можна викликати. На першій ітерації циклу це евівалентно виклику matches_sxz(noun), і якщо він поверне співпадіння, виклику apply_sxz(noun).

Якщо додатковий рівень абстракції вас заплутав, спробуйте розгорнути функцію, щоб побачити еквівалентність. Ввесь цикл 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. Не застосовується? Повернись до першого кроку.

Правила можна описувати будь-де. Функції plural() все одно.

Чи додавання додаткового рівня абстракції було того варте? Ну, поки що ні. Давайте подумаємо що потрібно щоб додати нове правило до функції. В першому прикладі це вимагатиме додавання оператора if в функцію plural(). А зараз це вимагатиме додавання двох функцій, і оновлення послідовності 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)

build_match_and_apply_functions() - функція що будує інші функції динамічно. Вона приймає параметри pattern, search та replace, а потім описує функцію matches_rule() яка викликає re.search() з шаблоном що був переданий функції параметром pattern, і word - параметром функції matches_rule() яку ми конструюємо.

    def apply_rule(word):
        return re.sub(search, replace, word)

Функція що застосовує правило будується аналогічно. Вона приймає один параметр, і викликає re.sub() з параметрами search та replace які передаються функції build_match_and_apply_functions(), та параметром функції apply_rule(), яку ми конструююємо - word. Підхід з використанням значень параметрів зовнішньої функції називається замиканням (англ. closure). По суті, ми описуємо константи всередині функції що будується: вона приймає один параметр, проте використовує додаткові два, значення яких задається при описі функції.

    return (matches_rule, apply_rule)

Ну, і насамкінець, функція build_match_and_apply_functions() повертає пару значень: дві функції які щойно були створені. Константи описані всередині цих функцій (pattern в функції matches_rule() та, search і replace в функції apply_rule()) залишаються разом з функціями навіть після того, як їх повернули назовні функції build_match_and_apply_functions(). Це неймовірно круто.


Якщо це вас заплутало (а воно й повинно було, бо це дуже дивні речі), можливо стане зрозуміліше, коли ви побачите як це використовувати.


patterns = \
  (
    ('[sxz]$',           '$',  'es'),
    ('[^aeioudgkprt]h$', '$',  'es'),
    ('(qu|[^aeiou])y$',  'y$', 'ies'),

Наші "правила" утворення множини тепер задаються як кортеж кортежів рядків (не функцій). Перший рядок в кожній групі - це регулярний вираз який буде використовуватись в re.search() для визначення того чи можна застосовувати правило. Другий і третій рядок в кожній групі - регулярні вирази пошуку й заміни що будуть використовуватись в re.sub() для переведення іменника в множину.

    ('$',                '$',  's')
  )

А тут, в останньому правилі є маленька зміна. В попередньому прикладі, функція match_default() просто повертала True, що означало, що якщо жодне з правил не спрацювало, ми просто додамо s в кінець слова. Цей приклад робить дещо функціонально еквівалентне. Останній регулярний вираз перевіряє чи в слова є кінець ($ співставляється з кінцем рядка). Звичайно, кожен рядок має кінець, навіть порожній, тому пошук такого шаблону завжди дає позитивний результат. Тому, він служить тій же цілі що й функція match_default() яка завжди повертала True: гарантує що якщо жодне з попередніх правил не виконалось, то останнє обов’язково виконається, і додасть s в кінець слова.

rules = [build_match_and_apply_functions(pattern, search, replace)
         for (pattern, search, replace) in patterns]

Це магічний рядок. Він бере послідовність рядків в шаблонах, і перетворює їх в послідовність функцій. Як? Передаючи рядки в функцію build_match_and_apply_functions(). Тобто, бере кожну трійку рядків, і викликає build_match_and_apply_functions() з ними як аргументами. Функція build_match_and_apply_functions() повертає кортеж що містить дві функції. Це означає що змінна rules функціонально еквівалентна попередньому прикладу: список кортежів, в якому кожен кортеж це пара функцій. Перша функція це функція що визначає чи можна застосовувати правило, а друга його застосовує.


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

def plural(noun):
    for matches_rule, apply_rule in rules:
        if matches_rule(noun):
            return apply_rule(noun)

Так як список rules такий самий як і в попередньому прикладі (насправді, він точно такий самий), нічого дивного в тому що функція plural() зовсім не змінилась. Вона цілком загальна, приймає список функцій - правил, і викликає їх по порядку. Їй все одно як ці функції були описані. В попередньому прикладі вони описані як окремі іменовані функції. Зараз вони будуються динамічно, з результатів застосування функції build_match_and_apply_functions() до елементів списку що містить шаблони. Але це не має значення, бо функція plural() все одно працює так само як і раніше.


* * *


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

[ред.]

Ви виокремили ввесь код що дублюється, і додали достатньо абстрацій для того щоб правила утворення множини можна було описувати в списку рядків. Наступним логічним кроком було б взяти ці рядки, і винести їх в окремий файл, де їх можна буде редагувати окремо від коду який ними користується.

Спершу, давайте створимо текстовий файл який міститиме потрібні правила. Ніяких вигадливих структур даних, просто рядки розділені пропусками записані в три колонки. Назвемо цей файл 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)

Функція build_match_and_apply_functions() не змінилась. Ви все ще користуєтесь замикання щоб динамічно створювати пари функцій які використовують змінні передані ззовні.

rules = []
with open('plural4-rules.txt', encoding='utf-8') as pattern_file:

Глобальна функція open() відкриває файл, та повертає файловий об'єкт. В даному випадку, файл що відкривається містить вирази для переведення іменників в множину. Оператор with створює те що називають контекстом: коли блок закінчується, Python автоматично закриє файл, навіть якщо всередині блоку був згенерований виняток. Ви дізнаєтесь більше про блок with та файлові об'єкти у розділі Файли.

    for line in pattern_file:

Вираз for line in <fileobject> читає дані з відкритого файлу, по рядочку за раз, і присвоює текст рядка змінній line. Ви дізнаєтесь більше про читання з файлів у розділі Файли.

        pattern, search, replace = line.split(None, 3)

Кожен рядок в файлів насправді містить три значення, але вони відокремлені табуляцією чи пропусками. Щоб їх розділити використайте метод рядка split(). Першим аргументом у split() передається None, що означає "розділяти по будь-якому порожньму місці, немає значення з пробілів чи табуляцій”. Другий аргумент - 3, і він означає "розділювати тричі, а потім облишити решту рядка. Рядок на зразок [sxz]$ $ es буде розбито на список рядків ['[sxz]$', '$', 'es'], що означатиме що змінній pattern присвоять '[sxz]$', search присвоять '$', а replace - 'es'. Ось так багато робить такий короткий рядок коду.

        rules.append(build_match_and_apply_functions(
                pattern, search, replace))

Ну і нарешті ми передаємо змінні pattern, search, та replace в функцію build_match_and_apply_functions() яка повертає кортеж функцій. Ми додаємо цей кортеж в список правил, і цей список в кінцевому результаті rules містять список функцій який очікує функція plural().


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


* * *


Генератори

[ред.]

Хіба б не було чудово мати загальну функцію 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, 3)
            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 
...

The presence of the yield keyword in make_counter means that this is not a normal function. It is a special kind of function which generates values one at a time. You can think of it as a resumable function. Calling it will return a generator that can be used to generate successive values of x.

Присутність ключового слова yield в make_counter означає що ця функція незвичайна. Це особливий вид функції яка генерує значення по одному за раз. Можете думати про неї як про повторювану функцію. Її виклик повертає генератор який можна використовувати щоб згенерувати послідовні значення x.

>>> counter = make_counter(2)

Щоб створити екземпляр генератора make_counter просто викличте його як звичайну функцію. Зауважте що це насправді не виконує код функції. Ми знаємо про це тому що в першому рядку функції make_counter() ми викликаємо print(), але нічого не надрукувалось.

>>> counter
<generator object at 0x001C9C10>

Функція make_counter() повертає генераторний об'єкт.

>>> next(counter)
entering make_counter
2

Функція next() бере генераторний об'єкт і повертає наступне згенероване значення. Першого разу при виклику next() для генератора, він виконує код в make_counter() аж до першої команди yield, потім повертає значення яке було повернено. В нашому випадку це 2, тому що на самому початку ми створили генератор викликавши make_counter(2).

>>> next(counter)
incrementing x
3

Послідовно викликаючи next() з одним і тим самим генераторним об'єктом продовжує виконання з того місця де воно завершилось і продовжується аж поки не зустрінеться з наступним оператором yield. Всі змінні, локальнимй стан і т.п. зберігаються при виконанні yield і відновлюються при виклику next(). Наступний рядок коду що очікує виконання викликає print(), який друкує збільшуване значення x. Після чого виконується присвоєння x = x + 1. Після чого цикл продовжується, і перший оператор на який ми в ньому натикаємось - це знову yield x, який зберігає стан всього і повертає поточне значення x (тепер вже 3).

>>> next(counter)
incrementing x
4

Коли ми знову викликаємо next(counter), все відбувається так само, але цього разу x має значення 4.


Так як make_counter містить нескінченний цикл, ми можемо в теорії робити це безперервно, і він буде просто збільшувати x та видавати значення. Але давайте замість цього розглянемо більш продуктивне використання генераторів.

Генератор чисел Фібоначчі

[ред.]
yield зупиняє виконання, next() продовжує з того місця на якому зупинились
def fib(max):
    a, b = 0, 1

Послідовність Фібоначі - це послідовність чисел де кожне число є сумою двох попередніх. Вона починається з 0 та 1, починає повільно зростати, а далі зростає все швидше і швидше. Щоб почати послідовність нам потрібно дві змінні зі значеннями 0 та 1.

    while a < max:
        yield a

a - поточне число послідовності, тому ми його повертаємо.

        a, b = b, a + b

b - наступне число послідовності, тому присвоюємо його a, але також обчислюємо наступне значення (a+b) і присвоюємо його b для подальшого використання. Зауважте що це відбувається одночасно: якщо a = 3 а b = 5, тоді a, b = b, a + b присвоїть a значення 5 (попереднє значення b) а b - 8 (сума попередніх значень a та b).

Отож в нас є функція що послідовно випльовує числа Фібоначчі. Звісно, це можна зробити і з рекурсією, але в такий спосіб це простіше читати. Також, це гарно працює з циклами for.


>>> from fibonacci import fib
>>> for n in fib(1000):

Можна використовувати генератори на зразок fib() прямо. Цикл for автоматично буде викликати функцію next() щоб отримувати значення з генератора fib() та присвоювати їх індексу циклу.

...     print(n, end=' ') 
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

Кожного разу в тілі циклу n отримує нове значення з команди yield всередині функції fib(), і все що вам потрібно зробити це його надрукувати. Як тільки в fib() закінчаться числа (a стане більшим за max, яке в даному випадку дорівнює 1000), і цикл передбачливо закінчується.

>>> list(fib(1000)) 
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987]

Це корисна ідіома: передати генератор в функцію list() і вона проітерується по всьому генератору (як і цикл в попередньому прикладі) та поверне список зі всіма значеннями.

Генератор правил утворення множини

[ред.]

Давайте повернемось і поглянемо як працює функція 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, 3)

Тут ніякої магії. Пам'ятаєте що рядки в файлі з правилами містять по три значення розділені пропусками, тому ми можемо використати line.split(None, 3) щоб отримати ці "колонки" та присвоїти їх локальним змінним.

            yield build_match_and_apply_functions(pattern, search, replace)

І ось тут ми використовуємо yield. Яке значення ми викидаємо? Дві функцкії, створені динамічно за допомогою нашого старого друга, функції build_match_and_apply_functions(). Іншими словами, rules() - це генератор що випльовує функції match та apply за вимогою.

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))

Так як rules() - це генератор, його можна використовувати прямо в циклі. При першій ітерації циклу ми викличемо функцію rules(), яка відкриє файл шаблонів, прочитає перший рядок, динамічно створить потрібні функції з шаблонів, і поверне створені функції нам. В наступній ітерації циклу for, ми продовжимо рівно з того місця де ми покинули функцію rules(). Перше що вона зробить - це прочитає наступний рядок з файлу (який все ще відкритий), динамічно збудує наступні функції на основі прочитаних шаблонів, і поверне ці дві функції.


Що ми виграли використовуючи генератори? Час запуску. В попередньому прикладі, коли ми імпортували модуль plural4, він читав ввесь файл шаблонів, і створював список всіх можливих функцій ще тоді коли ми навіть не думали викликати функцію plural(). З генераторами ви можете робити все ліниво: прочитати перше правило, створити функції, спробувати їх, і якщо вони підійдуть не потрібно буде навіть читати решту файла чи створювати будь-які інші функції.

Що ми втратили? Продуктивність! Щоразу як ми викликаємо функцію plural(), генератор rules() починає все з початку, що означає ще одне відкривання файлу з шаблонами, і читання з самого початку, по рядку за раз.

А що якщо б ми могли поєднати краще з обох підходів: мінімальний час запуску (не виконувати ніякий код при імпорті), і максимальну продуктивність (не створювати одні й ті самі функції заново). Ну, і все ще тримати правила в окремому файлі (тому що код це код, а дані це дані), поки звісно не доведеться читати один і той самий рядок вдруге.

Щоб це зробити, нам потрібно буде створити власний ітератор. Але перед цим потрібно буде вивчити класи мови Python.


* * *


Для подальшого читання

[ред.]

Регулярні вирази · Класи та ітератори