Dodawanie zachowania przy użyciu metod

Ukończone

Ostatecznym celem systemu jest wygenerowanie przydatnych danych wyjściowych. W tym celu trzeba przetworzyć dane wejściowe. Podczas przetwarzania może być potrzebna pomoc różnych metod i danych. W programowaniu obiektowym metody i dane umieszcza się w obiektach. Aby przetworzyć dane wejściowe i wygenerować wynik w programowania obiektowego, potrzebne są metody.

Metody w programowaniu obiektowym

Niezależnie od używanego podejścia metody umożliwiają wykonanie akcji. Tą akcją może być obliczenie wykonane na danych wejściowych lub zmiana wartości zmiennej.

Metody na obiektach w usłudze OOP mają dwie odmiany:

  • Metody zewnętrzne, które mogą być wywoływane przez inne obiekty.
  • Metody wewnętrzne, które nie są dostępne dla innych obiektów. Ponadto metody wewnętrzne ułatwiają wykonywanie zadania uruchomionego przez wywołanie metody zewnętrznej.

Niezależnie od typu metody mogą zmieniać wartość atrybutu obiektu; innymi słowy, jego stan.

Pojęcie stanu oraz to, kto i co może go zmienić, jest bardzo ważne. To istotna część projektowania klas i obiektów. Te pytania prowadzą nas do kolejnej części, dotyczącej hermetyzacji.

Hermetyzacja: Ochrona danych

Ogólnym założeniem hermetyzacji jest to, że dane w obiekcie są wewnętrzne, czyli dotyczą tylko tego obiektu. Dane są potrzebne do prawidłowego działania obiektu i metod, czyli do wykonania określonego zadania. Gdy mówisz, że dane są wewnętrzne, mówisz, że powinny być chronione przed innymi manipulacjami zewnętrznymi, a raczej niekontrolowanymi manipulacjami zewnętrznymi. Pytanie brzmi: dlaczego?

Do czego służy

Wyjaśnijmy, dlaczego dane nie powinny być bezpośrednio dostępne dla innych obiektów. Oto kilka przykładów:

  • Nie trzeba wiedzieć, co jest wewnątrz. Prowadząc samochód, możesz wcisnąć pedał, aby sterować sprzęgłem, dodać gazu lub zahamować. Ponieważ prowadzisz samochód na wyższym poziomie, nie dbasz o to, co dzieje się pod spodem; jak samochód wykonuje akcję. Tak samo jest z kodem. W większości przypadków nie musisz wiedzieć, w jaki sposób obiekt coś robi, o ile istnieje metoda, którą możesz wywołać, aby uzyskać oczekiwany efekt.

  • Nie należy wiedzieć, co jest wewnątrz. Wyobraź sobie, że zamiast pedałów do interakcji z samochodem, na przykład do przyspieszania, masz użyć śrubokrętu czy lutownicy. Brzmi groźnie, prawda? To dlatego, że jest to groźne. Albo weźmy bardziej konkretny przykład — klasę kwadratu zawierającą następujący kod:

    class Square:
         def __init__(self):
             self.height = 2
             self.width = 2
         def set_side(self, new_side):
             self.height = new_side
             self.width = new_side
    
    square = Square()
    square.height = 3 # not a square anymore
    

    W przykładzie z kwadratem zaburzamy koncepcję tego, czym jest kwadrat, ustawiając zmienną height (wysokość). Kwadrat został zakodowany tak, że trzeba wywołać metodę ustawiającą długość boku set_side(), aby działał prawidłowo. Bezpieczniej jest pozwolić, aby to obiekt zajmował się danymi. Niemal w każdym przypadku lepiej jest wybrać interakcję za pośrednictwem metody niż wprost ustawiać dane.

Poziomy dostępu

Jak można ochronić klasę i obiekt przed niepożądanym manipulowaniem danymi? Odpowiedzią są poziomy dostępu. Dane ze świata zewnętrznego i innych obiektów można ukryć, oznaczając dane i funkcje określonymi słowami kluczowymi. Te słowa kluczowe nazywane są modyfikatorami dostępu.

W języku Python dane ukrywa się, dodając odpowiednie prefiksy do nazw atrybutów. Jedno podkreślenie (_) na początku nazwy to sygnał dla świata zewnętrznego, że prawdopodobnie nie należy ruszać tych danych. Po zmodyfikowaniu klasy kwadratu uzyskasz taki kod:

  class Square:
      def __init__(self):
          self._height = 2
          self._width = 2
      def set_side(self, new_side):
          self._height = new_side
          self._width = new_side

  square = Square()
  square._height = 3 # not a square anymore

