01 декабря 2019

Назад в будущее: практическое руководство по путешествию во времени с Python

Пишу, перевожу и иллюстрирую IT-статьи. На proglib написал 140 материалов. Увлекаюсь Python, вебом и Data Science. Открыт к диалогу – ссылки на соцсети и мессенджеры: https://matyushkin.github.io/links/ Если понравился стиль изложения, упорядоченный список публикаций — https://github.com/matyushkin/lessons
В Python есть несколько встроенных библиотек для работы со временем и интервалами времени: time, datetime, calendar, timeit. Но когда какой модуль использовать? Рассмотрим на примерах.
Назад в будущее: практическое руководство по путешествию во времени с Python

Как в Python вывести строку, содержащую текущие дату и время? Как вычислить время, прошедшее между двумя днями? Как измерить длительность выполнения фрагмента кода? Как сгенерировать календарь на весь год или на один месяц? Как определить даты всех третьих четвергов одного года? На эти и другие вопросы вы получите ответы в данном руководстве.

Чтобы не обращаться далее к операции импорта, перечислим сразу все встроенные модули, рассматриваемые в этом руководстве:

        >>> import time
>>> import timeit
>>> import datetime
>>> import calendar
    

1. Работа со шкалой времени: модуль time

1.1. Понятие epoch

Работа с модулем time в существенной мере зависит от используемой операционной системы. Время в библиотеке привязано к фиксированной начальной точке – эпохе (epoch). Узнаем эту начальную точку:

        >>> time.gmtime(0)
time.struct_time(tm_year=1970, tm_mon=1, tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=1, tm_isdst=0)
    

В Unix-системах точкой отсчета (epoch) является 1 января 1970 г. Функция gmtime() вернула объект именованного кортежа struct_time.

С помощью функции time() время, прошедшее с этой начальной точки, можно также вывести в секундах (seconds since the epoch):

        >>> time.time()
1575098265.8943102
    

Так как точка epoch для разных операционных систем может отличаться, число секунд, возвращаемое функцией time(), также может быть различным.

Время до точки epoch тоже существует, но значения секунд seconds since the epoch отрицательны. Если мы передадим функции gmtime отрицательное значение секунд, мы перенесемся в прошлое относительно момента времени epoch:

        >>> time.gmtime(-10**8)
time.struct_time(tm_year=1966, tm_mon=10, tm_mday=31, tm_hour=14, tm_min=13, tm_sec=20, tm_wday=0, tm_yday=304, tm_isdst=0)
    

1.2. Секунды, struct_time и преобразование друг в друга

Назад в будущее: практическое руководство по путешествию во времени с Python

Итак, модуль time оперирует двумя основными типами объектов: struct_time и секундами с начала эпохи. Для взаимных преобразований используются следующие функции:

  1. gmtime(): из секунд в struct_time для UTC.
  2. localtime(): из секунд в struct_time для местного времени.
  3. calendar.timegm() (не модуль time): из struct_time для UTC в секунды.
  4. mktime(): из struct_time местного времени в секунды.

UTC – стандартное обозначение всемирного координированного времени без учета часовых поясов. Начало отсчета epoch привязано к UTC, то есть не зависит от местного времени. Поэтому UTC удобнее использовать для общения с внешним миром.

        >>> time.gmtime(1574869797)
time.struct_time(tm_year=2019, tm_mon=11, tm_mday=27, tm_hour=15, tm_min=49, tm_sec=57, tm_wday=2, tm_yday=331, tm_isdst=0)
    
        >>> time.localtime(1574869797)  # заметьте отличие в выводе значения tm_hour
time.struct_time(tm_year=2019, tm_mon=11, tm_mday=27, tm_hour=18, tm_min=49, tm_sec=57, tm_wday=2, tm_yday=331, tm_isdst=0)
    

В отсутствии аргумента функции gmtime() и localtime() возвращают значение для текущего времени – соответственно UTC и местное время.

