如何在不对其进行注释的情况下剥离Python日志记录调用?

时间:2009-02-07 00:08:12

标签: python optimization logging bytecode

今天我正在考虑一年前我写的一个Python项目,其中我使用logging非常广泛。我记得因为开销(hotshot表明它是我最大的瓶颈之一)而不得不在类似内循环的场景(90%代码)中注释掉大量的日志记录调用。

我现在想知道是否有一些规范的方法可以在Python应用程序中以编程方式去除日志记录调用,而无需一直注释和取消注释。我认为您可以使用检查/重新编译或字节码操作来执行此类操作,并且仅针对导致瓶颈的代码对象。这样,您可以添加操纵器作为后编译步骤并使用集中配置文件,如下所示:

[Leave ERROR and above]
my_module.SomeClass.method_with_lots_of_warn_calls

[Leave WARN and above]
my_module.SomeOtherClass.method_with_lots_of_info_calls

[Leave INFO and above]
my_module.SomeWeirdClass.method_with_lots_of_debug_calls

当然,您希望谨慎使用它,并且可能需要使用每个函数的粒度 - 仅适用于已将logging作为瓶颈的代码对象。有人知道这样的事吗?

注意:由于动态类型和后期绑定,有一些因素使性能更难以完成。例如,对名为debug的方法的任何调用都可能必须用if not isinstance(log, Logger)包装。在任何情况下,我都假设所有的细节都可以通过绅士的协议或一些运行时检查来克服。 : - )

10 个答案:

答案 0 :(得分:20)

如何使用logging.disable

如果日志消息的创建成本很高,我还发现必须使用logging.isEnabledFor

答案 1 :(得分:5)

我也看到断言以这种方式使用。

assert logging.warn('disable me with the -O option') is None

(我猜警告总是不返回..如果没有,你会得到一个AssertionError

但实际上,这只是一种有趣的方式:

if __debug__: logging.warn('disable me with the -O option')

当您使用-O选项运行其中包含该行的脚本时,该行将从优化的.pyo代码中删除。相反,如果您有自己的变量,就像下面一样,您将拥有一个始终执行的条件(无论变量是什么值),尽管条件应该比函数调用执行得更快:

my_debug = True
...
if my_debug: logging.warn('disable me by setting my_debug = False')

所以,如果我对 debug 的理解是正确的,那么它似乎是摆脱不必要的日志记录调用的好方法。另一方面是它还会禁用所有断言,因此如果需要断言则会出现问题。

答案 2 :(得分:5)

使用pypreprocessor

也可以在PYPI (Python Package Index)找到,并使用pip获取。

以下是一个基本用法示例:

from pypreprocessor import pypreprocessor

pypreprocessor.parse()

#define nologging

#ifdef nologging
...logging code you'd usually comment out manually...
#endif

基本上,预处理器会按照您之前手动执行的方式注释掉代码。它只是根据您定义的条件有条不紊地进行。

您还可以通过在导入和导入之间添加'pypreprocessor.removeMeta = True'来删除所有预处理程序指令并从后处理代码中注释掉代码。 parse()语句。

字节码输出(.pyc)文件将包含优化输出。

SideNote:pypreprocessor与python2x和python3k兼容。

免责声明:我是pypreprocessor的作者。

答案 3 :(得分:2)

作为一个不完美的快捷方式,如何使用MiniMock之类的东西模拟特定模块中的logging

例如,如果my_module.py是:

import logging
class C(object):
    def __init__(self, *args, **kw):
        logging.info("Instantiating")

您可以将my_module替换为:

from minimock import Mock
import my_module
my_module.logging = Mock('logging')
c = my_module.C()

在初始导入模块之前,您只需要执行一次。

通过模拟特定方法,或者让logging.getLogger返回一个模拟对象,其中某些方法无能为力,其他方法委托给真正的logging模块,获取特定于级别的行为就足够了。

在实践中,您可能希望用更简单,更快速的东西替换MiniMock;至少没有打印到stdout使用的东西!当然,这不能解决模块A从模块B导入logging的问题(因此A也导入B的日志粒度)...

这根本不会像没有运行日志语句那么快,但是应该比一直进入日志记录模块的深度要快得多,只是为了发现这条记录毕竟不应该被记录。

答案 4 :(得分:1)

您可以尝试这样的事情:

# Create something that accepts anything
class Fake(object):
    def __getattr__(self, key):
        return self
    def __call__(self, *args, **kwargs):
        return True

# Replace the logging module
import sys
sys.modules["logging"] = Fake()

它实际上用Fake的实例取代(或最初填充)日志记录模块的空间,它只是简单地接受任何内容。在尝试在任何地方使用日志记录模块之前,您必须运行上面的代码(只需一次!)。 以下是测试:

import logging

logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(levelname)-8s %(message)s',
                    datefmt='%a, %d %b %Y %H:%M:%S',
                    filename='/temp/myapp.log',
                    filemode='w')
logging.debug('A debug message')
logging.info('Some information')
logging.warning('A shot across the bows')

如上所述,没有任何记录,正如预期的那样。

答案 5 :(得分:1)

