Pillole di Python: la direttiva @property

Spread the love

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.

Lascia un commento

Your email address will not be published.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.