Wyzwanie Python #5: Rozwiązanie

Maciej Bartoszuk 5 stycznia 2022
 

Poniżej rozwiązanie do naszego piątego wyzwania

Figury geometryczne

W tym zadaniu tak naprawdę w dużej mierze korzystamy z hierarchii klas dotyczącej zwierząt. Najpierw tworzymy klasę abstrakcyjną Figura, która jedynie nadaje nam pewne ramy: wiemy, że klasa dziedzicząca po figurze powinna umieć zwrócić swoją nazwę, policzyć swój obwód oraz pole. Co więcej, mamy kod, który jest wspólny dla wszystkich klas dziedziczących: polega on na wypisaniu informacji o figurze, jej nazwy oraz pola i obwodu. M.in. w tym celu definiuje się takie abstrakcyjne klasy: aby to do nich wynieść wspólny kod i nie powtarzać go w każdej klasie dziedziczącej z osobna. Następnie definiujemy kolejne figury. Sprowadza się to do definiowania odpowiedniego konstruktora, który przyjmuje wymiary danej figury. W przypadku koła współrzędne środka nie są tak naprawdę potrzebne, liczy się tylko promień. Jednak klasa przechowująca zaledwie promień wydała nam się dość uboga, stąd decyzja o dodaniu współrzędnych środka (x i y). W przypadku trójkąta korzystamy ze wzoru Herona, rzadko używanego na lekcji matematyki, a który pozwala na obliczenie pola na podstawie samych długości boków.

Pytanie o relację dziedziczenia prostokąta i kwadratu było podchwytliwe. Tak naprawdę nie ma tu dobrej odpowiedzi. Na lekcjach matematyki zawsze mówi nam się, że kwadrat jest przypadkiem szczególnym prostokąta. Tak więc naturalnym wydaje się, że to kwadrat powinien dziedziczyć z klasy prostokąta. W innych językach programowania, gdzie funkcja przyjmuje argumenty odpowiedniego typu, funkcja, która przyjmuje prostokąt, powinna przyjąć także kwadrat. Bo wszędzie tam, gdzie pasuje prostokąt, powinien pasować także kwadrat. To wszystko są argumenty za kwadratem dziedziczącym z prostokąta. Jednak można spojrzeć na tę kwestię z innego punktu widzenia: kwadrat przechowuje jedno pole: długość boku. Prostokąt przechowuje dwa pola: dwie różne długości boków. Z tego punktu widzenia prostokąt rozszerza kwadrat, dodając jedno pole. W ten sposób dochodzimy do konkluzji, że to prostokąt powinien dziedziczyć po kwadracie. I takie właśnie rozwiązanie przyjęliśmy, tym bardziej, że w języku Python nie podajemy dokładnie, jakiego typu oczekujemy np. w sygnaturze funkcji. Za to chcieliśmy jeszcze raz zilustrować wywołanie konstruktora klasy bazowej, przy okazji prezentując to mniej intuicyjne podejście.

W main() testujemy poszczególne klasy. Jednak najciekawszą częścią jest stworzenie listy figur. Następnie piszemy jedną pętlę, która przetwarza figury i korzysta z utworzonego przez nas interfejsu: każdy element listy, niezależnie od tego, czy jest kwadratem czy kołem, ma metody pole() i obwod(). Bardzo ułatwia to napisanie zwięzłego, ogólnego kodu, a to właśnie do tego służy programowanie obiektowe.

Na koniec mała uwaga: w wypisz() wypisujemy obwód i pole. Sprawia to, że taka dość niewinna metoda wypisująca podstawowe informacje zmusza do wykonania pewnych obliczeń. Obliczenia w tym wypadku są raczej dość proste i mało obciążające, jednak w przypadku bardziej zaawansowanych obliczeń powinniśmy unikać takich rozwiązań. Jednak tu chcieliśmy głównie zilustrować użycie na poziomie klasy abstrakcyjnej wywołania metod abstrakcyjnych, których implementacji jeszcze nie znamy.

from abc import ABC, abstractmethod
import math

class Figura(ABC):
    @abstractmethod
    def nazwa(self):
        pass

    def wypisz(self):
        print(f"Jestem {self.nazwa()}. Moj obwod: {self.obwod()}, a pole: {self.pole()}.")

    @abstractmethod
    def obwod(self):
        pass

    @abstractmethod
    def pole(self):
        pass
