【问题标题】:timeit versus timing decoratortimeit 与计时装饰器
【发布时间】:2010-12-10 00:36:57
【问题描述】:

我正在尝试为一些代码计时。首先我使用了一个计时装饰器:

#!/usr/bin/env python

import time
from itertools import izip
from random import shuffle

def timing_val(func):
    def wrapper(*arg, **kw):
        '''source: http://www.daniweb.com/code/snippet368.html'''
        t1 = time.time()
        res = func(*arg, **kw)
        t2 = time.time()
        return (t2 - t1), res, func.__name__
    return wrapper

@timing_val
def time_izip(alist, n):
    i = iter(alist)
    return [x for x in izip(*[i] * n)]

@timing_val
def time_indexing(alist, n):
    return [alist[i:i + n] for i in range(0, len(alist), n)]

func_list = [locals()[key] for key in locals().keys()
             if callable(locals()[key]) and key.startswith('time')]
shuffle(func_list)  # Shuffle, just in case the order matters

alist = range(1000000)
times = []
for f in func_list:
    times.append(f(alist, 31))

times.sort(key=lambda x: x[0])
for (time, result, func_name) in times:
    print '%s took %0.3fms.' % (func_name, time * 1000.)

产量

% test.py
time_indexing took 73.230ms.
time_izip took 122.057ms.

这里我使用timeit:

%  python - m timeit - s '' 'alist=range(1000000);[alist[i:i+31] for i in range(0, len(alist), 31)]'
10 loops, best of 3:
    64 msec per loop
% python - m timeit - s 'from itertools import izip' 'alist=range(1000000);i=iter(alist);[x for x in izip(*[i]*31)]'
10 loops, best of 3:
    66.5 msec per loop

使用 timeit 的结果几乎相同,但使用计时装饰器,time_indexing 似乎比 time_izip 快。

造成这种差异的原因是什么?

应该相信任何一种方法吗?

如果有,是哪个?

【问题讨论】:

  • 顺便说一下,func_name 属性不能可靠地用于方法,只能用于函数,因此__name__ 可能更可取。试试time.sleep
  • @A-B-B:感谢指正。

标签: python timing timeit


【解决方案1】:

使用 functools 的包装来改进 Matt Alcock 的答案。

from functools import wraps
from time import time

def timing(f):
    @wraps(f)
    def wrap(*args, **kw):
        ts = time()
        result = f(*args, **kw)
        te = time()
        print 'func:%r args:[%r, %r] took: %2.4f sec' % \
          (f.__name__, args, kw, te-ts)
        return result
    return wrap

举个例子:

@timing
def f(a):
    for _ in range(a):
        i = 0
    return -1

调用方法f@timing包裹:

func:'f' args:[(100000000,), {}] took: 14.2240 sec
f(100000000)

这样做的好处是保留了原函数的属性;也就是说,函数名称和文档字符串等元数据正确地保留在返回的函数中。

【讨论】:

  • 我不明白这里添加了哪些额外的功能包装。你能扩展一下吗?
  • wraps 将有关内部函数的元数据复制到外部函数。没有它,装饰函数对象将引用包装器而不是内部函数。这只是在使用使用自省的工具(例如调试器)时才会出现的问题。例如,如果我们在没有wraps 的修饰函数上调用help,则帮助将在装饰器而不是内部函数上。
  • 但是,在测量时序的情况下,不会有真正的问题,所以我不明白为什么这应该适用于这种情况。包装器不会成为任何生产代码的一部分。
  • @skjerns 我认为您不能假设计时装饰器不会成为任何生产代码的一部分。在生产中记录仪器数据绝对是一回事。由于遵循此最佳实践并没有任何缺点,我认为最好默认使用wraps
  • 如果您使用的是 Python 3.6 或更高版本,您可以将 print 替换为类似于 print(f'Function {f.__name__} took {te-ts:2.4f} seconds') 的内容
【解决方案2】:

我会使用时序装饰器,因为您可以使用注释在代码周围散布时序,而不是让您的代码与时序逻辑混乱。

import time

def timeit(f):

    def timed(*args, **kw):

        ts = time.time()
        result = f(*args, **kw)
        te = time.time()

        print 'func:%r args:[%r, %r] took: %2.4f sec' % \
          (f.__name__, args, kw, te-ts)
        return result

    return timed

无论是使用注解,还是使用装饰器都很容易。

@timeit
def compute_magic(n):
     #function definition
     #....

或者为你想要计时的函数重新命名。

compute_magic = timeit(compute_magic)

【讨论】:

  • 我相信在这里使用 functools.wraps 会是一个小小的改进
  • 出于好奇,这个答案是从这里复制过来的吗?:andreas-jung.com/contents/…
  • 我认为在 Stackoverlow 的不同位置引用代码是完全可以的。我认为@emschorsch 没有任何问题
【解决方案3】:

使用时间。多次运行测试会给我带来更好的结果。

func_list=[locals()[key] for key in locals().keys() 
           if callable(locals()[key]) and key.startswith('time')]

alist=range(1000000)
times=[]
for f in func_list:
    n = 10
    times.append( min(  t for t,_,_ in (f(alist,31) for i in range(n)))) 

