Python - wprowadzenie

15 - Klasy i obiekty

Python jest językiem obiektowym. Jak wcześniej wspominałem, w zasadzie wszystko w Pythonie jest obiektem: liczby, listy, słowniki, funkcje itd. Zatem od pierwszej lekcji używaliśmy obiektów w nawet najprostszych pisanych przez nas programach, ale były to obiekty typów zaprojektowanych przez innych programistów. Przyszedł czas, aby nauczyć się tworzyć obiekty według własnego pomysłu.

Pierwszym krokiem jest stworzenie klasy, która jest czymś w rodzaju projektu, na podstawie którego tworzone są obiekty, które są egzemplarzami klasy. Obiekty mogą przechowywać dane, a także wykonywać kod, który jest umieszczony w metodach. Metody są funkcjami, które są częściami klasy.

Klasy można umieszczać w osobnych plikach (modułach), zwłaszcza jeśli są bardzo rozbudowane, ale wiele klas może być także umieszczonych w jednym module.

Zrozumienie czym są obiekty i klasy, jest kluczowym krokiem pozwalającym na efektywne programowanie obiektowe. Możemy sobie wyobrazić, że obiekt w naszym programie przypomina rzeczywisty obiekt z otaczającego nas świata. Zresztą często obiekty w programach im odpowiadają. Na przykład telefon komórkowy, z którego korzystasz, jest konkretnym egzemplarzem pewnej klasy obiektów (telefonów komórkowych). Posiada określoną markę, ma swój numer seryjny, a także numer przypisany do znajdującej się w nim karty SIM, przechowuje numery telefonów znajomych i innych osób, z którymi się kontaktujemy, ma zainstalowane różne aplikacje. Ma wiele cech wspólnych z moim telefonem komórkowym, ale też pewne różnice, np. inny numer, listę kontaktów, aplikacje itd. Te cechy i przechowywane informacje możemy porównać do danych przechowywanych w obiektach, które tworzymy w programie. Telefony komórkowe mogą też wykonywać pewne czynności, np. dzwonienie, odtwarzanie muzyki, przeglądanie internetu, wysyłanie SMS-ów itd. Podobnie obiekty w naszych programach zwykle mogą wykonywać pewne operacje za pomocą zdefiniowanych metod.

Klasa z kolei opisuje pewien typ obiektów (np. telefony komórkowe). Jest więc czymś w rodzaju “projektu” na podstawie którego tworzy się konkretne obiekty, które są osobnymi bytami. W klasie możemy zdefiniować przechowywane przez obiekty rodzaje informacji, a także operacje, które obiekty będą wykonywać. Najpierw więc tworzymy projekt (klasę), a potem na jej podstawie konkretne obiekty.

Tworzenie klas i egzemplarzy klas (obiektów)

Zacznijmy od stworzenia bardzo prostej klasy:

class MojaKlasa():
    pass

Po słowie kluczowym class znajduje się nazwa klasy. Nazwy klas wg. konwencji, zaczynają się od dużej litery, następne litery są małe, z wyjątkiem początkowych liter kolejnych słów tworzących nazwę klasy. Zwykle nie stosujemy podkreślników (_) między kolejnymi słowami. Po nazwie klasy znajduje się para nawiasów (w tym przypadku pustych), po których znajduje się dwukropek.

Podobnie jak w przypadku funkcji, czy pętli, blok kodu przynależny klasie jest wcięty. Słowo kluczowe pass zostało umieszczone, ponieważ nasza klasa jest pusta.

Teraz stwórzmy obiekt (egzemplarz klasy) na podstawie utworzonej klasy i sprawdźmy, jakiego jest typu:

pierwszy_obiekt = MojaKlasa()
print(type(pierwszy_obiekt))
<class '__main__.MojaKlasa'>

Może się wydawać, że tak prosta klasa jest właściwie bezużyteczna. Spróbuj jednak wykonać kod:

