在Python中创建类的开销:使用类的代码比使用本机DS慢两倍?

2024-04-26 19:07:41 发布

您现在位置:Python中文网/ 问答频道 /正文

我用Python创建了一个Stack类作为练习,使用了所有的list函数。例如,Stack.push()只是list.append(),Stack.pop()是list.pop(),Stack.isEmpty()只是list=[]。

我用Stack类实现了一个十进制到二进制的转换器,我注意到,尽管这两个函数在push()、pop()和isEmpty()的Stack类包装之外完全等价,但是使用Stack类实现的速度是使用Python列表实现的两倍。

这是因为在Python中使用类总是有一个固有的开销吗?如果是这样的话,从技术上讲,头顶是从哪里来的(“引擎盖下”)?最后,如果开销这么大,除非您必须使用类,否则最好不要使用类吗?

def dectobin1(num):
    s = Stack()
    while num > 0:
        s.push(num % 2)
        num = num // 2
    binnum = ''
    while not s.isEmpty():
        binnum = binnum + str(s.pop())
    return binnum

def dectobin2(num):
    l = []
    while num > 0:
        l.append(num % 2)
        num = num // 2
    binnum = ''
    while not l == []:
        binnum = binnum + str(l.pop())
    return binnum


t1 = Timer('dectobin1(255)', 'from __main__ import dectobin1')
print(t1.timeit(number = 1000))

0.0211110115051

t2 = Timer('dectobin2(255)', 'from __main__ import dectobin2')
print(t2.timeit(number = 1000))

0.0094211101532

Tags: 函数stackdefnotpoppushnumlist
2条回答

使用函数存在固有的开销(其中实例上的方法只是要传入的函数周围的包装器)。

函数调用要求将当前函数信息(Aframe)存储在堆栈(Python调用堆栈)中,并为要调用的函数创建新的帧。这些都需要时间和记忆:

>>> from timeit import timeit
>>> def f(): pass
...
>>> timeit(f, number=10**7)
0.8021022859902587

查找属性(方法也是属性)和创建方法对象(方法名的每个属性查找都会创建一个新的方法对象)的成本也较小:

>>> class Foo:
...     bar = None
...     def baz(self): pass
...
>>> timeit('instance.bar', 'from __main__ import Foo; instance = Foo()', number=10**7)
0.238075322995428
>>> timeit('instance.baz', 'from __main__ import Foo; instance = Foo()', number=10**7)
0.3402297169959638

因此,属性查找、方法对象创建和调用堆栈操作的总成本加起来就是您观察到的额外时间需求。

首先,有一个警告:函数调用很少会限制您的速度。这通常是不必要的微观优化。只有这样做,如果它是真正限制你的表现。在之前做一些好的分析,看看是否有更好的方法来优化。

确保您不会为这个小小的性能调整牺牲易读性!

Python中的类有点难理解。

它的工作方式是每个对象都有一个__dict__字段(dict),其中包含该对象包含的所有属性。此外,每个对象都有一个__class__对象,该对象再次包含一个__dict__字段(同样是dict),该字段包含所有类属性。

例如,看看这个:

>>> class X(): # I know this is an old-style class declaration, but this causes far less clutter for this demonstration
...     def y(self):
...             pass
...
>>> x = X()
>>> x.__class__.__dict__
{'y': <function y at 0x6ffffe29938>, '__module__': '__main__', '__doc__': None}

如果动态定义函数(因此不是在类声明中而是在对象创建之后),则函数不会转到x.__class__.__dict__,而是转到x.__dict__

还有两个dict保存了当前函数中可访问的所有变量。这里有globals()locals(),其中包括所有全局和局部变量。

现在我们假设有一个类x的对象X,它的函数yz是在类声明中声明的,第二个函数z是动态定义的。假设对象x是在全局空间中定义的。 此外,为了进行比较,还有两个函数flocal(),在局部空间中定义,和fglobal(),在全局空间中定义。

现在我将展示如果您调用这些函数中的每一个:

flocal():
    locals()["flocal"]()

fglobal():
    locals()["fglobal"] -> not found
    globals()["fglobal"]()

x.y():
    locals()["x"] -> not found
    globals()["x"].__dict__["y"] -> not found, because y is in class space
                  .__class__.__dict__["y"]()

x.z():
    locals()["x"] -> not found
    globals()["x"].__dict__["z"]() -> found in object dict, ignoring z() in class space

如您所见,类空间方法需要更多的时间来查找,对象空间方法也很慢。最快的选择是本地函数。

但你可以在不牺牲课程的情况下绕过它。比如说,x.y()被频繁调用,需要优化。

class X():
    def y(self):
        pass

x = X()
for i in range(100000):
    x.y() # slow

y = x.y # move the function lookup outside of loop
for i in range(100000):
    y() # faster

类似的事情发生在对象的成员变量上。它们也比局部变量慢。如果调用函数或使用对象中的成员变量(该对象是其他对象的成员变量)时,效果也会增加。例如

a.b.c.d.e.f()

会比较慢,因为每个点都需要另一个字典查找。

正式的Python性能指南建议避免代码的性能关键部分出现点: https://wiki.python.org/moin/PythonSpeed/PerformanceTips

相关问题 更多 >