Уроки курса
1 Введение в Python и философия дзен
30 мин
2 Переменные и динамическая типизация
30 мин
3 Базовые типы данных: числа, строки и булевы значения
30 мин
4 Изменяемые и неизменяемые объекты (Mutable vs Immutable)
30 мин
5 Форматирование строк и f-строки
30 мин
6 Углубленная работа со списками
30 мин
7 Кортежи и их особенности
30 мин
8 Словари под капотом
30 мин
9 Множества и математические операции
30 мин
10 Генераторы списков (List Comprehensions)
30 мин
11 Генераторы словарей и множеств
30 мин
12 Встроенные функции для коллекций
30 мин
13 Условные операторы и логические выражения
30 мин
14 Циклы while и управление потоком
30 мин
15 Итерация с циклом for
30 мин
16 Конструкции for...else и while...else
30 мин
17 Функции enumerate и zip
30 мин
18 Создание собственных функций (def)
30 мин
19 Позиционные и именованные аргументы
30 мин
20 Проблема изменяемых аргументов по умолчанию
30 мин
21 Произвольное число аргументов (*args и **kwargs)
30 мин
22 Область видимости переменных (LEGB)
30 мин
23 Анонимные функции (lambda)
30 мин
24 Функции высшего порядка
30 мин
25 Замыкания (Closures)
30 мин
26 Введение в объектно-ориентированное программирование
30 мин
27 Атрибуты классов и экземпляров
30 мин
28 Магический метод __init__
30 мин
29 Методы экземпляра
30 мин
30 Инкапсуляция и сокрытие данных
30 мин
31 Декоратор @property
30 мин
32 Наследование классов
30 мин
33 Переопределение методов и функция super()
30 мин
34 Полиморфизм в Python
30 мин
35 Магические методы строк (__str__ и __repr__)
30 мин
36 Обработка исключений (try-except)
30 мин
37 Блоки else и finally
30 мин
38 Генерация собственных исключений (raise)
30 мин
39 Открытие и чтение файлов
30 мин
40 Запись данных в файлы
30 мин
41 Контекстные менеджеры (with)
30 мин
42 Работа с форматом JSON
30 мин
43 Модули и импорты
30 мин
44 Полезные модули стандартной библиотеки
30 мин
45 Модуль datetime
30 мин
46 Модуль collections
30 мин
47 Виртуальные окружения (venv)
30 мин
48 Установка сторонних пакетов через pip
30 мин
49 Организация структуры Python-проекта
30 мин
50 Финальный проект: создание приложения
30 мин

Модуль datetime

Создание, форматирование и арифметические вычисления с датами и временем.

Прогресс урока: 0%

Введение в философию времени в программировании

Работа с датами и временем в программировании — это одна из самых сложных и коварных областей, с которой неизбежно сталкивается каждый разработчик на пути к уровню Intermediate и Senior. На первый взгляд может показаться, что время — это простая, линейная и предсказуемая концепция. В конце концов, в минуте всегда 60 секунд, в часе 60 минут, а в сутках 24 часа, не так ли? Однако в реальности все обстоит гораздо сложнее. Исторически сложилось так, что человечество изобрело множество календарей, ввело високосные года для компенсации неточностей вращения Земли вокруг Солнца, придумало летнее и зимнее время (Daylight Saving Time, DST) в целях экономии электроэнергии и разделило планету на десятки часовых поясов, границы которых периодически меняются по политическим причинам. Кроме того, существуют високосные секунды (leap seconds), которые периодически добавляются для синхронизации атомных часов с астрономическим временем. Все это делает время не линейным, а полным исключений и аномалий. В стандартной библиотеке Python для решения этих проблем предназначен модуль datetime. Он инкапсулирует в себе большинство сложностей, предоставляя разработчику удобные объектно-ориентированные интерфейсы для манипуляции временными метками. Модуль предоставляет несколько основных классов, каждый из которых имеет свою строгую зону ответственности: date для работы исключительно с датами (год, месяц, день) по идеализированному григорианскому календарю; time для работы со временем (часы, минуты, секунды, микросекунды) независимо от конкретного дня; datetime, который объединяет в себе дату и время; timedelta для вычисления разницы между двумя моментами времени или датами; и tzinfo, абстрактный базовый класс для реализации часовых поясов. Важнейшей концепцией, которую необходимо усвоить с самого начала, является разделение объектов времени на «наивные» (naive) и «осведомленные» (aware). Наивный объект не содержит информации о часовом поясе. Он представляет собой просто абстрактную дату или время, смысл которых зависит исключительно от контекста выполнения программы. Осведомленный объект, напротив, содержит точную информацию о смещении относительно Всемирного координированного времени (UTC), что делает его абсолютным и однозначным указателем на конкретный момент в истории Вселенной. В большинстве современных систем, особенно в веб-разработке и при работе с базами данных, золотым стандартом является хранение всех временных меток исключительно в UTC (осведомленные объекты) и конвертация их в локальное время пользователя только на этапе отображения (фронтенд или шаблон). Это позволяет избежать классических багов, когда данные, сохраненные сервером в Нью-Йорке, неправильно интерпретируются сервером в Токио. В этом уроке мы детально, шаг за шагом, разберем каждый из этих классов, рассмотрим их внутреннее устройство, изучим методы форматирования и парсинга, а также решим несколько реальных задач, связанных с расчетом рабочих графиков и вычислением времени доставки с учетом выходных дней и часовых поясов.

