6.9.6 Oggetti di tipo tzinfo

La classe tzinfo è una classe di base astratta, vale a dire che questa classe non dovrebbe venire istanziata direttamente. Occorre invece derivarne una classe concreta e, come minimo, fornire un'implementazione dei metodi standard di tzinfo necessari per i metodi della classe datetime che si intende utilizzare. Il modulo datetime non fornisce nessuna classe derivata concreta di tzinfo.

Un'istanza di (una classe concreta di) tzinfo può venire passata al costruttore per ogetti di tipo datetime e time. Questi ultimi considerano il tempo espresso dai loro attributi come tempo locale, e l'oggetto tzinfo associato ad essi fornisce metodi per determinare la distanza tra il tempo locale e l'UTC, il nome del fuso orario, l'offset DST (NdT: la differenza di ora legale), tutti relativi ad un oggetto di tipo datetime o time passato come argomento.

Requisiti speciali per effettuare la serializzazione: una classe derivata di tzinfo deve avere un metodo __init__ che possa venire chiamato senza argomenti, altrimenti tale oggetto potrà comunque venire serializzato, ma potrebbe succedere di non poterlo più deserializzare. Questo è un requisito tecnico che potrebbe essere reso meno rigido in futuro.

Una classe derivata concreta di tzinfo può dover implementare i seguenti metodi. Quali metodi esattamente siano necessari dipende dall'uso che si è fatto degli oggetti datetime di tipo ``complesso''. Se avete dei dubbi in proposito, semplicemente implementateli tutti.

utcoffset( self, dt)
Restituisce la distanza del tempo locale dall'UTC, in minuti e andando verso est rispetto all'UTC. Se il tempo locale è ad ovest dell'UTC, il risultato dovrebbe essere negativo. Notate che con questo si intende rappresentare la distanza totale dall'UTC; per esempio, se un oggetto tzinfo rappresenta sia il fuso orario che la correzione dovuta al DST (ora legale), allora utcoffset() dovrebbe restituire la somma dei due. Se l'offset UTC non è noto, utcoffset() restituisce None. Altrimenti, il valore restituito deve essere un oggetto timedelta che specifichi il numero complessivo di minuti nell'intervallo da -1439 a 1439, limiti inclusi ( 1440 = 24*60; la grandezza della distanza deve essere inferiore ad un giorno). La maggior parte delle implementazioni di utcoffset() saranno probabilmente simili ad una di queste due:

    return CONSTANT                 #  classe con distanza fissa da UTC
    return CONSTANT + self.dst(dt)  #  classe consapevole delle
                                    #+ correzioni per l'ora legale

Se utcoffset() non restituisce None, allora non dovrebbe farlo neanche dst().

L'implementazione predefinita di utcoffset() solleva l'eccezione NotImplementedError.

dst( self, dt)
Restituisce la correzione per l'ora legale (NdT: DST=Daylight Saving Time), in minuti orientati ad est rispetto all'UTC, oppure restituisce None se quest'informazione non è nota. Restituisce timedelta(0) se l'ora legale non è applicata. Se l'ora è applicata, restituisce la correzione di tempo come un oggetto timedelta (vedete utcoffset() per i dettagli). Notate che la differenza per l'ora legale, se applicabile, è già stata aggiunta alla differenza di fuso orario restituita da utcoffset(), per cui non vi è necessità di chiamare dst(), a meno che non siate interessati ad ottenere l'informazione sull'ora legale separatamente. Per esempio, datetime.timetuple() chiama il metodo dst() dell'oggetto referenziato dal suo attributo tzinfo per stabilire come l'opzione tm_isdst dovrebbe venire impostata, e tzinfo.fromutc() chiama dst() per tener conto dei cambi di ora legale quado si attraversano i fusi orari.

Un'istanza tz di una classe derivata di tzinfo che modellizzi sia l'ora solare che quella legale deve essere consistente, nel senso che l'espressione:

tz.utcoffset(dt) - tz.dst(dt)

