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.