pierwszy_obiekt.nazwa = 'Mój pierwszy obiekt.'
pierwszy_obiekt.dane = [12, 32, 17, 9, 23]
print(pierwszy_obiekt.nazwa)
print(pierwszy_obiekt.dane)
Mój pierwszy obiekt.
[12, 32, 17, 9, 23]

Właśnie przypisaliśmy atrybuty do obiektu. Podkreślam, nie są one w tym przypadku przypisane do klasy (nawiasem mówiąc, można przypisać też atrybuty klasie), ale właśnie do konkretnego obiektu. Żeby to sprawdzić, utwórzmy kolejny obiekt klasy MojaKlasa i przypiszmy mu własne atrybuty:

drugi_obiekt = MojaKlasa()
drugi_obiekt.nazwa = 'Mój drugi obiekt'
drugi_obiekt.dane = {'numer': 89,
                     'kolor': 'czerwony',
                     'liczby': (21, 34, 66, 1)}
drugi_obiekt.skomplikowany = False
# Sprawdzamy atrybuty drugiego obiektu 
# Uwaga: Konieczny Python 3.8 lub wyższy
print(f"{drugi_obiekt.nazwa = }")
print(f"{drugi_obiekt.dane = }")
print(f"{drugi_obiekt.skomplikowany = }")
# Sprawdzamy atrybuty pierwszego obiektu
print(f"{pierwszy_obiekt.nazwa = }")
print(f"{pierwszy_obiekt.dane = }")
# Tego atrybutu nie przypisaliśmy do pierwszego obiektu
print(f"{pierwszy_obiekt.skomplikowany = }")
drugi_obiekt.nazwa = 'Mój drugi obiekt'
drugi_obiekt.dane = {'numer': 89, 'kolor': 'czerwony', 'liczby': (21, 34, 66, 1)}
drugi_obiekt.skomplikowany = False
pierwszy_obiekt.nazwa = 'Mój pierwszy obiekt.'
pierwszy_obiekt.dane = [12, 32, 17, 9, 23]

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)

<ipython-input-10-7b144c6b6cfa> in <module>
     14 print(f"{pierwszy_obiekt.dane = }")
     15 # Tego atrybutu nie przypisaliśmy do pierwszego obiektu
---> 16 print(f"{pierwszy_obiekt.skomplikowany = }")

AttributeError: 'MojaKlasa' object has no attribute 'skomplikowany'

Jak widać, oba obiekty przechowują różne dane, nawet jeśli atrybuty nazwaliśmy tak samo. Utworzenie atrybutu dla jednego z obiektów, nie spowodowało utworzenia go dla drugiego. Jak widać, choć zostały stworzone na podstawie tej samej klasy, są osobnymi, niezależnymi ,,bytami’'.

Może się wydawać, że powyższy sposób tworzenia klas i obiektów, dla przechowywania danych jest całkiem sprytny i użyteczny. Jednak, jeśli chcemy wyłącznie przechowywać dane w obiektach, to lepiej (m. in. ze wzgledu na zużywane zasoby komputera i szybkośc działania) stosować odpowiednie struktury danych, np. listy czy słowniki które poznaliśmy, lub inne których nie omówilismy, ale są dostępne w odpowiednich modułach i bibliotekach (np. w module collections Biblioteki Standardowej Pythona).

Klasy i tworzone na ich podstawie obiekty stają się zdecydowanie bardziej użyteczne, kiedy dodamy do nich metody, pozwalające na wykonywanie określonych zadań.

Utwórzmy inną klasę:

class Sekwencja():
    """Przechowuje dane sekwencji i wykonuje na niej proste operacje."""
    
    def drukuj_informacje(self):
        print(f"Sekwencja: {self.sekwencja}")