python
import datetime

# Простейший пример использования модуля
current_datetime = datetime.datetime.now()
print(f"Текущая локальная дата и время: {current_datetime}")

Что означает термин «наивный» (naive) объект времени в контексте модуля datetime?

Глубокое изучение класса datetime.date

Класс datetime.date представляет собой идеализированную дату по пролептическому григорианскому календарю. Слово «пролептический» означает, что правила текущего григорианского календаря (введенного папой Григорием XIII в 1582 году) экстраполируются как в будущее, так и в прошлое, вплоть до года 1. Года с нулевым или отрицательным значением не поддерживаются. При создании объекта класса date вы обязаны передать три целочисленных аргумента: год (year), месяц (month) и день (day). Python проводит строгую валидацию этих значений на уровне исходного кода на языке C (CPython), что гарантирует невозможность создания некорректной даты. Год должен быть в диапазоне от MINYEAR (1) до MAXYEAR (9999). Месяц — от 1 до 12. День — от 1 до количества дней в указанном месяце соответствующего года. Если вы попытаетесь создать дату 29 февраля в невисокосный год, интерпретатор немедленно сгенерирует исключение ValueError. Важной особенностью объектов класса date, как и всех других основных классов модуля datetime, является их полная неизменяемость (immutability). Подобно числам, строкам и кортежам, после создания объекта date вы не можете изменить ни один из его атрибутов (year, month или day). Попытка выполнить присваивание, например `my_date.year = 2025`, приведет к ошибке AttributeError. Это архитектурное решение делает объекты дат потокобезопасными и позволяет использовать их в качестве ключей в словарях или элементов во множествах. Если вам необходимо получить новую дату на основе существующей с изменением, скажем, только года, вы должны использовать метод replace(). Этот метод возвращает совершенно новый объект date, копируя исходные значения и заменяя только те, которые вы явно указали через именованные аргументы (kwargs). Помимо ручного конструирования, класс date предоставляет несколько крайне полезных классовых методов (classmethods) для альтернативного создания экземпляров. Метод date.today() является самым часто используемым — он опрашивает системные часы операционной системы и возвращает текущую локальную дату. Важно понимать, что `today()` зависит от настроек часового пояса самой ОС, поэтому на серверах, работающих в UTC, он вернет дату по UTC, а на вашем домашнем ноутбуке — вашу локальную дату. Другой важный метод — date.fromtimestamp(timestamp), который конвертирует POSIX-таймстамп (количество секунд, прошедших с 1 января 1970 года, 00:00:00 UTC) в локальную дату. Этот метод критически важен при интеграции с внешними системами или базами данных, которые хранят время в виде целых или вещественных чисел. Наконец, метод date.fromisoformat(date_string), появившийся в Python 3.7, предоставляет сверхбыстрый способ парсинга строк в формате YYYY-MM-DD (стандарт ISO 8601). Он работает в разы быстрее, чем универсальный `strptime`, так как не требует разбора строки формата, а жестко ожидает конкретную структуру.

python
from datetime import date

# 1. Ручное создание даты
my_birthday = date(1995, 8, 15)
print(f"Год: {my_birthday.year}, Месяц: {my_birthday.month}, День: {my_birthday.day}")

# 2. Получение текущей даты (зависит от настроек ОС)
current_date = date.today()
print(f"Сегодня: {current_date}")

# 3. Использование метода replace для создания новой даты
next_birthday = my_birthday.replace(year=current_date.year)
print(f"Мой день рождения в этом году: {next_birthday}")

# 4. Парсинг из стандарта ISO 8601
parsed_date = date.fromisoformat('2023-12-31')
print(f"Распарсенная дата: {parsed_date}")

Какое исключение (ошибка) возникнет, если попытаться изменить атрибут year у уже созданного объекта date (например, my_date.year = 2024)?

Работа с днями недели и календарями (weekday, isocalendar)

Одной из самых частых задач в программировании является определение дня недели по заданной дате. Например, вам нужно проверить, выпадает ли доставка на выходной день, или сгенерировать еженедельный отчет. Класс date предоставляет два основных метода для получения дня недели: weekday() и isoweekday(). Разница между ними кроется в системе индексации. Метод `weekday()` возвращает день недели как целое число от 0 до 6, где 0 соответствует понедельнику (Monday), а 6 — воскресенью (Sunday). Это согласуется с традицией языка Python использовать нумерацию с нуля, которая применяется в списках и других последовательностях. С другой стороны, метод `isoweekday()` возвращает день недели в соответствии со стандартом ISO 8601, где нумерация начинается с 1 и заканчивается 7. Таким образом, понедельник — это 1, а воскресенье — 7. Выбор между этими двумя методами зависит от логики вашего приложения. Если вы планируете использовать полученный день недели в качестве индекса для доступа к списку с названиями дней (`days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']`), то удобнее использовать `weekday()`. Если же вы передаете данные во внешнюю систему, требующую совместимости с ISO, используйте `isoweekday()`. Распространенной ошибкой Junior-разработчиков является путаница между этими методами, что приводит к смещению логики на один день (например, выходными ошибочно считаются пятница и суббота). Помимо простых дней недели, часто возникает необходимость работы с порядковыми номерами недель в году. Для этого предназначен метод isocalendar(). Он возвращает именованный кортеж (в версиях Python 3.9+) с тремя элементами: ISO-годом, номером ISO-недели и номером ISO-дня недели (от 1 до 7). Стандарт ISO 8601 определяет первую неделю года как неделю, содержащую первый четверг года. Это правило может привести к неожиданным для новичков результатам на границах годов. Например, 1 января 2021 года была пятницей. Поскольку эта неделя не содержала первого четверга 2021 года (он выпал на 7 января), стандарт ISO считает 1, 2 и 3 января 2021 года принадлежащими к 53-й неделе предыдущего, 2020 года! Таким образом, вызов `date(2021, 1, 1).isocalendar()` вернет `datetime.IsoCalendarDate(year=2020, week=53, weekday=5)`. Понимание этой особенности критически важно при построении финансовых, бухгалтерских и аналитических систем, где группировка данных по неделям является стандартом. Если вы проигнорируете правило ISO-года и объедините данные, опираясь только на обычный календарный год и номер ISO-недели, ваши отчеты за первые и последние дни года будут содержать грубые ошибки.

