Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Accesso agli attributi

Nei linguaggi di programmazione più antichi e strutturati (come C++ e Java) esiste il concetto di visibilità di un membro (un attributo o un metodo) della classe. Ogni membro può essere specificato come:

Nei linguaggi (molto) strutturati:

  • public, ovvero ereditabile e visibile a chiunque utilizzi la classe e le sue istanze;
  • protected, ovvero visibile solo all’interno della propria classe di appartenenza e dall’interno di ogni sua classe derivata;
  • private, ovvero non ereditabile e visibile solo all’interno della propria classe di appartenenza. Questa è la visibilità di default.

Questi concetti in Python (che è un linguaggio molto più… moderno. Non so quanto in questo caso sia un bene…) sono stati tradotto in maniera molto particolare.

In Python:

  • public, ovvero ereditabile e visibile a chiunque utilizzi la classe e le sue istanze; Questa è la visibilità di default in Python
  • protected, ogni variabile membro che inizia con underscore _. Funziona come pubblico. Il livello protetto è sociale (ovvero i programmatori per educazione, dovrebbero evitare l’utilizzo esterno alla classe)
  • private, ogni variabile membro che inizia con doppio underscore __. Non ereditabile e Visibile solo all’interno della propria classe di appartenenza.

La visibilità di default è diventata quella public per eliminare alla radice qualunque problema di accesso. Di sicuro una mossa a favore di chi è poco esperto.

Per ottenere la visibilità protected basta iniziare il nome della variabile con un underscore _. Una roba tipo:

class Persona:
    def __init__(self, name):
        self._nome = name

Attenzione però! Il livello di visibilità che si introduce con un underscore è puramente sociale!!! Questo significa che se qualcuno volesse accedere alla vostra variabile dall’esterno, potrà sempre farlo e l’interprete Python non si lamenterà! Qualunque programmatore Python però… lo considererebbe alquanto scortese!

p = Persona("Ciccio")
print(p._nome) # ecco... adesso sono un programmatore maleducato. Corretto, ma maleducato!!!

La visibilità private si ottiene iniziando il nome di una variabile (o di una funzione membro) con 2 underscore. Stavolta però le cose cambiano… l’interprete custodisce eccome i membri privati e riporta un AttributeError. Insisto col mio esempio:

class Persona:
    def __init__(self, name):
        self.__nome = name

p = Persona("Ciccio")
print(p.__nome)      # ERRORE!!! Adesso sono anche un somaro...

Capito come Python rende la visibilità dei membri delle classi, passiamo alla domanda di concetto: che cosa può interessarci tutto ciò? Risposta: a proteggere le variabili! Dall’esterno. Da un uso libero. Da chi vuole visualizzarle senza permesso. Da chi vuole modificarne il valore come crede.


Inserisco un pezzo di codice di linguaggio C++ nella speranza di rendere evidente il concetto:

Esempio di classe in linguaggio C++
class Persona
{
// attributi privati (in C gli attributi si segnano con un solo underscore)
    string _nome;

// metodi pubblici
public:
    // setter method: permette di impostare il valore dell'attributo _nome
    bool setNome(string n) {
        if (n == "")
            return False;
        _nome = n;
        return True;
    };
    
    // getter method: ritorna il valore dell'attributo _nome
    string nome() const {
        return _nome;
    }; 
};

Anche se non conosciamo il linguaggio C++, quello che vediamo è molto semplice da comprendere: l’attributo _nome è privato e quindi NON modificabile direttamente dall’esterno della classe Persona.
Questo significa che un codice del genere da errore:

Persona pers;
pers._nome = "Andrea"; // ERRORE! L'attributo è PRIVATO!!!

Questa sovrastruttura, tipica dei linguaggi compilati come C e Java, fornisce un livello di protezione aggiuntivo ai valori delle variabili membro!

Se il programmatore vuole che all’esterno venga visualizzato il nome della Persona, inserisce nella classe un metodo getter (tipicamente nome()) cioè un metodo che ritorna il valore di _nome.

Se il programmatore vuole che dall’esterno sia possibile modificare il nome della Persona, inserisce nella classe un metodo setter (tipicamente setNome(string n)), cioè un metodo che permette di impostare un nuovo valore per _nome (se il valore accettabile!)


Warning

Privato o protetto?

Adesso che abbiamo capito il concetto di visibilità (a grandi linee, mi rendo conto…) facciamo una semplificazione: per ora utilizzeremo solo le visibilità:

  • pubbliche (cioè lasceremo tutto identico a prima…)
  • private (quindi metteremo DUE underscore e…)

La visibilità protetta sarà reintrodotta nel prossimo capitolo!!!

Nel linguaggio Python esiste uno strumento che permette una implementazione analoga (ovvero… simile, NON uguale) a questo livello di protezione delle variabili che si definisce il Python @property decorator.

Un codice funzionalmente analogo a quello sopra in C++, che scriviamo in Python, potrebbe essere fatto così:

class Persona:
    def __init__(self, name):
        # il ragionamento è identico con la visibilità protetta!!!
        self.__nome = name
    
    @property
    def nome(self):
        """ DOCUMENTA QUI LA TUA PROPRIETA': il nome della persona """
        return self.__nome
        
    @nome.setter
    def nome(self, name):
        if name == "":
            raise ValueError("Tutte le persone hanno un nome...")
        self.__nome = name
        return

Mamma mia, quante cose da spiegare… Cominciamo!

def __init__(self, name):
    self.__nome = name

Secondo la convenzione già esposta, si intende mantenere nascosta la variabile membro __nome.