Klasa ma na razie jedną metodę drukuj_informacje(), zauważ, że pierwszym i jej jedynym argumentem, jest self. Musi się on znaleźć w każdej definicji metody, jeśli metoda ma być wywoływana na obiekcie. Jeśli parametrów jest więcej, to self powinien być wymieniony jako pierwszy. Nie trzeba przekazywać żadnej wartości dla argumentu self, dzieje się to automatycznie i umożliwia powiązanie z konkretnym obiektem. Zauważ, że kiedy w metodzie drukuj_informacje() odwołujemy się do atrybutu sekwencja, to także poprzedziliśmy jego nazwę prefiksem self, ponieważ ten atrybut jest również powiązany z danym obiektem.

Utwórzmy zatem obiekt, przypiszmy do niego atrybut sekwencja i wywołajmy metodę drukuj_informacje:

sekw = Sekwencja()
sekw.sekwencja = 'AAGGATC'
sekw.drukuj_informacje()
Sekwencja: AAGGATC

W zasadzie wszystko działa, ale sprawdźmy, co się stanie, jeśli programista zapomni utworzyć atrybutu sekwencja i przypisać mu wartości:

sekw = Sekwencja()
sekw.drukuj_informacje()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)

<ipython-input-4-05fd225bf71d> in <module>
      1 sekw = Sekwencja()
----> 2 sekw.drukuj_informacje()   

<ipython-input-2-1f62621aebf0> in drukuj_informacje(self)
      3 
      4     def drukuj_informacje(self):
----> 5         print(f"Sekwencja: {self.sekwencja}")  

AttributeError: 'Sekwencja' object has no attribute 'sekwencja'

Jeśli projektujemy taką klasę jak powyżej, to zapewne naszym celem jest utworzenie obiektów, które będą przechowywać zapis sekwencji zasad czy aminokwasów oraz zapewne dodatkowych danych i wykonywać na nich jakieś operacje. Zatem sensowniej by było tak ją zaprojektować, aby już przy jej tworzeniu, wymusić podanie odpowiednich danych. Przyda nam się w tym celu specjalna metoda __init__() (zwróć uwagę na dwa podkreślniki na początku i końcu nazwy). Metoda o takiej nazwie jest uruchamiana przy tworzeniu obiektu i jest ona wykorzystywana na przykład do ustawiania atrybutów i ich wartości. Zmodyfikujmy zatem naszą klasę, tak aby przy tworzeniu jej egzemplarza zostały utworzone atrybuty sekwencja, ident (określająca identyfikator sekwencji) oraz dlugosc (dlugość sekwencji) i przypisane im wartości. Wartości dwu pierwszych podamy jako parametry, tworząc obiekt, dla trzeciego wartość zostanie wyliczona. Uzupełnijmy także metodę drukuj_informacje().

class Sekwencja():
    """Przechowuje dane sekwencji i wykonuje na niej proste operacje.""" 
        
    def __init__(self, sekwencja, ident):
        self.sekwencja = sekwencja
        self.ident = ident
        self.dlugosc = len(self.sekwencja)
        
    def drukuj_informacje(self):
        print(f"Sekwencja: {self.sekwencja}")
        print(f"Nazwa:     {self.ident}")
        print(f"Długość:   {self.dlugosc}")

Teraz utwórzmy obiekt, podając niezbędne dane i użyjmy go:

sekw = Sekwencja('AGCTAAG', 'seq_1')
sekw.drukuj_informacje()
Sekwencja: AGCTAAG
Nazwa:     seq_1
Długość:   7

Zauważ, że teraz nie możemy utworzyć egzemplarza klasy Sekwencja nie podając wymaganych atrybutów:

sekw = Sekwencja()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

<ipython-input-11-e5fbc31ea396> in <module>
----> 1 sekw = Sekwencja()

TypeError: __init__() missing 2 required positional arguments: 'sekwencja' and 'ident'

Metody dostępowe - getters i setters

