Тайминг в Python — это измерение того, сколько времени занимает выполнение фрагмента кода: засекли момент до, момент после и взяли разницу. Для разового замера берут time.perf_counter(), для сравнения двух маленьких вариантов кода — модуль timeit, а для поиска медленного места в программе — профилировщик cProfile.
Содержание
- Короткий ответ: замер через perf_counter
- Какие часы выбрать: time, monotonic, perf_counter, process_time
- Время по стене и процессорное время на одном примере
- Удобная обертка: контекстный менеджер и декоратор
- timeit: сравнить варианты кода честно
- cProfile: найти, где программа тратит время
- Почему замеры скачут и как получить устойчивый результат
- Если нужно не измерить, а подождать или запустить позже
- Выводы
- Где применяется / связь с практикой
- FAQ
Примеры проверены на Python 3.14.7 (Linux, Docker-образ python:3.14-slim) 23.09.2026. Числа времени у вас будут другими: они зависят от процессора, нагрузки и версии интерпретатора, совпадать должен только порядок величин и соотношения.
Три термина, которые дальше не смешиваются:
- Время по стене (wall-clock) — сколько прошло реального времени, включая ожидание сети, диска и
sleep. - Процессорное время (CPU time) — сколько процессор реально работал на ваш процесс; ожидание сюда не входит.
- Монотонные часы — часы, которые никогда не идут назад, даже если систему перевели или синхронизировали по NTP.
Короткий ответ: замер через perf_counter
Минимальный полный пример — его можно скопировать и запустить:
import time
def work():
return sum(i * i for i in range(1_000_000))
start = time.perf_counter()
result = work()
elapsed = time.perf_counter() - start
print(f"результат: {result}")
print(f"время: {elapsed:.3f} с")
В консоли появятся результат и время в секундах:
результат: 333332833333500000
время: 0.026 с
Разбор по строкам. time.perf_counter() возвращает число секунд с плавающей точкой от некоторой точки отсчета. Сама по себе эта точка ничего не значит — смысл имеет только разность двух вызовов. Поэтому значение perf_counter() нельзя печатать как «текущее время» и нельзя сравнивать между разными запусками программы.
Какие часы выбрать: time, monotonic, perf_counter, process_time
В модуле time несколько функций, похожих на «таймер». Они отвечают на разные вопросы:
| Функция | Что измеряет | Может прыгнуть назад | Когда брать |
|---|---|---|---|
time.time() |
Секунды с начала эпохи Unix (1970-01-01 UTC) | Да, при переводе часов и синхронизации | Отметка «когда произошло событие», запись в лог |
time.monotonic() |
Монотонное время по стене | Нет | Таймауты, «прошло ли 30 секунд» в долгоживущем процессе |
time.perf_counter() |
Монотонное время по стене с наилучшим доступным разрешением | Нет | Замер длительности кода |
time.process_time() |
Процессорное время текущего процесса (без sleep) |
Нет | Сколько работал сам код, без ожиданий |
У каждой функции есть вариант с суффиксом _ns (perf_counter_ns() и другие) — он возвращает целое число наносекунд и не теряет точность на округлении float.
Свойства часов на своей машине можно посмотреть, а не угадывать:
import time
for name in ("time", "monotonic", "perf_counter", "process_time"):
info = time.get_clock_info(name)
print(f"{name:13} monotonic={info.monotonic!s:5} "
f"adjustable={info.adjustable!s:5} resolution={info.resolution}")
Вывод на Linux:
time monotonic=False adjustable=True resolution=1e-09
monotonic monotonic=True adjustable=False resolution=1e-09
perf_counter monotonic=True adjustable=False resolution=1e-09
process_time monotonic=True adjustable=False resolution=1e-09
Главное здесь — колонка adjustable=True у time.time(): эти часы может сдвинуть система. Если синхронизация сработает посреди замера, разность получится неверной, в худшем случае отрицательной. Значение resolution зависит от ОС: на macOS тот же код показывает другие числа. Это разрешение часов, а не гарантированная точность замера.
Время по стене и процессорное время на одном примере
Разницу проще всего увидеть, если код сначала ждет, а потом считает:
import time
wall0, cpu0 = time.perf_counter(), time.process_time()
time.sleep(1) # ждем, процессор свободен
sum(i * i for i in range(3_000_000)) # считаем, процессор занят
wall = time.perf_counter() - wall0
cpu = time.process_time() - cpu0
print(f"по стене (perf_counter): {wall:.2f} с")
print(f"процессор (process_time): {cpu:.2f} с")
по стене (perf_counter): 1.09 с
процессор (process_time): 0.09 с
Секунда sleep вошла только в первое число. Как выбирать: если пользователь ждет ответа (запрос к API, загрузка файла), смотрите время по стене. Если хотите понять, сколько «весит» сам алгоритм без ожиданий, смотрите process_time(). Учтите границу: process_time() суммирует время всех потоков процесса, а время дочерних процессов в него не входит.
Удобная обертка: контекстный менеджер и декоратор
Писать start = ... вокруг каждого фрагмента неудобно. Обычно делают маленький таймер, который можно вешать и на блок, и на функцию:
import time
from contextlib import contextmanager
from functools import wraps
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.4f} с")
def timed(func):
@wraps(func)
def wrapper(*args, **kwargs):
with timer(func.__name__):
return func(*args, **kwargs)
return wrapper
@timed
def build_list(n):
return [str(i) for i in range(n)]
with timer("сортировка"):
sorted(range(2_000_000, 0, -1))
build_list(500_000)
сортировка: 0.0360 с
build_list: 0.0178 с
Блок finally печатает время, даже если внутри случилось исключение, а functools.wraps сохраняет имя исходной функции. Такой таймер подходит для грубых замеров в скриптах. Для сравнения двух реализаций одного и того же он слишком шумный — для этого есть timeit.
timeit: сравнить варианты кода честно
Один замер маленького фрагмента почти ничего не говорит: он длится микросекунды, и любая фоновая активность системы искажает результат. Модуль timeit запускает код много раз подряд и повторяет серию несколько раз.
Самый быстрый способ — командная строка:
python -m timeit -s "data = list(range(10_000))" "[x * 2 for x in data]"
2000 loops, best of 5: 160 usec per loop
Флаг -s задает подготовку (setup), которая не входит в замер. timeit сам подобрал число повторов в серии (2000), сделал 5 серий и показал лучшую.
То же из кода, сразу для трех вариантов:
import timeit
setup = "data = list(range(10_000))"
variants = {
"цикл for": "r = []\nfor x in data:\n r.append(x * 2)",
"list comp": "[x * 2 for x in data]",
"map + lambda": "list(map(lambda x: x * 2, data))",
}
for name, stmt in variants.items():
times = timeit.repeat(stmt, setup=setup, repeat=5, number=200)
best = min(times) / 200 * 1e6 # микросекунды на один запуск
print(f"{name:13} {best:8.1f} мкс")
цикл for 164.0 мкс
list comp 150.7 мкс
map + lambda 295.2 мкс
Почему берут минимум, а не среднее: помехи (другие процессы, прерывания) только добавляют время, но не отнимают. Минимум ближе всего к стоимости самого кода. Это соглашение документации timeit, а не закон: для задач с разбросом по природе (сеть, диск) смотрят медиану и разброс.
Обратите внимание на соотношение: в этом прогоне цикл и list comprehension почти равны, а map с lambda вдвое медленнее. На другой версии Python разница может измениться — поэтому такие выводы проверяют замером на своей версии, а не берут из статей.
Частая ошибка: NameError внутри timeit
Неверно — передать строку с вызовом своей функции:
import timeit
def work():
return sum(i * i for i in range(10_000))
print(timeit.timeit("work()", number=100))
Последние строки трейсбека:
File "<timeit-src>", line 6, in inner
NameError: name 'work' is not defined
Строка выполняется в отдельном пространстве имен, где вашей функции нет. Исправление — передать globals=globals() или саму функцию без кавычек:
import timeit
def work():
return sum(i * i for i in range(10_000))
t1 = timeit.timeit("work()", number=100, globals=globals())
t2 = timeit.timeit(work, number=100)
print(f"строка + globals: {t1 / 100 * 1000:.3f} мс на вызов")
print(f"функция напрямую: {t2 / 100 * 1000:.3f} мс на вызов")
строка + globals: 0.240 мс на вызов
функция напрямую: 0.238 мс на вызов
cProfile: найти, где программа тратит время
perf_counter и timeit отвечают на вопрос «сколько». Профилировщик отвечает на вопрос «где»: он считает вызовы каждой функции и время в ней. Для запуска всего скрипта достаточно команды python -m cProfile -s cumulative script.py. Ниже — вариант из кода, файл profile_demo.py:
import cProfile
import pstats
def parse(lines):
return [line.split(",") for line in lines]
def slow_total(rows):
total = 0
for row in rows:
total += sum(int(x) for x in row)
return total
def main():
lines = ["1,2,3,4,5"] * 200_000
rows = parse(lines)
return slow_total(rows)
with cProfile.Profile() as prof:
main()
stats = pstats.Stats(prof)
stats.sort_stats("cumulative").print_stats(6)
1600005 function calls in 0.275 seconds
Ordered by: cumulative time
List reduced from 8 to 6 due to restriction <6>
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.275 0.275 /tmp/profile_demo.py:13(main)
1 0.029 0.029 0.228 0.228 /tmp/profile_demo.py:7(slow_total)
200000 0.087 0.000 0.199 0.000 {built-in method builtins.sum}
1200000 0.112 0.000 0.112 0.000 /tmp/profile_demo.py:10(<genexpr>)
1 0.025 0.025 0.046 0.046 /tmp/profile_demo.py:4(parse)
200000 0.022 0.000 0.022 0.000 {method 'split' of 'str' objects}
Как читать: ncalls — число вызовов, tottime — время внутри самой функции без вложенных, cumtime — вместе с вложенными. Видно, что parse занимает 0.046 с, а slow_total — 0.228 с, и почти все это — миллион с лишним вызовов генератора с int(). Оптимизировать нужно именно это место.
Граница метода: cProfile сам добавляет накладные расходы на каждый вызов функции, поэтому абсолютные секунды в его отчете завышены. Им ищут горячие места, а итоговое ускорение подтверждают обычным замером без профилировщика.
Почему замеры скачут и как получить устойчивый результат
Пять одинаковых замеров подряд дают разные числа:
import time
def work():
return sum(i * i for i in range(200_000))
for attempt in range(5):
start = time.perf_counter()
work()
print(f"попытка {attempt + 1}: {(time.perf_counter() - start) * 1000:.2f} мс")
попытка 1: 5.38 мс
попытка 2: 5.49 мс
попытка 3: 5.37 мс
попытка 4: 5.37 мс
попытка 5: 5.60 мс
Здесь разброс около 4%, на ноутбуке с браузером и сменой частоты процессора он бывает заметно больше. Источники погрешности: фоновые процессы, изменение частоты процессора, сборщик мусора, прогрев кешей, первый импорт модулей.
Практические правила:
- Повторяйте замер и смотрите минимум или медиану, а не одно число.
- Сравнивайте варианты на одной машине, одной версии Python и одних данных.
- Не держите
printи логирование внутри измеряемого цикла — вывод медленнее многих вычислений. - Разницу в пределах разброса (здесь несколько процентов) не считайте результатом.
Если нужно не измерить, а подождать или запустить позже
Слово «таймер» в Python часто означает задержку, а не замер. Это другие инструменты:
| Задача | Инструмент | Что важно знать |
|---|---|---|
| Приостановить текущий поток | time.sleep(сек) |
Блокирует поток; в GUI замораживает окно |
| Вызвать функцию через N секунд, не останавливая программу | threading.Timer(сек, func) |
Функция выполнится в отдельном потоке |
| Пауза в асинхронном коде | await asyncio.sleep(сек) |
Не блокирует цикл событий, в отличие от time.sleep |
| Отложенный вызов в Tkinter | widget.after(мс, func) |
Интервал в миллисекундах |
Короткий пример на первые два пункта:
import threading
import time
start = time.perf_counter()
def remind():
print(f"сработал через {time.perf_counter() - start:.1f} с")
t = threading.Timer(1.5, remind) # вызвать remind через 1.5 с в отдельном потоке
t.start()
print("основной поток продолжает работу")
time.sleep(0.5) # пауза текущего потока
print(f"пауза закончилась через {time.perf_counter() - start:.1f} с")
t.join()
основной поток продолжает работу
пауза закончилась через 0.5 с
сработал через 1.5 с
sleep гарантирует паузу не меньше заданной (с оговоркой про сигналы), но не точно заданную: планировщик ОС может разбудить поток позже.
Выводы
- Для замера длительности кода берите
time.perf_counter(): часы монотонные и с лучшим доступным разрешением;time.time()может сдвинуться системой посреди замера. - Время по стене (
perf_counter) включает ожидания, процессорное (process_time) — только работу процессора; выбор зависит от вопроса. - Для сравнения маленьких фрагментов используйте
timeit: много запусков, несколько серий, минимум; свою функцию передавайте черезglobalsили объектом. - Чтобы найти медленное место, запускайте
cProfileи смотритеcumtime; ускорение подтверждайте замером без профилировщика. - Один замер — не результат: повторяйте, сравнивайте в одинаковых условиях и не принимайте разницу в пределах разброса за выигрыш.
Где применяется / связь с практикой
Замер времени — первый шаг любой оптимизации: сначала профилировщик показывает узкое место, потом timeit сравнивает варианты, потом обычный замер подтверждает выигрыш на реальных данных. Так работают с медленными API-обработчиками, ETL-скриптами, тестами производительности в CI.
Освойте тему на практике
Профилирование, асинхронный код, многопоточность и работа с производительностью Python разбираются на курсе Python Developer. Professional. Попробовать формат можно на бесплатных открытых уроках Otus.
FAQ
Чем perf_counter() отличается от monotonic()?
Обе функции монотонные и считают время по стене. perf_counter() использует часы с наилучшим доступным разрешением для коротких замеров; на части платформ это те же часы, что у monotonic(), что видно по time.get_clock_info().
Можно ли замерить время в Jupyter без кода обертки?
Да, в IPython и Jupyter есть магические команды %timeit для одной строки и %%timeit для ячейки. Они используют тот же подход, что модуль timeit, но это возможность IPython, а не обычного Python.
Как замерить асинхронную функцию?
Засеките perf_counter() до и после await внутри корутины. Результат будет временем по стене, включающим ожидание других задач в цикле событий, а не только работу самой функции.



