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

在Python中创建类的开销:完全相同的代码使用比本机DS慢两倍的类?,python,performance,function,class,Python,Performance,Function,Class,我使用所有列表函数在Python中创建了一个堆栈类作为练习。例如,Stack.push()就是list.append(),Stack.pop()就是list.pop(),Stack.isEmpty()就是list=[] 我用Stack类实现了一个十进制到二进制的转换器,我注意到,尽管这两个函数在push()、pop()和isEmpty()的堆栈类包装之外是完全等效的,但使用Stack类的实现速度是使用Python列表的实现速度的两倍 这是因为在Python中使用类总是存在固有的开销吗?如果是这样

我使用所有列表函数在Python中创建了一个堆栈类作为练习。例如,Stack.push()就是list.append(),Stack.pop()就是list.pop(),Stack.isEmpty()就是list=[]

我用Stack类实现了一个十进制到二进制的转换器,我注意到,尽管这两个函数在push()、pop()和isEmpty()的堆栈类包装之外是完全等效的,但使用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

使用函数会带来固有的开销(实例上的方法只是围绕函数的包装,以便传入
self

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

>>> from timeit import timeit
>>> def f(): pass
...
>>> timeit(f, number=10**7)
0.8021022859902587
查找属性(方法也是属性)和创建方法对象(方法名称的每个属性查找都会导致创建一个新的方法对象)的成本(更小):


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

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

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

Python中的类有点像黑客

它的工作方式是,每个对象都有一个
\uuuu dict\uuu
字段(dict),该字段包含对象包含的所有属性。此外,每个对象都有一个
\uuuuu class\uuuuuu
对象,该对象再次包含一个
\uuuu dict\uuuuuu
字段(再次是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.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性能指南建议避免代码中性能关键部分出现点:

谢谢你的回答,我不知道方法是属性的子集(我想你是这么说的)。对于学习Python中函数行为的系统方面以及类似的方面,您有什么建议吗?不幸的是,我现在没有时间看一本完整的系统编程书,但我真的想至少开始了解内存分配、函数帧、python调用堆栈等方面的情况。@jeremyradcliff:函数对象是,所以阅读这些可能是一个好的开始(
property
对象和
classmethod
staticmethod
对象使用相同的协议)。@jeremyradcliff:除此之外,先从Python字节码分解(the),然后查看,看看每个字节码的实际情况。这很好,再次感谢。我已经很长时间没有这么激动地钻研某些东西了。非常感谢您花时间编写此解释,它为我澄清了很多事情。您给出的上一个优化示例在函数带参数时是否有效?对于ex,类的函数有两个参数,您是否可以像y=x.y()那样将函数调用传递给变量?我尝试过这样做,但我收到一条错误消息,要求我传入参数。很抱歉,我在那里输入了一个错误。函数rebind应该是
y=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()