Dodajmy kolejne metody do klasy. Jedną zwracającą sekwencję, drugą pozwalającą na zmianę sekwencji, trzecią zwracającą odwróconą sekwencję i czwartą podającą długość sekwencji. Metody zwracające wartość, zazwyczaj zaczynają się od get (dlatego nazywa się je w jęz. angielskim getters), po czym następuje nazwa danej, która zostaje zwrócona. Co prawda, połączenie angielskiego prefiksu z polską nazwą atrybutu wygląda trochę dziwnie, ale pozostańmy przy tej zasadzie, aby trzymać się konwencji. Analogicznie, nazwy metod służących ustawianiu nazw atrybutów, zaczynają się od set (setters). Chociaż sama zmiana sekwencji jest możliwa, jeśli bezpośrednio odwołamy się do atrybutu sekwencja, jednak w tym przypadku lepiej jest to robić za pomocą metody, która przy okazji zmieni atrybut dlugosc. Z kolei ,,getter’’ dla długości sekwencji pozwoli, przed jej zwróceniem sprawdzić, czy na pewno długość jest właściwa. Mogłoby tak nie być, gdyby np. po utworzeniu obiektu z użyciem danej sekwencji, została ona zmieniona na inną, o innej długości, przez bezpośrednią zmianę wartości atrybutu sekwencja. Jeśli długość nie będzie się zgadzać, zostanie przy okazji ustawiona właściwa wartość atrybutu. Takie rozwiązanie nie chroni oczywiście przed pobraniem bezpośrednio nieprawidłowej wartości przez odwołanie się do atrybutu dlugosc.

class Sekwencja():
    """Przechowuje dane sekwencji i wykonuje na niej proste operacje.""" 
        
    def __init__(self, sekwencja, ident):
        """ Inicjalizacja obiektu, ustawienie wartości atrybutów."""
        self.sekwencja = sekwencja
        self.ident = ident
        self.dlugosc = len(self.sekwencja)
        
    def drukuj_informacje(self):
        """Drukuje wartości atrybutów."""
        print(f"Sekwencja: {self.sekwencja}")
        print(f"Nazwa:     {self.ident}")
        print(f"Długość:   {self.get_dlugosc()}")
   
    def get_sekwencja(self):
        """Zwraca sekwencję."""
        return self.sekwencja
    
    def set_sekwencja(self, sekwencja):
        """Ustawia wartość atrybutów: sekwencja, dlugosc."""
        self.sekwencja = sekwencja
        self.dlugosc = len(self.sekwencja)
    
    def get_odwrocona(self):
        """Zwraca odwróconą sekwencję."""
        return self.sekwencja[::-1]
    
    def get_dlugosc(self):
        """Zwraca długość sekwencji."""
        # Sprawdzenie, czy długość jest właściwa, jeśli nie to zostaje 
        # ustawiona właściwa wartośc parametru slugosc
        if self.dlugosc != len(self.sekwencja):
            self.dlugosc = len(self.sekwencja)
        return self.dlugosc
            
sekw = Sekwencja('AGCTAAG', 'seq_1')
sekw.drukuj_informacje()
print(f"Sekwencja odwrócona: {sekw.get_odwrocona()}")
# Zmieniamy bezpośrednio wartość atrybutu sekwencja
sekw.sekwencja = 'AGCTAAGAAT'
print(f"Sekwencja po zmianie: {sekw.get_sekwencja()}")
# Odwołujemy się bezpośrednio do atrybutu dlugosc
print(f"Długość pobrana z atrybutu: {sekw.dlugosc}")
# Pobieramy długość z metody:
print(f"Długość pobrana z metody: {sekw.get_dlugosc()}")
Sekwencja: AGCTAAG
Nazwa:     seq_1
Długość:   7
Sekwencja odwrócona: GAATCGA
Sekwencja po zmianie: AGCTAAGAAT
Długość pobrana z atrybutu: 7
Długość pobrana z metody: 10

Właściwości i dekoratory

