Kod

Programowanie obiektowe w Pythonie: podstawowe zasady

Programowanie obiektowe w Pythonie: podstawowe zasady

Opanuj programowanie w Pythonie dzięki bezpłatnemu kursowi ➞ Zapisz się na bezpłatny kurs Pythona Kurs i naucz się tworzyć boty Telegrama, parsery sieciowe i strony internetowe od podstaw pod okiem eksperta Sber.

Dowiedz się więcej

Metodologia programowania obiektowego stała się standardem w większości dużych firm, ponieważ znacznie upraszcza proces rozwoju. Mimo to wielu nowicjuszy w programowaniu czuje się onieśmielonych jej rozwojem. W tym artykule postaramy się pokazać, że to podejście wcale nie jest takie trudne.

Dlaczego wynaleziono OOP

W programowaniu obiektowym (OOP) podstawowym elementem jest obiekt. Obiekt ten można traktować jako unikalny kontener, który przechowuje zarówno dane, jak i opisuje operacje, które można na nich wykonać.

Aby zrozumieć wartość obiektów i cel ich tworzenia, warto porównać programowanie obiektowe z proceduralnym podejściem do tworzenia oprogramowania. W ramach tego drugiego podejścia cały kod programu można podzielić na dwie kategorie: główną aplikację i funkcje pomocnicze. Funkcje te można wywołać zarówno z kodu głównego, jak i z innych funkcji.

Infografiki: Skillbox Media

Ta metodologia programowania ma istotną wadę – kod Komponenty są silnie od siebie zależne. Na przykład program główny inicjuje wywołanie jednej funkcji, która z kolei wywołuje kolejną, a ta z kolei trzecią. Załóżmy, że ta druga funkcja może być wywoływana nie tylko przez program główny, ale także przez kilka innych funkcji jednocześnie. Całą tę mylącą sekwencję można schematycznie przedstawić na wykresie:

Infografiki: Skillbox Media

Podczas wprowadzania zmian w jednej z funkcji może się okazać, że Reszta kodu nie będzie w stanie sobie z tym poradzić – w rezultacie pojawią się błędy. W rezultacie inne części, które z kolei zależą od różnych funkcji, również będą musiały zostać zmodyfikowane. Ostatecznie znacznie bardziej efektywne będzie stworzenie zupełnie nowego programu od podstaw.

W programowaniu proceduralnym często konieczne jest powtarzanie kodu i tworzenie funkcji, które różnią się jedynie w drobnych szczegółach. Robi się to na przykład po to, aby zapewnić interakcję między różnymi komponentami programu.

W programowaniu obiektowym (OOP) podejście do organizacji kodu jest inne: zamiast łączyć funkcje z programem głównym, używa się obiektów, które zawierają zarówno własne zmienne, jak i funkcje. Pozwala to na bardziej ustrukturyzowaną hierarchię. W obiektach zmienne są zwykle nazywane polami lub atrybutami, a funkcje metodami.

Infografiki: Skillbox Media

Każdy obiekt pełni funkcję niezależnie i posiada wszystkie niezbędne funkcjonalności. W związku z tym uszkodzenie jednego z obiektów nie wpłynie na pozostałe. Co więcej, nawet po całkowitej restrukturyzacji wewnętrznej zawartości obiektu, pod warunkiem zachowania jego zachowania, kod będzie działał bezawaryjnie.

Jak działają klasy

W programowaniu obiektowym każdy element jest tworzony na podstawie konkretnej klasy, która jest modelem abstrakcyjnym. Model ten szczegółowo opisuje zarówno strukturę obiektu, jak i czynności, które można na nim wykonać.

Rozważmy klasę o nazwie „Kot”, która zawiera takie cechy, jak „rasa”, „maść”, „wiek”, a także czynności, które kot może wykonywać: „miau”, „mruczenie”, „mycie” i „spanie”. Ustawiając określone wartości dla tych cech, możemy tworzyć określone instancje tej klasy.

Załóżmy:

  • rasa abisyńska.
  • kolor = czerwony.
  • wiek = 4.

W ten sposób możemy tworzyć nieskończoną liczbę różnych kotów.

Infografika: Skillbox Media

