Cómo hacer que una zona horaria de fecha y hora no sea consciente en Python

508

Lo que necesito hacer

Tengo un objeto datetime que no conoce la zona horaria, al que necesito agregar una zona horaria para poder compararlo con otros objetos datetime que reconocen la zona horaria. No quiero convertir toda mi aplicación a la zona horaria sin saberlo para este caso heredado.

Lo que he probado

Primero, para demostrar el 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

Primero, probé 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
>>>

No es terriblemente sorprendente que esto haya fallado, ya que en realidad está tratando de hacer una conversión. Reemplazar parecía una mejor opción (según Python: ¿Cómo obtener un valor de datetime.today () que sea "consciente de la zona horaria"? ):

>>> 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
>>> 

Pero como puede ver, reemplazar parece establecer el tzinfo, pero no hace que el objeto sea consciente. Me estoy preparando para volver a tratar la cadena de entrada para tener una zona horaria antes de analizarla (estoy usando dateutil para analizar, si eso importa), pero eso parece increíblemente torpe.

Además, he intentado esto en python 2.6 y python 2.7, con los mismos resultados.

Contexto

Estoy escribiendo un analizador para algunos archivos de datos. Hay un formato antiguo que necesito admitir donde la cadena de fecha no tiene un indicador de zona horaria. Ya arreglé la fuente de datos, pero aún necesito admitir el formato de datos heredado. Una conversión única de los datos heredados no es una opción por varias razones comerciales de BS. Si bien, en general, no me gusta la idea de codificar una zona horaria predeterminada, en este caso parece la mejor opción. Sé con bastante confianza que todos los datos heredados en cuestión están en UTC, por lo que estoy dispuesto a aceptar el riesgo de incumplimiento en ese caso.

Mark Tozzi
fuente
1
unaware.replace()volvería Nonesi estuviera modificando el unawareobjeto en el lugar. El REPL muestra que .replace()devuelve un nuevo datetimeobjeto aquí.
jfs
3
Lo que necesitaba cuando vine aquí:import datetime; datetime.datetime.now(datetime.timezone.utc)
Martin Thoma
1
@ MartinThoma Yo usaría el argumento nombrado tzpara ser más legible:datetime.datetime.now(tz=datetime.timezone.utc)
Acumenus

Respuestas:

595

En general, para que una zona horaria de fecha y hora ingenua sea consciente, use el método de localización :

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

Para la zona horaria UTC, no es realmente necesario usarlo localizeya que no hay un cálculo del horario de verano para manejar:

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

trabajos. ( .replacedevuelve una nueva fecha y hora; no se modifica unaware)

unutbu
fuente
10
Bueno, me siento tonto. Reemplazar devuelve una nueva fecha y hora. Dice eso también en los documentos, y me lo perdí por completo. Gracias, eso es exactamente lo que estaba buscando.
Mark Tozzi
2
"Reemplazar devuelve una nueva fecha y hora". Sí. La pista que le da REPL es que le muestra el valor devuelto. :)
Karl Knechtel - fuera de casa el
gracias, tuve uso de dt_aware para desinstalar dt_unware = datetime.datetime (* (dt_aware.timetuple () [: 6])),
Sérgio
44
si la zona horaria no es UTC, entonces no use el constructor directamente: aware = datetime(..., tz)use .localize()en su lugar.
jfs
1
Vale la pena mencionar que la hora local puede ser ambigua. tz.localize(..., is_dst=None)afirma que no lo es.
jfs
167

Todos estos ejemplos usan un módulo externo, pero puede lograr el mismo resultado usando solo el módulo de fecha y hora, como también se presenta en esta respuesta 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'

Menos dependencias y sin problemas de pytz.

NOTA: Si desea usar esto con python3 y python2, puede usar esto también para la importación de la zona horaria (codificada para 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()
kang
fuente
10
Muy buena respuesta para prevenir los pytzproblemas, ¡me alegro de haberme desplazado un poco hacia abajo! No quería abordar con pytzmis servidores remotos de hecho :)
Tregoreg
77
Tenga from datetime import timezoneen cuenta que funciona en py3 pero no en py2.7.
7yl4r
11
Debe tener en cuenta que dt.replace(tzinfo=timezone.utc)devuelve una nueva fecha y hora, no se modifica dten su lugar. (Editaré para mostrar esto).
Blairg23
2
¿Cómo podría, en lugar de usar timezone.utc, proporcionar una zona horaria diferente como una cadena (por ejemplo, "América / Chicago")?
bumpkin
2
@bumpkin mejor tarde que nunca, supongo:tz = pytz.timezone('America/Chicago')
Florian
83

Tuve uso de dt_aware a dt_unaware

dt_unaware = dt_aware.replace(tzinfo=None)

y dt_unware a dt_aware

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

pero responder antes también es una buena solución.