Для преобразования объекта struct_time в секунды можно или передать сам объект, или кортеж целых чисел. Порядок элементов в кортеже:

  1. Год tm_year
  2. Месяц tm_mon – целое число (1 – Январь, 12 – Декабрь)
  3. День месяца tm_day
  4. Час tm_hour – целое число в диапазоне от 0 до 23
  5. Минута tm_min
  6. Секунда tm_sec
  7. День недели tm_wday – целое число от 0 (Понедельник) до 6 (Воскресенье)
  8. День года tm_yday
  9. Целочисленный флаг tm_isdst для учета перехода на летнее время (daylight saving time, DST): 1 – переход на летнее время учитывается, 0 – не учитывается, -1 – неизвестно
        >>> time.mktime((2015, 10, 21, 7, 28, 0, 2, 294, -1))
1445401680.0
    
Назад в будущее: практическое руководство по путешествию во времени с Python

Очевидно, что составлять такой кортеж вручную – задача неблагодарная, ведь нужно знать и день недели, и номер дня в году. Обычно используются «готовые» объекты (для «ручного» формирования дат удобнее применять описанный далее модуль datetime):

        >>> time.mktime(time.localtime())
1575099022.0
    

Для перевода не местного, а международного времени в секунды необходимо использовать модуль calendar:

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

Заметим, что, в отличие от представления в виде секунд, struct_time не хранит составляющих времени, меньших, чем секунда.

Удобство использования struct_time заключается в том, что это именованный кортеж. Значит, можно писать более ясный код: вместо индексов элементы объекта вызываются по ключам с говорящими именами:

        >>> t = time.localtime()
>>> 'Итак, на дворе {}-й год.'.format(t.tm_year)
    

Кроме вышеперечисленных параметров-меток, struct_time содержит скрытые. Так, местным законодательством каждой страны регулируется переход на летнее время. Узнать, действует ли сейчас летнее время, можно следующим образом (в России в 2014 году произведен переход на постоянное «зимнее» время):

        >>> t.tm_isdst
0
    

Считаем часовой пояс:

        >>> t.tm_zone
'MSK'
    

Смещение местного времени относительно UTC в секундах:

        >>> t.tm_gmtoff
1
    

1.3. Строки временных меток

Назад в будущее: практическое руководство по путешествию во времени с Python

Распространенная задача – преобразование объектов указанных типов в строки вида timestamp, например, Mon Dec 2 18:30:20 2019. Для этого применяются функции ctime() и asctime():

  • ctime() – принимает время в секундах
  • asctime() – принимает struct_time (по умолчанию используется местное время)
        >>> time.ctime(time.time())  # преобразует время в секундах в timestamp для местного времени
'Sat Nov 30 10:37:11 2019'
>>> time.asctime()  # Аналогично time.asctime(time.localtime())
'Sat Nov 30 11:05:01 2019'
>>> time.asctime(time.gmtime())  # Время UTC
'Sat Nov 30 08:05:28 2019'
    

Хотя строковый вывод функций ctime() и asctime() довольно удобен, может потребоваться альтернативный формат. Для гибкого форматирования в библиотеку time включена функция strftime(). Функция принимает строку шаблона форматирования со спецификаторами и сам объект времени.

        >>> time.strftime('%d.%m.%Y', time.localtime())
'30.11.2019'
    

Функция strftime() также удобна для автоматической локализации строк:

        >>> import locale
>>> locale.setlocale(locale.LC_TIME, 'ru_RU.utf8')
>>> time.strftime('Текущее время: %c', time.localtime())
'Текущее время: Сб 30 ноя 2019 11:07:41'
    