Dlatego każda instancja klasy „Cat” – niezależnie od tego, czy jest czerwona, szara czy czarna – będzie mogła miauczeć, mruczeć, myć się i spać, jeśli zaimplementujemy niezbędne metody.

Podstawy programowania obiektowego w Pythonie

Podstawy programowania obiektowego opierają się na czterech kluczowych koncepcjach: hermetyzacji, dziedziczeniu, polimorfizmie i abstrakcji. Mając to na uwadze, przyjrzyjmy się klasie „Cat” jako przykładowi, aby zilustrować te zasady w działaniu:

Metoda __init__ inicjuje klasę. Jest aktywowana natychmiast po utworzeniu instancji w celu ustawienia wartości atrybutów dynamicznych. Argument self jest odwołaniem do bieżącego obiektu, zapewniającym dostęp do atrybutów i metod używanych w procesie. W innych językach programowania odpowiednikiem self jest słowo kluczowe this.

Uwaga 1: Chociaż używanie słowa kluczowego self jest powszechną praktyką, jego użycie nie jest wymagane. Można je zastąpić dowolną inną nazwą. Warto jednak pamiętać, że taka zamiana może wprowadzić zamieszanie u osób analizujących kod.

Uwaga 2: Powszechną praktyką jest używanie wielkich liter w nazwach klas, podczas gdy nazwy obiektów są pisane małymi literami.

Opracowaliśmy klasę Cat, w której zdefiniowaliśmy trzy główne atrybuty: rasę, kolor i wiek. Zaimplementowaliśmy również dwie metody, które pozwalają naszemu kotu wydawać dźwięki: miauczenie metodą meow() i mruczenie metodą purr().

Utwórzmy kilka instancji naszej klasy:

Wspaniale! Skoro mamy już podstawy, przejdźmy do nauki podstaw programowania obiektowego.

Kontrola dostępu do danych obiektu jest niezbędna, aby zapobiec ich przypadkowej zmianie przez użytkowników, co może prowadzić do błędów. Z tego powodu programiści tworzą specjalne metody, które umożliwiają interakcję z danymi spoza klasy bez naruszania jej wewnętrznej struktury i integralności.

Zajmijmy się naszymi kotami. Możemy pozwolić na zmianę atrybutu „wiek”, ale tylko w kierunku wzrostu. Jeśli chodzi o atrybuty „rasa” i „kolor”, lepiej pozostawić je tylko do odczytu – rasa kota pozostaje niezmieniona, a kolor może się zmieniać, ale nie według jego uznania.

W naszej klasie „Kot” wszystkie atrybuty ustawiliśmy jako publiczne. Wprowadźmy niezbędne zmiany.

Kod stał się nieco bardziej złożony, ale teraz wyjaśnimy wszystkie szczegóły. Najpierw uczyniliśmy wszystkie atrybuty niedostępnymi z zewnątrz, dodając przed nimi symbol _. Ten znak informuje interpreter, że zmienne te mogą być używane tylko w metodach klasy.

Aby nadal mieć dostęp do atrybutów, implementujemy to za pomocą dekoratora @property i tworzymy osobne metody dla każdego z atrybutów – rasy, koloru i wieku. W tych metodach zwracamy wartości atrybutów prywatnych, co sprawia, że ​​są one tylko do odczytu.

Na koniec musimy umożliwić użytkownikom zmianę wieku kota. Aby to zrobić, użyjemy @age.setter i ponownie zdefiniujemy metodę age, dodając w jej treści prosty warunek i zwracając wartość odpowiadającego mu atrybutu.

Teraz utwórzmy obiekt tej klasy:

Zdefiniujmy wartości atrybutu:

Spróbujmy wprowadzić zmiany w atrybucie age:

Wszystko poszło świetnie. Przejdźmy teraz do zmiany innego atrybutu:

Błąd wystąpił, ponieważ zmiany tego atrybutu były zabronione.

Przeczytaj także:

Enkapsulacja i modyfikatory dostępu: część trzecia podręcznika programowania obiektowego programowanie.

Klasy mogą dziedziczyć swoje właściwości i metody z klas nadrzędnych. Rozważmy sytuację, w której musimy utworzyć klasę „Kot Domowy”. Klasa ta będzie prawie taka sama jak klasa „Kot”, ale będzie zawierała dodatkowe atrybuty, takie jak „właściciel” i „pseudonim”, a także nową metodę o nazwie „beg for a treat”.

