Python: от данных до своей сводки Урок 14 из 56
Даты и время: 45 дней, часовой пояс и формат, который читается двумя способами
Четырнадцатый урок курса по Python. Между 15 января и 1 марта — 45 дней, и считает их `timedelta`, а не вы. Плюс два подвоха: `01.02.2026` разбирается двумя форматами без единой ошибки — январём и февралём, — а наивное время нельзя вычесть из осознанного, и Python говорит об этом прямо.
Зачем это нужно
В прошлом уроке год приехал строкой "2025", и мы сделали из него число, потому что дальше его надо было сравнивать. С датами то же самое, только дороже.
Дата в выгрузке — текст: 2026-01-15, 15.01.2026, 15 января. Пока она текст, с ней ничего нельзя сделать: не вычесть, не прибавить неделю, не спросить «сколько дней прошло».
А когда её всё-таки разбирают, выясняется, что одна и та же строка читается двумя способами, и оба выглядят правдоподобно. Плюс вопрос, которого в числах не было: девять тридцать — это где?
Сразу целиком
Файл kunder.py. Запуск: python kunder.py из окружения.
Обязательное — первые два блока: строка в дату и разница дат. Третий и четвёртый показывают формат и часовой пояс, ради которых урок и написан.
"""Урок 14: дата — не строка, а точка на оси времени.
В прошлом уроке год приехал строкой "2025", и мы сделали из него число. С датами
то же самое, только дороже: "01.02.2026" читается двумя способами, и оба
выглядят правдоподобно.
"""
from datetime import date, datetime, timedelta, timezone
# Из выгрузки даты приходят текстом. ISO-формат читается без объяснений.
raw = ["2026-01-15", "2026-02-01", "2026-03-01"]
print("== строка в дату")
days = []
for text in raw:
day = date.fromisoformat(text)
days.append(day)
print(f"{text} → {day} | год {day.year}, месяц {day.month}, день {day.day}")
print()
print("== разница дат — это timedelta")
span = days[-1] - days[0]
print("между первым и последним:", span)
print("дней:", span.days)
print("через неделю после последнего:", days[-1] + timedelta(days=7))
print()
print("== разобрать по своему формату и напечатать по своему")
moment = datetime.strptime("01.02.2026 09:30", "%d.%m.%Y %H:%M")
print("разобрали:", moment)
print("напечатали:", moment.strftime("%d.%m.%Y, %H:%M"))
print("в файл пишут так:", moment.isoformat())
try:
datetime.strptime("2026-02-01", "%d.%m.%Y")
except ValueError as error:
print("чужой формат:", error)
print()
print("== часовой пояс: наивное время и осознанное")
naive = datetime(2026, 2, 1, 9, 30)
aware = datetime(2026, 2, 1, 9, 30, tzinfo=timezone.utc)
almaty = timezone(timedelta(hours=5))
print("наивное: ", naive, "| пояс:", naive.tzinfo)
print("осознанное:", aware, "| пояс:", aware.tzinfo)
print("то же мгновение в Алматы:", aware.astimezone(almaty))
try:
print(aware - naive)
except TypeError as error:
print("одно из другого не вычесть:", error)
Выводит:
== строка в дату
2026-01-15 → 2026-01-15 | год 2026, месяц 1, день 15
2026-02-01 → 2026-02-01 | год 2026, месяц 2, день 1
2026-03-01 → 2026-03-01 | год 2026, месяц 3, день 1
== разница дат — это timedelta
между первым и последним: 45 days, 0:00:00
дней: 45
через неделю после последнего: 2026-03-08
== разобрать по своему формату и напечатать по своему
разобрали: 2026-02-01 09:30:00
напечатали: 01.02.2026, 09:30
в файл пишут так: 2026-02-01T09:30:00
чужой формат: time data '2026-02-01' does not match format '%d.%m.%Y'
== часовой пояс: наивное время и осознанное
наивное: 2026-02-01 09:30:00 | пояс: None
осознанное: 2026-02-01 09:30:00+00:00 | пояс: UTC
то же мгновение в Алматы: 2026-02-01 14:30:00+05:00
одно из другого не вычесть: can't subtract offset-naive and offset-aware datetimes
Разбор
Строка становится датой один раз — при разборе
day = date.fromisoformat(text)
fromisoformat читает дату в формате ISO 8601: год-месяц-день, четыре цифры, два, два. Это тот же приём, что int(row["date"]) в прошлом уроке: текст превращают в значение сразу, а не «когда понадобится».
Понадобится сразу. Всё, что нужно от даты, — арифметика: разница, сдвиг, сравнение. Со строками ничего этого нет: "2026-03-01" - "2026-01-15" — ошибка.
Сравнение строк, правда, сработает: "2026-03-01" > "2026-01-15" — верно. Но верно оно случайно: ISO так устроен, что порядок строк совпадает с порядком дат. На 01.02.2026 то же сравнение молча соврёт.
Образ. Почтовый штемпель. Пока дата — оттиск на бумаге, это картинка; чтобы посчитать, сколько письмо шло, её надо прочитать.
Разница дат — это промежуток, а не число
между первым и последним: 45 days, 0:00:00
дней: 45
Вычитание двух дат даёт не число, а timedelta — промежуток. Печатается он по-своему, а число дней берут у него: .days. Рядом живут .seconds и .total_seconds(), когда важны часы и минуты.
Обратно тоже работает: days[-1] + timedelta(days=7) — дата через неделю. Прибавляют промежуток, а не «семь»: у дат нет сложения с числом, и это правильно, потому что «семь» — это семь чего.
Читает strptime, печатает strftime
moment = datetime.strptime("01.02.2026 09:30", "%d.%m.%Y %H:%M")
print(moment.strftime("%d.%m.%Y, %H:%M"))
Мнемоника простая: p — parse, разобрать текст в дату; f — format, напечатать дату текстом. Формат — строка с процентами: %d — день, %m — месяц, %Y — год из четырёх цифр, %H:%M — часы и минуты. Полный список — в документации.
Оба нужны, потому что у даты два адресата. В файл и в API дату пишут в ISO — moment.isoformat(), — и её прочитает кто угодно. Человеку показывают привычное 01.02.2026.
Есть и ловушка вежливости: %B даёт название месяца — но January, а не «январь», пока не настроена локаль системы. Поэтому в файле держат ISO, а название месяца, если оно нужно, собирают сами: у программы три языка, а локаль одна.
Формат, который читается двумя способами
чужой формат: time data '2026-02-01' does not match format '%d.%m.%Y'
Здесь Python сказал вслух: формат не подошёл. Но это везучий случай.
Невезучий — 01.02.2026. В Казахстане и России это первое февраля, в США — второе января. Обе даты существуют, обе разбираются без ошибки, и какая получится, решает не строка, а формат, который назвали вы. Назовёте не тот — не будет ни ошибки, ни предупреждения: будет другая дата. Такую ошибку находят через месяц, когда сходится не то.
Отсюда правило: в файлах и API — ISO 8601, 2026-02-01. Он однозначен, он сортируется как строка, и его понимает fromisoformat. «День.месяц.год» — это для экрана.
Наивное время и осознанное
наивное: 2026-02-01 09:30:00 | пояс: None
осознанное: 2026-02-01 09:30:00+00:00 | пояс: UTC
У datetime есть поле tzinfo, и оно бывает пустым. Пустое — это наивное время: девять тридцать, а где — неизвестно. Тот же самый None, что и в седьмом уроке, только теперь пропущен часовой пояс.
Наивное время не отвечает на вопрос «какое это мгновение». 9:30 в Алматы и 9:30 в Варшаве — разные моменты, между ними четыре часа. Пока время наивное, разница в четыре часа не видна — она просто не посчитана.
Осознанное время знает свой пояс, и его можно перевести: aware.astimezone(almaty) даёт то же мгновение, записанное по-алматински — 14:30. Мгновение одно, записи разные.
Отсюда попытка вычесть одно из другого:
одно из другого не вычесть: can't subtract offset-naive and offset-aware datetimes
Python отказывается — и это лучшее, что он мог сделать. Молчаливая разница в пять часов дороже громкой ошибки.
Правило, которое стоит запомнить целиком: хранят в UTC, показывают в местном. datetime.now() даёт наивное локальное время, datetime.now(timezone.utc) — осознанное; в файл и в базу пишут второе.
Пояс как смещение — timezone(timedelta(hours=5)) — знает только «плюс пять». Пояс по имени — ZoneInfo("Asia/Almaty") из модуля zoneinfo — знает ещё и историю переводов часов, а она в мире меняется чаще, чем кажется.
Чего в timedelta нет
Месяца и года. Промежуток измеряется в днях, часах, секундах — в том, что всегда одинаковой длины. «Месяц» одинаковой длины не бывает: он то 28 дней, то 31.
Прибавить месяц — значит сначала договориться, что делать с 31 января. Стандартная библиотека такого договора не навязывает; чужие библиотеки (dateutil) его предлагают вместе со своими правилами.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Почему дату превращают в
dateпри разборе, а не оставляют строкой? - Чем
strptimeотличается отstrftimeи как это запомнить? - Что такое наивное время и почему Python не даёт вычесть его из осознанного?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта программа?
from datetime import date
a = date(2026, 3, 1)
b = date(2026, 1, 15)
print((a - b).days, a > b)
2. Заполните пропуск. Вместо ... поставьте формат, которым разбирается эта строка.
from datetime import datetime
moment = datetime.strptime("01.02.2026 09:30", ...)
print(moment.date())
3. Почините. В выгрузке из Казахстана 01.02.2026 — это первое февраля. Программа не падает, но печатает не тот день. Найдите причину.
from datetime import datetime
day = datetime.strptime("01.02.2026", "%m.%d.%Y")
print(day.date())
Задание
Обязательное. Дано:
rows = [
("2026-01-15", 520.0),
("2026-02-01", 546.5),
("2026-03-01", 498.0),
]
report = "02.03.2026 08:00"
Разберите даты замеров и напечатайте каждую в виде 15.01.2026: 520.0. Затем напечатайте, сколько дней между первым и последним замером; средний промежуток между замерами с двумя знаками; дату следующего замера, если он через тридцать дней после последнего. В конце разберите момент отчёта по формату день.месяц.год часы:минуты, объявите его временем Алматы (UTC+5) и напечатайте дважды: в ISO по Алматы и в UTC.
Ожидаемый вывод:
15.01.2026: 520.0
01.02.2026: 546.5
01.03.2026: 498.0
дней между первым и последним: 45
средний промежуток: 22.50 дня
следующий замер: 31.03.2026
отчёт по Алматы: 2026-03-02T08:00:00+05:00
он же в UTC: 2026-03-02T03:00:00+00:00
Готово, когда: вывод совпадает построчно; даты становятся date или datetime при разборе, а не остаются строками; средний промежуток делится на количество промежутков, а не замеров; момент отчёта получает пояс там же, где разобран, — иначе в UTC он приедет тем же числом.
На своих данных. Возьмите три своих даты — чеки, платежи, что угодно с числом на нём. Посчитайте, сколько дней между первым и последним, и напечатайте каждую дату в привычном виде. Одну из них запишите в чужом формате (15/01/2026 или 15 Jan 2026) и разберите её своим strptime.
По желанию.
- Разберите
01.02.2026двумя форматами —%d.%m.%Yи%m.%d.%Y— и напечатайте обе даты рядом. - Возьмите
ZoneInfo("Asia/Almaty")вместо фиксированного смещения и сравните результат. - Попробуйте прибавить к дате
timedelta(months=1)и прочитайте, что скажет Python.
Куда это встанет в проекте
У сводки появляется ось времени. Замеры перестают быть просто числами: между ними считаются дни, а «когда собрано» пишется в UTC — не потому, что так красивее, а потому, что сводка когда-нибудь поедет на сервер, и его часы стоят не по Алматы.
Долги. Прибавить месяц нам по-прежнему нечем, и переводы часов фиксированное смещение не знает. Первое решается договором, второе — zoneinfo; к обоим вернёмся, когда сводка начнёт работать по расписанию.
Ответы
Показать ответы
На вопросы
- Потому что дальше нужна арифметика: разница, сдвиг на неделю, сравнение. Строка ничего этого не умеет, а сравнение строк совпадает со сравнением дат только в ISO и только случайно.
p— parse:strptimeчитает текст и делаетdatetime.f— format:strftimeберётdatetimeи делает текст. Первый нужен на входе, второй — на выходе.- Наивное — это
datetime, у которогоtzinfoпуст: время без места. Вычесть его из осознанного нельзя, потому что неизвестно, сколько между ними часов; Python отказывается вслух, вместо того чтобы посчитать разницу с ошибкой в целый пояс.
К разминке
45 True. Разница дат —timedelta, у него спрашивают.days; сравнивать даты можно напрямую, и 1 марта позже 15 января.
45 True
"%d.%m.%Y %H:%M". Формат описывает входную строку, а не то, что хочется получить: день, точка, месяц, точка, год, пробел, часы, двоеточие, минуты.
from datetime import datetime
moment = datetime.strptime("01.02.2026 09:30", "%d.%m.%Y %H:%M")
print(moment.date())
2026-02-01
- Формат назван американским:
%m.%d.%Y— сначала месяц. Строка ему подошла, ошибки не было, и получилось второе января вместо первого февраля. Так и выглядит самая дорогая ошибка в датах: не падение, а другая дата.
from datetime import datetime
day = datetime.strptime("01.02.2026", "%d.%m.%Y")
print(day.date())
2026-02-01
Источники
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.