Wyzwanie Python #4: Rozwiązanie

Maciej Bartoszuk 4 stycznia 2022
 

Poniżej rozwiązanie do naszego czwartego wyzwania

Funkcja kwadratowa

Zaczynamy od konstruktora, który definiuje trzy pola: a, b, c. Następnie wprowadzamy dwie metody pomocnicze, które, choć nie wpłyną na rozwiązanie równania kwadratowego, wydają się być bardzo naturalnymi składowymi takiej klasy: metoda wypisz(), która po prostu wypisuje funkcję kwadratową na ekran, oraz oblicz_wartosc(), która oblicza wartość funkcji kwadratowej w zadanym punkcie.

Przejdźmy teraz do metody rozwiaz(). Tutaj przede wszystkim musimy dobrze zidentyfikować wszelkie przypadki szczególne oraz ich obsługę. Przyjęliśmy następującą konwencję: funkcja zawsze zwraca dwuelementową krotkę. Gdy jest zero rozwiązań, krotka ma wartości nan, gdy jest ich nieskończenie wiele, wartościami są nieskończoności (inf). Gdy jest jedno rozwiązanie, powtarzamy je dwa razy. Wymieńmy przypadki szczególne:

  • Gdy funkcja jest funkcją stałą, czyli $a=b=0$. Wtedy mamy nieskończenie wiele rozwiązań, gdy $c=0$ oraz ani jednego, gdy $c\neq 0$.
  • Funkcja jest liniowa, gdy $a=0$. Wtedy rozwiązanie jest jedno: $x_0 = x_1 = -\frac{c}{b}$.
  • Funkcja jest kwadratowa, gdy $a\neq 0$. Wtedy jednak nadal mamy parę podprzypadków: gdy $\Delta$ jest mniejsza od zera, nie mamy żadnego rozwiązania. Gdy jest większa lub równa zero, stosujemy odpowiednie wzory na oba rozwiązania (tutaj wpada także przypadek, gdy $\Delta = 0$ i mamy tak naprawdę jedno rozwiązanie).

Aby obliczyć pierwiastek kwadratowy, importujemy pakiet math.

Na końcu main(), w którym testujemy poszczególne przypadki.

import math

class FunkcjaKwadratowa:
    def __init__(self, a, b, c):
        self.a = a
        self.b = b
        self.c = c

    def wypisz(self):
        print(f"{self.a}*x^2+{self.b}*x+{self.c}")

    def oblicz_wartosc(self, x):
        return self.a * x * x + self.b * x + self.c

    def rozwiaz(self):
        if self.a == 0 and self.b == 0:
            if self.c == 0:
                return float("inf"), float("inf")
            else:
                return float("nan"), float("nan")
        if self.a == 0:
            return -self.c/self.b, -self.c/self.b
        delta = self.b**2 - 4*self.a*self.c
        if delta < 0:
            return float("nan"), float("nan")
        return (-self.b-math.sqrt(delta))/(2*self.a), (-self.b+math.sqrt(delta))/(2*self.a)

def main():
    f1 = FunkcjaKwadratowa(2, 3, 1)
    f1.wypisz()
    print(f1.oblicz_wartosc(0))
    print(f1.oblicz_wartosc(1))

    print(FunkcjaKwadratowa(0, 0, 0).rozwiaz())
    print(FunkcjaKwadratowa(0, 0, 1).rozwiaz())
    print(FunkcjaKwadratowa(0, 2, 3).rozwiaz())
    print(FunkcjaKwadratowa(1, 2, 3).rozwiaz())
    print(FunkcjaKwadratowa(1, -5, 6).rozwiaz())
    print(FunkcjaKwadratowa(1, 4, 4).rozwiaz())

if __name__ == "__main__":
    main()

Liczba zespolona

To, co wprowadza nowego to zadanie, to głównie tworzenie i zwracanie jako wynik metody instancji naszej własnej klasy (zwracamy liczbę zespoloną w przypadku metod dodaj() czy mnoz()). Poza tym zwróćmy uwagę na wypisywanie liczby zespolonej: zwracamy uwagę na to, że część urojona może być ujemna i żeby zabezpieczyć się przed potworkami w stylu 2+-3i rozbijamy metodę na dwa przypadki. Cała reszta jest raczej samowyjaśniająca się. W mnoz() przypisaliśmy poszczególne składowe do łatwiejszych w operowaniu zmiennych jednoliterowych. To nie taka rzadka praktyka przy operowaniu na wzorach matematycznych: aby zwiększyć czytelność i zminimalizować możliwość pomyłki, rozbijamy obliczenia na części, których wyniki przypisujemy do zmiennych lub stosujemy aliasy, tak jak tutaj.

