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

问题描述 投票:0回答:2

我使用所有列表函数创建了一个

Stack
类作为 Python 练习。例如,
Stack.push()
就是
list.append()
Stack.pop()
就是
list.pop()
Stack.isEmpty()
就是
list == [ ]

我正在使用我的

Stack
类来实现十进制到二进制转换器,我注意到,尽管这两个函数完全等效,超出了我的 Stack 类对
push()
pop()
isEmpty()
的包装,使用
Stack
类的实现比使用 Python
list
的实现慢两倍。

这是因为在 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
python performance function class
2个回答
50
投票

首先,警告:函数调用很少会限制您的速度。这通常是不必要的微观优化。仅当这确实限制了您的表现时才这样做。之前做好一些分析,看看是否有更好的优化方法。

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

Python 中的类有点麻烦。

它的工作方式是每个对象都有一个

__dict__
字段(一个字典),其中包含该对象包含的所有属性。此外,每个对象都有一个
__class__
对象,它又包含一个
__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__

还有两个字典保存当前函数可访问的所有变量。有

globals()
locals()
,其中包括所有全局和局部变量。

现在假设您有一个类

x
的对象
X
,其中包含在类声明中声明的函数
y
z
,以及动态定义的第二个函数
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


6
投票

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

self
)。

函数调用需要将当前函数信息(frame)存储在堆栈(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

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

© www.soinside.com 2019 - 2024. All rights reserved.