Attributi privati e pubblici, di classe e di istanza

Attributi pubblici di istanza

Gli attributi di istanza sono quelli che sono legati al singolo oggetto. Agli attributi pubblici si può accedere direttamente anche dal programma chiamante, senza invocare una funzione della classe. Si veda il seguente esempio:

class Fruit(object):

    def __init__(self, name):
        self.setName(name)

    def setName(self, value):
        self.name = value   # name è un attributo pubblico
        return self

    def setQuality(self, value):
        self.quality = value   # quality è un attributo pubblico
        return self

    def getName(self):
        return self.name

    def getQuality(self):
        return self.quality

def main():
    myapple = Fruit('mela granny smith')
    myapple.setQuality(12)
    print('Nome: %s\nqualità: %d' % (myapple.getName(), myapple.getQuality()))

    myapple.quality = 20
    print('Nome: %s\nqualità: %d' % (myapple.getName(), myapple.getQuality()))
    # accesso agli attributi tramite funzione "getter"

    print(myapple.quality)
    # accesso all'attributo diretto

    myorange = Fruit('arancia siciliana')
    myorange.quality = -20
    # non viene fatto nessun controllo sul valore impostato...
    print('Nome: %s\nqualità: %d' % (myorange.getName(), myorange.getQuality()))

if __name__ == '__main__':
    main()

che produce il seguente output:

Nome: mela granny smith
qualità: 12
Nome: mela granny smith
qualità: 20
20
Nome: arancia siciliana
qualità: -20

Per inciso, quando si chiama una funzione definita all'interno della classe, si può usare la versione con indicazione esplicita della classe:

Fruit.setQuality(myapple, 12)

oppure la versione con l'indicazione dell'oggetto e la notazione puntata:

myapple.setQuality(12)

Attributi privati di istanza

