EREDITARIETA’ MULTIPLA
Nell’affrontare gli argomenti avanzati in Python direi di parlare subito di ereditarietà multipla, una caratteristica molto importante. Quando abbiamo parlato di ereditarietà, abbiamo visto come si può derivare una classe base da una superclasse. Ora affronteremo l’argomento ereditarietà multipla supportata pienamente in Python, che permette ad una classe base di ereditare da due o più classi.
La classe derivata eredita dalle due superclassi BClass e CClass. In esse sono stati definiti due attributi di tipo intero. Entrambi gli attributi vengono ereditati da AClass. Ovviamente anche i metodi delle superclassi vengono ereditati, vediamo un esempio.
Ora dobbiamo chiederci cosa succede se due o più superclassi da cui eredita una classe base hanno definito un metodo con stessa signature e stesso nome? Vediamo come Python gestisce la risoluzione dei metodi.
METHOD RESOLUTION ORDER (MRO)
La risposta al quesito precedente è che verrà invocato il metodo presente in BClass, questo perché Python per la risoluzione dei metodi usa una determinata regola che viene chiamata Method Resolution Order (MRO).
FUNZIONAMENTO DEL MRO
Dapprima in base a questo algoritmo il metodo xFunc() viene cercato nella sottoclasse AClass, se trovato viene immediatamente utilizzato e la ricerca si arresta. Altrimenti si risale di un livello la gerarchia di ereditarietà e sono esaminate le superclassi nell’ordine in cui sono definite nella dichiarazione della sottoclasse, prima BClass poi CClass. Essendo la funzione xFunc() definita in BClass sarà il metodo di questa superclasse ad essere eseguito. Se tale metodo non fosse definito in BClass allora sarà eseguito il metodo di CClass se definito.
MECCANISMO DI RICERCA
Il meccanismo di ricerca dell’MRO può propagarsi su tutta la catena di gerarchie che vede coinvolta AClass, nella figura si vede che se il metodo non è definito né in BClass né in CClass comunque la ricerca continua. Il metodo è definito in DClass, CClass essendo una sua sottoclasse ne eredita l’attributo che automaticamente viene ereditato anche in AClass in quanto sottoclasse di CClass. Se il metodo non fosse definito neanche in DClass l’MRO andrebbe a cercare nella classe object capostipite di tutta la catena di ereditarietà in Python, per cui l’ultima classe da cui cercherebbe l’attributo è proprio la classe object. Se non viene trovato neanche nella classe capostipite si ottiene un’eccezione AttributeError.
LE CLASSI OBJECT E TYPE
object e type sono in cima alla catena di generalizzazione in Python.
class MyClass:
pass
myObj = MyClass()
Partiamo dalla classe object, tutti gli oggetti in Python sono istanza di questa classe. Anche le classi in Python sono oggetti quindi MyClass() è un’istanza di object. myObj è un’istanza sia di MyClass() che di object essendo comunque un oggetto. Quindi sia MyClass() che myObj sono istanze di object. type è una classe base anch’essa in cima alla gerarchia di generalizzazione che contiene tutte le istanze delle classi, sia le classi predefinite del linguaggio, ma anche delle classi che noi definiamo.
RELAZIONI TRA TYPE E OBJECT
object è una classe e un oggetto (tutto in Python è un oggetto) quindi object è un’istanza di se stessa. Essendo una classe, object è anche un’istanza di type. Anche type è una classe e un oggetto quindi è istanza di object e un’istanza di se stessa in quanto classe.
IL COSTRUTTORE __NEW__
Il metodo __init__ che abbiamo usato come costruttore in realtà per essere più precisi non è un vero e proprio costruttore ma un inizializzatore. Quindi __init__ non serve a costruire le classi bensì ad inizializzarle dopo che l’istanza è stata costruita con lo statement __new__.
Python prima invoca il metodo __new__ che è un metodo statico e non necessita del decoratore essendo un metodo predefinito del linguaggio. Crea l’istanza di una classe, poi invoca __init__ che si trova l’istanza già creata e inizializza i suoi attributi. __init__ riceve in ingresso un’istanza già fabbricata con il metodo __new__.
__new__(cls [, ….])
cls è la classe di cui intendiamo produrre un’istanza seguita da argomenti facoltativi. L’istanza creata e tutti gli argomenti del costruttore __new__ vengono a loro volta passati a __init__.
Generalmente si costruisce un’istanza e si ritorna invocando il metodo __new__ nella superclasse di MyClass() che in questo caso corrisponde ad object. __new__ viene implementato di default nella classe object. Invocando con il metodo super la superclasse object lasciamo ad essa il compito di creare e ritornare l’istanza di MyClass(). Una volta che è stata creata e ritornata l’istanza, il flusso del programma procede correttamente invocando __init__ a cui viene passata l’istanza appena creata nell’argomento self e inizializzando l’istanza di MyClass().
class animale(): def __new__(cls): istanza1 = super().__new__(cls) print(f'Istanza animale creata') return istanza1 def __init__(self): print(f'Istanza animale inizializzata') def mangia(self): pass def respira(self): pass class mammifero(animale): def __new__(cls,nome): istanza2 = super().__new__(cls) print(f'Istanza {nome} creata') return istanza2 def __init__(self,nome): super().__init__() self.nome=nome print(f'Istanza {nome} inizializzata') def mangia(self): print(f'Classe mammifero istanza {self.nome} sta mangiando') def respira(self): print(f'Classe mammifero istanza {self.nome} sta respirando') class persona(mammifero): def __new__(cls,nome,nomepersona,cognome): print(nomepersona) print(cognome) istanza3 = super().__new__(cls,nome) return istanza3 def __init__(self,nome,nomepersona,cognome): super().__init__(nome) self.nomepersona=nomepersona self.cognome=cognome def denominazione(self): print(f'Classe persona denominazione : {self.nomepersona} {self.cognome}') p = persona('persona','Luigi','Rossi') p.denominazione() p.mangia() p.respira()
import math #QUESTO E' UN PICCOLO PROGRAMMA A SCOPO DIDATTICO CHE ILLUSTRA #L'EREDITARIETA' MULTIPLA. IN UN PROGRAMMA REALE NON SERVONO TRE #CLASSI PER GESTIRE SEMPLICI OPERAZIONI MATEMATICHE. class a(object): def __init__(self,numero): self.numero = numero def seno(self): s=f'Sono nella classe a. Il seno di {self.numero} è {math.sin(self.numero)}' print(s) def coseno(self): s=f'Il coseno di {self.numero} è {math.cos(self.numero)}' print(s) class b: def __init__(self,numero): self.numero = numero def tangente(self): s=f'La tangente di {self.numero} è {math.tan(self.numero)}' print(s) def arcotangente(self): s=f'L\'arco tangente di {self.numero} è {math.atan(self.numero)}' print(s) def seno(self): s=f'Sono nella classe b. Il seno di {self.numero} è {math.sin(self.numero)}' print(s) def radice(self): s=f'Sono nella classe b. La radice di {self.numero} è {math.sqrt(self.numero)}' print(s) class calcolatrice(a,b): def __init__(self,numero,valore): super().__init__(numero) s=f'Sono nella classe derivata valore = {valore}' print(s) c = calcolatrice(144,90) c.seno() c.radice() s=f'MRO: {calcolatrice.__mro__}' print(s) #LA CLASSE TYPE E OBJECT class MyClass(object): pass myObj = MyClass() print(isinstance(myObj,MyClass)) print(isinstance(myObj,object)) print(isinstance(myObj,type)) print(isinstance(MyClass,object)) print(isinstance(MyClass,type)) print(isinstance(object,object)) print(isinstance(object,type)) print(isinstance(type,type)) print(isinstance(type,object))
ITERABILI E ITERATORI
Un contenitore è una struttura dati contenente un certo numero di elementi e che ammette un test di appartenenza del tipo True o False. I contenitori che abbiamo già incontrato sono List, Dictionary, Set, Tuple e String. In generale un contenitore è anche un oggetto Iterabile. Non è vero il contrario, cioè tutti gli iterabili sono dei container, ad esempio un file aperto in Python è iterabile ma non è un contenitore che rimane pur sempre un buon esempio di oggetto iterabile. Un iterabile è un oggetto che ritorna un iteratore allo scopo di poter scorrere i propri elementi. Un oggetto iterabile deve implementare il metodo __iter()__ che ritorna un iteratore. L’iteratore è un oggetto che produce il prossimo elemento di un iterabile attraverso il metodo __next()__.
Un oggetto che implementa sia __iter()__ che __next()__ è allo stesso tempo un Iterabile e un Iteratore. __iter()__ ritorna un’istanza di se stesso. Vediamo un esempio con una lista che è un oggetto che soddisfa queste due caratteristiche.
Usando un ciclo for in per scorrere la lista stiamo facendo la stessa cosa, in più for in gestisce l’errore StopIteration.
IMPLEMENTARE UN ITERATORE
#L'algoritmo itera tra 0 e un valore intero passato come parametro class MyIterator(): def __iter__(self): self.myAttr = 0 return self def __next__(self): if self.myAttr <= self.number: n=self.myAttr self.myAttr+=1 return n else: raise StopIteration @property def number(self): return self.__number__ @number.setter def number(self,number): self.__number__ = number m = MyIterator() m.number=300 myIter = iter(m) for x in myIter: print(x)
GENERATOR FUNCTION
È una funzione che si comporta come una normale funzione Python tranne per un aspetto molto importante, la parola chiave yield. Se all’interno di una funzione compare questa keyword allora l’intera funzione viene trasformata in un iteratore. Dove compare yield la funzione si ferma, cede il valore corrente dell’iterazione al chiamante e se viene invocata nuovamente riparte dall’istruzione successiva a yield.
L’ISTRUZIONE RETURN
All’interno di una funzione generatore possiamo usare l’istruzione return che solleva l’eccezione StopIteration.
GENERATOR EXPRESSION
Una generator expression è di fatto equivalente a una List Comprehension. In figura è riportato il codice di una List Comprehension. Una volta che abbiamo prodotto la nuova lista noi la possiamo scorrere tutte le volte che vogliamo. Questo non è vero con una Generator Expression che si ferma alla prima iterazione.
Facciamo lo stesso esempio con una Generator Expression che anziché usare le parentesi quadre utilizza le tonde. Alla seconda iterazione la Generate Expression non stampa nulla, questo perché il generatore è stato esaurito durante la prima iterazione e quindi non può essere iterato nuovamente.
PERCHE’ SCEGLIERE UN GENERATOR EXPRESSION?
A questo punto dobbiamo porci la domanda. perché usare le Generator Expression? La risposta è questa: Una List Comprehension produce una lista e la generazione di questa lista viene eseguita subito in un solo colpo. Invece una Generator Expression viene eseguita in modo lazy cioè produce i suoi valori ad ogni singola iterazione. Se il numero di elementi da produrre è molto elevato una Generator Expression è molto più performante
APPROFONDIMENTO
LE CLASSI OBJECT E TYPE
In Python, le classi object e type svolgono un ruolo fondamentale nell’implementazione del sistema di tipi e della gerarchia di classi. La loro comprensione approfondita è cruciale per chi desidera padroneggiare la programmazione orientata agli oggetti (OOP) in Python. Di seguito fornirò una descrizione dettagliata di ciascuna di queste classi e del loro ruolo all’interno del linguaggio.
La Classe object
La classe object è la classe base di tutte le classi in Python. Questo significa che ogni classe in Python, direttamente o indirettamente, eredita da object. La classe object definisce alcune funzionalità di base che sono disponibili per tutti gli oggetti Python.
Caratteristiche principali della classe object:
•Ereditarietà Universale: Tutte le classi in Python, anche quelle definite dall’utente, ereditano da object. Se si crea una classe senza specificare una superclasse, Python la renderà automaticamente una sottoclasse di object.
•Metodi di Base: object fornisce alcuni metodi di base comuni a tutti gli oggetti Python, come:
•__new__(cls): È un metodo statico che è responsabile della creazione di una nuova istanza della classe. È chiamato prima di __init__.
•__init__(self): È il costruttore della classe, utilizzato per inizializzare l’oggetto dopo la sua creazione.
•__str__(self): Rappresenta l’oggetto come una stringa e viene richiamato dalla funzione str().
•__repr__(self): Fornisce una rappresentazione stringa dell’oggetto, utile per il debugging.
•__eq__(self, other): Definisce il comportamento dell’operatore di uguaglianza ==.
•OOP Puro: Python, a partire dalla versione 3, è un linguaggio “puro” orientato agli oggetti, nel senso che anche i tipi primitivi come gli interi, le stringhe e le liste derivano da object.
La Classe type
La classe type in Python è una classe speciale che gioca un duplice ruolo: è sia la classe base di tutte le metaclassi, sia la metaclasse predefinita per tutte le classi. Una metaclasse in Python è una “classe di classi” – una classe la cui istanza è una classe.
Caratteristiche principali della classe type:
•Creazione delle Classi: Quando si definisce una nuova classe in Python, in realtà si sta creando un’istanza della classe type. Ad esempio, se si scrive class MyClass: pass, Python esegue in background MyClass = type(‘MyClass’, (object,), {}).
•Funzione type(): La funzione incorporata type() ha due usi distinti:
•Se chiamata con un argomento, restituisce la classe (tipo) dell’oggetto passato. Ad esempio, type(10) restituisce <class ‘int’>.
•Se chiamata con tre argomenti, type(name, bases, dict) viene utilizzata per creare dinamicamente una nuova classe. Ad esempio, type(‘MyClass’, (object,), {‘x’: 5}) crea una nuova classe chiamata MyClass con una variabile di classe x inizializzata a 5.
•Metaclassi Personalizzate: Usando type come metaclasse, si possono creare metaclassi personalizzate, permettendo la modifica della creazione e del comportamento delle classi. Una metaclasse viene specificata usando la sintassi class MyClass(metaclass=MyMetaClass):.
•Relazione Circolare: Un aspetto interessante della classe type è la sua relazione circolare con object. Infatti:
•object è un’istanza di type.
•type è una sottoclasse di object.
•Allo stesso tempo, type è un’istanza di se stesso, il che crea una struttura autoreferenziale che permette a Python di gestire il sistema di tipi.
Relazione tra object e type
La relazione tra object e type è essenziale per comprendere il modello di tipi di Python:
•object è la classe base di tutte le classi, comprese le classi che derivano da type.
•type è la metaclasse di tutte le classi, inclusa object.
Questa relazione circolare tra type e object è ciò che rende possibile la flessibilità del sistema di tipi di Python, permettendo la creazione dinamica di classi e la personalizzazione delle loro metaclassi.
Conclusione
La comprensione della classe object e della classe type è fondamentale per chiunque desideri approfondire la programmazione in Python. object fornisce la base comune per tutti gli oggetti in Python, mentre type gestisce la creazione e il comportamento delle classi stesse. Questi concetti sono al cuore della programmazione orientata agli oggetti in Python e offrono una grande potenza e flessibilità per la creazione di strutture di codice avanzate e personalizzate.
IL COSTRUTTORE __NEW__
Il costruttore __new__ in Python è un metodo speciale che svolge un ruolo cruciale nel processo di creazione di un oggetto. A differenza di __init__, che viene utilizzato per inizializzare un oggetto già creato, __new__ è responsabile della creazione vera e propria dell’oggetto. Comprendere come funziona questo metodo può essere molto utile, specialmente quando si lavora con classi complesse o quando si implementano metaclassi personalizzate.
Caratteristiche e Funzionamento di __new__
1. Responsabilità di __new__:
• Il metodo __new__ è responsabile della creazione e dell’allocazione di memoria per una nuova istanza della classe.
• È il primo metodo ad essere chiamato quando si crea un nuovo oggetto.
• Viene chiamato prima di __init__.
2. Sintassi di __new__:
• La firma del metodo __new__ è generalmente la seguente:
def __new__(cls, *args, **kwargs):
# logica per la creazione dell’oggetto
• Il parametro cls rappresenta la classe stessa, simile a come self rappresenta l’istanza nel metodo __init__.
3. Uso Tipico di __new__:
• In molti casi, non è necessario sovrascrivere __new__, poiché la sua implementazione predefinita è sufficiente per la maggior parte delle classi.
•Si sovrascrive __new__ principalmente quando si ha bisogno di controllare la creazione di nuove istanze di una classe, ad esempio per implementare design pattern come il Singleton o quando si lavora con metaclassi.
4. Ritorno di un Oggetto:
• __new__ deve sempre restituire una nuova istanza della classe, che può essere creata chiamando super().__new__(cls), o direttamente usando object.__new__(cls).
• Se __new__ restituisce un’istanza di una classe diversa, il processo di inizializzazione (__init__) della classe corrente non verrà eseguito.
5. Esempio Pratico:
• Un esempio semplice di utilizzo di __new__ è la creazione di un Singleton:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value):
self.value = value
# Test del Singleton
s1 = Singleton(10)
s2 = Singleton(20)
print(s1 is s2) # True
print(s1.value) # 20
print(s2.value) # 20
• In questo esempio, __new__ garantisce che venga creata solo un’istanza di Singleton, indipendentemente da quante volte venga chiamato il costruttore.
Differenza tra __new__ e __init__
• __new__: È chiamato per creare una nuova istanza di una classe. È un metodo di classe e riceve il parametro cls, che rappresenta la classe stessa. Viene utilizzato quando è necessario controllare la creazione dell’oggetto, e deve restituire l’oggetto creato.
• __init__: Viene chiamato per inizializzare un oggetto già creato (da __new__). È un metodo di istanza e riceve il parametro self, che rappresenta l’oggetto appena creato. __init__ non deve restituire nulla.
Conclusione
Il metodo __new__ è uno strumento potente in Python che offre un controllo fine sulla creazione degli oggetti. Sebbene non sia necessario nella maggior parte dei casi, comprenderne il funzionamento è importante per situazioni avanzate, come la creazione di pattern di progettazione specifici o quando si lavora con metaclassi.
ITERABILI E ITERATORI
In Python, iterabili e iteratori sono concetti fondamentali quando si lavora con collezioni di dati. Vediamo in dettaglio cosa significano e come funzionano.
Iterabili (Iterable)
Un oggetto è considerato iterabile se può essere utilizzato in un ciclo (for). Esempi di iterabili includono liste, tuple, stringhe, dizionari, set, e qualsiasi altro oggetto che implementa il metodo speciale __iter__().
Quando si utilizza un ciclo for su un iterabile, Python crea automaticamente un iteratore per quell’oggetto e poi lo usa per iterare attraverso gli elementi.
Esempi di iterabili:
• Liste: [1, 2, 3]
• Tuple: (1, 2, 3)
• Stringhe: “abc”
Un iterabile è un oggetto che può restituire un iteratore.
Iteratori (Iterator)
Un iteratore è un oggetto che rappresenta uno stream di dati, può essere iterato (si può andare al prossimo elemento) e tiene traccia del suo stato durante l’iterazione. Gli iteratori implementano due metodi speciali:
• __iter__(): ritorna l’oggetto iteratore stesso.
• __next__(): ritorna il prossimo elemento della sequenza. Se non ci sono più elementi, solleva l’eccezione StopIteration.
Gli iteratori sono utilizzati per iterare attraverso gli elementi di un iterabile. Una volta consumato un iteratore, non è possibile riutilizzarlo senza re-inizializzarlo.
Esempio di iteratore:
# Creare un iteratore da una lista
numbers = [1, 2, 3]
iterator = iter(numbers)
# Utilizzare l’iteratore
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
print(next(iterator)) # Solleva StopIteration
Differenze tra Iterabile e Iteratore
• Un iterabile è un oggetto che ha la capacità di restituire un iteratore, ma non tiene traccia dello stato dell’iterazione.
• Un iteratore è un oggetto che tiene traccia dello stato corrente dell’iterazione e può essere usato per ottenere gli elementi di un iterabile uno alla volta.
Creazione di un Iteratore Personalizzato
È possibile creare un iteratore personalizzato definendo una classe che implementa i metodi __iter__() e __next__().
Ecco un esempio di un iteratore che restituisce i numeri da 1 a 5:
class MyIterator:
def __init__(self):
self.current = 1
def __iter__(self):
return self
def __next__(self):
if self.current <= 5:
result = self.current
self.current += 1
return result
else:
raise StopIteration
# Utilizzo dell’iteratore
iterator = MyIterator()
for number in iterator:
print(number)
Conclusione
•. Iterabili: Oggetti che possono essere iterati (liste, stringhe, ecc.).
• Iteratori: Oggetti che mantengono lo stato e generano elementi uno alla volta quando richiesti tramite next().
Capire la differenza tra iterabili e iteratori è essenziale per padroneggiare il flusso di dati e la gestione delle sequenze in Python.
Lascia un commento