我会使用一些奇特的日志记录装饰器,或者其中一些:

def doLogging(logTreshold):
    def logFunction(aFunc):
        def innerFunc(*args, **kwargs):
            if LOGLEVEL >= logTreshold:
                print ">>Called %s at %s"%(aFunc.__name__, time.strftime("%H:%M:%S"))
                print ">>Parameters: ", args, kwargs if kwargs else "" 
            try:
                return aFunc(*args, **kwargs)
            finally:
                print ">>%s took %s"%(aFunc.__name__, time.strftime("%H:%M:%S"))
        return innerFunc
    return logFunction

你需要的只是在每个模块中声明LOGLEVEL常量(或者只是全局并在所有模块中导入它)然后你可以像这样使用它:

@doLogging(2.5)
def myPreciousFunction(one, two, three=4):
    print "I'm doing some fancy computations :-)"
    return

如果LOGLEVEL不低于2.5,你将获得如下输出:

>>Called myPreciousFunction at 18:49:13
>>Parameters:  (1, 2) 
I'm doing some fancy computations :-)
>>myPreciousFunction took 18:49:13

正如您所看到的,需要做一些工作才能更好地处理kwargs,因此如果存在默认值,则会打印出来,但这是另一个问题。

您应该使用一些 logger 模块而不是原始的 print 语句,但我想专注于装饰者的想法并避免制作代码太长了。

无论如何 - 使用这样的装饰器,您可以获得功能级别的日志记录,任意多个日志级别,易于应用于新功能,以及禁用日志记录您只需要设置LOGLEVEL。如果您愿意,您可以为每个功能定义不同的输出流/文件。您可以将doLogging写为:

 def doLogging(logThreshold, outStream=sys.stdout):
      .....
      print >>outStream, ">>Called %s at %s" etc.

并利用基于每个功能定义的日志文件。

答案 6 :(得分:1)

这也是我的项目中的一个问题 - 日志记录最终会在探查器报告中得到一致。

我之前在PyFlakes(http://github.com/kevinw/pyflakes)的一个分支中使用了_ast模块......并且你可以在你的问题中做你建议的事情 - 在调用日志之前检查并注入警卫方法(你公认的警告,你必须做一些运行时类型检查)。有关简单示例,请参阅http://pyside.blogspot.com/2008/03/ast-compilation-from-python.html

编辑:我刚刚注意到我的planetpython.org Feed上的MetaPython - 示例用例是在导入时删除日志语句。

也许最好的解决方案是让某人重新实现日志作为C模块,但我不会是第一个跳到这样的机会:p

答案 7 :(得分:1)

:-)我们曾经称之为预处理器,虽然C的预处理器具有一些功能,但“山丘之王”是IBM大型机PL / I的预处理器。它在预处理器中提供了广泛的语言支持(完整赋值,条件,循环等),并且可以使用PL / I PP编写“编写程序的程序”。

我用完整的复杂程序和数据跟踪编写了许多应用程序(我们当时没有一个适合后端进程的调试器)用于开发和测试,然后在编译时使用相应的“运行时标志“简单地删除所有跟踪代码,没有任何性能影响。

我认为装饰者的想法很好。您可以编写装饰器来包装需要记录的函数。然后,对于运行时分发,装饰器变为“无操作”,这消除了调试语句。

Jon R

答案 8 :(得分:1)

我正在做一个项目,目前使用广泛的日志记录来测试使用Pandas库的数据分析API的逻辑和执行时间。

我发现这个字符串有类似的问题 - 例如即使logging.basicConfig级别设置为level = logging.WARNING

,logging.debug语句的开销是多少?

在部署之前,我已经编写了以下脚本来注释掉或取消注释调试日志记录:

import os
import fileinput

comment = True

# exclude files or directories matching string
fil_dir_exclude = ["__","_archive",".pyc"]

if comment :
    ## Variables to comment
    source_str = 'logging.debug'
    replace_str = '#logging.debug'
else :
    ## Variables to uncomment
    source_str = '#logging.debug'
    replace_str = 'logging.debug'

# walk through directories
for root, dirs, files in os.walk('root/directory') :
    # where files exist
    if files:
        # for each file
        for file_single in files :
            # build full file name
            file_name = os.path.join(root,file_single)
            # exclude files with matching string
            if not any(exclude_str in file_name for exclude_str in fil_dir_exclude) :
                # replace string in line
                for line in fileinput.input(file_name, inplace=True):
                    print "%s" % (line.replace(source_str, replace_str)),

这是一个文件递归,它根据条件列表排除文件,并根据此处的答案执行就地替换:Search and replace a line in a file in Python

答案 9 :(得分:0)

我喜欢'if __debug_'解决方案,除了将它放在每次通话前都有点分散注意力和丑陋。我遇到了同样的问题并通过编写一个自动解析源文件的脚本并用pass语句替换日志语句(并注释掉日志语句的副本)来克服它。它也可以撤消此转换。

当我将新代码部署到生产环境时,当我在生产环境中不需要许多日志记录语句并影响性能时,我会使用它。

您可以在此处找到该脚本:http://dound.com/2010/02/python-logging-performance/