web-dev-qa-db-fra.com

Évaluation de la chaîne de messages de l'enregistreur paresseux

J'utilise le module de journalisation Python standard dans mon application Python:

 importation de la journalisation 
 logging.basicConfig (niveau = logging.INFO) 
 logger = logging.getLogger ("log") 
 tant que True: 
 logger.debug ('Message de journal stupide "+' '.join ([str (i) pour i dans la plage (20)])) 
 # Faites quelque chose 

Le problème est que, bien que le niveau de débogage ne soit pas activé, ce message de journal stupide est évalué à chaque itération de boucle, ce qui nuit gravement aux performances.

Existe-t-il une solution à ça?

En C++, nous avons le paquet log4cxx qui fournit des macros comme celles-ci:
LOG4CXX_DEBUG(logger, messasage) 
Qui évalue efficacement à 

 if (log4cxx :: debugEnabled (enregistreur)) {
 log4cxx.log (enregistreur, log4cxx :: LOG4CXX_DEBUG, message) 
} 

Mais puisqu'il n'y a pas de macros en Python (autant que je sache), s'il existe un moyen efficace de se connecter?

58
Zaar Hai

Le module de journalisation prend déjà en charge partiellement ce que vous voulez faire. Faire ceci:

log.debug("Some message: a=%s b=%s", a, b)

... au lieu de cela:

log.debug("Some message: a=%s b=%s" % (a, b))

Le module de journalisation est suffisamment intelligent pour ne pas produire le message de journal complet à moins que le message ne soit réellement consigné quelque part.

Pour appliquer cette fonctionnalité à votre demande spécifique, vous pouvez créer une classe de lazyjoin.

class lazyjoin:
    def __init__(self, s, items):
        self.s = s
        self.items = items
    def __str__(self):
        return self.s.join(self.items)

Utilisez-le comme ceci (notez l'utilisation d'une expression génératrice, ajoutant à la paresse):

logger.info('Stupid log message %s', lazyjoin(' ', (str(i) for i in range(20))))

Voici une démo qui montre que cela fonctionne.

>>> import logging
>>> logging.basicConfig(level=logging.INFO)
>>> logger = logging.getLogger("log")
>>> class DoNotStr:
...     def __str__(self):
...         raise AssertionError("the code should not have called this")
... 
>>> logger.info('Message %s', DoNotStr())
Traceback (most recent call last):
...
AssertionError: the code should not have called this
>>> logger.debug('Message %s', DoNotStr())
>>>

Dans la démo, l'appel logger.info () a rencontré l'erreur d'assertion, alors que logger.debug () n'a pas été aussi loin.

67
Shane Hathaway

Bien entendu, ce qui suit n’est pas aussi efficace qu’une macro:

if logger.isEnabledFor(logging.DEBUG):
    logger.debug(
        'Stupid log message ' + ' '.join([str(i) for i in range(20)])
    )

mais simple, évalue paresseusement et est 4 fois plus rapide que la réponse acceptée :

class lazyjoin:
    def __init__(self, s, items):
        self.s = s
        self.items = items

    def __str__(self):
        return self.s.join(self.items)

logger.debug(
    'Stupid log message %s', lazyjoin(' ', (str(i) for i in range(20)))
)

Voir benchmark-src pour ma configuration.

34
schnittstabil
import logging
import time

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("log")

class Lazy(object):
    def __init__(self,func):
        self.func=func
    def __str__(self):
        return self.func()

logger.debug(Lazy(lambda: time.sleep(20)))

logger.info(Lazy(lambda: "Stupid log message " + ' '.join([str(i) for i in range(20)])))
# INFO:log:Stupid log message 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

Si vous exécutez le script, vous remarquerez que l'exécution de la première commande logger.debug ne prend pas 20 secondes. Cela montre que l'argument n'est pas évalué lorsque le niveau de consignation est inférieur au niveau défini.

24
unutbu

Comme le souligne Shane, en utilisant

log.debug("Some message: a=%s b=%s", a, b)

... au lieu de cela:

log.debug("Some message: a=%s b=%s" % (a, b))

permet de gagner du temps en effectuant le formatage de la chaîne uniquement si le message est réellement enregistré.

Cela ne résout toutefois pas complètement le problème, car vous devrez peut-être prétraiter les valeurs à formater dans la chaîne, telles que:

log.debug("Some message: a=%s b=%s", foo.get_a(), foo.get_b())

Dans ce cas, obj.get_a() et obj.get_b() seront calculés même si aucune journalisation ne se produit.

Une solution à ce problème consisterait à utiliser les fonctions lambda, mais cela nécessiterait des machines supplémentaires:

class lazy_log_debug(object):
    def __init__(self, func):
        self.func = func
        logging.debug("%s", self)
    def __str__(self):
        return self.func()

... alors vous pouvez vous connecter avec les éléments suivants:

lazy_log_debug(lambda: "Some message: a=%s b=%s" % (foo.get_a(), foo.get_b()))

Dans ce cas, la fonction lambda sera seulement appelée - si log.debug décide d'effectuer le formatage, appelant ainsi la méthode __str__.

Attention, les frais généraux de cette solution peuvent très bien dépasser les avantages :-) Mais au moins en théorie, cela permet de réaliser une journalisation parfaitement paresseuse.

13
Pierre-Antoine

Je présente, Lazyfy:

class Lazyfy(object):
    __slots__ = 'action', 'value'

    def __init__(self, action, *value):
        self.action = action
        self.value = value

    def __str__(self):
        return self.action(*self.value)

Usage:

from pprint import pformat
log.debug("big_result: %s", Lazyfy(pformat, big_result))
log.debug( "x y z: %s", Lazyfy( lambda x, y, z: ' ,'.join( [x, y, z] ), '1', '2', '3' ) )

L'exemple original:

logger.info('Stupid log message %s', Lazyfy(lambda: ' '.join((str(i) for i in range(20)))))

Comme vous le voyez, cela couvre également l'autre réponse qui utilise la fonction lambda, mais utilise davantage de mémoire avec l'attribut value et l'extension. Cependant, cela économise plus de mémoire avec: Utilisation de __slots__?

Enfin, de loin, la solution la plus efficace reste la suivante, suggérant une autre réponse:

if logger.isEnabledFor(logging.DEBUG): 
    logger.debug('Stupid log message ' + ' '.join([str(i) for i in range(20)]))
0
user

Si vous ne dépendez que de l'accès aux attributs d'état globaux, vous pouvez instancier une classe python et lazifier à l'aide de la méthode __str__:

class get_lazy_debug(object):
    def __repr__(self):
        return ' '.join(
                str(i) for i in range(20)
            )

# Allows to pass get_lazy_debug as a function parameter without 
# evaluating/creating its string!
get_lazy_debug = get_lazy_debug()

logger.debug( 'Stupid log message', get_lazy_debug )

En relation:

  1. instructions de débogage évaluées conditionnellement en Python
  2. Que sont les métaclasses en Python?
0
user