Con la direttiva @property in Python è possibile pubblicare i getter e i setter per le proprietà della classe senza doverli invocare esplicitamente, bensì utilizzando la variabile stessa. Nell’ambito Python il nome di queste direttive è decoratori. In realtà, come vedremo, si tratta solo di una pseudo variabile.
Esempio: una classe che prende un valore di temperatura intesa in gradi Celsius e lo trasforma in Fahrenheit:
class Celsius: def __init__(self, temperature = 0): self.temperature = temperature def to_fahrenheit(self): return (self.temperature * 1.8) + 32
Se utilizziamo questa classe otteniamo:
>>> from celsius0 import Celsius
>>> c=Celsius(10)
>>> c.temperature
10
>>>
Ora rilasciamo la versione nel nostro software. Subito dopo ci vengono a dire che il modulo accetta temperature sotto lo 0 assoluto (e questo è francamente imbarazzante), per cui rilasciamo subito questa patch che utilizza una proprietà privata (quella contrassegnata col prefisso _) e i get/setters per consentire il controllo del valore di ingresso:
class Celsius: def __init__(self, temperature = 0): self.set_temperature(temperature) def to_fahrenheit(self): return (self.get_temperature() * 1.8) + 32 # getter & setters def get_temperature(self): return self._temperature def set_temperature(self, value): if value < -273: raise ValueError("Temperature sotto -273 non sono ammesse") self._temperature = value
Ora però il nostro codice non è più retro compatibile e se qualcuno per caso ha usato la nostra classe, quando userà una istruzione del tipo
>>> from celsius import Celsius
>>> c = Celsius(100)
>>> c.temperature
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Celsius' object has no attribute 'temperature'
Ohi ohi. In effetti adesso per permettere il controllo abbiamo trasferito il valore alla variabile privata _temperature, perdendo per strada la variabile temperature. Ma Python ci permette di risolvere il problema dichiarando una pseudo-proprietà temperature che sostituisce i getter e i setter e fa in modo che dall’esterno si osservi effettivamente una proprietà (nel senso della variabile della classe) che si chiama nel vecchio modo, ossia della prima versione dl software (temperature); questo senza rinunciare alla proprietà privata che ci consente il controllo che il valore non sia sotto lo zero assoluto. Per questa ultima versione utilizziamo anche un file di constanti fisiche per portare il software al miglior grado di modularità:
# celsius.py # A silly class to show the @property decorator # allowing to implicitly define get/setters # ############################################################ # Celsius object ############################################################ import constants K0 = constants.KELVIN0 class Celsius: def __init__(self, temperature = 0): try: if temperature < K0: raise ValueError self._temperature = temperature except ValueError: print("Temperature sotto " + str(K0) + " non permesse") def to_fahrenheit(self): return (self.temperature * 1.8) + 32 @property def temperature(self): print("Acquisizione valore") return self._temperature @temperature.setter def temperature(self, value): try: if value < K0: raise ValueError self._temperature = value except ValueError: print("Temperature sotto " + str(K0) + " non permesse")
Qualche commento:
- Per evitare fastidiosi messaggi di traceback del runtime environment di Python utilizziamo il costrutto try/except che ci consente una più robusta gestione delle eccezioni
- utilizziamo le costanti importate dal file constants.py in cui ho definito una serie di costanti fisiche
- definiamo la sezione @property che ci consente di invocare il getter semplicemente utilizzando la pseudo variabile
- definiamo la sezione @temperature.setter che ci consente di sostituire il setter
Proviamo la nuova versione del nostro software
marcob@jsbach:python$ python3
Python 3.5.2 (default, Nov 12 2018, 13:43:14)
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from celsius import Celsius
>>> c=Celsius(-2000)
Temperature sotto -273.15 non permesse
>>> c=Celsius(-20)
>>> c.temperature <-- getter!
Acquisizione valore
-20
>>> c.temperature = 130 <-- setter!
>>> c.temperature
Acquisizione valore
130
Quindi @property è un modo di sostituire getter e setter in modo tale da utilizzare nominalmente la stessa variabile come se fosse una vera proprietà della classe (in effetti non esiste, esiste soltanto l’attributo privato _temperature).
Possiamo anche trovare l’utilizzo del decoratore @property sbirciando nel codice eht-imaging, il software Python scritto per ricostruire l’immagine del buco nero nell’ambito della collaborazione Event Horizon Telescope
Ad esempio nel file movie.py c’è la dichiarazione di questa proprietà frames:
@property def frames(self): frames = self._movdict[self.pol_prim] return frames @frames.setter def frames(self, frames): if len(frames[0]) != self.xdim*self.ydim: raise Exception("imvec size is not consistent with xdim*ydim!") #TODO -- more checks on consistency with the existing pol data??? self._movdict[self.pol_prim] = frames
Il getter: quando viene menzionata la pseudo variabile frames, ritorna l’attributo privato dell’oggetto self._movdict[self.pol_prim].
Il setter: dopo un controllo di consistenza, valorizza l’attributo self._movdict[self.pol_prim] con il valore assegnato alla pseudo variabile frames.
Commenti recenti