Rozłóżmy zagadnienie na czynniki pierwsze. Najpierw zdefiniujmy najprostszą postać klasy Car
:
Kopiuj
class Car:
def __init__(self, brand, year_production):
self.brand = brand
self.year_production = year_production
def __str__(self):
return "{0} was produced in {1}".format(self.brand, self.year_production)
Przejdźmy do wykorzystania klasy:
Kopiuj
my_car = Car("MINI Cooper", 2018)
print(my_car)
print(my_car.brand)
print(my_car.year_production)
my_car.year_production = 2019
print(my_car.year_production)
Widzimy, że możemy zmienić bardzo łatwo rok produkcji samochodu, co jest w realnym życiu niemożliwe. Rok produkcji powinien być niezmienny, zresztą podobnie jak marka samochodu (nie zrobisz z Malucha Poloneza i odwrotnie). Chcielibyśmy, aby nie dało się tej wartości zmienić. Możemy więc ukryć te atrybuty (marka, rok produkcji) i stworzyć metodę, która będzie służyć do ich odczytywania:
Kopiuj
class Car:
def __init__(self, brand, year_production):
self.__brand = brand
self.__year_production = year_production
def brand(self):
return self.__brand
def year_production(self):
return self.__year_production
def __str__(self):
return "{0} was produced in {1}".format(self.__brand, self.__year_production)
Teraz możemy stworzyć nowy obiekt i odczytać jego wartości:
Kopiuj
my_car = Car("BMW", 2010)
print(my_car.brand())
print(my_car.year_production())
Przy okazji zauważmy, że musimy ukryć atrybuty, dodając prefiks w postaci __
przed nazwą zmiennej. Gdyby tego nie zastosować, to wciąż można by zmienić atrybuty z pomocą zwykłego przypisania, tj. my_car.brand = "Opel"
. Z powyższego kodu widzimy również, że aby pobrać atrybut, musimy wywołać metodę, a więc dodać na końcu parę (
, )
, np. my_car.brand()
. Takie rozwiązanie niekoniecznie jest eleganckie. Lepiej byłoby nie używać nawiasów. W takim przypadku możemy wykorzystać dekorator @property
:
Kopiuj
class Car:
def __init__(self, brand, year_production):
self.__brand = brand
self.__year_production = year_production
@property
def brand(self):
return self.__brand
@property
def year_production(self):
return self.__year_production
def __str__(self):
return "{0} was produced in {1}".format(self.__brand, self.__year_production)
Teraz możemy pobierać prywatną wartość i nie możemy jej ponownie ustawić, bo program rzuci wyjątkiem:
Kopiuj
my_car = Car("Opel", 2007)
print(my_car.year_production)
my_car.year_production = 2012
print(my_car)
Ostatnia wersja klasy wydaje się więc tym, czego szukałeś. Ustawiasz atrybuty obiektu tylko raz w konstruktorze i to się nie może zmienić. Dodatkowo wystawiasz publiczny interfejs w postaci metod, które zwracają Ci odpowiednie wartości atrybutów. Z pomocą @property
pozbywasz się brzydkich nawiasów. Ale to koniec. Załóżmy, że udostępniłeś taką klasę, np. publikując ją na Python Package Index (odradzam, bo to trywialna klasa). Po jakimś czasie pisze do Ciebie znajomy, że przecież to bezsensu, że wyprodukowano samochód w 1642 roku, bo wtedy jeszcze nie było samochodów. Sprawdzasz na Wikipedii, że pierwsza masowa produkcja aut rozpoczęła się w 1903 roku (Ford). Chciałbyś to jakoś uwzględnić w swoim kodzie, który już wykorzystują inni. Na szczęście dekorator @property
daje Ci taką możliwość. Z jego pomocą możesz przerobić klasę tak, by ustawić setter:
Kopiuj
class Car:
def __init__(self, brand, year_production):
self.__brand = brand
self._year_production = year_production
@property
def brand(self):
return self.__brand
@property
def year_production(self):
return self.__year_production
@year_production.setter
def _year_production(self, year):
first_car_year_production = 1903
if year < first_car_year_production:
raise ValueError("Year of production must be >= {}".format(first_car_year_production))
else:
self.__year_production = year
def __str__(self):
return "{0} was produced in {1}".format(self.__brand, self.__year_production)
Zauważmy, że metodzie __init__
zmieniliśmy ostatnią linię. Wywołuje ona wewnętrzną metodę _year_production(self, year)
. Mogliśmy zostawić year_production(self, year)
, bo dzięki @property
możemy przeciążać tę metodę (drugą metodą jest getter year_production(self)
), ale prefiks _
sugeruje, że ta metoda jest metodą wewnętrzną i nie powinna być używana przez inne obiekty/programy/użytkowników. Dodatkowo jest ona oznaczona za pomocą dekoratora jako setter dla zmiennej year_production
.
Teraz już możesz tworzyć obiekty typu Car
. Jeśli ustawisz nieprawidłowy rok produkcji, to program zrzuci wyjątek. Atrybuty pobierasz z pomocą metod, ale dekorator @property
zmienia je na zwykłe atrybuty. I jeszcze jedno, metoda __repr__
służy do czegoś innego niż __str__
. Poczytaj sobie o różnicach. W każdym razie w Twoim przypadku lepiej użyć __str__
, co też uczyniłem.
property
jest kompatybilność wsteczna, co pokazałem na przykładzie kolejnych implementacji klasyCar
.