Powyższe rozwiązanie, z użyciem getterów i setterów, nie uchroniło nas przed możliwością bezpośredniej modyfikacji atrybutu sekwencja. Lepszym, a także bardziej ,,Pythonowym’’ sposobem jest użycie właściwości (ang. properties). Właściwości mają cechy zarówno atrybutu, jak i metody. To znaczy, przechowują dane tak jak atrybuty, ale zmieniamy i pobieramy je za pomocą odpowiednich metod, działających analogicznie do getterów i setterów.
W celu utworzenia właściwości można użyć dekoratorów. Nie wchodząc w szczegóły, przyjmijmy, że dekorator to funkcja, która modyfikuje działanie innej funkcji. Usuniemy z nazw funkcji get_ i set_ w zamian zaopatrzymy je w odpowiednie dekoratory.

Zmodyfikujmy kod naszej klasy:

class Sekwencja():
    """Przechowuje dane sekwencji i wykonuje na niej proste operacje.""" 
        
    def __init__(self, sekwencja, ident):
        """ Inicjalizacja obiektu, ustawienie wartości atrybutów."""
        self.__sekwencja = sekwencja
        self.__ident = ident
        self.__dlugosc = len(self.__sekwencja)
        
    def drukuj_informacje(self):
        """Drukuje wartości atrybutów."""
        print(f"Sekwencja: {self.__sekwencja}")
        print(f"Nazwa:     {self.__ident}")
        print(f"Długość:   {self.__dlugosc}")
    
    # To będzie działało jak getter
    @property
    def sekwencja(self):
        """Zwraca sekwencję."""
        return self.__sekwencja
    
    # To będzie działało jak setter
    @sekwencja.setter
    def sekwencja(self, sekwencja):
        """Ustawia wartość atrybutów: sekwencja, dlugosc."""
        self.__sekwencja = sekwencja
        self.__dlugosc = len(self.__sekwencja)
        
    @property
    def ident(self):
        return self.__ident
        
    @property
    def odwrocona(self):
        """Zwraca odwróconą sekwencję."""
        return self.__sekwencja[::-1]
    
    @property
    def dlugosc(self):
        """Zwraca długość sekwencji."""
        # Sprawdzenie, czy długość jest właściwa, jeśli nie to zostaje 
        # ustawiona właściwa wartośc parametru slugosc
        if self.__dlugosc != len(self.__sekwencja):
            self.__dlugosc = len(self.__sekwencja)
        return self.__dlugosc

Teraz klasa nie posiada już getterów i setterów. Będziemy się odwoływać do parametrów podobnie jak wcześniej bezpośrednio do atrybutów. Będą jednak wywoływane odpowiednie metody, tak jak było to w przypadku getterów i setterów. Zwróć też uwagę, że zmieniliśmy wewnętrzne nazwy atrybutów, dodając (wg. konwencji) przed ich nazwą __ (dwa podkreślniki). Do pewnego stopnia ,,ukryliśmy’’ atrybuty przed ich bezpośrednią, przypadkową modyfikacją.

sekw = Sekwencja('AGCTAAG', 'seq_1')
sekw.drukuj_informacje()
print(f"Sekwencja odwrócona: {sekw.odwrocona}")
# Zmieniamy wartość atrybutu sekwencja
sekw.sekwencja = 'AGCTAAGAAT'
print(f"Sekwencja po zmianie: {sekw.sekwencja}")
print(f"Długość: {sekw.dlugosc}")
Sekwencja: AGCTAAG
Nazwa:     seq_1
Długość:   7
Sekwencja odwrócona: GAATCGA
Sekwencja po zmianie: AGCTAAGAAT
Długość: 10

Dziedziczenie i klasy potomne

Warto by dodać do naszej klasy, kolejne metody. Taką, która zwracałaby sekwencję komplementarną, oraz komplementarną, odwróconą. W tym miejscu natrafiamy jednak na pewien problem. Określenie sekwencji komplementarnej zależy bowiem od tego, czy mamy do czynienia z sekwencją DNA, czy RNA. Np. dla sekwencji AAGCG komplementarną sekwencją DNA będzie TTCGC, w przypadku RNA będzie to UUCGC. Z kolei dla sekwencji aminokwasów w ogóle nie ma to sensu. Moglibyśmy zatem utworzyć oddzielne klasy, jedną dla DNA, drugą dla RNA a trzecią dla peptydów/białek, w których umieścilibyśmy metody wspólne dla wszystkich rodzajów sekwencji, a każdą z nich uzupełnilibyśmy o właściwy dla niej kod. Byłoby to jednak niepotrzebne powtarzanie tego samego kodu. Na szczęście w podobnych przypadkach możemy wykorzystać dziedziczenie.

