TreeStructInfo - API w Pythonie

2

Piszę API dla formatu @furious programming w Pythonie. Projekt wkroczył właśnie w wersję 0.1, czyli posiada funkcjonalność pozwalającą na jego podstawowe użycie (tylko pliki tekstowe, zapis i odczyt tylko przy użyciu stringów). Liczę na jak najwięcej hejtu.

Kod: https://github.com/WizzieP/pyTreeStructInfo

Mini-tutorial: https://github.com/WizzieP/pyTreeStructInfo/wiki/Tutorial

Przede wszystkim czy metoda parsowania musi zajeżdżać aż tak popularnym daniem z makaronu?

0

Super - w końcu coś się dzieje :]

Gdybyś miał jakiekolwiek pytania dotyczące formatu to pytaj śmiało (najlepiej w tym wątku, albo w moim - jak chcesz).

0

Hey, Pythoniści @Shalom, @n0name_l - nie uratujecie biednego nooba przed straszną zmorą nabrania złych nawyków? :( Toż to ino 300 linijek kodu, nie 3000 ^^

3

Ty sobie robisz jaja teraz? Masz tam drabinkę ifów na 200 linijek. Czy twoje pytanie o złe nawyki jest retoryczne czy trollujesz teraz? To wszystko powinny być ładne nowe klasy które tworzysz sobie na podstawie mapy z referencjami do kontruktorów na przykład, albo jeszcze jakoś bardziej elegancko.
Zapytam prosto: czy gdyby możliwych wartości było tam 1000 to też byś je tak wklepywał w tą drabinkę ifów?

1

Jeżeli już tworzysz coś open na githubie to stwórz chociaż 2 branche, **master **i **develop **- w tedy ktoś kto będzie chciał z tego skorzystać, będzie wiedział że stabilna wersja jest na masterze.

0

Tak, robię sobie jaja, trolluję, wszystko co tutaj napisałem było retoryczne, miało na celu obrażenie ciebie i twojego kota. Poza tym jest to część spisku światowego rządu wymierzonego w zasady pisania dobrego kodu. Dzięki za naskakiwanie na mnie, na to liczyłem.

Nie mam pojęcia jak pisać parsery, nigdy tego nie robiłem, co więcej - nie mam pojęcia jak programować. Projekt ten piszę po to, żeby się tego nauczyć, dlatego też wstawiam to tutaj i liczę, że ktoś mi pokaże dlaczego kod jest beznadziejny. Każdy kiedyś zaczynał.

Zapytam prosto: czy gdyby możliwych wartości było tam 1000 to też byś je tak wklepywał w tą drabinkę ifów?

Gdybym wiedział, że można inaczej, to pewnie nie.

Ok, jeśli zdecydowałeś, że chcesz mi pomóc - świetnie! :) Piszesz, że to wszystko powinny być ładne nowe klasy. Nie za bardzo rozumiem, mógłbyś rozwinąć albo podać jakiś przykład? Mam 3 klasy w API, jedna dla pliku, dwie dla elementów w plikach - na tym opiera się reprezentacja danych w pamięci. Rozumiem, że nie o to ci chodzi?

0

@Wizzie nie znam się na pajtonie: ale może pomoże Ci to: http://www.amazon.com/Practical-Object-Oriented-Design-Ruby-Addison-Wesley/dp/0321721330 książka na temat oop - pokazana w rubym ale pierwsze rozdziały pomogą Ci w zrozumieniu jak wyciągnąć logikę do obiektów - bo masz w dużo ifelsów to znaczy że to miejsce można zrefactorować obiektami.

Co do Twojego kodu to na szybko: metoda

 if current_node is None: # we are in the main tree
if l.startswith("node"):
name = l.split(maxsplit=1)[1].strip()
current_node = tsi_elements.TsiNode(name)
master_element.elements.append(current_node)
elif l.startswith("attr"):
quote_index = l.index("\"")
name = l[l.index("attr") + 5:quote_index]
current_attr = tsi_elements.TsiAttr(name, l[quote_index:l.rindex("\"")])
master_element.elements.append(current_attr)
elif l.startswith("\""):

To woła do Ciebie żebyś zrobił klasę Node która ma konstruktor pobierający jakąś wartość np:
zamiast:

if l.startswith("node"):
  name = l.split(maxsplit=1)[1].strip()
  current_node = tsi_elements.TsiNode(name)
  master_element.elements.append(current_node)

możesz tworzyć obiekt który który zrobi to "w tle"

1

@Wizzie chodzi o to żebyś te wszystkie swoje l.startswith() zamienił na wybranie z mapy odpowiedniej klasy a potem każda zawartość twojego ifa powinna być objęta osobną klasą. Jeśli nie rozumiesz tego co napisałem to porwałeś sie z motyką na slońce. Poducz się i wróć do tego później.

0

Dobra, pogooglowałem, potestowałem i chyba mam rozwiązanie, tylko nie wiem, czy nie da się tego zrobić lepiej. Co o tym myślicie?

def parse_data(master_element, path, flags):
    with open(path, flags) as f:
        lines = [l for l in (line.strip(" \t\n") for line in f) if l]  # removing blank lines and side whitespaces
        parser = Parser(master_element)  # obiekt będzie przechowywał wszystkie informacje o parsowanym pliku, potrzebne do prawidłowego parsowania
        keywords = {'node': Parser.parse_node, 'attr': Parser.parse_attr, 'link': Parser.parse_link}  # przykładowa mapa słów kluczowych
        for l i lines:
            for key in keywords:
                if l.startswith(key):
                    keywords[key](parser)  # wywołanie metody będącej wartością klucza 'key' - argument 'parser' to //self//

