#Pythonic get() och set()

Vi skrev om vår kod i kapitlen ovan vilket gjorde att vi nu har flera metoder som inte längre fungerar. Det gör att vi behöver uppdatera våra metoder till att använda _price istället för price. Vi kan sätta detta i ett större perspektiv. Tänk om vi jobbar i ett stort system med 100+ klasser med 10+ utvecklare som skriver koden. Hur hade vår ändring ovanför påverkat alla andra klasser? All annan kod utanför vår Car klass som använde price attributet slutar också att fungera. Vilket gör att med vår ändring hade vi också behöver leta upp alla andra ställen i systemet som använder price och uppdatera den koden till att använda vår getter och setter metoder.

Stycket ovanför beskriver hur man gör i de flesta andra OO programmeringsspråken, privata attribut får en get_attributename och en set_attributename metod. Där get returnerar värdet och set ändrar värdet. Men det ställer ju till problem om vi gör en sen tidigare attribut publik till privat för då ändrar vi på det publika API:et för klassen. Python har så klart ett mer Pythonic sätt att göra det på som dessutom inte introducerar en ändring i det publika API:et.

#property decorator

Vi lär oss en till decorator, tidigare kollade vi på @staticmethod som gör att en metod blir statisk. Nu ska vi lära oss @property decorator.

Vi har den gamla lösningen med get och set.

class Car():
    ...
    def __init__(self, model, price):
        ...
        self._price = price

    ...

    def get_price(self):
        return self._price

    def set_price(self, new_price):
        if float(new_price) / float(self._price) > 0.7:
            self._price = new_price
            print(f"New price is {self._price}")
        else:
            raise ValueError("New price is too low. You can max lower it with 30%")

Vi börjar med att göra get method pythonic med @decorator.

class Car():
    ...

    @property
    def price(self):
        print("getter method called")
        return self._price

>>> car = Car("volvo", 200000)
>>> print(car.price)
getter method called
200000
>>> print(car._price)
200000

Magic!

price() metoden anropas utan att vi skriver med (). Det är en av effekterna av @property decorator. Vi har _price som ett privat attribut och skyddar det samtidigt som vi har kvar det publika API:et med price, fast det är egentligen en metod.

Vi gör också set metoden till en decorator.

class Car():
    ...

    @property
    def price(self):
        print("getter method called")
        return self._price

    @price.setter
    def price(self, new_price):
        print("setter method called")
        if float(new_price) / float(self._price) > 0.7:
            self._price = new_price
            return "New price is " + str(self._price)

Här använder vi en annan decorator, @price.setter. När vi använder @propertyprice metoden skapas ett property objekt och car.price är egentligen det objektet och inte metoden. Fast det objektet anropar vår metod när koden försöker läsa objektet som ett värde, car.price. @price.setter gör att set moden för price läggs till i price objektet och anropas om koden tilldelar till det objektet, car.price = x.

Vi kollar hur det ser ut när vi använder det i koden.

>>> car = Car("volvo", 200000)
>>> car.price = 180000
setter method called
>>> print(car.price)
getter method called
180000
>>> car.price = 50000
setter method called
ValueError: New price is too low. You can max lower it with 30%

Python anropar olika metoder baserat på hur vi använder attributet i koden, om vi läser eller tilldelar. Nu har vi samma funktionalitet som med get och set metoden samtidigt som vår kod har kvar det publika API:et vilket gör att vår ändring från publik till privat inte förstör annan kod som använder Car objekt. Även de andra metoderna som inte fungerade förut fungerar igen.

class Car():
    wheels = 4
    car_count = 0

    def __init__(self, model, price):
        self.model = model
        self._price = price

        Car.car_count += 1
        self.car_nr = Car.car_count

    @property
    def price(self):
        print("getter method called")
        return self._price

    @price.setter
    def price(self, new_price):
        print("setter method called")
        if float(new_price) / float(self._price) > 0.7:
            self._price = new_price
        else:
            raise ValueError("New price is too low. You can max lower it with 30%")

    def present_car(self):
        return "The model {m} costs {p}$.".format(
            m=self.model, p=self._price
        )

    def __add__(self, other):
        if isinstance(other, Car):
            return self.price + other.price
        if isinstance(other, int):
            return self.price + other
        raise ValueError("Car doesn't not support addition with object")

    def __iadd__(self, other):
        if isinstance(other, Car):
            self.price += other.price
            return self
        if isinstance(other, int):
            self.price += other
            return self
        raise ValueError("Car doesn't not support addition with object")

    @staticmethod
    def calculate_price_reduction(price):
        return int(price * 0.66)

    def reduce_price(self):
        self.price = self.calculate_price_reduction(self.price)
        return "Priset för {m} är nu {p}.".format(m=self.model, p=self.price)

    @classmethod
    def wheel_message(cls):
        print("A car normally have {nr} wheels".format(nr=cls.wheels))


>>> car = Car("volvo", 200000)
>>> print(car.present_car())
The model volvo costs 200000$.
>>> print(car + 1000)
getter method called
201000
>>> car += 3100
getter method called
setter method called
>>> print(car.price)
getter method called
203100

Om ni har svårt att hänga med i alla anrop, kopiera in koden i Thonny eller annan debugger och stega igenom koden! Det kommer hjälpa er att förstå koden mycket bättre än att bara titta på den och exekvera den.

För en längre och lite mer djupgående förklaring av @property kan ni läsa Python @property decorator.

#Revision history

  • 2020-01-16: (B, aar) Finputsad inför VT20.
  • 2018-11-18: (A, aar) Första versionen, uppdelad av större dokument.

Document source.