Sergio
fuente
2
podría usar localtz.localize(dt_unware, is_dst=None)para generar una excepción si dt_unwarerepresenta la hora local no existente o ambigua (nota: no hubo tal problema en la revisión anterior de su respuesta donde localtzestaba UTC porque UTC no tiene transiciones DST
jfs
@JF Sebastian, primer comentario aplicado
Sérgio
1
Le agradezco que muestre ambas direcciones de la conversión.
Christian Long
42

Utilizo esta declaración en Django para convertir un tiempo inconsciente en consciente:

from django.utils import timezone

dt_aware = timezone.make_aware(dt_unaware, timezone.get_current_timezone())
Googol
fuente
2
Me gusta esta solución (+1), pero depende de Django, que no es lo que estaban buscando (-1). =)
mkoistinen
3
En realidad no eres el segundo argumento. El argumento predeterminado (Ninguno) significará que la zona horaria local se usa implícitamente. Lo mismo con el horario de verano (que es el tercer argumento_
Oli
14

Estoy de acuerdo con las respuestas anteriores, y está bien si está de acuerdo en comenzar en UTC. Pero creo que también es un escenario común para las personas trabajar con un valor consciente de tz que tiene una fecha y hora que tiene una zona horaria local no UTC.

Si solo fuera por su nombre, uno probablemente inferiría que replace () será aplicable y producirá el objeto adecuado de fecha y hora. Este no es el caso.

el reemplazo (tzinfo = ...) parece ser aleatorio en su comportamiento . Por lo tanto, es inútil. ¡No uses esto!

localizar es la función correcta para usar. Ejemplo:

localdatetime_aware = tz.localize(datetime_nonaware)

O un ejemplo más completo:

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

me da un valor de fecha y hora consciente de la zona horaria de la hora local actual:

datetime.datetime(2017, 11, 3, 7, 44, 51, 908574, tzinfo=<DstTzInfo 'Australia/Melbourne' AEDT+11:00:00 DST>)
paolov
fuente
3
Esto necesita más votos a favor, intentar hacerlo replace(tzinfo=...)en una zona horaria que no sea UTC afectará su fecha y hora. Tengo en -07:53lugar de -08:00por ejemplo. Ver stackoverflow.com/a/13994611/1224827
Blairg23
¿Puedes dar un ejemplo reproducible de replace(tzinfo=...)tener un comportamiento inesperado?
xjcl
11

Use dateutil.tz.tzlocal()para obtener la zona horaria en su uso de datetime.datetime.now()y 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())

Tenga en cuenta que datetime.astimezoneprimero convertirá su datetimeobjeto a UTC y luego a la zona horaria, que es lo mismo que llamar datetime.replacecon la información de la zona horaria original None.

Ahmet
fuente
1
Si quieres hacerlo UTC:.replace(tzinfo=dateutil.tz.UTC)
Martin Thoma
2
Una importación menos y solo:.replace(tzinfo=datetime.timezone.utc)
kubanczyk
9

Esto codifica las respuestas de @ Sérgio y @ unutbu . "Funcionará" con un pytz.timezoneobjeto o una cadena de zona horaria de 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) 

Esto parece lo datetime.localize()(o .inform()o .awarify()) debe hacer, aceptar ambas cadenas y objetos de zona horaria para el argumento tz y por defecto a la hora UTC, si no se especifica ninguna zona horaria.

placas
fuente
1
Gracias, esto me ayudó a "marcar" un objeto de fecha y hora sin procesar como "UTC" sin que el sistema primero suponga que es la hora local y luego vuelva a calcular los valores.
Nikhil VJ
4

Python 3.9 agrega el zoneinfomódulo, por lo que ahora solo se necesita la biblioteca estándar.

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

Adjunte una zona horaria:

>>> 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'

Adjunte la zona horaria local 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'

Posteriormente se convierte correctamente a otras zonas horarias:

>>> 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'

Lista de Wikipedia de zonas horarias disponibles


Windows no tiene una base de datos de zona horaria del sistema, por lo que aquí se necesita un paquete adicional:

pip install tzdata  

Hay un backport para permitir su uso en Python 3.6 a 3.8 :

pip install backports.zoneinfo

Entonces:

from backports.zoneinfo import ZoneInfo
xjcl
fuente
1
en Windows, también necesitaspip install tzdata
MrFuppes
@ MrFuppes Gracias por el consejo! Probaré esto mañana y mi respuesta. ¿Sabes cuál es la situación en Mac?
xjcl
no, lo siento, no tengo Mac para probar. Supongo que es como en Linux.
MrFuppes hace
0

En el formato de la respuesta de unutbu; Creé un módulo de utilidad que maneja cosas como esta, con una sintaxis más intuitiva. Se puede instalar 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')
Las tortugas son lindas
fuente
0

para aquellos que solo quieren hacer una fecha y hora consciente de la zona horaria

import datetime
import pytz

datetime.datetime(2019, 12, 7, tzinfo=pytz.UTC)
Harry Moreno
fuente
0

bastante nuevo en Python y encontré el mismo problema. Encuentro esta solución bastante simple y para mí funciona bien (Python 3.6):

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

Cambiar entre zonas horarias

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
Shide
fuente