Python property dekoratorius: getteriai ir setteriai

Kas tie property dekoratoriai ir kam jų reikia?

Jei programuojate Python kalba, tikriausiai esate girdėję apie property dekoratorių. Bet jei esate kaip aš prieš kelerius metus – žiūrite į šitą @property dalyką ir galvojate „kam čia dar vienas komplikacijos sluoksnis?” – tai šis straipsnis jums.

Property dekoratorius leidžia Python klasėse sukurti metodus, kurie veikia kaip atributai. Skamba keistai? Iš esmės tai reiškia, kad galite turėti visą logiką, validaciją ir kontrolę, kokią suteikia metodai, bet naudoti juos taip paprastai kaip paprastus kintamuosius. Nereikia rašyti tų bjaurių skliaustų obj.get_name() – tiesiog rašote obj.name.

Python filosofija visada buvo „mes visi suaugę žmonės čia”. Kitose kalbose, kaip Java ar C++, privačių kintamųjų apsauga yra griežta – ten visur getteriai ir setteriai. Python’e mes paprastai tiesiog naudojame viešus atributus ir pasitikime, kad programuotojai nedarys kvailų dalykų. Bet kartais reikia daugiau kontrolės, ir čia property tampa neįkainojamas įrankis.

Paprasta property pavyzdys

Pradėkime nuo paprasčiausio pavyzdžio. Tarkime, kuriate klasę darbuotojui:

class Darbuotojas:
    def __init__(self, vardas, atlyginimas):
        self.vardas = vardas
        self.atlyginimas = atlyginimas

darb = Darbuotojas("Jonas", 2000)
print(darb.atlyginimas)  # 2000

Viskas veikia puikiai, kol neatsiranda reikalavimas: atlyginimas negali būti neigiamas. Galėtumėte pridėti validaciją į __init__, bet kas nutiks, jei kas nors tiesiog parašys darb.atlyginimas = -5000? Validacija nesuveiks.

Čia ir ateina property į pagalbą:

class Darbuotojas:
    def __init__(self, vardas, atlyginimas):
        self.vardas = vardas
        self._atlyginimas = atlyginimas  # Pastebėkite apatinį brūkšnelį
    
    @property
    def atlyginimas(self):
        return self._atlyginimas
    
    @atlyginimas.setter
    def atlyginimas(self, reiksme):
        if reiksme < 0:
            raise ValueError("Atlyginimas negali būti neigiamas")
        self._atlyginimas = reiksme

darb = Darbuotojas("Jonas", 2000)
print(darb.atlyginimas)  # 2000 - veikia kaip paprastas atributas
darb.atlyginimas = 2500  # Veikia setter su validacija
darb.atlyginimas = -100  # Klaida: ValueError

Matote, kas įvyko? Dabar atlyginimas atrodo kaip paprastas atributas, bet už kulisų veikia metodai su visa reikiama logika.

Kaip tai veikia po gaubtu

Property dekoratorius iš tikrųjų sukuria specialų objektą, kuris perima atributo prieigą. Kai bandote gauti reikšmę (obj.atlyginimas), iškviečiamas getter metodas. Kai bandote nustatyti (obj.atlyginimas = 2500), iškviečiamas setter metodas.

Tas _atlyginimas su apatiniu brūkšneliu yra Python konvencija, kuri sako "šitas atributas yra vidinis, neliesk jo tiesiogiai". Tai ne privatus kintamasis tikrąja prasme – galite jį pasiekti, jei tikrai norite. Bet tai signalas kitiems programuotojams (ir jums po šešių mėnesių), kad reikėtų naudoti property interfeisą.

Beje, galite turėti property tik su getter'iu, be setter'io. Tai sukuria "read-only" atributą:

class Apskritimas:
    def __init__(self, spindulys):
        self.spindulys = spindulys
    
    @property
    def plotas(self):
        return 3.14159 * self.spindulys ** 2

apskr = Apskritimas(5)
print(apskr.plotas)  # 78.53975
apskr.plotas = 100  # Klaida: AttributeError - nėra setter'io

Čia plotas yra skaičiuojamas dinamiškai iš spindulio, ir jį keisti neturi prasmės – jei norite kitą plotą, keiskite spindulį.

Realūs panaudojimo atvejai

Gerai, teorija suprantama, bet kada tikrai naudotumėte property? Štai keletas situacijų iš tikro gyvenimo:

Validacija ir duomenų apsauga. Jau matėme su atlyginimu. Kitas pavyzdys – el. pašto adresas:

class Vartotojas:
    def __init__(self, epastas):
        self.epastas = epastas
    
    @property
    def epastas(self):
        return self._epastas
    
    @epastas.setter
    def epastas(self, reiksme):
        if '@' not in reiksme:
            raise ValueError("Neteisingas el. pašto formatas")
        self._epastas = reiksme.lower()  # Visada saugom mažosiomis

