Gestion du temps

Différents modules de la bibliothèque standard permettent de gérer les dates et le temps, des données qui ne sont pas toujours faciles à manipuler en raison de différentes conventions (durée des mois, années bissextiles, fuseaux horaires, heure d’été, secondes intercalaires, etc.).

Module time

Il existe plusieurs manières de représenter le temps en informatique, selon que l’on parle d’instants ou de dates.

Les timestamps

La plus simple d’entre toutes, c’est le timestamp qui représente un instant selon le nombre de secondes écoulées depuis une date de référence appelée epoch (généralement le 1er janvier 1970 à minuit UTC, norme Unix). On y accède via la fonction time du module time qui nous renvoie un nombre flottant (incluant donc les fractions de secondes).

>>> import time
>>> time.time()
1633091908.9014919
>>> time.time()
1633091911.8008878

Le timestamp peut aussi être un nombre négatif pour représenter les instants antérieurs à cet epoch.

Si une plus grande précision est nécessaire, la fonction time_ns permet de récupérer le timestamp sous la forme d’un nombre entier de nanosecondes écoulées.

>>> time.time_ns()
1633093266497388151

Étant un nombre, le timestamp est très facile à manipuler, et n’est pas soumis aux problématiques sur la gestion des fuseaux horaires. Il est donc utile pour des problématiques d’horodatage (noter à quel instant s’est produit un événement).
Cependant, on ne peut pas le considérer comme portable car son interprétation dépend de la date utilisée comme epoch. Il n’est ainsi pas recommandé de transmettre un timestamp à un autre programme car celui-ci pourrait l’interpréter différemment.

De plus, le timestamp est un nombre soumis à un stockage limité : auparavant sur 32 bits (aujourd’hui sur 64) il ne permettait alors de ne représenter qu’un intervalle restreint de dates.

Peut-être avez-vous entendu parler du bug de l’an 2038 ? Il s’agit de la date où les timestamps Unix atteindront leur capacité maximale sur 32 bits, rendant leur usage impossible après cette date.
Mais d’ici-là, tout le monde devrait être passé aux timestamps 64 bits.

Aussi, on pourrait être tenté d’utiliser des timestamps pour mesurer des durées dans un programme, en calculant la différence entre les timestamps. C’est une mauvaise pratique car ceux-ci ne sont pas monotones : étant alignés sur l’horloge du système, le temps peut « revenir en arrière » si l’horloge est recalibrée.

Pour un tel cas d’usage, il faut alors plutôt faire appel à la fonction monotonic (ou monotonic_ns) qui est une horloge monotonique. Le nombre ainsi renvoyé est aussi un nombre de secondes (ou de nanosecondes) mais la date de référence est indéterminée, ils ne sont alors utiles que pour calculer des durées.

>>> start = time.monotonic()
>>> ... # différentes opérations
>>> time.monotonic() - start
14.564803410001332
Structure de temps

Une autre manière de représenter le temps est de stocker des données liées à une date : année, mois, jour, heure, minutes, secondes, etc. C’est ce que fait l’objet struct_time du module time.

On peut obtenir un objet struct_time en appelant la fonction localtime par exemple.

>>> time.localtime()
time.struct_time(tm_year=2021, tm_mon=10, tm_mday=1, tm_hour=15, tm_min=12, tm_sec=52, tm_wday=4, tm_yday=274, tm_isdst=1)
>>> date = time.localtime()
>>> date.tm_year
2021
>>> date.tm_hour
15

Il s’agit donc d’une représentation du temps plus exploitable, dont on peut explorer les différentes composantes. Mais un tel objet est alors dépendant du fuseau horaire (le fuseau local pour localtime) et des autres conventions sur les dates.

La fonction gmtime permet de récupérer le struct_time correspondant au temps courant dans le fuseau horaire UTC.

>>> time.gmtime()
time.struct_time(tm_year=2021, tm_mon=10, tm_mday=1, tm_hour=13, tm_min=15, tm_sec=3, tm_wday=4, tm_yday=274, tm_isdst=0)
Utilitaires du module

Le module time met aussi à disposition quelques utilitaires.

Ainsi, il est possible de mettre le programme en pause pendant une certaine durée (en secondes) à l’aide de la fonction sleep.