Jedno podkreślenie na początku oznacza w języku Python, że dane są chronione, ale nadal można je modyfikować. Czy można to zrobić lepiej? Tak, można użyć dwóch znaków podkreślenia (__) na początku, aby oznaczyć dane jako prywatne. Klasa kwadratowa powinna teraz wyglądać następująco:

  class Square:
      def __init__(self):
          self.__height = 2
          self.__width = 2
    def set_side(self, new_side):
          self.__height = new_side
          self.__width = new_side

  square = Square()
  square.__height = 3 # raises AttributeError

Świetnie, jesteśmy więc bezpieczni. Ale czy dane są całkowicie chronione? Nie do końca. Język Python po prostu zmienia nazwę bazowych danych. Wprowadzając następujący kod, nadal można zmienić ich wartość:

square = Square()
square._Square__height = 3 # is allowed

W wielu innych językach, w których wdrożono ochronę danych, problem ten rozwiązano inaczej. Język Python wyróżnia się tym, że ochrona danych opiera się raczej na poziomach sugestii, a nie jest ściśle wdrożona.

Co to są metody pobierające i metody ustawiające?

Jak dotychczas powiedzieliśmy, dane nie powinny być na ogół dostępne z zewnątrz. Dane są problemem obiektu. Podobnie jak od wszystkich reguł i zdecydowanych zaleceń, są jednak wyjątki. Czasami trzeba zmienić dane lub zmienić je łatwiej niż trzeba dodać znaczną ilość kodu.

Metody pobierające i metody ustawiające, inaczej metody dostępu i metody zmieniające, są przeznaczone do odczytywania i zmieniania danych. Metody pobierające służą do umożliwienia odczytu danych wewnętrznych z zewnątrz, co nie brzmi źle, prawda? Natomiast metody ustawiające mają zdolność bezpośredniego zmieniania danych. Chodzi o to, aby metoda ustawiająca pełniła funkcję strażnika, zapobiegając wprowadzeniu złej wartości. Wróćmy do naszej klasy kwadratu i zobaczmy, jak działają metody pobierające i ustawiające w praktyce:

  class Square:
      def __init__(self):
          self.__height = 2
          self.__width = 2
      def set_side(self, new_side):
          self.__height = new_side
          self.__width = new_side
      def get_height(self):
          return self.__height
      def set_height(self, h):
          if h >= 0:
              self.__height = h
          else:
              raise Exception("value needs to be 0 or larger")

  square = Square()
  square.__height = 3 # raises AttributeError

Metoda set_height() zapobiega ustawieniu wartości ujemnej. Jeśli spróbujesz to zrobić, zgłasza wyjątek.

Używanie dekoratorów z metodami pobierającymi i ustawiającymi

Dekoratory to ważny temat w języku Python. Są one częścią większego zagadnienia nazywanego metaprogramowaniem. Dekoratory to funkcje przyjmujące funkcję jako dane wejściowe. Dzięki temu można zakodować funkcje do wielokrotnego użytku jako funkcje dekoratora, a następnie przy ich użyciu dekorować inne funkcje. Celem jest dodanie do funkcji wcześniej niedostępnych możliwości. Dekorator umożliwia na przykład dodanie pól do obiektu, pomiar czasu wywoływania funkcji i nie tylko.

W kontekście programowania obiektowego oraz metod pobierających i ustawiających dekorator @property umożliwia pominięcie części powtarzalnego kodu podczas dodawania metod pobierających i ustawiających. Dekorator @property wykonuje następujące czynności:

  • Tworzy pole zapasowe: podczas dekorowania funkcji za pomocą @property dekoratora tworzy pole prywatne. Możesz zastąpić to działanie innym, ale dobrze jest móc skorzystać z działania domyślnego.
  • Identyfikuje metodę ustawiającą: metoda ustawiająca może zmienić pole zapasowe.
  • Identyfikuje element pobierający: ta funkcja powinna zwrócić pole zapasowe.
  • Identyfikuje funkcję delete: ta funkcja może usunąć pole.

Zobaczmy ten dekorator w akcji:

class Square:
    def __init__(self, w, h):
        self.__height = h
        self.__width = w
  
    def set_side(self, new_side):
        self.__height = new_side
        self.__width = new_side

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, new_value):
        if new_value >= 0:
            self.__height = new_value
        else:
            raise Exception("Value must be larger than 0")

W powyższym kodzie do funkcji height() dodano dekorator @property. Akcja dekorowania powoduje utworzenie prywatnego pola __height. Pole __height nie jest zdefiniowane w funkcji __init__(), ponieważ definiuje je sam dekorator. Istnieje również inna dekoracja — @height.setter. Ta dekoracja wskazuje podobną metodę height() jako metodę ustawiającą. Nowa metoda height przyjmuje inny parametr value jako drugi parametr.

Możliwość manipulowania wysokością niezależnie od szerokości nadal może powodować problem. Rozważając wprowadzanie metod pobierających i ustawiających, należy dobrze rozumieć, do czego służy dana klasa, ponieważ wprowadzasz przy tym ryzyko.