class Trojkat(Figura):
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    def nazwa(self):
        return "trójkąt"

    def obwod(self):
        return self.a + self.b + self.c

    def pole(self):
        p = self.obwod()/2
        return math.sqrt(p*(p-self.a)*(p-self.b)*(p-self.c))
class Kolo(Figura):
    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        self.r = r

    def nazwa(self):
        return "koło"

    def obwod(self):
        return 2 * math.pi * self.r

    def pole(self):
        return math.pi * self.r ** 2
class Kwadrat(Figura):
    def __init__(self, a):
        self.a = a

    def nazwa(self):
        return "kwadrat"

    def obwod(self):
        return 4 * self.a

    def pole(self):
        return self.a ** 2
class Prostokat(Kwadrat):
    def __init__(self, a, b):
        super().__init__(a)
        self.b = b

    def nazwa(self):
        return "prostokąt"

    def obwod(self):
        return 2 * (self.a + self.b)

    def pole(self):
        return self.a * self.b
def main():
    t = Trojkat(3.0, 4.0, 5.0)
    t.wypisz()
    k = Kolo(4, 5, 1)
    k.wypisz()
    kw = Kwadrat(3)
    kw.wypisz()
    p = Prostokat(4, 5)
    p.wypisz()

    lista_figur = [t, k, kw, p]
    suma_pol = 0
    suma_obwodow = 0
    for f in lista_figur:
        suma_pol += f.pole()
        suma_obwodow += f.obwod()

    print(suma_pol)
    print(suma_obwodow)

if __name__ == "__main__":
    main()

Struktura wyrażeń arytmetycznych

Główna zasada jest dalej ta sama: jedna klasa abstrakcyjna, a następnie parę dziedziczących po niej. Dodaliśmy metodę wypisz(), której celem jest wypisanie, jak po kolei wykonują się obliczenia. To w niej wywołujemy wartosc(), która oblicza wartość danego węzła. Wypisywanie rozbiliśmy na część wspólną, czyli wypisanie nazwy działania, które będzie używane w większości klas dziedziczących (poza klasą Liczba), i to ona jest w klasie abstrakcyjnej, oraz resztę wypisywania, które odbywa się w każdej klasie dziedziczącej z osobna (wypisanie argumentów, jeśli są, oraz znak działania).

Najciekawszą częścią tego zadania jest fakt, że poszczególne klasy mogą mieć w polach (takich jak czynnik czy skladnik) inne instancje klas dziedziczących po Wezel. Wszystkie one udostępniają ten sam interfejs: wartosc() i wypisz(). Dlatego możemy napisać tak ogólny kod, który nie musi dokładnie wiedzieć, czy np. składnikami dodawania są pojedyncze liczby, czy może wyniki mnożenia. Zauważmy, że aby zachować ten poziom ogólności, musieliśmy “opakować” pojedyncze liczby w klasę Liczba, aby mieć ten sam interfejs.

Zauważmy, że wypisz() nie jest napisane wydajnie: gdy wywołujemy wypisz() poszczególnych składowych, zmuszamy je, aby obliczyły swoją wartość. Zaraz potem jeszcze raz zmuszamy je do obliczenia tej wartości, gdy wywołujemy na nich wartosc(). Aby zrobić to wydajnie, musielibyśmy stworzyć metodę wypisz_i_zwroc_wartosc(), która jednocześnie wypisywałaby informację o węźle i zwracała jego wartość.

Na koniec dodajmy, że wartosc() jest swego rodzaju rekurencją, ale bardziej wysublimowaną, niż zwykłe obliczanie silni: aby obliczyć wartość w węźle, trzeba rekurencyjnie obliczyć wartości jego dzieci.

from abc import ABC, abstractmethod
import math
class Wezel(ABC):
    @abstractmethod
    def nazwa(self):
        pass

    def wypisz(self):
        print(f"Wykonuję {self.nazwa()}.", end=' ')

    @abstractmethod
    def wartosc(self):
        pass

class Liczba(Wezel):
    def __init__(self, liczba):
        self.liczba = liczba

    def nazwa(self):
        return "liczba"

    def wypisz(self):
        print(f"Jestem liczbą {self.liczba}")

    def wartosc(self):
        return self.liczba
