Как отключить функцию print внутри функции?
Дано
Есть некоторое количество функций, которые производят какие-то вычисления. В некоторых ситуациях (для отладки или для контроля процессов) удобно, чтобы они периодически печатали результаты вычислений. Как-то так:
def some_calculations(b, a):
print('Начали вычислять...')
# чего-то вычисляем
s = b + a
print(f'Закончили вычислять. Результаты: {s}')
print('Опять начали вычислять...')
# чего-то вычисляем
p = b * a
print(f'Закончили вычислять. Результаты: {p}')
# и тут таких или подобных итераций несколько
return s, p
Но в некоторых случаях, чтобы не тратить время на долгую операцию вывода мне надо все эти print отключить. Я, по-началу, хотел определить какую-то булеву переменную, которую передавать в эти функции и около каждого принта описать условие выводим/не выводим в зависимости от этой булевой переменной. Но потом мне подумалось, что должен быть способ, который просто отключит все принты. Не надеясь на собственные знания, полез искать и нашел.
# выключили
sys.stdout = open(os.devnull, 'w')
# включили
sys.stdout = sys.__stdout__
Дальше, я решил замерить время выполнения
- Просто функции
- С выключенным выводом
- Функции, в которой принты обернуты в условие, зависящее от булевой переменной-флага.
Вот, вариант с флагом:
def some_calculations(b, a, flag = False):
if flag:
print('Начали вычислять...')
# чего-то вычисляем
s = b + a
if flag:
print(f'Закончили вычислять. Результаты: {s}')
if flag:
print('Опять начали вычислять...')
# чего-то вычисляем
p = b * a
if flag:
print(f'Закончили вычислять. Результаты: {p}')
# и тут таких или подобных итераций несколько
return s, p
В результате замеров оказалось, что вариант с выключенным выводом дает выгоду во времени выполнения функции с принтами в 5 раз, а вариант с флагами - в 65. Причина такой разницы понятна - способ, указанный в начале, не отключает принты, он все равно выполняет функцию print, только выводит в devnull, то есть просто подавляет вывод в консоль. А флаги обходят функцию print.
Вопрос
Можно ли отключить функцию (например, print), без обвязывания каждого вызова функции в условие, таким образом, чтобы это отключение реально обходило бы функцию, тем самым давая выгоду во времени выполнения, а не просто подавляло вывод.
Ответы (6 шт):
Раскрою мысль из комментария, написать класс, который бы отключал стандартный вывод. Вызывать функцию через конструкцию with
import os, sys
class WrapperNoPrints:
def __enter__(self):
self._original_stdout = sys.stdout
sys.stdout = None
def __exit__(self, exc_type, exc_val, exc_tb):
sys.stdout = self._original_stdout
и вызов
with WrapperNoPrints():
s, p = some_calculations(1, 2)
Ну и результаты из консоли
>>> with WrapperNoPrints():
... s, p = some_calculations(1, 2)
...
>>> s
3
>>> p
2
>>>
>>>
>>> s, p = some_calculations(1, 2)
Начали вычислять...
Закончили вычислять. Результаты: 3
Опять начали вычислять...
Закончили вычислять. Результаты: 2
Просто сделайте свою функцию, которая делает принт с условием. Например:
is_p = False
def my_print(val):
if is_p:
print(val)
my_print(5)
is_p = True
my_print("test")
Флаг is_p может находиться и в самой функции, и быть в классе (вместе с функцией). Меняя флаг - меняете поведение.
UPD. Аналогичный метод, но без использования глобальных переменных
def my_print(val, flg=True):
if flg:
print(val)
def my_test_func():
with_print = False
my_print('без передачи флага')
my_print('c передачей флага', with_print)
my_test_func()
В целом, ещё более правильное решение, наверное, сделать свой класс-логгер, в зависимости от настроек которого можно будет удобнее конфигурировать принты/вывод в файл (например, добавить время вызова)
Это же Питон, тут можно переопределять почти всё, что угодно. Т.е. можно даже просто переопределить функцию print, чтобы она просто ничего не делала:
save_print = print
print = lambda x: None
some_calculations(1, 2)
print = save_print
Но вообще лучше учите логирование, оно именно для таких сценариев использования и придумано - когда вам то нужно инфу выводить, то не нужно. Да ещё и с разной степенью детализации нужно бывает.
P.S. О правильном логировании:
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.INFO) # logging.DEBUG
def some_calculations(b, a):
logger.debug('Начали вычислять...')
# чего-то вычисляем
s = b + a
logger.debug('Закончили вычислять. Результаты: %s', s)
logger.debug('Опять начали вычислять...')
# чего-то вычисляем
p = b * a
logger.debug('Закончили вычислять. Результаты: %s', p)
# и тут таких или подобных итераций несколько
return s, p
При таком варианте логирования, как написано в документации:
Formatting of message arguments is deferred until it cannot be avoided.
То есть, например, если вы установили уровень логирования такой, что ваши вызовы логера не должны ничего писать в лог, то и аргументы не будут подставляться в шаблоны. Хотя это всё-равно медленнее, чем вариант без логирования, то зато не требует хитрых модификаций кода, чтобы иметь варианты с логированием и без него. Достаточно прописать нужный уровень логирования в конфигурации логгера.
| Вариант функции | Время работы | Замедление относительно базы |
|---|---|---|
| Вывод закомментирован | 200 наносекунд | база |
| Уровень DEBUG | 160 миллисекунд | 800 раз |
| Уровень INFO | 2.3 миллисекунды | 11 раз |
| Уровень INFO + шаблонизация | 1.8 миллисекунды | 9 раз |
Лучше использовать предназначенный для таких действий пакет logging
Пример:
- Необходимые импорты
import os
import sys
import logging
from dotenv import load_dotenv
- Берем из окружения уровень логирования:
load_dotenv()
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
- Создаем свой экземпляр логера
stream_handler = logging.StreamHandler(stream=sys.stdout) # Обработчик логов в stdout
logger = logging.getLogger(__name__) # Наш логер
logger.setLevel(LOG_LEVEL) # Устанавливаем уровень
logger.addHandler(stream_handler) # Устанавливаем обработчик
logger.info(f"Logging level - {LOG_LEVEL}")
- Запускаем код:
def some_calculations(b, a):
logger.debug('Начали вычислять...')
# чего-то вычисляем
s = b + a
logger.debug(f'Закончили вычислять. Результаты: {s}')
logger.debug('Опять начали вычислять...')
# чего-то вычисляем
p = b * a
logger.debug(f'Закончили вычислять. Результаты: {p}')
return s, p
some_calculations(1, 3)
- Вывод (уровень
DEBUG):
Logging level - DEBUG
Начали вычислять...
Закончили вычислять. Результаты: 4
Опять начали вычислять...
Закончили вычислять. Результаты: 3
- Вывод (уровень
INFO):
Logging level - INFO
Этот код работает у меня 4.1с:
LOG = True
def log(message):
if LOG:
print(message)
def calc_sum(a, b):
c = a + b
log(f'calc_sum({a}, {b}) == {c}')
return c
for _ in range(10_000_000):
c = calc_sum(40, 2)
$ time python log.py | tail -1 calc_sum(40, 2) == 42 real 0m4.089s user 0m4.135s sys 0m0.138s
Если "отключить" логирование (LOG = False), то 2.2с:
$ time python log.py | tail -1 real 0m2.197s user 0m2.197s sys 0m0.001s
Если полностью закомментировать вызов log (# log(f'calc_sum({a}, {b}) == {c}'), то 0.6с:
$ time python log.py | tail -1 real 0m0.575s user 0m0.571s sys 0m0.004s
Разницу 4.1 - 0.6 = 3.5 примем за 100%. Тогда "отключение" логов (4.1 - 2.2 = 1.9) возвращает 54%. Ещё 46% мы получим если уберём вызов вообще.
Потому что вызов log(f'calc_sum({a}, {b}) == {c}') вычисляется в два этапа.
Первый этап – вычисление f'...'. Хотя она называется строкой, это не литерал а полноценный вызов функции. "Отключив" логирование мы его вычисление не убрали, он всё ещё тратит время.
Второй этап это сама печать. "Отключение" её предотвращает, тут всё в порядке.
Итого наш "отключатель" работает только на половину. И это почти неизбежно, если мы хотим сохранить синтаксис с форматными строками. А мы хотим, это удобно. Так что нужен вариант с if LOG: print(...):
LOG = True
def calc_sum(a, b):
c = a + b
if LOG:
print(f'calc_sum({a}, {b}) == {c}')
return c
for _ in range(10_000_000):
c = calc_sum(40, 2)
3.8c с включёнными логами.
$ time python log.py | tail -1 calc_sum(40, 2) == 42 real 0m3.811s user 0m3.863s sys 0m0.124s
Чуть больше 0.6c с "отключёнными" логами (LOG = False):
$ time python log.py | tail -1 real 0m0.623s user 0m0.619s sys 0m0.004s
Чуть меньше 0.6c с закомментированным кодом.
$ time py temp.py | tail -1 real 0m0.562s user 0m0.554s sys 0m0.008s
В этом варианте "отключение" сохраняет 98% времени. Это уже рабочий вариант, хоть и не красивый. Так что использовать if или компромисс, который работает так же быстро как вариант с if:
def calc_sum(a, b):
c = a + b
if LOG: print(f'calc_sum({a}, {b}) == {c}')
return c
def calc_sum(a, b):
c = a + b
LOG and print(f'calc_sum({a}, {b}) == {c}')
return c
Это комбинация ответов Dmitry and CrazyElf:
import builtins
class no_print:
def __enter__(self):
self.print = builtins.print
builtins.print = lambda *args, **kwargs: None
return self
def __exit__(self, exc_type, exc_val, exc_tb):
builtins.print = self.print
return False # Don't suppress exceptions
Использовать так:
def some_calculations(b, a):
print('Начали вычислять...')
# чего-то вычисляем
s = b + a
print(f'Закончили вычислять. Результаты: {s}')
print('Опять начали вычислять...')
# чего-то вычисляем
p = b * a
print(f'Закончили вычислять. Результаты: {p}')
# и тут таких или подобных итераций несколько
return s, p
some_calculations(1, 2)
with no_print():
some_calculations(3, 4)
Живой пример: https://www.online-python.com/G17kaOdlZw