Wygląda chyba nieco lepiej, niż poprzednie rozwiązanie ^^

1

Prawie.

  1. Nie rób wiecej niż 1 zagłębienia na metodę, chyba że to pętla + jeden if. Dziel to na metody! Np.
for line i lines:
    parse_line(line)
  1. NIE używaj zmiennej l bo kiedyś pomylisz ją z 1 i będzie ból ;)
  2. Teraz wszystko upchnąłeś w biednej klasie Parser. Czemu? Klasa powinna mieć 40-50 linijek max. Metoda powinna mieć mniej niż 10. Zamiast upychać kod w milionie metod klasy Parser upchnij je w osobnych klasach. To przy okazji pomoże ci później gdybyś chciał nie tylko parsować ten format ale też generować...
0
Shalom napisał(a):
  1. Teraz wszystko upchnąłeś w biednej klasie Parser. Czemu? Klasa powinna mieć 40-50 linijek max. Metoda powinna mieć mniej niż 10. Zamiast upychać kod w milionie metod klasy Parser upchnij je w osobnych klasach. To przy okazji pomoże ci później gdybyś chciał nie tylko parsować ten format ale też generować...
    .

Na internetach piszą, żeby nie kodować w Pythonie tak jak w Javie, że wszystko wsadzamy w małe klasy, bo Python is not exclusively class-based, albo even more important is that in Python, you don't use classes for every- thing; if you need factories, singletons, multiple ways to create objects, polymorphic helpers, etc, you use plain functions, not classes or static methods.

Ok, zastępując każdą taką metodę osobną klasą, wyjdzie mi 6-8 singletonów, czy tam klas statycznych, zamiast 6-8 metod, robiących dokładnie to samo. Jakie są zalety takiego rozwiązania? No i każda taka klasa musi mieć referencje do takiej klasy Parser, jaką wyżej opisałem, bo musi mieć informacje o parsowanym pliku, żeby odpowiednio parsowała np. elementy referencjowane.

0

Jasne że nie ma co przesadzać, ale w żadną stronę. Bo nagle naklepiesz w tym pythonie kod rodem z asemblera albo z C...
Gdzie tu singletony? Ja bym z tego parsowanego pliku robił drzewiastą strukturę obiektów a nie żadne singletony. Znów: jeśli upychasz to w 6-8 funkcjach to nijak się to ma do dobrych praktyk. Czy te funkcje mają po 10 linijek max? Nie? No właśnie.
Rzuć okiem na to jak wygląda parser DOM dla XMLa bo gneralnie piszesz coś bardzo podobnego.

3

@Wizzie - ja nie programuję w Pythonie, więc jakichś cudownych przykładów Ci nie podam, jednak pytaj jak najwięcej tych, którzy ten język znają i potrafią w nim pisać eleganckie kawałki kodu;

Przede wszystkim stwórz klasy dla poszczególnych elementów - osobne klasy dla atrybutów, dla węzłów i linków do plików dołączanych; Z tych klas będzie tworzona struktura elementów w pamięci; Klasy te muszą być uniwersalne, więc klasa atrybutu musi być w stanie przechowywać informacje zarówno o standardowym atrybucie, jak i o atrybucie referencjonowanym (z ciałem wydzielonym z głównego ciała drzewa); Tak samo z węzłami - klasa węzła powinna być jedna, ale mogąca reprezentować węzeł standardowy, węzeł referencjonowany, główny węzeł drzewa (to najważniejsze) oraz węzeł wirtualny, czyli główny węzeł drzewa linkowanego z osobnego pliku; Linki do plików dołączanych mogą posiadać tylko jeden zapis i tylko jedną funkcję, więc one nie muszą być uniwersalne;

Jak widzisz uniwersalizm klas jest bardzo ważny; Następnie stwórz klasę drzewa, która zawierać musi kompletną strukturę ww. elementów; I znów uniwersalizm - klasa drzewa musi być w stanie reprezentować drzewo ładowane z pliku tekstowego, jak i wczytywane z pliku binarnego; Dodatkowo, klasa drzewa musi także potrafić reprezentować drzewo linkowane, aby dowolny system konfiguracji móc budować z tylko jednej klasy-wzorca;

Czyli podsumowując - każdy element drzewa musi opisywać tylko jedna klasa, bez rozróżniania typu takiego elementu (standardowy, referencjonowany, główny czy linkowany); Z kolei każde drzewo także musi być instancją jednej klasy; Piszę że musi celowo, dlatego że musi - kod będzie krótki, zwięzły i uniwersalny; Tak jest zbudowane moje API, które bardzo dobrze radzi sobie z przeróżnymi elementami i różnymi drzewami;

Kolejna ważna rzecz - skoro jedna klasa może reprezentować drzewa ładowane ze źródeł tekstowych i binarnych, musisz mieć dwie klasy parserów (parserów to może zbyt uogólnione - po prostu klas przetwarzających źródła), oraz dwie klasy generujące wyjście - tekstowe bądź binarne; Można te funkcjonalności połączyć i stworzyć tylko dwie klasy - jedna klasa ładuje drzewo ze źródła tekstowego lub binarnego (klasa wczytująca), a druga generująca wyjście - tekstowe lub binarne; To już wola programującego, jak wolałem stworzyć cztery klasy, wspomiane wyżej;

