Python: Diferencia horaria incorrecta (?) entre objetos `datetime` que abarcan un cambio de horario de verano

Resuelto Francois asked hace 7 meses • 0 respuestas

(editado para agregar)

PRÓLOGO: Hoy tomé conciencia del concepto de "tiempo en la pared". Siempre y para siempre lo consideraré perjudicial.


Tengo dos datetimes, uno que representa una determinada hora del día justo antes de un cambio de hora y el otro que representa la misma hora del día un día calendario después del primero datetime. Esperaría que la diferencia horaria entre estos dos objetos no fuera exactamente un día, pero eso es lo que veo (con python3.8, que es todo con lo que tengo para trabajar).

Tomando la diferencia de las marcas de tiempo asociadas con las datetimesdevoluciones exactamente lo que esperaría ver. Tomar la diferencia de datetimeobjetos cuando abarcan un cambio de tiempo me parece completamente incorrecto.

¿Es este el comportamiento esperado?

from datetime import datetime, timedelta
from dateutil.tz import gettz # pip install dateutil

def iso(dt):
    return dt.strftime('%FT%T%z')

# Daylight saving time begins at 2 a.m. local time on Sunday, March 10, 2024
us_central = gettz('US/Central')
before = datetime(2024, 3,  9, 15, 22, 1, tzinfo=us_central)
after  = datetime(2024, 3, 10, 15, 22, 1, tzinfo=us_central)

print()
print(f'before time change: {iso(before)}')
print(f' after time change: {iso(after)}')

naive = after - before
by_timestamps = timedelta(seconds = after.timestamp() - before.timestamp())
difference_difference = naive.total_seconds() - by_timestamps.total_seconds()

print()
print('Differences:')
print(f'        naive: {repr(naive)}')
print(f'by timestamps: {repr(by_timestamps)}')
print(f'        error: {difference_difference}s')

Salida (python3.8)

before time change: 2024-03-09T15:22:01-0600
 after time change: 2024-03-10T15:22:01-0500

Differences:
        naive: datetime.timedelta(days=1)
by timestamps: datetime.timedelta(seconds=82800)
        error: 3600.0s

(Editado para agregar)

Esto es muy contradictorio para mí. "La hora del muro" es muy extraño.

from datetime import datetime, timedelta
from dateutil.tz import gettz # pip install dateutil

# Daylight saving time begins at 2 a.m. local time on Sunday, March 10, 2024
us_central = gettz('US/Central')
before = datetime(2024, 3,  9, 15, 22, 1, tzinfo=us_central)
after  = datetime(2024, 3, 10, 15, 22, 1, tzinfo=us_central)

print(repr((before + timedelta(days=1)) - after))
print(repr((before + timedelta(seconds=86400)) - after))

# what I think the above should do...
print(repr(datetime.fromtimestamp(before.timestamp() + 86400, tz=before.tzinfo) - after))

Salida (python3.8)

datetime.timedelta(0)
datetime.timedelta(0)
datetime.timedelta(seconds=3600)
Francois avatar Feb 03 '24 05:02 Francois
Aceptado

Realice cálculos de diferencia horaria en UTC; de lo contrario, se utilizará el tiempo "mural". A continuación se corrige su código y hay un ejemplo de cómo avanzar en incrementos de 15 minutos durante el final del horario de verano (3 de noviembre de 2024, las 2 a. m. vuelven a la 1 a. m.). También utiliza el módulo integrado más nuevo zoneinfo(disponible desde Python 3.9) que utiliza la información de zona horaria del sistema o recurre al tzdatamódulo de terceros para obtener la información de zona horaria más reciente:

# May need "pip install -U tzdata" on some OSes for latest time zone info.
import datetime as dt
import zoneinfo as zi

# Daylight saving time begins at 2 a.m. local time on Sunday, March 10, 2024
us_central = zi.ZoneInfo('US/Central')