python
from datetime import date

some_date = date(2023, 10, 15)  # 15 октября 2023 года - Воскресенье

# Метод weekday(): Понедельник = 0, Воскресенье = 6
print(f"weekday() для воскресенья: {some_date.weekday()}")

# Метод isoweekday(): Понедельник = 1, Воскресенье = 7
print(f"isoweekday() для воскресенья: {some_date.isoweekday()}")

# Массив названий дней недели (индексация с нуля)
ru_days = ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье"]
print(f"Название дня: {ru_days[some_date.weekday()]}")

# Работа с isocalendar() на границе года
new_year_2021 = date(2021, 1, 1)
iso_cal = new_year_2021.isocalendar()
print(f"ISO календарь для 1 января 2021: Год={iso_cal.year}, Неделя={iso_cal.week}, День={iso_cal.weekday}")
День недели Значение weekday() Значение isoweekday()
Понедельник 0 1
Вторник 1 2
Среда 2 3
Четверг 3 4
Пятница 4 5
Суббота 5 6
Воскресенье 6 7

Как определить, является ли объект класса date выходным днем (суббота или воскресенье), используя метод isoweekday()?

Изоляция времени: класс datetime.time

Класс datetime.time абстрагирует концепцию времени суток, полностью оторванную от какой-либо конкретной даты. Он описывает идеальное время на идеальных 24-часовых часах. Этот класс полезен, когда в вашей предметной области есть понятия, привязанные только ко времени: часы работы магазина (с 09:00 до 20:00), расписание звонков будильника или интервалы отправки ежедневных email-рассылок. Конструктор класса принимает следующие аргументы: hour (часы, от 0 до 23), minute (минуты, от 0 до 59), second (секунды, от 0 до 59), microsecond (микросекунды, от 0 до 999999) и tzinfo (информация о часовом поясе). Все аргументы являются необязательными, и если вы вызовете конструктор без параметров — `time()` — вы получите объект, представляющий полночь: 00:00:00. Как и класс date, объекты класса time абсолютно неизменяемы. Любая модификация требует создания нового объекта с помощью метода replace(). Важным аспектом работы с изолированным временем является понимание логических ловушек, связанных со сравнением и вычислением интервалов. Наиболее классический баг — это попытка вычислить продолжительность ночной смены. Представьте ночной клуб, который открывается в 22:00 (`start = time(22, 0)`) и закрывается в 04:00 (`end = time(4, 0)`). С точки зрения математической логики класса time, 04:00 меньше, чем 22:00. Если вы попытаетесь проверить, находится ли текущее время `current = time(23, 30)` в рабочем интервале клуба с помощью простого условия `start <= current <= end`, оно всегда будет возвращать False! Логика сравнения изолированного времени не знает о концепции «следующего дня», так как класс time не имеет атрибута даты. Чтобы корректно решать такие задачи, разработчикам приходится либо применять сложную условную логику с оператором OR (если `start > end`, то время валидно, если оно `> start` ИЛИ `< end`), либо, что является признаком лучшей практики (best practice), комбинировать объекты time с конкретными датами, превращая их в полноценные объекты `datetime`, где математические операции и сравнения работают абсолютно прозрачно и предсказуемо. Кроме того, объекты time могут быть как наивными (без атрибута tzinfo), так и осведомленными. Попытка сравнить наивный объект time с осведомленным объектом time приведет к генерации исключения TypeError, так как Python не может логически сопоставить локальное неизвестное время со временем в конкретном часовом поясе. Класс time также поддерживает формат вывода ISO 8601 через метод isoformat(), который возвращает строку вида 'HH:MM:SS.mmmmmm'. Если микросекунды равны нулю, они опускаются в строковом представлении для сохранения чистоты и читаемости данных.

python
from datetime import time

# Создание времени: 14 часов, 30 минут, 0 секунд
meeting_time = time(14, 30)
print(f"Время встречи: {meeting_time}")

# Доступ к атрибутам (только чтение)
print(f"Часы: {meeting_time.hour}, Минуты: {meeting_time.minute}")