Jednak ile być tych klas nie stworzył, to każda powinna przyjmować np. w konstruktorze instancję klasy drzewa, aby móc ją przetworzyć (przy ładowaniu) bądź na jej podstawie wygenerować wyjście; Zarówno klasy ładujące drzewa z plików (ogólnie ze źródeł), jak i klasy zapisujące, muszą działać rekurencyjnie; Rekursja jest nieodzownym sposobem przetwarzania węzłów drzew, ale także przetwarzania drzew dołączanych o dowolnym typie i dowolnym poziomie linkowania; Nie jest to trudne do zrealizowania pod warunkiem, że rozumie się rekurencję i umie się z niej korzystać;

Nie wiem jak wygląda biblioteka standardowa w Pythonie, więc samo parsowanie musisz zaprogramować sam (lub z pomocą forumowiczów), jednak ja pisałem zestaw dedykowanych metod, analizujących pojedyncze linie i wyodrębniające z nich dane, wymagane do tworzenia nowych elementów drzew; Działają szybko, bo nie korzystają z redundantnych funkcji Pos, PosEx czy Copy, ale zajmują więcej linii; Najgorsze było stworzenia funkcji konwersji daty i czasu na łańcuch i vice versa, bo dość dużo przypadków muszą obsługiwać, ale poszło nadwyraz gładko; W Pythonie może to być łatwiejsze, a nawet może być do tego wbudowana funkcja - oby była, będziesz miał mniej roboty; Jednak w RTL Lazarusa był problem z UTF-8, więc trzeba było ją przepisać;

Parsowanie źródła tekstowego sprowadź do jak najprostszej postaci; U mnie główna metoda parsująca wygląda tak:

procedure TTSInfoTextInputReader.ProcessInput();
var
  strLine: AnsiString;
begin
  try
    try
      FTSInfoFile.FCurrentNode := FTSInfoFile.FRootNode;
      PrepareSourceLines();

      if IsCommentMarkerLine(FInput.Objects[FLineIndex]) then
      begin
        ExtractComment();
        AddTreeComment();
      end;

      if IsTreeHeaderLine(FInput[FLineIndex]) then
        AnalyzeTreeHeader()
      else
        ThrowException(EM_MISSING_TREE_HEADER, []);

      while FLineIndex < FInput.Count do
      begin
        strLine := FInput[FLineIndex];

        if IsCommentMarkerLine(FInput.Objects[FLineIndex]) then
          ExtractComment()
        else
          if IsAttributeLine(strLine) then AddAttribute() else
          if IsNodeLine(strLine)      then AddNode()      else
          if IsEndNodeLine(strLine)   then CloseNode()    else
          if IsLinkLine(strLine)      then AddLink()      else
          if IsEndTreeLine(strLine)   then Break
          else
            ThrowException(EM_INVALID_SOURCE_LINE, [strLine]);
      end;
    except
      FTSInfoFile.DamageClear();
    end;
  finally
    FTSInfoFile.FCurrentNode := FTSInfoFile.FRootNode;
    FTSInfoFile.FModified := False;
  end;
end;

Pole FTSInfoFile w klasie parsera zawiera referencję do instancji klasy uzupełnianego drzewa;

Najpierw ustawia się wskaźnik aktualnego węzła na główny węzeł drzewa; Następnie przygotowuje się źródło - metoda PrepareSourceLines usuwa wcięcia i puste linie, szuka frazy zakańczającej główne ciało drzewa i przenosi definicje elementów referencjonowanych w miejsca ich deklaracji; Źródłowe linie są przygotowane, więc w pierwszej kolejności trzeba spawdzić, czy drzewo posiada swój komentarz i jeśli tak - wyodrębnia się go i dodaje do odpowiedniego pola w klasie drzewa; Następnie parsuje się nagłówek i ekstrahuje opcjonalną nazwę drzewa; To tyle z rzeczy opcjonalnych - następnie to tylko pętelka po wszystkich liniach aż do końca głównego ciała drzewa; Każda iteracja wypakowuje opcjonalny komentarz elementu, a następnie w krótkiej drabince ifów następuje rozpoznanie elementu i jego dodanie do dorzewa; Jak coś jest błędnie zapisane - wyjątek; Linia nierozpoznana - wyjątek; W razie wystąpienia wyjątku drzewo jest czyszczone awaryjnie (metoda DamageClear) i usuwana jest flaga ffWrite ze zbioru, aby drzewo nie zostało przypadkowo zapisane do pliku, podczas gdy nie mogło zostać poprawnie wczytane; Na koniec ustawienie wskaźnika na aktualnie otwarty węzeł (pole FCurrentNode) na główny węzeł drzewa i ustawienie flagi pamiętającej modyfikacje na False; Tyle - drzewo w pamięci gotowe do molestowania;

