Как отключить функцию 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__

Дальше, я решил замерить время выполнения

  1. Просто функции
  2. С выключенным выводом
  3. Функции, в которой принты обернуты в условие, зависящее от булевой переменной-флага.

Вот, вариант с флагом:

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 шт):

Автор решения: Dmitry

Раскрою мысль из комментария, написать класс, который бы отключал стандартный вывод. Вызывать функцию через конструкцию 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
→ Ссылка
Автор решения: Dezmonder

Просто сделайте свою функцию, которая делает принт с условием. Например:

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()

В целом, ещё более правильное решение, наверное, сделать свой класс-логгер, в зависимости от настроек которого можно будет удобнее конфигурировать принты/вывод в файл (например, добавить время вызова)

→ Ссылка
Автор решения: CrazyElf

Это же Питон, тут можно переопределять почти всё, что угодно. Т.е. можно даже просто переопределить функцию 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 раз
→ Ссылка
Автор решения: Maksim Alekseev

Лучше использовать предназначенный для таких действий пакет 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
→ Ссылка
Автор решения: Stanislav Volodarskiy

Этот код работает у меня 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
→ Ссылка
Автор решения: Intelligent Shade of Blue

Это комбинация ответов 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

→ Ссылка