__TOC__

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

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

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

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

Якщо слово закінчується на 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 обов’язково застосується.

Такий підхід працює тому що все в 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() тепер спрощується. Вона бере послідовність правил що описані деінде, і ітерує по них узагальнено:

Візьми наступне правило. Правило можна застосовувати? Тоді застосуй, і поверни результат. Не застосовується? Повернись до першого кроку.

Правила можна описувати будь-де. Функції 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)