for (time,func_name) in zip(times, func_list):
    print '%s took %0.3fms.' % (func_name, time*1000.)

->

<function wrapper at 0x01FCB5F0> took 39.000ms.
<function wrapper at 0x01FCB670> took 41.000ms.

【讨论】:

  • 是的,这似乎是原因。谢谢!
  • 仅供参考,timeit 还会在测试期间禁用垃圾收集。这可能是另一个问题。
【解决方案4】:

我厌倦了from __main__ import foo,现在使用它 -- 用于简单的 args,%r 对它有效, 而不是在 Ipython 中。
(为什么timeit 仅适用于字符串,而不适用于 thunk / 闭包,即 timefunc( f, 任意 args ) ?)


import timeit

def timef( funcname, *args, **kwargs ):
    """ timeit a func with args, e.g.
            for window in ( 3, 31, 63, 127, 255 ):
                timef( "filter", window, 0 )
    This doesn't work in ipython;
    see Martelli, "ipython plays weird tricks with __main__" in Stackoverflow        
    """
    argstr = ", ".join([ "%r" % a for a in args]) if args  else ""
    kwargstr = ", ".join([ "%s=%r" % (k,v) for k,v in kwargs.items()]) \
        if kwargs  else ""
    comma = ", " if (argstr and kwargstr)  else ""
    fargs = "%s(%s%s%s)" % (funcname, argstr, comma, kwargstr)
        # print "test timef:", fargs
    t = timeit.Timer( fargs, "from __main__ import %s" % funcname )
    ntime = 3
    print "%.0f usec %s" % (t.timeit( ntime ) * 1e6 / ntime, fargs)

#...............................................................................
if __name__ == "__main__":
    def f( *args, **kwargs ):
        pass

    try:
        from __main__ import f
    except:
        print "ipython plays weird tricks with __main__, timef won't work"
    timef( "f")
    timef( "f", 1 )
    timef( "f", """ a b """ )
    timef( "f", 1, 2 )
    timef( "f", x=3 )
    timef( "f", x=3 )
    timef( "f", 1, 2, x=3, y=4 )

添加:另见“ipython 用 ma​​in 玩奇怪的把戏”,Martelli 在running-doctests-through-ipython

【讨论】:

  • 谢谢!这无疑使将函数放入 timeit 变得更容易。如果你使用fargs='%s(*%s,**%s)'%(funcname,args,kwargs),你可以省略argstr,kwargstr,comma,但也许它会让fargs更难阅读。
【解决方案5】:

受 Micah Smith 回答的启发,我直接制作了 funcy print(而不使用日志模块)。

以下方便在google colab使用。

# pip install funcy
from funcy import print_durations

@print_durations()
def myfunc(n=0):
  for i in range(n):
    pass

myfunc(123)
myfunc(123456789)

# 5.48 mks in myfunc(123)
# 3.37 s in myfunc(123456789)

【讨论】:

    【解决方案6】:

    这是您祈祷图书馆提供可移植解决方案的需求类型 - DRY!幸运的是funcy.log_durations 给出了答案。

    从文档中复制的示例:

    @log_durations(logging.info)
    def do_hard_work(n):
        samples = range(n)
        # ...
    
    # 121 ms in do_hard_work(10)
    # 143 ms in do_hard_work(11)
    # ...
    

    浏览 funcy 文档以了解其他变体,例如不同的关键字参数和 @log_iter_durations

    【讨论】:

      【解决方案7】:

      只是一个猜测,但差异可能是 range() 值差异的数量级吗?

      来自您的原始来源:

      alist=range(1000000)
      

      来自您的timeit 示例:

      alist=range(100000)
      

      对于它的价值,以下是我系统上的结果,范围设置为 100 万:

      $ python -V
      Python 2.6.4rc2
      
      $ python -m timeit -s 'from itertools import izip' 'alist=range(1000000);i=iter(alist);[x for x in izip(*[i]*31)]'
      10 loops, best of 3: 69.6 msec per loop
      
      $ python -m timeit -s '' 'alist=range(1000000);[alist[i:i+31] for i in range(0, len(alist), 31)]'
      10 loops, best of 3: 67.6 msec per loop
      

      我无法让您的其他代码运行,因为我无法在我的系统上导入“装饰器”模块。


      更新 - 当我在不涉及装饰器的情况下运行您的代码时,我看到了与您相同的差异。

      $ ./test.py
      time_indexing took 84.846ms.
      time_izip took 132.574ms.
      

      感谢您发布此问题;我今天学到了一些东西。 =)

      【讨论】:

      • 我删除了装饰器模块,这样我的代码更容易运行。你会试一试吗?运行脚本时,您是否发现速度有明显差异?此外,我将范围从 10^5 --> 10^6 更改,因此比较更加平等。谢谢。
      • 已更新,物有所值,但您现在似乎得到了答案。没有问题。
      【解决方案8】:

      不管这个特定的练习如何,我认为使用timeit 是更安全可靠的选择。与您的解决方案不同,它也是跨平台的。

      【讨论】:

      • 这不是跨平台的怎么办?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-09-04
      • 2013-02-06
      • 1970-01-01
      • 2011-10-04
      • 2011-09-18
      • 2011-04-12
      • 2019-02-23
      相关资源
      最近更新 更多