31. Funzionalità avanzate delle classi
Nel Capitolo 30 abbiamo imparato a creare classi di base con attributi e metodi di istanza. Ora esploreremo funzionalità di classe più sofisticate che ti danno un controllo granulare su come si comportano i tuoi oggetti. Queste funzionalità ti permettono di creare classi che sembrano tipi Python integrati, con una sintassi naturale per operazioni come addizione, confronto e indicizzazione.
31.1) Variabili di classe vs variabili di istanza
Quando creiamo attributi in una classe, abbiamo due posti fondamentalmente diversi in cui memorizzarli: sulla classe stessa oppure su singole istanze. Comprendere questa distinzione è cruciale per scrivere codice orientato agli oggetti corretto.
31.1.1) Comprendere le variabili di istanza
Le variabili di istanza sono attributi che appartengono a uno specifico oggetto. Ogni istanza ha la propria copia separata di queste variabili. Abbiamo usato le variabili di istanza per tutto il Capitolo 30: sono gli attributi che creiamo in __init__ usando self:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner # Variabile di istanza
self.balance = balance # Variabile di istanza
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.balance) # Output: 1000
print(account2.balance) # Output: 500Ogni istanza di BankAccount ha il proprio owner e balance. Cambiare account1.balance non influisce su account2.balance - sono completamente indipendenti.
31.1.2) Comprendere le variabili di classe
Le variabili di classe sono attributi che appartengono alla classe stessa, non a una particolare istanza. Tutte le istanze condividono la stessa variabile di classe. Definiamo le variabili di classe direttamente nel corpo della classe, fuori da qualsiasi metodo:
class BankAccount:
interest_rate = 0.02 # Variabile di classe - condivisa da tutte le istanze
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def apply_interest(self):
self.balance += self.balance * BankAccount.interest_rate
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
print(BankAccount.interest_rate) # Output: 0.02Nota che possiamo accedere a interest_rate tramite le istanze (account1.interest_rate) oppure tramite la classe stessa (BankAccount.interest_rate). Entrambe fanno riferimento alla stessa variabile.
Ecco cosa rende potenti le variabili di classe: quando cambiamo la variabile di classe, tutte le istanze vedono il cambiamento:
class BankAccount:
interest_rate = 0.02
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
print(account1.interest_rate) # Output: 0.02
print(account2.interest_rate) # Output: 0.02
# Cambia la variabile di classe
BankAccount.interest_rate = 0.03
print(account1.interest_rate) # Output: 0.03
print(account2.interest_rate) # Output: 0.03Entrambe le istanze vedono immediatamente il nuovo tasso di interesse perché stanno tutte guardando la stessa variabile di classe.
31.1.3) La trappola dello shadowing: quando le variabili di istanza nascondono le variabili di classe
Ecco un comportamento sottile ma importante: se assegni un attributo tramite un'istanza, Python crea una variabile di istanza che fa shadowing (nasconde) la variabile di classe:
class BankAccount:
interest_rate = 0.02
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
# Crea una variabile di istanza che fa shadowing della variabile di classe
account1.interest_rate = 0.05
print(account1.interest_rate) # Output: 0.05 (variabile di istanza)
print(account2.interest_rate) # Output: 0.02 (variabile di classe)
print(BankAccount.interest_rate) # Output: 0.02 (variabile di classe)Ora account1 ha la propria variabile di istanza interest_rate che nasconde la variabile di classe. La variabile di classe esiste ancora, ma account1.interest_rate fa riferimento alla variabile di istanza invece. Di solito non è ciò che vuoi: se devi cambiare una variabile di classe, cambiala tramite il nome della classe, non tramite un'istanza.
31.1.4) Usi pratici delle variabili di classe
Le variabili di classe sono utili per dati che dovrebbero essere condivisi tra tutte le istanze:
class Student:
school_name = "Python High School" # Uguale per tutti gli studenti
total_students = 0 # Traccia quanti studenti esistono
def __init__(self, name, grade):
self.name = name
self.grade = grade
Student.total_students += 1 # Incrementa quando si crea uno studente
def __str__(self):
return f"{self.name} (Grade {self.grade}) at {Student.school_name}"
student1 = Student("Alice", 10)
student2 = Student("Bob", 11)
student3 = Student("Carol", 10)
print(student1) # Output: Alice (Grade 10) at Python High School
print(f"Total students: {Student.total_students}") # Output: Total students: 3Nota come usiamo Student.total_students (non self.total_students) in __init__ per rendere chiaro che stiamo modificando la variabile di classe, non creando una variabile di istanza.
31.2) Gestire gli attributi con @property
A volte vuoi controllare cosa succede quando qualcuno accede o modifica un attributo. Per esempio, potresti voler validare che un valore sia positivo, oppure calcolare un valore al volo invece di memorizzarlo. Il decorator @property di Python ti permette di scrivere metodi che sembrano un semplice accesso ad attributi.
31.2.1) Il problema: l'accesso diretto agli attributi non può validare
Quando si accede direttamente agli attributi, non puoi validare o trasformare i valori:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
temp = Temperature(25)
print(temp.celsius) # Output: 25
# Nulla ci impedisce di impostare temperature fisicamente impossibili
temp.celsius = -500 # Sotto lo zero assoluto (-273.15°C)!
print(temp.celsius) # Output: -500
# Oppure valori assurdiamente alti
temp.celsius = 1000000
print(temp.celsius) # Output: 1000000Senza validazione, possiamo impostare accidentalmente dati non validi, portando a bug più avanti nel programma. Potremmo usare metodi come get_celsius() e set_celsius(), ma non è idiomatico in Python. Gli sviluppatori Python si aspettano di accedere agli attributi direttamente, non tramite metodi getter/setter come in Java o C++.
31.2.2) Usare @property per attributi calcolati
Il decorator @property trasforma un metodo in un "getter" a cui si accede come a un attributo:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
"""Convert celsius to fahrenheit on-the-fly"""
return self.celsius * 9/5 + 32
temp = Temperature(25)
print(temp.celsius) # Output: 25
print(temp.fahrenheit) # Output: 77.0 (computed, not stored)Nota che chiamiamo temp.fahrenheit senza parentesi: sembra accedere a un attributo, ma in realtà sta chiamando il metodo. Il valore in fahrenheit viene calcolato ogni volta che vi accediamo, quindi è sempre sincronizzato con celsius:
temp = Temperature(0)
print(temp.fahrenheit) # Output: 32.0
temp.celsius = 100
print(temp.fahrenheit) # Output: 212.0 (automatically updated)31.2.3) Aggiungere un setter con @property_name.setter
Per permettere di impostare una property, aggiungiamo un metodo setter usando il decorator @property_name.setter:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Convert fahrenheit to celsius when setting"""
self.celsius = (value - 32) * 5/9
temp = Temperature(0)
print(temp.celsius) # Output: 0
print(temp.fahrenheit) # Output: 32.0
# Imposta la temperatura usando fahrenheit
temp.fahrenheit = 212
print(temp.celsius) # Output: 100.0
print(temp.fahrenheit) # Output: 212.0Il metodo setter riceve il nuovo valore e può validarlo o trasformarlo prima di memorizzarlo.
31.2.4) Usare le proprietà per la validazione
Le proprietà sono eccellenti per imporre vincoli:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # L'underscore suggerisce "uso interno"
@property
def balance(self):
"""Ottieni il saldo corrente"""
return self._balance
@balance.setter
def balance(self, value):
"""Imposta il saldo, ma solo se non negativo"""
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
account = BankAccount("Alice", 1000)
print(account.balance) # Output: 1000
account.balance = 1500 # Funziona senza problemi
print(account.balance) # Output: 1500
# Questo solleva un errore
account.balance = -100
# Output: ValueError: Balance cannot be negativeNota la convenzione di denominazione: memorizziamo il valore reale in _balance (con un underscore iniziale) e lo esponiamo tramite la property balance. L'underscore è una convenzione Python che suggerisce "questo è un dettaglio di implementazione interno", anche se l'attributo è comunque tecnicamente accessibile. Questo schema ci permette di controllare l'accesso tramite la property mantenendo separata la memorizzazione effettiva.
31.2.5) Proprietà di sola lettura
Se definisci una property senza un setter, diventa di sola lettura:
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
"""Computed read-only property"""
return self.width * self.height
rect = Rectangle(5, 3)
print(rect.area) # Output: 15
rect.width = 10
print(rect.area) # Output: 30 (automatically updated)
# Provare a impostare area solleva un errore
rect.area = 50
# Output: AttributeError: property 'area' of 'Rectangle' object has no setterQuesto è utile per valori derivati che dovrebbero essere calcolati, non memorizzati.
31.3) Metodi di classe con @classmethod
A volte ti servono metodi che lavorano con la classe stessa invece che con le istanze. I metodi di classe ricevono la classe come primo argomento (convenzionalmente chiamato cls) invece di un'istanza (self).
31.3.1) Definire metodi di classe
Creiamo metodi di classe usando il decorator @classmethod:
class Student:
school_name = "Python High School"
def __init__(self, name, grade):
self.name = name
self.grade = grade
@classmethod
def get_school_name(cls):
"""Metodo di classe - riceve la classe, non un'istanza"""
return cls.school_name
# Chiama sulla classe stessa
print(Student.get_school_name()) # Output: Python High School
# Si può chiamare anche su un'istanza (ma cls è comunque la classe)
student = Student("Alice", 10)
print(student.get_school_name()) # Output: Python High SchoolIl parametro cls riceve automaticamente la classe, proprio come self riceve automaticamente l'istanza nei metodi normali.
31.3.2) Costruttori alternativi con metodi di classe
Uno degli usi più comuni dei metodi di classe è creare costruttori alternativi: modi diversi per creare istanze:
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""Crea una Date da una stringa come '2024-12-27'"""
year, month, day = date_string.split('-')
return cls(int(year), int(month), int(day))
@classmethod
def today(cls):
"""Crea una Date per oggi (esempio semplificato)"""
# Nel codice reale useresti il modulo datetime
return cls(2024, 12, 27)
def __str__(self):
return f"{self.year}-{self.month:02d}-{self.day:02d}"
# Costruttore normale
date1 = Date(2024, 12, 27)
print(date1) # Output: 2024-12-27
# Costruttore alternativo da stringa
date2 = Date.from_string("2024-12-27")
print(date2) # Output: 2024-12-27
# Costruttore alternativo per oggi
date3 = Date.today()
print(date3) # Output: 2024-12-27Nota come from_string e today restituiscano entrambi cls(...): questo crea una nuova istanza della classe. Usare cls invece di hardcodare Date fa sì che il codice funzioni correttamente con le sottoclassi (impareremo l'ereditarietà nel Capitolo 32).
31.3.3) Metodi di classe per pattern factory
I metodi di classe sono utili per creare istanze con configurazioni diverse:
class DatabaseConnection:
def __init__(self, host, port, database, username):
self.host = host
self.port = port
self.database = database
self.username = username
@classmethod
def for_development(cls):
"""Crea una connessione configurata per lo sviluppo"""
return cls("localhost", 5432, "dev_db", "dev_user")
@classmethod
def for_production(cls):
"""Crea una connessione configurata per la produzione"""
return cls("prod.example.com", 5432, "prod_db", "prod_user")
def __str__(self):
return f"Connection to {self.database} at {self.host}:{self.port}"
# Facile creare connessioni preconfigurate
dev_conn = DatabaseConnection.for_development()
prod_conn = DatabaseConnection.for_production()
print(dev_conn) # Output: Connection to dev_db at localhost:5432
print(prod_conn) # Output: Connection to prod_db at prod.example.com:543231.3.4) Metodi di classe per contare le istanze
I metodi di classe possono lavorare con variabili di classe per tracciare informazioni su tutte le istanze:
class Product:
total_products = 0
def __init__(self, name, price):
self.name = name
self.price = price
Product.total_products += 1
@classmethod
def get_total_products(cls):
"""Restituisce il numero totale di prodotti creati"""
return cls.total_products
@classmethod
def reset_count(cls):
"""Reimposta il contatore dei prodotti"""
cls.total_products = 0
product1 = Product("Laptop", 999)
product2 = Product("Mouse", 25)
product3 = Product("Keyboard", 75)
print(Product.get_total_products()) # Output: 3
Product.reset_count()
print(Product.get_total_products()) # Output: 031.4) Metodi statici con @staticmethod
I metodi statici sono metodi che non ricevono l'istanza (self) o la classe (cls) come primo argomento. Sono semplicemente funzioni normali che capitano di essere definite dentro una classe perché sono logicamente correlate a quella classe.
31.4.1) Definire metodi statici
Creiamo metodi statici usando il decorator @staticmethod:
class MathUtils:
@staticmethod
def is_even(number):
"""Verifica se un numero è pari"""
return number % 2 == 0
@staticmethod
def is_prime(number):
"""Verifica se un numero è primo (semplificato)"""
if number < 2:
return False
for i in range(2, int(number ** 0.5) + 1):
if number % i == 0:
return False
return True
# Chiama i metodi statici sulla classe
print(MathUtils.is_even(4)) # Output: True
print(MathUtils.is_even(7)) # Output: False
print(MathUtils.is_prime(17)) # Output: True
print(MathUtils.is_prime(18)) # Output: False
# Si possono chiamare anche su un'istanza (ma è la stessa funzione)
utils = MathUtils()
print(utils.is_even(10)) # Output: TrueI metodi statici non hanno bisogno di accedere ai dati di istanza o di classe: sono funzioni di utilità autonome.
31.4.2) Quando usare metodi statici vs metodi di classe vs metodi di istanza
Ecco come scegliere:
class Temperature:
# Variabile di classe
absolute_zero_celsius = -273.15
def __init__(self, celsius):
self.celsius = celsius
# Metodo di istanza - ha bisogno di accesso ai dati dell'istanza (self)
def to_fahrenheit(self):
return self.celsius * 9/5 + 32
# Metodo di classe - ha bisogno di accesso ai dati della classe (cls)
@classmethod
def get_absolute_zero(cls):
return cls.absolute_zero_celsius
# Metodo statico - non ha bisogno di dati dell'istanza o della classe
@staticmethod
def celsius_to_kelvin(celsius):
return celsius + 273.15
@staticmethod
def fahrenheit_to_celsius(fahrenheit):
return (fahrenheit - 32) * 5/9
temp = Temperature(25)
# Metodo di istanza - usa i dati dell'istanza
print(temp.to_fahrenheit()) # Output: 77.0
# Metodo di classe - usa i dati della classe
print(Temperature.get_absolute_zero()) # Output: -273.15
# Metodi statici - solo funzioni di utilità
print(Temperature.celsius_to_kelvin(25)) # Output: 298.15
print(Temperature.fahrenheit_to_celsius(77)) # Output: 25.0Linee guida:
- Usa i metodi di istanza quando ti serve accesso agli attributi dell'istanza (
self) - Usa i metodi di classe quando ti serve accesso agli attributi di classe o vuoi costruttori alternativi (
cls) - Usa i metodi statici quando non ti serve accesso ai dati di istanza o di classe, ma la funzione è logicamente correlata alla classe
Nota: I metodi statici potrebbero essere funzioni standalone, ma metterli nella classe raggruppa la funzionalità correlata ed evita di affollare il namespace globale.
| Tipo di metodo | Primo parametro | Usalo quando |
|---|---|---|
| Metodo di istanza | self | Serve accesso ai dati dell'istanza |
| Metodo di classe | cls | Serve accesso ai dati della classe o costruttori alternativi |
| Metodo statico | (nessuno) | Funzione di utilità correlata alla classe |
31.4.3) Esempio pratico: utility di validazione
I metodi statici sono ottimi per validazione e funzioni di utilità:
class User:
def __init__(self, username, password):
if not User.is_valid_username(username):
raise ValueError("Invalid username")
if not User.is_valid_password(password):
raise ValueError("Invalid password")
self.username = username
self._password = password
@staticmethod
def is_valid_username(username):
"""Verifica se username soddisfa i requisiti"""
return len(username) >= 3 and username.isalnum()
@staticmethod
def is_valid_password(password):
"""Verifica se password soddisfa i requisiti di sicurezza"""
return len(password) >= 8 and any(c.isdigit() for c in password)
# Questi metodi di validazione possono essere usati in modo indipendente
print(User.is_valid_username("alice123")) # Output: True
print(User.is_valid_username("ab")) # Output: False
print(User.is_valid_password("pass1234")) # Output: True
# E possono essere usati in qualsiasi metodo della classe
try:
user = User("ab", "short")
except ValueError as e:
print(f"Error: {e}") # Output: Error: Invalid username31.5) Comprendere i metodi speciali (Magic Methods)
I metodi speciali di Python (chiamati anche magic methods o dunder methods perché hanno doppi underscore) ti permettono di personalizzare come si comportano i tuoi oggetti con le operazioni integrate di Python. Abbiamo già usato __init__, __str__ e __repr__ nel Capitolo 30. Ora ne esploreremo molti altri.
31.5.1) Cosa fanno i metodi speciali
I metodi speciali vengono chiamati automaticamente da Python quando usi una certa sintassi o funzioni integrate:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"Point({self.x}, {self.y})"
point = Point(3, 4)
# Quando chiami print(), Python chiama __str__()
print(point) # Output: Point(3, 4)
# Questo è equivalente a: print(point.__str__())I metodi speciali ti permettono di far comportare le tue classi come tipi integrati. Per esempio, puoi fare in modo che i tuoi oggetti:
- Supportino operazioni aritmetiche (
+,-,*,/) - Siano confrontabili (
<,>,==) - Funzionino con
len(),ine l'indicizzazione - Agiscano come contenitori o sequenze
31.5.2) Categorie comuni di metodi speciali
Ecco le principali categorie di metodi speciali:
Rappresentazione come stringa (come vengono visualizzati gli oggetti):
__str__()- perprint()estr()__repr__()- per la REPL erepr()
Confronto (confrontare oggetti):
__eq__()- per==__ne__()- per!=__lt__()- per<__le__()- per<=__gt__()- per>__ge__()- per>=
Aritmetica (operazioni matematiche):
__add__()- per+__sub__()- per-__mul__()- per*__truediv__()- per/
Contenitore/Sequenza (comportamento simile a collezioni):
__len__()- perlen()__contains__()- perin__getitem__()- per l'indicizzazioneobj[key]__setitem__()- per l'assegnazioneobj[key] = value
Le esploreremo in dettaglio nelle sezioni seguenti.
31.6) Esempio 1: Interfaccia di collezione (len, contains)
Creiamo una classe che gestisce una collezione di elementi e facciamola funzionare con la funzione integrata len() di Python e l'operatore in.
31.6.1) Implementare len per len()
Il metodo speciale __len__() viene chiamato quando usi len() sul tuo oggetto:
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
"""Restituisce il numero di elementi nel carrello"""
return len(self.items)
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
cart.add_item("Orange")
# len() chiama __len__()
print(len(cart)) # Output: 3Senza __len__(), chiamare len(cart) solleverebbe un TypeError. Implementandolo, il nostro ShoppingCart funziona proprio come le collezioni integrate.
31.6.2) Implementare contains per l'operatore in
Il metodo speciale __contains__() viene chiamato quando usi l'operatore in:
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def __len__(self):
return len(self.items)
def __contains__(self, item):
"""Verifica se un elemento è nel carrello"""
return item in self.items
cart = ShoppingCart()
cart.add_item("Apple")
cart.add_item("Banana")
# L'operatore in chiama __contains__()
print("Apple" in cart) # Output: True
print("Orange" in cart) # Output: FalseOra il nostro carrello supporta la sintassi Python naturale per il test di appartenenza.
31.6.3) Costruire una classe di collezione più completa
Creiamo una classe di collezione più realistica che traccia i voti degli studenti:
class GradeBook:
def __init__(self):
self.grades = {} # student_name: list of grades
def add_grade(self, student, grade):
"""Aggiunge un voto per uno studente"""
if student not in self.grades:
self.grades[student] = []
self.grades[student].append(grade)
def __len__(self):
"""Restituisce il numero di studenti"""
return len(self.grades)
def __contains__(self, student):
"""Verifica se uno studente ha dei voti"""
return student in self.grades
def get_average(self, student):
"""Ottiene la media dei voti di uno studente"""
if student not in self:
return None
grades = self.grades[student]
return sum(grades) / len(grades)
def __str__(self):
return f"GradeBook with {len(self)} students"
gradebook = GradeBook()
gradebook.add_grade("Alice", 85)
gradebook.add_grade("Alice", 90)
gradebook.add_grade("Bob", 78)
gradebook.add_grade("Bob", 82)
gradebook.add_grade("Bob", 88)
print(gradebook) # Output: GradeBook with 2 students
print(len(gradebook)) # Output: 2
print("Alice" in gradebook) # Output: True
print("Carol" in gradebook) # Output: False
print(f"Alice's average: {gradebook.get_average('Alice')}") # Output: Alice's average: 87.5
print(f"Bob's average: {gradebook.get_average('Bob')}") # Output: Bob's average: 82.66666666666667Nota come get_average() usi if student not in self: questo chiama il nostro metodo __contains__(), facendo sì che il codice si legga in modo naturale.
31.7) Esempio 2: Overloading degli operatori (add, eq, lt)
L'overloading degli operatori significa definire cosa fanno operatori come +, == e < per le tue classi personalizzate. Questo fa sì che i tuoi oggetti funzionino in modo naturale con la sintassi di Python.
31.7.1) Implementare add per l'addizione
Il metodo speciale __add__() viene chiamato quando usi l'operatore +:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Somma due vettori"""
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
# L'operatore + chiama __add__()
v3 = v1 + v2
print(v3) # Output: Vector(4, 6)Quando Python vede v1 + v2, chiama v1.__add__(v2). Il metodo __add__() dell'operando sinistro riceve l'operando destro come argomento.
31.7.2) Implementare eq per l'uguaglianza
Il metodo speciale __eq__() viene chiamato quando usi l'operatore ==:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
"""Verifica se due vettori sono uguali"""
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(3, 4)
# L'operatore == chiama __eq__()
print(v1 == v2) # Output: True
print(v1 == v3) # Output: FalseSenza __eq__(), Python confronta l'identità degli oggetti (se sono lo stesso oggetto in memoria), non i loro valori. Con __eq__(), definiamo cosa significa l'uguaglianza per la nostra classe.
31.7.3) Implementare operatori di confronto
Implementiamo gli operatori di confronto per una classe Money:
class Money:
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
"""Verifica se gli importi sono uguali"""
return self.amount == other.amount
def __lt__(self, other):
"""Verifica se questo importo è minore dell'altro"""
return self.amount < other.amount
def __le__(self, other):
"""Verifica se questo importo è minore o uguale all'altro"""
return self.amount <= other.amount
def __gt__(self, other):
"""Verifica se questo importo è maggiore dell'altro"""
return self.amount > other.amount
def __ge__(self, other):
"""Verifica se questo importo è maggiore o uguale all'altro"""
return self.amount >= other.amount
def __str__(self):
return f"${self.amount:.2f}"
price1 = Money(10.50)
price2 = Money(15.75)
price3 = Money(10.50)
print(price1 == price3) # Output: True
print(price1 < price2) # Output: True
print(price1 <= price3) # Output: True
print(price2 > price1) # Output: True
print(price2 >= price1) # Output: True31.7.4) Gestire incompatibilità di tipo negli operatori
Quando implementi operatori, dovresti gestire i casi in cui l'altro operando non è del tipo previsto:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Somma due vettori oppure somma uno scalare a entrambe le componenti"""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
elif isinstance(other, (int, float)):
return Vector(self.x + other, self.y + other)
else:
return NotImplemented # Lascia che Python provi other.__radd__(self)
def __eq__(self, other):
if not isinstance(other, Vector):
return False
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1 + v2) # Output: Vector(4, 6) (vector addition)
print(v1 + 5) # Output: Vector(6, 7) (scalar addition)
print(v1 == v2) # Output: False
print(v1 == "not a vector") # Output: False (no error)Restituire NotImplemented (una speciale costante built-in) dice a Python di provare l'operazione riflessa sull'altro operando. Questo è importante per far funzionare correttamente gli operatori con tipi diversi.
31.8) Esempio 3: Accesso sequenziale (getitem, setitem)
I metodi speciali __getitem__() e __setitem__() ti permettono di usare la sintassi di indicizzazione (obj[key]) con le tue classi personalizzate. Questo fa sì che i tuoi oggetti si comportino come liste, dizionari o altre sequenze.
31.8.1) Implementare getitem per l'indicizzazione
Il metodo __getitem__() viene chiamato quando usi le parentesi quadre per accedere a un elemento:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
"""Ottieni una canzone per indice"""
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
# L'indicizzazione chiama __getitem__()
print(playlist[0]) # Output: Song A
print(playlist[1]) # Output: Song B
print(playlist[-1]) # Output: Song C (negative indexing works!)Poiché deleghiamo a self.songs[index], tutte le funzionalità di indicizzazione delle liste funzionano automaticamente: indici positivi, indici negativi e anche il sollevamento di IndexError per indici non validi.
31.8.2) Supportare lo slicing con getitem
Lo stesso metodo __getitem__() gestisce anche lo slicing:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
"""Ottieni una canzone per indice o slice"""
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
playlist.add_song("Song D")
# Anche lo slicing chiama __getitem__()
print(playlist[1:3]) # Output: ['Song B', 'Song C']
print(playlist[:2]) # Output: ['Song A', 'Song B']
print(playlist[::2]) # Output: ['Song A', 'Song C']Quando usi lo slicing, Python passa un oggetto slice a __getitem__(). Delegando a self.songs[index], supportiamo automaticamente tutta la sintassi delle slice.
31.8.3) Implementare setitem per l'assegnazione
Il metodo __setitem__() viene chiamato quando assegni a un indice:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __setitem__(self, index, value):
"""Sostituisce una canzone a un indice specifico"""
self.songs[index] = value
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
print(playlist[1]) # Output: Song B
# L'assegnazione chiama __setitem__()
playlist[1] = "New Song B"
print(playlist[1]) # Output: New Song B31.8.4) Rendere gli oggetti iterabili con getitem
Un interessante effetto collaterale: se implementi __getitem__() con indici interi a partire da 0, il tuo oggetto diventa automaticamente iterabile:
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __getitem__(self, index):
return self.songs[index]
def __len__(self):
return len(self.songs)
playlist = Playlist("My Favorites")
playlist.add_song("Song A")
playlist.add_song("Song B")
playlist.add_song("Song C")
# I cicli for funzionano automaticamente!
for song in playlist:
print(song)
# Output:
# Song A
# Song B
# Song CPython prova a iterare chiamando __getitem__(0), poi __getitem__(1), e così via finché non ottiene un IndexError. Questo è un vecchio protocollo di iterazione: impareremo il moderno protocollo degli iteratori nel Capitolo 35.
31.8.5) Accesso tipo dizionario con chiavi stringa
__getitem__() e __setitem__() funzionano con qualsiasi tipo di chiave, non solo interi:
class ScoreBoard:
def __init__(self):
self.scores = {}
def __getitem__(self, player_name):
"""Ottieni il punteggio di un giocatore"""
return self.scores.get(player_name, 0)
def __setitem__(self, player_name, score):
"""Imposta il punteggio di un giocatore"""
self.scores[player_name] = score
def __contains__(self, player_name):
return player_name in self.scores
def __len__(self):
return len(self.scores)
scoreboard = ScoreBoard()
# Imposta i punteggi usando chiavi stringa
scoreboard["Alice"] = 100
scoreboard["Bob"] = 85
# Aggiorna un punteggio
scoreboard["Alice"] = 120
# Ottieni i punteggi
print(scoreboard["Alice"]) # Output: 120
print(scoreboard["Bob"]) # Output: 85
print(scoreboard["Carol"]) # Output: 0
print("Alice" in scoreboard) # Output: True
print(len(scoreboard)) # Output: 2Questo capitolo ti ha mostrato come creare classi sofisticate che si integrano perfettamente con la sintassi di Python. Implementando variabili di classe, proprietà, metodi di classe, metodi statici e metodi speciali, puoi fare in modo che le tue classi personalizzate si comportino come tipi integrati. Nel Capitolo 32 esploreremo ereditarietà e polimorfismo, che ti permettono di costruire gerarchie di classi correlate che condividono ed estendono il comportamento.