Python è un linguaggio di programmazione multi-paradigma
In Python il concetto di OOP segue alcuni principi base: ereditarietà, incapsulamento, polimorfismo.
Classi
Un oggetto è un insieme di dati (variabili) e metodi (funzioni) che agiscono su tali dati; ogni valore è un oggetto (object.__class__); si utilizza il costrutto classe:
class MyNewClass: ''' docstring della nuova classe ''' pass
Una classe crea un un nuovo namespace locale in cui sono definiti tutti i suoi attributi (dati o funzioni)
__doctring__: restituisce la docstring della classe
pass: pass è un operazione nulla. E' utile come placeholder
Non appena definiamo una classe, viene creato un nuovo oggetto classe con lo stesso nome, ci consente di accedere ai diversi attributi e di creare un'istanza di nuovi oggetti di quella classe
#esempio class MyClass: "docstring della mia classe" x=5 def func(self): print("Hello") print(MyClass.x) # Output: 5 print(MyClass.func) # Output: <function MyClass.func at 0x000..> print(MyClass.__doc__) # Output: 'docstring della mia classse'
Creare un nuovo oggetto
nome_oggetto=nome_classe()
I metodi di un oggetto sono le funzioni corrispondenti di quella classe
class MyClass: "secondo esempio" x=5 def func(self): print("Hello") obj = MyClass() # create a new MyClass print(MyClass.func) # Output: <function MyClass.func at 0x000..> print(obj.func) # Output: <bound method MyClass.func of ...> obj.func() # Calling function func() Output: Hello
Self e Metodi di Istanza
Se eseguo l'istruzione MyClass.func(): TypeError: func() missing 1 reguired positional argument: 'self'
Invece obj.func() funziona senza argomento
Ogni volta che un oggetto chiama il suo metodo, l'oggetto stesso viene passato come primo argomento
obj.func() <-> MyClass.func(obj)
Chiamare un metodo con un elenco di n argomenti equivale a chiamare la funzione corrispondente con un elenco di argomenti creato inserendo l'oggetto del metodo prima del primo argomento
E' l'argomento implicito che rappresenta l'oggetto, per convenzione lo chiamiamo self
Costruttore
Ci sono delle funzioni di classi che iniziano con doppio underscore __
__init__() viene chiamata ogni volta che viene istanziato un nuovo oggetto di quella classe (inizializzatore)
class Studente: def __init__(self, nome, cognome): self.nome=nome self.cognome=cognome
Per creare variabili d'istanza
studente_uno=Studente("Mario","Rossi") studente_due=Studente("Luigi","Verdi") >>> print(studente_uno) #__main__.Studente object at 0x00..>
Variabili di Classe vs. Variabili di Istanza
class Dog: kind='canine' def __init__(self, name): self.name=name >>> d = Dog('Fido') >>> e = Dog('Buddy') >>> d.kind 'canine' >>> e.kind 'canine' >>> d.name 'Fido' >>> e.name 'Buddy'
Se lo stesso nome di attributo si verifica sia in un'istanza che in una classe, la ricerca degli attributi da la priorità all'istanza:
class Warehouse: purpose='storage' region='west' >>> w1=Warehouse() >>> print(w1.purpose, w1.region) storage west >>> w2=Warehouse() >>> w2.region='east' >>> print(w2.purpose, w2.region) storage east
Variabili e metodi privati
C'è una convenzione per indicare nomi che devono essere trattati come privati: __nome
Il carattere di sottolineatura iniziale __ prima del nome della variabile / funzione / metodo indica che è solo per uso interno
class A: def __init__(self, x, y) self.__private=x self.public=y def __prmet(self): return self.__private def pubmet(self): return self.public def ppmet(self): return self.__private, self.public >>> a=A("Mario","Rossi") >>> a.public 'Rossi' >>> a.pubmet() 'Rossi' >>> a.ppmet() ('Mario','Rossi')
Se si desidera che la classe base abbia delle sottoclassi e che alcuni attributi non siano da loro utilizzati, utilizzare la denominazione con doppi __ iniziali
Questo richiama l'algoritmo name mangling di Python, in cui il nome della classe è inserito nel nome dell'attributo
Questo aiuta a evitare name collision degli attributi nel caso le sottoclassi inavvertitamente contengono attributi con lo stesso nome
Si noti che solo il nome della classe viene utilizzato nel nome modificato, quindi se una sottoclasse sceglie sia lo stesso nome di classe che lo stesso nome di attributo, è ancora possibile ottenere collisioni di nomi.
Ereditarietà (Inheritance)
Definizione di una nuova classe con modifiche minime o nulle ad una classe esistente, la nuova classe è detta classe figlia (sottoclasse), quella da cui eredita è detta classe genitore (classe base)
class Persona: def __init__(self, nome, cognome): self.nome=nome self.cognome=cognome class Stutente(Persona): pass class Studente(Persona): def __init__(self, nome, cognome, scuola): self.nome=nome self.cognome=cognome self.scuola=scuola
Super()
La funzione super permette di estendere o modificare il metodo di una classe base nella classe figlia che lo eredita
class Studente(Persona): def __init__(self, nome, cognome, scuola): super().__init__(nome, cognome) self.scuola=scuola class Docente(Persona): def __init__(self, nome, cognome, materie=None): super().__init__(nome, cognome) if materie is None: self.materie=[] else: self.materie=materie
Python ha due funzioni built-in:
isinstance() per verificare il tipo (la classe) dell'istanza
issubclass() per verificare l'ereditarietà
Ritornano True o False
Python supporta anche una forma di ereditarietà multipla. Una definizione di classe con più classi di base:
class DerivedClassName(Base1, Base2, Base3): <statement-1> . . <statement-N>
Parte da Base1 poi (ricorsivamente) nelle classi base di Base1 e, solo se non vi è stato trovato, viene ricercato in Base2, e cosi via.
Overriding
Una classe può avere un'implementazione differente di un metodo della classe genitore
class Persona: def __init__(self, nome, cognome): self.nome=nome self.cognome=cognome def stampa(self): print('genitore') class Studente(Persona): def __init__(self, nome, cognome, scuola): super().__init__(nome, cognome) self.scuola=scuola def stampa(self): print("sottoclasse") >>> Mario=Studente("Mario","Rossi","Dibris") >>> Mario.stampa() # Studente.stampa(Mario) sottoclasse
Metodi di Classe
I metodi degli oggetti che abbiamo visto finora vengono chiamati attraverso il nome dell'istanza della classe (oggetto), che viene passato come parametro self all'interno del metodo.
I metodi di classe vengono invece chiamati direttamente dalla classe che viene passata come parametro cls all'interno del metodo, cls, come self, è semplice convenzione, si usa il decoratore @classmethod
Generalmente questi metido servono per istanziare una nuova istanza di classe, passando dei parametri diversi rispetto a quelli richiesti dal costruttore.
class Rectangle: def __init__(self, width, height): self.width=width self.height=height def area(self): return self.width * self.height @classmethod def new_square(cls, side_length): return cls(side_length, side_length) square = Rectangle.new_square(5) print(square.calculate_area())
Creiamo una classe per creare un dizionario per ogni utente, ed una sottoclasse per creare un dizionario con più informazioni
Facciamo finta che il e il cognome siano dati come string unica (es 'Paolo-Rossi')
*args permette di passare 0 o un numero arbitrario di parametri alla funzione
class Crea_profilo: def __init__(self, name, surname): self.name=name self.surname=surname def crea(slef): return {"Nome":self.name,"Cognome":self.surname} @classmethod def from_string(cls, string_persona, *args): nome, cognome, = string_persona.split("-") return cls(nome, cognome, *args) class Crea_profilo_età(Crea_profilo): def __init__(self, name, surname, age): super().__init__(name,surname) self.age=age def crea(self): return {"Nome":self.name, "Cognome":self.surname, "Età":self.age} #overriding >>> persona1=Crea_profilo("Mario","Rossi") >>> persona2=Crea_profilo.from_string("Paolo-Verdi") >>> persona3=Crea_profilo_età.from_string("Luigi-Giallo",54) >>> print(persona1.crea()) {'Nome': 'Mario', 'Cognome': 'Rossi'} >>> print(persona2.crea()) {'Nome': 'Paolo', 'Cognome': 'Verdi'} >>> print(persona3.crea()) {'Nome': 'Luigi', 'Cognome': 'Giallo','Età':54}
Metodi Statici
I metodi statici si richiamano senza creare un'istanza della classe e pertanto non ricevono un parametro self
Non possono accedere alla proprietà della classe
Per i metodi statici si usa il decoratore @staticmethod
class Dates: def __init__(self, date): self.date=date def getDate(self): return self.date @staticmethod def toDashDate(date): return date.replace("/","-") date=Dates("15-12-2016") dateFromDB="15/12/2016" dateWithDash=Dates.toDashDate(dateFromDB) if (date.getDate()==dateWithDash): print("Equal") else: print("Unequal")
Overloading degli operatori
Python consente di cambiare la definizione degli operatori predefiniti quando applicati a tipi definiti dall'utente
class Punto: ... def __add__(self, AltroPunto): return Punto(self.x + AltroPunto.x, self.y + AltroPunto.y) >>> P1=Punto(3,4) >>> P2=Punto(5,7) >>> P3=P1+P2 >>> print P3 (8,11)
L'espressione P1+P2 è equivalente a P1.__add__(P2)
Dunder Methods
Molto utili perchè garantiscono il compartimento polimorfico degli operatori
__init__ e __adds__ sono metodi speciali
>>> 5+5 10 >>> "Py"+"thon" 'Python' >>> int.__add__(5,5) 10 >>> str.__add__("Py","thon") 'Python'
Tanti altri metodi speciali:
__new__, __del__, __str__, __bool__, __sub__, __eq__ (uguale a), __ne__ (diverso da), __getitem__ (object[item]), __setitem__ (object[item]=value) ecc.
Per ogni classe posso definirli in modo diverso
Per maggiori info consultare la documentazione di Python!
Iterators
Per la maggior parte degli oggetti container abbiamo fatto il for-loop:
for element in [1,2,3]: print(element) for element in (1,2,3): print(element) for key in {'one':1,'two':2} print(key) for char in "123" print(char) for line in open("myfile.txt"): print(line, end='')
Iterator protocol
L'istruzione for chiama iter() sull'oggetto container
La funzione restituisce un oggetto iteratore (iterator object) che definisce il metodo __next__() che accede agli elementi nel contenitore, uno alla volta
Quando non ci sono più elementi, __next__() solleva un'eccezione StopIteration che dice al ciclo for di terminare; funzione integrata next()
>>> s="abc" >>> it=iter(s) >>> it <str_iterator object at 0x0..> >>> next(it) 'a' >>> next(it) 'b' >>> next(it) 'c' >>> next(it) StopIteration
Usare gli iteratori con le classi
class Reverse: """Iterator for looping over a sequence backwards.""" def __init__(self, data): self.data=data self.index=len(data) def __iter__(self): return self def __next__(self): if self.index==0: raise StopIteration self.index=self.index-1 return self.data[self.index] >>> rev=Reverse('spam') >>> iter(rev) <__main__.Reverse ojbect at 0x0..> >>> for char in rev: print(char) m a p s
Moduli (librerie)
I moduli sono dei file usati per raggruppare costanti, funzioni e classi
Ci consentono di suddividere e organizzare meglio i nostri progetti
Python include già una lista estensiva di moduli standard (anche conosciuti come standard library)
import nome_modulo #esempio import random
Possiamo usare le funzioni dir() e help() per esplorare i contenuti del modulo
Importare i moduli
import modulo #importare intero modulo from modulo import nome #importare una costante o una funzione specifica import modulo as mio_nome #rinominare il modulo >>> import random as rdm >>> rdm.randint(1,100) 41 from modulo import #importa tutto il modulo
Creare un modulo
Qualsiasi file con estensione.py può essere sia eseguito che importato
In Python non esiste una vera distinzione tra modulo e 'main'
Per creare un modulo: basta scrivere un normalissimo script python quindi salvato con estensione .py
# operazioni.py def somma(x,y): return x+y def sottrazione(x,y): rturn a-b
Per importarlo in un altro programma: import operazioni
Package
Python offre un ulteriore livello di organizzazione: i package. Un package è una raccolta di moduli, che in genere corrisponde alla directory che li contiene
File vuoto __init__.py
Recommended Comments
There are no comments to display.