Registro de Python idiomático: cadena de formato + lista de argumentos frente a formato de cadena en línea, ¿cuál es el preferido?

¿Es ventajoso llamar a las funciones de registro con cadena de formato + lista de argumentos en lugar de formato en línea?

He visto (y escrito) el código de registro que utiliza el formato de cadena en línea:

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

y, sin embargo, asumo que es mejor (en cuanto a rendimiento y más idiomático) usar:

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

porque la segunda forma evita las operaciones de formato de cadena antes de invocar la función de registro. Si el nivel de registro actual filtraría el mensaje de registro, no es necesario formatear, reduciendo el tiempo de cálculo y las asignaciones de memoria.

¿Estoy en el camino correcto aquí, o me he perdido algo?

En mi humilde opinión, para los mensajes que es muy probable que se muestren, como los que se le dan al error o que warn , no hay mucha diferencia.

Para los mensajes que son menos probables, definitivamente me quedaría con la segunda versión, principalmente por razones de rendimiento. A menudo doy objetos grandes como parámetro a la info , que implementan un método __str__ costoso. Claramente, enviar este pre-formato a la info sería un desperdicio de rendimiento.

ACTUALIZAR

Acabo de verificar el código fuente del módulo de logging y, de hecho, el formateo se realiza después de verificar el nivel de registro. Por ejemplo:

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

Se puede observar que msg y args no se tocan entre el log llamadas y la comprobación del nivel de registro.

ACTUALIZACIÓN 2

Spired by Levon, permítame agregar algunas pruebas para los objetos que tienen un método __str__ costoso:

 $ 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 

En la práctica, esto podría dar un aumento de rendimiento bastante alto.

En caso de que esto sea útil, aquí hay una prueba de sincronización rápida para solo las dos opciones de formato:

 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 

Parece que la segunda aproximación al borde.

Evitar el formato de cadena en línea ahorra tiempo si el nivel de registro actual filtra el mensaje de registro (como esperaba), pero no mucho:

 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 

Entonces, como señaló el usuario1202136 , la diferencia de rendimiento general depende de cuánto tiempo se tarda en formatear la cadena (lo que podría ser significativo dependiendo del costo de llamar a __str__ en los argumentos que se pasan a la función de registro).