Tutaj kończą się podstawy formatu - jeżeli Twoje API będzie sobie bezbłędnie radzić z drzewami, wypchanymi elementami standardowymi, przyjdzie czas na referencjonowanie elementów i linkowanie drzew z innych plików; Parsowanie źródła i generowanie wyjścia (tylko tekstowych) z wydzielonymi ciałami elementów referencjonowanych nie jest nie wiadomo jak trudne, jednak z poczatku wydaje się cholernie zagmatwane; Rekurencja w według mnie nie da prostoty implementacji, dlatego skorzystałem z fajnego sposobu znaczenia linii elementów referencjonowanych, które następnie są przesuwane w miejsca ich deklaracji, lub przesuwane poza główne ciało drzewa spod swoich deklaracji, podc zas generowania wyjścia; Stosowane są dwie metody - iteracyjna dla przetwarzania źródła lub tworzenia wyjścia, oraz rekurencyjna, dla przenoszenia definicji przed parsowaniem źródła i przenoszenia definicji elementów po wygenerowaniu wyjścia tekstowego; Obsługujące to metody nie są skomplikowane - dość łatwo je zapisać;

No i tyle, jeśli chodzi o pliki tekstowe; Ich przetwarzanie jest trudniejsze i bardziej czasochłonne, niż pliki binarne; Te drugie z kolei tworzone są tak łatwo, że kod klas ładujących drzewa ze źródeł binarnych i generujących wyjście binarne jest bardzo krótki; Oczywiście bez rekurencji się nie obejdzie, ale brak tam jakiegokolwiek parsowania, więc przy ładowaniu pliku binarnego wystarczy od razu pobierać ze strumienia porcje danych i na ich podstawie tworzyć elementy w pamięci; Nad poprawnością danych w drzewach musi czuwać klasa drzewa, więc podczas ładowania pliku ze źródła binarnego jedyne co trzeba sprawdzić to sygnaturę pliku; Zaś generowanie pliku binarnego na podstawie instancji klasy drzewa jest jeszcze szybsze, bo już w ogóle nic nie trzeba sprawdzać; Dlatego obsługa plików binarnych powinna być o wiele szybsza, niż ich tekstowych odpowiedników; W przypadku drzew bez elementów referencjonowanych powinien być to zysk około 40%, przy dłuższych i bardziej złożonych drzewach (z elementami referencjonowanymi) zysk powinien być jeszcze większy (muszę w końcu zrobić ten benchmark, bo te dane są trochę z d**y - badałem to podczas pisania parserów);

W każdym razie pliki binarne nie powinny dla nikogo stanowić problemu - są beznadziejnie proste w obsłudze, ale właśnie o to chodziło; Dobrze by było, aby zmiana formy drzewa była jak najprostsza; Jeżeli komuś zachce się zmienić formę drzewa (np. z tekstowej na binarną, bo mu konfiguracja mocno spuchła), niech mam możliwość uczynienia tego w jak najprostszy sposób; W moim API wystarczy zmienić zestaw flag drzewa, np. tak:

var
  tsiConfig: TSimpleTSInfoFile;

{...}

tsiConfig := TTSInfoFile.Create('C:\File.tsinfo', [ffLoadFile, ffTextFile, ffWrite]);

{..}

{ zmiana formy pliku z tekstowej na binarną }
tsiConfig.FileFlags := [ffLoadFile, ffBinaryFile, ffWrite];

i tyle - w destruktorze klasy drzewa użyty zostanie generator binarny, więc i drzewo zostanie zapisane w postaci binarnej; Przy odczycie drzewa z pliku wystarczy użyć innej flagi;


Jak widzisz całość nie jest trudna do zaprogramowania, jednak dopóki nie zobaczysz rozwiązania - może się to wydawać skomplikowane i bardzo trudne do zaprogramowania; Rozpisałem się, więc na koniec dodam, że jeżeli chcesz coś wiedzieć na temat formatu czy sposobów przetwarzania drzew to pytaj - z chęcią udzielę wskazów, bo swoje API znam na pamięć; Dodatkowo, jeśli będziesz chciał, możesz korzystać z dokumentacji do mojego API; Język nie gra roli, bo każdy element mojej biblioteki jest szczegółowo opisany w dokumentacji, wraz z podanym kodem (jeśli tym elementem jest metoda, funkcja, procedura); Możesz też korzystać z tutoriala do mojego API - wyczaisz jak z niego można korzystać i jak sam możesz ustalić sposoby obsługi klasy drzewa; Obsługa musi być prosta i przyjemna dla użytkownika, więc jest się nad czym zastanawiać.

0

Ok, większość z tego co opisałeś rozwiązałem już, część w identyczny sposób, część w inny. Jak widzę moim problemem było to, że kompletnie nie używałem funkcji i metod w parsowaniu, przez co wszystko wygląda tragicznie, zajmuje zbyt wiele kodu i ciężko jest coś w tym pozmieniać. Co do sposobu trzymania danych w programie, to rozwiązałem to pewnie w podobny sposób: klasa TsiFile zawiera w sobie drzewo, czy de facto listę elements, która może zawierać obiekty klas TsiAttr i TsiNode, TsiNode ma identyczną listę. Dodatkowo podczas gmerania w takim drzewie, programu nie obchodzi, czy TsiNode jest referencjowany, czy linkowany - zajmują się tym 3 pola tych klas - ref, link, link_flags. Więc przy zapisie sprawdzam sobie tylko:

if element.ref:
    # zapisuję linijkę 'ref attr...' itd.
if element.link:
    # zapisuję linijkę 'link "element.link" as "element.name" flags...

To mi chyba wyszło, wątpię, żeby można to było jakoś inaczej, lepiej rozwiązać.