Lazy loading. Kartais turite duomenis, kurių gavimas brangus (pvz., užklausa į duomenų bazę), bet jie ne visada reikalingi:

class Produktas:
    def __init__(self, id):
        self.id = id
        self._atsiliepimai = None  # Dar nekrauti
    
    @property
    def atsiliepimai(self):
        if self._atsiliepimai is None:
            # Krauname tik kai reikia
            self._atsiliepimai = self._krautiAtsiliepimus()
        return self._atsiliepimai
    
    def _krautiAtsiliepimus(self):
        # Čia būtų DB užklausa
        return ["Puikus produktas!", "Rekomenduoju"]

Computed properties. Kai reikšmė skaičiuojama iš kitų atributų:

class Stačiakampis:
    def __init__(self, plotis, aukstis):
        self.plotis = plotis
        self.aukstis = aukstis
    
    @property
    def plotas(self):
        return self.plotis * self.aukstis
    
    @property
    def perimetras(self):
        return 2 * (self.plotis + self.aukstis)

API suderinamumas. Tarkime, turėjote klasę su paprastu atributu, bet dabar reikia pridėti logiką. Su property galite tai padaryti nekeisdami kodo, kuris jau naudoja jūsų klasę:

# Senasis kodas
class Knyga:
    def __init__(self, pavadinimas):
        self.pavadinimas = pavadinimas

# Naujas kodas - pridėta validacija, bet API nepasikeitė
class Knyga:
    def __init__(self, pavadinimas):
        self.pavadinimas = pavadinimas
    
    @property
    def pavadinimas(self):
        return self._pavadinimas
    
    @pavadinimas.setter
    def pavadinimas(self, reiksme):
        if not reiksme or not reiksme.strip():
            raise ValueError("Pavadinimas negali būti tuščias")
        self._pavadinimas = reiksme.strip()

Deleter metodas ir kitos gudrybės

Be getter'io ir setter'io, property turi ir trečią komponentą – deleter'į. Jis iškviečiamas, kai naudojate del komandą:

class Failas:
    def __init__(self, kelias):
        self._kelias = kelias
        self._turinys = None
    
    @property
    def turinys(self):
        if self._turinys is None:
            with open(self._kelias, 'r') as f:
                self._turinys = f.read()
        return self._turinys
    
    @turinys.deleter
    def turinys(self):
        # Išvalome cache
        self._turinys = None
        print("Cache išvalytas")

failas = Failas("duomenys.txt")
print(failas.turinys)  # Nuskaito failą
print(failas.turinys)  # Grąžina iš cache
del failas.turinys     # Išvalo cache
print(failas.turinys)  # Vėl nuskaito failą

Deleter'iai naudojami retai, bet kartais būna naudingi resursų valdymui ar cache valymo logikai.

Dar viena naudinga smulkmena – galite naudoti property kaip funkciją, ne tik kaip dekoratorių:

class Pavyzdys:
    def __init__(self):
        self._x = 0
    
    def get_x(self):
        return self._x
    
    def set_x(self, reiksme):
        self._x = reiksme
    
    def del_x(self):
        del self._x
    
    x = property(get_x, set_x, del_x, "Dokumentacija x atributui")

Šitas būdas seniau buvo vienintelis, kol Python 2.4 versijoje atsirado dekoratoriai. Dabar jis naudojamas retai, bet gali būti naudingas, jei metodai jau egzistuoja ir nenorite jų pervadinėti.

Dažniausios klaidos ir kaip jų vengti

Property atrodo paprasti, bet yra keletas spąstų, į kuriuos lengva įkristi.

Begalinė rekursija. Tai klasika:

class Blogai:
    @property
    def vardas(self):
        return self.vardas  # KLAIDA! Kviečia save be galo
    
    @vardas.setter
    def vardas(self, reiksme):
        self.vardas = reiksme  # KLAIDA! Irgi rekursija

# Teisingai:
class Gerai:
    @property
    def vardas(self):
        return self._vardas
    
    @vardas.setter
    def vardas(self, reiksme):
        self._vardas = reiksme

Visada naudokite kitą pavadinimą vidiniam atributui (paprastai su apatiniu brūkšneliu).

Per daug logikos getter'iuose. Getter'iai turėtų būti greiti ir be šalutinių efektų. Jei jūsų getter kreipiasi į duomenų bazę ar daro sudėtingus skaičiavimus kiekvieną kartą, galbūt reikėtų apsvarstyti metodą vietoj property:

# Blogai - lėtas ir netikėtas
@property
def visi_uzsakymai(self):
    return database.query("SELECT * FROM orders WHERE user_id = ?", self.id)