# Время по умолчанию - полночь
midnight = time()
print(f"Полночь выглядит так: {midnight}")

# Демонстрация проблемы ночной смены (Junior vs Senior подход)
start_shift = time(22, 0)  # 22:00
end_shift = time(4, 0)     # 04:00 следующего дня
current_time = time(23, 30) # 23:30

# Неправильный подход (баг логики)
is_working_bad = start_shift <= current_time <= end_shift
print(f"Неправильная проверка смены: {is_working_bad}") # Вернет False

# Правильный подход для изолированного времени
if start_shift > end_shift:
    is_working_good = current_time >= start_shift or current_time <= end_shift
else:
    is_working_good = start_shift <= current_time <= end_shift

print(f"Правильная проверка смены: {is_working_good}") # Вернет True

Что произойдет, если попытаться сравнить (используя оператор ==) наивный объект time и осведомленный (с указанным часовым поясом tzinfo) объект time?

Если вызвать конструктор datetime.time() без передачи каких-либо аргументов, чему будет равен атрибут `hour` созданного объекта?

Центральный элемент: класс datetime.datetime

Класс datetime.datetime является сердцем одноименного модуля. Он объединяет в себе все атрибуты как класса date (год, месяц, день), так и класса time (часы, минуты, секунды, микросекунды, информация о часовом поясе). Это самый универсальный и часто используемый класс, так как в большинстве реальных задач — от логирования событий в системе до хранения времени создания записей в базе данных — требуется знать и дату, и точное время. Конструктор класса `datetime` принимает обязательные позиционные аргументы: год, месяц и день. Остальные аргументы (часы, минуты и т.д.) по умолчанию равны нулю. Как и его родительские компоненты, объекты datetime абсолютно неизменяемы (immutable). Самый популярный способ создания объекта текущего момента времени — использование метода класса datetime.now(). Вызов `datetime.now()` возвращает текущую локальную дату и время. Если вы передадите в этот метод объект часового пояса (экземпляр `tzinfo`), он вернет текущее время в указанном поясе. В старых версиях Python разработчики повсеместно использовали метод datetime.utcnow() для получения текущего времени по Гринвичу (UTC). Однако важно знать, что этот метод является опасной ловушкой: он возвращает наивный объект datetime, который содержит время UTC, но не имеет атрибута `tzinfo`. Это приводит к путанице, так как система считает этот объект локальным временем. Начиная с Python 3.12, метод `utcnow()` официально помечен как устаревший (deprecated). Современный, правильный (Pythonic way) способ получить текущее время в UTC — это вызов datetime.now(datetime.timezone.utc). Это гарантированно возвращает осведомленный (aware) объект, который надежно работает при любых математических операциях или конвертациях. Класс datetime также предоставляет удобный метод combine(date, time). Он позволяет «склеить» изолированный объект класса `date` и изолированный объект класса `time` в один полноценный объект `datetime`. Это невероятно полезно, когда, например, пользователь выбирает дату в одном виджете пользовательского интерфейса, а время — в другом, и вам нужно объединить их перед отправкой на сервер. Еще один важнейший метод — astimezone(tz). Он позволяет конвертировать осведомленный объект datetime из одного часового пояса в другой. Если вызвать `astimezone()` без аргументов для осведомленного объекта, он конвертирует время в локальный часовой пояс вашей операционной системы. Если же применить `astimezone()` к наивному объекту, Python сначала предположит, что этот объект находится в локальном часовом поясе системы, что может привести к непредсказуемым последствиям, если вы подразумевали, что это UTC-время. Поэтому золотое правило работы с datetime: всегда делайте ваши объекты осведомленными как можно раньше (при создании или парсинге) и работайте только с UTC внутри бизнес-логики приложения.

python
from datetime import datetime, date, time, timezone

# 1. Получение текущего локального времени (наивный объект)
local_now = datetime.now()
print(f"Локальное время: {local_now}")

# 2. ПРАВИЛЬНЫЙ способ получения текущего UTC времени (осведомленный объект)
# Обратите внимание на импорт timezone
utc_now = datetime.now(timezone.utc)
print(f"Текущее время UTC: {utc_now}")
print(f"Есть ли у utc_now tzinfo? {utc_now.tzinfo}")

# 3. Комбинирование даты и времени
selected_date = date(2025, 1, 1)
selected_time = time(12, 0, 0)
combined_dt = datetime.combine(selected_date, selected_time)
print(f"Скомбинированный datetime: {combined_dt}")

# 4. Конвертация таймстампа
timestamp = 1672531199.0 # Таймстамп для 31 декабря 2022, 23:59:59 UTC
dt_from_ts = datetime.fromtimestamp(timestamp, tz=timezone.utc)
print(f"Восстановлено из таймстампа: {dt_from_ts}")

Почему использование метода datetime.utcnow() считается плохой практикой в современных версиях Python (и объявлено устаревшим в Python 3.12)?

Напишите код (только вызов метода), который является современным и рекомендуемым способом получения текущего времени в UTC. Используйте классы datetime и timezone. (Подсказка: используйте метод now с аргументом).

Форматирование дат: Метод strftime (String Format Time)