@furious programming Nie znam się na Delphi, ale twoje funkcje, np. AddAttribute(), nie biorą jako parametru strLine, czy coś? Jeśli tak, to u mnie po refaktoryzacji będzie to wyglądać trochę podobnie. ProcessInput() wykorzystujesz także przy czytaniu linii poza głównym drzewem (referencjowanych)? Czy masz na to inną metodę?


@Shalom

Ja bym z tego parsowanego pliku robił drzewiastą strukturę obiektów a nie żadne singletony.

Jeszcze raz, bo się rozminęliśmy. Pisząc o singletonach, myślałem, że rozmawiamy cały czas o kodzie na parsowanie pliku tekstowego, a nie na przechowywanie go w pamięci - bo to mam zrobione. Problemem jest tutaj mój poryty kod na właśnie parsowanie go z pliku do tego drzewa w pamięci. Do tej pory cały mój kod wyglądał tak, jak widziałeś i słusznie krytykowałeś, czyli drabinka ifów powielona właściwie 2 lub 3 razy, bo w mojej implementacji linijka np. "ref attr" mogła być w dwóch miejscach w pliku - w głównym drzewie (deklaracja) i poza nim (definicja) :)

Teraz chce ten kod wsadzić w jakieś sensowne opakowania, żeby dało się to czytać i rozwijać. Zaproponowałem coś takiego, że mam obiekt klasy Parser, który przechowuje informacje o pliku potrzebne do jego prawidłowego parsowania (czyli np., że obecnie parsujemy elementy referencjonowane poza głównym drzewem, a nie w nim, czyli zajmujemy się definicjami, a nie deklaracjami), a jego metody są wywoływane przy znalezieniu odpowiedniego słowa kluczowego w pliku. Metody te z kolei przetwarzają tekst na drzewo w pamięci naszej klasy TsiFile.
Czyli, zakładając że nasz plik to sama rama drzewa i jeden atrybut:
Plik Tekstowy -> obiekt TsiFile -> konstruktor obiektu wywołuje parse_text_file() -> tam tworzę obiekt Parser -> parsuje header -> napotykam linijkę zaczynającą się 'attr' -> metoda parse_attr() klasy Parser tworzy obiekt TsiAttr i wsadza go w nasze drzewo w pamięci obiektu TsiFile -> tadam, mamy gotowe drzewo w pamięci.
Ty natomiast napisałeś, że zamiast w metodach mam kod upchać w klasach. Ja nie widzę w tym głębszego sensu, skoro kodu nawet przybędzie, a wszystko będzie dużo bardziej zagmatwane. Chyba, że się rozminęliśmy i pisałeś o czymś innym.

1

napotykam linijkę zaczynającą się 'attr' -> metoda parse_attr() klasy Parser tworzy obiekt TsiAttr i wsadza go w nasze drzewo
A czemu nie:
napotykam linijkę zaczynającą się 'attr' ->tworzę obiekt TsiAttr na bazie danej linii i wsadzam go w nasze drzewo
Tzn czemu TsiAttr nie ma wiedzieć w jaki sposób parsować (a także wypisywać później) linie ze sobą związane?

0

O tym właśnie dzisiaj myślałem, ale stwierdziłem, że przecież tyle instancji, a w każdej dodatkowa metoda będą pobierać dużo pamięci. Jednak się myliłem :)

Ok, dzięki za pomoc - zabieram się za przepisywanie tego potwora ^^

0

Jak widzę moim problemem było to, że kompletnie nie używałem funkcji i metod w parsowaniu, przez co wszystko wygląda tragicznie, zajmuje zbyt wiele kodu i ciężko jest coś w tym pozmieniać.

Klasy powinny zawierać krótko zdefiniowane metody, odpowiedzialne jedynie za jedną funkcję, np. sprawdzenie czy linia zawiera deklarację atrybutu; Jeżeli nie podzielisz procesu parsowania na mniejsze części, to się w kodzie pogubisz;

klasa TsiFile zawiera w sobie drzewo, czy de facto listę elements, która może zawierać obiekty klas TsiAttr i TsiNode, TsiNode ma identyczną listę.

TSI to akronim (Tree Structure Information), więc powinieneś go pisać wielkimi literami - mała wskazówka;

Źle to robisz, jeśli ja z kolei dobrze rozumiem to co napisałeś; Drzewo powinno zawierać tylko i wyłącznie jedną instancję klasy węzła, czyli główny węzeł drzewa, tworzony zawsze, nawet jeśli drzewo jest zupełnie puste; Natomiast klasa węzła powinna już posiadać listę elementów potomnych, a nawet trzy listy, uzupełniane według hierarchii ważności elementów; Jeżeli elementy najniższego stopnia zagłębienia (czyli umieszczone bezpośrednio w głównym ciele drzewa) będziesz trzymał w liście, a nie w węźle - pożegnaj się z uniwersalną metodą przetwarzającą węzły rekurencyjnie; Dlaczego? Bo będziesz musiał napisać osobno metodę przetwarzającą lub zapisującą elementy zawarte bezpośrednio w głównym ciele drzewa, oraz osobną, zapisującą elementy zawarte w węzłach potomnych;

