Programmazione ad oggetti

La programmazione ad oggetti nasce per superare difficolta' intrinseche in linguaggi come il C, che si incontrano quando si utilizzano questi linguaggi per scrivere programmi molto grandi (centinaia di migliaia di istruzioni). I concetti alla base della programmazione ad oggetti risalgono agli anni 60-70, ma la programmazione ad oggetti ha iniziato a prendere piede solo negli anni 90, col C++, ed e' stata molto usata per scrivere interfacce grafiche. E' poi diventata di moda, e tutti i linguaggi moderni come Java, Python, Ruby, sono ad oggetti; la programmazione ad oggetti e' stata inserita perfino in vecchi linguaggi come il FORTRAN od il COBOL.

La programmazione ad oggetti utilizza strutture di dati chiamate "classi"; le classi sono insiemi di dati eterogenei, e contengono anche le funzioni (dette anche "metodi") che operano sui dati della classe. Dati e funzioni della classe sono anche detti "membri" della classe. I dati sono detti "attributi" della classe.

Bisogna distinguere la definizione di classe dalla sua istanza. La definizione della classe ne descrive la struttura, ma di per se non riserva spazio di memoria per la classe e non crea nulla, viene trattata a tutti gli effetti come un nuovo tipo di dato.

L'istanza (oggetto) e' una realizzazione della classe, individuata da un nome. L'istanza ha il suo spazio in memoria, i suoi dati, le sue funzioni etc.

La classe viene definita una volta e poi se ne possono fare tante istanze, ognuna col suo nome ed i suoi dati. Ci sono dati propri delle singole istanze e dati "della classe", comuni a tutte le istanze.

Una classe ha dei membri privati, che sono visibili solo dall'interno della classe, e membri pubblici, che sono visibili dall'esterno. Si interagisce con la classe utilizzando i suoi membri pubblici, di preferenza chiamando le sue funzioni pubbliche.

L'insieme delle funzioni e dati con cui si interagisce con la classe sono chiamati "interfaccia" della classe. La classe viene utilizzata come una scatola nera, di cui e' noto l'uso, ma non il contenuto.

Quindi una volta definita l'interfaccia di una classe, si sa come usarla nel resto del programma e se ne puo' ignorare la struttura interna. In grossi progetti si inizia a strutturare il software definendo le interfacce delle classi. Una volta fatto questo, parti diverse del progetto possono essere assegnate a gruppi di sviluppatori diversi.

Una classe puo' essere costruita aggiungendo parti ad una classe pre-esistente. In questo caso si parla di "ereditarieta'"; la nuova classe ("classe derivata") "eredita" membri dalla classe pre-esistente, detta "classe base" o classe padre. La classe derivata puo' anche sostituire membri della classe base con membri definiti al suo interno, in questo caso si dice che fa l'"ovverride" di membri della classe base.

In certi linguaggi (C++) si parla di "classi astratte" per indicare classi che servono solo a specificare delle interfacce, che saranno poi implementate in classi derivate. In questo modo le classi finiscono per definire solo dei comportamenti, e non dicono nulla di come questi comportamenti saranno implementati. Questo approccio permette di applicare allo sviluppo del software un modello di top-down; ove prima si disegna lo schema generale del software e poi si definiscono i dettagli implementativi.

Una classe puo' essere costruita in modo da potere essere utilizzata con diversi tipi di dati (interi, float, caratteri od altro). In questo caso, in cui si utilizza la stessa interfaccia per dati diversi, si parla di "polimorfismo", o di "programmazione generica". Questo permette di ridurre i tempi di sviluppo, non bisogna scrivere classi diverse a seconda del tipo di dati che devono trattare.

