Programowanie obiektowe, w skrócie OOP, to filozofia programowania. OOP traktuje obiekty jako podstawową jednostkę programów. Obiekt zawiera dane i funkcje danych eksploatacyjnych.

Programowanie zorientowane na proces postrzega program komputerowy jako serię zestawów poleceń, to znaczy sekwencyjne wykonywanie zestawu funkcji. Aby uprościć programowanie, funkcje zorientowane na proces nadal dzieli się na podfunkcje, to znaczy duże funkcje są dzielone na małe funkcje, aby zmniejszyć złożoność systemu.

Programowanie obiektowe traktuje program komputerowy jako zbiór obiektów, a każdy obiekt może odbierać komunikaty od innych obiektów i przetwarzać te komunikaty. Wykonanie programu komputerowego to ciąg komunikatów przekazywanych pomiędzy obiektami.

W Pythonie wszystkie typy danych można traktować jak obiekty i oczywiście obiekty można również dostosowywać. Niestandardowy typ danych obiektu to koncepcja klasy w zorientowaniu obiektowym.

Posługujemy się przykładem, aby zilustrować różnicę pomiędzy przebiegiem programu zorientowanego na proces i obiektowo.

Załóżmy, że chcemy przetworzyć listę ocen ucznia. Aby przedstawić ocenę ucznia, program zorientowany na proces można przedstawić za pomocą dyktatu:

std1 = { 'name': 'Michael', 'score': 98 }
std2 = { 'name': 'Bob', 'score': 81 }

Przetwarzanie ocen uczniów można wdrożyć za pomocą funkcji, takich jak drukowanie ocen uczniów:

def print_score(std):
    print('%s: %s' % (std['name'], std['score']))

Jeśli przyjmiemy koncepcje programowania obiektowego, naszym pierwszym priorytetem nie będzie myślenie o procesie wykonywania programu, ale to, że typ danych Student powinien być traktowany jako obiekt, który ma dwa atrybuty: name i score. Jeśli chcesz wydrukować wynik ucznia, musisz najpierw utworzyć obiekt odpowiadający temu uczniowi, a następnie wysłać do obiektu wiadomość print_score, aby obiekt mógł wydrukować własne dane.

class Student(object):

def __init__(self, name, score):
        self.name = name
        self.score = score
    def print_score(self):
        print('%s: %s' % (self.name, self.score))

Wysłanie wiadomości do obiektu jest w rzeczywistości wywołaniem powiązanej z obiektem funkcji, którą nazywamy Metodą obiektu. Program zorientowany obiektowo pisze się w następujący sposób:

bart = Student('Bart Simpson', 59)
lisa = Student('Lisa Simpson', 87)
bart.print_score()
lisa.print_score()

Idea projektowania obiektowego pochodzi z natury, ponieważ w naturze pojęcia klasy i instancji są bardzo naturalne. Klasa to pojęcie abstrakcyjne. Na przykład zdefiniowana przez nas Klasa — Student odnosi się do koncepcji studentów, natomiast Instancja to konkretny Student. Na przykład Bart Simpson i Lisa Simpson to dwie konkretne Studentki.

Dlatego ideą projektowania obiektowego jest wyodrębnienie klasy i utworzenie instancji w oparciu o klasę.

Poziom abstrakcji obiektowej jest wyższy niż funkcji, ponieważ Klasa zawiera zarówno dane, jak i metody obsługi danych.

Klasa i instancja

Najważniejszymi pojęciami zorientowanymi obiektowo są klasa i instancja. Należy pamiętać, że klasa to abstrakcyjny szablon, taki jak klasa Student, a instancja to konkretny „obiekt” stworzony na podstawie tej klasy. Każdy obiekt ma tę samą metodę, ale poszczególne dane mogą się różnić.

Wciąż biorąc za przykład klasę Student, definiowanie klasy w Pythonie odbywa się za pomocą słowa kluczowego class:

class Student(object):
    pass

Zaraz po class znajduje się nazwa klasy, czyli Student. Nazwa klasy to zwykle słowo zaczynające się od dużej litery, po którym następuje object, wskazujące, z której klasy dana klasa jest dziedziczona. O pojęciu dziedziczenia porozmawiamy później. Jeśli nie ma odpowiedniej klasy dziedziczenia, po prostu użyj klasy object, która jest klasą, którą ostatecznie odziedziczą wszystkie klasy.

Po zdefiniowaniu klasy Student można utworzyć instancję Student w oparciu o klasę Student. Tworzenie instancji odbywa się poprzez nazwę klasy + ():