Z drugiej strony, jeśli klasa węzła posiada trzy listy dla elementów trzech typów, klasy parsujące czy generujące mogą sortować te listy według wtypów elementów; W sumie bez sortowania, bo np. metoda dodająca nowy atrybut do drzewa korzysta z listy atrybutów - inne listy są tej metodzie niepotrzebne; Trzy listy dają kolejny plus obsługi, bo z góry wiadomo jakie elementy znajdują się na danej liście; Jeżeli szukasz węzła, to oczywiste jest, że w liście węzłów potomnych nie znajdują się atrybuty czy linki - dzięki temu odpada Ci sprawdzanie jakiej klasy są iterowane elementy i rzutowanie przy operacjach na nich;

Niby wiele problemów z tego nie wynika, ale nadłożysz kodu, aby powiązać jedną metodę parsującą lub generującą węzły z drzewami dołączanymi;

To mi chyba wyszło, wątpię, żeby można to było jakoś inaczej, lepiej rozwiązać.

Nie wiem czy dobrze do tego podchodzisz, bo łączysz funkcjonalność różnych elementów w jednej klasie;

U mnie stan referencjonowania to pole FReferenced typu Boolean, które istnieje tylko w klasach TTSInfoAttribute i TTSInfoNode, bo tylko te elementy mogą być referencjonowane; klasa TTSInfoLink nie posiada informacji o referencjonowaniu (bo linki nie mogą być referencjonowane), ale i nie posiada flag pliku dołączanego; Z racji tej, że klasa drzewa posiada zestaw flag potrzebnych do identyfikacji formy drzewa i sposobu korzystania z niego, klasa linku korzysta właśnie z tego zbioru, nie posiadając zdublowanego zbioru;

Nie znam się na Delphi, ale twoje funkcje, np. AddAttribute(), nie biorą jako parametru strLine, czy coś?

Metody z prefiksem Is otrzymują w parametrze aktualnie analizowaną linię źródłową; Metody z prefiksem Add jej nie pobierają, a w zamian korzystają z pola FLineIndex; Taki zabieg jest raczej konieczny, dlatego że atrybuty i węzły potomne mogą/muszą być wieloliniowe, więc trzeba by podawać ewentualnie listę linii danego elementu, a to na poziomie analizy źródła jest niemożliwe lub trudne do implementacji (wprowadzi zamieszanie); Metody z Add operują bezpośrednio na polu FLineIndex i liście źródłowej, na bieżąco inkrementując indeks aktualnie przetwarzanej linii; Muszą to robić, bo główna pętla w metodzie ProcessInput nie wie czy rozpoznany element jest jednoliniowy czy wieloliniowy, dlatego sama także wykorzystuje pole FLineIndex; Dzięki temu wieloliniowość deklaracji i definicji elementów nie stanowi żadnego wyzwania/problemu;

Polecam taką kolejność parsowania pliku tekstowego:

  1. przygotuj źródło:

  2. usuń puste linie,

  3. usuń wcięcia,

  4. znajdź linię z frazą kluczową end tree - bez jej istnienia nie będzie możliwe poprawne znalezienie przeniesienie definicji elementów referencjonowanych,

  5. przenieś definicje elementów referencjonowanych w miejsca ich deklaracji;

  6. oznacz pierwsze linie komentarzy znacznikami - jeżeli tego nie zrobisz, nie będziesz w stanie poprawnie rozdzielić komentarzy elementów referencjonowanych, jeżeli taki element posiada komentarz deklaracji i komentarz definicji

  7. sprawdź, czy na początku pliku znajduje się komentarz:

  8. jeśli tak - wyodrębnij go i dodaj do pola z głównym komentarzem drzewa

  9. sprawdź, czy linia zawiera nagłówek drzewa:

  10. sprawdź poprawność zapisu nagłówka:

  11. jeśli jest poprawnie zapisany - wypakuj nazwę drzewa do odpowiedniego pola (jeżeli nazwa drzewa istnieje w nagłówku)
    To jest pierwsza-przygotowawcza faza procesu parsowania źródła, operująca na danych dynamicznych (opcjonalnych); Teraz faza budowania struktury drzewa w pamięci:

  12. w pętli, aż do końca źródła:

  13. sprawdź czy linia zawiera komentarz (sprawdź wcześniej nadany linii znacznik):

  14. jeśli linia zawiera komentarz - wypakuj go do pomocniczej np. macierzy:

  15. wypakuj pierwszy komentarz,

  16. sprawdź, czy po nim znajduje się kolejny komentarz,

  17. jeśli tak - także wypakuj go do pomocniczej np. macierzy

  18. spawdź typ elementu:

  19. jeśli to atrybut - dodaj go do listy atrybutów aktualnie uzupełnianego węzła (w przypadku uzupełniania listy elementów osadzonych bezpośrednio w głównym ciele drzewa, musisz usupełnić główny węzeł drzewa - jak widzisz zastosowanie głównego węzła drzewa jest nieodzowne);

  20. jeśli to węzeł - dodaj go do listy węzłów potomnych aktualnie uzupełnianego węzła i od razu przejdź do niego (ustaw wskaźnik aktualnie uzupełnianego węzła na ten, który właśnie dodałeś);

  21. jeśli to linia zamykająca węzeł - ustaw wskaźnik aktualnie uzupełnianego węzła na rodzic węzła aktualnie uzupełnianego (klasa węzła musi posiadać wskazanie na instancję klasy swojego węzła-rodzica);

  22. jeśli to link - dodaj go do listy linków aktualnie uzupełnianego węzła, przy czym klasa linku powinna utworzuć także pustą klasę drzewa linkowanego i od razu rekurencyjnie wywołać parser, który załaduje drzewo linkowane do pamięci;

  23. jeśli to linia zamykająca ciało drzewa - wyjdź z pętli, to koniec parsowania;
    Rozumiesz teraz działanie procesu prasowania źródła?