# Geriau - aiškiai metodas
def gauti_uzsakymus(self):
    return database.query("SELECT * FROM orders WHERE user_id = ?", self.id)

Setter'iai su netikėtu elgesiu. Vartotojai tikisi, kad priskyrimas bus paprastas. Jei jūsų setter'is daro daug daugiau nei nustato reikšmę, tai gali būti klaidinantis:

# Įtartina
@vardas.setter
def vardas(self, reiksme):
    self._vardas = reiksme
    self.issaugoti_duomenu_bazeje()  # Netikėtas šalutinis efektas
    self.siusti_email_apie_pakeitima()  # Dar labiau netikėtas

Pamirštas setter dekoratorius. Jei turite tik @property be @vardas.setter, atributas bus read-only. Tai gali būti tyčia, bet dažnai būna klaida:

class Pavyzdys:
    @property
    def x(self):
        return self._x

obj = Pavyzdys()
obj.x = 5  # AttributeError: can't set attribute

Property vs metodai: kada naudoti ką?

Tai amžinas klausimas. Štai mano praktinės gairės:

Naudokite property, kai:

  • Reikšmė atrodo kaip objekto atributas (pvz., person.age, circle.area)
  • Getter'is greitas ir be šalutinių efektų
  • Reikšmė gali būti skaičiuojama iš kitų atributų
  • Norite pridėti validaciją prie egzistuojančio atributo
  • Tai būsena, ne veiksmas

Naudokite metodus, kai:

  • Operacija lėta (DB užklausos, tinklo kreipiniai)
  • Yra šalutinių efektų
  • Reikia parametrų (property negali priimti papildomų argumentų)
  • Tai veiksmas, ne būsena (pvz., obj.save(), obj.calculate())
  • Metodas kviečiamas retai ar specialiais atvejais

Pavyzdžiui:

class BankoSaskaita:
    # Property - tai būsena
    @property
    def balansas(self):
        return self._balansas
    
    # Metodas - tai veiksmas
    def pervesti(self, suma, gavejui):
        if suma > self._balansas:
            raise ValueError("Nepakanka lėšų")
        self._balansas -= suma
        gavejui.papildyti(suma)

Pažangesnės technikos su property

Kai įgaunate patirties, galite pradėti naudoti property sudėtingesnėms situacijoms.

Property su type checking:

class StipriaiTipizuota:
    @property
    def amzius(self):
        return self._amzius
    
    @amzius.setter
    def amzius(self, reiksme):
        if not isinstance(reiksme, int):
            raise TypeError(f"Amžius turi būti int, ne {type(reiksme)}")
        if reiksme < 0 or reiksme > 150:
            raise ValueError(f"Neįmanomas amžius: {reiksme}")
        self._amzius = reiksme

Property su logging:

import logging

class Audituojama:
    @property
    def slapta_reiksme(self):
        logging.info(f"Prieiga prie slapta_reiksme: {self._slapta_reiksme}")
        return self._slapta_reiksme
    
    @slapta_reiksme.setter
    def slapta_reiksme(self, reiksme):
        logging.info(f"Keičiama slapta_reiksme: {self._slapta_reiksme} -> {reiksme}")
        self._slapta_reiksme = reiksme

Property su cache invalidation:

class SuCache:
    def __init__(self):
        self._duomenys = []
        self._suma_cache = None
    
    @property
    def duomenys(self):
        return self._duomenys
    
    @duomenys.setter
    def duomenys(self, reiksme):
        self._duomenys = reiksme
        self._suma_cache = None  # Invalidiname cache
    
    @property
    def suma(self):
        if self._suma_cache is None:
            self._suma_cache = sum(self._duomenys)
        return self._suma_cache

Dinamiškai generuojami property. Tai pažangus triukas, bet kartais naudingas:

def sukurti_property(atributo_vardas):
    vidinis_vardas = f"_{atributo_vardas}"
    
    def getter(self):
        return getattr(self, vidinis_vardas)
    
    def setter(self, reiksme):
        print(f"Nustatoma {atributo_vardas} = {reiksme}")
        setattr(self, vidinis_vardas, reiksme)
    
    return property(getter, setter)

class Dinamiska:
    x = sukurti_property("x")
    y = sukurti_property("y")
    z = sukurti_property("z")

Ką daryti su property ir paveldėjimu

Property ir paveldėjimas kartais gali būti sudėtingi. Štai ką reikia žinoti:

Property galite override'inti vaikinėse klasėse:

class Tevine:
    @property
    def reiksme(self):
        return "Tėvinė reikšmė"

class Vaikine(Tevine):
    @property
    def reiksme(self):
        return "Vaikinė reikšmė"