Na podstawie klasy, którą w takiej sytuacji możemy nazwać klasą nadrzędną, możemy utworzyć klasy potomne, które dziedziczą po niej wszystkie metody i atrybuty. Do klas potomnych możemy dopisywać kolejne metody i atrybuty, a także nadpisywać metody odziedziczone z klasy nadrzędnej.

Utwórzmy klasę potomną, dziedziczącą po klasie Sekwencja, dostosowaną do sekwencji DNA. Zauważ, że w definicji klasy, w nawiasie, znajduje się nazwa klasy nadrzędnej. Zwróć także uwagę na pierwszą linię kodu metody __init__(). Jeśli w klasie potomnej występuje metoda o takiej samej nazwie jak w klasie nadrzędnej, to metoda w klasie potomnej nadpisuje metodę w klasie nadrzędnej. Mimo to można się w takiej sytuacji odwołać do nadpisanej metody z klasy nadrzędnej, używając funkcji super(). Gdybyśmy tego nie zrobili, należałoby ponownie umieścić w metodzie __init__() klasy nadrzędnej ten sam kod, który jest zapisany w klasie nadrzędnej. Użyjemy super() także w nadpisywanej metodzie drukuj_informacje().

class SekwencjaDNA(Sekwencja):
    """Przechowuje dane sekwencji i wykonuje na niej proste operacje.""" 
    
    # Nadpisujemy metodę __init__() klasy nadrzędnej
    def __init__(self, sekwencja, ident):
        # Wywołujemy metodę __init__() klasy nadrzędnej, która inicjalizuje
        # atrybuty wspólne dla obu klas.
        super().__init__(sekwencja, ident)
        # Ustawiamy dodatkowe atrybuty
        self.__typ = 'DNA'
        self.zasady_komplementarne = {
            'A': 'T',
            'T': 'A',
            'C': 'G',
            'G': 'C'
        }
        
    def drukuj_informacje(self):
        super().drukuj_informacje()
        print(f"typ:       {self.__typ}")
    
    @property
    def typ(self):
        return self.__typ
    
    @property
    def komplementarna(self):
        """Zwraca sekwencję komplementarną."""
        
        self.komplementarna_lista = [] 
        for z in self.sekwencja:
            self.komplementarna_lista.append(self.zasady_komplementarne[z])
        return ''.join(self.komplementarna_lista)
    
    @property
    def odwrocona_komplementarna(self):
        """Zwraca sekwencję odwróconą, komplementarną."""
        
        return self.komplementarna[::-1]
dna = SekwencjaDNA('ACGTTG', 'seq_1')
print(f"Sekwencja:                {dna.sekwencja}")
print(f"Odwrócona:                {dna.odwrocona}")
print(f"Komplementarna:           {dna.komplementarna}")
print(f"Odwrócona komplementarna: {dna.odwrocona_komplementarna}")  
dna.drukuj_informacje()
Sekwencja:                ACGTTG
Odwrócona:                GTTGCA
Komplementarna:           TGCAAC
Odwrócona komplementarna: CAACGT
Sekwencja: ACGTTG
Nazwa:     seq_1
Długość:   6
typ:       DNA

Kolejna klasa będzie przechowywała sekwencje RNA. Ponieważ większość kodu będzie mieć wspólnego z klasą SekwencjaDNA, będzie jej klasą potomną. Musimy także nadpisać atrybut typ a także metodę drukuj_informacje(). W tej metodzie nie możemy odwołać się za pomocą super() do klasy SekwencjaDNA, ponieważ wtedy zostanie zwrócona wartość atrybutu typ dla niej (DNA), musimy zatem odwołać się do jeszcze bardziej nadrzędnej klasy (Sekwencja) Zwróć uwagę jak to zostało zrobione.

