Идиоматическое ведение журнала Python: форматирование строки + список аргументов против форматирования строки строки - что предпочтительнее?

Полезно ли вызывать функции ведения журнала с помощью строки формата + список аргументов против форматирования inline?

Я видел (и написал) код регистрации, который использует форматирование строки inline:

logging.warn("%s %s %s" % (arg1, arg2, arg3))

и все же я предполагаю, что лучше (с точки зрения производительности и более идиоматического) использовать:

logging.warn("%s %s %s", arg1, arg2, arg3)

потому что вторая форма избегает операций форматирования строк до вызова функции ведения журнала. Если текущий уровень ведения журнала отфильтровывает сообщение журнала, форматирование не требуется, что сокращает время обработки и распределение памяти.

Я на правильном пути здесь, или я что-то пропустил?

Ответ 1

IMHO, для сообщений, которые, скорее всего, будут отображаться, например, для error или warn, это не имеет большого значения.

Для сообщений, которые менее вероятно отображаются, я бы определенно пошел на вторую версию, главным образом по соображениям производительности. Я часто передаю большие объекты в качестве параметра info, которые реализуют дорогостоящий метод __str__. Понятно, что отправкой этого предварительно отформатированного в info будет потеря производительности.

UPDATE

Я только что проверил исходный код модуля logging, и, действительно, форматирование выполняется после проверки уровня журнала. Например:

class Logger(Filterer):
    # snip
    def debug(self, msg, *args, **kwargs):
        # snip
        if self.isenabledfor(debug):
            self._log(debug, msg, args, **kwargs)

Можно заметить, что msg и args не связаны между вызовом log и проверкой уровня журнала.

ОБНОВЛЕНИЕ 2

Выделив Левона, позвольте мне добавить несколько тестов для объектов с дорогостоящим методом __str__:

$ python -m timeit -n 1000000 -s "import logging" -s "logger = logging.getLogger('foo')" -s "logger.setLevel(logging.ERROR)" "logger.warn('%s', range(0,100))"
1000000 loops, best of 3: 1.52 usec per loop
$ python -m timeit -n 1000000 -s "import logging" -s "logger = logging.getLogger('foo')" -s "logger.setLevel(logging.ERROR)" "logger.warn('%s' % range(0,100))"
1000000 loops, best of 3: 10.4 usec per loop

На практике это может привести к значительному повышению производительности.

Ответ 2

В случае, если это полезно, вот быстрый анализ времени для двух параметров форматирования:

In [61]: arg1='hello'
In [62]: arg2='this'
In [63]: arg3='is a test'

In [70]: timeit -n 10000000 "%s %s %s" % (arg1, arg2, arg3)
10000000 loops, best of 3: 284 ns per loop

In [71]: timeit -n 10000000  "%s %s %s", arg1, arg2, arg3
10000000 loops, best of 3: 119 ns per loop

кажется, дает второй подход к краю.

Ответ 3

Избегая форматирования строки в строке, сохраняется некоторое время, если текущий уровень ведения журнала фильтрует сообщение журнала (как я ожидал) - но не много:

In [1]: import logging

In [2]: logger = logging.getLogger('foo')

In [3]: logger.setLevel(logging.ERROR)

In [4]: %timeit -n 1000000 logger.warn('%s %s %s' % ('a', 'b', 'c'))
1000000 loops, best of 3: 1.09 us per loop

In [12]: %timeit -n 1000000 logger.warn('%s %s %s', 'a', 'b', 'c')
1000000 loops, best of 3: 946 ns per loop

Итак, поскольку user1202136 указал, общая разница в производительности зависит от того, сколько времени требуется для форматирования строки (что может быть значительным в зависимости от стоимости вызова __str__ по аргументам, переданным функции регистрации.)