Come rendere consapevole un fuso orario datetime inconsapevole in Python


508

Cosa devo fare

Ho un oggetto datetime ignaro del fuso orario, al quale devo aggiungere un fuso orario per poterlo confrontare con altri oggetti datetime sensibili al fuso orario. Non voglio convertire la mia intera applicazione in fuso orario ignaro di questo caso precedente.

Quello che ho provato

Innanzitutto, per dimostrare il problema:

Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> import pytz
>>> unaware = datetime.datetime(2011,8,15,8,15,12,0)
>>> unaware
datetime.datetime(2011, 8, 15, 8, 15, 12)
>>> aware = datetime.datetime(2011,8,15,8,15,12,0,pytz.UTC)
>>> aware
datetime.datetime(2011, 8, 15, 8, 15, 12, tzinfo=<UTC>)
>>> aware == unaware
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes

Innanzitutto, ho provato astimezone:

>>> unaware.astimezone(pytz.UTC)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: astimezone() cannot be applied to a naive datetime
>>>

Non è terribilmente sorprendente che questo sia fallito, dal momento che sta effettivamente cercando di fare una conversione. Sostituire sembrava una scelta migliore (secondo Python: come ottenere un valore di datetime.today () che è "timezone aware"? ):

>>> unaware.replace(tzinfo=pytz.UTC)
datetime.datetime(2011, 8, 15, 8, 15, 12, tzinfo=<UTC>)
>>> unaware == aware
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes
>>> 

Ma come puoi vedere, sostituire sembra impostare tzinfo, ma non rendere consapevole l'oggetto. Mi sto preparando a ricorrere al dottore della stringa di input per avere un fuso orario prima di analizzarlo (sto usando dateutil per l'analisi, se questo è importante), ma sembra incredibilmente kludgy.

Inoltre, ho provato questo in entrambi Python 2.6 e Python 2.7, con gli stessi risultati.

Contesto

Sto scrivendo un parser per alcuni file di dati. C'è un vecchio formato che devo supportare in cui la stringa della data non ha un indicatore del fuso orario. Ho già riparato l'origine dati, ma devo ancora supportare il formato dati legacy. Una conversione una tantum dei dati legacy non è un'opzione per vari motivi di business BS. Mentre in generale, non mi piace l'idea di codificare in modo rigido un fuso orario predefinito, in questo caso sembra l'opzione migliore. So con ragionevole certezza che tutti i dati legacy in questione sono in UTC, quindi sono pronto ad accettare il rischio di inadempienza in questo caso.


1
unaware.replace()ritornerebbe Nonese stesse modificando l' unawareoggetto sul posto. Il REPL mostra che .replace()restituisce datetimequi un nuovo oggetto.
jfs,

3
Cosa mi serviva quando venni qui:import datetime; datetime.datetime.now(datetime.timezone.utc)
Martin Thoma il

1
@MartinThoma Vorrei usare l' tzarg chiamato per essere più leggibile:datetime.datetime.now(tz=datetime.timezone.utc)
Acumenus

Risposte:


595

In generale, per rendere un datetime ingenuo consapevole del fuso orario, utilizzare il metodo localize :

import datetime
import pytz

unaware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0)
aware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0, pytz.UTC)

now_aware = pytz.utc.localize(unaware)
assert aware == now_aware

Per il fuso orario UTC, non è realmente necessario utilizzare localizepoiché non esiste un calcolo dell'ora legale per gestire:

now_aware = unaware.replace(tzinfo=pytz.UTC)

lavori. ( .replacerestituisce un nuovo datetime; non modifica unaware.)


10
Bene, mi sento sciocco. Sostituisci restituisce un nuovo datetime. Lo dice anche lì nei documenti, e mi è mancato completamente. Grazie, è esattamente quello che stavo cercando.
Mark Tozzi,

2
"Sostituisci restituisce un nuovo datetime." Sì. Il suggerimento che ti dà il REPL è che ti sta mostrando il valore restituito. :)
Karl Knechtel - lontano da casa il

grazie, ho usato da dt_aware per ignorare dt_unware = datetime.datetime (* (dt_aware.timetuple () [: 6])),
Sérgio

4
se il fuso orario non è UTC, non utilizzare direttamente il costruttore:, aware = datetime(..., tz)utilizzare .localize()invece.
jfs

1
Vale la pena ricordare che l'ora locale può essere ambigua. tz.localize(..., is_dst=None)afferma che non lo è.
jfs

167

Tutti questi esempi utilizzano un modulo esterno, ma è possibile ottenere lo stesso risultato utilizzando solo il modulo datetime, come presentato anche in questa risposta SO :

from datetime import datetime
from datetime import timezone

dt = datetime.now()
dt.replace(tzinfo=timezone.utc)

print(dt.replace(tzinfo=timezone.utc).isoformat())
'2017-01-12T22:11:31+00:00'

