Zanurkuj w Pythonie/plural.py, etap 4
Aby definiowanie nowych reguł było prostsze, spróbujemy usunąć z kodu występujące tam powtórzenia.
Przykład 17.9. plural4.py
import re
def buildMatchAndApplyFunctions((pattern, search, replace)):
matchFunction = lambda word: re.search(pattern, word) #(1)
applyFunction = lambda word: re.sub(search, replace, word) #(2)
return (matchFunction, applyFunction) #(3)
- buildMatchAndApplyFunctions to funkcja, której zadaniem jest dynamiczne konstruowanie innych funkcji. Pobiera ona trzy parametry: pattern, search oraz replace (właściwie pobiera jeden parametr będący krotką, ale o tym za chwilę), dzięki którym można zbudować funkcję dopasowującą przy użyciu składni lambda tak, aby pobierała ona jeden parametr (word), a następnie wywoływała re.search z wzorcem (pattern) przekazanym do funkcji buildMatchAndApplyFunctions oraz z parametrem word przekazanym do właśnie budowanej funkcji dopasowującej. Ojej.
- W taki sam sposób odbywa się budowanie funkcji modyfikującej. Funkcja modyfikująca pobiera jeden parametr i wywołuje re.sub z parametrami search i replace przekazanymi do funkcji buildMatchAndApplyFunctions oraz parametrem word przekazanym do właśnie budowanej funkcji modyfikującej. Pokazana tutaj technika używania wartości zewnętrznych parametrów w funkcjach budowanych dynamicznie nosi nazwę dopełnień (ang. closures). W gruncie rzeczy, podczas budowania funkcji modyfikującej, zdefiniowane zostają dwie stałe: funkcja pobiera jeden parametr (word), jednak dodatkowo używa ona dwóch innych wartości (search oraz replace), które zostają ustalone w momencie definiowania funkcji modyfikującej.
- Na końcu funkcja buildMatchAndApplyFunctions zwraca krotkę zawierającą dwie wartości: dwie właśnie utworzone funkcje. Stałe, które zostały zdefiniowane podczas ich budowania (pattern w matchFunction oraz search i replace w applyFunction) pozostają zapamiętane w tych funkcjach, nawet po powrocie z funkcji buildMatchAndApplyFunctions. To szalenie fajna sprawa.
Jeśli jest to wciąż niesamowicie zagmatwane (powinno być, bo rzecz jest złożona), spróbujmy zobaczyć z bliska, jak tego użyć - może się odrobinę wyjaśni.
Przykład 17.10. Ciąg dalszy plural4.py
patterns = \
(
('[sxz]$', '$', 'es'),
('[^aeioudgkprt]h$', '$', 'es'),
('(qu|[^aeiou])y$', 'y$', 'ies'),
('$', '$', 's')
) #(1)
rules = map(buildMatchAndApplyFunctions, patterns) #(2)
- Nasze reguły tworzenia liczby mnogiej są teraz zdefiniowane jako seria napisów (nie funkcji). Pierwszy napis to wyrażenie regularne, które zostanie użyte w funkcji re.search w celu zbadania, czy reguła pasuje do zadanego rzeczownika; drugi i trzeci napis to parametry search oraz replace funkcji re.sub, która zostanie użyta w ramach funkcji modyfikującej do zmiany zadanego rzeczownika w odpowiednią postać liczby mnogiej.
- To jest magiczna linijka. Pobiera ona listę napisów jako parametr patterns, a następnie przekształca je w listę funkcji. W jaki sposób? Otóż przez odwzorowanie listy napisów na listę funkcji przy użyciu funkcji buildMatchAndApplyFunctions, która, tak się akurat składa, pobiera trzy napisy jako parametr i zwraca krotkę zawierającą dwie funkcje. Oznacza to, że zmienna rules będzie miała ostatecznie taką samą wartość, jak w poprzednim przykładzie: listę dwuelementowych krotek, spośród których każda zawiera dwie funkcje: pierwszą jest funkcja dopasowująca, która wywołuje re.search a drugą jest funkcja modyfikująca, która wywołuje re.sub.
Przysięgam, że nie zmyślam: zmienna rules będzie zawierała dokładnie taką samą listę funkcji, jaką zawierała w poprzednim przykładzie. Odwikłajmy definicję zmiennej rules i sprawdźmy:
Przykład 17.11. Odwikłanie definicji zmiennej rules
rules = \
(
(
lambda word: re.search('[sxz]$', word),
lambda word: re.sub('$', 'es', word)
),
(
lambda word: re.search('[^aeioudgkprt]h$', word),
lambda word: re.sub('$', 'es', word)
),
(
lambda word: re.search('[^aeiou]y$', word),
lambda word: re.sub('y$', 'ies', word)
),
(
lambda word: re.search('$', word),
lambda word: re.sub('$', 's', word)
)
)
Przykład 17.12. Dokończenie plural4.py
def plural(noun):
for matchesRule, applyRule in rules: #(1)
if matchesRule(noun):
return applyRule(noun)
- Ponieważ lista reguł zaszyta w zmiennej rules jest dokładnie taka sama, jak w poprzednim przykładzie, nikogo nie powinno dziwić, że funkcja plural nie zmieniła się. Pamiętajmy, że jest ona zupełnie ogólna; pobiera listę funkcji definiujących reguły i wywołuje je w podanej kolejności. Nie ma dla niej znaczenia, w jaki sposób reguły zostały zdefiniowane. Na etapie 2 były to osobno zdefiniowane funkcje nazwane. Na etapie 3 były zdefiniowane jako anonimowe funkcje lambda. Teraz, na etapie 4, są one budowane dynamicznie poprzez odwzorowanie listy napisów przy użyciu funkcji buildMatchAndApplyFunctions. Nie ma to znaczenia; funkcja plural działa cały czas tak samo.
Na wypadek, gdyby jeszcze nie bolała was od tego głowa, przyznam się, że w definicji funkcji buildMatchAndApplyFunctions znajduje się pewna subtelność, o której dotychczas nie mówiłem. Wróćmy na chwilę i przyjrzyjmy się bliżej:
Przykład 17.13. Bliższe spotkanie z funkcją buildMatchAndApplyFunctions
def buildMatchAndApplyFunctions((pattern, search, replace)): #(1)
- Zauważyliście podwójne nawiasy? Funkcja ta tak naprawdę nie przyjmuje trzech parametrów; przyjmuje ona dokładnie jeden parametr będący trzyelementową krotką. W momencie wywoływania funkcji, krotka ta jest rozwijana, a trzy elementy krotki są przypisywane do trzech różnych zmiennych o nazwach pattern, search oraz replace. Jesteście już zagubieni? Zobaczmy to w działaniu.
Przykład 17.14. Rozwijanie krotek podczas wywoływania funkcji
>>> def foo((a, b, c)): ... print c ... print b ... print a >>> parameters = ('apple', 'bear', 'catnap') >>> foo(parameters) #(1) catnap bear apple
- Poprawnym sposobem wywołania funkcji foo jest przekazanie jej trzyelementowej krotki. W momencie wywoływania tej funkcji, elementy krotki są przypisywane różnym zmiennym lokalnym wewnątrz funkcji foo.
Wróćmy na chwilę do naszego programu i sprawdźmy, dlaczego trik w postaci automatycznego rozwijania krotek był w ogóle potrzebny. Zauważmy, że lista patterns to lista krotek, a każda krotka posiada trzy elementy. Wywołanie map(buildMatchAndApplyFunctions, patterns) oznacza, że funkcja buildMatchAndApplyFunctions nie zostanie wywołana z trzema parametrami. Użycie map do odwzorowania listy przy pomocy funkcji zawsze odbywa się przez wywołanie tej funkcji z dokładnie jednym parametrem: każdym elementem listy. W przypadku listy patterns, każdy element listy jest krotką, a więc buildMatchAndApplyFunctions zawsze jest wywoływana z krotką, przy czym automatyczne rozwinięcie tej krotki zostało zastosowane w definicji funkcji buildMatchAndApplyFunctions po to, aby elementy tej krotki zostały automatycznie przypisane do nazwanych zmiennych, z którymi można dalej pracować.