重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
就是告诉你有错误。你for之前那个clock()是什么?哪里来的?
10年积累的成都网站设计、网站建设经验,可以快速应对客户对网站的新想法和需求。提供各种问题对应的解决方案。让选择我们的客户得到更好、更有力的网络服务。我虽然不认识你,你也不认识我。但先网站设计后付款的网站建设流程,更有桃源免费网站建设让你可以放心的选择与我们合作。
就算你把它删除掉,最后那个print里用的clock又怎么办?
如果你想使用time.clock()做计时:
你需要在文件开始引入这个包里的clock:
from time import clock
然后,在开始计时的地方保存clock()的值:
t = clock()
在结束计时的地方计算
clock() - t
即为用时。
如果要使用time.perf_counter()做计时:
修改相应的import语句为
from time import perf_counter as clock
其它代码不需要修改即可使用。
但有一点需要注意的是,perf_counter自第一次引用后,它就开始计时,之后无论调用多少次,它都是返回当前时间到开始计时的时间差,这会产生一个问题:
如果在其它模块中导入了它到全局范围,那么,所有模块中使用此函数的将都使用同一个计时器,这会让使用它得到的时长超出期望。
一般情况下,这是用来测试一段程序的用时的,应当避免使用影响到其它环境或受其它环境影响的方式,所以建议你使用第一种方式去处理这个问题
Julia 与 Python
的比较
我是否应丢弃 Python 和其他语言,使用 Julia 执行技术计算?在看到 上的基准测试后,人们一定会这么想。Python
和其他高级语言在速度上远远有些落后。但是,我想到的第一个问题有所不同:Julia 团队能否以最适合 Python 的方式编写 Python 基准测试?
我对这种跨语言比较的观点是,应该根据要执行的任务来定义基准测试,然后由语言专家编写执行这些任务的最佳代码。如果代码全由一个语言团队编写,则存在其他语言未得到最佳使用的风险。
Julia 团队有一件事做得对,那就是他们将他们使用的代码发布到了 github 上。具体地讲,Python 代码可在此处找到。
第一眼看到该代码,就可以证实我所害怕的偏见。该代码是以 C 风格编写的,在数组和列表上大量使用了循环。这不是使用 Python 的最佳方式。
我不会责怪 Julia 团队,因为我很内疚自己也有同样的偏见。但我受到了残酷的教训:付出任何代价都要避免数组或列表上的循环,因为它们确实会拖慢 Python
中的速度,请参阅 Python 不是 C。
考虑到对 C 风格的这种偏见,一个有趣的问题(至少对我而言)是,我们能否改进这些基准测试,更好地使用 Python 及其工具?
在我给出答案之前,我想说我绝不会试图贬低 Julia。在进一步开发和改进后,Julia 无疑是一种值得关注的语言。我只是想分析 Python
方面的事情。实际上,我正在以此为借口来探索各种可用于让代码更快运行的 Python 工具。
在下面的内容中,我使用 Docker 镜像在 Jupyter Notebook 中使用 Python 3.4.3,其中已安装了所有的 Python 科学工具组合。我还会通过
Windows 机器上的 Python 2.7.10,使用 Anaconda 来运行代码。计时是对 Python 3.4.3 执行的。包含下面的所有基准测试的完整代码的 Notebook 可在此处找到。
鉴于各种社交媒体上的评论,我添加了这样一句话:我没有在这里使用 Python 的替代性实现。我没有编写任何 C
代码:如果您不信,可试试寻找分号。本文中使用的所有工具都是 Anaconda 或其他发行版中提供的标准的 Cython 实现。下面的所有代码都在单个 Notebook中运行。
我尝试过使用来自 github 的 Julia 微性能文件,但不能使用 Julia 0.4.2 原封不动地运行它。我必须编辑它并将 @timeit 替换为
@time,它才能运行。在对它们计时之前,我还必须添加对计时函数的调用,否则编译时间也将包含在内。我使用的文件位于此处。我在用于运行 Python 的同一个机器上使用 Julia 命令行接口运行它。
回页首
计时代码
Julia 团队使用的第一项基准测试是 Fibonacci 函数的一段简单编码。
def fib(n):
if n2:
return n
return fib(n-1)+fib(n-2)
此函数的值随 n 的增加而快速增加,例如:
fib(100) = 354224848179261915075
可以注意到,Python 任意精度 (arbitrary precision) 很方便。在 C 等语言中编写相同的函数需要花一些编码工作来避免整数溢出。在 Julia
中,需要使用 BigInt 类型。
所有 Julia 基准测试都与运行时间有关。这是 Julia 中使用和不使用 BigInt 的计时:
0.000080 seconds (149 allocations:10.167 KB)
0.012717 seconds (262.69 k allocations:4.342 MB)
在 Python Notebook 中获得运行时间的一种方式是使用神奇的 %timeit。例如,在一个新单元中键入:
%timeit fib(20)
执行它会获得输出:
100 loops, best of 3:3.33 ms per loop
这意味着计时器执行了以下操作:
运行 fib(20) 100 次,存储总运行时间
运行 fib(20) 100 次,存储总运行时间
运行 fib(20) 100 次,存储总运行时间
从 3 次运行中获取最小的运行时间,将它除以 100,然后输出结果,该结果就是 fib(20) 的最佳运行时间
这些循环的大小(100 次和 3 次)会由计时器自动调整。可能会根据被计时的代码的运行速度来更改循环大小。
Python 计时与使用了 BigInt 时的 Julia 计时相比出色得多:3 毫秒与 12 毫秒。在使用任意精度时,Python 的速度是 Julia 的 4
倍。
但是,Python 比 Julia 默认的 64 位整数要慢。我们看看如何在 Python 中强制使用 64 位整数。
回页首
使用 Cython 编译
一种编译方式是使用 Cython 编译器。这个编译器是使用 Python
编写的。它可以通过以下命令安装:
pip install Cython
如果使用 Anaconda,安装会有所不同。因为安装有点复杂,所以我编写了一篇相关的博客文章:将 Cython For Anaconda 安装在 Windows 上
安装后,我们使用神奇的 %load_ext 将 Cython 加载到 Notebook 中:
%load_ext Cython
然后就可以在我们的 Notebook 中编译代码。我们只需要将想要编译的代码放在一个单元中,包括所需的导入语句,使用神奇的 %%cython 启动该单元:
%%cython
def fib_cython(n):
if n2:
return n
return fib_cython(n-1)+fib_cython(n-2)
执行该单元会无缝地编译这段代码。我们为该函数使用一个稍微不同的名称,以反映出它是使用 Cython
编译的。当然,一般不需要这么做。我们可以将之前的函数替换为相同名称的已编译函数。
对它计时会得到:
1000 loops, best of 3:1.22 ms per loop
哇,几乎比最初的 Python 代码快 3 倍!我们现在比使用 BigInt 的 Julia 快 100 倍。
我们还可以尝试静态类型。使用关键字 cpdef 而不是 def 来声明该函数。它使我们能够使用相应的 C 类型来键入函数的参数。我们的代码变成了:
%%cython
cpdef long fib_cython_type(long n):
if n2:
return n
return fib_cython_type(n-1)+fib_cython_type(n-2)
执行该单元后,对它计时会得到:
10000 loops, best of 3:36 µs per loop
太棒了,我们现在只花费了 36 微秒,比最初的基准测试快约 100 倍!这与 Julia 所花的 80 毫秒相比更出色。
有人可能会说,静态类型违背了 Python
的用途。一般来讲,我比较同意这种说法,我们稍后将查看一种在不牺牲性能的情况下避免这种情形的方法。但我并不认为这是一个问题。Fibonacci
函数必须使用整数来调用。我们在静态类型中失去的是 Python 所提供的任意精度。对于 Fibonacci,使用 C 类型 long
会限制输入参数的大小,因为太大的参数会导致整数溢出。
请注意,Julia 计算也是使用 64 位整数执行的,因此将我们的静态类型版本与 Julia 的对比是公平的。
回页首
缓存计算
我们在保留 Python 任意精度的情况下能做得更好。fib 函数重复执行同一种计算许多次。例如,fib(20) 将调用 fib(19) 和
fib(18)。fib(19) 将调用 fib(18) 和 fib(17)。结果 fib(18) 被调用了两次。简单分析表明,fib(17) 将被调用 3
次,fib(16) 将被调用 5 次,等等。
在 Python 3 中,我们可以使用 functools 标准库来避免这些重复的计算。
from functools import lru_cache as cache
@cache(maxsize=None)
def fib_cache(n):
if n2:
return n
return fib_cache(n-1)+fib_cache(n-2)
对此函数计时会得到:
1000000 loops, best of 3:910 ns per loop
速度又增加了 40 倍,比最初的 Python 代码快约 3,600 倍!考虑到我们仅向递归函数添加了一条注释,此结果非常令人难忘。
Python 2.7 中没有提供这种自动缓存。我们需要显式地转换代码,才能避免这种情况下的重复计算。
def fib_seq(n):
if n 2:
return n
a,b = 1,0
for i in range(n-1):
a,b = a+b,a
return a
请注意,此代码使用了 Python 同时分配两个局部变量的能力。对它计时会得到:
1000000 loops, best of 3:1.77 µs per loop
我们又快了 20 倍!让我们在使用和不使用静态类型的情况下编译我们的函数。请注意,我们使用了 cdef 关键字来键入局部变量。
%%cython
def fib_seq_cython(n):
if n 2:
return n
a,b = 1,0
for i in range(n-1):
a,b = a+b,a
return a
cpdef long fib_seq_cython_type(long n):
if n 2:
return n
cdef long a,b
a,b = 1,0
for i in range(n-1):
a,b = a+b,b
return a
我们可在一个单元中对两个版本计时:
%timeit fib_seq_cython(20)
%timeit fib_seq_cython_type(20)
结果为:
1000000 loops, best of 3:953 ns per loop
10000000 loops, best of 3:51.9 ns per loop
静态类型代码现在花费的时间为 51.9 纳秒,比最初的基准测试快约 60,000(六万)倍。
如果我们想计算任意输入的 Fibonacci 数,我们应坚持使用无类型版本,该版本的运行速度快 3,500 倍。还不错,对吧?
回页首
使用 Numba 编译
让我们使用另一个名为 Numba 的工具。它是针对部分 Python 版本的一个即时
(jit) 编译器。它不是对所有 Python 版本都适用,但在适用的情况下,它会带来奇迹。
安装它可能很麻烦。推荐使用像 Anaconda 这样的 Python 发行版或一个已安装了 Numba 的 Docker 镜像。完成安装后,我们导入它的 jit 编译器:
from numba import jit
它的使用非常简单。我们仅需要向想要编译的函数添加一点修饰。我们的代码变成了:
@jit
def fib_seq_numba(n):
if n 2:
return n
(a,b) = (1,0)
for i in range(n-1):
(a,b) = (a+b,a)
return a
对它计时会得到:
1000000 loops, best of 3:225 ns per loop
比无类型的 Cython 代码更快,比最初的 Python 代码快约 16,000 倍!
回页首
使用 Numpy
我们现在来看看第二项基准测试。它是快速排序算法的实现。Julia 团队使用了以下 Python 代码:
def qsort_kernel(a, lo, hi):
i = lo
j = hi
while i hi:
pivot = a[(lo+hi) // 2]
while i = j:
while a[i] pivot:
i += 1
while a[j] pivot:
j -= 1
if i = j:
a[i], a[j] = a[j], a[i]
i += 1
j -= 1
if lo j:
qsort_kernel(a, lo, j)
lo = i
j = hi
return a
我将他们的基准测试代码包装在一个函数中:
import random
def benchmark_qsort():
lst = [ random.random() for i in range(1,5000) ]
qsort_kernel(lst, 0, len(lst)-1)
对它计时会得到:
100 loops, best of 3:18.3 ms per loop
上述代码与 C 代码非常相似。Cython 应该能很好地处理它。除了使用 Cython 和静态类型之外,让我们使用 Numpy
数组代替列表。在数组大小较大时,比如数千个或更多元素,Numpy 数组确实比
Python 列表更快。
安装 Numpy 可能会花一些时间,推荐使用 Anaconda 或一个已安装了 Python 科学工具组合的 Docker 镜像。
在使用 Cython 时,需要将 Numpy 导入到应用了 Cython 的单元中。在使用 C 类型时,还必须使用 cimport 将它作为 C 模块导入。Numpy
数组使用一种表示数组元素类型和数组维数(一维、二维等)的特殊语法来声明。
%%cython
import numpy as np
cimport numpy as np
cpdef np.ndarray[double, ndim=1] \
qsort_kernel_cython_numpy_type(np.ndarray[double, ndim=1] a, \
long lo, \
long hi):
cdef:
long i, j
double pivot
i = lo
j = hi
while i hi:
pivot = a[(lo+hi) // 2]
while i = j:
while a[i] pivot:
i += 1
while a[j] pivot:
j -= 1
if i = j:
a[i], a[j] = a[j], a[i]
i += 1
j -= 1
if lo j:
qsort_kernel_cython_numpy_type(a, lo, j)
lo = i
j = hi
return a
cpdef benchmark_qsort_numpy_cython():
lst = np.random.rand(5000)
qsort_kernel_cython_numpy_type(lst, 0, len(lst)-1)
对 benchmark_qsort_numpy_cython() 函数计时会得到:
1000 loops, best of 3:1.32 ms per loop
我们比最初的基准测试快了约 15 倍,但这仍然不是使用 Python 的最佳方法。最佳方法是使用 Numpy 内置的 sort()
函数。它的默认行为是使用快速排序算法。对此代码计时:
def benchmark_sort_numpy():
lst = np.random.rand(5000)
np.sort(lst)
会得到:
1000 loops, best of 3:350 µs per loop
我们现在比最初的基准测试快 52 倍!Julia 在该基准测试上花费了 419 微秒,因此编译的 Python 快 20%。
我知道,一些读者会说我不会进行同类比较。我不同意。请记住,我们现在的任务是使用主机语言以最佳的方式排序输入数组。在这种情况下,最佳方法是使用一个内置的函数。
time.sleep在python3.11中替换为python。
INTRO:众所周知,time.sleep的准确率取决于操作系统和计算负载。 Windows 中的准确性非常差。
类似于 /questions/17499837一个方法可以使用 time.clock 实现忙等待方法作为 time.sleep 的替代方法.这种方法会造成不必要的负载,影响系统中的其他模 block 。这在进行模拟时是不可取的。
减少花在忙等待上的时间,而不是依赖 time.sleep , 一个类使用方法 select.select并利用超时属性。
使用time.time来统计函数的执行时间,程序只会执行一次,存在很大的随机因素。
timtit包就可以重复执行函数多次,然后将多次执行结果取平均值。相比起来更优。
然而程序执行时间很大程度还受计算机性能的影响,衡量程序好坏更靠谱的手段是计算时间复杂度。
理解Python中的装饰器
@makebold
@makeitalic
def say():
return "Hello"
打印出如下的输出:
biHelloi/b
你会怎么做?最后给出的答案是:
def makebold(fn):
def wrapped():
return "b" + fn() + "/b"
return wrapped
def makeitalic(fn):
def wrapped():
return "i" + fn() + "/i"
return wrapped
@makebold
@makeitalic
def hello():
return "hello world"
print hello() ## 返回 bihello world/i/b
现在我们来看看如何从一些最基础的方式来理解Python的装饰器。英文讨论参考Here。
装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
1.1. 需求是怎么来的?
装饰器的定义很是抽象,我们来看一个小例子。
def foo():
print 'in foo()'
foo()
这是一个很无聊的函数没错。但是突然有一个更无聊的人,我们称呼他为B君,说我想看看执行这个函数用了多长时间,好吧,那么我们可以这样做:
import time
def foo():
start = time.clock()
print 'in foo()'
end = time.clock()
print 'used:', end - start
foo()
很好,功能看起来无懈可击。可是蛋疼的B君此刻突然不想看这个函数了,他对另一个叫foo2的函数产生了更浓厚的兴趣。
怎么办呢?如果把以上新增加的代码复制到foo2里,这就犯了大忌了~复制什么的难道不是最讨厌了么!而且,如果B君继续看了其他的函数呢?
1.2. 以不变应万变,是变也
还记得吗,函数在Python中是一等公民,那么我们可以考虑重新定义一个函数timeit,将foo的引用传递给他,然后在timeit中调用foo并进行计时,这样,我们就达到了不改动foo定义的目的,而且,不论B君看了多少个函数,我们都不用去修改函数定义了!
import time
def foo():
print 'in foo()'
def timeit(func):
start = time.clock()
func()
end =time.clock()
print 'used:', end - start
timeit(foo)
看起来逻辑上并没有问题,一切都很美好并且运作正常!……等等,我们似乎修改了调用部分的代码。原本我们是这样调用的:foo(),修改以后变成了:timeit(foo)。这样的话,如果foo在N处都被调用了,你就不得不去修改这N处的代码。或者更极端的,考虑其中某处调用的代码无法修改这个情况,比如:这个函数是你交给别人使用的。
1.3. 最大限度地少改动!
既然如此,我们就来想想办法不修改调用的代码;如果不修改调用代码,也就意味着调用foo()需要产生调用timeit(foo)的效果。我们可以想到将timeit赋值给foo,但是timeit似乎带有一个参数……想办法把参数统一吧!如果timeit(foo)不是直接产生调用效果,而是返回一个与foo参数列表一致的函数的话……就很好办了,将timeit(foo)的返回值赋值给foo,然后,调用foo()的代码完全不用修改!
#-*- coding: UTF-8 -*-
import time
def foo():
print 'in foo()'
# 定义一个计时器,传入一个,并返回另一个附加了计时功能的方法
def timeit(func):
# 定义一个内嵌的包装函数,给传入的函数加上计时功能的包装
def wrapper():
start = time.clock()
func()
end =time.clock()
print 'used:', end - start
# 将包装后的函数返回
return wrapper
foo = timeit(foo)
foo()
这样,一个简易的计时器就做好了!我们只需要在定义foo以后调用foo之前,加上foo = timeit(foo),就可以达到计时的目的,这也就是装饰器的概念,看起来像是foo被timeit装饰了。在在这个例子中,函数进入和退出时需要计时,这被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。与传统编程习惯的从上往下执行方式相比较而言,像是在函数执行的流程中横向地插入了一段逻辑。在特定的业务领域里,能减少大量重复代码。面向切面编程还有相当多的术语,这里就不多做介绍,感兴趣的话可以去找找相关的资料。
这个例子仅用于演示,并没有考虑foo带有参数和有返回值的情况,完善它的重任就交给你了 :)
上面这段代码看起来似乎已经不能再精简了,Python于是提供了一个语法糖来降低字符输入量。
import time
def timeit(func):
def wrapper():
start = time.clock()
func()
end =time.clock()
print 'used:', end - start
return wrapper
@timeit
def foo():
print 'in foo()'
foo()
重点关注第11行的@timeit,在定义上加上这一行与另外写foo = timeit(foo)完全等价,千万不要以为@有另外的魔力。除了字符输入少了一些,还有一个额外的好处:这样看上去更有装饰器的感觉。
-------------------
要理解python的装饰器,我们首先必须明白在Python中函数也是被视为对象。这一点很重要。先看一个例子:
def shout(word="yes") :
return word.capitalize()+" !"
print shout()
# 输出 : 'Yes !'
# 作为一个对象,你可以把函数赋给任何其他对象变量
scream = shout
# 注意我们没有使用圆括号,因为我们不是在调用函数
# 我们把函数shout赋给scream,也就是说你可以通过scream调用shout
print scream()
# 输出 : 'Yes !'
# 还有,你可以删除旧的名字shout,但是你仍然可以通过scream来访问该函数
del shout
try :
print shout()
except NameError, e :
print e
#输出 : "name 'shout' is not defined"
print scream()
# 输出 : 'Yes !'
我们暂且把这个话题放旁边,我们先看看python另外一个很有意思的属性:可以在函数中定义函数:
def talk() :
# 你可以在talk中定义另外一个函数
def whisper(word="yes") :
return word.lower()+"...";
# ... 并且立马使用它
print whisper()
# 你每次调用'talk',定义在talk里面的whisper同样也会被调用
talk()
# 输出 :
# yes...
# 但是"whisper" 不会单独存在:
try :
print whisper()
except NameError, e :
print e
#输出 : "name 'whisper' is not defined"*
函数引用
从以上两个例子我们可以得出,函数既然作为一个对象,因此:
1. 其可以被赋给其他变量
2. 其可以被定义在另外一个函数内
这也就是说,函数可以返回一个函数,看下面的例子:
def getTalk(type="shout") :
# 我们定义另外一个函数
def shout(word="yes") :
return word.capitalize()+" !"
def whisper(word="yes") :
return word.lower()+"...";
# 然后我们返回其中一个
if type == "shout" :
# 我们没有使用(),因为我们不是在调用该函数
# 我们是在返回该函数
return shout
else :
return whisper
# 然后怎么使用呢 ?
# 把该函数赋予某个变量
talk = getTalk()
# 这里你可以看到talk其实是一个函数对象:
print talk
#输出 : function shout at 0xb7ea817c
# 该对象由函数返回的其中一个对象:
print talk()
# 或者你可以直接如下调用 :
print getTalk("whisper")()
#输出 : yes...
还有,既然可以返回一个函数,我们可以把它作为参数传递给函数:
def doSomethingBefore(func) :
print "I do something before then I call the function you gave me"
print func()
doSomethingBefore(scream)
#输出 :
#I do something before then I call the function you gave me
#Yes !
这里你已经足够能理解装饰器了,其他它可被视为封装器。也就是说,它能够让你在装饰前后执行代码而无须改变函数本身内容。
手工装饰
那么如何进行手动装饰呢?
# 装饰器是一个函数,而其参数为另外一个函数
def my_shiny_new_decorator(a_function_to_decorate) :
# 在内部定义了另外一个函数:一个封装器。
# 这个函数将原始函数进行封装,所以你可以在它之前或者之后执行一些代码
def the_wrapper_around_the_original_function() :
# 放一些你希望在真正函数执行前的一些代码
print "Before the function runs"
# 执行原始函数
a_function_to_decorate()
# 放一些你希望在原始函数执行后的一些代码
print "After the function runs"
#在此刻,"a_function_to_decrorate"还没有被执行,我们返回了创建的封装函数
#封装器包含了函数以及其前后执行的代码,其已经准备完毕
return the_wrapper_around_the_original_function
# 现在想象下,你创建了一个你永远也不远再次接触的函数
def a_stand_alone_function() :
print "I am a stand alone function, don't you dare modify me"
a_stand_alone_function()
#输出: I am a stand alone function, don't you dare modify me
# 好了,你可以封装它实现行为的扩展。可以简单的把它丢给装饰器
# 装饰器将动态地把它和你要的代码封装起来,并且返回一个新的可用的函数。
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#输出 :
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs
现在你也许要求当每次调用a_stand_alone_function时,实际调用却是a_stand_alone_function_decorated。实现也很简单,可以用my_shiny_new_decorator来给a_stand_alone_function重新赋值。
a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
#输出 :
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs
# And guess what, that's EXACTLY what decorators do !
装饰器揭秘
前面的例子,我们可以使用装饰器的语法:
@my_shiny_new_decorator
def another_stand_alone_function() :
print "Leave me alone"
another_stand_alone_function()
#输出 :
#Before the function runs
#Leave me alone
#After the function runs
当然你也可以累积装饰:
def bread(func) :
def wrapper() :
print "/''''''\"
func()
print "\______/"
return wrapper
def ingredients(func) :
def wrapper() :
print "#tomatoes#"
func()
print "~salad~"
return wrapper
def sandwich(food="--ham--") :
print food
sandwich()
#输出 : --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
#outputs :
#/''''''\
# #tomatoes#
# --ham--
# ~salad~
#\______/
使用python装饰器语法:
@bread
@ingredients
def sandwich(food="--ham--") :
print food
sandwich()
#输出 :
#/''''''\
# #tomatoes#
# --ham--
# ~salad~
#\______/