>>> time.sleep(3)

On trouve aussi certaines fonctions pour faire des conversions entre les types précédents. Ainsi la fonction mktime permet de transformer un objet struct_time (dans le fuseau courant) en un timestamp.

>>> time.mktime(date)
1633093972.0

Aussi, localtime et gmtime peuvent prendre un timestamp en argument et renvoyer la date associée (respectivement dans le fuseau local ou en UTC).

>>> time.localtime(1633093972.0)
time.struct_time(tm_year=2021, tm_mon=10, tm_mday=1, tm_hour=15, tm_min=12, tm_sec=52, tm_wday=4, tm_yday=274, tm_isdst=1)
>>> time.gmtime(1633093972.0)
time.struct_time(tm_year=2021, tm_mon=10, tm_mday=1, tm_hour=13, tm_min=12, tm_sec=52, tm_wday=4, tm_yday=274, tm_isdst=0)

Enfin, on trouve d’autres fonctions de calcul du temps dans le module, comme process_time (et process_time_ns) qui sert à calculer le nombre de secondes de travail effectif (excluant les pauses) du programme, ainsi que perf_counter (et perf_counter_ns) spécialement dédiée aux calculs de performance du programme avec une résolution adaptée (les dates de référence de ces différentes fonctions sont indéterminées).

>>> start = time.process_time()
>>> time.sleep(3)
>>> time.process_time() - start
0.0006125660000000088
>>> start = time.perf_counter()
>>> time.sleep(3)
>>> time.perf_counter() - start
3.0024995610001497

Et n’hésitez pas à jeter un œil à la documentation du module time pour aller plus loin.

Module datetime

Le module datetime fournit une interface haut-niveau pour gérer les temps et les dates, construit autour du module time, avec le type datetime.

Un objet datetime représente une date précise (avec année, mois, jour, heure, minutes, secondes et microsecondes), avec ou sans fuseau horaire.
Une date avec fuseau horaire représente donc un instant précis, on dit qu’elle est avisée. Une date sans fuseau est dite naïve car son interprétation dépend du fuseau horaire courant.

>>> datetime.datetime(2000, 4, 12, 8, 30, 55)
datetime.datetime(2000, 4, 12, 8, 30, 55)

La méthode now du type datetime permet de récupérer l’objet associé à l’instant courant (exprimé dans le fuseau local). Par défaut, elle renvoie une date naïve.

>>> dt = datetime.datetime.now()
>>> dt
datetime.datetime(2021, 10, 1, 16, 19, 43, 840744)

Il est possible de préciser un fuseau horaire en argument pour obtenir une date avisée selon ce fuseau, par exemple en utilisant datetime.timezone.utc.

>>> dt = datetime.datetime.now(datetime.timezone.utc)
>>> dt_utc
datetime.datetime(2021, 10, 1, 14, 19, 43, 840744, tzinfo=datetime.timezone.utc)

On voit que le fuseau horaire est stocké dans l’attribut tzinfo de l’objet datetime.

Le format datetime ne permet que de représenter un ensemble limité de dates : seules les années 1 à 9999 sont autorisées.

Conversions

Les datetime peuvent être convertis vers d’autres types de dates à l’aide de méthodes spécifiques :

  • datetime.fromtimestamp permet de construire un objet datetime depuis un timestamp. Un fuseau optionnel peut être donné en argument.

    >>> datetime.datetime.fromtimestamp(1633093972)
    datetime.datetime(2021, 10, 1, 15, 12, 52)
    >>> datetime.datetime.fromtimestamp(1633093972, datetime.timezone.utc)
    datetime.datetime(2021, 10, 1, 13, 12, 52, tzinfo=datetime.timezone.utc)
    
  • La méthode timestamp permet l’opération inverse (que l’objet datetime soit naïf ou avisé).

    >>> dt.timestamp()
    1633097983.840744
    >>> dt_utc.timestamp()
    1633097983.840744
    
  • On peut aussi convertir des datetime vers des struct_time à l’aide de la méthode timetuple.

    >>> dt.timetuple()
    time.struct_time(tm_year=2021, tm_mon=10, tm_mday=1, tm_hour=16, tm_min=19, tm_sec=43, tm_wday=4, tm_yday=274, tm_isdst=-1)
    >>> dt_utc.timetuple()
    time.struct_time(tm_year=2021, tm_mon=10, tm_mday=1, tm_hour=14, tm_min=19, tm_sec=43, tm_wday=4, tm_yday=274, tm_isdst=-1)
    