Meno dipendenze e nessun problema pytz.

NOTA: se si desidera utilizzarlo con python3 e python2, è possibile utilizzare anche questo per l'importazione del fuso orario (codificato per UTC):

try:
    from datetime import timezone
    utc = timezone.utc
except ImportError:
    #Hi there python2 user
    class UTC(tzinfo):
        def utcoffset(self, dt):
            return timedelta(0)
        def tzname(self, dt):
            return "UTC"
        def dst(self, dt):
            return timedelta(0)
    utc = UTC()

10
Ottima risposta per prevenire i pytzproblemi, sono contento di averlo sceso un po '! Non volevo pytzdavvero affrontare i miei server remoti :)
Tregoreg,

7
Nota che from datetime import timezonefunziona in py3 ma non in py2.7.
7yl4r,

11
Si noti che dt.replace(tzinfo=timezone.utc)restituisce un nuovo datetime, non viene modificato dtin posizione. (Modificherò per mostrare questo).
Blairg23

2
Come potresti, invece di utilizzare timezone.utc, fornire un fuso orario diverso come stringa (ad es. "America / Chicago")?
Bumpkin,

2
@bumpkin meglio tardi che mai, immagino:tz = pytz.timezone('America/Chicago')
Florian,

83

Ho usato da dt_aware a dt_unaware

dt_unaware = dt_aware.replace(tzinfo=None)

e dt_unware a dt_aware

from pytz import timezone
localtz = timezone('Europe/Lisbon')
dt_aware = localtz.localize(dt_unware)

ma rispondere prima è anche una buona soluzione.


2
potresti utilizzare localtz.localize(dt_unware, is_dst=None)per sollevare un'eccezione se dt_unwarerappresenta l'ora locale inesistente o ambigua (nota: non vi erano problemi di questo tipo nella precedente revisione della tua risposta in cui localtzera UTC perché UTC non ha transizioni DST
jfs

@JF Sebastian, primo commento applicato
Sérgio,

1
Ti apprezzo che mostri entrambe le direzioni della conversione.
Christian Long,

42

Uso questa affermazione in Django per convertire un tempo inconsapevole in un consapevole:

from django.utils import timezone

dt_aware = timezone.make_aware(dt_unaware, timezone.get_current_timezone())

2
Mi piace questa soluzione (+1), ma dipende da Django, che non è quello che stavano cercando (-1). =)
mkoistinen,

3
In realtà non è il secondo argomento. L' argomento predefinito (Nessuno) indica che il fuso orario locale viene utilizzato implicitamente. Lo stesso con l'ora legale (che è il terzo argomento_
Oli

14

Sono d'accordo con le risposte precedenti, e va bene se sei a posto per iniziare in UTC. Ma penso che sia anche uno scenario comune per le persone che lavorano con un valore tz consapevole che ha un datetime che ha un fuso orario locale non UTC.

Se dovessi semplicemente andare per nome, uno probabilmente dedurrebbe sostituire () sarà applicabile e produrrebbe l'oggetto giusto per il datetime. Questo non è il caso.

il sostituto (tzinfo = ...) sembra essere casuale nel suo comportamento . È quindi inutile. Non usare questo!

localize è la funzione corretta da usare. Esempio:

localdatetime_aware = tz.localize(datetime_nonaware)

O un esempio più completo:

import pytz
from datetime import datetime
pytz.timezone('Australia/Melbourne').localize(datetime.now())

mi dà un valore datetime consapevole del fuso orario dell'ora locale corrente:

datetime.datetime(2017, 11, 3, 7, 44, 51, 908574, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)

3
Ciò ha bisogno di più voti, provando a fare replace(tzinfo=...)un fuso orario diverso da UTC rovinerà il tuo datetime. Ho preso -07:53invece -08:00per esempio. Vedere stackoverflow.com/a/13994611/1224827
Blairg23

Puoi dare un esempio riproducibile di replace(tzinfo=...)comportamenti inaspettati?
xjcl

11

Utilizzare dateutil.tz.tzlocal()per ottenere il fuso orario nell'uso di datetime.datetime.now()e datetime.datetime.astimezone():

from datetime import datetime
from dateutil import tz

unlocalisedDatetime = datetime.now()

localisedDatetime1 = datetime.now(tz = tz.tzlocal())
localisedDatetime2 = datetime(2017, 6, 24, 12, 24, 36, tz.tzlocal())
localisedDatetime3 = unlocalisedDatetime.astimezone(tz = tz.tzlocal())
localisedDatetime4 = unlocalisedDatetime.replace(tzinfo = tz.tzlocal())

Nota che datetime.astimezoneprima convertirai il tuo datetimeoggetto in UTC e poi nel fuso orario, che è lo stesso che chiamare datetime.replacecon le informazioni sul fuso orario originale None.


1
Se vuoi farlo UTC:.replace(tzinfo=dateutil.tz.UTC)
Martin Thoma il

2
.replace(tzinfo=datetime.timezone.utc)
Un'importazione in

9

Questo codifica le risposte di @Sérgio e @ unutbu . Funzionerà "semplicemente" con un pytz.timezoneoggetto o una stringa Fuso orario IANA .

def make_tz_aware(dt, tz='UTC', is_dst=None):
    """Add timezone information to a datetime object, only if it is naive."""
    tz = dt.tzinfo or tz
    try:
        tz = pytz.timezone(tz)
    except AttributeError:
        pass
    return tz.localize(dt, is_dst=is_dst) 

Questo sembra che cosa datetime.localize()(o .inform()o .awarify()) dovrebbe fare, accettare sia le stringhe che gli oggetti del fuso orario per l'argomento tz e di default UTC se non viene specificato alcun fuso orario.


1
Grazie, questo mi ha aiutato a "marchiare" un oggetto datetime non elaborato come "UTC" senza che il sistema assumesse prima che fosse l'ora locale e quindi ricalcolasse i valori!
Nikhil VJ,

4

Python 3.9 aggiunge il zoneinfo modulo, quindi ora è necessaria solo la libreria standard!

from zoneinfo import ZoneInfo
from datetime import datetime
unaware = datetime(2020, 10, 31, 12)

Allegare un fuso orario:

>>> unaware.replace(tzinfo=ZoneInfo('Asia/Tokyo'))
datetime.datetime(2020, 10, 31, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Asia/Tokyo'))
>>> str(_)
'2020-10-31 12:00:00+09:00'

Collega il fuso orario locale del sistema:

>>> unaware.replace(tzinfo=ZoneInfo('localtime'))
datetime.datetime(2020, 10, 31, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='localtime'))
>>> str(_)
'2020-10-31 12:00:00+01:00'

Successivamente viene correttamente convertito in altri fusi orari:

>>> unaware.replace(tzinfo=ZoneInfo('localtime')).astimezone(ZoneInfo('Asia/Tokyo'))
datetime.datetime(2020, 10, 31, 20, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='Asia/Tokyo'))
>>> str(_)
'2020-10-31 20:00:00+09:00'

