Questo notebook si basa sulle lezioni 8 e 9 di: https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/
Python contiene molti tipi di dati:
Ciascuno è un oggetto, caratterizzato da
type
)un set di procedure che permettono di interagire con gli esemplari (instance
) di un oggetto
1234 e 356 sono esemplari di int
'Hello' e 'World' sono esemplari di str
1234 + 356 è la procedura che permette di sommare due interi
OOP permette di separare la rappresentzione interna dall'interfaccia attraverso cui interagire con gli oggetti e dai dettagli delle manipolazioni a cui gli oggetti possono essere soggetti: dato un esemplare di oggetto automobile
una delle sue caratteristiche è la velocità
. Per ottenere che l'esemplare aumenti la sua velocità devo devo applicare la procedura aumentare la pressione sul pedale dell'acceleratore
. Tutti gli esemplari di oggetto automobile
aumentano la propria velocità usando la stessa procedura. Non ho bisogna di conoscere i dettagli di come un aumento della pressione sull'acceleratore porta al risultato di aumentare la velocità. Un eventuale miglioramento dei meccanismi interni non richiede una modifica dell'interfaccia pedale dell'acceleratore
e quindi del modo con cui l'utilizzatore interagisce con l'esemplare.
Bisogna distinguere chiaramente fra creare una classe e utilizzare un esemplare di una classe:
#keyword nome classe da cui eredita (opzionale)
class Coordinate(object):
# definire qui gli attributi (dati e procedure)
def __init__(self,p,q): # metodo indispensabile. Inizializza gli esemplari.
# a self viene sostituito il nome assegnato a ogni esemplare
# p e p sono input che vengono assegnati agli attributi `x` e `y`
self.x = p
self.y = q
c = Coordinate(3,4)
origin = Coordinate(0,0)
# su `c` e `origin` possiamo utilizzare solo i metodi `x` e `y`
print(c.x)
print(origin.x)
3 0
Aggiungiamo alla classe Coordinate
un metodo per calcolare la distanza fra due punti. Uno dei due punti viene identificato con self
in modo da poter chiamare il metodo su un singolo punto.
A parte l'uso di self
e della notazione .
per la chiamata, i metodi interni ad una classe si comportano esattamente come funzioni: prendono input, eseguono operazioni, restituiscono risultati.
class Coordinate(object):
def __init__(self,p,q):
self.x = p
self.y = q
def distance(self, other):
x_diff_sq = (self.x-other.x)**2
y_diff_sq = (self.y-other.y)**2
return (x_diff_sq + y_diff_sq)**0.5
c = Coordinate(3,4)
zero = Coordinate(0,0)
print(c.distance(zero)) # c viene automaticamente assegnato a `self`
# essendo c un esemplare di Coordinate, `distance` è automaticamente disponibile
5.0
c = Coordinate(3,4)
zero = Coordinate(0,0)
print(Coordinate.distance(c, zero)) # `distance` può essere chiamata come una funzione contenuta nel modulo `Coordinate`
# gli oggetti che vengono passati come input devono essere entrambi esemplari di `Coordinate`;
# In caso contrario `distance` non potrebbe accedere alle proprietà `x` e `y` degli oggetti
5.0
r = (2,3)
s = (0,0)
Coordinate(r,s)
<__main__.Coordinate at 0x103500e80>
__str__
¶c = Coordinate(3,4)
print(c)
<__main__.Coordinate object at 0x1035000a0>
Il comportamento di default di print
su un membro di una classe non è molto utile. Può essere modificato definendo il metodo speciale str. Supponiamo di volere che print(c)
restituisca <3,4>. Possiamo fare così:
class Coordinate(object):
def __init__(self,p,q):
self.x = p
self.y = q
def distance(self, other):
x_diff_sq = (self.x-other.x)**2
y_diff_sq = (self.y-other.y)**2
return (x_diff_sq + y_diff_sq)**0.5
def __str__(self):
return "<"+str(self.x)+","+str(self.y)+">"
c = Coordinate(3,4)
print(c)
<3,4>
Esistono molti altri metodi speciali. Per esempio:
__add__(self, other) --> self + other
__sub__(self, other) --> self - other
__eq__(self, other) --> self == other
__lt__(self, other) --> self < other
__len__(self) --> len(self)
Ciascuno di questi metodi può essere ridefinito come abbiamo fatto per __str__
.
Definiamo una classe Fraction
che rappresenti una frazione come una coppia numeratore-denominatore e permetta di eseguire le operazioni fra frazioni in modo esatto, senza trasformarle in floating point.
# Better solution in https://stackoverflow.com/questions/36539460/implementing-negation-by-overriding-neg-in-python
class Fraction():
# missing check that denominators are non zero and that input are integers
def __init__(self,n,d=1): # default value for d
# built in simplification
for div in range(2,min(abs(n),abs(d))+1): # Notice start- and end-point
# abs needed for negative fractions
while n%div==0 and d%div==0: # can have multiple powers of div
n=n/div
d=d/div
self.n = int(n)
self.d = int(d)
def __str__(self):
if self.d ==1:
return str(self.n) # don't print denominator if equal 1
else:
return str(self.n)+"/"+str(self.d)
def __add__(self, other): # defines self + other
nr = self.n*other.d + self.d*other.n
dr = self.d*other.d
res = Fraction(nr,dr)
return res
def __sub__(self, other): # defines self - other
nr = self.n*other.d - self.d*other.n
dr = self.d*other.d
res = Fraction(nr,dr)
return res
def __mul__(self, other): # defines self*other
nr = self.n*other.n
dr = self.d*other.d
res = Fraction(nr,dr)
return res
def __truediv__(self, other): # defines self/other
nr = self.n*other.d
dr = self.d*other.n
res = Fraction(nr,dr)
return res
def __neg__(self): # defines - self
nr = -self.n
dr = self.d
res = Fraction(nr,dr)
return res
def __eq__(self, other): # defines self==other
if self.n == other.n and self.d == other.d:
return True
else:
return False
Alcuni esempi di utilizzo della classe Fraction
.
a = Fraction(4,6)
print(a)
2/3
b = Fraction(3,9)
print(b)
1/3
print(a+b)
1/1
print(b-a)
-1/3
print(a==b)
False
print(a==a)
True
print(a*b)
2/9
print(a/b)
2/1
print(a.__neg__())
print(a.__neg__().n)
minusa =-a
print('type:',type(minusa),'\nvalue of minusa:',minusa,'\nnumerator:',minusa.n,'\ndenominator:',minusa.d)
-2/3 -2 type: <class '__main__.Fraction'> value of minusa: -2/3 numerator: -2 denominator: 3
f = Fraction(3)
print(f)
3
Abbiamo visto che se nell\' __init__
di una classe C
è definito l'attributo x
è possibile accedere al valore dell'attributo per un esemplare es
con la notazione es.x
.
È però preferibile e fortemente incoraggiato accedere e modificare, dall'esterno della classe, gli attributi di un esemplare attraverso metodi appositi, detti getters e setters, che devono essere definiti all'interno della classe.
class Animal(object):
def __init__(self, age): # è possibile e necessario dichiarare solamente `age` quando una instance viene creata
self.age = age
self.name = None
def get_age(self): # getter. Restituiscono l'informazione. Posso stamparla o manipolarla ulteriormente.
return self.age
def get_name(self): # getter
return self.name
def set_age(self, newage): # setter
self.age = newage
def set_name(self, newname=""): # setter con valore di default
self.name = newname
def __str__(self):
return "animal:"+str(self.name)+":"+str(self.age)
cane = Animal(3)
print('age:',cane.get_age())
print('name:',cane.get_name())
age: 3 name: None
cane.set_name('Fido')
print('name:',cane.get_name())
name: Fido
Una delle idee basilari dell'Object Oriented programmi è di interagire con gli oggetti solamente attraverso le funzioni della classe, nascondendo i dettagli interni dell'implementazione.
Una classe può ereditare metodi e attributi da una superclasse già esistente.
Dalla (super)classe Animal
possiamo costruire sottoclassi che possiedono metodi e attributi particolari.
Una sottoclasse con due metodi addizionali e una implementazione diversa del metodo __str__
:
class Cat(Animal): # `Cat` eredita tutti gli attributi e i metodi di `Animal`
def speak(self): # Il metodo `speak` stampa un' informazione. Restituisce `None`.
print("meow")
def purr(self):
print("Purr")
def __str__(self):
return "cat:"+str(self.name)+":"+str(self.age)
Tutti i metodi di Animal
sono applicabili a una instance di Cat
:
gatto = Cat(5)
gatto.set_name('Fuffy')
print('age:',gatto.get_age())
print('name:',gatto.get_name())
age: 5 name: Fuffy
Metodi nuovi o modificati
gatto.speak()
gatto.purr()
print(gatto)
meow Purr cat:Fuffy:5
Una sottoclasse con metodi e attributi addizionali:
class Person(Animal): # `Person` eredita tutti gli attributi e i metodi di `Animal`. Non eredita da `Cat`.
def __init__(self, name, age): # La creazione di una instance di `Person` richiede un nome e un'età
Animal.__init__(self, age)
self.set_name(name)
self.friends = [] # attributo non presente nella superclasse
def get_friends(self): # getter
return self.friends
def add_friend(self, fname): # setter appropriato per una lista
if fname not in self.friends:
self.friends.append(fname)
def speak(self): # metodo con nome identico ad un metodo di `Cat` ma implementazione diversa
print("hello")
def age_diff(self, other):
diff = self.age - other.age
print(abs(diff), "year difference")
def __str__(self):
return "person:"+str(self.name)+":"+str(self.age)
p1 = Person("Bob",22)
p1.add_friend("Carol")
p1.add_friend("George")
p1.get_friends()
['Carol', 'George']
Una Person
non può fare le fusa:
p1.purr()
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-77-6ea84170e471> in <module> ----> 1 p1.purr() AttributeError: 'Person' object has no attribute 'purr'
Una sottoclasse che eredita da un'altra sottoclasse con metodi e attributi addizionali e/o modificati
import random
class Student(Person): # `Student` eredita metodi e attributi da `Person`
def __init__(self, name, age, major=None):
Person.__init__(self, name, age)
self.major = major
def get_major(self): # getter
return self.major
def change_major(self, major): # setter
self.major = major
def speak(self):
r = random.random()
if r < 0.25:
print("I have homework")
elif 0.25 <= r < 0.5:
print("I need sleep")
elif 0.5 <= r < 0.75:
print("I should eat")
else:
print("I am watching tv")
def __str__(self):
return "student:"+str(self.name)+":"+str(self.age)+":"+str(self.major)
Uno Student
, essendo una Person
, può avere amici:
s1 = Student(age= 19,name="Peter")
s1.add_friend("Susan")
s1.get_friends()
['Susan']
Ha una raffinata capacità di conversazione:
s1.speak()
s1.speak()
s1.speak()
I should eat I am watching tv I am watching tv
Una sottoclasse che possiede un attributo condiviso con tutti gli esemplari della classe
# Note to self: opportuno commentare le parti di codice che non si vogliono attivare immediatamente
# Evita di riscrivere più volte la parte iniziale
class Rabbit(Animal):
tag = 1
def __init__(self, age, parent1=None, parent2=None):
Animal.__init__(self, age)
self.parent1 = parent1
self.parent2 = parent2
self.rid = Rabbit.tag
Rabbit.tag += 1 # ogni volta che viene creato un esemplare tag aumenta di uno
def get_rid(self):
return str(self.rid).zfill(3) # zfill(3) aggiunge gli eventuali zeri iniziali per arrivare a tre caratteri
def get_parent1(self):
return self.parent1
def get_parent2(self):
return self.parent2
def __add__(self, other):
# returning object of same type as this class
return Rabbit(0, self, other)
def __eq__(self, other): # L'operatore == controlla che due esemplari abbiano gli stessi due genitori
parents_same = (self.parent1.rid == other.parent1.rid
and self.parent2.rid == other.parent2.rid)
parents_opposite = (self.parent2.rid == other.parent1.rid
and self.parent1.rid == other.parent2.rid)
return parents_same or parents_opposite
r1 = Rabbit(1)
r2 = Rabbit(1,"Leopold","Margarete")
print('id of r1:',r1.get_rid())
print('id of r1:',r2.get_rid())
id of r1: 001 id of r1: 002
#Add decoratori + @classmethod + @staticmethod