Il polimorfismo puo' essere implementato in diversi modi: con i "template" del C++ si scrivono classi per tipi di dati generici, ed al momento della compilazione il compilatore crea la classe relativa al tipo di dato per cui e' richiesta. Un altro approccio e' quello in cui non si sa quale tipo di dato si utilizzera' fino all'esecuzione del programma. E solo in esecuzione viene definito il tipo di dato che verra' utilizzato. Questo di chiama "late binding" o "run time binding". In C++ questo viene implementato utilizzando puntatori ad una classe base, che ha diverse classi derivate a seconda dei tipi. Quando e' il momento viene utilizzata la classe derivata del tipo giusto. In Python i tipi delle variabili sono definiti solo all'assegnazione della variabili, e le funzioni non sono che procedure generiche da applicare alle variabili e non dipendono in principio dai tipi delle variabili. Il Python implementa in modo naturale il polimorfismo.

In certi linguaggi e' possibile definire come operano gli operatori tipo somma, moltiplicazione etc., sulle istanze di una classe. Gli operatori algebrici diventano speciali funzioni della classe, e si puo' dar senso ad operazioni algebriche fra oggetti complessi, non solo fra numeri. Questo si chiama override degli operatori.

Programmazione ad oggetti in Python

Creazioni di classi ed istanze

L'implementazione delle classi in Python e' semplice. Si riduce ad un modo di isolare ed individuare nomi di oggetti. Definire una classe significa dare ai nomi di variabili e funzioni una struttura gerarchica, che riflette la gerarchia di classi base e classi derivate. In Python le classi sono semplicemente contenitori di nomi.

I programatori Python hanno l'abitudine di usare per i nomi delle classi nomi "capitalized", ovvero identificativi composti da parole con la prima lettera maiuscola. Questa convenzione non e' obbligatoria, ma e' sempre raccomandata.

Una classe e' definita con l'istruzione class:

class C3:
  """ blocco che definisce la classe"""
  a=[1,2,3]
  b="abc'
  ....

Questa istruzione definisce la classe di nome C3; variabili, e funzioni della classe sono definite dopo l'istruzione class, il blocco della definizione della classe, al solito, e' identificato da un rientro. Le variabili definite entro una classe sono locali alla classe (private), sono accessibili alla classe ed alle sue derivate. Per accedere ad esse fuori della classe bisogna che siano identificate in modo esplicito come membri della classe.

class C1(object):
  kk=3
  ...
  ...