# Compute times as a local time zone but convert to UTC.
before = dt.datetime(2024, 3,  9, 15, 22, 1, tzinfo=us_central).astimezone(dt.timezone.utc)
after  = dt.datetime(2024, 3, 10, 15, 22, 1, tzinfo=us_central).astimezone(dt.timezone.utc)

# Display in preferred time zone
print()
print(f'before time change: {before.astimezone(us_central)}')
print(f' after time change: {after.astimezone(us_central)}')

naive = after - before
by_timestamps = dt.timedelta(seconds = after.timestamp() - before.timestamp())
difference_difference = naive.total_seconds() - by_timestamps.total_seconds()

print()
print('Differences:')
print(f'        naive: {repr(naive)}')
print(f'by timestamps: {repr(by_timestamps)}')
print(f'        error: {difference_difference}s')

current = dt.datetime(2024, 11, 3, 1, tzinfo=us_central).astimezone(dt.timezone.utc)
for minutes in range(12):
    print(current.astimezone(us_central))
    current += dt.timedelta(seconds=15 * 60)

Salida (con anotaciones):


before time change: 2024-03-09 15:22:01-06:00
 after time change: 2024-03-10 15:22:01-05:00

Differences:
        naive: datetime.timedelta(seconds=82800)
by timestamps: datetime.timedelta(seconds=82800)
        error: 0.0s
2024-11-03 01:00:00-05:00
2024-11-03 01:15:00-05:00
2024-11-03 01:30:00-05:00
2024-11-03 01:45:00-05:00
2024-11-03 01:00:00-06:00
2024-11-03 01:15:00-06:00
2024-11-03 01:30:00-06:00
2024-11-03 01:45:00-06:00
2024-11-03 02:00:00-06:00
2024-11-03 02:15:00-06:00
2024-11-03 02:30:00-06:00
2024-11-03 02:45:00-06:00

También tenga en cuenta que no se recomienda el uso de datetime.datetime.utcnow()y debido a que se devuelven marcas de tiempo ingenuas que usan la hora del sistema local (y, por lo tanto, la hora "mural") para matemáticas. También han quedado obsoletos en Python 3.12.datetime.datetime.utcfromtimestamp()

Mark Tolonen avatar Feb 02 '2024 23:02 Mark Tolonen

Del código fuente de cpython (el módulo alternativo puro de Python, aunque espero que los resultados sean equivalentes al código C):

class datetime:
    ...
    def __sub__(self, other):
        "Subtract two datetimes, or a datetime and a timedelta."
        if not isinstance(other, datetime):
            if isinstance(other, timedelta):
                return self + -other
            return NotImplemented

        days1 = self.toordinal()
        days2 = other.toordinal()
        secs1 = self._second + self._minute * 60 + self._hour * 3600
        secs2 = other._second + other._minute * 60 + other._hour * 3600
        base = timedelta(days1 - days2,
                         secs1 - secs2,
                         self._microsecond - other._microsecond)
        if self._tzinfo is other._tzinfo:
            return base
        myoff = self.utcoffset()
        otoff = other.utcoffset()
        if myoff == otoff:
            return base
        if myoff is None or otoff is None:
            raise TypeError("cannot mix naive and timezone-aware time")
        return base + otoff - myoff

Tenga en cuenta que no hay ningún código para tener en cuenta los cambios de horario de verano.

Por lo tanto, sugeriría localizar sus objetos de fecha y hora en UTC antes de realizar cualquier cálculo del delta de tiempo.

Y si su código necesita ser preciso al segundo, tenga en cuenta que Python también ignora los segundos intercalares, e incluso el tiempo de Unix (generado por .timestamp()) toma malas decisiones. Restar dos marcas de tiempo de Unix alrededor de un segundo intercalar dará como resultado una diferencia de uno, porque aunque las marcas de tiempo parecen independientes del calendario, fueron parcheadas para compatibilidad con códigos que asumían una cantidad fija de segundos por día.

BoppreH avatar Feb 02 '2024 22:02 BoppreH