class SekwencjaRNA(SekwencjaDNA):
    """Przechowuje dane sekwencji i wykonuje na niej proste operacje.""" 
    
    # Nadpisujemy metodę __init__() klasy nadrzędnej
    def __init__(self, sekwencja, ident):
        # Wywołujemy metodę __init__() klasy nadrzędnej, która inicjalizuje
        # atrybuty wspólne dla obu klas.
        super().__init__(sekwencja, ident)
        # Nadpisujemy dodatkowe atrybuty
        self.__typ = 'RNA'
        self.zasady_komplementarne = {
            'A': 'U',
            'U': 'A',
            'C': 'G',
            'G': 'C'
        }
    
    # Atrybut(y) też nadpisujemy
    @property
    def typ(self):
        return self.__typ

    # Nadpisujemy także tę metodę, inczej otrzymamy wartość dla 
    # klasy nadrzędnej (tu DNA)
    def drukuj_informacje(self):
        # W ten sposób wywołujemy metodę `drukuj_informacje()`
        # ale nie z klasy nadrzędnej (SekewncjaDNA), ale dla 
        # nadrzędnej wobec niej (Sekwencja)
        super(SekwencjaDNA, self).drukuj_informacje()
        print(f"typ:       {self.__typ}")

Sprawdźmy teraz nową klasę:

rna = SekwencjaRNA('ACGUUG', 'seq_2')
print(f"Sekwencja:                {rna.sekwencja}")
print(f"Typ:                      {rna.typ}")
print(f"Odwrócona:                {rna.odwrocona}")
print(f"Komplementarna:           {rna.komplementarna}")
print(f"Odwrócona komplementarna: {rna.odwrocona_komplementarna}") 
rna.drukuj_informacje()
Sekwencja:                ACGUUG
Typ:                      RNA
Odwrócona:                GUUGCA
Komplementarna:           UGCAAC
Odwrócona komplementarna: CAACGU
Sekwencja: ACGUUG
Nazwa:     seq_2
Długość:   6
typ:       RNA

Dopiszmy teraz do klasy Sekwencja, jeszcze jedną metodę, porównującą sekwencję przechowywaną w innym obiekcie:

class Sekwencja():
    """Przechowuje dane sekwencji i wykonuje na niej proste operacje.""" 
        
    def __init__(self, sekwencja, ident):
        """ Inicjalizacja obiektu, ustawienie wartości atrybutów."""
        self.__sekwencja = sekwencja
        self.__ident = ident
        self.__dlugosc = len(self.sekwencja)
        
    def drukuj_informacje(self):
        """Drukuje wartości atrybutów."""
        print(f"Sekwencja: {self.__sekwencja}")
        print(f"Nazwa:     {self.__ident}")
        print(f"Długość:   {self.__dlugosc}")
    
    # To będzie działało jak getter
    @property
    def sekwencja(self):
        """Zwraca sekwencję."""
        return self.__sekwencja
    
    # To będzie działało jak setter
    @sekwencja.setter
    def sekwencja(self, sekwencja):
        """Ustawia wartość atrybutów: sekwencja, dlugosc."""
        self.__sekwencja = sekwencja
        self.__dlugosc = len(self.__sekwencja)
        
    @property
    def ident(self):
        return self.__ident
        
    @property
    def odwrocona(self):
        """Zwraca odwróconą sekwencję."""
        return self.__sekwencja[::-1]
    
    @property
    def dlugosc(self):
        """Zwraca długość sekwencji."""
        # Sprawdzenie, czy długość jest właściwa, jeśli nie to zostaje 
        # ustawiona właściwa wartośc parametru slugosc
        if self.__dlugosc != len(self.__sekwencja):
            self.__dlugosc = len(self.__sekwencja)
        return self.__dlugosc
    
    def porownaj(self, sekwencja_2):
        self.identyczne = ''
        for z_1, z_2 in zip(self.sekwencja, sekwencja_2.sekwencja):
            if z_1 == z_2:
                self.identyczne = self.identyczne + '*'
            else:
                self.identyczne = self.identyczne + ' '
        print(f"{self.sekwencja}")
        print(f"{self.identyczne}")
        print(f"{sekwencja_2.sekwencja}")      
