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.

Akcesory i mutatory w Pythonie cz.1

Koncepcja programowania obiektowego wiąże się z pojęciem enkapsulacji, czyli ukrywania pól klasy i udostępniania tylko publicznych metod (publicznego API) do odczytu lub zmiany wartości tych pól. W javie np. rozwiązane jest to na poziomie języka za pomocą modyfikatora dostępu – private.

Python nie umożliwia zastosowania takich sposobów hermetyzacji. Stosowanie zmiennych prywatnych w stylu: _zmienna jest tylko kwestią konwencji.

Rozważmy następujący przykład, w którym tworzymy klasę reprezentującą pracownika:

class Employee:
	def __init__(self, name, salary, phone):
	    self.name = name
	    self.salary = salary
	    self.phone = phone

W Javie powinniśmy zadeklarować zmienne name, salary i phone jako prywatne i odwoływać się do nich przez metody dostępowe getters /setters. W Pythonie nie zaleca się pisania metod dostępowych, których jedynym zadaniem jest zwracanie wartości atrybutu lub ustawianie wartości atrybutu. Zamiast tego możemy odnosić się do tych pól bezpośrednio. Ma to wiele zalet, ale traci się korzyści z hermetyzacji np.

johnDoe = Employee('John Doe', 8000, '555 111 22 33')
johnDoe.salary = 9000
print(johnDoe.salary)

>>część druga<<

Akcesory i mutatory w Pythonie cz.2

Stosowanie bezpośredniego dostępu do atrybutów klasy jest problemem w przypadku konieczności dokonywania zmian np. wprowadzenie kontroli poprawności wpisanych danych.

Bazując na kodzie wcześniej utworzonej klasy Employee załóżmy, że chcielibyśmy dokonać walidacji wpisanego numeru telefonu tzn. czy ma odpowiednią ilość cyfr oraz przy okazji usunąć wprowadzone zbędne znaki, które nie są cyframi.

Aby to osiągnąć nie musimy zmieniać sposobu, w jaki komunikujemy się ze zmienną phone. Możemy nadal odwoływać się bezpośrednio poprzez nazwę zmiennej. Jest to możliwe dzięki użyciu dekoratora @property

class Employee:

    def __init__(self, name, salary, phone):
        self.name = name
        self.salary = salary
        self.phone = phone
    @property
        def phone(self):
           return self._phone
    @phone.setter
        def phone(self, value):
            phone = ''.join(filter(lambda x: x.isdigit(), value))
            if len(phone) != 10:
                raise ValueError('Wrong phone number!')
            self._phone = phone

Wywołanie konstruktora klasy ustawia poszczególne atrybuty klasy, przy czym przypisanie argumentu phone do atrybutu self.phone powoduje wywołanie mutatora i dzięki temu walidację wprowadzonego numeru telefonu już w momencie tworzenia obiektu klasy Employee.

Podobnie przypisanie nowej wartości do atrybutu phone wywoła odpowiednią metodę walidującą tzn.

johnDoe = Employee('John Doe', 8000, "555 111 22 33")
johnDoe.phone = '000' #incorrect number length