Довольно редко сырые объекты datetime выводятся пользователю напрямую (с помощью функции `print()`). Стандартное строковое представление объекта, возвращаемое магическим методом `__str__` (который неявно вызывается при принте), выглядит как '2023-10-25 14:30:00.000000'. Однако бизнес-требования почти всегда диктуют иные форматы. Например, для русскоязычной аудитории привычнее видеть '25 октября 2023 г.', а для лог-файлов часто используется формат '25/Oct/2023:14:30:00'. Для преобразования объекта datetime (или date, или time) в строку желаемого формата используется метод strftime(format). Название метода расшифровывается как «String Format Time». Этот метод принимает единственную строку формата, которая содержит специальные спецификаторы (директивы), начинающиеся с символа процента (%). Эти спецификаторы работают как заполнители (placeholders), которые модуль datetime находит в строке и заменяет на соответствующие значения из объекта. Понимание и запоминание основных спецификаторов — обязательный навык. Рассмотрим самые важные из них. Для года: %Y возвращает полный четырехзначный год (например, 2023), а %y возвращает двузначный год без века (например, 23). Для месяца: %m возвращает номер месяца с ведущим нулем (от 01 до 12), %b — сокращенное название месяца (Jan, Feb), а %B — полное название (January, February). Важно отметить, что названия месяцев и дней недели зависят от локали (locale) операционной системы. Если локаль не настроена явно на русский язык в Python-скрипте с помощью модуля `locale`, эти спецификаторы вернут английские названия. Для дня: %d — день месяца с нулем (01-31). Для времени: %H — часы в 24-часовом формате (00-23), %I — часы в 12-часовом формате (01-12), %M — минуты (00-59), %S — секунды (00-59). Если вы используете 12-часовой формат, обязательно добавляйте спецификатор %p, который выводит AM или PM. Вы можете комбинировать эти спецификаторы с любыми обычными символами: пробелами, двоеточиями, запятыми или слэшами. Например, строка формата "%d/%m/%Y %H:%M:%S" превратит объект datetime в классическую европейскую запись '25/10/2023 14:30:00'. Ошибки при использовании `strftime` обычно сводятся к путанице между регистрами символов. Использование `%m` (месяц) вместо `%M` (минуты) — один из самых распространенных багов, который может привести к тому, что ваше время всегда будет показывать, например, 10 минут, если сейчас октябрь. Процесс генерации строки через `strftime` является достаточно ресурсоемкой операцией по сравнению с простой конкатенацией строк, поэтому в высоконагруженных циклах форматирования (например, при выгрузке миллионов строк в CSV-файл) рекомендуется использовать `isoformat()` или создавать кастомные оптимизированные функции форматирования.

python
from datetime import datetime

# Тестовая дата: 5 апреля 2024 года, 14:05:09
dt = datetime(2024, 4, 5, 14, 5, 9)

# 1. Европейский формат (День.Месяц.Год)
european_format = dt.strftime("%d.%m.%Y")
print(f"Европа: {european_format}") # 05.04.2024

# 2. Американский формат 12-часовой (Месяц/День/Год Час:Минута AM/PM)
american_format = dt.strftime("%m/%d/%Y %I:%M %p")
print(f"США: {american_format}") # 04/05/2024 02:05 PM

# 3. Полный текстовый формат
# Примечание: язык зависит от настроек локали системы!
text_format = dt.strftime("%A, %d %B %Y")
print(f"Текст: {text_format}") # Friday, 05 April 2024

# 4. Демонстрация классической ошибки новичка: путаница %m и %M
bad_format = dt.strftime("Время: %H:%m") # %m - это месяц (04)
correct_format = dt.strftime("Время: %H:%M") # %M - это минуты (05)
print(f"Ошибка новичка: {bad_format}") # Время: 14:04 (НЕПРАВИЛЬНО)
print(f"Правильно: {correct_format}")     # Время: 14:05
Спецификатор Описание Пример вывода
%Y Год с веком (4 цифры) 2023
%y Год без века (2 цифры) 23
%m Месяц (01-12) 10
%M Минуты (00-59) 45
%H Часы 24ч (00-23) 14
%d День месяца (01-31) 05

Какой из предложенных форматов strftime правильно преобразует объект datetime в строку вида '31-12-2023 23:59'?

Парсинг дат: Метод strptime (String Parse Time)