>>> bart = Student()
>>> bart
<__main__.Student object at 0x10a67a590>
>>> Student
<class '__main__.Student'>

Jak widać, zmienna bart wskazuje na instancję Student, a kolejne 0x10a67a590 to adres pamięci. Adres każdego obiektu jest inny, a sam Student jest klasą.

Możesz dowolnie powiązać atrybuty ze zmienną instancji. Na przykład powiąż atrybut name z instancją bart:

>>> bart.name = 'Bart Simpson'
>>> bart.name
'Bart Simpson'

Ponieważ klasy mogą działać jako szablony, podczas tworzenia instancji możemy wymusić wypełnienie niektórych atrybutów, które naszym zdaniem muszą być powiązane. Definiując specjalną metodę __init__, podczas tworzenia instancji powiązane są z nią atrybuty name, score i inne:

class Student(object):

def __init__(self, name, score):
        self.name = name
        self.score = score
★ There are two underscores before and after the special method "__init__"! ! !

Należy pamiętać, że pierwszym parametrem metody __init__ jest zawsze self, który reprezentuje samą utworzoną instancję. Dlatego w ramach metody __init__ można powiązać różne atrybuty z self, ponieważ self wskazuje na samą utworzoną instancję.

Za pomocą metody __init__ podczas tworzenia instancji nie można przekazywać pustych parametrów. Należy przekazać parametry pasujące do metody __init__, ale self nie jest konieczne. Interpreter Pythona sam przekaże zmienne instancji:

>>> bart = Student('Bart Simpson', 59)
>>> bart.name
'Bart Simpson'
>>> bart.score
59

W porównaniu ze zwykłymi funkcjami jedyną różnicą między funkcjami zdefiniowanymi w klasie jest to, że pierwszym parametrem jest zawsze zmienna instancji self i nie trzeba przekazywać tego parametru podczas wywoływania. Poza tym metody klas nie różnią się od zwykłych funkcji, więc nadal możesz używać parametrów domyślnych, parametrów zmiennych, parametrów słów kluczowych i parametrów nazwanych słów kluczowych.

Enkapsulacja danych

Ważną cechą programowania obiektowego jest enkapsulacja danych. W powyższej klasie Student każda instancja ma własne dane name i score. Dostęp do tych danych możemy uzyskać poprzez funkcje takie jak wydruk oceny ucznia:

>>> def print_score(std):
...     print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
Bart Simpson: 59

Ponieważ instancja Student sama jest właścicielem tych danych, aby uzyskać do nich dostęp, nie ma potrzeby uzyskiwania dostępu do nich z funkcji zewnętrznych. Możesz bezpośrednio zdefiniować funkcję dostępu do danych wewnątrz klasy Student, hermetyzując w ten sposób „dane”. Te funkcje hermetyzujące dane są powiązane z samą klasą Student, którą nazywamy metodami klasowymi:

class Student(object):

def __init__(self, name, score):
        self.name = name
        self.score = score
    def print_score(self):
        print('%s: %s' % (self.name, self.score))

Aby zdefiniować metodę, należy postępować tak samo, jak w przypadku zwykłej funkcji, z tą różnicą, że pierwszym parametrem jest self. Aby wywołać metodę, wystarczy wywołać ją bezpośrednio na zmiennej instancji. Z wyjątkiem self, nie musisz go zdawać. Inne parametry są przekazywane normalnie:

>>> bart.print_score()
Bart Simpson: 59

W ten sposób, kiedy patrzymy na klasę Student z zewnątrz, musimy jedynie wiedzieć, że aby utworzyć instancję, należy podać name i score, a sposób drukowania jest zdefiniowany wewnątrz klasy Student. Te dane i logika są „hermetyzowane”. Łatwo zadzwonić, ale nie musisz znać szczegółów wewnętrznej realizacji.

Kolejną zaletą enkapsulacji jest to, że możesz dodać nowe metody do klasy Student, takie jak get_grade:

class Student(object):
    ...

def get_grade(self):
        if self.score >= 90:
            return 'A'
        elif self.score >= 60:
            return 'B'
        else:
            return 'C'

Tę samą metodę get_grade można wywołać bezpośrednio na zmiennej instancji bez znajomości wewnętrznych szczegółów implementacji:

class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def get_grade(self):
        if self.score >= 90:
            return 'A'
        elif self.score >= 60:
            return 'B'
        else:
            return 'C'

Parametry wejściowe:

lisa = Student('Lisa', 99)
bart = Student('Bart', 59)
print(lisa.name, lisa.get_grade())
print(bart.name, bart.get_grade())

Wynik biegu:

Lisa A
Bart C