Elenco di Wikipedia dei fusi orari disponibili


Windows non ha un database di fuso orario di sistema, quindi qui è necessario un pacchetto aggiuntivo:

pip install tzdata  

Esiste un backport per consentire l'uso in Python da 3.6 a 3.8 :

pip install backports.zoneinfo

Poi:

from backports.zoneinfo import ZoneInfo

1
su Windows, è necessario anchepip install tzdata
MrFuppes

@MrFuppes Grazie per il suggerimento! Lo proverò domani e alla mia risposta. Sai qual è la situazione sui Mac?
xjcl

no, scusa, non ho Mac in giro per provare. La mia ipotesi sarebbe che è come su Linux.
MrFuppes

0

Nel formato della risposta di Unutbu; Ho creato un modulo di utilità che gestisce cose come questa, con una sintassi più intuitiva. Può essere installato con pip.

import datetime
import saturn

unaware = datetime.datetime(2011, 8, 15, 8, 15, 12, 0)
now_aware = saturn.fix_naive(unaware)

now_aware_madrid = saturn.fix_naive(unaware, 'Europe/Madrid')

0

per quelli che vogliono solo rendere un datetime consapevole del fuso orario

import datetime
import pytz

datetime.datetime(2019, 12, 7, tzinfo=pytz.UTC)

0

abbastanza nuovo per Python e ho riscontrato lo stesso problema. Trovo questa soluzione abbastanza semplice e per me funziona benissimo (Python 3.6):

unaware=parser.parse("2020-05-01 0:00:00")
aware=unaware.replace(tzinfo=tz.tzlocal()).astimezone(tz.tzlocal())

0

Cambio tra fusi orari

import pytz
from datetime import datetime

other_tz = pytz.timezone('Europe/Madrid')

# From random aware datetime...
aware_datetime = datetime.utcnow().astimezone(other_tz)
>> 2020-05-21 08:28:26.984948+02:00

# 1. Change aware datetime to UTC and remove tzinfo to obtain an unaware datetime
unaware_datetime = aware_datetime.astimezone(pytz.UTC).replace(tzinfo=None)
>> 2020-05-21 06:28:26.984948

# 2. Set tzinfo to UTC directly on an unaware datetime to obtain an utc aware datetime
aware_datetime_utc = unaware_datetime.replace(tzinfo=pytz.UTC)
>> 2020-05-21 06:28:26.984948+00:00

# 3. Convert the aware utc datetime into another timezone
reconverted_aware_datetime = aware_datetime_utc.astimezone(other_tz)
>> 2020-05-21 08:28:26.984948+02:00

# Initial Aware Datetime and Reconverted Aware Datetime are equal
print(aware_datetime1 == aware_datetime2)
>> True
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.