Обратной стороной медали форматирования является парсинг — процесс преобразования строкового представления даты и времени обратно в полноценный объект класса `datetime`. В модуле datetime за это отвечает метод класса datetime.strptime(date_string, format). Название расшифровывается как «String Parse Time». В отличие от `strftime`, который вызывается на самом объекте, `strptime` — это классовый метод, который принимает два аргумента: саму строку с датой и строку формата (шаблон), описывающую структуру переданной строки. Строка формата использует абсолютно те же самые спецификаторы (%Y, %m, %d и т.д.), что и `strftime`. Алгоритм работы `strptime` строг и беспощаден. Он считывает строку символа за символом и сопоставляет ее с вашим шаблоном. Если он ожидает найти двузначное число для месяца (%m), а находит букву, или если он ожидает пробел между датой и временем, а находит запятую, интерпретатор мгновенно прерывает работу и генерирует исключение ValueError. Это делает процесс парсинга очень надежным, но требует от разработчика высокой точности при составлении шаблона формата. Если в строке присутствуют «лишние» данные, которые не описаны в шаблоне, или если строка короче, чем ожидает шаблон, также будет выброшен ValueError. Например, если сервер возвращает дату в виде '2023-10-25T14:30:00Z' (стандартный формат ISO 8601, часто встречающийся в JSON API), и вы попытаетесь распарсить его шаблоном `"%Y-%m-%d %H:%M:%S"`, код упадет, так как буква 'T' и 'Z' не соответствуют ожидаемому пробелу. Правильный шаблон для такой строки был бы `"%Y-%m-%dT%H:%M:%SZ"` (обратите внимание на экранирование литералов). Важный нюанс: если в строке формата не указаны значения для часов, минут или секунд, созданный объект `datetime` автоматически заполнит их нулями (полночь). Аналогично, если вы парсите только время, по умолчанию будет установлен 1900 год, 1 месяц, 1 день. Это историческое наследие языка C, на котором написан интерпретатор Python. При работе с массивами данных из внешних источников (например, из CSV файлов, скачанных из разных систем) часто возникает ситуация, когда даты могут приходить в разных форматах (одна строка '15.10.2023', другая '2023/10/15'). В таких случаях профессиональные разработчики обычно пишут функции-обертки, которые пытаются применить `strptime` в блоке `try-except`, перебирая список возможных форматов до первого успешного парсинга. Однако стоит помнить, что множественные перехваты исключений — медленная операция, и если вам нужно распарсить миллионы строк, лучше использовать сторонние библиотеки, такие как dateutil.parser или pandas, которые содержат высокооптимизированные эвристические алгоритмы для автоматического определения форматов дат без необходимости явно указывать шаблоны.

python
from datetime import datetime

# Пример 1: Успешный парсинг стандартной строки
api_response = "2023/12/31 23:59"
parsed_dt = datetime.strptime(api_response, "%Y/%m/%d %H:%M")
print(f"Распарсенный объект: repr={repr(parsed_dt)}")
print(f"Тип объекта: {type(parsed_dt)}")

# Пример 2: Парсинг строки, содержащей текст
log_entry = "System booted at [15-Oct-2023 08:30:00] by user admin"
# Мы должны извлечь подстроку перед парсингом, либо использовать regex.
# Если мы точно знаем позицию:
date_str = "15-Oct-2023 08:30:00"
dt_log = datetime.strptime(date_str, "%d-%b-%Y %H:%M:%S")
print(f"Время из лога: {dt_log}")

# Пример 3: Перехват ValueError при неверном формате
bad_string = "2023-15-10" # Месяц 15 не существует
try:
    datetime.strptime(bad_string, "%Y-%m-%d")
except ValueError as e:
    print(f"Ошибка парсинга: {e}")

Какое исключение (error) генерирует метод datetime.strptime, если переданная строка не соответствует указанному формату?

Математика времени: класс datetime.timedelta

До сих пор мы рассматривали даты и время как фиксированные точки на временной шкале. Однако программирование часто требует вычислений: узнать дату, которая наступит через 30 дней после регистрации пользователя; рассчитать возраст в днях; или вычислить время, затраченное алгоритмом на выполнение задачи. Для представления продолжительности (длительности) времени, то есть разницы между двумя датами или моментами времени, в модуле datetime существует класс timedelta. Объект timedelta представляет собой интервал. Когда вы вычитаете один объект date из другого (или один datetime из другого), Python неявно (under the hood) создает и возвращает объект timedelta. Конструктор класса `timedelta` позволяет создавать интервалы вручную, принимая именованные аргументы: days, seconds, microseconds, milliseconds, minutes, hours, weeks. Внутренне (в C-реализации) CPython нормализует все эти аргументы и хранит интервал исключительно в трех атрибутах: days (целое число дней), seconds (целое число секунд от 0 до 86399) и microseconds (микросекунды от 0 до 999999). Это означает, что если вы создадите `timedelta(hours=25)`, внутри он будет сохранен как `days=1` и `seconds=3600`. Это свойство нормализации крайне важно понимать. Объекты timedelta поддерживают все основные математические операции. Вы можете прибавить timedelta к объекту datetime (получив новую дату в будущем), вычесть его (получив дату в прошлом), умножить timedelta на целое число или число с плавающей точкой, и даже разделить один объект timedelta на другой (результатом будет число float, показывающее соотношение интервалов). Существует одна классическая логическая ошибка, которую совершают практически все начинающие программисты при работе с этим классом. Когда им нужно получить общую продолжительность интервала в секундах, они обращаются к атрибуту `.seconds`. Однако этот атрибут возвращает только остаток секунд после вычета полных дней! Представьте, что интервал равен 2 дням и 5 секундам. Атрибут `.seconds` вернет значение 5. Это критический баг. Чтобы получить истинную общую продолжительность всего интервала, сконвертированного в секунды, всегда следует вызывать метод total_seconds(). Он вернет значение типа float, учитывающее и дни, и секунды, и микросекунды (в нашем примере `2 * 86400 + 5 = 172805.0`). Еще один важный момент: вы не найдете в конструкторе timedelta аргументов `months` (месяцы) или `years` (годы). Почему разработчики языка Python не добавили их? Ответ кроется в самой природе времени. Месяц — это непостоянная величина. В феврале 28 или 29 дней, в январе 31. Год может быть високосным (366 дней) или обычным (365). Если бы вы сказали `date(2024, 1, 31) + timedelta(months=1)`, какой день должен получиться? 28 февраля? 29 февраля? 2 или 3 марта? Из-за этой математической неоднозначности стандартный модуль datetime не поддерживает прямое добавление месяцев. Если вам нужна такая функциональность, принято использовать стороннюю библиотеку dateutil с ее мощным классом relativedelta, который умеет корректно обрабатывать календарные сдвиги, перенося даты на конец месяца.