Aby uczynić „Kot Domowy” dziedziczącym klasę „Kot”, wystarczy to określić w kodzie i dodać niezbędne atrybuty i metody. Cała pozostała funkcjonalność zostanie automatycznie odziedziczona z klasy nadrzędnej.

Proponuję wprowadzenie nowej klasy:

W pierwszym wierszu kodu dziedziczymy wszystkie metody i właściwości z klasy Kot. Aby wszystko zostało utworzone poprawnie, należy wywołać metodę super() wewnątrz metody __init__() i za jej pośrednictwem zainicjować atrybuty klasy nadrzędnej. Dlatego do tej metody przekazujemy parametry takie jak „rasa”, „kolor” i „wiek”.

Oprócz właściwości odziedziczonych po klasie nadrzędnej, klasa potomna ma również unikalne atrybuty: „właściciel” i „nazwa”. Atrybuty te są przeznaczone wyłącznie do użytku w tej klasie i nie będą dostępne dla klasy nadrzędnej.

Atrybuty klasy potomnej zostały początkowo ustawione na prywatne i utworzono dla nich osobne metody. Dodatkowo zaimplementowaliśmy metodę getTreat(), której nie ma w klasie nadrzędnej.

Utwórzmy instancję klasy:

Zauważyliśmy, że zarówno nowa, jak i tradycyjna metoda działają efektywnie. Proces dziedziczenia został pomyślnie ukończony.

Przeczytaj także:

Dziedziczenie i trochę o polimorfizmie: piąta część przewodnika po programowaniu obiektowym.

Ta zasada umożliwia użycie tej samej polecenia dla obiektów należących do różnych klas, nawet jeśli ich implementacje się różnią. Na przykład mamy klasę „Kot” i całkowicie niezależną klasę „Papuga”, z których obie mają metodę „sleep”. Chociaż proces zasypiania u tych zwierząt przebiega inaczej (kot zazwyczaj zwija się w kulkę, podczas gdy papuga woli siedzieć na grzędzie), to samo polecenie może być użyte do wykonania tej funkcji.

Załóżmy, że mamy dwie klasy – „Kot” i „Papuga”.

Wyobraźmy sobie, że mamy metodę, która oczekuje na obiekt z określoną metodą o nazwie „sleep”.

Sprawdźmy, jak to będzie działać.

Pomimo różnic między klasami, metody o tych samych nazwach działają w podobny sposób. Zjawisko to nazywa się polimorfizmem.

Przeczytaj także:

Polimorfizm w programowaniu obiektowym (OOP) to kluczowa koncepcja, która pozwala obiektom różnych klas na odmienne przetwarzanie tej samej wiadomości. Osiąga się to dzięki możliwości różnych klas do dostarczania własnych implementacji metod, co zapewnia elastyczność i rozszerzalność kodu.

Przeciążanie metod to mechanizm umożliwiający tworzenie wielu wersji tej samej metody w ramach jednej klasy. Wersje te mogą różnić się liczbą lub rodzajem parametrów, co pozwala na wywołanie tej samej metody z różnymi argumentami, poprawiając w ten sposób czytelność i użyteczność kodu.

Jeśli chodzi o przeciążanie operatorów, to podejście pozwala na modyfikację zachowania standardowych operatorów dla typów danych zdefiniowanych przez użytkownika. Na przykład możliwe jest zdefiniowanie sposobu działania operatora dodawania z instancjami własnych klas, co czyni kod bardziej intuicyjnym i łatwiejszym w utrzymaniu.

W związku z tym polimorfizm, wraz z przeciążaniem metod i operatorów, odgrywa ważną rolę w tworzeniu bardziej wszechstronnego i adaptowalnego oprogramowania, umożliwiając programistom efektywne zarządzanie złożonością i utrzymanie wysokiego stopnia abstrakcji.

Projektując klasę, redukujemy ją do niezbędnych atrybutów i metod, które są istotne w danym kontekście, unikając zbędnych szczegółów i ignorując mniej istotne elementy. Na przykład wszystkie drapieżniki mają metodę „hunt”, dlatego każde zwierzę należące do tej kategorii będzie miało domyślnie zdolność polowania.