deve restituire lo stesso risultato per ogni oggetto dt di tipo datetime tale che dt.tzinfo == tz. Per classi derivate di tzinfo bene implementate, questa espressione corrisponde alla ``differenza standard'', che non dovrebbe dipendere dalla data o dal tempo, ma solo dalla posizione geografica. L'implementazione di datetime.astimezone() fa affidamento su questa assunzione, ma non è in grado di individuarne le violazioni; è responsabilità del programmatore fare in modo che non ve ne siano. Se una classe derivata di tzinfo non può garantire tale condizione, può alternativamente tentare di sostituire l'implementazione predefinita di tzinfo.fromutc() in modo da funzionare correttamente con astimezone(), indipendentemente dal fatto che la condizione sia vera.

La maggior parte delle iplementazioni di dst() probabilmente somiglieranno ad una di queste due:

    def dst(self):
        # una classe con distanza fissa: non tiene conto del DST
        return timedelta(0)

oppure

    def dst(self):
        # Codice per porre il valore di "dston" e "dstoff" ai tempi di
        # transizione dell'ora legale per il fuso orario considerato,
        # basandosi su dt.year ed esprimendo tali tempi in tempo locale.
        # Quindi: 

        if dston <= dt.replace(tzinfo=None) < dstoff:
            return timedelta(hours=1)
        else:
            return timedelta(0)

L'implementazione predefinita di dst() solleva l'eccezione NotImplementedError.

tzname( self, dt)
Restituisce una stringa corrispondente al nome della zona di fuso orario corrispondente all'argomento dt, di tipo datetime. Nel modulo datetime non viene definito niente riguardo a tali nomi, e non vi sono requisiti che abbiano un qualche significato specifico. Per esempio, "GMT", "UTC", "-500", "-5:00", "EDT", "US/Eastern", "America/New York" sono tutte risposte valide. Questo metodo deve restituire None se il nome di una stringa non è noto. Notate che la ragione principale per cui questo è un metodo piuttosto che una stringa di valore costante, consiste nel fatto che è possibile che una classe derivata di tzinfo voglia restituire nomi differenti per diversi valori dell'argomento, specialmente nel caso in cui la classe tzinfo tenga conto dell'ora legale.

L'implementazione predefinita di tzname() solleva l'eccezione NotImplementedError.

Questi metodi vengono chiamati da un oggetto di tipo datetime o time, in risposta a chiamate dei loro metodi con lo stesso nome. Un oggetto datetime passa se stesso come argomento mentre un oggetto time passa None come argomento. I metodi di una classe derivata di tzinfo devono dunque essere preparati ad accettare come argomento sia un'istanza di datetime, ossia dt, che None.

Quando None viene passato come argomento, il compito di decidere la risposta migliore viene lasciato al progettista della classe derivata di tzinfo. Per esempio, restituire None è appropriato se nell'implementazione della classe si vuole specificare che gli oggetti di tipo time non sono interessati alla gestione del fuso orario. Può però essere più utile che utcoffset(None) restituisca la distanza standard dall'UTC, visto che non vi è altro modo per stabilire la distanza standard.

Quando viene passato un oggetto datetime come risultato di una chiamata ad un metodo di datetime, allora dt.tzinfo sarà uguale a self. I metodi di tzinfo possono fare affidamento su questo, a meno che il codice utente non chiami direttamente i metodi della classe tzinfo. La ragione di ciò è di consentire che i metodi di tzinfo considerino l'argomento dt come rappresentante il tempo locale, senza preoccuparsi di oggetti rappresentanti un tempo espresso in fusi orari differenti.

Vi è un altro metodo della classe tzinfo che le classi derivate possono voler sostituire:

fromutc( self, dt)
Questo metodo viene chiamato nella implementazione predefinita di datetime.astimezone(). Usato in questo modo, dt.tzinfo corrisponde a self, e gli attributi di data e tempo di dt vanno considerati come rappresentanti un tempo con riferimento UTC. Lo scopo di fromutc() è quello di correggere gli attributi di data e tempo, resituendo un oggetto datetime equivalente ma rappresentante il tempo nel fuso orario di self.

La maggior parte delle classi derivate di tzinfo dovrebbero essere in grado di ereditare l'implementazione predefinita di fromutc() senza problemi. Tale implementazione è abbastanza robusta da gestire fusi orari con differenza oraria costante e fusi orari che tengano conto sia dell'ora solare (standard) che dell'ora legale, ed in quest'ultimo caso può persino gestire i casi in cui i tempi di passaggio all'ora legale cambino da un anno all'altro. Un esempio di una situazione che l'implementazione predefinita di fromutc() potrebbe non gestire sempre correttamente si ha quando la differenza oraria standard (rispetto all'UTC) dipende da una specifica data e tempo passato, situazione che si può verificare per motivi politici. L'implementazione predefinita di astimezone() e fromutc() può non produrre il risultato voluto, se il risultato è una delle ore successive al momento in cui la differenza oraria è cambiata.