sekw_1 = Sekwencja('AACGTA', 'seq_1')
sekw_2 = Sekwencja('ATCGCA', 'seq_2')
sekw_1.porownaj(sekw_2)
AACGTA
* ** *
ATCGCA

Zauważ, że nowa metoda pozwala na porównanie ze sobą obiektów tego samego (w tym przypadku) typu.

Zadanie

Napisz program, który będzie składał się z dwu plików:

Plik telefon.py - zawiera klasę Telefony. Obiekty tej klasy powinny przechowywać informację o marce i numerze telefonu. W tej klasie umieść metodę __init__(), właściwośći marka i numer oraz trzy metody:

  • odbierz_sms() pobierającą tekst SMS-u i numer telefonu, na który wysyłana jest wiadomość. Metoda drukuje informacje o: numerze telefonu, który odbiera SMS (na nim uruchamiana jest metoda), numer nadawcy i treść SMS-a.
  • wyslij_sms() metoda przyjmuje numer telefonu, na który wysyłany jest SMS, treść SMS, oraz listę z obiektami typu Telefon. Metoda znajduje telefon z właściwym numerem i “wysyła SMS” wywołując na znalezionym obiekcie metodę odbierz_sms()
  • drukuj_dane() - drukuje markę i numer telefonu.

Plik komorki.py - główny plik programu. W tym pliku:

  • Tworzona jest lista obiektów typu Telefon, które przechowują informacje o swojej marce i numerze telefonu.
  • Funkcja wyslij_sms() - pobiera numer telefonu, następnie użytkownik wpisuje numer telefonu na który należy wysłać SMS oraz treść wiadomości, następnie wywołuje metodę wyslij_sms() na przkazanym do niej obiekcie.
  • Główną pętlę programu - Uzytkownik podaje numer telefonu, który jest znajdywany w liście obiektów i przypisany do nazwy, np. tel_1. Następnie uruchamia się pętal wewnętrzna w której uzytkownik może wybrać opcje: - wypisać dane telefonu - wysłać SMS - wybrać inny telefon - zakończyć program

Przykładowa sesja:

Podaj numer telefonu z którego chcesz korzystać: 222333222
Nie ma takiego numeru.

Podaj numer telefonu z którego chcesz korzystać: 222333444

Co chcesz zrobić?
        d - podaj dane telefonu
        s - wyślij SMS
        i - wybierz inny telefon
        z - zakończ program
: d
Marka: Pear
Numer: 222333444

Co chcesz zrobić?
        d - podaj dane telefonu
        s - wyślij SMS
        i - wybierz inny telefon
        z - zakończ program
: s

Podaj numer, do którego chcesz wysłać wiadomość: 123123123
Podaj tekst wiadomości:
Witaj Świecie!            
Jestem telefonem o numerze 123123123
Wiadomość od numeru 222333444: "Witaj Świecie!"

Co chcesz zrobić?
        d - podaj dane telefonu
        s - wyślij SMS
        i - wybierz inny telefon
        z - zakończ program
: i

Podaj numer telefonu z którego chcesz korzystać: 999888999

Co chcesz zrobić?
        d - podaj dane telefonu
        s - wyślij SMS
        i - wybierz inny telefon
        z - zakończ program
: d
Marka: Cmokia
Numer: 999888999

Co chcesz zrobić?
        d - podaj dane telefonu
        s - wyślij SMS
        i - wybierz inny telefon
        z - zakończ program
: z
---- KONIEC ----
Last updated on 29 Dec 2020
Published on 29 Dec 2020