Les conversions sont aussi possibles vers et depuis des chaînes de caractères, notamment en format ISO avec les méthodes isoformat et fromisoformat.

>>> dt.isoformat()
'2021-10-01T16:19:43.840744'
>>> dt_utc.isoformat()
'2021-10-01T14:19:43.840744+00:00'
>>> datetime.datetime.fromisoformat('2021-10-01T16:19:43.840744')
datetime.datetime(2021, 10, 1, 16, 19, 43, 840744)
>>> datetime.datetime.fromisoformat('2021-10-01T14:19:43.840744+00:00')
datetime.datetime(2021, 10, 1, 14, 19, 43, 840744, tzinfo=datetime.timezone.utc)

Mais d’autres conversions en chaînes sont possibles, avec strftime par exemple. Cette méthode accepte une chaîne pour représenter le format de sortie, où différents codes de formatage sont disponibles comme :

  • %a et %A pour le nom du jour de la semaine (forme abrégée ou forme longue)
  • %d pour le numéro de jour dans le mois
  • %b et %B pour le nom du mois (forme abrégée ou forme longue)
  • %m pour le numéro du mois
  • %y et %Y pour l’année (sur 2 ou 4 chiffres)
  • %H, %M et %S respectivement pour les heures, minutes et secondes
  • %z et %Z pour le fuseau horaire (en tant que décalage ou par son nom)
>>> dt.strftime('Le %A %d %B %Y à %Hh%M')
'Le vendredi 01 octobre 2021 à 16h19'
>>> dt_utc.strftime('Le %A %d %B %Y à %Hh%M (%Z)')
'Le vendredi 01 octobre 2021 à 14h19 (UTC)'

Il se peut que vous obteniez des noms anglais pour les jours et mois, cela est dû à la locale définie pour les conversions. Vous pouvez définir une locale française à l’aide des lignes suivantes :

import locale
locale.setlocale(locale.LC_ALL, 'fr_FR')

(fr_BE pour la Belgique et fr_CA pour le Canada sont aussi disponibles)

On notera que ces options de formatage sont aussi disponibles au sein des fstrings pour représenter des objets datetime.

>>> f'{dt:%d/%m/%Y %H:%M}'
'01/10/2021 16:19'
>>> f'{dt_utc:%d/%m/%Y %H:%M%z}'
'01/10/2021 14:19+0000'

L’opération inverse est elle aussi possible (mais plus compliquée) avec la méthode strptime : on spécifie la chaîne représentant la date et le format attendu en arguments, la méthode nous renvoie alors l’objet datetime correspondant.

>>> datetime.datetime.strptime('01/10/2021 14:19+0000', '%d/%m/%Y %H:%M%z')
datetime.datetime(2021, 10, 1, 14, 19, tzinfo=datetime.timezone.utc)
Durées

Il est possible de soustraire des objets datetime pour obtenir une durée, qui représente le nombre de jours et secondes qui séparent les deux dates.

>>> dt - datetime.datetime(2000, 4, 12, 8, 30, 55)
datetime.timedelta(days=7842, seconds=28128, microseconds=840744)

Ces durées se matérialisent par le type timedelta. Elles peuvent s’additionner et se soustraire entre-elles. Il est aussi possible de les multiplier par des nombres.

>>> datetime.timedelta(days=1) + datetime.timedelta(days=1)
datetime.timedelta(days=2)
>>> datetime.timedelta(days=1) - datetime.timedelta(days=1)
datetime.timedelta(0)
>>> datetime.timedelta(days=1) * 10
datetime.timedelta(days=10)

Et bien sûr, on peut additionner une durée à un datetime (naïf ou avisé) pour obtenir un nouveau datetime.

