Klasy abstrakcyjne w Pythonie

Klasy abstrakcyjne są przeznaczone do wydzielenia funkcjonalności, która będzie następnie implementowana w klasach, które dziedziczą po klasie abstrakcyjnej.

Przypuśćmy, że mamy klasę Employee opisującą pracownika oraz klasę Manager, która jest klasą potomną względem klasy Employee. Chcemy utworzyć nową klasę Intern, która będzie opisywać osoby odbywające staż w firmie. Klasa Intern nie należy do wcześniej utworzonych klas Employee oraz Manager, jednak dzieli z nimi pewne atrybuty i metody.

W celu wydzielenia wspólnej funkcjonalności dla wszystkich powyższych klas tworzę klasę Person, która będzie zawierać metodę init() oraz metodę get_full_name(). Będą to metody abstrakcyjne. tzn.

import abc
class Person(abc.ABC):
    ''' Abstract Person class '''

    @abc.abstractmethod
    def __init__(self, first, last):
        self.first = first
        self.last = last

    @abc.abstractmethod
    def get_full_name(self):
        pass

Przykładowo klasa Employee będzie implementować powyższą klasę abstrakcyjną:

class Employee(Person):
    ''' Sample Employee class '''

    def __init__(self, first, last, salary):
        super().__init__(first, last)
        self.salary = salary

    def get_full_name(self):
        return f'{self.__class__.__name__}: {self.first} {self.last}'

Próba utworzenia instancji z klasy abstrakcyjnej Person zakończy się natomiast wyrzuceniem wyjątku TypeError.

Również nie zaimplementowanie wszystkich metod abstrakcyjnych w klasie potomnej powoduje wyjątek TypeError.

Dziedziczenie cz.1

We wcześniejszym wpisie o programowaniu obiektowym w Pythonie opisałem podstawy dziedziczenia. Klasę bazową (Superclass), z której klasa potomna dziedziczy wszystkie atrybuty i metody zapisujemy w deklaracji klasy pochodnej (Subclass) jako argument tzn.

class Subclass(Superclass):
    pass

Gdy klasa potomna dziedziczy po kilku klasach bazowych, to dziedziczy też atrybuty i metody z tych klas. Gdy jednak w dwóch różnych klasach bazowych są atrybuty lub metody o takich samych nazwach, to klasa potomna dziedziczy ten atrybut lub tę metodę, której klasa była wcześniej na liście argumentów w definicji klasy potomnej tzn.

class Person:
    greeting = 'Hi!'
    def function1(self):
        print('hello')

class Employee:
    greeting = "What's up!"
    def function1(self):
        print('world')
    def function2(self):
        pass

class Manager(Employee, Person):
    pass