Ty natomiast napisałeś, że zamiast w metodach mam kod upchać w klasach. Ja nie widzę w tym głębszego sensu, skoro kodu nawet przybędzie, a wszystko będzie dużo bardziej zagmatwane. Chyba, że się rozminęliśmy i pisałeś o czymś innym.

Chyba źle mnie zrozumiałeś;

Klasy elementów muszą zawierać tylko i wyłącznie informacje o samych sobie; Klasa węzła musi posiadać wskazanie na węzeł w którym się znajduje, a także listy własnych elementów potomnych; klasy elementów nie powinny posiadać kodu odpowiedzialnego operacje na tych elementach, np. kodu do zmiany nazwy elementu, sprawdzenia poprawności identyfikatora itd.; Taki kod powinna posiadać klasa parsera (aby móc ze zględów bezpieczeństwa przerwać proces parsowania) oraz klasa drzewa (aby uniemożliwić stworzenie elementu z błędną nazwą); Tego typu algorytmy moga być wspólne, bo robią dokładnie to samo; U mnie metoda ValidIdentifier jest wspólna i wydzielona do osobnego modułu, zawierającego kod uniwersalny dla klas całej biblioteki (nie wszystkich, ale kilku, więc nie muszę go replikować);

Klasę parsera podziel na metody, odpowiedzialne tylko za pojedyncze funkcje - o tym pisałem w poprzednim poście; Zobacz jak podzielona jest moja klasa parsera - wszelkie jej metody są względnie krótkie (góra jednoekranowe), dzięki temu są czytelne; Poza tym jedna metoda parsera może być wykorzystywana w kilku innych z tej samej klasy (np. ExtractComment, wypakowująca komentarze elementów dowolnego typu, czy choćby MarkComments), więc oszczędza się pisania - są uniwersalne w obrębie danej klasy;

PS: Te znaczniki, za pomocą których oznaczam komentarze muszą mieć różne wartości; Inaczej nie będzie możliwości odróżnić komentarzy w sytuacji, gdy referencjonowany element posiada tylko jeden komentarz; Przy generowaniu wyjścia nie będziesz mógł odróżnić typu komentarza - czy to był komentarz deklaracji elementu, czy może definicji;

Dlatego też znaczniki komentarzy u mnie mają różne wartości:

const
  DECLARATION_COMMENT_MARKER = Integer($DEC);
  DEFINITION_COMMENT_MARKER  = Integer($DEF);

Nawet po ich heksadecymalnej wartości można poznać, jakiego typu komentarz reprezentują (dobrze się złożyło).

0

Ty natomiast napisałeś, że zamiast w metodach mam kod upchać w klasach. Ja nie widzę w tym głębszego sensu, skoro kodu nawet przybędzie, a wszystko będzie dużo bardziej zagmatwane. Chyba, że się rozminęliśmy i pisałeś o czymś innym.

Chyba źle mnie zrozumiałeś;

To było skierowane do postu Shaloma, stąd znacznik @Shalom ^^

Źle to robisz, jeśli ja z kolei dobrze rozumiem to co napisałeś; Drzewo powinno zawierać tylko i wyłącznie jedną instancję klasy węzła, czyli główny węzeł drzewa, tworzony zawsze, nawet jeśli drzewo jest zupełnie puste; Natomiast klasa węzła powinna już posiadać listę elementów potomnych, a nawet trzy listy, uzupełniane według hierarchii ważności elementów; Jeżeli elementy najniższego stopnia zagłębienia (czyli umieszczone bezpośrednio w głównym ciele drzewa) będziesz trzymał w liście, a nie w węźle - pożegnaj się z uniwersalną metodą przetwarzającą węzły rekurencyjnie; Dlaczego? Bo będziesz musiał napisać osobno metodę przetwarzającą lub zapisującą elementy zawarte bezpośrednio w głównym ciele drzewa, oraz osobną, zapisującą elementy zawarte w węzłach potomnych;

Z drugiej strony, jeśli klasa węzła posiada trzy listy dla elementów trzech typów, klasy parsujące czy generujące mogą sortować te listy według wtypów elementów; W sumie bez sortowania, bo np. metoda dodająca nowy atrybut do drzewa korzysta z listy atrybutów - inne listy są tej metodzie niepotrzebne; Trzy listy dają kolejny plus obsługi, bo z góry wiadomo jakie elementy znajdują się na danej liście; Jeżeli szukasz węzła, to oczywiste jest, że w liście węzłów potomnych nie znajdują się atrybuty czy linki - dzięki temu odpada Ci sprawdzanie jakiej klasy są iterowane elementy i rzutowanie przy operacjach na nich;

Niby wiele problemów z tego nie wynika, ale nadłożysz kodu, aby powiązać jedną metodę parsującą lub generującą węzły z drzewami dołączanymi;

Python jest kaczo i dynamicznie typowany, rzutowanie i wszystkie inne zabawy odpadają. TSIFile mogę traktować jak TSINode w tej samej metodzie - obie klasy mają pola name i listę elements. Wygląda jak kaczka, więc jest kaczką ^^