Ignorando il codice necessario per gestire i casi di errore, l'implementazione di default di fromutc() funziona così:

  def fromutc(self, dt):
      # solleva l'eccezione ValueError se dt.tzinfo è diverso da self
      dtoff = dt.utcoffset()
      dtdst = dt.dst()
      # solleva ValueError se dtoff o dtdts sono uguali a None
      delta = dtoff - dtdst  #  questa è la differenza oraria
                             #+ standard di self 
      if delta:
          dt += delta   # converte nel tempo locale standard
          dtdst = dt.dst()
          # solleva l'eccezione ValueError se dtdst vale None
      if dtdst:
          return dt + dtdst
      else:
          return dt

Esempi di classi derivate di tzinfo:

from datetime import tzinfo, timedelta, datetime

ZERO = timedelta(0)
HOUR = timedelta(hours=1)

# Una classe UTC.

class UTC(tzinfo):
    """UTC"""

    def utcoffset(self, dt):
        return ZERO

    def tzname(self, dt):
        return "UTC"

    def dst(self, dt):
        return ZERO

utc = UTC()

# Una classe che crea oggetti tzinfo per fusi orari con distanza
# oraria fissa dall'UTC.  Notate che FixedOffset(0, "UTC") è un
# altro modo per creare un oggetto tzinfo rappresentante l'UTC.

class FixedOffset(tzinfo):
    """Distanza fissa in minuti ad est dell'UTC."""

    def __init__(self, offset, name):
        self.__offset = timedelta(minutes = offset)
        self.__name = name

    def utcoffset(self, dt):
        return self.__offset

    def tzname(self, dt):
        return self.__name

    def dst(self, dt):
        return ZERO

#  Una classe che cattura il concetto di tempo locale
#+ supportato dalla piattaforma

import time as _time

STDOFFSET = timedelta(seconds = -_time.timezone)
if _time.daylight:
    DSTOFFSET = timedelta(seconds = -_time.altzone)
else:
    DSTOFFSET = STDOFFSET

DSTDIFF = DSTOFFSET - STDOFFSET

class LocalTimezone(tzinfo):

    def utcoffset(self, dt):
        if self._isdst(dt):
            return DSTOFFSET
        else:
            return STDOFFSET

    def dst(self, dt):
        if self._isdst(dt):
            return DSTDIFF
        else:
            return ZERO

    def tzname(self, dt):
        return _time.tzname[self._isdst(dt)]

    def _isdst(self, dt):
        tt = (dt.year, dt.month, dt.day,
              dt.hour, dt.minute, dt.second,
              dt.weekday(), 0, -1)
        stamp = _time.mktime(tt)
        tt = _time.localtime(stamp)
        return tt.tm_isdst > 0

Local = LocalTimezone()

# Una comleta implementazioni delle regole correnti di DST (ora legale) 
# per i principali fusi orari degli Stati Uniti.

def first_sunday_on_or_after(dt):
    days_to_go = 6 - dt.weekday()
    if days_to_go:
        dt += timedelta(days_to_go)
    return dt

# Negli Stati Uniti, il DST comincia alle 2 AM (tempo standard) della 
# prima domenica di Aprile.
DSTSTART = datetime(1, 4, 1, 2)
# e termina alle 2 AM (tempo DST: 1 AM tempo standard) dell'ultima
# domenica di ottobre, cioè la prima domenica a partire dal 25 Ottobre, 
# incluso questo giorno.
DSTEND = datetime(1, 10, 25, 1)

