Tipi personalizzati, per così dire

Introduzione

Spesso capita di dover gestire "tipi di dati" personalizzati. Questa esigenza può essere gestita facilmente mediante la definizione di proprie classi, da zero oppure basandosi su classi predefinite a disposizione. Qui di seguito presentiamo due semplici esempi, quello di un valore intero positivo e quello di una data.

Un classe per valori interi positivi

Immaginiamo, per le nostre immagini, di voler porre il vincolo che la larghezza e l'altezza siano valori positivi. Anziché reinventare la ruota, possiamo basarci sulla classe int e derivarne una classe personalizzata, che chiameremo PosInt, in questo modo:

class PosInt(int):
    """Una classe per la rappresentazione di valori interi positivi"""
    def __init__(self, v):
        try:
            self = int(v)
        except ValueError as err:
            return ValueError
        if self<=0:
            raise ValueError

In pratica, ridefiniamo il metodo __init__() in modo che prenda il valore del parametro e lo converta in un intero. Se non ci riesce, restituirà un errore di tipo ValueError. Ma lo stesso errore verrà restituito anche nel caso di valore minore o uguale a zero.

In una successiva lezione approfondiremo la questione della gestione degli errori e vedremo cosa significa l'istruzione raise.

Si noti che, essendo la classe Posint derivata dalla classe int, tutti i metodi della classe int sono disponibili anche per gli oggetti della classe PosInt, come si vede con l'output della funzione dir(), che elenca i metodi disponibili:

n=PosInt(12)
print(dir(n))

['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dict__',
[...snip...]
 'denominator', 'imag', 'numerator', 'real']

Sulle classi derivate torneremo più avanti.

Una classe per la gestione delle date

Immaginiamo di volere una classe per la gestione di una data, il cui costruttore abbia come parametro una stringa nella forma 'gg/mm/aaaa' (che rappresenta una data valida).

Ad esempio, vogliamo essere in grado di scrivere

d1 = Date('21/12/2010')

ma non deve invece funzionare

d2 = Date('34/12/2010')

Inoltre, sulla data memorizzata avremo bisogno di compiere delle operazioni (come minimo, estrarre l'anno, il mese e il giorno).

I possibili approcci sono:

  1. definire una classe propria, partendo da zero
  2. cercare nella documentazione se esiste una classe apposita tra quelle predefinite, che faccia al caso nostro
  3. definire una classe propria, derivandola da qualcosa di esistente

Il primo approccio sarebbe interessante, ma solo a scopo didattico (ottimo esercizio). Qui lo tralasciamo.

Con il secondo approccio verifichiamo velocemente che esiste già una classe, nel modulo datetime, che potrebbe essere utile. Il problema è che il costruttore non riceve una stringa, bensì una tupla con i valori dell'anno, del mese, ecc., e quindi non fa al caso nostro.

Dovremo quindi fare ricorso al terzo approccio, ovvero scrivere una nostra classe. Presento di seguito due soluzioni possibili.

Una classe che usa al suo interno un oggetto datetime (classe wrapper)

Con il codice seguente definiamo una classe che al suo interno ha l'attributo privato __date di tipo datetime. Le varie funzioni non sono altro che rinvii alle funzioni predefinite dell'oggetto datetime (metodi proxy). Per comprendere il funzionamento della classe, è necessario leggere la documentazione riguardante i metodi strptime() e il metodo strftime().

from datetime import *

class Date():
    def __init__(self, value):
        self.__date=datetime.strptime(value, "%d/%m/%Y")
    def getYear(self):
        return self.__date.year
    def getMonth(self):
        return self.__date.month
    def getDay(self):
        return self.__date.day
    def __str__(self):
        return self.__date.strftime("%d/%m/%Y")

print("Esempio con classe wrapper (1)")    
n=Date('20/10/2010')
print(n, type(n))
print(n.getYear())

Esempio con classe wrapper (1)
20/10/2010 <class '__main__.Date'>
2010

Si noti che il metodo __str__(), se definito, serve a fornire una raprresentazione testuale standard di un oggetto, senza doverla richiamare esplicitamente. Nell'esempio qui sopra, se n è una data, si può scrivere tranquillamente print(n).

Si può migliorare il codice di questa classe ridefinendo la funzione __getattr__(), in modo che venga sempre richiamato il corrispondente attributo della data presente in __date:

class Date():
    def __init__(self, value):
        self.__date=datetime.strptime(value, "%d/%m/%Y")
    def __getattr__(self, name):
        return getattr(self.__date, name)
    def __str__(self):
        return self.__date.strftime("%d/%m/%Y")

print("Esempio con classe wrapper (2)")    
n=Date('20/10/2010')
print(n, type(n))
print(n.year)

Esempio con classe wrapper (2)
20/10/2010 <class '__main__.Date'>
2010

Il codice diventa più snello e abbiamo acquistato la possibilità di accedere a tutte le proprietà della data.

Una classe che deriva dalla classe datetime

Un'altra possibilità è di definire una classe derivata dalla classe datetime, semplicemente ragionando sul costruttore (in pratica, se ci si accorge che il costruttore riceve un parametro unico di tipo stringa, si invoca la funzione della classe che trasforma la stringa in data). La comprensione di questo esempio richiede qualche conoscenza fino ad ora non acquisita (ad esempio l'unpacking di sequenze, che vedremo successivamente, o la ridefinizione del metodo __new__()). Però si può verificare facilmente che la classe si comporta come dovrebbe.

class Date(datetime):
    def __new__(cls, *args):
        if len(args) == 1 and isinstance(args[0], str):
            return cls.strptime(args[0], "%d/%m/%Y")
        return datetime.__new__(cls, *args)
    def __str__(self):
        return self.strftime("%d/%m/%Y")

print("Esempio con classe derivata")    
n=Date('12/11/2010')
print(n, type(n))
print(n.year)

n=Date(2010, 12, 31)
print(n, type(n))
print(n.year)

Output:

Esempio con classe derivata
12/11/2010 <class '__main__.Date'>
2010
31/12/2010 <class '__main__.Date'>
2010

Il vantaggio è che così possiamo comunque usare anche il costruttore tipico (basato su una tupla di numeri).

Esercizi

  1. Creare due classi, OddInt e EvenInt, che consentano solo valori rispettivamente dispari e pari.

  2. Creare una classe CenturyDate che consenta solo date di questo secolo.

results matching ""

    No results matching ""