Vai al contenuto

Classi e Oggetti

La prima cosa da fare è cercare di capire cosa si intende quando si utilizzano i termini Classe e Oggetto: vediamo se si riesce al primo colpo!!!

Partiamo da un concetto qualsiasi, ad esempio... la sedia!

Le classi servono per descrivere i concetti: parlando delle sedie, potremmo citare ad esempio:

  • il materiale
  • il colore
  • l'altezza
  • l'ampiezza della seduta
  • etc...

Una classe descrive il concetto attraverso una serie di informazioni (ad esempio, quelle che abbiamo citato prima) che saranno dello stesso tipo per tutte le sedie (cioè, tutte le sedie sono fatte di un materiale, hanno un colore, etc...) ma che chiaramente possono avere valori diversi (sedie di legno o di ferro, bianche oppure blu, etc...).

Gli oggetti rappresentano le istanze dei concetti descritti dalle classi. Ad esempio, se state leggendo seduti, la vostra sedia è un oggetto, mentre un'altra sedia è un altro oggetto.

Se in cucina avete sei sedie significa che avete sei oggetti (diversi) della (stessa) classe Sedia.

Classi e Oggetti

Le Classi sono un concetto analogo ai tipi di variabile, ad esempio il tipo intero, che descrive tutti i numeri che sono interi.

Gli Oggetti sono un concetto analogo alle variabili, che possono contenere un unico valore! Quello è l'oggetto!

Spero di essere stato chiaro... più di così non riesco!!!

Le Classi

Partiamo sempre da un concetto, ad esempio l'Automobile.

Per implementare la classe Automobile potremmo definire caratteristiche come la marca, il modello, il colore, la cilindrata.

Un'automobile è però un concetto in grado anche di alcuni comportamenti: ad esempio avvia, spegni, accelera, frena, curva, ecc...

Attributi e Metodi di un oggetto

Le caratteristiche, o attributi di un oggetto, sono quegli elementi utili a descriverne le proprietà e lo stato. Sono solitamente individuate tramite dei sostantivi.

I comportamenti, o metodi di un oggetto sono quelle funzionalità che mette a disposizione per interagire con esso. Sono solitamente individuati tramite dei verbi.

Attributi e metodi definiti all'interno di un oggetto vengono comunemente definiti i membri dell'oggetto.

Per provare a scrivere il nostro primo esempio in codice, partiamo da una classe semplicissima, come la classe Cerchio.

I suoi attributi sono semplicemente il raggio, mentre come metodi metteremo quelli per il calcolo di Area e Circonferenza.

Definizione della classe Cerchio
# File "cerchio.py"
import math

class Cerchio:
    # inizializza gli attributi della classe
    def __init__(self, raggioInserito):
        # aggiunge l’attributo "raggio" alla classe e lo inizializza
        self.raggio = raggioInserito

    def calcolaArea(self):
        a = math.pi * self.raggio * self.raggio
        return a

    def calcolaCirconferenza(self):
        c = 2* math.pi * self.raggio
        return c

# -------------------------------------------

if __name__ == "__main__":
    cx = Cerchio(4)   # crea un oggetto della classe Cerchio con raggio 4
    print("Cerchio")
    print("raggio:", cx.raggio)
    print("area:", cx.calcolaArea())
    print("circonferenza:", cx.calcolaCirconferenza())

che dovrebbe visualizzare:

Cerchio
raggio: 4
area: 50.26548245743669
circonferenza: 25.132741228718345

A questo punto i concetti dovrebbero essere chiari, mentre la sintassi utilizzata ancora no!!!

Cerchiamo di chiarire spiegando il codice nei punti più oscuri:

Terminologia

Chiariamo bene i termini che andremo ad utilizzare per definire una classe manualmente e che sono utilizzati da Python.

class Cerchio:

Questo si capisce: come per dichiarare una funzione si utilizza la clausola def, per definire una classe utilizziamo la clausola class.

Ricordate?

Per evitare confusione fra i concetti mi piace utilizzare i termini classe e oggetto. In OOP solitamente si usa il termine "classe" per la definizione dell'oggetto, mentre "oggetto" è l'istanza del tipo classe definito. Prima abbiamo scritto:

cx = Cerchio(4)

Bene: cx è un oggetto di tipo Cerchio, un'istanza della classe, mentre Cerchio è la classe.

self

Avrete notato questo parametro predefinito nelle funzioni della classe e per inizializzare la variabile membro. Questo parametro viene automaticamente istanziato da Python in questo modo:

