Programowanie funkcyjne a obiektowe

W tej części omówiono programowanie funkcyjne. Na zakończenie warto porównać ten paradygmat programowania. do przedstawionego w poprzedniej części programowania obiektowego. W tym celu posłużymy się zadaniem, którego celem jest znalezienie wyrazów, które najczęściej występują w książce J.R.R. Tolkiena „Władca Pierścieni” tom I, dostęnej jako plik tekstowy.

Problem

Dany jest plik tekstowy lotr1-utf8.txt. Należy:

  • odczytać tekst

  • podzielić go na słowa

  • oczyścić ze znaków interpunkcyjnych

  • zamienić na małe litery

  • odfiltrować krótkie słowa

  • policzyć częstość występowania

  • wypisać 30 najczęstszych

Styl deklaratywny

Najbardziej bezpośrednie rozwiązanie polega na napisaniu kodu krok po kroku:

 1def sort_by_count(key):
 2    return word_counts[key]
 3
 4
 5plik = open("lotr1-utf8.txt", "r")
 6word_counts = {}
 7for line in plik:
 8    tokens = line.split()
 9    for word in tokens:
10        word = word.lower()
11        word = word.strip(":;,.'`[]()-_")
12        if len(word) < 3: continue
13        if word in word_counts:
14            word_counts[word] += 1
15        else:
16            word_counts[word] = 1
17
18counted_items = word_counts.keys()
19result = sorted(counted_items, key=sort_by_count, reverse=True)
20print(result[:30])
21

Charakterystyka

  • jawne pętle for

  • ręczne zarządzanie strukturami danych (słownik)

  • instrukcje warunkowe i zmiana stanu

  • kolejność operacji dokładnie określona

Zalety

  • prostota i czytelność dla początkujących

  • pełna kontrola nad wykonaniem

  • łatwe debugowanie krok po kroku

Wady

  • trudniej wydzielić logiczne etapy przetwarzania

  • kod mniej modularny

Styl obiektowy

W podejściu obiektowym organizujemy kod wokół obiektu, który przechowuje stan i udostępnia operacje:

 1
 2class LiczSlowa:
 3    def __init__(self):
 4        self.__counts = {}
 5
 6    def add(self, word):
 7        word = word.strip(":,;'`.[]()-_").lower()
 8        if word not in self.__counts: self.__counts[word] = 0
 9        self.__counts[word] += 1
10
11    def words_sorted(self, if_reverse=True):
12        counted_items = self.__counts.keys()
13        return sorted(counted_items, key=self.__sort_by_count, reverse=if_reverse)
14
15    def __getitem__(self, word):
16        return self.__counts[word]
17
18    def __sort_by_count(self, key):
19        return self.__counts[key]
20
21
22if __name__ == "__main__":
23
24    licznik = LiczSlowa()
25
26    plik = open("lotr1-utf8.txt", "r")
27    for line in plik:
28        tokens = line.split()
29        for word in tokens:
30            licznik.add(word)
31    print(licznik.words_sorted()[:30])

Charakterystyka

  • dane (słownik) są ukryte wewnątrz klasy

  • operacje są metodami obiektu

  • enkapsulacja (prywatne pole __counts)

  • interfejs: add(), words_sorted()

Zalety

  • dobra organizacja kodu

  • enkapsulacja danych

  • możliwość rozszerzania (np. inne metody analizy)

Wady

  • dużo więcej kodu do napisania

  • operacje są „rozproszone” po metodach

  • trudniej zobaczyć cały przepływ danych

Styl funkcyjny

W podejściu funkcyjnym program jest budowany jako łańcuch transformacji danych. Program ten deklaruje operacje map() transformujące dane oraz filtr pomijający wyrazy zbyt krótkie. Samo zlicznie wykonuje operacja reduce(). Operacje układają się w protokół:

plik → słowa → usunięcie interpunkcji → filtr długości → lower() → zliczanie → sortowanie
 1from functools import reduce
 2
 3
 4def read_words(fname: str):
 5    with open(fname, "r", encoding="utf-8") as f:
 6        for line in f:
 7            for word in line.strip().split():
 8                yield word
 9
10
11punct_to_remove = ",.;:()[]-_"
12def clean_punc(word: str):
13    return word.rstrip(punct_to_remove)
14
15
16# Reducer function
17def count_words(acc, word):
18    acc[word] = acc.get(word, 0) + 1
19    return acc
20
21
22no_punc_words = map(clean_punc, read_words("lotr1-utf8.txt"))
23long_enough = filter(lambda w: len(w)>2, no_punc_words)
24all_lowercase = map(lambda w: w.lower(), long_enough)
25word_counts = reduce(count_words, all_lowercase, {})
26
27top_words = sorted(word_counts.items(), key=lambda item: item[1], reverse=True)[:30]
28
29# Print the top 10 words
30for word, count in top_words:
31    print(f"{word}: {count}")
32

Charakterystyka

  • brak jawnych pętli (zastąpione przez map, filter, reduce)

  • dane przepływają przez kolejne transformacje

  • funkcje są przekazywane jako argumenty

Zalety

  • wyraźne oddzielenie etapów przetwarzania

  • kod bardziej deklaratywny („co” zamiast „jak”)

  • łatwe komponowanie operacji

  • dobre dopasowanie do przetwarzania danych

Wady

  • dla początkujących mniej intuicyjny, np operacja reduce()

  • debugowanie kodu może być trudniejsze

  • nie w pełni idiomatyczny w Pythonie

Podsumowanie

Jak widzisz, każdy z trzech stylów reprezentuje inne podejście do programowania:

  • styl deklaratywny: opis krok po kroku

  • styl obiektowy: organizacja wokół obiektów i stanu

  • styl funkcyjny: transformacje danych przez funkcje