class C3(C1,C2):
      """ classe C3 che eredita dalle
          classi C1 e C2 ""
      a=66
      .........
      .........

Questa sintassi indica che la classe C3 eredita i membri delle classi C1 e C2. I membri di C1 e C2 sono riconosciuti come membri di C3. La classe C1 eredita da object, la classe 'padre' di tutte le classi in Python. In Python 3 tutte le classi, anche se non lo si specifica, ereditano da object. In Python 2 occorre specificarlo, oppure si creano di classi di vecchio tipo, che mancano di alcune caratteristiche ed hanno diverso modo di cercare i nomi degli attributi nella gerarchia definita dall'ereditarieta'.

Per creare istanze della classe C3 si scrivono istruzioni del tipo:

oggetto1=C3()
oggetto2=C3()

ora oggetto 1 ed oggetto2 si riferiscono a 2 diverse istanze della classe C3; sono in pratica riferimenti a due diverse copie di C3. Occorre fare attenzione a non dimenticare le parentesi tonde: oggetto1=C3 non crea un istanza, ma un nuovo riferimento per la classe C3.

Per distruggere un'istanza si assegna al nome dell'istanza la stringa vuota:

a=NomeClasse()
b=NomeClasse()

b=""   # L'istanza 'b' non ha piu' riferimenti e viene eliminata.

Attributi

Le variabili di una classe sono chiamate attributi, le sue funzioni metodi. Insieme, attributi e metodi sono i membri della classe. Nel programma Python, una volta istanziata la classe, gli attributi dell'istanza si indicano con: nomeistanza.nomeattributo

Esempio:

class NomeClasse(object):
    kk=3

a=NomeClasse()
b=NomeClasse()

      a.kk vale 3
      b.kk vale 3

Esistono attributi della classe (validi per tutte le istanze) ed attributi della singola istanza. Nell'esempio sopra kk e' un attributo della classe, tutte le istanze lo hanno eguale quando sono create, ma poi si puo' cambiare. Se lo si cambia riferendosi all'istanza si comporta come appartenente all'istanza e viene ridefinito come una variabile dell'istanza, se invece si cambia riferendosi alla classe cambia per tutte le istanze che non lo hanno ridefinito.

Ad esempio, se ridefinizione di kk per la sola istanza 'a':

a.kk=10

b.kk vale ancora 3, NomeClasse.kk vale 3, ma se ridefinisco kk per la classe:

NomeClasse.kk=100

a.kk resta 10, dato che e' stato cambiato nell'istanza, ma ora b.kk e' cambiato, e se creo una nuova istanza questa avra' il nuovo valore di kk.

Un attributo della classe puo' anche essere definito da 'fuori' della classe:

nomeclasse.jj=77

a.xyz=63

Ora sia b.jj che a.jj valgono 77, se invece definisco un attributo solo per un'istanza quello vale solo per l'istanza ove lo ho definito:

a.xyz=63

L'istanza 'a' ha un nuovo attributo xyz, ma b.xyz , NomeClasse.xyz non esistono

Gli attributi della classe sono conservati internamente in un dizionario della classe, chiamato: __dict__ Il dizionario puo' essere stampato con:

print(NomeClasse.__dict__ )

Anche l'istanza ha il dizionario __dict__ ove sono solo gli attributi propri dell'istanza e non della classe, per vederlo:

print(a.__dict__)

Gli attributi che iniziano con un doppio underscore: "__" sono locali alla classe e non sono visti da fuori. In realta' sono solo nascosti ed hanno nome: "_nomeclasse__nomeattr" , per cui per un'istanza della classe si trovano come: "nomeistanza._nomeclasse__nomeattr" , per la classe come: "nomeclasse._nomeclasse__nomeattr"

Anche gli attributi che iniziano con un singolo underscore: "_" sono, per abitudine dei programmatori, attributi locali, ma questi non sono neanche nascosti.

Docstring

Come per le funzioni, anche nelle classi e' buona norma inserire una descrizione della classe all'inizio, in una stringa che viene conservata nella variabile "__doc__" della classe che puo' essere stampata con la funzione "print" ed e' mostrata anche dalla funzione "help". Sia print che help possono essere chiamate con la classe come argomento o con un'istanza come argomento:

class NomeClasse(object):
   ''' classe di prova
   per fare prove '''
   kk=3

a=NomeClasse()

help(NomeClasse)          # ma anche help(a)
print(NomeClasse.__doc__) # ma anche print(a.__doc__)

Metodi

Le funzioni definite entro le classi, chiamate metodi della classe, devono avere come primo argomento la parola self che identifica l'istanza su cui opera la funzione. Le classi in Python non sono che sequenze di operazioni effettuate su variabili individuate da nomi ed occorre distinguere su che istanza si sta operando. Entro una funzione "self" si usa per indicare che una variabile od una funzione appartiene all'istanza della classe e non alla classe in genere. In Python3 esistono istruzioni speciali, come il decoratore @classmethod, che permettono eccezioni a questa regola, ma in genere abbiamo:

class ProvaUno(object):
   kk=3
   def somma(self,a)
      return (a+self.kk,a+ProvaUno.kk)

Entro le funzioni, le variabili di classe o di istanza, e non locali alla funzione, vanno precedute dal nome della classe, con il punto, oppure da self, che punta all'istanza. Altrimenti sono solo interne alla funzione e non sono viste da fuori.

La funzione viene chiamata sull'istanza, con:

d=ProvaUno()
d.somma(10)   # e ritorna la tupla: (13,13)

d.kk=10
d.somma(3)    # ritorna (13,6) , kk e' cambiata solo per l'istanza

Notare come nella chiamata self non ci sia; siccome la chiamata e' qualificata con il nome dell'istanza davanti, Python sa gia' quale e' l'istanza, e nella chiamata l'istanza e' sottintesa. L'interprete Python la mette lui automaticamente.

Si puo' anche chiamare una funzione sulla classe, ma in questo caso si deve dare l'istanza come primo argomento:

ProvaUno.somma(d,100)  # restituisce (1010, 1003)

In Python3, se la funzione non utilizza variabili dell'istanza, questa si puo' non mettere in argomento.

Altro esempio:

class C3:
   " stringa di documentazione della classe C3"
   a=6
   def printa(self):
     print( C3.a )
   def f(self):
     print( "f di C3, istanza:",self)
   def somma(self,c,d):
      self.b=7
      return c+d+C3.a+self.b


I1=C3()
I2=C3()

I1.f()

"""
Qui chiamo la funzione f sull'istanza I1,
l'istanza e' passata alla funzione in modo
automatico, nell'argomento self.
f stampera' la stringa:
"f di C3, istanza: <__main__.C3 object at 0x1b84ad0>"
"""

I1.somma(1,2)  # ottengo il valore: 16

a e' una variabile della classe e non dell'istanza.

C3.a=100       # cambio una variabile della classe

a questo punto:

I2.somma(1,2)  # Ora ottengo 110
I1.somma(1,2)  # anche qui, 110
fornisce 110, come anche I1.somma(1,2)


class C4:
   " stringa di documentazione della classe C4"
   a=6
   def printa(self):
     print( C4.a )
   def f(self):
     print (" f di C4, istanza: ",self)
   def somma(self,c,d):
      self.b=7
      return c+d+C4.a+self.b
   def somma2(self,c,d):
      self.b=7
      return c+d+self.a+self.b

I4=C4()

I4.somma(1,2) ed I4.somma2(1,2) mi danno il valore 16.
Nella funzione somma2 a contiene infatti il numero 6, sia come
valore della classe che come valore dell'istanza.
Se cambio solo come variabile di istanza:
I4.a=1000
ho che I4.somma(1,2) mi da sempre 16, mentre I4,somma2(1,2) mi da 1010.

Inizializzazione delle istanze

La funzione __init__ , se presente, viene chiamata automaticamente quando si crea un'istanza: Gli argomenti della __init__ sono gli argomenti dell'istruzione che crea l'istanza:

class ProvaDue(object):
   "docstring di prova "
   kk=3
   def __init__(self,a,b):
      self.ka=a
      self.kb=b
   def printargs(self):
      print "args:",self.ka,",",self.kb

instan=ProvaDue(10,20)        # istanza , con argomenti

instan.printargs()            # stampa le variabili di istanza

Prima di __init__ viene chiamata __new__ ; funzione usata per cose particolari, come generare classi diverse in funzione di certi parametri (class factory) e vari trucchetti. Questo non avviene per le classi di vecchio tipo del Python 2.

La funzione __del__ viene chiamata prima che l'istanza venga distrutta, anche se la distruzione avviene ad opera del sistema automatico di garbage collection di Python.

Ereditarieta', dettagli

Abbiamo visto che nell'ereditarieta' la classe figlia ha , oltre ai suoi, anche gli attributi della classe padre:

class A(object):
   k=3

class B(A):      # eredita A
   j=5

B.k vale 3, la classe B ha k B.j vale 5, ma A.j non esiste

Per vedere le relazioni fra classi si possono usare le funzioni:

issubclass(derivata,parent) : che da true se e' una sottoclasse
isinstance(istanza,classe)  : che da true se e' una istanza

In Python esiste l'ereditarieta' multipla ed una classe puo' ereditarne diverse:

class AA(object):
   aa=222

class BB(object):
   bb=444


class CC(AA,BB):
   pass

Qui la classe CC non contiene nulla di suo (ha solo l'istruzione "pass"), ma ha sia l'attributo aa, che l'attributo bb.

Quando la struttura dell'ereditarieta' e' complicata puo' essere che un attributo con lo stesso nome compaia in diverse parti della gerarchia (il modo di cercare nella gerarchia e' chiamato: MRO: Method Resolution Order). In Python 3 la ricerca degli attributi avviene salendo di un livello e cercando, da sinistra a destra, in tutte le classi del livello superiore, poi salendo ancora di un livello e cosi' via. Le vecchie classi del Python 2 invece risalivano tutta la gerarchia relativa al primo "parent" da sinistra, poi tutta la gerarchia relativa al secondo e cosi' via.

Uso attributi in una classe derivata

Contrariamente a quanto ci si aspetterebbe, in una classe derivata, od entro funzioni di una classe derivata, non si possono usare, senza qualificarli, attributi della classe padre, non si puo' quindi mettere:

class A(object):
   k=3

class B(A):
   j=5
   jj=j+k       # Questo, con k nella classe parent, non funziona

B.jj=B.j+B.k    # Questo e' corretto

In questo caso Python cerca k nello scope globale e non lo trova, o, se trova un reference di nome k, usa quello. Questo perche' in Python gli statements dentro la classe sono valutati prima che la classe venga effettivamente creata e si definiscano le regole di ricerca di attributi nello spazio dei nomi. In Python la gerarchia delle classi e' infatti solo un criterio di ricerca nello spazio dei nomi.

Invece tutto va bene se jj=j+k viene definito dopo la definizione della classe, ad esempio, se fuori della classe si ha:

B.jj=B.j+B.k

Qui infatti k e' riconosciuto come membro di B, in quanto ereditato da A ( Vedi: http://stackoverflow.com/questions/9760595/accessing-parent-class-attribute-from-sub-class-body ed anche: http://bugs.python.org/issue11339

Problemi analoghi si incontrano se, entro una classe, si usano metodi della classe senza qualificarli con il nome della classe o con self. Infatti nell'eseguire gli statements dentro la classe Python semplicemente gli mette davanti un qualificatore, e se, nell'uso, il qualificatore manca, il programma da errore. Per cui la classe seguente da errore, quando si chiama la funzione addtwice:

class Bag:
def __init__(self):
    self.data = []
def add(self, x):
    self.data.append(x)
def addtwice(self, x):
    add(x)
    add(x)

Funziona se si mette (vedi "The Python Tutorial" , di Van Rossum):

class Bag:
def __init__(self):
    self.data = []
def add(self, x):
    self.data.append(x)
def addtwice(self, x):
    self.add(x)
    self.add(x)

Funzioni di classi ereditate

In una classe, se si devono chiamare le funzioni di una classe ereditata o di un'altra classe, si deve fornire l'argomento "self", e chiamare la funzione sulla classe:

class AA(object):
   def print3(self):
      print("3 in AA")
class BB(AA):
   def print33(self):
      AA.print3(self)    # chiamo print3 della classe AA, dandogli l'istanza
      print("33 in BB")

b=BB()
b.print33()      # stampa : "3 in AA"  e, nella riga seguente: "33 in BB"

Override attributi

La classe figlia puo' ridefinire attributi della classe padre:

class A(object):
   k=3

class B(A):      # eredita A
   j=5

class C(B):      # eredita B
   k=33          # ridefinisce K, delle classe A

Qui C eredita B che eredita A, ma C ridefinisce k, per cui A.k e B.k restano 3, ma C.k vale 33

Inizializzazione ed ereditarieta'

L'inizializzazione di una istanza e' effettuata dalla funzione __init__. Occorre pero' tener presente che una classe non chiama in modo automatico la __init__ della classe padre. Questa operazione, se necessaria, deve essere fatta in modo esplicito nella classe __init__ della figlia, con istruzione del tipo:

super.__init__(self,..)

Overloading operatori e funzioni speciali

Gli operatori algebrici possono essere ridefiniti per una classe, facendoli corrispondere a funzioni speciali della classe; In questo modo e' possibile definire operazioni fra oggetti complessi con la sintassi delle normali operazioni algebriche.

Questo viene fatto definendo le funzioni speciali; queste sono le funzioni vengono chiamate da Python quando incontra operazioni fra istanze della classe.

Ad esempio, una classe che descrive vettori puo' definire una somma ed un prodotto vettoriale con:

class Vector(object):
   def __init__(self,a,b):
     self.a=a
     self.b=b

   def __add__(self,other):
     return (self.a+other.a,self.b+other.b)

   def __mul__(self,other):
     return self.a*other.a+self.b*other.b


x=Vector(1,2)
y=Vector(10,20)

print x+y           # fornisce la tupla (11,22)
print x*y           # fornisce il numero 50

Ci sono di queste funzioni per i confronti, gli operatori logici etc.:

__add__(self,other) , __sub__(self,other) ,__div__(self,other)
__lt__ (self,other), __le__(self,other)
__eq__(self,other) , __ne__(self,other)
__gt__(self,other) ,  __ge__(self,other)

Ci sono tutti gli operatori, perfino un operatore "__call__" : usato nel caso la classe sia chiamata come fosse una funzione, con una coppia di parentesi ed argomenti.

Sono importanti alcune funzioni chiamate quando si cerca, o non si trova, un attributo. Queste permettono di definire l'attributo a run-time:

__getattr__(self,nome)      : viene chiamata quando non si trova
                              un attributo, di dato nome.
                              Questa funzione ritorna l'attributo,
                              definendolo.

__getattribute__(self,nome) : viene chiamata quando ci si riferisce
                              ad un attributo che esiste,
                              ma NON se e' definita __getattr__ .
                              Permette di modificare un attributo a run-time

__getitem__ (self,index)    : viene chiamata quando si incontra,
                              per la classe, l'indice fra quadre X[i]  ,
                              ove X e' l'istanza di una classe.
                              Questo fa apparire la classe come una lista.

       Quando una classe simula una lista sono utili anche, per
       operazioni sugli elementi della sequenza che la classe simula:
       __setitem__ , __delitem__ __len__ __contains__ __index__

       Ci sono anche  operatori per creare iteratori sulla sequenza:
       __iter__ ,__next__

Altri membri speciali:

__new__    : viene chiamato prima di __init__, per usi particolari

__del__    : chiamato prima della distruzione della classe

__str__    : viene chiamato per convertire l'oggetto in una
             stringa per le stampe dell'oggetto.

__repr__   : viene chiamato per una rappresentazione testuale
             dell'oggetto, ad esempio nell'uso interattivo

__call__   : usato caso mai la classe sia chiamata come fosse
             una funzione

__name__  __class__  : sono il nome della classe ed un
                       puntatore alla classe stessa

__bases__ : tupla di classi base (classi da cui si eredita)
            La classe base di tutte e': object.

__dict__  : dizionario di attributi della classe

Decoratori di classi

In Python > 2.6 ci sono i decoratori anche per le classi. sono funzioni che prendono in argomento una classe, ci fanno modifiche, aggiunte, o fanno cose accessorie, poi restituiscono la classe modificata.

Esempio:

def decname(classname,a,b,c):
   ... operazioni varie con classname
   return classname

C'e' una sintassi abbreviata quando si vuole applicare il decoratore ad una classe:

@decname
class classname(object):

Alcuni decoratori predefiniti sono:

  • @staticmethod :

    e' un decoratore che rende una funzione statica, ovvero che viene chiamata senza riferirsi ad un'istanza, ma e' una funzione della classe, unica per tutta la classe. (vale per python > 2.2)

  • @classmethod :

    e' come @staticmethod, ma in automatico ha come primo argomento il nome della classe.(python > 2.2)

  • @abstractmethod :

    in Python3, crea funzione astratta (virtuale) in una classe padre. Questa funzione deve essere ridefinita nelle classi figlie. Ed una classe con metodi astratti non puo' essere istanziata direttamente, ma solo ereditata.

Metaclassi

Internamente Python crea le classi (le definizioni delle classi) istanziando la classe: "type", che, e' una "metaclasse", cioe' una classe le cui istanze sono delle classi.

E' possibile estendere la classe "type" e creare una propria metaclasse, ove si ridefiniscono le funzioni __init__ e la __new__ in modo da modificare il comportamento di base dela classi. Per utilizzare la propria metaclasse invece della metaclesse type si usa una sintassi del tipo:

class nomeclassse(metaclass=nomemetaclasse):