Zanurkuj w Pythonie/plural.py, etap 4

Z Wikibooks, biblioteki wolnych podręczników.

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)
  1. 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.
  2. 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.
  3. 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)
  1. 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.
  2. 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)
  1. 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)
  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
  1. 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ć.