Przeanalizujmy klasę Predator:

Ta klasa łączy wszystkich przedstawicieli świata zwierząt związanych z drapieżnikami, w tym na przykład koty:

Kot ma swoje własne cechy: „name” to imię, a „color” to kolor. Jednakże, będąc potomkinią drapieżników, posiada zdolności łowieckie:

Przeczytaj także:

Klasy abstrakcyjne i interfejsy: szósta część przewodnika po programowaniu obiektowym.

Praktyczne zastosowanie programowania obiektowego w Pythonie

Kontynuujmy naszą pracę twórczą i opracowujmy nowe klasy.

Wyobraź sobie: Twój znajomy otrzymał zaproszenie na luksusowe wydarzenie w elitarnym klubie. Panuje tam dość nietypowa etykieta: w różnych momentach wieczoru uczestnicy muszą spożywać ściśle określone napoje. Co więcej, każdy z nich musi być wypity w zależności od sytuacji – albo w małych porcjach 10 ml, albo nieco większych, 20 ml, albo jednym łykiem, opróżniając szklankę do pełna. Warto również zauważyć, że wielkość łyku tego samego napoju może się nieoczekiwanie zmienić w trakcie imprezy.

Uczysz się mnóstwa zawiłych zasad i postanawiasz pomóc znajomemu, ale możesz się z nim komunikować tylko przez słuchawkę. W tym przypadku Twój znajomy staje się pośrednikiem, z którym wchodzisz w interakcję w kwestii napojów.

Zacznijmy od opracowania klasy o nazwie Drink.

Każdy napój ma pewne cechy, takie jak nazwa, cena w rublach i objętość w mililitrach. Załóżmy, że na naszej imprezie używamy szklanki o stałej objętości 200 ml, podczas gdy pozostałe parametry mogą się różnić w zależności od konkretnego napoju.

W związku z tym objętość można uznać za parametr statyczny, który pozostaje taki sam dla wszystkich instancji klasy. Natomiast nazwa i cena są cechami dynamicznymi, ponieważ nie dotyczą całej klasy jako całości, ale poszczególnych obiektów, a ich wartości są ustawiane dopiero po utworzeniu tych obiektów.

Utworzymy obiekt kawy, który będzie reprezentował instancję klasy Drink. W kontekście naszego przykładu proces tworzenia tego nowego obiektu można porównać do składania zamówienia na świeżo zaparzony napój.

Mamy teraz obiekt kawy, który zawiera statyczny atrybut o nazwie volume, odziedziczony z klasy Drink, a także dynamiczne atrybuty o nazwach name i price, ustawione podczas tworzenia tego obiektu. Spróbujmy uzyskać dostęp do tych atrybutów:

Ponieważ atrybuty statyczne są ustawiane na poziomie klasy, można uzyskać do nich dostęp nie tylko poprzez instancje obiektów, ale także bezpośrednio poprzez samą klasę.

Nie będziemy mogli uzyskać dostępu do atrybutów dynamicznych.

Złożyłeś zamówienie na napój i teraz musisz podjąć jakieś działanie. Ponieważ komunikujesz się przez słuchawkę, nie masz wizualnej kontroli nad stanem napoju Twojego znajomego. W tej sytuacji warto poprosić go o poinformowanie Cię o aktualnej sytuacji. Aby to zaimplementować, dodamy nową metodę do klasy Drink:

Firma bierze pierwszy łyk. Wydajmy znajomemu polecenie połączenia się z nami. Aby to zrobić, musimy dodać kolejny atrybut – remains – który będzie wskazywał, ile mililitrów napoju pozostało. Początkowo ta wartość będzie równa pełnej objętości pojemnika. Następnie napiszemy metodę, która poinformuje użytkownika, ile dokładnie ma wypić, zgodnie z zasadami etykiety:

Typy dostępu w Pythonie

Aby uniknąć konieczności ciągłego sprawdzania, czy wystarczy napoju na kolejny łyk, utworzymy metodę pomocniczą _is_enough. Następnie zaktualizujemy metodę sip i dodatkowo zaimplementujemy metody small_sip i drink_all.