Список спецификаторов шаблона:

  • %a, %A – аббревиатура и полное название дня недели (Чт, Четверг)
  • %b, %B – то же для месяца с учетом склонения (ноя, ноября)
  • – локализованная строка временной метки
  • %d – день месяца (28)
  • %H, %I – Час в 24- и 12-часовом представлении (17, 05)
  • %j – номер дня года (в представлении от 001 до 366)
  • %m – двузначное представление месяца (от 01 до 12)
  • %M – двузначное представление минут (от 00 до 59)
  • %p – местный эквивалент AM и PM
  • %S – двузначное представление секунд
  • %W – двузначное представление номера недели, первый день – Пн (%U для Вс)
  • %w – двузначное представление номера дня недели
  • %x, %X – принятый способ представления даты и времени.
  • %y, %Y – двузначное (без века) и четырехзначное представление года
  • %z, %Z – обозначение часового пояса в четырехзначном формате со знаком плюс или минус и в виде названия часового пояса

Пример одновременного использования нескольких спецификаторов:

        s = """Сегодня %A, %d %B. В России эту дату обычно записывают
следующим образом: %x или сокращенно: %d.%m.%y.
Это %j день года, %W неделя. На часах %X.
Часовой пояс: %Z."""

print(time.strftime(s, time.localtime()))
    

Что, если у нас есть строка, содержащая метку времени, а мы хотим распарсить ее в объект struct_time, чтобы обработать его в Python? Для этого есть функция strptime(). Первый аргумент – строка, второй – правило, описанное через те же спецификаторы:

        >>> time.strptime('Окт 21 2015 07:28', '%b %d %Y %H:%M')
time.struct_time(tm_year=2015, tm_mon=10, tm_mday=21, tm_hour=7, tm_min=28, tm_sec=0, tm_wday=2, tm_yday=294, tm_isdst=-1)
    

Функция strptime() позволяет кратко задавать struct_time, не используя все девять позиций кортежа. Неизвестные элементы вычисляются или на их место подставляются значения по умолчанию.

1.4. Приостановка выполнения кода и оценка производительности

Назад в будущее: практическое руководство по путешествию во времени с Python

Одна из наиболее часто используемых функций модуля time – функция sleep(), выполняющая задержку исполнения программного кода на переданное число секунд (можно использовать дробные значения):

        print(time.strftime('Текущее время: %X.'))
print('Задержка...')
time.sleep(5)
print('Прошло время.')
print(time.strftime('Текущее время: %X.'))
    
        Текущее время: 11:19:30.
Задержка...
Прошло время.
Текущее время: 11:19:35.
    

Функция sleep() нередко используется для тестирования кода, намеренного внесения задержек на различных этапах выполнения программы.

Для оценки производительности однократно запускаемых команд применяется функция perf_counter(), обеспечивающая лучшее разрешение по времени на коротких интервалах:

        def longrunning_function():
    for i in range(3):
        time.sleep(1)
        
def shortrunning_function():
    n = 1
    for i in range(2, 100):
        n *= i

start = time.perf_counter()
longrunning_function()
end = time.perf_counter()
print("Выполнение longrunning_function() заняло {} c.".format(end-start))

start = time.perf_counter()
shortrunning_function()
end = time.perf_counter()
print("Выполнение shortrunning_function() заняло {} c.".format(end-start))
    
        Выполнение longrunning_function() заняло 3.0040391949996774 c.
Выполнение shortrunning_function() заняло 0.00023569400036649313 c.
    

В Python версии 3.7 добавлена функция perf_counter_ns() – работает так же, но длительность выводится в наносекундах, что удобнее для совсем малых интервалов времени и быстро исполняемых команд.

Более удобные методы для измерения производительности фрагмента кода предоставляет модуль timeit.

2. Оценка производительности: timeit

Назад в будущее: практическое руководство по путешествию во времени с Python

В момент запуска программы в фоновом режиме также запускается множество сторонних процессов. Модуль timeit за счет многократного запуска фрагмента нивелирует неоднородность длительности его выполнения.