obj = Vaikine()
print(obj.reiksme)  # "Vaikinė reikšmė"

Bet jei norite išplėsti, ne pakeisti, reikia šiek tiek gudrybių:

class Tevine:
    @property
    def vardas(self):
        return self._vardas
    
    @vardas.setter
    def vardas(self, reiksme):
        self._vardas = reiksme

class Vaikine(Tevine):
    @Tevine.vardas.setter
    def vardas(self, reiksme):
        # Pridedame papildomą validaciją
        if len(reiksme) < 2:
            raise ValueError("Vardas per trumpas")
        # Kviečiame tėvinį setter'į
        Tevine.vardas.fset(self, reiksme)

Arba galite naudoti super(), bet tai šiek tiek sudėtingiau su property:

class Tevine:
    def _get_vardas(self):
        return self._vardas
    
    def _set_vardas(self, reiksme):
        self._vardas = reiksme
    
    vardas = property(_get_vardas, _set_vardas)

class Vaikine(Tevine):
    def _set_vardas(self, reiksme):
        print(f"Nustatomas vardas: {reiksme}")
        super()._set_vardas(reiksme)
    
    vardas = property(Tevine._get_vardas, _set_vardas)

Kada property gali būti per daug

Property yra galingas įrankis, bet kaip ir su bet kuo galingų, galima perdaryti. Štai keletas ženklų, kad galbūt einat per toli:

Jei turite klasę, kur beveik kiekvienas atributas yra property su sudėtinga logika, galbūt jūsų klasė daro per daug. Pagalvokite apie refaktoringą į kelias mažesnes klases.

Jei jūsų property getter'is užtrunka ilgai ar turi šalutinių efektų, tai turėtų būti metodas. Property turėtų jaustis kaip atributo prieiga, ne funkcijos kvietimas.

Jei save pagaunate rašantį property, kuris tiesiog grąžina vidinį atributą be jokios papildomos logikos, tikriausiai jums jo nereikia:

# Nereikalingas property
class Nereikalinga:
    @property
    def vardas(self):
        return self._vardas
    
    @vardas.setter
    def vardas(self, reiksme):
        self._vardas = reiksme

# Tiesiog naudokite paprastą atributą
class Paprasta:
    def __init__(self):
        self.vardas = None

Prisiminkite Python filosofiją: "Simple is better than complex". Property naudokite tada, kai jie prideda vertės – validaciją, skaičiavimus, ar API suderinamumą. Ne tiesiog dėl to, kad Java programuotojai taip daro.

Praktiniai patarimai kasdieniam darbui

Baigiant, štai keletas praktinių patarimų, kuriuos išmokau per metus:

Dokumentuokite savo property. Ypač jei jie daro ką nors netikėto:

@property
def pilnas_vardas(self):
    """Grąžina pilną vardą formato 'Vardas Pavardė'.
    
    Jei pavardė nenustatyta, grąžina tik vardą.
    """
    if self.pavarde:
        return f"{self.vardas} {self.pavarde}"
    return self.vardas

Būkite nuoseklūs. Jei naudojate property vienoje klasėje, panašios funkcionalumo klasės turėtų naudoti tą patį stilių.

Testuokite abu kelius. Kai testuojate property, nepamirškite testuoti tiek getter'io, tiek setter'io:

def test_atlyginimas():
    darb = Darbuotojas("Jonas", 2000)
    assert darb.atlyginimas == 2000  # Testuoja getter'į
    
    darb.atlyginimas = 2500  # Testuoja setter'į
    assert darb.atlyginimas == 2500
    
    with pytest.raises(ValueError):  # Testuoja validaciją
        darb.atlyginimas = -100

Naudokite type hints. Python 3.6+ galite pridėti tipo anotacijas prie property:

class Modernus:
    @property
    def amzius(self) -> int:
        return self._amzius
    
    @amzius.setter
    def amzius(self, reiksme: int) -> None:
        if reiksme < 0:
            raise ValueError("Amžius negali būti neigiamas")
        self._amzius = reiksme

Žinokite, kada sustoti. Jei save pagaunate rašantį property, kuris kviečia kitą property, kuris kviečia metodą, kuris... Stop. Supaprastinkite.

Property dekoratorius yra vienas iš tų Python funkcijų, kurios iš pradžių atrodo kaip nereikalingas komplikavimas, bet kai juos išmokstate naudoti teisingai, jie tampa neatsiejama jūsų įrankių dėžės dalimi. Jie leidžia rašyti švaresnį, saugesnį kodą, išlaikant Python'o elegancijos jausmą. Tikiuosi, šis straipsnis padėjo suprasti ne tik kaip juos naudoti, bet ir kada juos naudoti – o tai dažnai yra svarbiau.

Daugiau

Fluentd logų kolektorius