0

To było skierowane do postu Shaloma, stąd znacznik @Shalom ^^

Chyba że tak - zwracam honor :]

Python jest kaczo i dynamicznie typowany, rzutowanie i wszystkie inne zabawy odpadają. TSIFile mogę traktować jak TSINode w tej samej metodzie - obie klasy mają pola name i listę elements. Wygląda jak kaczka, więc jest kaczką ^^

To całe "kacze typowanie" raczej nie jest rozwiązaniem czy usprawiedliwieniem błędnego projektowania kodu;

Jeżeli węzeł będzie posiadać tylko jedną listę elementów potomnych, na której znajdują się elementy trzech typów - atrybuty, węzły i linki - to w jaki sposób chcesz podczas generowania wyjścia sprawnie je zapisać w odpowiedniej kolejności? Bez ich posortowania się nie obejdzie, a to dodatkowy czas pożre; Inna sytuacja - pobierasz ścieżkę do elementu, np. do atrybutu, do którego musisz zapisać nową wartość; Najpierw przebijasz się przez zagnieżdżone węzły potomne, aby dotrzeć do wskazanego atrybutu; Jeżeli będziesz miał jedną listę - zmarnujesz czas na odsiewanie elementów innego typu niż węzły; A na koniec znów stracisz czas na znalezienie wskazanego atrybutu, bo znajduje się na liście z elementami innego typu;

W przypadku posiadania trzech dedykowanych list nie tracisz czasu na operacje nie mające sensu; Szukasz najpierw kolejnych węzłów tylko w listach węzłów, a na koniec szukasz atrybutu na liście zawierającej tylko atrybuty; Oszczędność czasu będzie duża, a tym bardziej w przypadku, gdy w drzewie znajduje się tysiące elementów; Gorzej będzie, jeśli kolejność elementów w węzłach będzie pomieszana, czyli niezgodna z ich hierarchią ważności; To nie jest błąd składni, więc pomieszane deklaracje są poprawne w chwili parsowania, ale jeszcze bardziej wydłuży się czas wyszukiwania elementów, po stworzeniu DOMu w pamięci;

Ja widzę Twoje rozwiązanie jako nieoptymalne - to tak jakbyś chciał kupić w supermarkecie gazetę, ale zaczynał jej szukanie od stoiska z wędliną, zamiast od razu pójść pod odpowiedni regał i tylko w nim szukać;

Tak więc lepiej zastanów się nad konstrukcją kodu już teraz, bo później będzie dużo do poprawiania :]

napotykam linijkę zaczynającą się 'attr' ->tworzę obiekt TsiAttr na bazie danej linii i wsadzam go w nasze drzewo

Nie tak szybko - atrybut może być zapisany także na wielu liniach, np. jeśli posiada komentarz (lub dwa komentarze, w przypadku atrybutu referencjonowanego), a także może posiadać wartość wieloliniową, więc na podstawie jednej linii wiele nie zdziałasz; Czyli przykładowy standardowy atrybut może być zapisany tak:

:: first comment line
:: second comment line
attr Some Attribute "first value line"
                    "second value line"
                    "..."
                    "last value line"

Chyba że najpierw utworzysz sobie element w drzewie (w pamięci), a następnie sprawdzisz kolejne linie i ewentualnie kolejne dane dodasz do nowego elementu już bez jego tworzenia.

0

to w jaki sposób chcesz podczas generowania wyjścia sprawnie je zapisać w odpowiedniej kolejności?

Myślałem, że mam zapisywać w takiej kolejności, w jaką twórca pliku sobie zaplanował. Dlaczego miałbym to zmieniać?

0

Dlatego, że ustalona kolejność według hierarchii ważności elementów zwiększa czytelność plików konfiguracyjnych i ułatwia ich analizę;

Jeżeli jeden (dowolny) węzeł będzie posiadać tyle elementów potomnych, że ich zapis zajmować będzie np. kilka(naście) ekranów - od razu wiadomo gdzie szukać danego elementu; Jeżeli szukasz (wzrokiem) w takim węźle np. atrybutu, to od razu wiesz, aby rozpocząć szukanie od początku ciała węzła; Jeżeli szukasz linku do pliku dołączanego - szukasz go od końca ciała tego węzła;

Nie tracisz czasu na sprawdzenie całej zawartości węzła, bo na podstawie hierarchii ważności, cała zawartość jest posortowana; Posortowana według typów elementów, nie ich nazw; Natomiast użytkownik bądź programista sam określa kolejność elementów jednego typu, które nie powinny być sortowane według identyfikatorów - API powinno zapisać elementy jednego typu w takiej samej kolejności, w jakiej zostały wczytane lub utworzone w drzewie;

Poza tym, taka kolejność jest konieczna (choć są wyjątki, ale implementacyjne - dotyczące parserów) w przypadku elementów referencjonowanych; Jeśli pomylisz ich kolejność, możesz uniemożliwić ich rekurencyjne przetwarzanie i wywalić parser;


Polecam nie skupiać się nad sensem składni formatu, a na poprawnej implementacji API; Wymyślałem i sprawdzałem wiele wariantów, więc obecna forma jest najlepszym rozwiązaniem na stworzenie czytelnych drzew konfiguracyjnych, łatwych do analizy nawet przez nieinformatyków.

1 użytkowników online, w tym zalogowanych: 0, gości: 1