cx.area() diventa Cerchio.area(cx)

In questo modo il parametro self permette accesso alla classe, ma l'utente della classe non ha bisogno di occuparsene. Spero sia chiaro, più facile di così non riesco. In pratica, la variabile self va messa come primo membro di ogni metodo di una classe, in modo da poter collegare il metodo alla classe stessa.

Metodi che iniziano (e finiscono) con doppio underscore __ Ce ne sono parecchi in ogni classe e hanno ognuno un compito specifico. Vengono definite Funzioni Speciali. Ne introdurremo molte altre più avanti. La cosa importante da capire su di esse è che questi metodi non vanno eseguiti "volontariamente" (ad esempio come fareste con il metodo "area()" della classe Cerchio se voleste calcolarne l'area) ma vengono eseguiti automaticamente in determinate situazioni. Quindi ogni volta che incontrate una funzione speciale dovete farvi sempre due domande:

  1. In quale momento particolare questa funzione speciale viene eseguita automaticamente?
  2. Voglio modificare il comportamento della classe in quel caso particolare? Bene, se la risposta a questa domanda è sì significa che dovete re-implementare quella funzione. Man mano vedremo come.

Per adesso vediamo le 2 funzioni speciali più comuni in assoluto:

def __init__( self , . . . )

Questa particolare funzione speciale viene eseguita automaticamente quando si definisce un oggetto di una classe. La sua implementazione serve per dare un valore iniziale agli attributi della classe, siano essi inseriti come parametri della stessa o inizializzati ad un valore scelto dal programmatore.

Vediamo un esempio per chiarirci al meglio le idee. Definiamo la classe Quadrato. L'utente potrà scegliere il lato del Quadrato, ma inizialmente esso sarà disegnato con sfondo bianco e lati neri.

Classe Quadrato
class Quadrato:
    def __init__(self, lato):
        self.lato = lato
        self.coloreSfondo = "bianco"
        self.coloreBordo = "nero"

# ...

# nella riga sotto viene eseguita automaticamente la funzione __init__
obj = Quadrato(4)

# si definisce così un oggetto della classe Quadrato, di lato 4
# con colore di sfondo bianco e colore del bordo nero.
def __str__ (self)

La funzione __str__ serve per visualizzare in maniera semplice informazioni sulla classe (praticamente per visualizzare il valore dei suoi attributi). Questa funzione viene eseguita automaticamente quando si esegue la funzione print() con parametro un oggetto della classe.

La funzione __str__ non prende MAI parametri e ritorna sempre una stringa, ricordatelo! Ad esempio, definiamo la funzione __str__ per la classe Quadrato definita sopra:

def __str__(self):
    s = ""
    s += "Quadrato di lato " + str(self.lato)
    s += ", sfondo " + self.coloreSfondo
    s += ", bordo " + self.coloreBordo
    return s

...
# riferito all’oggetto obj definito prima
print(obj)
# visualizzerà "Quadrato di lato 4, sfondo bianco, bordo nero"

Esercizio svolto: la classe Rettangolo

Definire un oggetto Rettangolo, tramite i parametri base e altezza e implementare i metodi per il calcolo dell'area e del perimetro. Fornire un test per un Rettangolo di base = 5 cm e altezza = 3 cm, visualizzando i parametri e calcolando area e perimetro dello stesso.

class Rettangolo:
    def __init__(self, b = 0, h = 0):
        self.base = b
        self.altezza = h

    def __str__(self):
        s = "Rettangolo: " + str(self.__dict__)
        return s

    def calcolaArea(self):
        a = self.base * self.altezza
        return a

    def calcolaPerimetro(self):
        p = 2*(self.base + self.altezza)
        return p

if __name__ == "__main__":
    ret = Rettangolo(5,3)
    print(ret)
    print("Base:", ret.base)
    print("Altezza:", ret.altezza)
    print("Area:", ret.calcolaArea())
    print("Perimetro:", ret.calcolaPerimetro())    

self.__dict__

self.__dict__ è una variabile automatica degli oggetti Python che contiene il dizionario delle variabili membro!

Per ogni attributo, il nome dello stesso diventa una chiave del dizionario, mentre il suo valore diventa il valore associato!

Esercizi di comprensione

Prima di andare avanti, proviamo a definire alcune classi e proporre con esse qualche test in cui inserire e modificare i valori degli attributi definiti e visualizzare i risultati delle chiamate ai metodi definiti.

Per ognuna ricordate che è obbligatorio definire sempre la funzione __init__ e la funzione __str__ e verificarne il funzionamento, definendo un oggetto della classe e visualizzandone i valori con la print().


Esercizio 701

Definire la classe Persona con attributi nome, cognome, data e luogo di nascita, sesso (M/F). La funzione di __init__ della classe non deve avere argomenti.

Definite un maschio e una femmina della classe a libera scelta.


Esercizio 702

Definire la classe TriangoloRettangolo inserendo come attributi i due cateti. Aggiungere i metodi per il calcolo dell'ipotenusa, dell'area e del perimetro.

Definire un oggetto della classe TriangoloRettangolo con cateti uguali a 4 cm e 3 cm, visualizzare i suoi attributi e calcolare l'ipotenusa, l'area e il perimetro.


Esercizio 703

Definire la classe Animale con attributi nome e specie. Aggiungere il metodo "corri" (ritorna la stringa "sto correndo...") e "mangia" (ritorna la stringa "sto mangiando").

Definire un cane di nome "Piero" e farlo correre e mangiare. Visualizzare i suoi attributi.


Esercizio 704

Definire la classe Persona con attributi nome, età e sesso (M/F). La funzione di __init__ della classe deve prendere come argomento solo il nome della persona, mentre l'età va impostata automaticamente a ZERO e il sesso a "M" (o "F", scegliete voi).

Aggiungere i metodi invecchia (aggiunge un anno di età) e saluta (restituisce "signore" o "signora" a seconda del sesso della persona).

Definire 2 persone: "Augusto", maschio di 47 anni e "Marianna", femmina di 44 anni. Utilizzare i metodi invecchia e saluta per entrambi, procedere poi a visualizzare gli attributi di entrambi.


Esercizio 705

Definire la classe ContoCorrente con attributi proprietario e capitale; il proprietario va definito tramite parametro della funzione __init__ mentre il capitale va inizializzato a ZERO.

Definire il metodo deposita, che prende un parametro reale, controlla che sia positivo ed eventualmente lo aggiunge al capitale e il metodo preleva, che prende anch'esso un parametro reale, verifica che il parametro sia positivo, verifica che sia minore del capitale ed eventualmente lo sottrae da esso (il metodo ritorna True se possibile, False altrimenti).

Definire il Conto Corrente di "Gigetto". Depositare in esso 1000 euro. Prelevare 600 euro per 2 volte. La seconda volta l'operazione dovrebbe fallire. Visualizzare dopo ogni operazione i valori dell'oggetto con la funzione print().


Esercizio 706

Definire la classe Crittografia con attributo un numero intero che indica lo spiazzamento dei caratteri. Questo numero sarà utilizzato per criptare le stringhe traslando i caratteri di numero posti sull'alfabeto: ad esempio se il numero è 3 e vuoi criptare la stringa "ale", prendi la "a" e vai avanti di 3 sull'alfabeto ("d"), prendi la "l" e vai avanti di 3 sull'alfabeto ("o") e lo stesso con la "e": la stringa criptata ottenuta è "doh".

La classe contiene due funzioni:

  • cripta, che prende una stringa come parametro e restituisce la stessa trasformata (criptata) secondo la regola descritta sopra;

  • decripta, che prende una stringa come parametro e la rimette "a posto" (la decripta).

Definire due oggetti della classe Crittografia con parametro a piacere e provare a "criptare" e "decriptare" una stringa, verificando che la stringa decriptata sia uguale a quella inserita prima di essere criptata.


Esercizio 707

Definire la classe Automobile con attributi marca, modello, velocità e numero di persone trasportate. La funzione init prende come parametri la marca e il modello e imposta a ZERO gli altri attributi. Implementare i seguenti metodi:

  • faiSalirePersona: aggiunge una persona al numero di persone trasportate fino ad un massimo di 5. Ritorna True se è stato possibile aggiungere una persona, False altrimenti.
  • faiScenderePersona: toglie una persona al numero di persone trasportate (Se possibile ovviamente). Ritorna True se è stato possibile togliere una persona, False altrimenti.
  • accelera: aggiunge 20 kmh alla velocità di marcia, fino ad un massimo di 120 kmh. Funziona solo se il numero di persone trasportate è positivo. Ritorna True se è stato possibile aumentare la velocità, False altrimenti.
  • rallenta: toglie 20 kmh alla velocità di marcia, ovviamente fino a fermarsi. Ritorna True se è stato possibile diminuire la velocità, False altrimenti.
  • frena: azzera la velocità di marcia. Ritorna True se è stato possibile frenare, False altrimenti.

Definire un oggetto della classe Automobile e progettare un test in modo che ognuna delle funzioni venga eseguita almeno 2 volte, una ritornando True, una ritornando False.


Esercizio 708

Definire la classe TrapezioRettangolo, che prende come parametri la base minore, la base maggiore e l'altezza del Trapezio.

Definire i metodi calcolLatoObliquo, calcolaArea, calcolaPerimetro. Dichiarare un oggetto TrapezioRettangolo e procedere a visualizzare i suoi parametri, la sua area e il suo perimetro.


Esercizio 709

Definire la classe "Orario", con parametri i tre interi per ore, minuti e secondi. Implementare inoltre le seguenti funzioni:

  • contaSecondiDaMezzanotte(): restituisce l'intero che rappresenta il numero di secondi trascorsi dalla mezzanotte.
  • aggiungiTempo(ore, minuti, secondi): aggiunge tempo all'orario corrente. Ad esempio, se l'oggetto della classe Orario segna le 03:14:22 e si esegue su di esso la funzione aggiungoTempo(1,2,3) l'orario diventa le 04:16:25. Attenzione a quando "il giro ricomincia"...
  • verificaMomento(): ritorna la stringa "mattina" se orario è fra le 8 e le 13, "pomeriggio" se fra 13 e 20, "sera" fra 20 e 23, "notte" altrimenti

Esercizio 710

Definire la classe CartaFedeltà, per la gestione degli utenti di un grande magazzino. La carta fedeltà è nominativa (appartiene ad un solo cliente) e consente l'accumulo dei punti (attraverso il metodo accumulaPunti(soldiSpesi)) calcolati sulla base della spesa effettuata: ogni 12€ di spesa si aggiunga un punto. All'inizio ovviamente il numero di punti è ZERO. Il cliente può decidere, in ogni momento, di usufruire di una parte dei punti accumulati per l'ottenimento di un premio (implementare un opportuno metodo utilizzaPunti(quantita): il metodo ritorna True... False altrimenti).

Definire le carte fedeltà per 2 clienti. Il primo cliente fa una spesa pari a 150€. Il secondo cliente fa una spesa di 300€. In seguito, il primo cliente fa una spesa pari a 1500€ e, dopo aver pagato, decide di utilizzare 100 dei punti accumulati per ritirare un premio. Il secondo cliente chiede di utilizzare 50 dei punti accumulati.


Esercizio 711

Definire la classe Giocatore con nome, numero di maglia e ruolo ricoperto. Il nome del giocatore va impostato tramite parametro, mentre il numero va impostato inizialmente a ZERO e il ruolo a "X".

Definire la funzione impostaRuolo(stringa) che prende una stringa come parametro e imposta il ruolo del giocatore. Fare in modo che i ruoli accettabili siano solo "P" (per portiere), "D" (per difensore), "C" (per centrocampista), "A" (per attaccante). La funzione ritorna True o False a seconda del fatto se il ruolo viene effettivamente modificato oppure no.

Definire la funzione cambiaNumero(intero) che modifica il numero di maglia solo se esso varia fra 1 e 99. Anche qui, la funzione ritorna True o False...

DIFFICILE E OPZIONALE: è possibile fare in modo anche che i numeri di maglia dei vari giocatori siano tutti diversi fra loro? Proponi una soluzione al problema. Definire un giocatore di nome "Edoardo". Tramite la funzione impostaRuolo, impostare il suo ruolo ad attaccante (con "A") e poi modificarlo a terzino ("T"). La seconda modifica dovrebbe fallire. Utilizzare la funzione cambiaNumero per modificare il suo numero a 103 e poi a 7. Definire un giocatore di nome "Alessandro". Tramite la funzione impostaRuolo, impostare il suo ruolo a centrocampista (con "C"). Utilizzare la funzione cambiaNumero per modificare il suo numero a 7 (se avete implementato la parte opzionale, dovrebbe fallire) e poi a 11.


Esercizio 712

Definire la classe EstrazioneLotto. La classe contiene una lista, inizialmente vuota, di stringhe che rappresentano le città ove ci sono le ruote di estrazione. Prevedere un metodo aggiungiRuota(stringa) che verifica se il nome della città da inserire sia già presente nelle ruote e in caso negativo la aggiunge alla lista. Definire una funzioni estrai(stringa) che verifica se il nome della città passata come parametro è presente nelle ruote. In caso negativo, ritorna una tupla vuota. In caso positivo, ritorna una tupla di 5 numeri casuali, diversi fra loro, fra 1 e 90.

ULTERIORE DIFFICOLTA' (opzionale): fare in modo che i numeri della tupla siano ordinati in senso crescente.

Definire una funzione eseguiEstrazioniDellaSettimana() che permette a tutte le ruote presenti di estrarre i numeri del lotto. La funzione ritorna un dizionario che ha come chiavi i nomi delle ruote in cui avvengono le estrazioni e come valori le tuple dei 5 numeri estratti.

Definire un oggetto della classe EstrazioneLotto.

Inserire tramite il metodo aggiungiRuota le seguenti città: Jesi, Senigallia, Ancona, Jesi (dovrebbe fallire, già presente), Monsano, Moie, Ancona (err). Visualizzare il risultato della funzione estrai("Jesi"), estrai("Ancona"), estrai("Milano"). L'ultima dovrebbe ritornare una tupla vuota. Eseguire la funzione estrazioniDellaSettimana e visualizzare ordinatamente il dizionario ottenuto, con una visualizzazione simile a questa:

  • Jesi: (1,5,9,23,89)
  • Senigallia: (23,34,45,67,88)
  • Ancona: (39, 43, 44, 78, 81)

Esercizio 713: Agenda

Una agenda contiene una serie di impegni identificati con una descrizione generica e con il giorno in cui questo impegno è preso. Esempi di impegni potrebbero essere:

  • Calcetto, Lunedì
  • Parrucchiere, Mercoledì
  • Pizza con gli amici, Venerdì

Quando si crea un oggetto della classe Agenda si parte (ovviamente) con una lista di impegni vuota! La classe presenta inoltre le seguenti funzioni:

  • inserisciImpegno ( descrizione, giorno ) : prende i dati dai parametri della funzione e inserisce il nuovo impegno in agenda. Non ritorna nulla.
  • elencaImpegniDi ( giorno ) : prende come parametro un giorno della settimana e ritorna la lista delle descrizioni degli impegni per quel giorno.
  • rimuoviImpegno ( descrizione ) : prende come parametro la descrizione di un impegno e, se lo trova in agenda, rimuove l’impegno corrispondente. Ritorna True se viene rimosso un impegno, False altrimenti.
  • trovaImpegno ( descrizione ) : prende come parametro la descrizione di un impegno e, se lo trova in agenda, ritorna il giorno in cui quell’impegno è stato preso. Se non trova nulla, ritorna la stringa "NON TROVATO".

Definire la classe Agenda, un oggetto della classe stessa, inserire in essa almeno 4 impegni (tramite la funzione inserisciImpegno) e fare un test di utilizzo di tutte le altre funzioni


Esercizio 714: Rubrica

Una rubrica contiene una lista di contatti. Ogni contatto comprende un nome ed un numero di telefono (memorizzabile comunque come una stringa). Esempi di contatti potrebbero essere:

  • prof , 555-12345
  • tizio di Dallas, 214-748-3647
  • casa, 0731-24680

Quando si crea un oggetto della classe Rubrica, si parte con un elenco di contatti vuoto. La classe presenta inoltre le seguenti funzioni:

  • inserisciContatto ( nome , numero ) : prende i dati dai parametri della funzione e inserisce il nuovo contatto in rubrica. Non ritorna nulla.
  • modificaContatto ( nome , nuovoNumero ) : modifica il contatto identificato dal parametro nome, aggiornando il suo numero con il parametro nuovoNumero. Ritorna True se il numero viene aggiornato, False altrimenti.
  • rimuoviContatto ( nome ) : rimuove dalla rubrica il contatto identificato dal parametro nome. Ritorna True se è stato possibile rimuovere il contatto, false altrimenti.
  • trovaNumero ( nome ) : ritorna il numero del contatto con nome la stringa riportata nel parametro. Se non si trova nessun contatto, ritorna la stringa "NESSUN CONTATTO".

Definire la classe Rubrica, un oggetto della stessa, inserirvi almeno 4 contatti (tramite la funzione inserisciContatto) e fare un test di utilizzo di tutte le altre funzioni.

Accesso agli attributi (incapsulamento)

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 solo 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:

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!)


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 e setter.

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...

Mi sembra facile. Provate a verificare la vostra comprensione coi seguenti esercizi.

Esercizi sulle proprietà


Esercizio 720: 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 721

(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 722

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.

Kelvin, Celsius, Farheneit

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

Esercizio 723

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


Esercizio 724

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.