У модуля timeit есть интерфейс командной строки и интерфейс для вызова в коде. Во втором случае выводится время в секундах, которое длится общее количество запусков. Так как значение number по умолчанию составляет 1 млн повторений, можно считать, что при дефолтном запуске выводится среднее время операции в микросекундах. При вызове timeit в командной строке достаточное количество повторений определяется автоматически.

Сравним скорость выполнения операция конкатенации при использовании генератора и функции map():

        $ python3 -m timeit '"-".join(str(n) for n in range(100))'
100000 loops, best of 3: 14.6 usec per loop
$ python3 -m timeit '"-".join(map(str, range(100)))'
100000 loops, best of 3: 9.72 usec per loop
    

Сравним с вызовом через интерпретатор Python:

        >>> timeit.timeit('"-".join(str(n) for n in range(100))')
15.504044628999509
>>> timeit.timeit('"-".join(map(str, range(100)))')
10.823708094999347

    

Кроме куска кода, функции timeit() можно передать строку setup, однократно выполняемую перед началом повторения кода stmt. В setup, например, можно вынести импорт библиотек:

        mysetup = 'from math import sqrt'

mycode = '''
mylist = []
for i in range(100):
    mylist.append(sqrt(i))
'''
        
timeit.timeit(stmt = mycode,
              setup = mysetup,
              number = 10000)
    
        0.09423511200020585
    

В блокнотах Jupyter команда timeit относится к числу магических. С одним знаком процента она действует в пределах строки кода, с двумя – в границах ячейки:

        %timeit s = "-".join(str(n) for n in range(100))
    
        15.3 µs ± 276 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    
        %%timeit
mylist = []
for i in range(100):
    mylist.append(i**0.5)
    
        14.5 µs ± 369 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    

Магические команды %time и %%time делают те же операции без многократного повторения. Это приводит к завышенным результатам, но позволяет быстрее получить оценку производительности:

        %time s = "-".join(str(n) for n in range(100))
    
        CPU times: user 104 µs, sys: 1e+03 ns, total: 105 µs
Wall time: 113 µs
    
        %%time
mylist = []
for i in range(100):
    mylist.append(i**0.5)
    
        CPU times: user 119 µs, sys: 1e+03 ns, total: 120 µs
Wall time: 130 µs
    

3. Работа с датами: datetime

Назад в будущее: практическое руководство по путешествию во времени с Python

Вернемся к вопросу перемещения во времени. Модуль datetime поддерживает различные операции для работы с датами, например, определение интервала между двумя днями.

Структура представления времени в datetime похожа на struct_time в модуле time:

        >>> t = datetime.datetime.now()
>>> t
datetime.datetime(2019, 11, 30, 12, 7, 58, 431007)
>>> print(t)
2019-11-30 12:07:58.431007
    

Выведем отдельно дату и время:

        >>> print('Сегодня {}. Время: {}.'.format(t.date(), t.time()))
Сегодня 2019-11-30. Время: 12:07:58.431007.
    

Аналогично извлекаются год, месяц и т.д.:

        "Год {}, месяц {}, день {}, {} ч. {} мин. {} сек.".format(t.year,
                                                          t.month,
                                                          t.day,
                                                          t.hour,
                                                          t.minute,
                                                          t.second)
    

Модуль datetime также удобен для «ручного» задания дат и автоматизации арифметических операций с датами. Узнаем интервал времени между двумя главными датами сюжета фильма «Назад в будущее 2»:

        >>> today = datetime.datetime(year=1985, month=10, day=26, hour=21, minute=0)
>>> future = datetime.datetime(year=2015, month=10, day=21, hour=19, minute=28)
>>> delta = future-today
>>> print(delta)
10951 days, 22:28:00
    

Добавление найденной разности к первой дате «возвращает» нас в «будущее»:

        >>> print(today + delta)
2015-10-21 19:28:00
    

Узнаем, какое число будет через четыре недели. Для форматирования строк в модуле datetime имеется функция strftime() с теми же спецификаторами, что и в модуле time:

        >>> today = datetime.datetime.now()