>>> dt + datetime.timedelta(days=1)
datetime.datetime(2021, 10, 2, 16, 19, 43, 840744)
>>> dt_utc + datetime.timedelta(days=1)
datetime.datetime(2021, 10, 2, 14, 19, 43, 840744, tzinfo=datetime.timezone.utc)
Fuseaux horaires

On l’a vu : les objets datetime peuvent contenir ou non des informations de fuseau horaire, selon l’usage que l’on veut en faire, et les deux types sont généralement gérés par les différentes fonctions.
Il est cependant à noter qu’on ne peux pas mélanger dates naïves et avisées au sein des mêmes opérations.

>>> dt_utc - dt
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't subtract offset-naive and offset-aware datetimes

Le module datetime ne fournit par défaut que le fuseau horaire UTC (Temps Universel Coordonné) avec datetime.timezone.utc.

Mais le type timezone permet de construire des fuseaux à décalage fixe par rapport à UTC, en prenant un timedelta en argument.

>>> tz = datetime.timezone(datetime.timedelta(seconds=3600))
>>> datetime.datetime.now(tz)
datetime.datetime(2021, 10, 1, 15, 19, 43, 840744, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)))

On notera aussi que la méthode astimezone permet de convertir une date vers un autre fuseau horaire. Les dates naïves sont considérées comme appartenant au fuseau local.

>>> dt.astimezone(tz)
datetime.datetime(2021, 10, 1, 15, 19, 43, 840744, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)))
>>> dt.astimezone(datetime.timezone.utc)
datetime.datetime(2021, 10, 1, 14, 19, 43, 840744, tzinfo=datetime.timezone.utc)
>>> dt_utc.astimezone(tz)
datetime.datetime(2021, 10, 1, 15, 19, 43, 840744, tzinfo=datetime.timezone(datetime.timedelta(seconds=3600)))

astimezone opère une conversion sur la date pour correspondre au fuseau horaire choisi. Pour simplement ajouter un fuseau horaire à une date sans faire de conversion, vous pouvez utiliser la méthode replace avec l’argument nommé tzinfo.

>>> dt.replace(tzinfo=datetime.timezone.utc)
datetime.datetime(2021, 10, 1, 16, 19, 43, 840744, tzinfo=datetime.timezone.utc)

Depuis Python 3.9, le module zoneinfo apporte une collection de fuseaux horaires pour traiter les fuseaux courants.

>>> from zoneinfo import ZoneInfo
>>> tz = ZoneInfo('Europe/Paris')
>>> dt.astimezone(tz)
datetime.datetime(2021, 10, 1, 16, 19, 43, 840744, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))

Pour plus d’informations sur ces modules, vous pouvez consulter les documentations de datetime et zoneinfo.

Module calendar

Le module calendar est un module qui sert principalement à afficher de simples calendriers dans le terminal.

>>> import calendar
>>> calendar.prmonth(2021, 10)
    octobre 2021
lu ma me je ve sa di
             1  2  3
 4  5  6  7  8  9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31

Le module contient ainsi des fonctions month et calendar. La première prend une année et un mois en arguments et renvoie la représentation de ce mois. La seconde prend une année et renvoie la représentation de tous les mois de cette année.
Les tailles des lignes et des colonnes sont configurables à l’aide des différents paramètres de ces fonctions.

Les fonctions prmonth et prcal sont des raccourcis pour directement afficher ces représentations sur le terminal.

Le module apporte aussi différents attributs pour connaître les noms de jours et de mois :

  • day_name est le tableau des noms de jours de la semaine
  • day_abbr est celui des noms de jours abrégés
  • month_name est le tableau des noms de mois
  • month_abbr est celui des noms de mois abrégés
>>> calendar.day_name[0]
'lundi'
>>> calendar.day_abbr[1]
'mar.'
>>> calendar.month_name[3]
'mars'
>>> calendar.month_abbr[7]
'juil.'

Enfin, on trouve aussi dans ce module une fonction timegm qui permet de convertir un objet struct_time en timestamp.

>>> calendar.timegm(time.gmtime())
1633102464

D’autres fonctions sont encore disponibles dans le module, je vous laisse les découvrir sur la page de documentation.


La gestion du temps et des dates n’est pas une chose aisée, je vous invite d’ailleurs à consulter ce tutoriel de @SpaceFox pour en apprendre plus sur les subtilités.