Se si vuole che ad un determinato attributo (ma il discorso vale anche per le funzioni definite all'interno di una classe) sia accessibile solo dalle funzioni interne alla classe, la convenzione in Python è di usare un nome che inizia con due segni di sottolineatura (ma vedi nota in fondo al riguardo). In questo modo viene a crearsi un attributo privato. (Si noti che in Python gli attributi privati sono comunque accessibili, usando particolari meccanismi, e questo viene considerato utile nel caso, ad esempio, di sessioni di debugging.)

Modifichiamo la classe precedente come segue:

class Fruit(object):

    def __init__(self, name):
        self.setName(name)

    def setName(self, value):
        self.name = value   # name è un attributo pubblico
        return self

    def setQuality(self, value):
        self.__quality = value   # quality è un attributo privato
        return self

    def getName(self):
        return self.name

    def getQuality(self):
        return self.__quality

def main():
    myapple = Fruit('mela granny smith')
    myapple.setQuality(12)
    # equivalente a Fruit.setQuality(myapple, 12)
    print('Nome: %s\nqualità: %d' % (myapple.getName(), myapple.getQuality()))

    # myapple.__quality = 20
    # non possibile, visto che l'attributo è privato

    # print(myapple.__quality)
    # accesso diretto all'attributo
    # non possibile, visto che l'attributo è privato

if __name__ == '__main__':
    main()

Controllo dell'attributo

In questo modo, possiamo far sì che qualsiasi modifica dell'attributo __quality passi attraverso il richiamo di una funzione della classe. Ad esempio, se volessimo forzare un controllo sul valore, potremmo ridefinire la funzione setQuality() così:

class Fruit(object):

    def __init__(self, name):
        self.setName(name)

    def setName(self, value):
        self.name = value
        return self

    def setQuality(self, value):
        if 0 <= value <= 100: 
            self.__quality = value
        else:
            self.__quality = 50
        return self

    def getName(self):
        return self.name

    def getQuality(self):
        return self.__quality

def main():
    myapple = Fruit('mela granny smith')
    myapple.setQuality(12)
    print('Nome: %s\nqualità: %d' % (myapple.getName(), myapple.getQuality()))

    myapple.setQuality(-10)
    # impostiamo un valore fuori dal range valido
    print('Nome: %s\nqualità: %d' % (myapple.getName(), myapple.getQuality()))
    # ... e vediamo che viene impostato a 50

if __name__ == '__main__':
    main()

L'output sarà il seguente:

Nome: mela granny smith
qualità: 12
Nome: mela granny smith
qualità: 50

Funzioni private

Anche le funzioni, come già accennato, possono essere private. Immaginiamo di voler aggiungere una funzione che aggiunga un messaggio di allarme ogni qual volta il valore della quantità viene impostato in modo non corretto. Sarà sufficiente che il nome della funzione inizi con il solito doppio underscore:

class Fruit(object):

    def __init__(self, name):
        self.setName(name)
        self.__alerts = [] # un attributo privato (lista degli allarmi)

    def setName(self, value):
        self.name = value
        return self

    def setQuality(self, value):
        if 0 <= value <= 100: 
            self.__quality = value
        else:
            self.__quality = 50
            self.__addAlert('bad quality: %d' % value)
        return self

    def __addAlert(self, value):
        self.__alerts.append(value)
        return self

    def getName(self):
        return self.name

    def getQuality(self):
        return self.__quality

    def getAlerts(self):
        return self.__alerts

def main():
    myapple = Fruit('mela granny smith')
    myapple.setQuality(12)
    print('Nome: %s\nqualità: %d' % (myapple.getName(), myapple.getQuality()))

    myapple.setQuality(-10)
    # impostiamo un valore fuori dal range valido
    print('Nome: %s\nqualità: %d' % (myapple.getName(), myapple.getQuality()))
    # ... e vediamo che viene impostato a 50
    print("Lista degli allarmi: ", myapple.getAlerts())

if __name__ == '__main__':
    main()

L'output sarà come il seguente:

Nome: mela granny smith
qualità: 12
Nome: mela granny smith
qualità: 50
Lista degli allarmi:  ['bad quality: -10']

Attributi legati alla classe

Può a volte essere necessario gestire degli attributi che hanno a che fare non con le singole istanze, ma con la classe in quanto tale. Si immagini ad esempio di voler memorizzare un elenco degli elementi istanziati nell'attributo di classe salad. Si può scrivere un codice come il seguente:

class Fruit(object):
    salad = [] # attributo di classe, vale per tutte le istanze
    # in questo esempio, è una lista di tutti i frutti istanziati

    def __init__(self, name):
        self.setName(name)
        self.__alerts = []
        self.salad.append(self)

    def __str__(self):
        return self.getName()

    def getName(self):
        return self.name

    def setName(self, value):
        self.name = value
        return self

    def getIdx(self):
        return self.__idx

    def setQuality(self, value):
        if 0 <= value <= 100: 
            self.__quality = value
        else:
            self.__quality = 50
            self.__addAlert('bad quality: %d' % value)
        return self

    def __addAlert(self, value):
        self.__alerts.append(value)
        return self

    def getName(self):
        return self.name

    def getQuality(self):
        return self.__quality

    def getAlerts(self):
        return self.__alerts

def main():
    myapple = Fruit('mela granny smith')
    print('macedonia a cui appartiene la mela:') 
    print(*myapple.salad, sep=", ")

    myorange= Fruit('arancia siciliana')
    print('macedonia a cui appartiene l\'arancia:') 
    print(*myorange.salad, sep=", ")

    mykiwi= Fruit('kiwi friulano')
    print('macedonia a cui appartiene il kiwi:') 
    print(*mykiwi.salad, sep=", ")

    print('==========')    
    print('macedonia a cui appartiene la mela:') 
    print(*myapple.salad, sep=", ")
    print('macedonia a cui appartiene l\'arancia:') 
    print(*myorange.salad, sep=", ")
    print('macedonia a cui appartiene il kiwi:') 
    print(*mykiwi.salad, sep=", ")


if __name__ == '__main__':
    main()

Come si può vedere, per qualsiasi frutto istanziato, la lista degli elementi risulta la medesima:

macedonia a cui appartiene la mela:
mela granny smith
macedonia a cui appartiene l'arancia:
mela granny smith, arancia siciliana
macedonia a cui appartiene il kiwi:
mela granny smith, arancia siciliana, kiwi friulano
==========
macedonia a cui appartiene la mela:
mela granny smith, arancia siciliana, kiwi friulano
macedonia a cui appartiene l'arancia:
mela granny smith, arancia siciliana, kiwi friulano
macedonia a cui appartiene il kiwi:
mela granny smith, arancia siciliana, kiwi friulano

Va notato che, affinché questo metodo funzioni, l'attributo di istanza deve essere di tipo mutabile (liste, dizionari, ecc.) e non di tipo immutabile (numeri, stringhe, tuple, ecc.) -- a meno che non si implementi un metodo di classe (vedi più avanti).

Una possibile implementazione è la seguente, che usa un dizionario come attributo privato di classe:

class Fruit(object):
    __salad = {'quantity': 0} # attributo di classe, vale per tutte le istanze
    # in questo esempio, è un dizionario, di cui ci interessa un solo valore
    # qui lo rendiamo privato

    def __init__(self, name):
        self.setName(name)
        self.__alerts = []
        self.__salad['quantity'] += 1

        ...

    def getSaladInfoByKey(self, key):
        return self.__salad[key]

def main():
    myapple = Fruit('mela granny smith')
    print('Numero di frutti fino ad ora: %d' % myapple.getSaladInfoByKey('quantity'))

    myorange= Fruit('arancia siciliana')
    print('Numero di frutti fino ad ora: %d' % myorange.getSaladInfoByKey('quantity'))

    mykiwi= Fruit('kiwi friulano')
    print('Numero di frutti fino ad ora: %d' % mykiwi.getSaladInfoByKey('quantity'))

L'output sarà il seguente:

Numero di frutti fino ad ora: 1
Numero di frutti fino ad ora: 2
Numero di frutti fino ad ora: 3

In alternativa, è possibile definire un metodo di classe (che, a differenza dei metodi di istanza, riceve come primo parametro un riferimento alla classe e non all'oggetto istanziato):

class Fruit(object):
    __salad = 0

    @classmethod
    def __incSaladCount(cls):
        cls.__salad += 1

    @classmethod
    def getSaladCount(cls):
        return cls.__salad

    def __init__(self, name):
        self.setName(name)
        self.__alerts = []
        self.__incSaladCount()

....

def main():
    myapple = Fruit('mela granny smith')
    print('Numero di frutti fino ad ora: %d' % myapple.getSaladCount())

    myorange= Fruit('arancia siciliana')
    print('Numero di frutti fino ad ora: %d' % myorange.getSaladCount())

    mykiwi= Fruit('kiwi friulano')
    print('Numero di frutti fino ad ora: %d' % mykiwi.getSaladCount())
    print('Numero di frutti fino ad ora: %d' % myapple.getSaladCount())
    print('Numero di frutti fino ad ora: %d' % Fruit.getSaladCount())

Si noti che richiamare

mykiwi.getSaladCount()
myapple.getSaladCount()
Fruit.getSaladCount()

è esattamente equivalente.

Uno o due underscore? (piccola nota sul name mangling)

Piccolo approfondimento necessario. In Python non esiste un vero e proprio meccanismo di protezione degli attributi, come in Java o C++. Ci si basa semplicemente sulla convenzione. Un programmatore sa che, quando il nome di un attributo o di una funzione inizia con il simbolo di underscore, questo attributo o funzione deve essere considerato privato, e di conseguenza sa che se cerca di accedere a quell'attributo "da fuori" sta facendo una cosa "sporca" e poco raccomandabile (sebbene lecita).

Se il nome di un attributo inizia con un doppio underscore, a quell'attributo non è direttamente (facilmente) possibile accedere da fuori, a causa del name mangling. Si consideri il seguente esempio:

class Foo():
    def __init__(self):
        self.bar = 1
        self._bar = 2
        self.__bar = 3

f = Foo()
print("f.bar: %d" % f.bar)
print("f._bar: %d" % f._bar)
print("f.__bar: %d" % f.__bar)

Il risultato è questo:

f.bar: 1
f._bar: 2
Traceback (most recent call last):
  File "...private.py", line 10, in <module>
    print("f.__bar: %d" % f.__bar)
AttributeError: 'Foo' object has no attribute '__bar'

Come si vede, l'accesso all'attributo __bar dall'esterno della classe non è stato possibile, mentre quello all'attributo _bar sì. Per rendersi conto del motivo, si deve sapere che tutti gli attributi di un oggetto vengono memorizzati in un dizionario di nome __dict__, nel quale le chiavi sono costituite dai nomi degli attributi, ma -- nel caso di attributi il cui nome inizia con un doppio underscore -- viene anteposto ad esso il nome della classe. Noto questo, è comunque possibile accedere agli attributi "privati":

class Foo():
    def __init__(self):
        self.bar = 1
        self._bar = 2
        self.__bar = 3

f = Foo()
print("f.bar: %d" % f.bar)
print("f._bar: %d" % f._bar)
#print("f.__bar: %d" % f.__bar)

print(f.__dict__)
print("f.__bar (accesso indiretto): %d" % f.__dict__['_Foo__bar'])
print("f.__bar (accesso con nome della classe): %d" % f._Foo__bar)

Output:

f.bar: 1
f._bar: 2
{'_bar': 2, 'bar': 1, '_Foo__bar': 3}
f.__bar (accesso indiretto): 3
f.__bar (accesso con nome della classe): 3

Il name mangling è stato introdotto soprattutto per evitare conflitti sui nomi quando si usa l'ereditarietà, non tanto per la protezione reale degli attributi. È importante tenere presente questo fatto, poiché si potrebbe avere qualche problema nell'uso di classi derivate (di cui parleremo approfonditamente più avanti). Si consideri questo esempio:

class Foo():
    def __init__(self):
        self.bar = 1
        self._bar = 2
        self.__bar = 3

class DerivedFromFoo(Foo):
    def update(self):
        self.bar = 10
        self._bar = 20
        self.__bar = 30

f = DerivedFromFoo()
print(f.__dict__)

f.update()
print(f.__dict__)

Come si può vedere osservando l'output qui sotto, la funzione update() della classe derivata non modifica il valore di __bar, ma crea nel dizionario un nuovo elemento:

{'_bar': 2, 'bar': 1, '_Foo__bar': 3}
{'_bar': 20, '_DerivedFromFoo__bar': 30, 'bar': 10, '_Foo__bar': 3}

In conclusione, se gli attributi devono essere accessibili dalle classi derivate, ma non dall'esterno, si fa iniziare il loro nome con un singolo underscore (quasi equivalente agli attributi protected di Java o C++), mentre se si vuole che siano privati si fa iniziare il loro nome con un doppio underscore.

Nomi dei metodi speciali

Tutte le classi dispongono di alcuni metodi predefiniti che si possono ridefinire in caso di necessità. Questi metodi hanno nomi preceduti e seguiti da doppio underscore. Ad esempio, la funzione __init__() contiene le istruzioni di inizializzazione di un'istanza, la funzione __str__() restituisce informazioni sull'istanza in forma di stringa, ecc. Ulteriori informazioni si possono trovare nella documentazione ufficiale di Python.

results matching ""

    No results matching ""