Warto również zwrócić uwagę na jedną ważną rzecz: w wierszu coffee.remains = 10 dokonaliśmy zewnętrznej zmiany w obiekcie, przypisując jego atrybutowi remains wartość 10. Jest to możliwe, ponieważ w Pythonie wszystkie atrybuty i metody są domyślnie publicznie dostępne.

Programowanie obiektowe (OOP) zapewnia różne poziomy dostępu, które pomagają kontrolować interakcję z wewnętrznymi komponentami klasy. Poziomy te obejmują public, protected i private. Metody i atrybuty chronione są dostępne nie tylko w samej klasie, ale także w jej podklasach. Natomiast elementy prywatne mogą być używane tylko w obrębie klasy i nie można ich wywoływać nawet z klas pochodnych.

Język programowania Python stosuje następującą konwencję: atrybuty i metody chronione są poprzedzone pojedynczym podkreśleniem (_example), podczas gdy elementy prywatne są poprzedzone podwójnym podkreśleniem (__example). W szczególności w metodzie _is_enough użyliśmy pojedynczego podkreślenia, aby wskazać jej status ochrony.

W Pythonie samo oznaczenie atrybutów i metod jako chronionych lub prywatnych nie uniemożliwia dostępu do nich spoza programu. Nadal możemy wywołać metodę _is_enough z dowolnego miejsca w programie:

Atrybutów i metod prywatnych nie można wywołać bezpośrednio, ale istnieje sposób na obejście tego ograniczenia:

Należy pamiętać, że ignorowanie poziomów dostępu jest uważane za naruszenie kluczowej zasady programowania obiektowego: enkapsulacji. Dlatego, o ile jest to technicznie możliwe, praktycy Pythona doszli do porozumienia, aby nie wchodzić w interakcje z metodami chronionymi i prywatnymi z zewnątrz.

Dlatego atrybuty volume i remains zostaną zabezpieczone, aby przypomnieć im, że powinny być używane wyłącznie w klasie Drink i jej podklasach. Struktura wygląda teraz następująco:

Zasady dziedziczenia w Pythonie

Impreza nabiera tempa, gdy dzieje się coś nieoczekiwanego. Twój znajomy, który zdążył już zaadaptować się do atmosfery, a nawet zaczął się cieszyć imprezą, nagle szepcze Ci do ucha, że ​​wieczór znów robi się napięty. „Ogłosili czas na sok!” – relacjonuje zaniepokojony. „Każdy sok ma swój własny, niepowtarzalny smak, trudno go rozgryźć!”

Zgadza się. Houston, napotkaliśmy problem. Na pierwszy rzut oka sok wydaje się zwykłym napojem: można go popijać lub wypić jednym haustem, ma swoją cenę i objętość. Jednak, jak zauważył pan Shnurov, istnieje jeden ważny niuans: w przeciwieństwie do innych napojów, sok ma unikalną cechę, która nie jest typowa dla kategorii napojów – smak owoców lub jagód, z których został zrobiony.

Bez paniki: nawet w najbardziej złożonych sytuacjach zawsze istnieją co najmniej dwa rozwiązania. Moglibyśmy oczywiście po prostu utworzyć dokładną kopię klasy Drink i wprowadzić niezbędne zmiany. Wybierzemy jednak bardziej eleganckie podejście – opracujemy klasę Juice, która odziedziczy właściwości klasy Drink.

Należy zauważyć, że klasa pochodna nie ma możliwości bezpośredniego dostępu do prywatnych metod i atrybutów klasy nadrzędnej.

Tworzymy instancję klasy Juice i implementujemy metody odziedziczone po klasie nadrzędnej Drink.

Klasa Drink, będąc klasą nadrzędną, przyznała swojej klasie pochodnej dostęp do swoich atrybutów i metod, co zaoszczędziło nam konieczności ich przepisywania.

Teraz zwróćmy uwagę na atrybut name. W klasie Drink, gdy mieliśmy możliwość zamawiania różnych napojów – od kawy i herbaty po kwas chlebowy i koktajle – logiczne było każdorazowe podawanie nazwy. Jednak w klasie Juice nazwa jest zawsze taka sama: „sok”. Nasuwa się pytanie: po co żądać atrybutu name przy każdym zamówieniu soku?

W klasie Juice zmienimy metodę __init__, ustawiając wartość atrybutu name na „sok”. Następnie utwórzmy ponownie instancję soku jabłkowego:

Po utworzeniu obiektu apple_juice następuje seria sekwencyjnych działań, które obejmują inicjalizację jego właściwości i ustawienie wartości powiązanych z tym obiektem. Najpierw tworzona jest instancja, która może zawierać określone parametry, takie jak składniki, objętość, a nawet informacje o producencie. Następnie do tych parametrów przypisywane są odpowiednie wartości, co pozwala obiektowi apple_juice stać się pełnoprawnym przedstawicielem swojej klasy. Proces ten może również obejmować wykonywanie różnych metod zapewniających niezbędną funkcjonalność i umożliwiających obiektowi interakcję z innymi elementami systemu.

1. Tworzymy instancję klasy Juice, przekazując wartości price i taste jako argumenty do jej inicjatora.

Inicjator klasy Juice używa funkcji super(), aby wywołać inicjator swojej klasy nadrzędnej, Drink.

Inicjator klasy Drink wymaga dwóch argumentów: name i price. Argument name otrzymuje wartość statycznego atrybutu _juice_name, który został zdefiniowany w klasie Juice. Podczas gdy wartość argumentu price jest pobierana z inicjatora klasy Juice,

Konstruktor klasy Drink ustawia wartości atrybutów name, price i _remains.

Konstruktor klasy Juice ustawia wartość atrybutu taste.

Jeśli nadal masz problem ze zrozumieniem, skąd pochodzą dane i dokąd trafiają, spójrz na poniższy diagram. Podświetla trasy, przez które atrybuty są przypisywane do swoich wartości w różnych kolorach:

Obraz: Skillbox Media

Więc powiedziałeś swojemu znajomemu, że nie ma Nie musisz deklarować, że chcesz sok — w przeciwnym razie inni mogą pomyśleć, że to człowiek o wiejskich nawykach. Wszyscy już wiedzą, że nie prosi o kompot. Zwróć jednak uwagę, co się stanie, gdy poprosimy go o podanie informacji o obiekcie klasy Juice:

Mówi nam, że pije sok, ale nie precyzuje, który. Aby uzyskać więcej informacji od naszego przyjaciela, powinniśmy zmodyfikować metodę drink_info z klasy nadrzędnej:

W ten sposób zaimplementowaliśmy zasadę polimorfizmu. Niezależnie od tego, jaki napój preferuje nasz przyjaciel — czy to kawę, czy sok — możemy uzyskać od niego informacje o jego wyborze za pomocą tego samego polecenia drink_info. A nasz przyjaciel zdecyduje, jak odpowiedzieć: jeśli lubi sok, opowie nam o jego smaku, a jeśli lubi jakiś inny napój, podzieli się jego nazwą.

Należy zauważyć, że w Pythonie wszystkie klasy automatycznie dziedziczą z nadklasy obiektów, co pozwala im na dostęp do jej atrybutów i metod. Do takich odziedziczonych metod należą na przykład wbudowane funkcje __new__, __init__, __del__ i kilka innych.

Impreza powoli dobiega końca, a Twój znajomy jak dotąd udawało się pozostać niezauważonym. Czasy popijania dobiegły końca i teraz każdy może delektować się tym, na co ma ochotę. Wygląda na to, że czas na relaks. Jednak, jak już wiesz, mamy świetnego gospodarza i ekscytujące konkursy: goście są zaskoczeni nowym pomysłem. Goście są proszeni o zajęcie miejsc przy stolikach na podstawie ceny swoich drinków. Wszyscy zaczynają wykrzykiwać ceny swoich kieliszków, a kelnerzy wskazują im nowe miejsca. Twój znajomy nadal jest zdezorientowany, ale na pewno mu pomożemy.

Ponieważ możliwe jest określenie ceny dowolnego napoju, utworzymy metodę tell_price w klasie Drink, która będzie automatycznie dostępna w klasie Juice, która jest klasą potomną.

Teraz upewnimy się, że działa ona zarówno z obiektami Drink, jak i Juice.

Drodzy koledzy, wyobraźcie sobie taką sytuację: Wasz znajomy wychodzi z imprezy z nową dziewczyną i zaproszeniem na kolejne wydarzenie. Wszystko to stało się możliwe dzięki Wam i zasadom programowania obiektowego.