Zanurkuj w Pythonie/Tworzenie oddzielnych funkcji obsługi względem typu węzła

Tworzenie oddzielnych funkcji obsługi względem typu węzła

edytuj

Trzecim użytecznym chwytem podczas przetwarzania XML-a jest podzielenie kodu w logiczny sposób na funkcje oparte na typie węzła i nazwie elementu. Parsując dokument przetwarzamy rozmaite typy węzłów, które są reprezentowane przez obiekty Pythona. Poziom główny dokumentu jest bezpośrednio reprezentowany przez obiekt klasy Document. Z kolei Document zawiera jeden lub więcej obiektów klasy Element (reprezentujące znaczniki XML-a), a każdy z nich może zawierać inne obiekty klasy Element, obiekty klasy Text (fragmenty tekstu), czy obiektów Comment (osadzone komentarze w dokumencie). Python pozwala w łatwy sposób napisać funkcję pośredniczącą, która rozdziela logikę dla każdego rodzaju węzła.

Przykład. Nazwy klas parsowanych obiektów XML
>>> from xml.dom import minidom
>>> xmldoc = minidom.parse('kant.xml') #(1)
>>> xmldoc
<xml.dom.minidom.Document instance at 0x01359DE8>
>>> xmldoc.__class__                   #(2)
<class xml.dom.minidom.Document at 0x01105D40>
>>> xmldoc.__class__.__name__          #(3)
'Document'
  1. Załóżmy na moment, że kant.xml jest w bieżącym katalogu.
  2. Jak powiedzieliśmy w podrozdziale "Pakiety", obiekt zwrócony przez parsowany dokument jest instancją klasy Document, która została zdefiniowana w minidom.py w pakiecie xml.dom. Jak zobaczyliśmy w podrozdziale "Tworzenie instancji klasy", __class__ jest wbudowanym atrybutem każdego obiektu Pythona.
  3. Ponadto __name__ jest wbudowanym atrybutem każdej klasy Pythona. Atrybut ten przechowuje napis, a napis ten nie jest niczym tajemniczym, jest po prostu nazwą danej klasy. (Zobacz podrozdział "Definiowanie klas".)

To fajnie, możemy pobrać nazwę klasy dowolnego węzła XML-a (ponieważ węzły są reprezentowane przez Pythonowe obiekty). Jak można wykorzystać tę zaletę, aby rozdzielić logikę parsowania dla każdego typu węzła? Odpowiedzią jest getattr, który pierwszy raz zobaczyliśmy w podrozdziale "Funkcja getattr".

Przykład. parse, ogólna funkcja pośrednicząca dla węzła XML
    def parse(self, node):          
        parseMethod = getattr(self, "parse_%s" % node.__class__.__name__) #(1) (2)
        parseMethod(node) #(3)
  1. Od razu, zauważmy, że konstruujemy dłuższy napis oparty na nazwie klasy przekazanego węzła (jako argument node). Zatem, jeśli przekażemy węzeł Document-u, konstruujemy napis 'parse_Document' itd.
  2. Teraz, jeśli potraktujemy tę nazwę jako nazwę funkcji, otrzymamy dzięki getattr referencję do funkcji.
  3. Ostatecznie, możemy wywołać tę funkcję, przekazując sam node jako argument. Następny przykład przedstawia definicję tych funkcji.
Przykład. Funkcje wywoływane przez funkcję pośredniczącą parse
    def parse_Document(self, node): #(1)
        self.parse(node.documentElement)

    def parse_Text(self, node):    #(2)
        text = node.data
        if self.capitalizeNextWord:
            self.pieces.append(text[0].upper())
            self.pieces.append(text[1:])
            self.capitalizeNextWord = 0
        else:
            self.pieces.append(text)

    def parse_Comment(self, node): #(3)
        pass

    def parse_Element(self, node): #(4)
        handlerMethod = getattr(self, "do_%s" % node.tagName)
        handlerMethod(node)
  1. parse_Document jest wywołany tylko raz, ponieważ jest tylko jeden węzeł klasy Document w dokumencie XML i tylko jeden obiekt klasy Document w przeparsowanej reprezentacji XML-a. Tu po prostu idziemy dalej i parsujemy część główną pliku gramatyki.
  2. parse_Text jest wywoływany tylko na węzłach reprezentujących fragmenty tekstu. Funkcja wykonuje kilka specjalnych operacji związanych z automatycznym wstawianiem dużej litery na początku słowa pierwszego zdania, ale w innym wypadku po prostu dodaje reprezentowany tekst do listy.
  3. parse_Comment jest tylko "przejażdżką"; metoda ta nic nie robi, ponieważ nie musimy się troszczyć o komentarze wstawione w plikach definiującym gramatykę. Pomimo tego, zauważmy, że nadal musimy zdefiniować funkcję i wyraźnie stwierdzić, żeby nic nie robiła. Jeśli funkcja nie będzie istniała, funkcja parse nawali tak szybko, jak napotka się na komentarz, ponieważ będzie próbowała znaleźć nieistniejącą funkcję parse_Comment. Definiując oddzielną funkcję dla każdego typu węzła, nawet jeśli nam ta funkcja nie jest potrzebna, pozwalamy ogólnej funkcji parsującej być prostą i krótką.
  4. Metoda parse_Element jest w rzeczywistości funkcją pośredniczącą, opartą na nazwie znacznika elementu. Idea jest taka sama: weź odróżniające się od siebie elementy (elementy, które różnią się nazwą znacznika) i wyślij je do odpowiedniej, odrębnej funkcji. Konstruujemy napis typu 'do_xref' (dla znacznika <xref>), znajdujemy funkcję o takiej nazwie i wywołujemy ją. I robimy podobnie dla każdej innej nazwy znacznika, która zostanie znaleziona, oczywiście w pliku gramatyki (czyli znaczniki <p>, czy też <choice>).

W tym przykładzie funkcja pośrednicząca parse i parse_Element po prostu znajdują inne metody w tej samej klasie. Jeśli przetwarzanie jest bardzo złożone (lub mamy bardzo dużo nazw znaczników), powinniśmy rozdzielić swój kod na kilka oddzielnych modułów i wykorzystać dynamiczne importowanie, aby zaimportować każdy moduł, a następnie wywołać wszystkie potrzebne nam funkcje. Dynamiczne importowanie zostanie omówione w rozdziale "Programowanie funkcyjne".