python
from datetime import datetime, timedelta

# 1. Получение timedelta путем вычитания дат
new_year = datetime(2025, 1, 1)
now = datetime(2024, 1, 1)
time_left = new_year - now
print(f"Тип time_left: {type(time_left)}")
print(f"До Нового года: {time_left.days} дней")

# 2. Создание timedelta вручную и сдвиг времени в будущее
current_dt = datetime(2023, 10, 25, 12, 0)
delta = timedelta(weeks=2, days=3, hours=4) # 17 дней и 4 часа
future_dt = current_dt + delta
print(f"Дата через 2 недели и 3 дня: {future_dt}")

# 3. Демонстрация ошибки с атрибутом .seconds
long_interval = timedelta(days=2, seconds=10)

# НЕПРАВИЛЬНО (вернет только 10)
bad_seconds = long_interval.seconds
print(f"Атрибут .seconds (ошибка): {bad_seconds}")

# ПРАВИЛЬНО (вернет 172810.0)
good_seconds = long_interval.total_seconds()
print(f"Метод .total_seconds() (правильно): {good_seconds}")

Почему при создании объекта timedelta нельзя передать аргументы `months` или `years` (например, timedelta(months=1))?

Часовые пояса и зональная осведомленность (zoneinfo)

Долгое время работа с часовыми поясами (time zones) в стандартной библиотеке Python была крайне ограничена. Модуль datetime предоставлял абстрактный базовый класс `tzinfo` и лишь одну конкретную реализацию — класс `timezone`, который мог описывать только фиксированное смещение относительно UTC (например, UTC+3). Этого было абсолютно недостаточно для реального мира, где существуют сложные правила перехода на летнее и зимнее время (DST — Daylight Saving Time), которые еще и исторически меняются правительствами стран. Разработчикам приходилось устанавливать стороннюю библиотеку pytz, которая содержала базу данных IANA (база данных часовых поясов Олсона). Однако, библиотека pytz имела нестандартный и сложный API: нельзя было просто передать пояс в конструктор datetime, нужно было использовать специальные методы `localize` и `normalize`, что регулярно приводило к ошибкам у новичков. Ситуация кардинально изменилась с выходом Python 3.9. В стандартную библиотеку был добавлен модуль zoneinfo. Это стало огромным прорывом. Модуль `zoneinfo` предоставляет класс `ZoneInfo`, который реализует интерфейс `tzinfo` и использует ту же самую базу данных IANA, встроенную в операционную систему (или пакет `tzdata` на Windows). Теперь создание осведомленного (aware) объекта времени для конкретного города стало максимально интуитивным и «питоничным». Вы импортируете `ZoneInfo`, передаете ему строковый ключ часового пояса (например, 'Europe/Moscow', 'America/New_York') и передаете созданный объект в аргумент `tzinfo` конструктора datetime. Самое мощное свойство `ZoneInfo` заключается в том, что он знает исторические правила. Если вы создадите дату для июля в часовом поясе 'Europe/Berlin', `ZoneInfo` автоматически применит летнее смещение (UTC+2). Если вы создадите дату для декабря, он применит зимнее смещение (UTC+1). Вам не нужно производить эти расчеты вручную. Если вы используете арифметику `timedelta` с осведомленными объектами, перескакивая через границу перехода на летнее время, модуль datetime корректно вычислит разницу, хотя это может привести к неожиданным результатам, если вы ожидаете ровно 24 часа в сутках. В дни перевода часов сутки могут длиться 23 или 25 часов! Еще один важный концепт — конвертация между поясами. Как только у вас есть осведомленный объект, скажем, в поясе Нью-Йорка, вы можете мгновенно узнать, сколько сейчас времени в Токио, вызвав метод astimezone(ZoneInfo('Asia/Tokyo')). Рекомендуемая архитектура (Best Practice) для любых приложений: на уровне базы данных и внутреннего ядра системы хранить все метки времени исключительно в формате UTC (осведомленные объекты с `timezone.utc`). При получении данных от пользователя (через формы или API), немедленно парсить их, прикреплять локальный пояс пользователя и конвертировать в UTC с помощью `astimezone(timezone.utc)`. А при отдаче данных пользователю (на фронтенд) выполнять обратную конвертацию из UTC в пояс пользователя. Этот паттерн избавляет от 99% всех багов, связанных с датой и временем.

python
from datetime import datetime, timezone
import zoneinfo
# Примечание: zoneinfo доступен только в Python 3.9+

# 1. Создание осведомленного объекта для Нью-Йорка
ny_tz = zoneinfo.ZoneInfo("America/New_York")
ny_time = datetime(2023, 10, 25, 12, 0, tzinfo=ny_tz)
print(f"Время в Нью-Йорке: {ny_time}")
print(f"Смещение: {ny_time.tzname()}") # Вернет EDT (Eastern Daylight Time)