class USTimeZone(tzinfo):

    def __init__(self, hours, reprname, stdname, dstname):
        self.stdoffset = timedelta(hours=hours)
        self.reprname = reprname
        self.stdname = stdname
        self.dstname = dstname

    def __repr__(self):
        return self.reprname

    def tzname(self, dt):
        if self.dst(dt):
            return self.dstname
        else:
            return self.stdname

    def utcoffset(self, dt):
        return self.stdoffset + self.dst(dt)

    def dst(self, dt):
        if dt is None or dt.tzinfo is None:
            # Un'eccezione può essere una buona idea qui, in uno od 
	    # entrambi i casi. Dipende da come li si vuole trattare. 
	    # L'implementazione di default di fromutc() ( chiamata dalla 
	    # implementazione di default di astimezone() ) passa un oggetto 
	    # datetime "dt" in cui dt.tzinfo è uguale a self.
            return ZERO
        assert dt.tzinfo is self

        # Trova la prima domenica di Aprile e l'ultima di Ottobre.
        start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
        end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))

        # Non è possibile confrontare oggetti consapevoli con altri 
	# semplicistici, per cui è meglio prima rimuovere da dt
	# l'informazione sul fuso orario.
        if start <= dt.replace(tzinfo=None) < end:
            return HOUR
        else:
            return ZERO

Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
Central  = USTimeZone(-6, "Central",  "CST", "CDT")
Mountain = USTimeZone(-7, "Mountain", "MST", "MDT")
Pacific  = USTimeZone(-8, "Pacific",  "PST", "PDT")

Notate le inevitabili sottigliezze necessarie per implementare una classe derivata di tzinfo che tenga conto sia del tempo standard che dell'ora legale, in corrispondenza dei punti di transizione dell'ora legale, due volte all'anno. Per concretezza, considerate il fuso orario Est degli Stati Uniti, dove l' EDT (NdT: East Daylight Time, ora legale per il fuso Est) comincia il minuto successivo alle 1:59 (EST) della prima domenica di Aprile e termina il minuto successivo alle 1:59 (EDT) dell'ultima domenica di Ottobre:

    UTC   3:MM  4:MM  5:MM  6:MM  7:MM  8:MM
    EST  22:MM 23:MM  0:MM  1:MM  2:MM  3:MM
    EDT  23:MM  0:MM  1:MM  2:MM  3:MM  4:MM

  start  22:MM 23:MM  0:MM  1:MM  3:MM  4:MM

    end  23:MM  0:MM  1:MM  1:MM  2:MM  3:MM

Quando l'ora legale inizia (la riga "start"), le lancette degli orologi locali saltano dalle 1:59 alle 3:00. Un tempo di orologio del tipo 2:MM non ha proprio senso in quel giorno, per cui astimezone(Eastern) non restituirà alcun risultato quando l'attributo hour vale 2 nel giorno di inizio del DST. Perché sia possibile che il metodo astimezone() rispetti questa garanzia, il metodo rzinfo.dst() deve considerare l'intervallo di tempo compreso nell'ora mancante (2:MM per la zona Est) come espresso in ora legale.

Quando l'ora legale termina (la riga "end"), vi è un problema potenzialmente peggiore: vi è infatti un'ora che non può venire indicata in modo non ambiguo con un tempo di orologio locale, vale a dire l'ultima ora del periodo di ora legale. Nella zona Est, questi sono i tempi del tipo 5:MM UTC del giorno in cui l'ora legale finisce. Le lancette degli orologi locali saltano all'indietro dalle 1:59 (ora legale) fino a tornare alle 1:00 (ora solare). Tempi locali espressi nella forma 1:MM sono ambigui. Il metodo astimezone() imita il comportamento del tempo di un orologio locale, facendo quindi corrispondere due ore UTC adiacenti alla stessa ora locale. Nell'esempio della zona Est, i tempi UTC del tipo 5:MM e 6:MM corrispondono entrambi a tempi del tipo 1:MM, una volta convertiti in ora locale. Allo scopo di consentire a astimezone() di garantire questo comportamento, il metodo tzinfo.dst() deve considerare i tempi nella "ora ripetuta" come espressi in ora solare standard. Questo viene facilmente ottenuto, come nell'esempio, rappresentando i tempi DST corrispondenti al passaggio all'ora legale e viceversa nel tempo solare standard del fuso orario interessato.

Applicazioni che non sono in grado di gestire questo tipo di ambiguità dovrebbero evitare di usare classi derivate ibride di tzinfo; non vi sono ambiguità quando si usa l'UTC, o ogni altra classe derivata di tzinfo con una differenza oraria fissa rispetto all'UTC (come ad esempio una classe rappresentante solo l'EST (differenza costante -5 ore) o solo l'EDT (differenza costante -4 ore)).

Vedete Circa questo documento... per informazioni su modifiche e suggerimenti.