class Zespolona:
    def __init__(self, re, im):
        self.re = re
        self.im = im

    def wypisz(self):
        if self.im < 0:
            print(f"{self.re}{self.im}i")
        else:
            print(f"{self.re}+{self.im}i")

    def modul(self):
        return math.sqrt(self.re * self.re + self.im * self.im)

    @staticmethod
    def dodaj(z1, z2):
        return Zespolona(z1.re + z2.re, z1.im + z2.im)

    @staticmethod
    def odejmij(z1, z2):
        return Zespolona(z1.re - z2.re, z1.im - z2.im)

    # (a+bi) * (c+di) = ac-bd + (bc+ad)i
    @staticmethod
    def mnoz(z1, z2):
        a = z1.re
        b = z1.im
        c = z2.re
        d = z2.im
        return Zespolona(a*c-b*d, b*c+a*d)
def main():
    z1 = Zespolona(3, 4)
    z2 = Zespolona(2, 6)
    z1.wypisz()
    print(z1.modul())
    Zespolona.dodaj(z1, z2).wypisz()
    Zespolona.odejmij(z1, z2).wypisz()
    Zespolona.mnoz(z1, z2).wypisz()
if __name__ == "__main__":
   main()

Ułamek

Zacznijmy od skracania. Aby skrócić ułamek, należy znaleźć największy wspólny dzielnik (po polsku “nwd”, a po angielsku “gcd”) licznika i mianownika. Następnie dzielimy licznik i mianownik przez znaleziony największy wspólny dzielnik. Całe szczęście w Pythonie zostało zaimplementowane znajdowanie największego wspólnego dzielnika w funkcji gcd() w pakiecie math. Musimy dokonać dzielenia całkowitoliczbowego (// zamiast /). Nie dlatego, jak poprzednio, aby zaokrąglić wynik do liczby całkowitej, bo wiemy, że w wyniku nie będziemy mieli części ułamkowej. Chcemy, aby dane przechowywane w polach klasy były typu int, a nie float, ponieważ w przeciwnym wypadku nie uda nam się wywołać ponownie, w przyszłości, gcd(), gdyż funkcja ta sprawdza, czy aby na pewno działa na typie int.

W każdym działaniu upewniamy się, że zwracamy skrócony ułamek. Staramy się, aby użytkownik, o ile sam nie stworzy takiego obiektu, nie dostawał w wyniku działania funkcji nieskróconego ułamka. W dodaj() i odejmij() posiłkujemy się zmiennymi pomocniczymi, aby obliczyć liczniki, gdy sprowadzimy ułamki do wspólnego mianownika. To znów ma na celu zwiększenie czytelności oraz zminimalizowanie szansy na błąd.

class Ulamek:
    def __init__(self, licznik, mianownik):
        self.licznik = licznik
        self.mianownik = mianownik

    def wypisz(self):
            print(f"({self.licznik})/({self.mianownik})")

    def skroc(self):
        nwd = math.gcd(self.licznik, self.mianownik)
        self.licznik //= nwd
        self.mianownik //= nwd

    @staticmethod
    def dodaj(u1, u2):
        lewy_licznik = u1.licznik * u2.mianownik
        prawy_licznik = u2.licznik * u1.mianownik
        wynik = Ulamek(lewy_licznik + prawy_licznik, u1.mianownik * u2.mianownik)
        wynik.skroc()
        return wynik

    @staticmethod
    def odejmij(u1, u2):
        lewy_licznik = u1.licznik * u2.mianownik
        prawy_licznik = u2.licznik * u1.mianownik
        wynik = Ulamek(lewy_licznik - prawy_licznik, u1.mianownik * u2.mianownik)
        wynik.skroc()
        return wynik

    @staticmethod
    def mnoz(u1, u2):
        wynik = Ulamek(u1.licznik * u2.licznik, u1.mianownik * u2.mianownik)
        wynik.skroc()
        return wynik

    @staticmethod
    def dziel(u1, u2):
        wynik = Ulamek(u1.licznik * u2.mianownik, u1.mianownik * u2.licznik)
        wynik.skroc()
        return wynik
def main():
    u1 = Ulamek(3, 4)
    u2 = Ulamek(2, 6) # nieskrocony
    u1.wypisz()
    u2.wypisz()
    u2.skroc()
    u2.wypisz()
    print("Dodawanie")
    Ulamek.dodaj(u1, u2).wypisz()
    print("Odejmowanie")
    Ulamek.odejmij(u1, u2).wypisz()
    print("Mnozenie")
    Ulamek.mnoz(u1, u2).wypisz()
    print("Dzielenie")
    Ulamek.dziel(u1, u2).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