class Suma(Wezel):
    def __init__(self, skladnik1, skladnik2):
        self.skladnik1 = skladnik1
        self.skladnik2 = skladnik2

    def nazwa(self):
        return "dodawanie"

    def wypisz(self):
        self.skladnik1.wypisz()
        self.skladnik2.wypisz()
        super().wypisz()
        print(f"{self.skladnik1.wartosc()}+{self.skladnik2.wartosc()}={self.wartosc()}")

    def wartosc(self):
        return self.skladnik1.wartosc() - self.skladnik2.wartosc()
class Roznica(Wezel):
    def __init__(self, odjemna, odjemnik):
        self.odjemna = odjemna
        self.odjemnik = odjemnik

    def nazwa(self):
        return "odejmowanie"

    def wypisz(self):
        self.odjemna.wypisz()
        self.odjemnik.wypisz()
        super().wypisz()
        print(f"{self.odjemna.wartosc()}-{self.odjemnik.wartosc()}={self.wartosc()}")

    def wartosc(self):
        return self.odjemna.wartosc() - self.odjemnik.wartosc()
class Iloczyn(Wezel):
    def __init__(self, czynnik1, czynnik2):
        self.czynnik1 = czynnik1
        self.czynnik2 = czynnik2

    def nazwa(self):
        return "mnożenie"

    def wypisz(self):
        self.czynnik1.wypisz()
        self.czynnik2.wypisz()
        super().wypisz()
        print(f"{self.czynnik1.wartosc()}*{self.czynnik2.wartosc()}={self.wartosc()}")

    def wartosc(self):
        return self.czynnik1.wartosc() * self.czynnik2.wartosc()
class Iloraz(Wezel):
    def __init__(self, dzielna, dzielnik):
        self.dzielna = dzielna
        self.dzielnik = dzielnik

    def nazwa(self):
        return "dzielenie"

    def wypisz(self):
        self.dzielna.wypisz()
        self.dzielnik.wypisz()
        super().wypisz()
        print(f"{self.dzielna.wartosc()}/{self.dzielnik.wartosc()}={self.wartosc()}")

    def wartosc(self):
        return self.dzielna.wartosc() / self.dzielnik.wartosc()
class Silnia(Wezel):
    def __init__(self, liczba):
        self.liczba = liczba

    def nazwa(self):
        return "silnia"

    def wypisz(self):
        self.liczba.wypisz()
        super().wypisz()
        print(f"{self.liczba.wartosc()}!={self.wartosc()}")

    def wartosc(self):
        return math.factorial(self.liczba.wartosc())
def main():
    minus_jeden = Liczba(-1)
    cztery = Liczba(4)
    piec = Liczba(5)
    siedem = Liczba(7)
    osiem = Liczba(8)

    dodawanie = Suma(piec, siedem)
    odejmowanie = Roznica(osiem, cztery)
    mnozenie = Iloczyn(dodawanie, odejmowanie)
    dzielenie = Iloraz(mnozenie, minus_jeden)
    silnia = Silnia(dzielenie)

    silnia.wypisz()
if __name__ == "__main__":
    main()

Maciej Bartoszuk

Ukończył z wyróżnieniem informatykę na wydziale Matematyki i Nauk Informacyjnych Politechniki Warszawskiej, gdzie aktualnie pracuje w zakładzie Sztucznej Inteligencji i Metod Obliczeniowych. Tam też od 2013 roku prowadzi zajęcia dydaktyczne z programowania w R, Pythonie, C/C++, C#. Uczestnik studiów doktoranckich w Instytucie Podstaw Informatyki Polskiej Akademii Nauk w latach 2013-2015. W 2018 roku obronił doktorat z wyróżnieniem na swoim rodzimym wydziale: System do oceny podobieństwa kodów źródłowych w językach funkcyjnych oparty na metodach uczenia maszynowego i agregacji danych, który obejmuje zarówno algorytmy przetwarzania kodów źródłowych programów, jak i data science. Współautor książki Przetwarzanie i analiza danych w języku Python wydanej przez PWN. Ponadto trener na bootcampach Data Science, gdzie uczy programować w języku Python pod kątem analizy danych.
Komentarze
Ostatnie posty
Data Science News #204
Data Science News #203
Data Science News #202
Data Science News #201