if __name__ == '__main__':
    john_doe = Manager()
    print(dir(john_doe)
    print(john_doe.greeting)
    john_doe.function1() 

Jak widać na podstawie powyższego przykładu klasa Manager posiada obie metody: function1() i function2() oraz zmienną greeting. Wartością zmiennej greeting będzie napis What’s up! a wynikiem funkcji będzie wyświetlenie napisu world.

>>część druga<<

Dziedziczenie cz.2

Podczas dziedziczenia metody z klasy bazowej możemy nadpisać kod metody (przesłonić metodę) i wtedy uzyskać całkiem nową funkcjonalność, bądź też powiększyć funkcjonalność metody. Metoda __init__() uzyskała zwiększoną funkcjonalność w klasie pochodnej, natomiast kod metody function1() został przesłonięty w podklasie tzn.

class Person:
    def __init__(self, name):
    	self.name = name
    def function1(self):
    	print('Hello')

class Employee(Person):
    def __init__(self, name, job):
    	super().__init__(name)
    	self.job = job
    def function1(self):
    	print('Hi!')

if __name__ == '__main__':
    john_doe = Employee('John Doe', 'programmer')
    print(john_doe.name)
    print(john_doe.job)
    john_doe.function1()

Funkcja super wywołuje metodę z klasy basowej, w tym przypadku __init__(), dzięki temu metoda w podklasie wykorzystuje kod z klasy bazowej i dodaje nową funkcjonalność.

Zamiast zastosować konstrukcję z funkcją super() można zastosować konstrukcję z nazwą klasy bazowej tj.

Person.__init__(self, name)

Jest to szczególnie przydatne, gdy klasa potomna dziedziczy po kilku klasach bazowych i zależy nam na wykorzystaniu konkretnej metody z określonej klasy bazowej.

Podstawy programowania obiektowego w Pythonie cz.1

Jako przykład posłuży klasa o nazwie Person. Według konwencji nazwa klasy powinna zaczynać się dużą literą, a w razie nazwy składającej się z wielu wyrazów stosujemy Pascal case – wszystkie wyrazy pisane łącznie i zaczynające się dużymi literami. Podstawowa definicja klasy wygląda następująco:

class Person:
    pass

Wszystkie klasy domyślnie dziedziczą z podstawowej klasy object, więc można byłoby zdefiniować klasę w następujący sposób:

class Person(object):
    pass

Z klasy, która zawiera tylko pustą instrukcję pass można już tworzyć obiekty. Tworzenie obiektu nie wymaga podania specjalnego słowa kluczowego np. new

john_doe = Person()

print(john_doe) # możemy sprawdzić jakiego typu jest zmienna

print(dir(john_doe)) # możemy wyświetlić atrybuty i metody

Aby zdefiniować klasę, która dziedziczy po innej klasie należy umieścić nazwę klasy bazowej jako argument tzn.:

class Employee(Person):
    pass

Python obsługuje wielodziedziczenie, tzn. można utworzyć klasę, która dziedziczy po wielu klasach bazowych np.

class Executive(Employee, Manager):
    pass 

>>część druga<<

Podstawy programowania obiektowego w Pythonie cz.2

Podczas tworzenia egzemplarza klasy wykonywana jest specjalna metoda __init__(), którą można utożsamiać z konstruktorem z innych języków. Ma ona postać:

def __init__(self, arg1, arg2, ... ):
    pass

Pierwszym argumentem jest domyślnie self, który stanowi referencję do bieżącej instancji klasy. Pozostałe argumenty są opcjonalne i podobnie jak w konstruktorach z innych języków służą do ustawiania wartości atrybutów. Nazwy atrybutów instancji także muszą być poprzedzone zmienną self.

Oprócz zmiennych instancji istnieją również zmienne klasy. Przechowują wartości, które są takie same dla wszystkich instancji klasy. Możliwe jest również wyświetlenie wartości tych zmiennych bez tworzenia egzemplarza klasy, tylko poprzez podanie nazwy klasy np. dla klasy Employee tworzymy zmienną przechowującą wartość procentową premii do pensji, którą otrzymuje każdy pracownik tzn.

class Employee:
    bonus = 10
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
john_doe = Employee('John Doe', 8000)    # utworzenie instancji klasy
print(john_doe.name)    # wyświetla wartość atrybutu name instancji
print(john_doe.bonus)   # wyświetla wartość zmiennej klasy z instancji
print(Employee.bonus)   # wyświetla wartość zmiennej klasy

>>część trzecia<<

Podstawy programowania obiektowego w Pythonie cz.3

Zmienna klasy może przechowywać np. liczbę wszystkich pracowników przedsiębiorstwa. Ma taką samą wartość niezależnie czy wywołuje się ją z klasy czy z jakiejkolwiek jej instancji. Inkrementację zmiennej klasowej przechowującej liczbę pracowników można umieścić np. w metodzie __init__(), wtedy utworzenie nowego pracownika zwiększa wartość zmiennej klasowej.

Można jednakże utworzyć zmienną instancji o takiej samej nazwie jak zmienna klasy. W takim przypadku zmienna klasowa zostanie przesłonięta przez wartość atrybutu tzn.

class Employee:
    bonus = 10
    def set_bonus(self, bonus):
        self.bonus = bonus

john_doe = Employee()
print(john_doe.bonus)   # wyświetla wartość 10
john_doe.set_bonus(20)  # ustalenie specjalnej wartości premii
print(john_doe.bonus)   # wyświetla wartość 20 - premia dla pracownika
print(Employee.bonus)   # wyświetla wartość 10 - bazowa premia   

>>część czwarta<<

Podstawy programowania obiektowego w Pythonie cz.4

Analogicznie jak w przypadku atrybutów i zmiennych klasowych, taki sam podział można zaobserwować dla metod czyli funkcji zawartych w klasie.

Metody instancji w definicji jako pierwszy argument muszą posiadać referencję do instancji klasy – self np.

class Employee:
    bonus = 10
    def set_bonus(self, bonus):
        self.bonus = bonus

Powyższa metoda set_bonus ustawia zmienną instancji, która przesłania zmienną klasową bonus.

Gdy jednak chcemy utworzyć metodę klasową, to musimy użyć argumentu cls zamiast self, czyli referencja do klasy a nie do instancji. Ponadto należy użyć dekoratora @classmethod tzn.

@classmethod
    def set_base_bonus(cls, bonus):
        cls.bonus = bonus

Powyższa metoda ustawi bazową wartość zmiennej bonus w klasie Employee i wszystkich jej instancjach.

>>część piąta<<

Podstawy programowania obiektowego w Pythonie cz.5

Metody klasy mogą stanowić też sposób na tworzenie alternatywnych konstruktorów o różnej liczbie argumentów (przeciążanie konstruktora) np. biorąc pod uwagę przykładową klasę Employee, która jako argumenty przyjmuje nazwę pracownika i wartość pensji, możemy utworzyć metodę klasy, która przyjmie 3 argumenty: imię, nazwisko i wartość wynagrodzenia i zwraca egzemplarz klasy Employee.

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
    @classmethod
    def from_full_name(cls, first, last, salary):
        name = '{} {}'.format(first, last)
        return cls(name, salary)

john_doe = Employee.from_full_name('John', 'Doe', 8000)

Oprócz metod klasy istnieją też metody statyczne, które są poprzedzone dekoratoraem @staticmethod. Metody statyczne nie przyjmują jako argumentu ani odwołania self do instancji, ani cls do klasy. Mogą przyjmować jawne argumenty, będące parametrami tych funkcji. Funkcje statyczne są powiązane tematycznie z klasą, w której są zawarte, ale nie powinny operować na zmiennych klasy lub instancji.