>>> future = today + datetime.timedelta(days=28)
>>> f = '%d.%m.%y'
>>> print(today.strftime('Сегодня: ' + f))
Сегодня: 30.11.19
>>> print(future.strftime('Через 28 дней будет: ' + f))
Через 28 дней будет: 28.12.19
    

Если вам важнее оперировать не датами, а неделями, днями недели, месяцами, годами, то вам нужен модуль calendar.

4. Работа с календарем: calendar

Назад в будущее: практическое руководство по путешествию во времени с Python

Модуль calendar содержит функции для работы с календарем. В частности, умеет генерировать строки и HTML для вывода каленадарей месяцев и годов. Для наглядности напечатаем календарь на декабрь 2019 года:

        >>> calendar.prmonth(2019, 12)
    декабря 2019
Пн Вт Ср Чт Пт Сб Вс
                   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
    

Всё хорошо, кроме того, что в заголовке используется шаблон названия месяцев в родительном падеже (так он обозначен в указанной выше локали системы). Мы можем вручную переобозначить константу именования месяцев:

        # пустая строка в списке соответствует нулевому месяцу, первый месяц - январь
>>> month_names = ['', 'январь', 'февраль', 'март', 'апрель', 'май', 'июнь',
'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь']
>>> calendar.month_name = month_names
>>> calendar.prmonth(2019, 12)
    декабрь 2019
Пн Вт Ср Чт Пт Сб Вс
                   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
    

Или использовать сокращения:

        >>> calendar.month_name = calendar.month_abbr
>>> calendar.prmonth(1985, 10)
      окт 1985
Пн Вт Ср Чт Пт Сб Вс
    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
    

При помощи calendar можно не только «рисовать» календари, но и осуществлять итерации по их составляющим.

В качестве примера рассмотрим следующую практическую задачу. Во многих музеях существует один день месяца, когда посещение музея для всех лиц или отдельных категорий граждан происходит без взимания платы. Например, в Эрмитаже это третий четверг месяца. Вычислим даты бесплатных дней посещения Эрмитажа на 2020 год:

        free_days = []

for i in range(1, 13):
    c = calendar.monthcalendar(2020, i)
    first_week = c[0]
    third_week = c[2]
    fourth_week = c[3]

    # Если на первой неделе месяца есть четверг, то третий
    # четверг должен быть на третьей неделе. Если нет, то
    # на четвертой
    if first_week[calendar.THURSDAY]:
        free_day = third_week[calendar.THURSDAY]
    else:
        free_day = fourth_week[calendar.THURSDAY]
    s = '{0} {1}'.format(free_day, calendar.month_name[i])
    free_days.append(s)

print(", ".join(free_days))
    
        16 янв, 20 фев, 19 мар, 16 апр, 21 мая, 18 июн, 16 июл, 20 авг, 17 сен, 15 окт, 19 ноя, 17 дек
    

5. Сторонние библиотеки

Назад в будущее: практическое руководство по путешествию во времени с Python

Конечно, даже в самом подробном руководстве не описать всего, что рассказано в документациях встроенных библиотек. Если вам не хватает их функционала, имеется ряд сторонних решений:

  • dateutil – расширение стандартного модуля datetime для более специфичных операций, например, парсинга дат и их составляющих
  • pytz – для сложных манипуляций с часовыми поясами и летним временем
  • delorean – библиотека, названная в честь машины времени из фильма «Назад в будущее», упрощающая работу с датами
  • arrow – библиотека, стремящаяся заменить собой все вышеперечисленные, объединив их лучшие качества и заполнив пробелы
  • astropy – выполнение астрономических расчётов
  • tqdm – создание текстовых и интерактивных виджетов, отображающих процент выполнения длительного процесса (в том числе в Jupyter)

Знаете другие библиотеки для управления временем в Python? Расскажите о них ;)

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