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
)instance
) di un oggettoPer esempio:
1234 e 356 sono esemplari di int
'Hello' e 'World' sono esemplari di str
1234 + 356 è la procedura che permette di sommare due interi
'Hello' + 'World' è la procedura che permette di concatenare due stringhe
La metodologia Object-Oriented Programming (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. Per fare un esempio tratto dalla vita quotidiana, dato un esemplare di oggetto automobile
una delle sue caratteristiche è la velocità
. Per ottenere che l'esemplare aumenti la sua velocità 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.
Definire una classe corrisponde a definire una oggetto su misura delle nostre esigenze che contiene una serie definita di dati e di metodi che agiscono su questi dati.
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 q 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.y)
print(origin.x)
4 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,other
che è anch'esso esemplare della classe.
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
distance
può anche essere chiamata esplicitamente 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 e si avrebbe un errore.
print(Coordinate.distance(c, zero))
5.0
r = (2,3)
s = (0,0)
Coordinate.distance(r,s)
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[20], line 3 1 r = (2,3) 2 s = (0,0) ----> 3 Coordinate.distance(r,s) Cell In[18], line 8, in Coordinate.distance(self, other) 7 def distance(self, other): ----> 8 x_diff_sq = (self.x-other.x)**2 9 y_diff_sq = (self.y-other.y)**2 10 return (x_diff_sq + y_diff_sq)**0.5 AttributeError: 'tuple' object has no attribute 'x'
__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 standard. 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
Una classe con metodi generici che vengono specificati in sottoclassi.
La classe 'Shape' contiene l'attributo name e i metodi generici area e perimeter.
class Shape:
def __init__(self, name):
self.name = name
def area(self):
pass
def perimeter(self):
pass
Introduciamo due classi (Circle and Rectangle), che estendono Shape e contengono gli attributi necessari per calcolare area e perimetroa. I metodi generici vengono sovrascritti per ottenere il risultato corretto. L'attributo name viene inizializzato al volore voluto usando il costruttore super().
# super() va chiarito
import math
class Circle(Shape):
def __init__(self, radius):
super().__init__('circle')
self.radius = radius
def area(self):
return math.pi * math.pow(self.radius, 2)
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, b, h):
super().__init__('rectangle')
self.b = b
self.h = h
def perimeter(self):
return 2 * (self.b + self.h)
def area(self):
return self.b * self.h
Creiamo un oggetto Circle con raggio=5 un oggetto Rectangle con base=3 and altezza=2.
my_circle = Circle(radius=5)
# Alternative way
circle1 = Circle(5)
my_rectangle = Rectangle(b=3, h=2)
type(my_circle) # perchè stampa __main__.Circle?
__main__.Circle
Stampiamo l'area e il perimetro di ciascun oggetto:
print(f"my_ircle area: {my_circle.area():.2f}, perimeter: {circle.perimeter():.2f}")
print(f"circle1 area: {circle1.area():.2f}, perimeter: {circle1.perimeter():.2f}")
print(f"my_rectangle area: {my_rectangle.area():.2f}, perimeter: {rectangle.perimeter():.2f}")
Circle area: 78.54, perimeter: 31.42 Circle1 area: 78.54, perimeter: 31.42 Rectangle area: 6.00, perimeter: 10.00
print(f"my_circle radius: {my_circle.radius:.2f}")
print(f"my_circle name: '{my_circle.name}'")
my_circle radius: 5.00 my_circle name: 'circle'
Scriviamo una funzione print_shape che prende un esemplare di Shape e stampa il suo nome, l'area e il perimetro. Poi le pasiamo gli oggetti che abbiamo creato.
def print_shape(shape: Shape):
print(f"{shape.name} area: {shape.area():.2f}, {shape.perimeter():.2f} perimeter")
print_shape(circle)
print_shape(rectangle)
circle area: 78.54, 31.42 perimeter rectangle area: 6.00, 10.00 perimeter
Se si cerca di stampare direttamente le istanze:
print(circle)
print(rectangle)
<__main__.Circle object at 0x00000274FF826088> <__main__.Rectangle object at 0x00000274FF826848>
#Add decoratori + @classmethod + @staticmethod