# 2. Конвертация времени из Нью-Йорка в Токио
tokyo_tz = zoneinfo.ZoneInfo("Asia/Tokyo")
tokyo_time = ny_time.astimezone(tokyo_tz)
print(f"Время в Токио в тот же момент: {tokyo_time}")

# 3. Правильный паттерн работы (UTC -> Локаль)
# Сохраняем в БД всегда в UTC
db_time_utc = datetime.now(timezone.utc)
print(f"В базе данных сохранено: {db_time_utc}")

# Пользователь из Лондона запрашивает данные
london_tz = zoneinfo.ZoneInfo("Europe/London")
display_time = db_time_utc.astimezone(london_tz)
print(f"Пользователю в Лондоне покажем: {display_time.strftime('%Y-%m-%d %H:%M')}")

Как называется модуль, добавленный в Python 3.9, который предоставляет современный доступ к базе данных часовых поясов IANA и заменяет стороннюю библиотеку pytz?

Задание

Практическое задание: Вычисление даты доставки заказа. Представьте, что вы разрабатываете интернет-магазин. Заказ оформлен сегодня. Доставка занимает ровно 5 рабочих дней. Выходные дни (суббота и воскресенье) не учитываются в срок доставки. Напишите алгоритм, который вычисляет точную дату доставки.

  • Создайте объект datetime.date.today() для фиксации текущего дня.
  • Создайте переменную (счетчик) оставшихся рабочих дней для доставки (например, days_to_add = 5).
  • Используйте цикл while, который будет выполняться, пока days_to_add > 0.
  • Внутри цикла прибавляйте к текущей дате 1 день с помощью timedelta(days=1).
  • Проверяйте день недели полученной даты с помощью метода isoweekday().
  • Если день недели меньше 6 (с понедельника по пятницу), уменьшайте счетчик days_to_add на 1.
  • Если день недели 6 или 7, переходите к следующей итерации (пропускайте выходной).
  • После завершения цикла выведите итоговую дату доставки с помощью strftime в формате 'DD.MM.YYYY'.
10 баллов
python
# Решение практического задания:
from datetime import date, timedelta

start_date = date.today()
print(f"Дата оформления заказа: {start_date.strftime('%d.%m.%Y')} (день недели: {start_date.isoweekday()})")

delivery_date = start_date
days_to_add = 5

while days_to_add > 0:
    # Сдвигаем на 1 день вперед
    delivery_date += timedelta(days=1)
    
    # Проверяем, является ли день рабочим (1-5 это Пн-Пт)
    if delivery_date.isoweekday() < 6:
        days_to_add -= 1
        
print(f"Дата доставки заказа: {delivery_date.strftime('%d.%m.%Y')} (день недели: {delivery_date.isoweekday()})")

Заключение и лучшие практики работы с datetime

Модуль datetime предоставляет мощные и гибкие инструменты, но их неправильное использование часто приводит к трудноуловимым багам. Давайте резюмируем лучшие практики (Best Practices), которые отличают код Senior-разработчика. Во-первых, избегайте работы с наивными объектами времени. Если ваше приложение хоть как-то связано с интернетом, серверами или пользователями из разных регионов, всегда используйте осведомленные (aware) объекты datetime. В момент получения времени из системных часов используйте `datetime.now(timezone.utc)`. Во-вторых, никогда не храните локальное время в базах данных. Локальное время зависит от настроек сервера. Если вы перенесете базу данных с сервера во Франкфурте на сервер в Калифорнии, все ваши наивные локальные метки потеряют смысл. Храните время в UTC. Конвертируйте в локальный пояс пользователя только на уровне UI (фронтенда или шаблонизатора) непосредственно перед рендерингом страницы. В-третьих, будьте осторожны с арифметикой дат на границах переходов на летнее/зимнее время (DST). Если вы прибавляете 24 часа (`timedelta(hours=24)`) к дню, когда происходит перевод стрелок назад (часы переводятся с 3 ночи на 2 ночи, день становится длительностью 25 часов), вы можете получить время, отличное от того же часа следующего дня. Если вам важна привязка к календарным дням, а не к абсолютным физическим секундам, прибавляйте `timedelta(days=1)`. В-четвертых, помните о производительности методов форматирования. Методы `strftime` и `strptime` относительно медленны из-за необходимости парсинга строки шаблона. Если вы логируете данные в высоконагруженной системе, предпочтительнее использовать метод `.isoformat()`, который работает значительно быстрее, так как генерирует стандартную строку ISO 8601 без разбора шаблонов. И, наконец, если бизнес-логика требует сложного манипулирования интервалами (например, 'добавить ровно 3 месяца к текущей дате' или 'найти ближайшую третью пятницу месяца'), не пытайтесь изобретать велосипед и писать свои сложные алгоритмы поверх стандартного datetime. Используйте индустриальный стандарт — библиотеку dateutil, а в случае работы с огромными массивами временных рядов (Data Science, аналитика) — объекты Timestamp из библиотеки pandas. Стандартный модуль datetime — это надежный фундамент, и глубокое понимание его архитектуры — неизменяемости объектов, разницы между time, date и datetime, механики timedelta и концепции tzinfo — является обязательным шагом для каждого профессионального Python-инженера.