@property
def nome(self):
    """ DOCUMENTA QUI LA TUA PROPRIETA': il nome della persona """
    return self.__nome

Questo decoratore (@property con la chiocciolina davanti si dice decoratore in Python) fa in modo che nome (in questo caso, il nome della funzione) diventi una Python property. La docstring interna specifica la descrizione della proprietà.

Una proprietà è una caratteristica tipica di un oggetto, a cui possono essere aggiunti funzioni getter , setter e… un’altra che non studieremo!!!.

La definizione della proprietà implica automaticamente l’esistenza della funzione getter, quella che ritorna il valore. Se vogliamo inserire anche la funzione setter, che permette i modificare il valore della proprietà, allora scriviamo:

@nome.setter
def nome(self, name):
    if name == "":
        # questa la ignoriamo??? La scriviamo così e basta :D
        raise ValueError("Tutte le persone hanno un nome...")
    self.__nome = name
    return

A questo punto, la proprietà nome diventa una sorta di nuova variabile membro della classe, che invoca automaticamente le funzioni getter e setter ad essa collegate:

pers = Persona()
pers.nome = "Andrea"        # esegue la funzione "setter" automaticamente
print("Nome:", pers.nome)   # esegue la funzione "getter" automaticamente

Questo modo di implementare le variabili membro di una classe è la via scelta dai programmatori Python per gestire i valori delle stesse.

Read-Only Properties

Capire il concetto di proprietà di sola lettura è molto semplice: basta dimenticarsi di aggiungere ad una prorietà la funzione setter… ed ecco che diventa impossibile modificarla al di fuori della classe!!!

class Oggetto:
    def __init__(self):
        self.__valore = 0
    
    @property
    def valore(self):
        return self.__valore

obj = Oggetto()
obj.valore = 5 # ERRORE!!!! NO setter!!!

Questa caratteristica delle proprietà ritorna utile quando abbiamo delle caratteristiche derivate da altre: ad esempio, l’area di un rettangolo può essere definita come proprietà read-only, in modo che essa venga calcolata automaticamente a partire dai valori attuali di base e altezza, ma non possa essere impostata da codice. Proviamo:

class Rettangolo:
    def __init__(self, b, h):
        self.base = b
        self.altezza = h
    
    @property
    def area(self):
        return self.base * self.altezza

ret = Rettangolo(5,4)
print("Area:", ret.area)    # scrive "Area: 20"
ret.area = 30               # ERRORE!!! Quali sono base e altezza di un rettangolo di area 30? Boh...

Provate a verificare la vostra comprensione coi seguenti esercizi.

Esercizi sulle proprietà


Esercizio 1200: SVOLTO

(Ri)definire la classe Rettangolo, facendo in modo che base e altezza siano numeri comunque positivi e che area e perimetro siano calcolate automaticamente come proprietà in sola lettura.

Il codice che segue mi sembra alquanto chiaro. Provate a leggerlo con calma, a copiarlo sul vostro computer e a provare alcune modifiche per essere sicuri di aver capito tutto!

class Rettangolo:
    def __init__(self, b, h):
        # i valori passati nella init NON sono sottoposti al controllo
        # imposto dai decoratori sotto, quindi...
        self.__base = abs(b)
        self.__altezza = abs(h)
    
    def __str__(self):
        return "Rettangolo: " + str(self.__dict__)
        
    @property
    def base(self):
        return self.__base
    
    @base.setter
    def base(self, value):
        if value < 0:
            raise ValueError("La base di un rettangolo non può essere negativa")
        self.__base = value
        return
    
    @property
    def altezza(self):
        return self.__altezza
    
    @altezza.setter
    def altezza(self, value):
        if value < 0:
            raise ValueError("L'altezza di un rettangolo non può essere negativa")
        self.__altezza = value
        return
    
    @property
    def area(self):
        return self.__base * self.__altezza
    
    @property
    def perimetro(self):
        return (self.__base + self.__altezza) * 2
    
if __name__ == "__main__":
    r1 = Rettangolo(5,3)
    print(r1)
    # se decommenti sotto vedi il ValueError con la nostra spiegazione
    #r1.base = -12
    r1.base = 12
    print("base:", r1.base)
    print("altezza:", r1.altezza)
    # area e perimetro sono proprietà NON funzioni, quindi vanno SENZA parentesi
    print("area:", r1.area)
    print("perimetro:", r1.perimetro) 
    # se decommenti sotto, vedi l'interprete lamentarsi per l'assegnazione
    #r1.area = 23

E adesso sotto con un esercizio analogo!


Esercizio 1201

(Ri)definire la classe Cerchio, facendo in modo che il raggio sia un numero sempre positivo e che il diametro, l’area e la circonferenza siano calcolate automaticamente come proprietà in sola lettura.


Esercizio 1202

Definire una classe Temperatura, che NON prende parametri in fase di inizializzazione. Ha una sola variabile membro, la temperatura in gradi Kelvin, che (come sapete) deve essere non negativa. Presenta inoltre due proprietà in sola lettura per visualizzare la temperatura in gradi Celsius e in gradi Farheneit.

Tip

Kelvin, Celsius, Farheneit

temp:K 
C = temp - 273.15
F = temp * 9 / 5 - 459.67

Esercizio 1203

Definire la classe Data con giorno, mese, anno che NON possono essere modificati (capito come fanno le classi Datetime???)


Esercizio 1204

Definire la classe Contatore. Essa ha un solo valore membro (la conta, appunto). La conta parte da zero, non può essere modificata e viene incrementata ogni volta che viene visualizzata.